@vizij/render 0.1.0 → 0.1.1

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.d.mts CHANGED
@@ -357,12 +357,40 @@ interface VizijBundleAnimationEntry {
357
357
  [key: string]: unknown;
358
358
  };
359
359
  }
360
+ interface VizijSpeechConfig {
361
+ /** TTS voice name (e.g., "Ruth") */
362
+ voice?: string;
363
+ /** Speech mode */
364
+ mode?: "echo" | "conversation";
365
+ /** Agent name for LLM system prompt */
366
+ agentName?: string;
367
+ /** Custom system prompt (supports {{agent_name}} template) */
368
+ systemPrompt?: string;
369
+ /** Input path for avatar-speaking state (default: /speech/speaking) */
370
+ speakingInputPath?: string;
371
+ /** Input path for user-speaking state (default: /speech/user_speaking) */
372
+ userSpeakingInputPath?: string;
373
+ /** Input path for thinking state (default: /speech/thinking) */
374
+ thinkingInputPath?: string;
375
+ /** Pose group ID for viseme mapping */
376
+ visemeGroupId?: string;
377
+ /** Pose group ID for emotion mapping */
378
+ emotionGroupId?: string;
379
+ /** TTS API base URL */
380
+ apiBaseUrl?: string;
381
+ /** Auto-activate microphone when speech is ready (default: false) */
382
+ autoActivateMic?: boolean;
383
+ }
360
384
  interface VizijBundleExtension {
361
385
  version: VizijBundleVersion;
362
386
  exportedAt?: string;
363
387
  graphs?: VizijBundleGraphEntry[];
364
388
  poses?: VizijBundlePoseSection | null;
365
389
  animations?: VizijBundleAnimationEntry[];
390
+ /**
391
+ * Bundle-level metadata. May include `speechConfig: VizijSpeechConfig`
392
+ * for configuring the STT/LLM/TTS speech pipeline.
393
+ */
366
394
  metadata?: Record<string, unknown>;
367
395
  }
368
396
 
@@ -571,6 +599,12 @@ declare function useVizijStoreGetter(): () => VizijData & VizijActions;
571
599
  declare class EmptyModelError extends Error {
572
600
  constructor(message: string);
573
601
  }
