@volter/editor-threejs 0.5.65 → 0.5.67
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/NOTICE +2 -0
- package/contributions/animation-mixers.service.ts +20 -0
- package/contributions/animation-timeline.utility.tsx +44 -0
- package/contributions/three-integration.service.ts +13 -0
- package/dist-node/serving.mjs +405 -0
- package/package.json +113 -5
- package/serving/animation-live-module.ts +70 -0
- package/serving/animation-stamp.ts +88 -0
- package/serving/index.ts +14 -0
- package/serving/model-import-conversion.ts +344 -0
- package/src/adapter/ingest/scene-capture.ts +1 -23
- package/src/adapter/renderer-config.ts +3 -4
- package/src/adapter/three-contract.ts +72 -0
- package/src/animation/live-mixers.ts +55 -0
- package/src/ecs/object-marks.ts +1 -1
- package/src/ecs/user-data.ts +0 -16
- package/src/host-hierarchy-objects.ts +31 -0
- package/src/kit/animation/three-clips-subject.ts +190 -0
- package/src/kit/asset-compare.ts +294 -0
- package/src/kit/asset-preview-command.ts +265 -0
- package/src/kit/asset-preview-framing.ts +357 -0
- package/src/kit/asset-preview.ts +2802 -0
- package/src/kit/asset-workflow/model-inspection.ts +830 -0
- package/src/kit/authoring/component-instance-root.ts +171 -0
- package/src/kit/authoring/design-time-settle.ts +343 -0
- package/src/kit/authoring/live-object-transform.ts +62 -0
- package/src/kit/authoring/object3d-document-session-registry.ts +154 -0
- package/src/kit/authoring/object3d-document-session.ts +1965 -0
- package/src/kit/authoring/object3d-gesture-controller.ts +113 -0
- package/src/kit/authoring/quarks-particle-systems.ts +19 -0
- package/src/kit/authoring/shell-viewport-policy.ts +48 -0
- package/src/kit/authoring/source-object3d-authoring-adapter.ts +526 -0
- package/src/kit/authoring/three-projection-core.ts +226 -0
- package/src/kit/authoring/viewport-pick-context.ts +39 -0
- package/src/kit/authoring/viewport-raycast.ts +240 -0
- package/src/kit/authoring/world-hidden-viewport.ts +95 -0
- package/src/kit/camera-authoring.ts +175 -0
- package/src/kit/components/CameraInfo.tsx +56 -0
- package/src/kit/components/InspectorObjectPreview.tsx +57 -0
- package/src/kit/components/Object3DDocumentToolbar.tsx +549 -0
- package/src/kit/components/Object3DDocumentViewport.tsx +58 -0
- package/src/kit/components/StageHost.tsx +2547 -0
- package/src/kit/components/StageOverlays.tsx +21 -0
- package/src/kit/components/StatsOverlay.tsx +78 -0
- package/src/kit/components/ToolObject3DPreview.tsx +39 -0
- package/src/kit/components/ViewportFurniture.tsx +655 -0
- package/src/kit/components/ViewportOverlay.tsx +215 -0
- package/src/kit/components/ViewportShadingMenu.tsx +340 -0
- package/src/kit/components/ViewportViewMenu.tsx +155 -0
- package/src/kit/components/asset-viewers/EntityModelDocument.tsx +121 -0
- package/src/kit/components/asset-viewers/EnvironmentAssetDocument.tsx +440 -0
- package/src/kit/components/asset-viewers/LiveModuleDocument.tsx +395 -0
- package/src/kit/components/asset-viewers/LutAssetDocument.tsx +444 -0
- package/src/kit/components/asset-viewers/ModelAssetDocument.tsx +105 -0
- package/src/kit/components/asset-viewers/Object3DPreview.tsx +356 -0
- package/src/kit/components/asset-viewers/QuarksAssetDocument.tsx +527 -0
- package/src/kit/components/asset-viewers/ShaderAssetDocument.tsx +743 -0
- package/src/kit/components/asset-viewers/three-asset-viewers.tsx +132 -0
- package/src/kit/components/object3d-contribution-surfaces.tsx +33 -0
- package/src/kit/components/stage-keyboard.tsx +40 -0
- package/src/kit/components/stage-overlay-set.tsx +105 -0
- package/src/kit/components/stage-presence-markers.ts +482 -0
- package/src/kit/components/stage-transform-chrome.ts +30 -0
- package/src/kit/components/stage-transform-tools.tsx +73 -0
- package/src/kit/components/stage-view-name.ts +30 -0
- package/src/kit/components/standard-viewport-dressing.ts +1042 -0
- package/src/kit/components/world-root-binding.ts +64 -0
- package/src/kit/constraint-helper.ts +338 -0
- package/src/kit/editor-shell-store.ts +814 -0
- package/src/kit/editor-viewport.ts +6621 -0
- package/src/kit/entity-lod.ts +31 -0
- package/src/kit/entity-object.ts +92 -0
- package/src/kit/hierarchy-mark-reader.ts +74 -0
- package/src/kit/instanced-presentation.ts +164 -0
- package/src/kit/live-module-source.ts +230 -0
- package/src/kit/model-thumbnail.ts +539 -0
- package/src/kit/play-camera-flight.ts +300 -0
- package/src/kit/projection/three.ts +898 -0
- package/src/kit/reflection-probe-helper.ts +142 -0
- package/src/kit/scene-document-viewport.ts +51 -0
- package/src/kit/scene-framing.ts +315 -0
- package/src/kit/scene-view-fog.ts +89 -0
- package/src/kit/spatial-handle-visuals.ts +332 -0
- package/src/kit/stories/three-story-model.ts +66 -0
- package/src/kit/three-canvas-render.ts +44 -0
- package/src/kit/three-hierarchy-row-media.ts +26 -0
- package/src/kit/three-inspection-media.ts +73 -0
- package/src/kit/three-integration.ts +86 -0
- package/src/kit/three-state.ts +33 -0
- package/src/kit/three-viewport/bone-selection-highlight.ts +119 -0
- package/src/kit/three-viewport/camera-fit.ts +41 -0
- package/src/kit/three-viewport/interactive-renderer.ts +132 -0
- package/src/kit/three-viewport/selection-brackets.ts +355 -0
- package/src/kit/three-viewport/selection-outline.ts +333 -0
- package/src/kit/three-viewport/skeleton-helper.ts +61 -0
- package/src/kit/three-viewport/source-color.ts +197 -0
- package/src/kit/three-viewport/studio-environment.ts +96 -0
- package/src/kit/trigger-volume-helper.ts +116 -0
- package/src/kit/viewport-actions.ts +128 -0
- package/src/kit/viewport-authoring-policy.ts +154 -0
- package/src/kit/viewport-commands.ts +318 -0
- package/src/kit/viewport-hotkeys.ts +119 -0
- package/src/kit/viewport-shading-boundary.ts +12 -0
- package/src/kit/viewport-status-facet.ts +53 -0
- package/src/object3d-contributions.ts +494 -0
- package/src/render/viewport-shading.ts +6 -2
- package/src/viewport/content-bounds.ts +38 -4
- package/src/viewport/environment.ts +16 -0
- package/src/viewport-api.ts +92 -0
- package/src/viewport-door.ts +237 -0
- package/src/animation/animation-clock.ts +0 -479
- package/src/animation/runtime-inspection.ts +0 -45
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE THREE VIEWPORT'S RELAY VERBS — framing, the camera, view presets, the
|
|
3
|
+
* open Object3D document's orbit/turntable/frame, the viewport photograph and
|
|
4
|
+
* the display and transform toggles. They are the Three viewport's, so they
|
|
5
|
+
* reach the relay as a command contribution (`@volter/editor-sdk/commands`,
|
|
6
|
+
* `command-registry.ts`) rather than as rows of the kit's own table: a
|
|
7
|
+
* composition with no Three viewport has none of them. Each budget travelled
|
|
8
|
+
* with its verb from `command-table.ts`.
|
|
9
|
+
*/
|
|
10
|
+
import type {
|
|
11
|
+
CommandContribution,
|
|
12
|
+
CommandDerivedRefresh,
|
|
13
|
+
CommandSpec,
|
|
14
|
+
EditorCommandMessage,
|
|
15
|
+
EditorCommandResult,
|
|
16
|
+
} from '@volter/editor-sdk/commands';
|
|
17
|
+
import { commandLine } from '@volter/editor-sdk/kit/product-command';
|
|
18
|
+
import { activeWorkspaceDocumentId } from '@volter/editor-sdk/kit/workspace-document-registry';
|
|
19
|
+
import { object3DDocumentSession } from './authoring/object3d-document-session-registry';
|
|
20
|
+
import { activeDocumentAuthoring } from '@volter/editor-sdk/kit/authoring/shell-document-ops';
|
|
21
|
+
import { handleAssetPreviewCommand } from './asset-preview-command';
|
|
22
|
+
import { captureSizeFromCommand } from '@volter/editor-sdk/kit/capture-size';
|
|
23
|
+
import type { EditorShellStore, HelperVisibility } from './editor-shell-store';
|
|
24
|
+
import { editorHost } from '@volter/editor-sdk/host';
|
|
25
|
+
import { entityObject3D } from './entity-object';
|
|
26
|
+
import { focusedStageStore } from '@volter/editor-sdk/kit/stage-context';
|
|
27
|
+
import {
|
|
28
|
+
setViewGridVisible,
|
|
29
|
+
startingPresentation,
|
|
30
|
+
viewPresentationBinding,
|
|
31
|
+
} from '@volter/editor-sdk/kit/viewport-presentation';
|
|
32
|
+
import { threeStoreForHost } from './three-state';
|
|
33
|
+
import { threeStateOf } from './three-state';
|
|
34
|
+
|
|
35
|
+
function activeObject3DDocumentSession() {
|
|
36
|
+
const documentId = activeWorkspaceDocumentId();
|
|
37
|
+
return documentId ? object3DDocumentSession(documentId) : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** One verb over the session's Three store. */
|
|
41
|
+
function verb(
|
|
42
|
+
run: (store: EditorShellStore, cmd: EditorCommandMessage) => EditorCommandResult | Promise<EditorCommandResult>,
|
|
43
|
+
timeoutMs?: number,
|
|
44
|
+
derivedRefresh: CommandDerivedRefresh = 'none',
|
|
45
|
+
): CommandSpec {
|
|
46
|
+
return {
|
|
47
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
48
|
+
derivedRefresh,
|
|
49
|
+
handle: (cmd) => {
|
|
50
|
+
const store = threeStoreForHost();
|
|
51
|
+
if (!store) return { ok: false, error: `${cmd.type} needs an editor session; none has opened yet.` };
|
|
52
|
+
return run(store, cmd);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const OK: EditorCommandResult = { ok: true };
|
|
58
|
+
|
|
59
|
+
type ShadingMode = Parameters<EditorShellStore['setShadingMode']>[0];
|
|
60
|
+
|
|
61
|
+
export const viewportCommands: CommandContribution['commands'] = {
|
|
62
|
+
'focus-entity': verb((store, cmd) => {
|
|
63
|
+
if (!activeObject3DDocumentSession()?.frameIds([cmd['id'] as string])) {
|
|
64
|
+
store.shell.focusOnEntity(cmd['id'] as string);
|
|
65
|
+
}
|
|
66
|
+
return OK;
|
|
67
|
+
}),
|
|
68
|
+
// The STRICT entity-targeted sibling of `focus-entity`: the same edit
|
|
69
|
+
// viewport framing, but an id nothing in the scene answers to is a named
|
|
70
|
+
// refusal rather than the silent no-op `focus-entity` keeps. That matters for
|
|
71
|
+
// a scripted flow that frames an entity and then photographs it: framing that
|
|
72
|
+
// quietly did nothing hands back a confident picture of whatever the camera
|
|
73
|
+
// happened to be on.
|
|
74
|
+
'frame-entity': verb((store, cmd) => {
|
|
75
|
+
const id = cmd['id'];
|
|
76
|
+
if (typeof id !== 'string' || id.length === 0) {
|
|
77
|
+
return { ok: false, error: 'frame-entity requires a string "id" (the entity to frame).' };
|
|
78
|
+
}
|
|
79
|
+
const documentSession = activeObject3DDocumentSession();
|
|
80
|
+
if (documentSession) {
|
|
81
|
+
if (!documentSession.frameIds([id])) {
|
|
82
|
+
return { ok: false, error: `Entity not found: ${id}`, data: { code: 'ENTITY_NOT_FOUND' } };
|
|
83
|
+
}
|
|
84
|
+
return OK;
|
|
85
|
+
}
|
|
86
|
+
// A Canvas Scene has no Object3D by design. Its native framing answer is
|
|
87
|
+
// the adapter-owned screen rect consumed by CanvasSceneControls, so strict
|
|
88
|
+
// existence is the owned hierarchy node plus the rect capability. Resolved
|
|
89
|
+
// against the active document just like Hierarchy/Inspector do.
|
|
90
|
+
const activeAdapter = activeDocumentAuthoring(store.shell);
|
|
91
|
+
if (activeAdapter.hierarchy.node(id) !== null && activeAdapter.rects) {
|
|
92
|
+
store.shell.focusOnEntity(id);
|
|
93
|
+
return OK;
|
|
94
|
+
}
|
|
95
|
+
// The SAME resolver the framing itself uses (`entity-object.ts`): an
|
|
96
|
+
// adopted play scene's nodes are the adapter's, not the shell map's.
|
|
97
|
+
if (!entityObject3D(activeAdapter, store.objectMap, id)) {
|
|
98
|
+
return { ok: false, error: `Entity not found: ${id}`, data: { code: 'ENTITY_NOT_FOUND' } };
|
|
99
|
+
}
|
|
100
|
+
store.shell.focusOnEntity(id);
|
|
101
|
+
return OK;
|
|
102
|
+
}),
|
|
103
|
+
'focus-selection': verb((store) => {
|
|
104
|
+
if (!activeObject3DDocumentSession()?.frameSelection()) store.shell.focusOnSelection();
|
|
105
|
+
return OK;
|
|
106
|
+
}),
|
|
107
|
+
'view-preset': verb((store, cmd) => {
|
|
108
|
+
// An absent or unknown preset is refused BY NAME: unguarded it reached
|
|
109
|
+
// `cameraPresetDirection` and threw a TypeError that named neither the
|
|
110
|
+
// command nor the vocabulary (measured live, 2026-09-18).
|
|
111
|
+
const requested = cmd['preset'];
|
|
112
|
+
const presets = ['top', 'front', 'right', 'perspective'] as const;
|
|
113
|
+
if (typeof requested !== 'string' || !(presets as readonly string[]).includes(requested)) {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
error: `view-preset requires one of ${presets.join(', ')}, got ${
|
|
117
|
+
requested === undefined ? 'nothing' : JSON.stringify(requested)
|
|
118
|
+
}.`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const preset = requested as (typeof presets)[number];
|
|
122
|
+
const documentSession = activeObject3DDocumentSession();
|
|
123
|
+
if (documentSession) {
|
|
124
|
+
documentSession.setViewPreset(preset === 'perspective' ? 'isometric' : preset);
|
|
125
|
+
} else store.shell.setViewPreset(preset);
|
|
126
|
+
return OK;
|
|
127
|
+
}),
|
|
128
|
+
'set-camera': verb((store, cmd) => {
|
|
129
|
+
const position = cmd['position'] as { x: number; y: number; z: number } | undefined;
|
|
130
|
+
const target = cmd['target'] as { x: number; y: number; z: number } | undefined;
|
|
131
|
+
const fov = cmd['fov'] as number | undefined;
|
|
132
|
+
if (
|
|
133
|
+
!position ||
|
|
134
|
+
!target ||
|
|
135
|
+
typeof position.x !== 'number' ||
|
|
136
|
+
typeof position.y !== 'number' ||
|
|
137
|
+
typeof position.z !== 'number' ||
|
|
138
|
+
typeof target.x !== 'number' ||
|
|
139
|
+
typeof target.y !== 'number' ||
|
|
140
|
+
typeof target.z !== 'number'
|
|
141
|
+
) {
|
|
142
|
+
return { ok: false, error: 'set-camera requires numeric {x,y,z} position and target.' };
|
|
143
|
+
}
|
|
144
|
+
const documentSession = activeObject3DDocumentSession();
|
|
145
|
+
if (documentSession) documentSession.setCameraPose(position, target, fov);
|
|
146
|
+
else store.shell.setCameraPose(position, target, fov);
|
|
147
|
+
return OK;
|
|
148
|
+
}),
|
|
149
|
+
// THE AGENT'S LOOKING, AS A WATCHABLE ACT. These drive the OPEN Object3D
|
|
150
|
+
// document's own camera — the one the human is looking through — and
|
|
151
|
+
// orbit/turntable ack only when the animated move ends, so their budget is
|
|
152
|
+
// the move's own wall clock. Both refuse a length past 60 s themselves. There
|
|
153
|
+
// is no Scene fallback: the Scene viewport's camera answers to
|
|
154
|
+
// `view-preset`/`set-camera` and has no framed subject to circle.
|
|
155
|
+
'document-orbit': verb((_store, cmd) => documentMove(cmd), 120_000),
|
|
156
|
+
'document-turntable': verb((_store, cmd) => documentMove(cmd), 120_000),
|
|
157
|
+
'document-frame': verb((_store, cmd) => {
|
|
158
|
+
const documentSession = activeObject3DDocumentSession();
|
|
159
|
+
if (!documentSession) {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
error:
|
|
163
|
+
'document-frame needs an Object3D document open and active. For the Scene ' +
|
|
164
|
+
'viewport use `frame-entity`/`focus-selection`.',
|
|
165
|
+
data: { code: 'NO_ACTIVE_OBJECT3D_DOCUMENT' },
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const fit = cmd['fit'];
|
|
169
|
+
if (fit !== undefined && (typeof fit !== 'number' || !(fit >= 0.1 && fit <= 10))) {
|
|
170
|
+
return { ok: false, error: 'document-frame: "fit" must be a number in 0.1..10.' };
|
|
171
|
+
}
|
|
172
|
+
if (!documentSession.frame(typeof fit === 'number' ? fit : 1)) {
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
error: 'Nothing to frame: the document subject has no measurable bounds.',
|
|
176
|
+
data: { code: 'EMPTY_FRAME_BOUNDS' },
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return { ok: true, data: { ...documentSession.cameraPose() } };
|
|
180
|
+
}),
|
|
181
|
+
'capture-viewport': verb(async (store, cmd) => {
|
|
182
|
+
// Fresh, unthrottled on-demand capture (see EditorShellStore.captureViewportImage).
|
|
183
|
+
const requested = captureSizeFromCommand(cmd);
|
|
184
|
+
if ('error' in requested) return { ok: false, error: requested.error };
|
|
185
|
+
// AN ADOPTED SCENE IS PRESENTED BY ITS ADOPTER. The store's renderer and
|
|
186
|
+
// camera are its own viewport's; re-rendering a scene another document
|
|
187
|
+
// adopted through them came back white.
|
|
188
|
+
if (store.hasAdoptedScene) {
|
|
189
|
+
try {
|
|
190
|
+
const capture = await editorHost().documents.captureActive(requested.size);
|
|
191
|
+
return { ok: true, data: { base64: capture.base64, mimeType: capture.mimeType } };
|
|
192
|
+
} catch (error) {
|
|
193
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const dataUrl = store.captureViewportImage(requested.size);
|
|
197
|
+
if (!dataUrl) {
|
|
198
|
+
// The refusal names the MECHANISM and the door that does answer: this door
|
|
199
|
+
// photographs the editor's own three.js viewport, which a canvas-surface
|
|
200
|
+
// world never binds.
|
|
201
|
+
return {
|
|
202
|
+
ok: false,
|
|
203
|
+
error:
|
|
204
|
+
"This door photographs the editor's own three.js Scene viewport, and nothing has bound one " +
|
|
205
|
+
'(no renderer/scene/camera). A canvas-surface world (a first-party canvas root, or a ' +
|
|
206
|
+
'PixiJS/Phaser/Babylon ingest) never binds it — it draws on its own canvas in the Game ' +
|
|
207
|
+
'document. Capture that through `capture-active-document` (`editor.captureActiveDocument()`) ' +
|
|
208
|
+
`or the running game through \`bridge-screenshot\` (${commandLine('screenshot')}).`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const comma = dataUrl.indexOf(',');
|
|
212
|
+
const base64 = comma >= 0 ? dataUrl.slice(comma + 1) : dataUrl;
|
|
213
|
+
return { ok: true, data: { base64, mimeType: 'image/png' } };
|
|
214
|
+
}, undefined, 'if-content-changed'),
|
|
215
|
+
// The Asset Lab's photographs; the budget covers a multi-shot set's renders.
|
|
216
|
+
'capture-asset-preview': verb((store, cmd) => handleAssetPreviewCommand(store, cmd), 30_000, 'if-content-changed'),
|
|
217
|
+
// Display — set semantics (only toggle when the value differs).
|
|
218
|
+
'set-grid': verb((_store, cmd) => {
|
|
219
|
+
// The ACTIVE view's grid switch (`kit/viewport-presentation`), one per view.
|
|
220
|
+
const viewId = activeWorkspaceDocumentId();
|
|
221
|
+
if (!viewId || !viewPresentationBinding(viewId)) {
|
|
222
|
+
return { ok: false, error: 'set-grid: the active document draws no grid.' };
|
|
223
|
+
}
|
|
224
|
+
setViewGridVisible(viewId, cmd['enabled'] as boolean);
|
|
225
|
+
return OK;
|
|
226
|
+
}),
|
|
227
|
+
// Helpers and the stats tile are per-STAGE view options, read by the focused
|
|
228
|
+
// stage's own viewport and its overlay set — a verb that wrote the shell's
|
|
229
|
+
// copy while the person looked at a model document would toggle nothing.
|
|
230
|
+
'set-helpers': verb((store, cmd) => {
|
|
231
|
+
const stage = focusedStageStore(store.shell);
|
|
232
|
+
if (stage.showHelpers !== (cmd['enabled'] as boolean)) stage.toggleHelpers();
|
|
233
|
+
return OK;
|
|
234
|
+
}),
|
|
235
|
+
'set-stats': verb((store, cmd) => {
|
|
236
|
+
const stage = focusedStageStore(store.shell);
|
|
237
|
+
if (threeStateOf(stage).showStats !== (cmd['enabled'] as boolean)) threeStateOf(stage).toggleStats();
|
|
238
|
+
return OK;
|
|
239
|
+
}),
|
|
240
|
+
'set-shading-mode': verb((store, cmd) => {
|
|
241
|
+
const documentSession = activeObject3DDocumentSession();
|
|
242
|
+
// `preview` and `rendered` are lit as a stage says (Blender's Material Preview and Rendered);
|
|
243
|
+
// where no stage says, they would draw as Solid under another name.
|
|
244
|
+
const mode = cmd['mode'];
|
|
245
|
+
if (mode === 'preview' || mode === 'rendered') {
|
|
246
|
+
const documentId = activeWorkspaceDocumentId();
|
|
247
|
+
const stageKind = documentId ? viewPresentationBinding(documentId)?.stageKind : undefined;
|
|
248
|
+
if (!documentSession || !stageKind || startingPresentation(stageKind)?.modes?.[mode]?.lighting === undefined) {
|
|
249
|
+
return {
|
|
250
|
+
ok: false,
|
|
251
|
+
error: `Shading mode "${mode}" is lit as a stage says, and the active document's stage says nothing for it.`,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (documentSession) documentSession.setMode(cmd['mode'] as ShadingMode);
|
|
256
|
+
else store.setShadingMode(cmd['mode'] as ShadingMode);
|
|
257
|
+
return OK;
|
|
258
|
+
}),
|
|
259
|
+
'set-helper-type': verb((store, cmd) => {
|
|
260
|
+
const stage = focusedStageStore(store.shell);
|
|
261
|
+
const helperType = cmd['helperType'] as keyof HelperVisibility;
|
|
262
|
+
const documentSession = activeObject3DDocumentSession();
|
|
263
|
+
if (documentSession && helperType === 'bounds') {
|
|
264
|
+
documentSession.setBounds(cmd['enabled'] as boolean);
|
|
265
|
+
return OK;
|
|
266
|
+
}
|
|
267
|
+
if (documentSession && helperType === 'skeletons') {
|
|
268
|
+
documentSession.setSkeleton(cmd['enabled'] as boolean);
|
|
269
|
+
return OK;
|
|
270
|
+
}
|
|
271
|
+
if (stage.helperVisibility[helperType] !== (cmd['enabled'] as boolean)) {
|
|
272
|
+
stage.toggleHelperType(helperType);
|
|
273
|
+
}
|
|
274
|
+
return OK;
|
|
275
|
+
}),
|
|
276
|
+
// Transform tools — set semantics.
|
|
277
|
+
'set-transform-mode': verb((store, cmd) => {
|
|
278
|
+
store.shell.setTransformMode(cmd['mode'] as 'combined' | 'translate' | 'rotate' | 'scale');
|
|
279
|
+
return OK;
|
|
280
|
+
}),
|
|
281
|
+
'set-transform-space': verb((store, cmd) => {
|
|
282
|
+
store.shell.setTransformSpace(cmd['space'] as 'world' | 'local');
|
|
283
|
+
return OK;
|
|
284
|
+
}),
|
|
285
|
+
'set-snap': verb((store, cmd) => {
|
|
286
|
+
if (store.shell.snapEnabled !== (cmd['enabled'] as boolean)) store.shell.toggleSnap();
|
|
287
|
+
return OK;
|
|
288
|
+
}),
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
async function documentMove(cmd: EditorCommandMessage): Promise<EditorCommandResult> {
|
|
292
|
+
const documentSession = activeObject3DDocumentSession();
|
|
293
|
+
if (!documentSession) {
|
|
294
|
+
return {
|
|
295
|
+
ok: false,
|
|
296
|
+
error:
|
|
297
|
+
`${cmd.type} needs an Object3D document open and active (a model, a live ` +
|
|
298
|
+
'module, an entity model). Open one with `editor.openAsset(<path>)` first.',
|
|
299
|
+
data: { code: 'NO_ACTIVE_OBJECT3D_DOCUMENT' },
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
const seconds = cmd.type === 'document-orbit' ? cmd['duration'] : cmd['seconds'];
|
|
303
|
+
if (seconds !== undefined && (typeof seconds !== 'number' || !(seconds >= 0 && seconds <= 60))) {
|
|
304
|
+
return { ok: false, error: `${cmd.type}: the move's length must be a number of seconds in 0..60.` };
|
|
305
|
+
}
|
|
306
|
+
const outcome =
|
|
307
|
+
cmd.type === 'document-orbit'
|
|
308
|
+
? await documentSession.orbit({
|
|
309
|
+
...(typeof cmd['azimuth'] === 'number' ? { azimuth: cmd['azimuth'] } : {}),
|
|
310
|
+
...(typeof cmd['elevation'] === 'number' ? { elevation: cmd['elevation'] } : {}),
|
|
311
|
+
...(typeof seconds === 'number' ? { duration: seconds } : {}),
|
|
312
|
+
})
|
|
313
|
+
: await documentSession.turntable({
|
|
314
|
+
...(typeof seconds === 'number' ? { seconds } : {}),
|
|
315
|
+
...(typeof cmd['revolutions'] === 'number' ? { revolutions: cmd['revolutions'] } : {}),
|
|
316
|
+
});
|
|
317
|
+
return { ok: true, data: { ...outcome } };
|
|
318
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE THREE VIEWPORT'S KEYBOARD ACTIONS (the gizmo-mode trio, snap, pivot, the
|
|
3
|
+
* numpad views, vertex snap). Only a three stage has an `EditorViewport`, so the
|
|
4
|
+
* stage binds these while it is mounted (`components/stage-keyboard.tsx`). They
|
|
5
|
+
* reach the keyboard through the host door (`host.keyboard`): the viewport says
|
|
6
|
+
* what each action does, and the active KEYMAP decides which keys land on it —
|
|
7
|
+
* W/E/R under `vgai`, G/R/S under `blender`.
|
|
8
|
+
*/
|
|
9
|
+
import { editorHost } from '@volter/editor-sdk/host';
|
|
10
|
+
import { getActiveScope, installHotkeys, setActiveScope } from '@volter/editor-sdk/kit/hotkeys';
|
|
11
|
+
import { showTransientHint } from '@volter/editor-sdk/kit/transient-hint';
|
|
12
|
+
import type { EditorShellStore, TransformMode } from './editor-shell-store';
|
|
13
|
+
import type { EditorViewport } from './editor-viewport';
|
|
14
|
+
import { requestTransformMode } from '@volter/editor-sdk/kit/transform-mode-request';
|
|
15
|
+
|
|
16
|
+
export function registerViewportHotkeys(
|
|
17
|
+
store: EditorShellStore,
|
|
18
|
+
viewport: EditorViewport,
|
|
19
|
+
canvas: HTMLCanvasElement,
|
|
20
|
+
): () => void {
|
|
21
|
+
const { keyboard } = editorHost();
|
|
22
|
+
const transformMode = (mode: TransformMode) => () => {
|
|
23
|
+
if (!viewport.isFlying) requestTransformMode(store.shell, mode);
|
|
24
|
+
};
|
|
25
|
+
const unbind = keyboard.bindActions([
|
|
26
|
+
{ id: 'transform.select', scope: 'stage', run: transformMode('select') },
|
|
27
|
+
{ id: 'transform.combined', scope: 'stage', run: transformMode('combined') },
|
|
28
|
+
{ id: 'transform.translate', scope: 'stage', run: transformMode('translate') },
|
|
29
|
+
{ id: 'transform.rotate', scope: 'stage', run: transformMode('rotate') },
|
|
30
|
+
{ id: 'transform.scale', scope: 'stage', run: transformMode('scale') },
|
|
31
|
+
// Snap has a key and says what it did: a human build session asked "how
|
|
32
|
+
// do you toggle the snap" with the magnet button on screen the whole time.
|
|
33
|
+
// The hint names the ACTIVE keymap's key, never a remembered literal.
|
|
34
|
+
// Holding Ctrl/⌘ during a drag snaps temporarily (editor-viewport.ts).
|
|
35
|
+
{
|
|
36
|
+
id: 'viewport.toggleSnap',
|
|
37
|
+
scope: 'stage',
|
|
38
|
+
run: () => {
|
|
39
|
+
store.shell.toggleSnap();
|
|
40
|
+
const { translate, rotate, scale } = store.shell.snapValues;
|
|
41
|
+
const toggles = `${keyboard.shortcutFor('viewport.toggleSnap') ?? ''} toggles; hold Ctrl while dragging to snap once`;
|
|
42
|
+
showTransientHint(
|
|
43
|
+
store.shell.snapEnabled
|
|
44
|
+
? `Snap on — ${translate} units, ${rotate}°, ×${scale} (${toggles})`
|
|
45
|
+
: `Snap off (${toggles})`,
|
|
46
|
+
);
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{ id: 'viewport.frameSelection', scope: 'stage', run: () => store.shell.focusOnSelection() },
|
|
50
|
+
{
|
|
51
|
+
id: 'viewport.cyclePivot',
|
|
52
|
+
scope: 'stage',
|
|
53
|
+
run: () => {
|
|
54
|
+
const pivotModes = ['active-element', 'median-point', 'individual-origins'] as const;
|
|
55
|
+
const idx = pivotModes.indexOf(store.shell.pivotMode);
|
|
56
|
+
store.shell.setPivotMode(pivotModes[(idx + 1) % pivotModes.length]!);
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{ id: 'view.top', scope: 'stage', run: () => store.shell.setViewPreset('top') },
|
|
60
|
+
{ id: 'view.front', scope: 'stage', run: () => store.shell.setViewPreset('front') },
|
|
61
|
+
{ id: 'view.right', scope: 'stage', run: () => store.shell.setViewPreset('right') },
|
|
62
|
+
{ id: 'view.bottom', scope: 'stage', run: () => store.shell.setViewPreset('bottom') },
|
|
63
|
+
{ id: 'view.back', scope: 'stage', run: () => store.shell.setViewPreset('back') },
|
|
64
|
+
{ id: 'view.left', scope: 'stage', run: () => store.shell.setViewPreset('left') },
|
|
65
|
+
{ id: 'view.orbitLeft', scope: 'stage', run: () => store.shell.stepView('orbit-left') },
|
|
66
|
+
{ id: 'view.orbitRight', scope: 'stage', run: () => store.shell.stepView('orbit-right') },
|
|
67
|
+
{ id: 'view.orbitUp', scope: 'stage', run: () => store.shell.stepView('orbit-up') },
|
|
68
|
+
{ id: 'view.orbitDown', scope: 'stage', run: () => store.shell.stepView('orbit-down') },
|
|
69
|
+
{ id: 'view.opposite', scope: 'stage', run: () => store.shell.stepView('opposite') },
|
|
70
|
+
{ id: 'view.rollLeft', scope: 'stage', run: () => store.shell.stepView('roll-left') },
|
|
71
|
+
{ id: 'view.rollRight', scope: 'stage', run: () => store.shell.stepView('roll-right') },
|
|
72
|
+
{ id: 'view.all', scope: 'stage', run: () => store.shell.frameAll() },
|
|
73
|
+
{ id: 'view.zoomIn', scope: 'stage', run: () => store.shell.zoomView(1) },
|
|
74
|
+
{ id: 'view.zoomOut', scope: 'stage', run: () => store.shell.zoomView(-1) },
|
|
75
|
+
{ id: 'view.perspective', scope: 'stage', run: () => store.shell.setViewPreset('perspective') },
|
|
76
|
+
{ id: 'view.projection', scope: 'stage', run: () => store.shell.toggleProjection() },
|
|
77
|
+
{ id: 'view.camera', scope: 'stage', run: () => store.shell.toggleCameraView() },
|
|
78
|
+
{ id: 'viewport.snapToFloor', scope: 'stage', run: () => viewport.snapSelectionToFloor() },
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
const onPointerDown = () => setActiveScope('viewport');
|
|
82
|
+
canvas.addEventListener('pointerdown', onPointerDown);
|
|
83
|
+
installHotkeys();
|
|
84
|
+
|
|
85
|
+
// Vertex snap: HOLD to activate. A keybinding fires once per keydown and has
|
|
86
|
+
// no held concept, so this pair reads the same keymap entry directly rather
|
|
87
|
+
// than a literal key.
|
|
88
|
+
const isVertexSnapKey = (e: KeyboardEvent): boolean =>
|
|
89
|
+
keyboard.chordsFor('viewport.vertexSnapHold').some((chord) => chord.key === e.key.toLowerCase());
|
|
90
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
91
|
+
if (isVertexSnapKey(e) && !e.metaKey && !e.ctrlKey && !e.repeat) {
|
|
92
|
+
// THE STAGE'S OWN SCOPE, which is what keeps this from being a second
|
|
93
|
+
// keyboard owner: it fires only while the viewport holds the editor's
|
|
94
|
+
// scope, the same fact the frame publishes as `vgai.stage.focused`.
|
|
95
|
+
if (getActiveScope() !== 'viewport') return;
|
|
96
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
|
97
|
+
store.setVertexSnapActive(true);
|
|
98
|
+
viewport.activateVertexSnap();
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const onKeyUp = (e: KeyboardEvent) => {
|
|
102
|
+
if (isVertexSnapKey(e) && !e.metaKey && !e.ctrlKey) {
|
|
103
|
+
store.setVertexSnapActive(false);
|
|
104
|
+
viewport.deactivateVertexSnap();
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
// Not a second keyboard owner: it consumes nothing — no `preventDefault`, no
|
|
108
|
+
// action table — reads only whether a chord is DOWN, and fires only while the
|
|
109
|
+
// viewport holds the editor's own scope, like `editor-viewport.ts`'s fly keys.
|
|
110
|
+
window.addEventListener('keydown', onKeyDown);
|
|
111
|
+
window.addEventListener('keyup', onKeyUp);
|
|
112
|
+
|
|
113
|
+
return () => {
|
|
114
|
+
unbind();
|
|
115
|
+
canvas.removeEventListener('pointerdown', onPointerDown);
|
|
116
|
+
window.removeEventListener('keydown', onKeyDown);
|
|
117
|
+
window.removeEventListener('keyup', onKeyUp);
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { isEditorOwnedObject } from '@volter/editor-threejs/viewport/editor-layers';
|
|
2
|
+
import type { ViewportShadingRenderer } from '@volter/editor-threejs/render/viewport-shading';
|
|
3
|
+
import type * as THREE from 'three';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Scene diagnostic shading targets authored/runtime meshes only. Editor
|
|
7
|
+
* infrastructure shares the native scene for authoring, but its custom
|
|
8
|
+
* gizmo materials are part of the editor implementation and must stay native.
|
|
9
|
+
*/
|
|
10
|
+
export const isEditorViewportShadingTarget: Parameters<ViewportShadingRenderer['render']>[3] = (
|
|
11
|
+
mesh: THREE.Mesh,
|
|
12
|
+
) => !isEditorOwnedObject(mesh);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE VIEWPORT'S STATUS KEYS, reported by the Three set as a status facet
|
|
3
|
+
* (`host.session.reportFacet`): grid, helpers, stats, shading, helper visibility,
|
|
4
|
+
* the armed transform tool, space and snap, and the camera — each for the stage
|
|
5
|
+
* the person is looking at (`focusedStageStore`, ARCHITECTURE-CORE §One stage
|
|
6
|
+
* unit 4), with an Object3D document's own presentation where it has one.
|
|
7
|
+
*/
|
|
8
|
+
import { editorHost } from '@volter/editor-sdk/host';
|
|
9
|
+
import { activeWorkspaceDocumentId } from '@volter/editor-sdk/kit/workspace-document-registry';
|
|
10
|
+
import { object3DDocumentSession } from './authoring/object3d-document-session-registry';
|
|
11
|
+
import { focusedStageStore } from '@volter/editor-sdk/kit/stage-context';
|
|
12
|
+
import { viewGridVisible } from '@volter/editor-sdk/kit/viewport-presentation';
|
|
13
|
+
import { threeStoreForHost } from './three-state';
|
|
14
|
+
import { threeStateOf } from './three-state';
|
|
15
|
+
|
|
16
|
+
export function reportViewportStatus(): () => void {
|
|
17
|
+
return editorHost().session.reportFacet(() => {
|
|
18
|
+
const store = threeStoreForHost();
|
|
19
|
+
if (!store) return {};
|
|
20
|
+
const stage = focusedStageStore(store.shell);
|
|
21
|
+
const activeDocumentId = activeWorkspaceDocumentId();
|
|
22
|
+
const presentation = activeDocumentId
|
|
23
|
+
? object3DDocumentSession(activeDocumentId)?.presentation()
|
|
24
|
+
: undefined;
|
|
25
|
+
// A camera is a per-stage fact: the pose of the stage the person is looking
|
|
26
|
+
// through, absent until one is bound — never a fabricated value.
|
|
27
|
+
const pose = threeStateOf(stage).cameraPose;
|
|
28
|
+
return {
|
|
29
|
+
showGrid: activeDocumentId ? viewGridVisible(activeDocumentId) : true,
|
|
30
|
+
showHelpers: stage.showHelpers,
|
|
31
|
+
showStats: threeStateOf(stage).showStats,
|
|
32
|
+
shadingMode: presentation?.mode ?? store.shadingMode,
|
|
33
|
+
helperVisibility: {
|
|
34
|
+
...stage.helperVisibility,
|
|
35
|
+
...(presentation ? { bounds: presentation.bounds, skeletons: presentation.skeleton } : {}),
|
|
36
|
+
},
|
|
37
|
+
// The armed tool is the ACTIVE document's stage's: every stage owns a store
|
|
38
|
+
// and the shelf's tools write the one the person is looking at.
|
|
39
|
+
transformMode: stage.transformMode,
|
|
40
|
+
transformSpace: store.shell.transformSpace,
|
|
41
|
+
snapEnabled: store.shell.snapEnabled,
|
|
42
|
+
...(pose
|
|
43
|
+
? {
|
|
44
|
+
camera: {
|
|
45
|
+
position: { x: pose.position.x, y: pose.position.y, z: pose.position.z },
|
|
46
|
+
target: { x: pose.target.x, y: pose.target.y, z: pose.target.z },
|
|
47
|
+
fov: pose.fov,
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
: {}),
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
}
|