@volter/editor-threejs 0.5.134 → 0.5.136

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@volter/editor-threejs",
3
3
  "author": "Volter AI, Inc.",
4
- "version": "0.5.134",
4
+ "version": "0.5.136",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -135,12 +135,12 @@
135
135
  "serving": "./dist-node/serving.mjs"
136
136
  },
137
137
  "dependencies": {
138
- "@volter/editor-project": "0.5.134",
138
+ "@volter/editor-project": "0.5.136",
139
139
  "@sparkjsdev/spark": "^2.1.0",
140
140
  "three.quarks": "^0.16.0",
141
141
  "zod": "^4.3.6",
142
142
  "typescript": "5.9.3",
143
- "@volter/editor-sdk": "0.5.134",
143
+ "@volter/editor-sdk": "0.5.136",
144
144
  "postprocessing": "6.38.3",
145
145
  "quarks.core": "0.16.0",
146
146
  "@fortawesome/free-solid-svg-icons": "^7.2.0"
@@ -174,7 +174,7 @@ export class Object3DDocumentSession {
174
174
  private readonly throughOrthographic = new THREE.OrthographicCamera();
175
175
  /** The host's mirror of the document scene onto the rendered scene — see
176
176
  * {@link Object3DDocumentSession.setBeforeRender}. */
177
- private beforeRender: (() => void | (() => void)) | null = null;
177
+ private beforeRender: ((interactive: boolean) => void | (() => void)) | null = null;
178
178
  private state = INITIAL_PRESENTATION;
179
179
  private version = 0;
180
180
  private readonly listeners = new Set<() => void>();
@@ -1253,7 +1253,7 @@ export class Object3DDocumentSession {
1253
1253
  * The host owns the step and clears it in its own teardown; the session only
1254
1254
  * holds the reference.
1255
1255
  */
1256
- setBeforeRender(step: (() => void | (() => void)) | null): void {
1256
+ setBeforeRender(step: ((interactive: boolean) => void | (() => void)) | null): void {
1257
1257
  this.beforeRender = step;
1258
1258
  }
1259
1259
 
@@ -1338,8 +1338,8 @@ export class Object3DDocumentSession {
1338
1338
  for (const resolve of waiters) resolve(frame);
1339
1339
  }
1340
1340
 
1341
- render(renderSolid: (camera: THREE.Camera) => void): void {
1342
- const finishDraw = this.beforeRender?.();
1341
+ render(renderSolid: (camera: THREE.Camera) => void, interactive = false): void {
1342
+ const finishDraw = this.beforeRender?.(interactive);
1343
1343
  try {
1344
1344
  this.boneSelectionHighlight?.update();
1345
1345
  const camera = this.camera();
@@ -1391,7 +1391,7 @@ export class Object3DDocumentSession {
1391
1391
  }
1392
1392
 
1393
1393
  /** Visible authoring draw with the editor-only native selection silhouette. */
1394
- renderViewport(deltaSeconds = 0): void {
1394
+ renderViewport(deltaSeconds = 0, interactive = false): void {
1395
1395
  this.advanceLook(deltaSeconds);
1396
1396
  this.ensureComposer();
1397
1397
  this.render((camera) => {
@@ -1407,7 +1407,7 @@ export class Object3DDocumentSession {
1407
1407
  composer.setMainScene(this.scene);
1408
1408
  composer.setMainCamera(camera);
1409
1409
  composer.render(deltaSeconds);
1410
- });
1410
+ }, interactive);
1411
1411
  }
1412
1412
 
1413
1413
  resize(width: number, height: number): void {
@@ -1904,9 +1904,11 @@ export function Object3DDocumentViewport({
1904
1904
  }
1905
1905
  };
1906
1906
  host.syncHostScene = syncHostScene;
1907
- documentSession.setBeforeRender(() => {
1907
+ documentSession.setBeforeRender((interactive) => {
1908
1908
  host.syncHostScene?.();
1909
- return source.prepareDraw?.(documentSession.camera());
1909
+ const mode = documentSession.presentation().mode;
1910
+ const navigation = interactive && ['solid', 'preview', 'clay', 'unlit', 'matcap'].includes(mode);
1911
+ return source.prepareDraw?.(documentSession.camera(), {interactive: navigation, height: renderer.domElement.height});
1910
1912
  });
1911
1913
  // This loop draws the compass and every other overlay pass over the
1912
1914
  // document's own render, so it — and only it — can serve the chrome
@@ -2007,6 +2009,7 @@ export function Object3DDocumentViewport({
2007
2009
  drawnToneMapping = renderer.toneMapping;
2008
2010
  drawnExposure = renderer.toneMappingExposure;
2009
2011
  };
2012
+ let navigationMeasurement: THREE.Quaternion | null = null;
2010
2013
  const animate = (time: number, resumed: boolean) => {
2011
2014
  if (disposed || !renderer || !viewport) return;
2012
2015
  const activeRenderer = renderer;
@@ -2032,6 +2035,11 @@ export function Object3DDocumentViewport({
2032
2035
  // scene panel uses, so anything riding the loop (a camera flight)
2033
2036
  // has final say over the pose and orbit damping never fights it.
2034
2037
  if (!chromeless) runViewportFrame(documentId, delta);
2038
+ if (navigationMeasurement) {
2039
+ const camera = documentSession.camera();
2040
+ camera.quaternion.copy(navigationMeasurement);
2041
+ camera.updateMatrixWorld(true);
2042
+ }
2035
2043
  viewport.batchedRenderer.update(delta);
2036
2044
  profiler?.endPhase('editor');
2037
2045
  if (!resumed && !mustDraw(advanced)) {
@@ -2050,7 +2058,10 @@ export function Object3DDocumentViewport({
2050
2058
  host.scene.overrideMaterial = scene.overrideMaterial;
2051
2059
  try {
2052
2060
  activeViewport.renderWithInfrastructure(() => {
2053
- documentSession.renderViewport(delta);
2061
+ // Photographs and the default frame-cost reading draw the
2062
+ // full resident scene. Only navigation may use source-owned
2063
+ // temporary geometry; the source restores it after the draw.
2064
+ documentSession.renderViewport(delta, navigationMeasurement !== null || (!resumed && !documentSession.needsFrame()));
2054
2065
  activeViewport.renderViewCube(activeRenderer);
2055
2066
  });
2056
2067
  } finally {
@@ -2102,7 +2113,7 @@ export function Object3DDocumentViewport({
2102
2113
  * resumed so it draws, run back to back and each waited out on the GPU with a one-pixel
2103
2114
  * read, which a real frame never does; the first frame warms and is not counted.
2104
2115
  */
2105
- const measureFrameCost = (frames: number): StageFrameCost => {
2116
+ const measureFrameCost = (frames: number, quality: 'full' | 'navigation' = 'full'): StageFrameCost => {
2106
2117
  if (!renderer) throw new Error('This stage has no renderer yet.');
2107
2118
  const activeRenderer = renderer;
2108
2119
  const gl = activeRenderer.getContext();
@@ -2110,17 +2121,31 @@ export function Object3DDocumentViewport({
2110
2121
  const times: number[] = [];
2111
2122
  const submitTimes: number[] = [];
2112
2123
  const completionTimes: number[] = [];
2113
- for (let index = 0; index <= frames; index++) {
2114
- const start = performance.now();
2115
- animate(start, true);
2116
- const submitted = performance.now();
2117
- gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel);
2118
- const completed = performance.now();
2119
- if (index > 0) {
2120
- times.push(completed - start);
2121
- submitTimes.push(submitted - start);
2122
- completionTimes.push(completed - submitted);
2124
+ const camera = documentSession.camera();
2125
+ const originalTurn = camera.quaternion.clone();
2126
+ const axis = new THREE.Vector3(0, 1, 0);
2127
+ try {
2128
+ for (let index = 0; index <= frames; index++) {
2129
+ // A real changed camera exercises culling, sorting and the
2130
+ // source's navigation path. Never persist this diagnostic pose.
2131
+ if (quality === 'navigation') navigationMeasurement = originalTurn.clone().multiply(
2132
+ new THREE.Quaternion().setFromAxisAngle(axis, (index + 1) * 0.0005));
2133
+ const start = performance.now();
2134
+ animate(start, true);
2135
+ const submitted = performance.now();
2136
+ gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel);
2137
+ const completed = performance.now();
2138
+ if (index > 0) {
2139
+ times.push(completed - start);
2140
+ submitTimes.push(submitted - start);
2141
+ completionTimes.push(completed - submitted);
2142
+ }
2123
2143
  }
2144
+ } finally {
2145
+ navigationMeasurement = null;
2146
+ camera.quaternion.copy(originalTurn);
2147
+ camera.updateMatrixWorld(true);
2148
+ dirty = true;
2124
2149
  }
2125
2150
  const drawn = { calls: activeRenderer.info.render.calls, triangles: activeRenderer.info.render.triangles };
2126
2151
  let meshes = 0;
@@ -2141,6 +2166,7 @@ export function Object3DDocumentViewport({
2141
2166
  const median = (values: number[]) => round(values.sort((a, b) => a - b)[Math.floor(values.length / 2)] ?? 0);
2142
2167
  return {
2143
2168
  frames,
2169
+ quality,
2144
2170
  medianMs: round(at(0.5)),
2145
2171
  p95Ms: round(at(0.95)),
2146
2172
  minMs: round(sorted[0] ?? 0),
@@ -2177,7 +2203,7 @@ export function Object3DDocumentViewport({
2177
2203
  drawCamera: () => host.session?.camera() ?? viewport.camera,
2178
2204
  orbit: viewport.orbitControls,
2179
2205
  scene: host.scene,
2180
- frameCost: (frames) => measureFrameCost(frames),
2206
+ frameCost: (frames, quality) => measureFrameCost(frames, quality),
2181
2207
  },
2182
2208
  () => null,
2183
2209
  (kind, object) => viewport.setHelper(kind, object),
@@ -193,6 +193,9 @@ export const viewportCommands: CommandContribution['commands'] = {
193
193
  const named = cmd['stage'];
194
194
  if (named !== undefined && typeof named !== 'string')
195
195
  return { ok: false, error: 'document-frame-cost: "stage" names a stage by its document id.' };
196
+ const quality = cmd['quality'] ?? 'full';
197
+ if (quality !== 'full' && quality !== 'navigation')
198
+ return {ok: false, error: 'document-frame-cost: "quality" must be "full" or "navigation".'};
196
199
  const documentId = named ?? activeWorkspaceDocumentId();
197
200
  const stages = viewportStages();
198
201
  const stage = stages.find((one) => one.documentId === documentId);
@@ -206,7 +209,7 @@ export const viewportCommands: CommandContribution['commands'] = {
206
209
  const measure = stage.rig().frameCost;
207
210
  if (!measure)
208
211
  return { ok: false, error: `document-frame-cost: the stage for ${JSON.stringify(stage.documentId)} has no frame of its own to measure.` };
209
- return { ok: true, data: { stage: stage.documentId, ...measure(frames) } };
212
+ return { ok: true, data: { stage: stage.documentId, ...measure(frames, quality) } };
210
213
  }, 120_000),
211
214
  'capture-viewport': verb(async (store, cmd) => {
212
215
  // Fresh, unthrottled on-demand capture (see EditorShellStore.captureViewportImage).
@@ -54,8 +54,10 @@ export interface ToolObject3DPreviewSource {
54
54
  update?(deltaSeconds: number): void;
55
55
  /** Prepare camera-dependent draw buffers immediately before rendering.
56
56
  * Does not advance content time or mutate the authored document. An optional
57
- * returned function restores temporary draw state, including after errors. */
58
- prepareDraw?(camera: Camera): void | (() => void);
57
+ * returned function restores temporary draw state, including after errors.
58
+ * Interactive draws may use disposable navigation geometry; omitted/false
59
+ * always means full detail (captures and default frame-cost readings). */
60
+ prepareDraw?(camera: Camera, options?: {interactive: boolean; height: number}): void | (() => void);
59
61
  /**
60
62
  * EVERY CHANGE TO WHAT THE SOURCE DRAWS, announced: the host calls
61
63
  * `listener` whenever the source's graph, materials, textures or poses may
@@ -32,7 +32,7 @@ export interface ViewportRig {
32
32
  * `requestAnimationFrame`, each waited out on the GPU, so the number is the frame's cost and
33
33
  * not the display's refresh. Absent on a stage with no frame of its own.
34
34
  */
35
- frameCost?(frames: number): StageFrameCost;
35
+ frameCost?(frames: number, quality?: 'full' | 'navigation'): StageFrameCost;
36
36
  readonly orbit: { readonly target: THREE.Vector3; enabled: boolean; update(): void };
37
37
  /** The editor's own scene (helpers live on its editor layer). */
38
38
  readonly scene: THREE.Scene;
@@ -101,4 +101,3 @@ export interface ViewportDoor {
101
101
  /** Fires whenever a stage mounts or unmounts; the return unsubscribes. */
102
102
  onStages(fn: (stages: readonly ViewportStage[]) => void): () => void;
103
103
  }
104
-