602
+ type ParserJsonFallbackSource = {
603
+ url?: string;
604
+ blob?: Blob;
605
+ arrayBuffer?: ArrayBuffer;
606
+ };
607
+ declare function parseGlbJsonChunk(buffer: ArrayBuffer): unknown | undefined;
574
608
  declare function loadGLTF(url: string, namespaces: string[], aggressiveImport?: boolean, rootBounds?: {
575
609
  center: RawVector2;
576
610
  size: RawVector2;
@@ -584,11 +618,12 @@ type LoadedVizijAsset = {
584
618
  animatables: Record<string, AnimatableValue>;
585
619
  bundle: VizijBundleExtension | null;
586
620
  animations: VizijAnimationClipData[];
621
+ scene: Group$1;
587
622
  };
588
623
  declare function loadGLTFWithBundle(url: string, namespaces: string[], aggressiveImport?: boolean, rootBounds?: {
589
624
  center: RawVector2;
590
625
  size: RawVector2;
591
- }): Promise<LoadedVizijAsset>;
626
+ }, parserJsonFallback?: ParserJsonFallbackSource): Promise<LoadedVizijAsset>;
592
627
  declare function loadGLTFFromBlobWithBundle(blob: Blob, namespaces: string[], aggressiveImport?: boolean, rootBounds?: {
593
628
  center: RawVector2;
594
629
  size: RawVector2;
@@ -607,10 +642,12 @@ type ExportSceneOptions = {
607
642
  bundle?: VizijBundleExtension | null;
608
643
  animations?: AnimationClip[];
609
644
  binary?: boolean;
645
+ onError?: (error: Error) => void;
646
+ onComplete?: () => void;
610
647
  };
611
648
  declare function exportScene(data: Group$1, fileNameOrOptions?: string | ExportSceneOptions): void;
612
649
 
613
650
  declare function extractVizijBundle(object: Object3D, parserJson?: unknown): VizijBundleExtension | null;
614
651
  declare function applyVizijBundle(object: Object3D, bundle: VizijBundleExtension | null): () => void;
615
652
 
616
- export { type AnimatedFeature, type Ellipse, type EllipseFeature, EmptyModelError, type ExportSceneOptions, type Feature, type Group, type GroupFeature, InnerVizij, type InnerVizijProps, type LoadedVizijAsset, type Rectangle, type RectangleFeature, type RenderableBase, type RenderableFeature, type Selection, type Shape, type ShapeFeature, ShapeMaterial, type StaticFeature, type Stored, type StoredAnimatedFeature, type StoredEllipse, type StoredFeatures, type StoredGroup, type StoredRectangle, type StoredRenderable, type StoredShape, Vizij, type VizijActions, type VizijAnimationClipData, type VizijAnimationId, type VizijAnimationTrackData, type VizijBundleAnimationClip, type VizijBundleAnimationEntry, type VizijBundleAnimationKeyframe, type VizijBundleAnimationTrack, type VizijBundleExtension, type VizijBundleGraphEntry, type VizijBundleGraphKind, type VizijBundleGraphMetadata, type VizijBundlePoseSection, type VizijBundleVersion, VizijContext, type VizijData, type VizijGraphId, type VizijPoseDefinition, type VizijPoseId, type VizijPoseRigConfig, type VizijProps, VizijSlice, type VizijStore, type VizijStoreGetter, type VizijStoreSetter, type World, applyVizijBundle, createVizijStore, exportScene, extractVizijBundle, loadGLTF, loadGLTFFromBlob, loadGLTFFromBlobWithBundle, loadGLTFWithBundle, loadGltfFromBlob, useDefaultVizijStore, useFeatures, useVizijStore, useVizijStoreGetter, useVizijStoreSetter, useVizijStoreSubscription };
653
+ export { type AnimatedFeature, type Ellipse, type EllipseFeature, EmptyModelError, type ExportSceneOptions, type Feature, type Group, type GroupFeature, InnerVizij, type InnerVizijProps, type LoadedVizijAsset, type Rectangle, type RectangleFeature, type RenderableBase, type RenderableFeature, type Selection, type Shape, type ShapeFeature, ShapeMaterial, type StaticFeature, type Stored, type StoredAnimatedFeature, type StoredEllipse, type StoredFeatures, type StoredGroup, type StoredRectangle, type StoredRenderable, type StoredShape, Vizij, type VizijActions, type VizijAnimationClipData, type VizijAnimationId, type VizijAnimationTrackData, type VizijBundleAnimationClip, type VizijBundleAnimationEntry, type VizijBundleAnimationKeyframe, type VizijBundleAnimationTrack, type VizijBundleExtension, type VizijBundleGraphEntry, type VizijBundleGraphKind, type VizijBundleGraphMetadata, type VizijBundlePoseSection, type VizijBundleVersion, VizijContext, type VizijData, type VizijGraphId, type VizijPoseDefinition, type VizijPoseId, type VizijPoseRigConfig, type VizijProps, VizijSlice, type VizijSpeechConfig, type VizijStore, type VizijStoreGetter, type VizijStoreSetter, type World, applyVizijBundle, createVizijStore, exportScene, extractVizijBundle, loadGLTF, loadGLTFFromBlob, loadGLTFFromBlobWithBundle, loadGLTFWithBundle, loadGltfFromBlob, parseGlbJsonChunk, useDefaultVizijStore, useFeatures, useVizijStore, useVizijStoreGetter, useVizijStoreSetter, useVizijStoreSubscription };
package/dist/index.d.ts CHANGED
@@ -357,12 +357,40 @@ interface VizijBundleAnimationEntry {
357
357
  [key: string]: unknown;
358
358
  };
359
359
  }
360
+ interface VizijSpeechConfig {
361
+ /** TTS voice name (e.g., "Ruth") */
362
+ voice?: string;
363
+ /** Speech mode */
364
+ mode?: "echo" | "conversation";
365
+ /** Agent name for LLM system prompt */
366
+ agentName?: string;
367
+ /** Custom system prompt (supports {{agent_name}} template) */
368
+ systemPrompt?: string;
369
+ /** Input path for avatar-speaking state (default: /speech/speaking) */
370
+ speakingInputPath?: string;
371
+ /** Input path for user-speaking state (default: /speech/user_speaking) */
372
+ userSpeakingInputPath?: string;
373
+ /** Input path for thinking state (default: /speech/thinking) */
374
+ thinkingInputPath?: string;
375
+ /** Pose group ID for viseme mapping */
376
+ visemeGroupId?: string;
377
+ /** Pose group ID for emotion mapping */
378
+ emotionGroupId?: string;
379
+ /** TTS API base URL */
380
+ apiBaseUrl?: string;
381
+ /** Auto-activate microphone when speech is ready (default: false) */
382
+ autoActivateMic?: boolean;
383
+ }
360
384
  interface VizijBundleExtension {
361
385
  version: VizijBundleVersion;
362
386
  exportedAt?: string;
363
387
  graphs?: VizijBundleGraphEntry[];
364
388
  poses?: VizijBundlePoseSection | null;
365
389
  animations?: VizijBundleAnimationEntry[];
390
+ /**
391
+ * Bundle-level metadata. May include `speechConfig: VizijSpeechConfig`
392
+ * for configuring the STT/LLM/TTS speech pipeline.
393
+ */
366
394
  metadata?: Record<string, unknown>;
367
395
  }
368
396
 
@@ -571,6 +599,12 @@ declare function useVizijStoreGetter(): () => VizijData & VizijActions;
571
599
  declare class EmptyModelError extends Error {
572
600
  constructor(message: string);
573
601
  }
602
+ type ParserJsonFallbackSource = {
603
+ url?: string;
604
+ blob?: Blob;
605
+ arrayBuffer?: ArrayBuffer;
606
+ };
607
+ declare function parseGlbJsonChunk(buffer: ArrayBuffer): unknown | undefined;
574
608
  declare function loadGLTF(url: string, namespaces: string[], aggressiveImport?: boolean, rootBounds?: {
575
609
  center: RawVector2;
576
610
  size: RawVector2;
@@ -584,11 +618,12 @@ type LoadedVizijAsset = {
584
618
  animatables: Record<string, AnimatableValue>;
585
619
  bundle: VizijBundleExtension | null;
586
620
  animations: VizijAnimationClipData[];
621
+ scene: Group$1;
587
622
  };
588
623
  declare function loadGLTFWithBundle(url: string, namespaces: string[], aggressiveImport?: boolean, rootBounds?: {
589
624
  center: RawVector2;
590
625
  size: RawVector2;
591
- }): Promise<LoadedVizijAsset>;
626
+ }, parserJsonFallback?: ParserJsonFallbackSource): Promise<LoadedVizijAsset>;
592
627
  declare function loadGLTFFromBlobWithBundle(blob: Blob, namespaces: string[], aggressiveImport?: boolean, rootBounds?: {
593
628
  center: RawVector2;
594
629
  size: RawVector2;
@@ -607,10 +642,12 @@ type ExportSceneOptions = {
607
642
  bundle?: VizijBundleExtension | null;
608
643
  animations?: AnimationClip[];
609
644
  binary?: boolean;
645
+ onError?: (error: Error) => void;
646
+ onComplete?: () => void;
610
647
  };
611
648
  declare function exportScene(data: Group$1, fileNameOrOptions?: string | ExportSceneOptions): void;
612
649
 
613
650
  declare function extractVizijBundle(object: Object3D, parserJson?: unknown): VizijBundleExtension | null;
614
651
  declare function applyVizijBundle(object: Object3D, bundle: VizijBundleExtension | null): () => void;
615
652
 
616
- export { type AnimatedFeature, type Ellipse, type EllipseFeature, EmptyModelError, type ExportSceneOptions, type Feature, type Group, type GroupFeature, InnerVizij, type InnerVizijProps, type LoadedVizijAsset, type Rectangle, type RectangleFeature, type RenderableBase, type RenderableFeature, type Selection, type Shape, type ShapeFeature, ShapeMaterial, type StaticFeature, type Stored, type StoredAnimatedFeature, type StoredEllipse, type StoredFeatures, type StoredGroup, type StoredRectangle, type StoredRenderable, type StoredShape, Vizij, type VizijActions, type VizijAnimationClipData, type VizijAnimationId, type VizijAnimationTrackData, type VizijBundleAnimationClip, type VizijBundleAnimationEntry, type VizijBundleAnimationKeyframe, type VizijBundleAnimationTrack, type VizijBundleExtension, type VizijBundleGraphEntry, type VizijBundleGraphKind, type VizijBundleGraphMetadata, type VizijBundlePoseSection, type VizijBundleVersion, VizijContext, type VizijData, type VizijGraphId, type VizijPoseDefinition, type VizijPoseId, type VizijPoseRigConfig, type VizijProps, VizijSlice, type VizijStore, type VizijStoreGetter, type VizijStoreSetter, type World, applyVizijBundle, createVizijStore, exportScene, extractVizijBundle, loadGLTF, loadGLTFFromBlob, loadGLTFFromBlobWithBundle, loadGLTFWithBundle, loadGltfFromBlob, useDefaultVizijStore, useFeatures, useVizijStore, useVizijStoreGetter, useVizijStoreSetter, useVizijStoreSubscription };
653
+ export { type AnimatedFeature, type Ellipse, type EllipseFeature, EmptyModelError, type ExportSceneOptions, type Feature, type Group, type GroupFeature, InnerVizij, type InnerVizijProps, type LoadedVizijAsset, type Rectangle, type RectangleFeature, type RenderableBase, type RenderableFeature, type Selection, type Shape, type ShapeFeature, ShapeMaterial, type StaticFeature, type Stored, type StoredAnimatedFeature, type StoredEllipse, type StoredFeatures, type StoredGroup, type StoredRectangle, type StoredRenderable, type StoredShape, Vizij, type VizijActions, type VizijAnimationClipData, type VizijAnimationId, type VizijAnimationTrackData, type VizijBundleAnimationClip, type VizijBundleAnimationEntry, type VizijBundleAnimationKeyframe, type VizijBundleAnimationTrack, type VizijBundleExtension, type VizijBundleGraphEntry, type VizijBundleGraphKind, type VizijBundleGraphMetadata, type VizijBundlePoseSection, type VizijBundleVersion, VizijContext, type VizijData, type VizijGraphId, type VizijPoseDefinition, type VizijPoseId, type VizijPoseRigConfig, type VizijProps, VizijSlice, type VizijSpeechConfig, type VizijStore, type VizijStoreGetter, type VizijStoreSetter, type World, applyVizijBundle, createVizijStore, exportScene, extractVizijBundle, loadGLTF, loadGLTFFromBlob, loadGLTFFromBlobWithBundle, loadGLTFWithBundle, loadGltfFromBlob, parseGlbJsonChunk, useDefaultVizijStore, useFeatures, useVizijStore, useVizijStoreGetter, useVizijStoreSetter, useVizijStoreSubscription };
package/dist/index.js CHANGED
@@ -45,6 +45,7 @@ __export(index_exports, {
45
45
  loadGLTFFromBlobWithBundle: () => loadGLTFFromBlobWithBundle,
46
46
  loadGLTFWithBundle: () => loadGLTFWithBundle,
47
47
  loadGltfFromBlob: () => loadGltfFromBlob,
48
+ parseGlbJsonChunk: () => parseGlbJsonChunk,
48
49
  useDefaultVizijStore: () => useDefaultVizijStore,
49
50
  useFeatures: () => useFeatures,
50
51
  useVizijStore: () => useVizijStore,
@@ -1434,6 +1435,35 @@ function createAnimatable(value) {
1434
1435
  }
1435
1436
  createAnimatable({ type: "euler", name: "Rotation" });
1436
1437
 
1438
+ // src/functions/exportable-bodies.ts
1439
+ function asGroupEntries(world) {
1440
+ return Object.values(world).filter((entry) => entry.type === "group");
1441
+ }
1442
+ function selectExportableGroupEntries(world, filterIds) {
1443
+ const groupEntries = asGroupEntries(world);
1444
+ const filterSet = Array.isArray(filterIds) && filterIds.length > 0 ? new Set(filterIds) : null;
1445
+ if (filterSet) {
1446
+ return groupEntries.filter((entry) => filterSet.has(entry.id));
1447
+ }
1448
+ const rootBoundsGroups = groupEntries.filter(
1449
+ (entry) => Boolean(entry.rootBounds)
1450
+ );
1451
+ if (rootBoundsGroups.length > 0) {
1452
+ return rootBoundsGroups;
1453
+ }
1454
+ const explicitRootGroups = groupEntries.filter(
1455
+ (entry) => entry.root === true
1456
+ );
1457
+ if (explicitRootGroups.length > 0) {
1458
+ return explicitRootGroups;
1459
+ }
1460
+ const topLevelGroups = groupEntries.filter((entry) => !entry.parent);
1461
+ if (topLevelGroups.length > 0) {
1462
+ return topLevelGroups;
1463
+ }
1464
+ return groupEntries;
1465
+ }
1466
+
1437
1467
  // src/store.ts
1438
1468
  THREE3.Object3D.DEFAULT_UP.set(0, 0, 1);
1439
1469
  (0, import_immer.enableMapSet)();
@@ -1509,23 +1539,17 @@ var VizijSlice = (set, get) => ({
1509
1539
  },
1510
1540
  getExportableBodies: (filterIds) => {
1511
1541
  const worldData = get().world;
1512
- if (!filterIds) {
1513
- const bodies = Object.values(worldData).filter((entry) => entry.type === "group" && entry.rootBounds).map((entry) => {
1514
- const firstNs = Object.keys(entry.refs)[0];
1515
- const refGroup = entry.refs[firstNs].current;
1516
- return refGroup;
1517
- });
1518
- return bodies;
1519
- } else {
1520
- const bodies = Object.values(worldData).filter(
1521
- (entry) => entry.type === "group" && entry.rootBounds && filterIds.includes(entry.id)
1522
- ).map((entry) => {
1523
- const firstNs = Object.keys(entry.refs)[0];
1524
- const refGroup = entry.refs[firstNs].current;
1525
- return refGroup;
1526
- });
1527
- return bodies;
1528
- }
1542
+ const candidateGroups = selectExportableGroupEntries(
1543
+ worldData,
1544
+ filterIds
1545
+ );
1546
+ return candidateGroups.flatMap((entry) => {
1547
+ const refs = Object.values(
1548
+ entry.refs ?? {}
1549
+ );
1550
+ const resolved = refs.find((ref) => ref?.current)?.current ?? null;
1551
+ return resolved ? [resolved] : [];
1552
+ });
1529
1553
  },
1530
1554
  setGeometry: (id, geometry) => {
1531
1555
  set(
@@ -2304,6 +2328,15 @@ function importMesh(mesh, namespaces, colorLookup) {
2304
2328
  world = { ...world, ...newWorldItems };
2305
2329
  animatables = { ...animatables, ...newAnimatables };
2306
2330
  children.push(childId);
2331
+ } else if (shouldImportAsGroupChild(child)) {
2332
+ const [newWorldItems, newAnimatables, childId, newMeshColors] = importGroup(child, namespaces, {
2333
+ ...colorLookup,
2334
+ ...newColorLookup
2335
+ });
2336
+ newColorLookup = { ...newColorLookup, ...newMeshColors };
2337
+ world = { ...world, ...newWorldItems };
2338
+ animatables = { ...animatables, ...newAnimatables };
2339
+ children.push(childId);
2307
2340
  }
2308
2341
  });
2309
2342
  const newShape = {
@@ -2347,6 +2380,24 @@ function getShapeMaterial(mesh, useEmissive) {
2347
2380
  return "standard" /* Standard */;
2348
2381
  }
2349
2382
  }
2383
+ function shouldImportAsGroupChild(child) {
2384
+ if (!child.isObject3D) {
2385
+ return false;
2386
+ }
2387
+ if (child.isMesh) {
2388
+ return false;
2389
+ }
2390
+ if (child.isCamera) {
2391
+ return false;
2392
+ }
2393
+ if (child.isLight) {
2394
+ return false;
2395
+ }
2396
+ if (child.isBone) {
2397
+ return false;
2398
+ }
2399
+ return true;
2400
+ }
2350
2401
 
2351
2402
  // src/functions/gltf-loading/import-group.ts
2352
2403
  import_three4.Object3D.DEFAULT_UP.set(0, 0, 1);
@@ -2406,7 +2457,7 @@ function importGroup(group, namespaces, colorLookup, rootBounds) {
2406
2457
  world = { ...world, ...newWorldItems };
2407
2458
  animatables = { ...animatables, ...newAnimatables };
2408
2459
  children.push(childId);
2409
- } else if (child.isGroup || child.isObject3D && child.children.length !== 0) {
2460
+ } else if (shouldImportAsGroupChild2(child)) {
2410
2461
  const [newWorldItems, newAnimatables, childId, newMeshColors] = importGroup(child, namespaces, {
2411
2462
  ...colorLookup,
2412
2463
  ...newColorLookup
@@ -2435,6 +2486,24 @@ function importGroup(group, namespaces, colorLookup, rootBounds) {
2435
2486
  world = { ...world, [newGroup.id]: newGroup };
2436
2487
  return [world, animatables, newGroup.id, newColorLookup];
2437
2488
  }
2489
+ function shouldImportAsGroupChild2(child) {
2490
+ if (!child.isObject3D) {
2491
+ return false;
2492
+ }
2493
+ if (child.isMesh) {
2494
+ return false;
2495
+ }
2496
+ if (child.isCamera) {
2497
+ return false;
2498
+ }
2499
+ if (child.isLight) {
2500
+ return false;
2501
+ }
2502
+ if (child.isBone) {
2503
+ return false;
2504
+ }
2505
+ return true;
2506
+ }
2438
2507
 
2439
2508
  // src/functions/gltf-loading/import-scene.ts
2440
2509
  import_three5.Object3D.DEFAULT_UP.set(0, 0, 1);
@@ -2745,6 +2814,13 @@ function searchParserJsonForBundle(parserJson) {
2745
2814
  if (!parserJson || typeof parserJson !== "object") {
2746
2815
  return null;
2747
2816
  }
2817
+ const rootExtensions = parserJson && typeof parserJson === "object" ? parserJson.extensions : null;
2818
+ if (rootExtensions && typeof rootExtensions === "object") {
2819
+ const match = readExtensionValue(rootExtensions);
2820
+ if (match) {
2821
+ return cloneBundle(match.value);
2822
+ }
2823
+ }
2748
2824
  const nodes = Array.isArray(parserJson.nodes) ? parserJson.nodes : [];
2749
2825
  for (const node of nodes) {
2750
2826
  const extensions = node && typeof node === "object" ? node.extensions : null;
@@ -3180,23 +3256,94 @@ function extractVizijAnimations(parserJson, clips) {
3180
3256
 
3181
3257
  // src/functions/load-gltf.ts
3182
3258
  THREE5.Object3D.DEFAULT_UP.set(0, 0, 1);
3259
+ var GLB_MAGIC = 1179937895;
3260
+ var GLB_VERSION = 2;
3261
+ var GLB_HEADER_BYTES = 12;
3262
+ var GLB_CHUNK_HEADER_BYTES = 8;
3263
+ var GLB_JSON_CHUNK_TYPE = 1313821514;
3183
3264
  var EmptyModelError = class extends Error {
3184
3265
  constructor(message) {
3185
3266
  super(message);
3186
3267
  this.name = "EmptyModelError";
3187
3268
  }
3188
3269
  };
3270
+ function parseGlbJsonChunk(buffer) {
3271
+ if (buffer.byteLength < GLB_HEADER_BYTES + GLB_CHUNK_HEADER_BYTES) {
3272
+ return void 0;
3273
+ }
3274
+ const view = new DataView(buffer);
3275
+ const magic = view.getUint32(0, true);
3276
+ const version = view.getUint32(4, true);
3277
+ if (magic !== GLB_MAGIC || version !== GLB_VERSION) {
3278
+ return void 0;
3279
+ }
3280
+ const chunkLength = view.getUint32(GLB_HEADER_BYTES, true);
3281
+ const chunkType = view.getUint32(GLB_HEADER_BYTES + 4, true);
3282
+ if (chunkType !== GLB_JSON_CHUNK_TYPE) {
3283
+ return void 0;
3284
+ }
3285
+ const chunkStart = GLB_HEADER_BYTES + GLB_CHUNK_HEADER_BYTES;
3286
+ const chunkEnd = chunkStart + chunkLength;
3287
+ if (chunkEnd > buffer.byteLength) {
3288
+ return void 0;
3289
+ }
3290
+ try {
3291
+ const chunkBytes = new Uint8Array(buffer, chunkStart, chunkLength);
3292
+ const jsonText = new TextDecoder().decode(chunkBytes);
3293
+ return JSON.parse(jsonText);
3294
+ } catch {
3295
+ return void 0;
3296
+ }
3297
+ }
3298
+ async function resolveParserJson(parserJson, fallback) {
3299
+ if (parserJson && typeof parserJson === "object") {
3300
+ return parserJson;
3301
+ }
3302
+ if (fallback.arrayBuffer) {
3303
+ const fromArrayBuffer = parseGlbJsonChunk(fallback.arrayBuffer);
3304
+ if (fromArrayBuffer && typeof fromArrayBuffer === "object") {
3305
+ return fromArrayBuffer;
3306
+ }
3307
+ }
3308
+ if (fallback.blob) {
3309
+ try {
3310
+ const blobBuffer = typeof fallback.blob.arrayBuffer === "function" ? await fallback.blob.arrayBuffer() : await new Response(fallback.blob).arrayBuffer();
3311
+ const fromBlob = parseGlbJsonChunk(blobBuffer);
3312
+ if (fromBlob && typeof fromBlob === "object") {
3313
+ return fromBlob;
3314
+ }
3315
+ } catch {
3316
+ }
3317
+ }
3318
+ if (fallback.url && typeof fetch === "function") {
3319
+ try {
3320
+ const response = await fetch(fallback.url);
3321
+ if (response.ok) {
3322
+ const binary = await response.arrayBuffer();
3323
+ const fromUrl = parseGlbJsonChunk(binary);
3324
+ if (fromUrl && typeof fromUrl === "object") {
3325
+ return fromUrl;
3326
+ }
3327
+ }
3328
+ } catch {
3329
+ }
3330
+ }
3331
+ return parserJson;
3332
+ }
3189
3333
  async function loadGLTF(url, namespaces, aggressiveImport = false, rootBounds) {
3190
3334
  const modelLoader = new import_three_stdlib.GLTFLoader();
3191
3335
  modelLoader.setDRACOLoader(new import_three_stdlib.DRACOLoader());
3192
3336
  const modelData = await modelLoader.loadAsync(url);
3337
+ const parserJson = await resolveParserJson(modelData?.parser?.json, {
3338
+ url
3339
+ });
3193
3340
  const actualizedNamespaces = namespaces.length > 0 ? namespaces : ["default"];
3194
3341
  const asset = parseScene(
3195
3342
  modelData.scene,
3196
3343
  actualizedNamespaces,
3197
3344
  aggressiveImport,
3198
3345
  rootBounds,
3199
- modelData?.parser?.json,
3346
+ parserJson,
3200
3347
  modelData.animations
3201
3348
  );
3202
3349
  return [asset.world, asset.animatables, asset.animations];
@@ -3210,7 +3357,8 @@ async function loadGLTFFromBlob(blob, namespaces, aggressiveImport = false, root
3210
3357
  objectUrl,
3211
3358
  actualizedNamespaces,
3212
3359
  aggressiveImport,
3213
- rootBounds
3360
+ rootBounds,
3361
+ { blob }
3214
3362
  );
3215
3363
  return [asset.world, asset.animatables, asset.animations];
3216
3364
  } finally {
@@ -3224,14 +3372,18 @@ async function loadGLTFFromBlob(blob, namespaces, aggressiveImport = false, root
3224
3372
  loader.parse(
3225
3373
  arrayBuffer,
3226
3374
  "",
3227
- (gltf) => {
3375
+ async (gltf) => {
3228
3376
  try {
3377
+ const parserJson = await resolveParserJson(
3378
+ gltf?.parser?.json,
3379
+ { arrayBuffer }
3380
+ );
3229
3381
  const asset = parseScene(
3230
3382
  gltf.scene,
3231
3383
  actualizedNamespaces,
3232
3384
  aggressiveImport,
3233
3385
  rootBounds,
3234
- gltf?.parser?.json,
3386
+ parserJson,
3235
3387
  gltf.animations
3236
3388
  );
3237
3389
  resolve([asset.world, asset.animatables, asset.animations]);
@@ -3258,19 +3410,23 @@ function parseScene(scene, namespaces, aggressiveImport, rootBounds, parserJson,
3258
3410
  );
3259
3411
  const bundle = extractVizijBundle(scene, parserJson);
3260
3412
  const animations = extractVizijAnimations(parserJson, clips);
3261
- return { world, animatables, bundle, animations };
3413
+ return { world, animatables, bundle, animations, scene };
3262
3414
  }
3263
- async function loadGLTFWithBundle(url, namespaces, aggressiveImport = false, rootBounds) {
3415
+ async function loadGLTFWithBundle(url, namespaces, aggressiveImport = false, rootBounds, parserJsonFallback) {
3264
3416
  const modelLoader = new import_three_stdlib.GLTFLoader();
3265
3417
  modelLoader.setDRACOLoader(new import_three_stdlib.DRACOLoader());
3266
3418
  const modelData = await modelLoader.loadAsync(url);
3419
+ const parserJson = await resolveParserJson(modelData?.parser?.json, {
3420
+ url,
3421
+ ...parserJsonFallback
3422
+ });
3267
3423
  const actualizedNamespaces = namespaces.length > 0 ? namespaces : ["default"];
3268
3424
  return parseScene(
3269
3425
  modelData.scene,
3270
3426
  actualizedNamespaces,
3271
3427
  aggressiveImport,
3272
3428
  rootBounds,
3273
- modelData?.parser?.json,
3429
+ parserJson,
3274
3430
  modelData.animations
3275
3431
  );
3276
3432
  }
@@ -3283,7 +3439,8 @@ async function loadGLTFFromBlobWithBundle(blob, namespaces, aggressiveImport = f
3283
3439
  objectUrl,
3284
3440
  actualizedNamespaces,
3285
3441
  aggressiveImport,
3286
- rootBounds
3442
+ rootBounds,
3443
+ { blob }
3287
3444
  );
3288
3445
  } finally {
3289
3446
  URL.revokeObjectURL(objectUrl);
@@ -3296,14 +3453,18 @@ async function loadGLTFFromBlobWithBundle(blob, namespaces, aggressiveImport = f
3296
3453
  loader.parse(
3297
3454
  arrayBuffer,
3298
3455
  "",
3299
- (gltf) => {
3456
+ async (gltf) => {
3300
3457
  try {
3458
+ const parserJson = await resolveParserJson(
3459
+ gltf?.parser?.json,
3460
+ { arrayBuffer }
3461
+ );
3301
3462
  const asset = parseScene(
3302
3463
  gltf.scene,
3303
3464
  actualizedNamespaces,
3304
3465
  aggressiveImport,
3305
3466
  rootBounds,
3306
- gltf?.parser?.json,
3467
+ parserJson,
3307
3468
  gltf.animations
3308
3469
  );
3309
3470
  resolve(asset);
@@ -3355,6 +3516,160 @@ var loadGltfFromBlob = (blob, namespaces) => {
3355
3516
  var import_three_stdlib3 = require("three-stdlib");
3356
3517
  var THREE6 = __toESM(require("three"));
3357
3518
  THREE6.Object3D.DEFAULT_UP.set(0, 0, 1);
3519
+ function normalizeExportError(error) {
3520
+ if (error instanceof Error) {
3521
+ return error;
3522
+ }
3523
+ if (typeof error === "string") {
3524
+ return new Error(error);
3525
+ }
3526
+ return new Error("Failed to export scene.");
3527
+ }
3528
+ function triggerBlobDownload(blob, filename) {
3529
+ const url = URL.createObjectURL(blob);
3530
+ const link = document.createElement("a");
3531
+ link.href = url;
3532
+ link.download = filename;
3533
+ link.style.display = "none";
3534
+ document.body.appendChild(link);
3535
+ link.click();
3536
+ document.body.removeChild(link);
3537
+ setTimeout(() => URL.revokeObjectURL(url), 0);
3538
+ }
3539
+ var GLB_MAGIC2 = 1179937895;
3540
+ var GLB_VERSION2 = 2;
3541
+ var GLB_JSON_CHUNK_TYPE2 = 1313821514;
3542
+ var GLB_HEADER_BYTES2 = 12;
3543
+ var GLB_CHUNK_HEADER_BYTES2 = 8;
3544
+ var GLB_JSON_PADDING_BYTE = 32;
3545
+ function isNearlyEqual(a, b, epsilon = 1e-6) {
3546
+ return Math.abs(a - b) <= epsilon;
3547
+ }
3548
+ function isIdentityTransformNode(node) {
3549
+ const translation = node.translation;
3550
+ if (Array.isArray(translation) && (translation.length !== 3 || !isNearlyEqual(Number(translation[0]), 0) || !isNearlyEqual(Number(translation[1]), 0) || !isNearlyEqual(Number(translation[2]), 0))) {
3551
+ return false;
3552
+ }
3553
+ const rotation = node.rotation;
3554
+ 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))) {
3555
+ return false;
3556
+ }
3557
+ const scale = node.scale;
3558
+ if (Array.isArray(scale) && (scale.length !== 3 || !isNearlyEqual(Number(scale[0]), 1) || !isNearlyEqual(Number(scale[1]), 1) || !isNearlyEqual(Number(scale[2]), 1))) {
3559
+ return false;
3560
+ }
3561
+ return true;
3562
+ }
3563
+ function isPassThroughWrapperNode(node) {
3564
+ if (!node || typeof node !== "object") {
3565
+ return false;
3566
+ }
3567
+ if (!Array.isArray(node.children) || node.children.length === 0) {
3568
+ return false;
3569
+ }
3570
+ const hasOnlyNumericChildren = node.children.every(
3571
+ (index) => Number.isInteger(index)
3572
+ );
3573
+ if (!hasOnlyNumericChildren) {
3574
+ return false;
3575
+ }
3576
+ if (node.mesh !== void 0 || node.camera !== void 0 || node.skin !== void 0) {
3577
+ return false;
3578
+ }
3579
+ if (node.extensions && typeof node.extensions === "object" && Object.keys(node.extensions).length > 0) {
3580
+ return false;
3581
+ }
3582
+ return isIdentityTransformNode(node);
3583
+ }
3584
+ function normalizeExportedSceneJson(json, fallbackSceneName) {
3585
+ if (!json || typeof json !== "object") {
3586
+ return false;
3587
+ }
3588
+ if (!Array.isArray(json.scenes) || !Array.isArray(json.nodes)) {
3589
+ return false;
3590
+ }
3591
+ const sceneIndexRaw = json.scene;
3592
+ const sceneIndex = typeof sceneIndexRaw === "number" && Number.isInteger(sceneIndexRaw) ? sceneIndexRaw : 0;
3593
+ const sceneDef = json.scenes[sceneIndex];
3594
+ if (!sceneDef || typeof sceneDef !== "object") {
3595
+ return false;
3596
+ }
3597
+ let changed = false;
3598
+ let wrapperNodeName;
3599
+ if (Array.isArray(sceneDef.nodes) && sceneDef.nodes.length === 1) {
3600
+ const wrapperIndex = sceneDef.nodes[0];
3601
+ if (Number.isInteger(wrapperIndex)) {
3602
+ const wrapperNode = json.nodes[wrapperIndex];
3603
+ if (isPassThroughWrapperNode(wrapperNode)) {
3604
+ sceneDef.nodes = [...wrapperNode.children];
3605
+ wrapperNodeName = typeof wrapperNode.name === "string" ? wrapperNode.name : void 0;
3606
+ changed = true;
3607
+ }
3608
+ }
3609
+ }
3610
+ const currentSceneName = typeof sceneDef.name === "string" ? sceneDef.name.trim() : "";
3611
+ if (currentSceneName === "AuxScene") {
3612
+ const nextSceneName = (wrapperNodeName?.trim() || fallbackSceneName?.trim() || "Scene").trim();
3613
+ if (nextSceneName.length > 0 && nextSceneName !== currentSceneName) {
3614
+ sceneDef.name = nextSceneName;
3615
+ changed = true;
3616
+ }
3617
+ }
3618
+ return changed;
3619
+ }
3620
+ function sanitizeExportedGlb(buffer, fallbackSceneName) {
3621
+ if (buffer.byteLength < GLB_HEADER_BYTES2 + GLB_CHUNK_HEADER_BYTES2) {
3622
+ return buffer;
3623
+ }
3624
+ const originalBytes = new Uint8Array(buffer);
3625
+ const view = new DataView(buffer);
3626
+ const magic = view.getUint32(0, true);
3627
+ const version = view.getUint32(4, true);
3628
+ if (magic !== GLB_MAGIC2 || version !== GLB_VERSION2) {
3629
+ return buffer;
3630
+ }
3631
+ const jsonChunkLength = view.getUint32(GLB_HEADER_BYTES2, true);
3632
+ const jsonChunkType = view.getUint32(GLB_HEADER_BYTES2 + 4, true);
3633
+ if (jsonChunkType !== GLB_JSON_CHUNK_TYPE2) {
3634
+ return buffer;
3635
+ }
3636
+ const jsonChunkStart = GLB_HEADER_BYTES2 + GLB_CHUNK_HEADER_BYTES2;
3637
+ const jsonChunkEnd = jsonChunkStart + jsonChunkLength;
3638
+ if (jsonChunkEnd > originalBytes.length) {
3639
+ return buffer;
3640
+ }
3641
+ let jsonPayload;
3642
+ try {
3643
+ const jsonText = new TextDecoder().decode(
3644
+ originalBytes.slice(jsonChunkStart, jsonChunkEnd)
3645
+ );
3646
+ jsonPayload = JSON.parse(jsonText);
3647
+ } catch {
3648
+ return buffer;
3649
+ }
3650
+ const changed = normalizeExportedSceneJson(jsonPayload, fallbackSceneName);
3651
+ if (!changed) {
3652
+ return buffer;
3653
+ }
3654
+ const encodedJson = new TextEncoder().encode(JSON.stringify(jsonPayload));
3655
+ const paddedJsonLength = encodedJson.length + 3 & ~3;
3656
+ const paddedJson = new Uint8Array(paddedJsonLength);
3657
+ paddedJson.fill(GLB_JSON_PADDING_BYTE);
3658
+ paddedJson.set(encodedJson);
3659
+ const remainingChunks = originalBytes.slice(jsonChunkEnd);
3660
+ const totalLength = GLB_HEADER_BYTES2 + GLB_CHUNK_HEADER_BYTES2 + paddedJsonLength + remainingChunks.length;
3661
+ const sanitized = new ArrayBuffer(totalLength);
3662
+ const sanitizedBytes = new Uint8Array(sanitized);
3663
+ const sanitizedView = new DataView(sanitized);
3664
+ sanitizedView.setUint32(0, GLB_MAGIC2, true);
3665
+ sanitizedView.setUint32(4, GLB_VERSION2, true);
3666
+ sanitizedView.setUint32(8, totalLength, true);
3667
+ sanitizedView.setUint32(GLB_HEADER_BYTES2, paddedJsonLength, true);
3668
+ sanitizedView.setUint32(GLB_HEADER_BYTES2 + 4, GLB_JSON_CHUNK_TYPE2, true);
3669
+ sanitizedBytes.set(paddedJson, jsonChunkStart);
3670
+ sanitizedBytes.set(remainingChunks, jsonChunkStart + paddedJsonLength);
3671
+ return sanitized;
3672
+ }
3358
3673
  function exportScene(data, fileNameOrOptions = "scene.glb") {
3359
3674
  const options = typeof fileNameOrOptions === "string" ? { fileName: fileNameOrOptions } : fileNameOrOptions ?? {};
3360
3675
  const fileName = options.fileName ?? "scene.glb";
@@ -3369,7 +3684,15 @@ function exportScene(data, fileNameOrOptions = "scene.glb") {
3369
3684
  }
3370
3685
  }
3371
3686
  }));
3372
- const detachBundle = shouldAttachBundle && options.bundle ? applyVizijBundle(data, options.bundle) : () => {
3687
+ const sourceRoot = data;
3688
+ const exportRoot = sourceRoot instanceof THREE6.Scene ? sourceRoot : sourceRoot.clone(true);
3689
+ const exportTarget = exportRoot instanceof THREE6.Scene ? exportRoot : (() => {
3690
+ const scene = new THREE6.Scene();
3691
+ scene.name = data.name?.trim() || "Scene";
3692
+ scene.add(exportRoot);
3693
+ return scene;
3694
+ })();
3695
+ const detachBundle = shouldAttachBundle && options.bundle ? applyVizijBundle(exportRoot, options.bundle) : () => {
3373
3696
  };
3374
3697
  const binary = options.binary ?? true;
3375
3698
  const exporterOptions = {
@@ -3383,33 +3706,40 @@ function exportScene(data, fileNameOrOptions = "scene.glb") {
3383
3706
  }
3384
3707
  try {
3385
3708
  exporter.parse(
3386
- data,
3709
+ exportTarget,
3387
3710
  (gltf) => {
3388
3711
  detachBundle();
3389
3712
  if (!(gltf instanceof ArrayBuffer)) {
3390
- throw new Error("Failed to export scene!");
3713
+ const error = new Error("Failed to export scene.");
3714
+ options.onError?.(error);
3715
+ return;
3391
3716
  }
3392
- const link = document.createElement("a");
3393
- link.href = URL.createObjectURL(
3394
- new Blob([gltf], {
3395
- type: "application/octet-stream"
3396
- })
3717
+ const sanitizedGltf = sanitizeExportedGlb(
3718
+ gltf,
3719
+ data.name?.trim() || void 0
3397
3720
  );
3398
3721
  const trimmed = fileName.trim();
3399
3722
  const safeFileName = trimmed.length > 0 ? trimmed : "scene.glb";
3400
3723
  const downloadName = safeFileName.toLowerCase().endsWith(".glb") ? safeFileName : `${safeFileName}.glb`;
3401
- link.download = downloadName;
3402
- link.click();
3403
- URL.revokeObjectURL(link.href);
3724
+ triggerBlobDownload(
3725
+ new Blob([sanitizedGltf], {
3726
+ type: "application/octet-stream"
3727
+ }),
3728
+ downloadName
3729
+ );
3730
+ options.onComplete?.();
3404
3731
  },
3405
- () => {
3732
+ (error) => {
3406
3733
  detachBundle();
3734
+ options.onError?.(normalizeExportError(error));
3407
3735
  },
3408
3736
  exporterOptions
3409
3737
  );
3410
3738
  } catch (error) {
3411
3739
  detachBundle();
3412
- throw error;
3740
+ const normalizedError = normalizeExportError(error);
3741
+ options.onError?.(normalizedError);
3742
+ throw normalizedError;
3413
3743
  }
3414
3744
  }
3415
3745
  // Annotate the CommonJS export names for ESM import in node:
@@ -3429,6 +3759,7 @@ function exportScene(data, fileNameOrOptions = "scene.glb") {
3429
3759
  loadGLTFFromBlobWithBundle,
3430
3760
  loadGLTFWithBundle,
3431
3761
  loadGltfFromBlob,
3762
+ parseGlbJsonChunk,
3432
3763
  useDefaultVizijStore,
3433
3764
  useFeatures,
3434
3765
  useVizijStore,
package/dist/index.mjs CHANGED
@@ -1403,6 +1403,35 @@ function createAnimatable(value) {
1403
1403
  }
1404
1404
  createAnimatable({ type: "euler", name: "Rotation" });
1405
1405
 
1406
+ // src/functions/exportable-bodies.ts
1407
+ function asGroupEntries(world) {
1408
+ return Object.values(world).filter((entry) => entry.type === "group");
1409
+ }
1410
+ function selectExportableGroupEntries(world, filterIds) {
1411
+ const groupEntries = asGroupEntries(world);
1412
+ const filterSet = Array.isArray(filterIds) && filterIds.length > 0 ? new Set(filterIds) : null;
1413
+ if (filterSet) {
1414
+ return groupEntries.filter((entry) => filterSet.has(entry.id));
1415
+ }
1416
+ const rootBoundsGroups = groupEntries.filter(
1417
+ (entry) => Boolean(entry.rootBounds)
1418
+ );
1419
+ if (rootBoundsGroups.length > 0) {
1420
+ return rootBoundsGroups;
1421
+ }
1422
+ const explicitRootGroups = groupEntries.filter(
1423
+ (entry) => entry.root === true
1424
+ );
1425
+ if (explicitRootGroups.length > 0) {
1426
+ return explicitRootGroups;
1427
+ }
1428
+ const topLevelGroups = groupEntries.filter((entry) => !entry.parent);
1429
+ if (topLevelGroups.length > 0) {
1430
+ return topLevelGroups;
1431
+ }
1432
+ return groupEntries;
1433
+ }
1434
+
1406
1435
  // src/store.ts
1407
1436
  THREE3.Object3D.DEFAULT_UP.set(0, 0, 1);
1408
1437
  enableMapSet();
@@ -1478,23 +1507,17 @@ var VizijSlice = (set, get) => ({
1478
1507
  },
1479
1508
  getExportableBodies: (filterIds) => {
1480
1509
  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
- }
1510
+ const candidateGroups = selectExportableGroupEntries(
1511
+ worldData,
1512
+ filterIds
1513
+ );
1514
+ 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] : [];
1520
+ });
1498
1521
  },
1499
1522
  setGeometry: (id, geometry) => {
1500
1523
  set(
@@ -2281,6 +2304,15 @@ function importMesh(mesh, namespaces, colorLookup) {
2281
2304
  world = { ...world, ...newWorldItems };
2282
2305
  animatables = { ...animatables, ...newAnimatables };
2283
2306
  children.push(childId);
2307
+ } else if (shouldImportAsGroupChild(child)) {
2308
+ const [newWorldItems, newAnimatables, childId, newMeshColors] = importGroup(child, namespaces, {
2309
+ ...colorLookup,
2310
+ ...newColorLookup
2311
+ });
2312
+ newColorLookup = { ...newColorLookup, ...newMeshColors };
2313
+ world = { ...world, ...newWorldItems };
2314
+ animatables = { ...animatables, ...newAnimatables };
2315
+ children.push(childId);
2284
2316
  }
2285
2317
  });
2286
2318
  const newShape = {
@@ -2324,6 +2356,24 @@ function getShapeMaterial(mesh, useEmissive) {
2324
2356
  return "standard" /* Standard */;
2325
2357
  }
2326
2358
  }
2359
+ function shouldImportAsGroupChild(child) {
2360
+ if (!child.isObject3D) {
2361
+ return false;
2362
+ }
2363
+ if (child.isMesh) {
2364
+ return false;
2365
+ }
2366
+ if (child.isCamera) {
2367
+ return false;
2368
+ }
2369
+ if (child.isLight) {
2370
+ return false;
2371
+ }
2372
+ if (child.isBone) {
2373
+ return false;
2374
+ }
2375
+ return true;
2376
+ }
2327
2377
 
2328
2378
  // src/functions/gltf-loading/import-group.ts
2329
2379
  Object3D6.DEFAULT_UP.set(0, 0, 1);
@@ -2383,7 +2433,7 @@ function importGroup(group, namespaces, colorLookup, rootBounds) {
2383
2433
  world = { ...world, ...newWorldItems };
2384
2434
  animatables = { ...animatables, ...newAnimatables };
2385
2435
  children.push(childId);
2386
- } else if (child.isGroup || child.isObject3D && child.children.length !== 0) {
2436
+ } else if (shouldImportAsGroupChild2(child)) {
2387
2437
  const [newWorldItems, newAnimatables, childId, newMeshColors] = importGroup(child, namespaces, {
2388
2438
  ...colorLookup,
2389
2439
  ...newColorLookup
@@ -2412,6 +2462,24 @@ function importGroup(group, namespaces, colorLookup, rootBounds) {
2412
2462
  world = { ...world, [newGroup.id]: newGroup };
2413
2463
  return [world, animatables, newGroup.id, newColorLookup];
2414
2464
  }
2465
+ function shouldImportAsGroupChild2(child) {
2466
+ if (!child.isObject3D) {
2467
+ return false;
2468
+ }
2469
+ if (child.isMesh) {
2470
+ return false;
2471
+ }
2472
+ if (child.isCamera) {
2473
+ return false;
2474
+ }
2475
+ if (child.isLight) {
2476
+ return false;
2477
+ }
2478
+ if (child.isBone) {
2479
+ return false;
2480
+ }
2481
+ return true;
2482
+ }
2415
2483
 
2416
2484
  // src/functions/gltf-loading/import-scene.ts
2417
2485
  Object3D7.DEFAULT_UP.set(0, 0, 1);
@@ -2722,6 +2790,13 @@ function searchParserJsonForBundle(parserJson) {
2722
2790
  if (!parserJson || typeof parserJson !== "object") {
2723
2791
  return null;
2724
2792
  }
2793
+ const rootExtensions = parserJson && typeof parserJson === "object" ? parserJson.extensions : null;
2794
+ if (rootExtensions && typeof rootExtensions === "object") {
2795
+ const match = readExtensionValue(rootExtensions);
2796
+ if (match) {
2797
+ return cloneBundle(match.value);
2798
+ }
2799
+ }
2725
2800
  const nodes = Array.isArray(parserJson.nodes) ? parserJson.nodes : [];
2726
2801
  for (const node of nodes) {
2727
2802
  const extensions = node && typeof node === "object" ? node.extensions : null;
@@ -3157,23 +3232,94 @@ function extractVizijAnimations(parserJson, clips) {
3157
3232
 
3158
3233
  // src/functions/load-gltf.ts
3159
3234
  THREE5.Object3D.DEFAULT_UP.set(0, 0, 1);
3235
+ var GLB_MAGIC = 1179937895;
3236
+ var GLB_VERSION = 2;
3237
+ var GLB_HEADER_BYTES = 12;
3238
+ var GLB_CHUNK_HEADER_BYTES = 8;
3239
+ var GLB_JSON_CHUNK_TYPE = 1313821514;
3160
3240
  var EmptyModelError = class extends Error {
3161
3241
  constructor(message) {
3162
3242
  super(message);
3163
3243
  this.name = "EmptyModelError";
3164
3244
  }
3165
3245
  };
3246
+ function parseGlbJsonChunk(buffer) {
3247
+ if (buffer.byteLength < GLB_HEADER_BYTES + GLB_CHUNK_HEADER_BYTES) {
3248
+ return void 0;
3249
+ }
3250
+ const view = new DataView(buffer);
3251
+ const magic = view.getUint32(0, true);
3252
+ const version = view.getUint32(4, true);
3253
+ if (magic !== GLB_MAGIC || version !== GLB_VERSION) {
3254
+ return void 0;
3255
+ }
3256
+ const chunkLength = view.getUint32(GLB_HEADER_BYTES, true);
3257
+ const chunkType = view.getUint32(GLB_HEADER_BYTES + 4, true);
3258
+ if (chunkType !== GLB_JSON_CHUNK_TYPE) {
3259
+ return void 0;
3260
+ }
3261
+ const chunkStart = GLB_HEADER_BYTES + GLB_CHUNK_HEADER_BYTES;
3262
+ const chunkEnd = chunkStart + chunkLength;
3263
+ if (chunkEnd > buffer.byteLength) {
3264
+ return void 0;
3265
+ }
3266
+ try {
3267
+ const chunkBytes = new Uint8Array(buffer, chunkStart, chunkLength);
3268
+ const jsonText = new TextDecoder().decode(chunkBytes);
3269
+ return JSON.parse(jsonText);
3270
+ } catch {
3271
+ return void 0;
3272
+ }
3273
+ }
3274
+ async function resolveParserJson(parserJson, fallback) {
3275
+ if (parserJson && typeof parserJson === "object") {
3276
+ return parserJson;
3277
+ }
3278
+ if (fallback.arrayBuffer) {
3279
+ const fromArrayBuffer = parseGlbJsonChunk(fallback.arrayBuffer);
3280
+ if (fromArrayBuffer && typeof fromArrayBuffer === "object") {
3281
+ return fromArrayBuffer;
3282
+ }
3283
+ }
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
+ if (fallback.url && typeof fetch === "function") {
3295
+ try {
3296
+ const response = await fetch(fallback.url);
3297
+ if (response.ok) {
3298
+ const binary = await response.arrayBuffer();
3299
+ const fromUrl = parseGlbJsonChunk(binary);
3300
+ if (fromUrl && typeof fromUrl === "object") {
3301
+ return fromUrl;
3302
+ }
3303
+ }
3304
+ } catch {
3305
+ }
3306
+ }
3307
+ return parserJson;
3308
+ }
3166
3309
  async function loadGLTF(url, namespaces, aggressiveImport = false, rootBounds) {
3167
3310
  const modelLoader = new GLTFLoader();
3168
3311
  modelLoader.setDRACOLoader(new DRACOLoader());
3169
3312
  const modelData = await modelLoader.loadAsync(url);
3313
+ const parserJson = await resolveParserJson(modelData?.parser?.json, {
3314
+ url
3315
+ });
3170
3316
  const actualizedNamespaces = namespaces.length > 0 ? namespaces : ["default"];
3171
3317
  const asset = parseScene(
3172
3318
  modelData.scene,
3173
3319
  actualizedNamespaces,
3174
3320
  aggressiveImport,
3175
3321
  rootBounds,
3176
- modelData?.parser?.json,
3322
+ parserJson,
3177
3323
  modelData.animations
3178
3324
  );
3179
3325
  return [asset.world, asset.animatables, asset.animations];
@@ -3187,7 +3333,8 @@ async function loadGLTFFromBlob(blob, namespaces, aggressiveImport = false, root
3187
3333
  objectUrl,
3188
3334
  actualizedNamespaces,
3189
3335
  aggressiveImport,
3190
- rootBounds
3336
+ rootBounds,
3337
+ { blob }
3191
3338
  );
3192
3339
  return [asset.world, asset.animatables, asset.animations];
3193
3340
  } finally {
@@ -3201,14 +3348,18 @@ async function loadGLTFFromBlob(blob, namespaces, aggressiveImport = false, root
3201
3348
  loader.parse(
3202
3349
  arrayBuffer,
3203
3350
  "",
3204
- (gltf) => {
3351
+ async (gltf) => {
3205
3352
  try {
3353
+ const parserJson = await resolveParserJson(
3354
+ gltf?.parser?.json,
3355
+ { arrayBuffer }
3356
+ );
3206
3357
  const asset = parseScene(
3207
3358
  gltf.scene,
3208
3359
  actualizedNamespaces,
3209
3360
  aggressiveImport,
3210
3361
  rootBounds,
3211
- gltf?.parser?.json,
3362
+ parserJson,
3212
3363
  gltf.animations
3213
3364
  );
3214
3365
  resolve([asset.world, asset.animatables, asset.animations]);
@@ -3235,19 +3386,23 @@ function parseScene(scene, namespaces, aggressiveImport, rootBounds, parserJson,
3235
3386
  );
3236
3387
  const bundle = extractVizijBundle(scene, parserJson);
3237
3388
  const animations = extractVizijAnimations(parserJson, clips);
3238
- return { world, animatables, bundle, animations };
3389
+ return { world, animatables, bundle, animations, scene };
3239
3390
  }
3240
- async function loadGLTFWithBundle(url, namespaces, aggressiveImport = false, rootBounds) {
3391
+ async function loadGLTFWithBundle(url, namespaces, aggressiveImport = false, rootBounds, parserJsonFallback) {
3241
3392
  const modelLoader = new GLTFLoader();
3242
3393
  modelLoader.setDRACOLoader(new DRACOLoader());
3243
3394
  const modelData = await modelLoader.loadAsync(url);
3395
+ const parserJson = await resolveParserJson(modelData?.parser?.json, {
3396
+ url,
3397
+ ...parserJsonFallback
3398
+ });
3244
3399
  const actualizedNamespaces = namespaces.length > 0 ? namespaces : ["default"];
3245
3400
  return parseScene(
3246
3401
  modelData.scene,
3247
3402
  actualizedNamespaces,
3248
3403
  aggressiveImport,
3249
3404
  rootBounds,
3250
- modelData?.parser?.json,
3405
+ parserJson,
3251
3406
  modelData.animations
3252
3407
  );
3253
3408
  }
@@ -3260,7 +3415,8 @@ async function loadGLTFFromBlobWithBundle(blob, namespaces, aggressiveImport = f
3260
3415
  objectUrl,
3261
3416
  actualizedNamespaces,
3262
3417
  aggressiveImport,
3263
- rootBounds
3418
+ rootBounds,
3419
+ { blob }
3264
3420
  );
3265
3421
  } finally {
3266
3422
  URL.revokeObjectURL(objectUrl);
@@ -3273,14 +3429,18 @@ async function loadGLTFFromBlobWithBundle(blob, namespaces, aggressiveImport = f
3273
3429
  loader.parse(
3274
3430
  arrayBuffer,
3275
3431
  "",
3276
- (gltf) => {
3432
+ async (gltf) => {
3277
3433
  try {
3434
+ const parserJson = await resolveParserJson(
3435
+ gltf?.parser?.json,
3436
+ { arrayBuffer }
3437
+ );
3278
3438
  const asset = parseScene(
3279
3439
  gltf.scene,
3280
3440
  actualizedNamespaces,
3281
3441
  aggressiveImport,
3282
3442
  rootBounds,
3283
- gltf?.parser?.json,
3443
+ parserJson,
3284
3444
  gltf.animations
3285
3445
  );
3286
3446
  resolve(asset);
@@ -3332,6 +3492,160 @@ var loadGltfFromBlob = (blob, namespaces) => {
3332
3492
  import { GLTFExporter } from "three-stdlib";
3333
3493
  import * as THREE6 from "three";
3334
3494
  THREE6.Object3D.DEFAULT_UP.set(0, 0, 1);
3495
+ function normalizeExportError(error) {
3496
+ if (error instanceof Error) {
3497
+ return error;
3498
+ }
3499
+ if (typeof error === "string") {
3500
+ return new Error(error);
3501
+ }
3502
+ return new Error("Failed to export scene.");
3503
+ }
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
+ var GLB_MAGIC2 = 1179937895;
3516
+ var GLB_VERSION2 = 2;
3517
+ var GLB_JSON_CHUNK_TYPE2 = 1313821514;
3518
+ var GLB_HEADER_BYTES2 = 12;
3519
+ var GLB_CHUNK_HEADER_BYTES2 = 8;
3520
+ var GLB_JSON_PADDING_BYTE = 32;
3521
+ function isNearlyEqual(a, b, epsilon = 1e-6) {
3522
+ return Math.abs(a - b) <= epsilon;
3523
+ }
3524
+ function isIdentityTransformNode(node) {
3525
+ const translation = node.translation;
3526
+ if (Array.isArray(translation) && (translation.length !== 3 || !isNearlyEqual(Number(translation[0]), 0) || !isNearlyEqual(Number(translation[1]), 0) || !isNearlyEqual(Number(translation[2]), 0))) {
3527
+ return false;
3528
+ }
3529
+ const rotation = node.rotation;
3530
+ 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))) {
3531
+ return false;
3532
+ }
3533
+ const scale = node.scale;
3534
+ if (Array.isArray(scale) && (scale.length !== 3 || !isNearlyEqual(Number(scale[0]), 1) || !isNearlyEqual(Number(scale[1]), 1) || !isNearlyEqual(Number(scale[2]), 1))) {
3535
+ return false;
3536
+ }
3537
+ return true;
3538
+ }
3539
+ function isPassThroughWrapperNode(node) {
3540
+ if (!node || typeof node !== "object") {
3541
+ return false;
3542
+ }
3543
+ if (!Array.isArray(node.children) || node.children.length === 0) {
3544
+ return false;
3545
+ }
3546
+ const hasOnlyNumericChildren = node.children.every(
3547
+ (index) => Number.isInteger(index)
3548
+ );
3549
+ if (!hasOnlyNumericChildren) {
3550
+ return false;
3551
+ }
3552
+ if (node.mesh !== void 0 || node.camera !== void 0 || node.skin !== void 0) {
3553
+ return false;
3554
+ }
3555
+ if (node.extensions && typeof node.extensions === "object" && Object.keys(node.extensions).length > 0) {
3556
+ return false;
3557
+ }
3558
+ return isIdentityTransformNode(node);
3559
+ }
3560
+ function normalizeExportedSceneJson(json, fallbackSceneName) {
3561
+ if (!json || typeof json !== "object") {
3562
+ return false;
3563
+ }
3564
+ if (!Array.isArray(json.scenes) || !Array.isArray(json.nodes)) {
3565
+ return false;
3566
+ }
3567
+ const sceneIndexRaw = json.scene;
3568
+ const sceneIndex = typeof sceneIndexRaw === "number" && Number.isInteger(sceneIndexRaw) ? sceneIndexRaw : 0;
3569
+ const sceneDef = json.scenes[sceneIndex];
3570
+ if (!sceneDef || typeof sceneDef !== "object") {
3571
+ return false;
3572
+ }
3573
+ let changed = false;
3574
+ let wrapperNodeName;
3575
+ if (Array.isArray(sceneDef.nodes) && sceneDef.nodes.length === 1) {
3576
+ const wrapperIndex = sceneDef.nodes[0];
3577
+ if (Number.isInteger(wrapperIndex)) {
3578
+ const wrapperNode = json.nodes[wrapperIndex];
3579
+ if (isPassThroughWrapperNode(wrapperNode)) {
3580
+ sceneDef.nodes = [...wrapperNode.children];
3581
+ wrapperNodeName = typeof wrapperNode.name === "string" ? wrapperNode.name : void 0;
3582
+ changed = true;
3583
+ }
3584
+ }
3585
+ }
3586
+ const currentSceneName = typeof sceneDef.name === "string" ? sceneDef.name.trim() : "";
3587
+ if (currentSceneName === "AuxScene") {
3588
+ const nextSceneName = (wrapperNodeName?.trim() || fallbackSceneName?.trim() || "Scene").trim();
3589
+ if (nextSceneName.length > 0 && nextSceneName !== currentSceneName) {
3590
+ sceneDef.name = nextSceneName;
3591
+ changed = true;
3592
+ }
3593
+ }
3594
+ return changed;
3595
+ }
3596
+ function sanitizeExportedGlb(buffer, fallbackSceneName) {
3597
+ if (buffer.byteLength < GLB_HEADER_BYTES2 + GLB_CHUNK_HEADER_BYTES2) {
3598
+ return buffer;
3599
+ }
3600
+ const originalBytes = new Uint8Array(buffer);
3601
+ const view = new DataView(buffer);
3602
+ const magic = view.getUint32(0, true);
3603
+ const version = view.getUint32(4, true);
3604
+ if (magic !== GLB_MAGIC2 || version !== GLB_VERSION2) {
3605
+ return buffer;
3606
+ }
3607
+ const jsonChunkLength = view.getUint32(GLB_HEADER_BYTES2, true);
3608
+ const jsonChunkType = view.getUint32(GLB_HEADER_BYTES2 + 4, true);
3609
+ if (jsonChunkType !== GLB_JSON_CHUNK_TYPE2) {
3610
+ return buffer;
3611
+ }
3612
+ const jsonChunkStart = GLB_HEADER_BYTES2 + GLB_CHUNK_HEADER_BYTES2;
3613
+ const jsonChunkEnd = jsonChunkStart + jsonChunkLength;
3614
+ if (jsonChunkEnd > originalBytes.length) {
3615
+ return buffer;
3616
+ }
3617
+ let jsonPayload;
3618
+ try {
3619
+ const jsonText = new TextDecoder().decode(
3620
+ originalBytes.slice(jsonChunkStart, jsonChunkEnd)
3621
+ );
3622
+ jsonPayload = JSON.parse(jsonText);
3623
+ } catch {
3624
+ return buffer;
3625
+ }
3626
+ const changed = normalizeExportedSceneJson(jsonPayload, fallbackSceneName);
3627
+ if (!changed) {
3628
+ return buffer;
3629
+ }
3630
+ const encodedJson = new TextEncoder().encode(JSON.stringify(jsonPayload));
3631
+ const paddedJsonLength = encodedJson.length + 3 & ~3;
3632
+ const paddedJson = new Uint8Array(paddedJsonLength);
3633
+ paddedJson.fill(GLB_JSON_PADDING_BYTE);
3634
+ paddedJson.set(encodedJson);
3635
+ const remainingChunks = originalBytes.slice(jsonChunkEnd);
3636
+ const totalLength = GLB_HEADER_BYTES2 + GLB_CHUNK_HEADER_BYTES2 + paddedJsonLength + remainingChunks.length;
3637
+ const sanitized = new ArrayBuffer(totalLength);
3638
+ const sanitizedBytes = new Uint8Array(sanitized);
3639
+ const sanitizedView = new DataView(sanitized);
3640
+ sanitizedView.setUint32(0, GLB_MAGIC2, true);
3641
+ sanitizedView.setUint32(4, GLB_VERSION2, true);
3642
+ sanitizedView.setUint32(8, totalLength, true);
3643
+ sanitizedView.setUint32(GLB_HEADER_BYTES2, paddedJsonLength, true);
3644
+ sanitizedView.setUint32(GLB_HEADER_BYTES2 + 4, GLB_JSON_CHUNK_TYPE2, true);
3645
+ sanitizedBytes.set(paddedJson, jsonChunkStart);
3646
+ sanitizedBytes.set(remainingChunks, jsonChunkStart + paddedJsonLength);
3647
+ return sanitized;
3648
+ }
3335
3649
  function exportScene(data, fileNameOrOptions = "scene.glb") {
3336
3650
  const options = typeof fileNameOrOptions === "string" ? { fileName: fileNameOrOptions } : fileNameOrOptions ?? {};
3337
3651
  const fileName = options.fileName ?? "scene.glb";
@@ -3346,7 +3660,15 @@ function exportScene(data, fileNameOrOptions = "scene.glb") {
3346
3660
  }
3347
3661
  }
3348
3662
  }));
3349
- const detachBundle = shouldAttachBundle && options.bundle ? applyVizijBundle(data, options.bundle) : () => {
3663
+ const sourceRoot = data;
3664
+ const exportRoot = sourceRoot instanceof THREE6.Scene ? sourceRoot : sourceRoot.clone(true);
3665
+ const exportTarget = exportRoot instanceof THREE6.Scene ? exportRoot : (() => {
3666
+ const scene = new THREE6.Scene();
3667
+ scene.name = data.name?.trim() || "Scene";
3668
+ scene.add(exportRoot);
3669
+ return scene;
3670
+ })();
3671
+ const detachBundle = shouldAttachBundle && options.bundle ? applyVizijBundle(exportRoot, options.bundle) : () => {
3350
3672
  };
3351
3673
  const binary = options.binary ?? true;
3352
3674
  const exporterOptions = {
@@ -3360,33 +3682,40 @@ function exportScene(data, fileNameOrOptions = "scene.glb") {
3360
3682
  }
3361
3683
  try {
3362
3684
  exporter.parse(
3363
- data,
3685
+ exportTarget,
3364
3686
  (gltf) => {
3365
3687
  detachBundle();
3366
3688
  if (!(gltf instanceof ArrayBuffer)) {
3367
- throw new Error("Failed to export scene!");
3689
+ const error = new Error("Failed to export scene.");
3690
+ options.onError?.(error);
3691
+ return;
3368
3692
  }
3369
- const link = document.createElement("a");
3370
- link.href = URL.createObjectURL(
3371
- new Blob([gltf], {
3372
- type: "application/octet-stream"
3373
- })
3693
+ const sanitizedGltf = sanitizeExportedGlb(
3694
+ gltf,
3695
+ data.name?.trim() || void 0
3374
3696
  );
3375
3697
  const trimmed = fileName.trim();
3376
3698
  const safeFileName = trimmed.length > 0 ? trimmed : "scene.glb";
3377
3699
  const downloadName = safeFileName.toLowerCase().endsWith(".glb") ? safeFileName : `${safeFileName}.glb`;
3378
- link.download = downloadName;
3379
- link.click();
3380
- URL.revokeObjectURL(link.href);
3700
+ triggerBlobDownload(
3701
+ new Blob([sanitizedGltf], {
3702
+ type: "application/octet-stream"
3703
+ }),
3704
+ downloadName
3705
+ );
3706
+ options.onComplete?.();
3381
3707
  },
3382
- () => {
3708
+ (error) => {
3383
3709
  detachBundle();
3710
+ options.onError?.(normalizeExportError(error));
3384
3711
  },
3385
3712
  exporterOptions
3386
3713
  );
3387
3714
  } catch (error) {
3388
3715
  detachBundle();
3389
- throw error;
3716
+ const normalizedError = normalizeExportError(error);
3717
+ options.onError?.(normalizedError);
3718
+ throw normalizedError;
3390
3719
  }
3391
3720
  }
3392
3721
  export {
@@ -3405,6 +3734,7 @@ export {
3405
3734
  loadGLTFFromBlobWithBundle,
3406
3735
  loadGLTFWithBundle,
3407
3736
  loadGltfFromBlob,
3737
+ parseGlbJsonChunk,
3408
3738
  useDefaultVizijStore,
3409
3739
  useFeatures,
3410
3740
  useVizijStore,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vizij/render",
3
3
  "description": "Higher-level visualization and interaction components for robot and ai faces.",
4
- "version": "0.1.0",
4
+ "version": "0.1.1",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
7
7
  "types": "dist/index.d.ts",
@@ -76,7 +76,7 @@
76
76
  "lint:fix": "pnpm --filter \"$npm_package_name\" exec eslint --ext .js,.jsx,.ts,.tsx --fix -- .",
77
77
  "prettier:check": "prettier --check .",
78
78
  "prettier:write": "prettier --write .",
79
- "test": "node --loader ./tests/node-ts-loader.mjs --test tests/*.node-test.mjs",
79
+ "test": "node --import ./tests/register-ts-loader.mjs --test tests/*.node-test.mjs",
80
80
  "clean": "rm -rf dist .turbo coverage tsconfig.tsbuildinfo",
81
81
  "reset": "rm -rf node_modules",
82
82
  "reset:hard": "pnpm run reset && rm -f pnpm-lock.yaml package-lock.json yarn.lock",