@volter/editor-threejs 0.5.211 → 0.5.213

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.
Files changed (133) hide show
  1. package/NOTICE +7 -2
  2. package/README.md +8 -13
  3. package/contributions/animation-mixers.service.ts +2 -4
  4. package/contributions/animation-timeline.utility.tsx +1 -2
  5. package/contributions/three.page.ts +50 -0
  6. package/dist-node/serving.mjs +84 -3
  7. package/dist-node/shared-three.mjs +145 -0
  8. package/package.json +296 -16
  9. package/serving/animation-stamp.ts +1 -1
  10. package/serving/index.ts +22 -5
  11. package/serving/model-import-conversion.ts +6 -3
  12. package/serving/shared-three.ts +176 -0
  13. package/src/adapter/constraint.ts +1 -18
  14. package/src/adapter/object3d-authoring-subject.ts +1 -12
  15. package/src/adapter/reflection-probe.ts +1 -9
  16. package/src/adapter/three-contract.ts +6 -60
  17. package/src/animation/live-mixers.ts +0 -4
  18. package/src/api.ts +7 -0
  19. package/src/asset-loaders.ts +2 -178
  20. package/src/capture/photograph-sampling.ts +333 -0
  21. package/src/capture/scene.ts +1 -1
  22. package/src/ecs/object-marks.ts +6 -14
  23. package/src/ecs/user-data.ts +3 -138
  24. package/src/kit/animation/three-clips-subject.ts +19 -42
  25. package/src/kit/asset-compare-core.ts +161 -0
  26. package/src/kit/asset-compare.ts +8 -27
  27. package/src/kit/asset-preview-command.ts +14 -19
  28. package/src/kit/asset-preview-framing.ts +8 -67
  29. package/src/kit/asset-preview.ts +109 -342
  30. package/src/kit/asset-workflow/model-inspection.ts +11 -154
  31. package/src/kit/asset-workflow/preview-resource-lifetime.ts +25 -0
  32. package/src/kit/authoring/component-instance-root.ts +3 -39
  33. package/src/kit/authoring/live-object-transform.ts +1 -30
  34. package/src/kit/authoring/object3d-document-session-registry.ts +7 -15
  35. package/src/kit/authoring/object3d-document-session.ts +471 -208
  36. package/src/kit/authoring/shell-viewport-policy.ts +18 -24
  37. package/src/kit/authoring/source-object3d-authoring-adapter.ts +6 -48
  38. package/src/kit/authoring/three-projection-core.ts +11 -40
  39. package/src/kit/authoring/viewport-pick-context.ts +5 -12
  40. package/src/kit/authoring/viewport-raycast.ts +15 -93
  41. package/src/kit/authoring/world-hidden-viewport.ts +8 -61
  42. package/src/kit/capture-camera-pose.ts +68 -0
  43. package/src/kit/collaboration-presence.ts +41 -0
  44. package/src/kit/components/CameraInfo.tsx +4 -6
  45. package/src/kit/components/InspectorObjectPreview.tsx +3 -30
  46. package/src/kit/components/Object3DDocumentToolbar.css +75 -0
  47. package/src/kit/components/Object3DDocumentToolbar.tsx +18 -84
  48. package/src/kit/components/Object3DDocumentViewport.tsx +6 -11
  49. package/src/kit/components/StageHost.css +40 -0
  50. package/src/kit/components/StageHost.tsx +192 -439
  51. package/src/kit/components/StageOverlays.tsx +1 -2
  52. package/src/kit/components/StatsOverlay.tsx +5 -17
  53. package/src/kit/components/ViewportFurniture.tsx +27 -154
  54. package/src/kit/components/ViewportOverlay.tsx +13 -17
  55. package/src/kit/components/ViewportOverlaysMenu.tsx +177 -0
  56. package/src/kit/components/ViewportShadingMenu.tsx +2 -42
  57. package/src/kit/components/ViewportViewMenu.tsx +7 -18
  58. package/src/kit/components/asset-viewers/EntityModelDocument.tsx +6 -17
  59. package/src/kit/components/asset-viewers/EnvironmentAssetDocument.tsx +9 -11
  60. package/src/kit/components/asset-viewers/LiveModuleDocument.tsx +31 -133
  61. package/src/kit/components/asset-viewers/LutAssetDocument.tsx +10 -12
  62. package/src/kit/components/asset-viewers/ModelAssetDocument.tsx +4 -8
  63. package/src/kit/components/asset-viewers/Object3DPreview.tsx +13 -64
  64. package/src/kit/components/asset-viewers/QuarksAssetDocument.tsx +6 -10
  65. package/src/kit/components/asset-viewers/ShaderAssetDocument.tsx +48 -65
  66. package/src/kit/components/asset-viewers/shader-source.ts +144 -0
  67. package/src/kit/components/asset-viewers/three-asset-viewers.tsx +2 -14
  68. package/src/kit/components/object3d-contribution-surfaces.tsx +5 -3
  69. package/src/kit/components/stage-keyboard.tsx +6 -12
  70. package/src/kit/components/stage-overlay-set.tsx +11 -20
  71. package/src/kit/components/stage-presence-markers.ts +9 -32
  72. package/src/kit/components/stage-transform-chrome.ts +4 -4
  73. package/src/kit/components/stage-transform-tools.tsx +7 -14
  74. package/src/kit/components/standard-viewport-dressing.ts +47 -167
  75. package/src/kit/components/world-root-binding.ts +2 -7
  76. package/src/kit/constraint-helper.ts +1 -1
  77. package/src/kit/document-renderer-session.ts +114 -0
  78. package/src/kit/editor-presentation-activity.ts +58 -0
  79. package/src/kit/editor-shell-store.ts +31 -403
  80. package/src/kit/editor-viewport.ts +486 -1762
  81. package/src/kit/entity-lod.ts +3 -16
  82. package/src/kit/entity-object.ts +1 -39
  83. package/src/kit/hierarchy-mark-reader.ts +13 -30
  84. package/src/kit/instanced-presentation.ts +1 -47
  85. package/src/kit/live-gesture-lock.ts +35 -0
  86. package/src/kit/live-module-source.ts +12 -52
  87. package/src/kit/model-thumbnail.ts +7 -35
  88. package/src/kit/play-camera-flight.ts +7 -16
  89. package/src/kit/projection/three.ts +13 -366
  90. package/src/kit/retained-document-states.ts +91 -0
  91. package/src/kit/scene-document-viewport.ts +3 -3
  92. package/src/kit/scene-framing.ts +11 -152
  93. package/src/kit/scene-view-fog.ts +2 -47
  94. package/src/kit/spatial-handle-visuals.ts +19 -28
  95. package/src/kit/stories/three-story-model.ts +5 -33
  96. package/src/kit/three-canvas-render.ts +3 -4
  97. package/src/kit/three-hierarchy-row-media.ts +3 -11
  98. package/src/kit/three-inspection-media.ts +6 -6
  99. package/src/kit/three-integration.ts +18 -18
  100. package/src/kit/three-state.ts +2 -19
  101. package/src/kit/three-viewport/camera-fit.ts +2 -6
  102. package/src/kit/three-viewport/selection-brackets.ts +5 -20
  103. package/src/kit/three-viewport/selection-outline.ts +4 -56
  104. package/src/kit/three-viewport/skeleton-helper.ts +1 -32
  105. package/src/kit/three-viewport/source-color.ts +1 -33
  106. package/src/kit/three-viewport/studio-environment.ts +4 -55
  107. package/src/kit/three-viewport-presentation.ts +23 -0
  108. package/src/kit/transport-strip.tsx +147 -0
  109. package/src/kit/viewport-actions.ts +8 -50
  110. package/src/kit/viewport-authoring-policy.ts +5 -52
  111. package/src/kit/viewport-commands.ts +28 -57
  112. package/src/kit/viewport-hotkeys.ts +29 -31
  113. package/src/kit/viewport-status-facet.ts +4 -5
  114. package/src/loader.ts +8 -71
  115. package/src/object3d-contributions.ts +18 -34
  116. package/src/viewport/clip-planes.ts +4 -32
  117. package/src/viewport/content-bounds.ts +13 -142
  118. package/src/viewport/editor-layers.ts +4 -26
  119. package/src/viewport/environment.ts +1 -1
  120. package/src/viewport/preview-renderer.ts +7 -45
  121. package/src/viewport/renderer-ownership.ts +14 -62
  122. package/src/viewport-api.ts +14 -33
  123. package/src/viewport-door.ts +16 -86
  124. package/contributions/three-integration.service.ts +0 -13
  125. package/src/adapter/hierarchy-marks.ts +0 -156
  126. package/src/adapter/ingest/scene-capture.ts +0 -843
  127. package/src/adapter/ingest/structural-ids.ts +0 -139
  128. package/src/adapter/ingest/visible-capture-window.ts +0 -302
  129. package/src/adapter/renderer-config.ts +0 -115
  130. package/src/asset-parse-error.ts +0 -29
  131. package/src/host-hierarchy-objects.ts +0 -31
  132. package/src/kit/authoring/design-time-settle.ts +0 -343
  133. package/src/kit/camera-authoring.ts +0 -175
