@volter/editor-threejs 0.5.92 → 0.5.94
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 +3 -3
- package/src/kit/components/Object3DDocumentToolbar.tsx +15 -0
- package/src/kit/components/StageHost.tsx +50 -0
- package/src/kit/components/ViewportFurniture.tsx +1 -1
- package/src/kit/editor-viewport.ts +5 -2
- package/src/kit/viewport-commands.ts +29 -0
- package/src/viewport-api.ts +12 -0
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.
|
|
4
|
+
"version": "0.5.94",
|
|
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.
|
|
138
|
+
"@volter/editor-project": "0.5.94",
|
|
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.
|
|
143
|
+
"@volter/editor-sdk": "0.5.94",
|
|
144
144
|
"postprocessing": "6.38.3",
|
|
145
145
|
"quarks.core": "0.16.0",
|
|
146
146
|
"@fortawesome/free-solid-svg-icons": "^7.2.0"
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
Button,
|
|
14
14
|
EditorBanner,
|
|
15
15
|
EditorIcon,
|
|
16
|
+
editorIcons,
|
|
16
17
|
EditorPopover,
|
|
17
18
|
EditorToolbar,
|
|
18
19
|
IconButton,
|
|
@@ -430,6 +431,20 @@ export function Object3DDocumentToolbar({
|
|
|
430
431
|
</Text>
|
|
431
432
|
</label>
|
|
432
433
|
</ViewportDisplayModeMenu>
|
|
434
|
+
{/* BLENDER'S SHOW GIZMO (`View3D.show_gizmo`), beside Show Overlays as Blender's header
|
|
435
|
+
has them: this stage's transform and navigation gizmos, apart from its overlays. */}
|
|
436
|
+
{stage === null ? null : (
|
|
437
|
+
<Tooltip text={`Gizmos: ${stage.showGizmos ? 'On' : 'Off'}`}>
|
|
438
|
+
<IconButton
|
|
439
|
+
aria-label="Toggle gizmos"
|
|
440
|
+
aria-pressed={stage.showGizmos}
|
|
441
|
+
size="comfortable"
|
|
442
|
+
onClick={() => stage.toggleGizmos()}
|
|
443
|
+
>
|
|
444
|
+
<EditorIcon icon={editorIcons.viewport.gizmos} size="md" />
|
|
445
|
+
</IconButton>
|
|
446
|
+
</Tooltip>
|
|
447
|
+
)}
|
|
433
448
|
{/* THE EYE WAS THE OVERLAYS CONTROL ALL ALONG — measured by what it
|
|
434
449
|
toggles, which is Grid and Bounds, exactly Blender's Show Overlays
|
|
435
450
|
subject. It takes Blender's mark, and Blender's split toggle+chevron:
|
|
@@ -80,6 +80,7 @@ import {
|
|
|
80
80
|
recordViewportFirstFrame,
|
|
81
81
|
} from '@volter/editor-sdk/kit/viewport-activation-timings';
|
|
82
82
|
import { bindViewportRig, runViewportFrame } from '../../viewport-door';
|
|
83
|
+
import type { StageFrameCost } from '../../viewport-api';
|
|
83
84
|
import {
|
|
84
85
|
notifyWorkspaceDocumentSelectionChanged,
|
|
85
86
|
openWorkspaceDocuments,
|
|
@@ -2089,6 +2090,54 @@ export function Object3DDocumentViewport({
|
|
|
2089
2090
|
};
|
|
2090
2091
|
|
|
2091
2092
|
host.frame = animate;
|
|
2093
|
+
/**
|
|
2094
|
+
* THE FRAME WITHOUT THE DISPLAY'S CAP (`ViewportRig.frameCost`): this stage's own `animate`,
|
|
2095
|
+
* resumed so it draws, run back to back and each waited out on the GPU with a one-pixel
|
|
2096
|
+
* read, which a real frame never does; the first frame warms and is not counted.
|
|
2097
|
+
*/
|
|
2098
|
+
const measureFrameCost = (frames: number): StageFrameCost => {
|
|
2099
|
+
if (!renderer) throw new Error('This stage has no renderer yet.');
|
|
2100
|
+
const activeRenderer = renderer;
|
|
2101
|
+
const gl = activeRenderer.getContext();
|
|
2102
|
+
const pixel = new Uint8Array(4);
|
|
2103
|
+
const times: number[] = [];
|
|
2104
|
+
for (let index = 0; index <= frames; index++) {
|
|
2105
|
+
const start = performance.now();
|
|
2106
|
+
animate(start, true);
|
|
2107
|
+
gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel);
|
|
2108
|
+
if (index > 0) times.push(performance.now() - start);
|
|
2109
|
+
}
|
|
2110
|
+
const drawn = { calls: activeRenderer.info.render.calls, triangles: activeRenderer.info.render.triangles };
|
|
2111
|
+
let meshes = 0;
|
|
2112
|
+
let sceneTriangles = 0;
|
|
2113
|
+
host.scene.traverseVisible((object) => {
|
|
2114
|
+
const mesh = object as THREE.Mesh;
|
|
2115
|
+
if (!mesh.isMesh || !mesh.geometry) return;
|
|
2116
|
+
meshes++;
|
|
2117
|
+
const index = mesh.geometry.getIndex();
|
|
2118
|
+
const count = index ? index.count : (mesh.geometry.getAttribute('position')?.count ?? 0);
|
|
2119
|
+
sceneTriangles += Math.floor(count / 3) * ((mesh as THREE.InstancedMesh).count ?? 1);
|
|
2120
|
+
});
|
|
2121
|
+
const sorted = [...times].sort((a, b) => a - b);
|
|
2122
|
+
const at = (q: number) => sorted[Math.min(sorted.length - 1, Math.floor(q * sorted.length))] ?? 0;
|
|
2123
|
+
const size = activeRenderer.getDrawingBufferSize(new THREE.Vector2());
|
|
2124
|
+
const ratio = activeRenderer.getPixelRatio();
|
|
2125
|
+
const round = (value: number) => Math.round(value * 100) / 100;
|
|
2126
|
+
return {
|
|
2127
|
+
frames,
|
|
2128
|
+
medianMs: round(at(0.5)),
|
|
2129
|
+
p95Ms: round(at(0.95)),
|
|
2130
|
+
minMs: round(sorted[0] ?? 0),
|
|
2131
|
+
width: size.x,
|
|
2132
|
+
height: size.y,
|
|
2133
|
+
cssWidth: round(size.x / ratio),
|
|
2134
|
+
cssHeight: round(size.y / ratio),
|
|
2135
|
+
devicePixelRatio: ratio,
|
|
2136
|
+
drawn,
|
|
2137
|
+
scene: { meshes, triangles: sceneTriangles },
|
|
2138
|
+
drawMode: viewPresentation(documentId).drawMode,
|
|
2139
|
+
};
|
|
2140
|
+
};
|
|
2092
2141
|
rendererSessionRef.current = host.rendererSession;
|
|
2093
2142
|
if (!host.initialized) {
|
|
2094
2143
|
if (!chromeless) {
|
|
@@ -2110,6 +2159,7 @@ export function Object3DDocumentViewport({
|
|
|
2110
2159
|
drawCamera: () => host.session?.camera() ?? viewport.camera,
|
|
2111
2160
|
orbit: viewport.orbitControls,
|
|
2112
2161
|
scene: host.scene,
|
|
2162
|
+
frameCost: (frames) => measureFrameCost(frames),
|
|
2113
2163
|
},
|
|
2114
2164
|
() => null,
|
|
2115
2165
|
(kind, object) => viewport.setHelper(kind, object),
|
|
@@ -472,7 +472,7 @@ export function ViewportFurniture({
|
|
|
472
472
|
header's view control and the Home and numpad-period keys, as Blender's is its View
|
|
473
473
|
menu. The Camera button is not drawn yet: looking through a scene camera is not a
|
|
474
474
|
view this stage has. */}
|
|
475
|
-
{navigationControls ? (
|
|
475
|
+
{navigationControls && store.showGizmos ? (
|
|
476
476
|
<div
|
|
477
477
|
data-testid="viewport-navigation"
|
|
478
478
|
role="toolbar"
|
|
@@ -3165,7 +3165,7 @@ export class EditorViewport {
|
|
|
3165
3165
|
// same rationale as the per-notify `applyRootHiddenVisibility` above.
|
|
3166
3166
|
if (hidden) policy.suppressRootEnvironment(this._scene);
|
|
3167
3167
|
const gizmoEpoch = this._store.gizmoEpoch;
|
|
3168
|
-
const gizmoSig = `${this._store.shell.showHelpers}|${JSON.stringify(vis)}|${hiddenRootId ?? ''}|${hidden}|${this._threejsToolContextActive}`;
|
|
3168
|
+
const gizmoSig = `${this._store.shell.showHelpers}|${this._store.shell.showGizmos}|${JSON.stringify(vis)}|${hiddenRootId ?? ''}|${hidden}|${this._threejsToolContextActive}`;
|
|
3169
3169
|
const mapChanged = this._objectMap !== this._lastGizmoObjectMap;
|
|
3170
3170
|
const settingsChanged = gizmoSig !== this._lastGizmoSig;
|
|
3171
3171
|
const membershipChanges = mapChanged
|
|
@@ -3261,7 +3261,8 @@ export class EditorViewport {
|
|
|
3261
3261
|
? anyChannelWritable(selectedId)
|
|
3262
3262
|
: (authoring.transforms?.editability?.(selectedId, activeTransformChannel).writable ?? true)
|
|
3263
3263
|
: false;
|
|
3264
|
-
|
|
3264
|
+
// Blender's Show Gizmo off draws no transform gizmo, and the selection stays as it is.
|
|
3265
|
+
if (selectedId && selectedTransformWritable && this._store.shell.showGizmos) {
|
|
3265
3266
|
if (!this._anyGizmoDragging()) {
|
|
3266
3267
|
if (this._store.shell.pivotMode === 'median-point' && this._store.shell.selectedEntityIds.size > 1) {
|
|
3267
3268
|
const median = new THREE.Vector3();
|
|
@@ -3982,6 +3983,7 @@ export class EditorViewport {
|
|
|
3982
3983
|
renderViewCube(renderer: THREE.WebGLRenderer): void {
|
|
3983
3984
|
if (!this._threeSurfaceShowing) return; // no three stage, no compass (Blender's is persistent on one)
|
|
3984
3985
|
if (this._navigation === 'hidden') return;
|
|
3986
|
+
if (!this._store.shell.showGizmos) return; // the stage's Show Gizmo is off
|
|
3985
3987
|
if (this._cameraView) return; // exact camera view owns the viewport
|
|
3986
3988
|
const w = renderer.domElement.clientWidth;
|
|
3987
3989
|
const h = renderer.domElement.clientHeight;
|
|
@@ -4859,6 +4861,7 @@ export class EditorViewport {
|
|
|
4859
4861
|
private _isOverViewCube(clientX: number, clientY: number): boolean {
|
|
4860
4862
|
// An indicator is only drawn, and a triad has nothing to click: clicks pass to the stage.
|
|
4861
4863
|
if (this._navigation !== 'interactive' || this._gizmoLook.navigationForm === 'triad') return false;
|
|
4864
|
+
if (!this._store.shell.showGizmos) return false;
|
|
4862
4865
|
const rect = this._canvas.getBoundingClientRect();
|
|
4863
4866
|
const x = clientX - rect.left;
|
|
4864
4867
|
const y = clientY - rect.top;
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
viewPresentationBinding,
|
|
32
32
|
} from '@volter/editor-sdk/kit/viewport-presentation';
|
|
33
33
|
import { threeStoreForHost } from './three-state';
|
|
34
|
+
import { viewportStages } from '../viewport-door';
|
|
34
35
|
import { threeStateOf } from './three-state';
|
|
35
36
|
|
|
36
37
|
function activeObject3DDocumentSession() {
|
|
@@ -179,6 +180,34 @@ export const viewportCommands: CommandContribution['commands'] = {
|
|
|
179
180
|
}
|
|
180
181
|
return { ok: true, data: { ...documentSession.cameraPose() } };
|
|
181
182
|
}),
|
|
183
|
+
/**
|
|
184
|
+
* WHAT A FRAME OF A 3D DOCUMENT'S STAGE COSTS WHEN NOTHING CAPS IT (`ViewportRig.frameCost`):
|
|
185
|
+
* the active document's stage, or the one `stage` names by its id (a split's second area is
|
|
186
|
+
* `<documentId>#area-2`). The frame is the stage's own, so a draw mode, a split or a scene
|
|
187
|
+
* changes it the way it changes what a person sees; the reading says which it measured.
|
|
188
|
+
*/
|
|
189
|
+
'document-frame-cost': verb((_store, cmd) => {
|
|
190
|
+
const frames = cmd['frames'] ?? 60;
|
|
191
|
+
if (typeof frames !== 'number' || !Number.isInteger(frames) || frames < 1 || frames > 600)
|
|
192
|
+
return { ok: false, error: 'document-frame-cost: "frames" must be a whole number in 1..600.' };
|
|
193
|
+
const named = cmd['stage'];
|
|
194
|
+
if (named !== undefined && typeof named !== 'string')
|
|
195
|
+
return { ok: false, error: 'document-frame-cost: "stage" names a stage by its document id.' };
|
|
196
|
+
const documentId = named ?? activeWorkspaceDocumentId();
|
|
197
|
+
const stages = viewportStages();
|
|
198
|
+
const stage = stages.find((one) => one.documentId === documentId);
|
|
199
|
+
if (!stage)
|
|
200
|
+
return {
|
|
201
|
+
ok: false,
|
|
202
|
+
error:
|
|
203
|
+
`document-frame-cost: no 3D stage draws ${documentId === null ? 'the active document' : JSON.stringify(documentId)}. ` +
|
|
204
|
+
`The mounted stages are: ${stages.map((one) => one.documentId).join(', ') || 'none'}.`,
|
|
205
|
+
};
|
|
206
|
+
const measure = stage.rig().frameCost;
|
|
207
|
+
if (!measure)
|
|
208
|
+
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) } };
|
|
210
|
+
}, 120_000),
|
|
182
211
|
'capture-viewport': verb(async (store, cmd) => {
|
|
183
212
|
// Fresh, unthrottled on-demand capture (see EditorShellStore.captureViewportImage).
|
|
184
213
|
const requested = captureSizeFromCommand(cmd);
|
package/src/viewport-api.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { MountedRoot } from '@volter/editor-project/adapter/root-adapter';
|
|
8
8
|
import type * as THREE from 'three';
|
|
9
|
+
import type { StageFrameCostReading } from '@volter/editor-sdk';
|
|
9
10
|
|
|
10
11
|
/** A live instance's roots, presented as the authored viewport's subject. */
|
|
11
12
|
export interface ViewportPresentation {
|
|
@@ -15,12 +16,23 @@ export interface ViewportPresentation {
|
|
|
15
16
|
dispose(): void;
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
/** What a frame of a stage costs, uncapped ({@link ViewportRig.frameCost}): the wire's reading
|
|
20
|
+
* without the stage's id, which the door adds. */
|
|
21
|
+
export type StageFrameCost = Omit<StageFrameCostReading, 'stage'>;
|
|
22
|
+
|
|
18
23
|
/** The authored viewport's camera rig, while a viewport is mounted. */
|
|
19
24
|
export interface ViewportRig {
|
|
20
25
|
readonly camera: THREE.PerspectiveCamera;
|
|
21
26
|
/** The camera the stage draws with NOW: {@link camera}, or the orthographic one in an
|
|
22
27
|
* orthographic view. What a document lighting or picking in step with the picture needs. */
|
|
23
28
|
drawCamera(): THREE.Camera;
|
|
29
|
+
/**
|
|
30
|
+
* WHAT A FRAME OF THIS STAGE COSTS WHEN NOTHING CAPS IT: the stage's own frame (its update,
|
|
31
|
+
* its hooks, its render and view cube) run `frames` times back to back outside
|
|
32
|
+
* `requestAnimationFrame`, each waited out on the GPU, so the number is the frame's cost and
|
|
33
|
+
* not the display's refresh. Absent on a stage with no frame of its own.
|
|
34
|
+
*/
|
|
35
|
+
frameCost?(frames: number): StageFrameCost;
|
|
24
36
|
readonly orbit: { readonly target: THREE.Vector3; enabled: boolean; update(): void };
|
|
25
37
|
/** The editor's own scene (helpers live on its editor layer). */
|
|
26
38
|
readonly scene: THREE.Scene;
|