@@ -1,5 +1,6 @@
1
1
  import type { ToolObject3DDocumentAuthoringFactory } from '../../../object3d-contributions';
2
- import { themeVars } from '@volter/sdk/widgets';
2
+ import { StateSurface } from '@volter/sdk/widgets';
3
+ import { Progress } from '@volter/sdk/progress';
3
4
  import type {
4
5
  AuthoringAdapter,
5
6
  InspectorProvider,
@@ -16,7 +17,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
16
17
  import type * as THREE from 'three';
17
18
  import { type ParticleSystem, QuarksLoader } from 'three.quarks';
18
19
  import { quarksParticleSystems } from '../../authoring/quarks-particle-systems';
19
- import { projectOutputPath } from '@volter/sdk/kit/project-provenance';
20
+ import { projectPathOfServedAsset } from '@volter/sdk/project/output-roots';
20
21
  import { Object3DDocumentViewport } from '../StageHost';
21
22
  import { threeObject } from '../../../adapter/three-contract';
22
23
 
@@ -445,7 +446,7 @@ export async function loadNativeQuarksJsonSource(
445
446
  return null;
446
447
  }
447
448
  return isNativeQuarksJson(json)
448
- ? { text, json, projectPath: projectOutputPath(assetPath) }
449
+ ? { text, json, projectPath: projectPathOfServedAsset(assetPath) }
449
450
  : null;
450
451
  }
451
452
 
@@ -503,14 +504,9 @@ export function QuarksAssetDocument({
503
504
 
504
505
  if (error)
505
506
  return (
506
- <div role="alert" style={{ padding: 20, color: themeVars.semantic.danger }}>
507
- {error}
508
- </div>
509
- );
510
- if (!artifact)
511
- return (
512
- <div style={{ padding: 20, color: themeVars.content.muted }}>Loading {displayName}…</div>
507
+ <StateSurface tone="error" title={`Could not load ${displayName}`} detail={error} />
513
508
  );
509
+ if (!artifact) return <Progress documentId={documentId} title={`Loading ${displayName}`} />;
514
510
  return (
515
511
  <Object3DDocumentViewport
516
512
  documentId={documentId}
@@ -1,12 +1,13 @@
1
+ import { editorHost } from '@volter/sdk/host';
1
2
  import { faCode, faSliders, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons';
2
- import { Checkbox, ColorInput, NumberInput, Select, themeVars } from '@volter/sdk/widgets';
3
+ import { Checkbox, ColorInput, EditorBanner, NumberInput, Select, spaceVar, StateSurface, Text, themeVars } from '@volter/sdk/widgets';
4
+ import { Progress } from '@volter/sdk/progress';
3
5
  import { useEffect, useMemo, useRef, useState } from 'react';
4
6
  import * as THREE from 'three';
5
- import { readProjectTextFile } from '@volter/sdk/kit/editor-api';
6
- import type { InspectionSection } from '@volter/sdk/kit/inspection-model';
7
- import { CONTRIBUTED_SECTION_ORDER, PROPERTIES_SECTION_ORDER } from '@volter/sdk/kit/inspection-model';
8
- import { AssetEditorShell } from '@volter/sdk/kit/components/AssetEditorShell';
9
- import { subscribeProjectAsset } from '@volter/sdk/kit/components/asset-editor-persistence';
7
+ import { readProjectTextFile } from '@volter/sdk/api/project-source';
8
+ import type { InspectionSection } from '@volter/sdk/inspection-model';
9
+ import { CONTRIBUTED_SECTION_ORDER, PROPERTIES_SECTION_ORDER } from '@volter/sdk/inspection-model';
10
+ import { AssetEditorShell } from '@volter/sdk/components/AssetEditorShell';
10
11
  import {
11
12
  FALLBACK_FRAGMENT_SHADER,
12
13
  FALLBACK_VERTEX_SHADER,
@@ -18,7 +19,7 @@ import {
18
19
  type ShaderUniformDescriptor,
19
20
  shaderCompanionPath,
20
21
  shaderStageFromPath,
21
- } from '@volter/sdk/kit/components/asset-viewers/shader-source';
22
+ } from './shader-source';
22
23
 
23
24
  type PreviewGeometry = 'sphere' | 'box' | 'plane';
24
25
  type UniformPreviewValue = number | boolean | string | number[];
@@ -267,7 +268,7 @@ function ShaderPreview({
267
268
  const observer = new ResizeObserver(resize);
268
269
  observer.observe(host);
269
270
 
270
- const clock = new THREE.Clock();
271
+ const timer = new THREE.Timer();
271
272
  try {
272
273
  renderer.compile(scene, camera);
273
274
  renderer.render(scene, camera);
@@ -294,7 +295,8 @@ function ShaderPreview({
294
295
 
295
296
  const render = () => {
296
297
  if (disposed) return;
297
- const elapsed = clock.getElapsedTime();
298
+ timer.update();
299
+ const elapsed = timer.getElapsed();
298
300
  for (const descriptor of descriptors) {
299
301
  const uniform = uniforms[descriptor.name];
300
302
  if (!uniform) continue;
@@ -448,6 +450,14 @@ function UniformControlRow({
448
450
  );
449
451
  }
450
452
 
453
+ /** One diagnostic as a line: where (the file, its line and column) and what. */
454
+ function diagnosticLine(diagnostic: ShaderDiagnostic, files: ShaderFiles): string {
455
+ const file = diagnostic.stage === 'program' ? null : files[diagnostic.stage];
456
+ const where = file?.available ? basename(file.path) : diagnostic.stage;
457
+ const at = diagnostic.line ? `:${diagnostic.line}${diagnostic.column ? `:${diagnostic.column}` : ''}` : '';
458
+ return `${where}${at} · ${diagnostic.message}`;
459
+ }
460
+
451
461
  function DiagnosticList({
452
462
  diagnostics,
453
463
  files,
@@ -456,37 +466,21 @@ function DiagnosticList({
456
466
  files: ShaderFiles;
457
467
  }) {
458
468
  if (diagnostics.length === 0) {
459
- return (
460
- <div style={{ color: themeVars.semantic.success, fontSize: 11, padding: 4 }}>
461
- Shader program compiled successfully.
462
- </div>
463
- );
469
+ return <StateSurface inline tone="success" title="Shader program compiled successfully." />;
464
470
  }
465
471
  return (
466
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6, padding: 4 }}>
467
- {diagnostics.map((diagnostic, index) => {
468
- const file = diagnostic.stage === 'program' ? null : files[diagnostic.stage];
469
- return (
470
- <div
471
- key={`${diagnostic.stage}:${diagnostic.line ?? 0}:${diagnostic.message}:${index}`}
472
- style={{
473
- color:
474
- diagnostic.severity === 'warning'
475
- ? themeVars.semantic.warning
476
- : themeVars.semantic.danger,
477
- fontSize: 10,
478
- lineHeight: 1.45,
479
- }}
480
- >
481
- {file?.available ? basename(file.path) : diagnostic.stage}
482
- {diagnostic.line
483
- ? `:${diagnostic.line}${diagnostic.column ? `:${diagnostic.column}` : ''}`
484
- : ''}
485
- {' · '}
486
- {diagnostic.message}
487
- </div>
488
- );
489
- })}
472
+ <div style={{ display: 'flex', flexDirection: 'column', gap: spaceVar[2] }}>
473
+ {diagnostics.map((diagnostic, index) => (
474
+ <Text
475
+ key={`${diagnostic.stage}:${diagnostic.line ?? 0}:${diagnostic.message}:${index}`}
476
+ as="div"
477
+ variant="code"
478
+ selectable
479
+ tone={diagnostic.severity === 'warning' ? 'warning' : 'danger'}
480
+ >
481
+ {diagnosticLine(diagnostic, files)}
482
+ </Text>
483
+ ))}
490
484
  </div>
491
485
  );
492
486
  }
@@ -535,8 +529,8 @@ export function ShaderAssetDocument({
535
529
  const reload = () => setFileRevision((revision) => revision + 1);
536
530
  const companionPath = shaderCompanionPath(assetPath);
537
531
  const unsubscribers = [
538
- subscribeProjectAsset(assetPath, reload),
539
- ...(companionPath ? [subscribeProjectAsset(companionPath, reload)] : []),
532
+ editorHost().files.subscribeAsset(assetPath, reload),
533
+ ...(companionPath ? [editorHost().files.subscribeAsset(companionPath, reload)] : []),
540
534
  ];
541
535
  return () => {
542
536
  for (const unsubscribe of unsubscribers) unsubscribe();
@@ -588,17 +582,18 @@ export function ShaderAssetDocument({
588
582
  order: PROPERTIES_SECTION_ORDER,
589
583
  body: {
590
584
  kind: 'custom',
591
- render: () => (
585
+ // (shown once the files are read: the document passes its sections only then)
586
+ render: () => files && (
592
587
  <div
593
588
  style={{ display: 'flex', flexDirection: 'column', gap: 5, padding: 4, fontSize: 10 }}
594
589
  >
595
590
  <span>
596
- Vertex · {files ? basename(files.vertex.path) : 'Loading…'}
597
- {files && !files.vertex.available ? ' (preview fallback)' : ''}
591
+ Vertex · {basename(files.vertex.path)}
592
+ {!files.vertex.available ? ' (preview fallback)' : ''}
598
593
  </span>
599
594
  <span>
600
- Fragment · {files ? basename(files.fragment.path) : 'Loading…'}
601
- {files && !files.fragment.available ? ' (preview fallback)' : ''}
595
+ Fragment · {basename(files.fragment.path)}
596
+ {!files.fragment.available ? ' (preview fallback)' : ''}
602
597
  </span>
603
598
  <span style={{ color: themeVars.content.muted }}>
604
599
  Ordinary sibling GLSL files; runtime composition stays in TS/TSX.
@@ -657,9 +652,7 @@ export function ShaderAssetDocument({
657
652
  title={basename(assetPath)}
658
653
  status="Shader unavailable"
659
654
  >
660
- <div role="alert" style={{ padding: 20, color: themeVars.semantic.danger }}>
661
- {loadError}
662
- </div>
655
+ <StateSurface tone="error" title={`Could not load ${basename(assetPath)}`} detail={loadError} />
663
656
  </AssetEditorShell>
664
657
  );
665
658
  }
@@ -670,9 +663,8 @@ export function ShaderAssetDocument({
670
663
  active={active}
671
664
  type="shader"
672
665
  title={basename(assetPath)}
673
- status="Loading GLSL"
674
666
  >
675
- <div style={{ padding: 20, color: themeVars.content.muted }}>Loading shader files…</div>
667
+ <Progress documentId={documentId} title={`Loading ${basename(assetPath)}`} />
676
668
  </AssetEditorShell>
677
669
  );
678
670
  }
@@ -720,22 +712,13 @@ export function ShaderAssetDocument({
720
712
  </Select>
721
713
  </div>
722
714
  {diagnostics.length > 0 && (
723
- <div
724
- style={{
725
- position: 'absolute',
726
- left: 12,
727
- right: 12,
728
- bottom: 12,
729
- maxHeight: 120,
730
- overflow: 'auto',
731
- padding: 8,
732
- border: `1px solid ${themeVars.semantic.danger}`,
733
- borderRadius: 6,
734
- background: themeVars.surface.overlay,
735
- }}
715
+ <EditorBanner
716
+ tone={diagnostics.some((diagnostic) => diagnostic.severity !== 'warning') ? 'error' : 'warning'}
717
+ style={{ position: 'absolute', left: 0, right: 0, bottom: 0 }}
718
+ detail={diagnostics.map((diagnostic) => diagnosticLine(diagnostic, files)).join('\n')}
736
719
  >
737
- <DiagnosticList diagnostics={diagnostics} files={files} />
738
- </div>
720
+ {`${diagnostics.length} issue${diagnostics.length === 1 ? '' : 's'}`}
721
+ </EditorBanner>
739
722
  )}
740
723
  </div>
741
724
  </AssetEditorShell>
@@ -0,0 +1,144 @@
1
+ /** Native GLSL document helpers. GLSL remains the artifact; these functions
2
+ * only describe it for Asset Lab and never serialize a Volter shader shape. */
3
+
4
+ export type ShaderStage = 'vertex' | 'fragment';
5
+
6
+ export interface ShaderUniformDescriptor {
7
+ readonly name: string;
8
+ readonly type: string;
9
+ readonly arraySize?: number | undefined;
10
+ readonly stages: readonly ShaderStage[];
11
+ }
12
+
13
+ export interface ShaderDiagnostic {
14
+ readonly stage: ShaderStage | 'program';
15
+ readonly severity: 'error' | 'warning';
16
+ readonly message: string;
17
+ readonly line?: number | undefined;
18
+ readonly column?: number | undefined;
19
+ }
20
+
21
+ const VERTEX_SUFFIXES = ['.vert', '.vert.glsl'] as const;
22
+ const FRAGMENT_SUFFIXES = ['.frag', '.frag.glsl'] as const;
23
+
24
+ function cleanPath(path: string): string {
25
+ return path.split(/[?#]/, 1)[0]!.toLowerCase();
26
+ }
27
+
28
+ export function shaderStageFromPath(path: string, source = ''): ShaderStage {
29
+ const clean = cleanPath(path);
30
+ if (VERTEX_SUFFIXES.some((suffix) => clean.endsWith(suffix))) return 'vertex';
31
+ if (FRAGMENT_SUFFIXES.some((suffix) => clean.endsWith(suffix))) return 'fragment';
32
+ return /\bgl_Position\b/.test(source) ? 'vertex' : 'fragment';
33
+ }
34
+
35
+ /** Conventional sibling only. A standalone `.glsl` file stays standalone. */
36
+ export function shaderCompanionPath(path: string): string | null {
37
+ const clean = path.split(/[?#]/, 1)[0]!;
38
+ if (/\.vert\.glsl$/i.test(clean)) return clean.replace(/\.vert\.glsl$/i, '.frag.glsl');
39
+ if (/\.frag\.glsl$/i.test(clean)) return clean.replace(/\.frag\.glsl$/i, '.vert.glsl');
40
+ if (/\.vert$/i.test(clean)) return clean.replace(/\.vert$/i, '.frag');
41
+ if (/\.frag$/i.test(clean)) return clean.replace(/\.frag$/i, '.vert');
42
+ return null;
43
+ }
44
+
45
+ function withoutComments(source: string): string {
46
+ // Preserve newlines so reflection never perturbs source-line diagnostics.
47
+ return source
48
+ .replace(/\/\*[\s\S]*?\*\//g, (comment) => comment.replace(/[^\n]/g, ' '))
49
+ .replace(/\/\/[^\n]*/g, '');
50
+ }
51
+
52
+ function declarations(source: string, stage: ShaderStage): ShaderUniformDescriptor[] {
53
+ const result: ShaderUniformDescriptor[] = [];
54
+ const pattern =
55
+ /\buniform\s+(?:(?:lowp|mediump|highp)\s+)?([A-Za-z_]\w*)\s+([A-Za-z_]\w*)\s*(?:\[\s*(\d+)\s*\])?\s*;/g;
56
+ for (const match of withoutComments(source).matchAll(pattern)) {
57
+ const type = match[1];
58
+ const name = match[2];
59
+ if (!type || !name) continue;
60
+ const arraySize = match[3] ? Number.parseInt(match[3], 10) : undefined;
61
+ result.push({ name, type, ...(arraySize ? { arraySize } : {}), stages: [stage] });
62
+ }
63
+ return result;
64
+ }
65
+
66
+ export function reflectShaderUniforms(
67
+ vertexSource: string,
68
+ fragmentSource: string,
69
+ ): ShaderUniformDescriptor[] {
70
+ const reflected = new Map<string, ShaderUniformDescriptor>();
71
+ for (const uniform of [
72
+ ...declarations(vertexSource, 'vertex'),
73
+ ...declarations(fragmentSource, 'fragment'),
74
+ ]) {
75
+ const current = reflected.get(uniform.name);
76
+ if (!current) {
77
+ reflected.set(uniform.name, uniform);
78
+ continue;
79
+ }
80
+ reflected.set(uniform.name, {
81
+ ...current,
82
+ stages: [...new Set([...current.stages, ...uniform.stages])],
83
+ });
84
+ }
85
+ return [...reflected.values()];
86
+ }
87
+
88
+ export function parseShaderDiagnostics(
89
+ log: string,
90
+ stage: ShaderDiagnostic['stage'],
91
+ ): ShaderDiagnostic[] {
92
+ const diagnostics = log
93
+ .split('\n')
94
+ .map((line) => parseDiagnosticLine(line.trim(), stage))
95
+ .filter((diagnostic): diagnostic is ShaderDiagnostic => diagnostic !== null);
96
+ if (diagnostics.length === 0 && log.trim()) {
97
+ diagnostics.push({ stage, severity: 'error', message: log.trim() });
98
+ }
99
+ return diagnostics;
100
+ }
101
+
102
+ function parseDiagnosticLine(
103
+ text: string,
104
+ stage: ShaderDiagnostic['stage'],
105
+ ): ShaderDiagnostic | null {
106
+ if (!text) return null;
107
+ const colon = text.match(/^(ERROR|WARNING):\s*\d+:(\d+)(?::|\((\d+)\):)\s*(.*)$/i);
108
+ if (colon) {
109
+ return {
110
+ stage,
111
+ severity: colon[1]!.toLowerCase() === 'warning' ? 'warning' : 'error',
112
+ line: Number.parseInt(colon[2]!, 10),
113
+ ...(colon[3] ? { column: Number.parseInt(colon[3], 10) } : {}),
114
+ message: colon[4] || text,
115
+ };
116
+ }
117
+ const paren = text.match(/^\d+:(\d+)\((\d+)\):\s*(?:(error|warning)\s*:?)?\s*(.*)$/i);
118
+ if (!paren) return null;
119
+ return {
120
+ stage,
121
+ severity: paren[3]?.toLowerCase() === 'warning' ? 'warning' : 'error',
122
+ line: Number.parseInt(paren[1]!, 10),
123
+ column: Number.parseInt(paren[2]!, 10),
124
+ message: paren[4] || text,
125
+ };
126
+ }
127
+
128
+ /** Reset driver-reported lines after Three's injected ShaderMaterial prefix. */
129
+ export function previewShaderSource(source: string): string {
130
+ return `#line 1\n${source}`;
131
+ }
132
+
133
+ export const FALLBACK_VERTEX_SHADER = `varying vec2 vUv;
134
+
135
+ void main() {
136
+ vUv = uv;
137
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
138
+ }`;
139
+
140
+ export const FALLBACK_FRAGMENT_SHADER = `varying vec2 vUv;
141
+
142
+ void main() {
143
+ gl_FragColor = vec4(vUv, 0.55, 1.0);
144
+ }`;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * THE THREE.JS ASSET LAB VIEWERS, registered by route
3
- * (`@volter/sdk/kit/asset-viewers`): a glTF model, an environment map, a
3
+ * (`@volter/sdk/asset-viewers`): a glTF model, an environment map, a
4
4
  * LUT, a shader, a live modeling module and a live entity's model. Each loads on
5
5
  * first use, so a document that never opens one never pays for three.js.
6
6
  */
@@ -10,7 +10,7 @@ import {
10
10
  type JsonContentViewer,
11
11
  registerAssetViewer,
12
12
  registerJsonContentViewer,
13
- } from '@volter/sdk/kit/asset-viewers';
13
+ } from '@volter/sdk/asset-viewers';
14
14
  import { type ComponentType, lazy } from 'react';
15
15
 
16
16
  const viewers: Readonly<Record<AssetViewerRoute, ComponentType<AssetViewerProps>>> = {
@@ -64,18 +64,6 @@ const viewers: Readonly<Record<AssetViewerRoute, ComponentType<AssetViewerProps>
64
64
  ),
65
65
  };
66
66
  }),
67
- 'model-preview': lazy(async () => {
68
- const { Object3DPreview } = await import('./Object3DPreview');
69
- return {
70
- default: (props: AssetViewerProps) => (
71
- <Object3DPreview
72
- assetPath={props.assetPath}
73
- {...(props.materialPath ? { materialPath: props.materialPath } : {})}
74
- displayName={props.displayName}
75
- />
76
- ),
77
- };
78
- }),
79
67
  // A project script that BUILDS an Object3D opens as the live modeling
80
68
  // document; anything else keeps the text preview it is handed as `fallback`.
81
69
  module: lazy(async () => {
@@ -1,4 +1,5 @@
1
- import { announceDocumentStage } from '@volter/sdk/kit/document-viewports';
1
+ import { announceDocumentStage } from '@volter/sdk/document-viewports';
2
+ import { Progress } from '@volter/sdk/progress';
2
3
  import { lazy, Suspense, useEffect } from 'react';
3
4
  import type { ToolObject3DAuthoringProps, ToolObject3DPreviewProps } from '../../object3d-contributions';
4
5
 
@@ -12,7 +13,8 @@ const Authoring = lazy(async () => ({ default: (await import('./StageHost')).Too
12
13
 
13
14
  export function Object3DPreviewSurface(props: ToolObject3DPreviewProps) {
14
15
  return (
15
- <Suspense fallback={<div style={{ minHeight: 240 }}>Loading 3D preview…</div>}>
16
+ // (a preview card reports no wait: it is a card in a panel or a catalog, neither a document nor a view)
17
+ <Suspense fallback={null}>
16
18
  <Preview {...props} />
17
19
  </Suspense>
18
20
  );
@@ -26,7 +28,7 @@ export function Object3DAuthoringSurface(props: ToolObject3DAuthoringProps) {
26
28
  // `workspace.document` contributions do not, and which is which belongs to the PROJECT.
27
29
  useEffect(() => announceDocumentStage(props.documentId), [props.documentId]);
28
30
  return (
29
- <Suspense fallback={<div style={{ minHeight: 240 }}>Loading 3D authoring surface…</div>}>
31
+ <Suspense fallback={<Progress documentId={props.documentId} title="Loading 3D authoring surface" />}>
30
32
  <Authoring {...props} />
31
33
  </Suspense>
32
34
  );
@@ -3,18 +3,12 @@
3
3
  * Frame, pivot, snap and the vertex-snap hold, bound for the stage whose
4
4
  * document is ACTIVE.
5
5
  *
6
- * They belong to the STAGE, and until 2026-09-19 they were mounted from
7
- * `stage-overlay-set.tsx`, which a stage only reaches when it has the shell
8
- * above it (`StageHost.tsx`'s `shellStore && !chromeless`). A document
9
- * contributed by a PACKAGE mounts outside `EditorProvider`, so the Model
10
- * document reached NONE of them: measured live in the Code-OSS frame on a
11
- * models project, the editor's action table held 29 entries — the shell set
12
- * alone — and the stage's nine (`transform.combined/translate/rotate/scale`,
13
- * `viewport.toggleSnap/frameSelection/cyclePivot/snapToFloor`, the four view
14
- * presets) were absent, so G/R/S on the Model stage answered nothing at all.
15
- * The overlay set is an OVERLAY condition (a bounded host must not pay for
16
- * the collaboration client it reaches); which keys a stage answers is not,
17
- * and the two were one line by accident.
6
+ * They belong to the STAGE, not to `stage-overlay-set.tsx`, which a stage
7
+ * only reaches when it has the shell above it (`StageHost.tsx`'s
8
+ * `shellStore && !chromeless`): a document contributed by a PACKAGE mounts
9
+ * outside `EditorProvider` and still answers G/R/S. The overlay set is an
10
+ * OVERLAY condition (a bounded host must not pay for the collaboration client
11
+ * it reaches); which keys a stage answers is not.
18
12
  *
19
13
  * The ONE registry still means ONE holder (`hotkeys.ts`'s scope table, and
20
14
  * `key-actions.ts`'s action table under the frame): the active
@@ -2,26 +2,26 @@
2
2
  * hints and statistics. Package documents supply their own chrome. */
3
3
 
4
4
  import { useSyncExternalStore } from 'react';
5
- import { activeAuthoringVersion, subscribeActiveAuthoring } from '@volter/sdk/kit/authoring/active-adapter';
5
+ import { activeAuthoringVersion, subscribeActiveAuthoring } from '@volter/sdk/authoring/active-adapter';
6
6
  import {
7
7
  object3DDocumentSessionsVersion,
8
8
  subscribeObject3DDocumentSessions,
9
9
  } from '../authoring/object3d-document-session-registry';
10
10
  import type { EditorShellStore } from '../editor-shell-store';
11
11
  import type { EditorViewport } from '../editor-viewport';
12
- import { documentStageContext, type StageChrome, threeSelectionToolsApply } from '@volter/sdk/kit/stage-context';
12
+ import { documentStageContext, type StageChrome, stageSelectionToolsApply } from '@volter/sdk/stage-context';
13
13
  import {
14
14
  subscribeWorkspaceDocuments,
15
15
  workspaceDocumentRegistryVersion,
16
- } from '@volter/sdk/kit/workspace-document-registry';
16
+ } from '@volter/sdk/workspace-document-registry';
17
17
  import {
18
18
  subscribeViewportPresentation,
19
19
  viewPresentation,
20
20
  viewportPresentationVersion,
21
- } from '@volter/sdk/kit/viewport-presentation';
21
+ } from '@volter/sdk/viewport-presentation';
22
22
  import { CameraInfo } from './CameraInfo';
23
23
  import { StatsOverlay } from './StatsOverlay';
24
- import { TransientHintOverlay } from '@volter/sdk/kit/components/TransientHint';
24
+ import { TransientHintOverlay } from '@volter/sdk/components/TransientHint';
25
25
  import { ViewportOverlay } from './ViewportOverlay';
26
26
 
27
27
  /** What the HOST hands over about the stage itself — its own store, the
@@ -52,8 +52,7 @@ export function StageOverlaySet({
52
52
  active,
53
53
  chrome,
54
54
  }: StageOverlaySetProps) {
55
- /** THIS stage's own store — the shell's until the stage has finished
56
- * building, which is the same store every overlay read before unit 4. */
55
+ /** Use the stage's store once built, and the shell store during bootstrap. */
57
56
  const stageStore = stage?.store ?? store;
58
57
  useSyncExternalStore(store.shell.subscribe, store.shell.getShellSnapshot ?? store.shell.getSnapshot);
59
58
  // The stage's own state (its grid, helpers, stats, tool mode) lives on ITS
@@ -64,42 +63,34 @@ export function StageOverlaySet({
64
63
  useSyncExternalStore(subscribeWorkspaceDocuments, workspaceDocumentRegistryVersion);
65
64
  // THIS stage's context — not the focused one. A capability is present where
66
65
  // its condition holds, and the condition is asked of the stage that would
67
- // draw it (ARCHITECTURE-CORE §One stage).
66
+ // draw it.
68
67
  const ctx = documentStageContext(store.shell, documentId, chrome);
69
68
  /** Whether the selection tools draw here — and that is the WHOLE condition:
70
69
  * they read THIS stage's own store (`stage.store`, the one the shared
71
70
  * panels reach through `focusedStageStore()`), so a prefab's grid button
72
71
  * toggles the prefab's grid and its readout reports the camera its reader
73
72
  * is looking through. */
74
- const showsSelectionTools = threeSelectionToolsApply(ctx);
73
+ const showsSelectionTools = stageSelectionToolsApply(ctx);
75
74
  // The camera readout is the editor's own; a view whose target draws none leaves it off
76
75
  // (`overlays.cameraReadout`).
77
76
  useSyncExternalStore(subscribeViewportPresentation, viewportPresentationVersion, viewportPresentationVersion);
78
77
  const cameraReadout = viewPresentation(documentId).overlays.cameraReadout;
79
78
  return (
80
79
  <>
81
- {/* THE STAGE'S OWN KEYBOARD is no longer mounted here: this module is
82
- reached only by a stage that has the shell above it, and a
83
- package-contributed document (the Model document) has none — so it
84
- answered no stage key at all. It is `StageHost.tsx`'s now, beside
85
- the stage handle, under the same activation condition
86
- (`stage-keyboard.tsx` carries the measurement). */}
80
+ {/* StageHost mounts the stage keyboard beside its handle under the stage's activation condition. */}
87
81
  {showsSelectionTools ? (
88
82
  <>
89
83
  <ViewportOverlay store={stageStore} documentId={documentId} />
90
84
  {cameraReadout ? <CameraInfo /> : null}
91
85
  </>
92
86
  ) : null}
93
- {ctx.surface === 'three' && stageStore.showStats && <StatsOverlay />}
87
+ {ctx.surface === 'stage' && stageStore.showStats && <StatsOverlay />}
94
88
  {/* H2 — the ONE transient hint channel. Lives here because a refused
95
89
  transform is a viewport gesture; the overlay is absent from the DOM
96
90
  entirely until something has a reason to say. ONE channel means one
97
91
  mount — the ACTIVE stage's — so a hint never draws twice. */}
98
92
  {active ? <TransientHintOverlay /> : null}
99
- {/* W4 (inventory row V9): the scene document mounts NO network overlay.
100
- Compact connection state lives in the status bar
101
- (`status-contributions.tsx`); the detailed surface is the `network`
102
- workspace utility (`NetworkInspectorPanel`), revealed from there. */}
93
+ {/* Connection state belongs to the status bar; the network utility provides detail. */}
103
94
  </>
104
95
  );
105
96
  }
@@ -1,36 +1,15 @@
1
- /**
2
- * THE OTHER PARTICIPANTS, ON A STAGE — the spatial half of collaboration
3
- * presence: what THIS stage publishes about the reader's own camera and
4
- * gestures, and the camera frusta, selection boxes and pointer rays it paints
5
- * for everyone else (WORK.md §Presence and the substrate, presence unit 4).
6
- *
7
- * It was the scene panel's, so only the world root had it. Under
8
- * ARCHITECTURE-CORE §One stage it is a CAPABILITY with a condition — a stage
9
- * painting a three surface — so every 3D document mounts it and a prefab
10
- * story shows the same markers the Scene does.
11
- *
12
- * WHICH STAGE A PARTICIPANT IS ON. Presence is ONE record per participant
13
- * (`collaboration-presence.ts`), and its `document` field is written by the
14
- * shell for the ACTIVE workspace document (`DefaultEditorLayout.tsx`). So a
15
- * stage publishes its camera only while its own document is the active one —
16
- * otherwise several stages would overwrite each other's pose in the single
17
- * record — and paints a participant only on the stage whose id matches that
18
- * participant's `presence.document`. Measured against the spec's "publish it
19
- * from the host with the stage's documentId": the presence record has ONE
20
- * `document` field and the shell already owns it, so the stage owns the
21
- * CONDITION on writing, not a second writer of that field.
22
- */
1
+ /** Publish this stage’s camera only while its document is active, and draw each participant on the stage named by presence.document. The shell owns the document field. */
23
2
 
24
- import type { CollaborationParticipant } from '@volter/sdk/session/collaboration-types';
3
+ import { editorHost } from '@volter/sdk/host';
4
+ import type { CollaborationParticipant } from '@volter/sdk/collaboration-types';
25
5
  import { EDITOR_THEME_CLASS, graphiteDarkEditorTheme } from '@volter/sdk/widgets';
26
6
  import { contentWorldBounds } from '@volter/editor-threejs/viewport/content-bounds';
27
7
  import * as THREE from 'three';
28
- import { collaborationSnapshot } from '@volter/sdk/kit/collaboration-client';
29
- import { reportCollaborationPresence } from '@volter/sdk/kit/collaboration-presence';
30
- import { EDITOR_PARTICIPANT_ID } from '@volter/sdk/kit/editor-presence';
8
+ import { reportCollaborationPresence } from '../collaboration-presence';
9
+ import { COLLABORATION_PARTICIPANT_ID as EDITOR_PARTICIPANT_ID } from '@volter/sdk/editor-session-attribution';
31
10
  import type { EditorShellStore } from '../editor-shell-store';
32
11
  import type { EditorViewport } from '../editor-viewport';
33
- import { activeWorkspaceDocumentId } from '@volter/sdk/kit/workspace-document-registry';
12
+ import { activeWorkspaceDocumentId } from '@volter/sdk/workspace-document-registry';
34
13
 
35
14
  interface RemoteCameraMarker {
36
15
  camera: THREE.PerspectiveCamera;
@@ -58,9 +37,7 @@ interface RemoteRayMarker {
58
37
  /**
59
38
  * Remote-camera labels paint into a DETACHED canvas, and a 2D context cannot
60
39
  * resolve `var(--volter-…)` — so the theme's value has to be read off the live
61
- * theme root before it reaches `fillStyle`. Before a theme is installed (unit
62
- * environments), fall back to the default theme's own value rather than a raw
63
- * literal, so the label never depends on a color the theme contract doesn't own.
40
+ * theme root before it reaches `fillStyle`. Use the default theme's value during bootstrap.
64
41
  */
65
42
  function contentOnAccentColor(): string {
66
43
  const root = document.querySelector(`.${EDITOR_THEME_CLASS}`);
@@ -324,7 +301,7 @@ export function bindStagePresenceMarkers(options: StagePresenceOptions): StagePr
324
301
  const remoteRayMarkers = new Map<string, RemoteRayMarker>();
325
302
  let remotePresenceSequence = -1;
326
303
  const syncRemoteSpatialMarkers = (): void => {
327
- const snapshot = collaborationSnapshot();
304
+ const snapshot = editorHost().collaboration.snapshot();
328
305
  if (!snapshot || snapshot.sequence === remotePresenceSequence) return;
329
306
  remotePresenceSequence = snapshot.sequence;
330
307
  /** THE PAINT CONDITION: a participant is on the stage its own presence
@@ -409,7 +386,7 @@ export function bindStagePresenceMarkers(options: StagePresenceOptions): StagePr
409
386
  if (!publishes()) return;
410
387
  const participantId = (event as CustomEvent<{ participantId?: unknown }>).detail?.participantId;
411
388
  if (typeof participantId !== 'string') return;
412
- const camera = collaborationSnapshot()?.participants.find(
389
+ const camera = editorHost().collaboration.snapshot()?.participants.find(
413
390
  (participant) => participant.participantId === participantId,
414
391
  )?.presence.camera;
415
392
  if (!camera) return;