@volter/editor-model-play 0.5.188 → 0.5.189
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.
|
@@ -5,6 +5,7 @@ import { registerDocumentPlayExtension, type DocumentPlayControlProps } from '@v
|
|
|
5
5
|
import { escapeModelPlay, modelPlaying, setModelPlaying, subscribeModelPlay } from '../src/model-play';
|
|
6
6
|
import { runPlayScript } from '../src/play-script';
|
|
7
7
|
import type * as THREE from 'three';
|
|
8
|
+
import { getCurrentProject, onProjectChange } from '@volter/editor-sdk/kit/active-project';
|
|
8
9
|
|
|
9
10
|
export const point = 'workspace.service';
|
|
10
11
|
function usePlaying(documentId: string | undefined): boolean {
|
|
@@ -28,7 +29,16 @@ function Menu({ documentId, onClose }: DocumentPlayControlProps) {
|
|
|
28
29
|
}
|
|
29
30
|
export function start(): () => void {
|
|
30
31
|
const unregister = registerDocumentPlayExtension('model', {
|
|
31
|
-
Control, Menu, playing: modelPlaying, setPlaying: setModelPlaying, escape: escapeModelPlay,
|
|
32
|
+
Control, Menu, playing: modelPlaying, setPlaying: setModelPlaying, escape: escapeModelPlay,
|
|
33
|
+
subscribe(listener) {
|
|
34
|
+
const stopPlay = subscribeModelPlay(listener), stopProject = onProjectChange(listener);
|
|
35
|
+
return () => { stopPlay(); stopProject(); };
|
|
36
|
+
},
|
|
37
|
+
aspectRatio() {
|
|
38
|
+
const size = getCurrentProject()?.config.resolution;
|
|
39
|
+
const ratio = size ? size.width / size.height : null;
|
|
40
|
+
return ratio !== null && Number.isFinite(ratio) && ratio > 0 ? ratio : null;
|
|
41
|
+
},
|
|
32
42
|
run(stage) {
|
|
33
43
|
// The document kind lends native scene objects; this tool owns their Three types.
|
|
34
44
|
return runPlayScript({ ...stage, blend: stage.sourcePath, root: stage.root as THREE.Object3D,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/editor-model-play",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.189",
|
|
4
4
|
"author": "Volter AI, Inc.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
]
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@volter/editor-sdk": "0.5.
|
|
30
|
+
"@volter/editor-sdk": "0.5.189"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"react": "^19.0.0",
|
package/src/play-script.ts
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* update, holding keys empty until arrival. Stop freezes the copy and blends
|
|
24
24
|
* back before disposing it; Escape during either blend completes that blend.
|
|
25
25
|
*/
|
|
26
|
+
import { editorHost } from '@volter/editor-sdk/host';
|
|
26
27
|
import { getCurrentProject } from '@volter/editor-sdk/kit/active-project';
|
|
27
28
|
import { surfaceAcceptsKey, surfaceHoldsKeyboard } from '@volter/editor-sdk/kit/surface-keyboard';
|
|
28
29
|
import {
|
|
@@ -130,6 +131,21 @@ export function runPlayScript(options: {
|
|
|
130
131
|
let attempt = 0;
|
|
131
132
|
let game: ModelPlayGame | null = null;
|
|
132
133
|
let composition: PlayComposition | null = null;
|
|
134
|
+
let startedAt: number | null = null;
|
|
135
|
+
let endedAt: number | null = null;
|
|
136
|
+
const live = editorHost().live;
|
|
137
|
+
const unregisterLive = live.register({
|
|
138
|
+
id: `model-script:${options.documentId}`,
|
|
139
|
+
mounted: () => !stopped && game !== null,
|
|
140
|
+
// The live game owns the viewport through the camera return. Its update
|
|
141
|
+
// loop is held during that transition; Edit begins when disposal completes.
|
|
142
|
+
playing: () => !stopped && game !== null,
|
|
143
|
+
stop: () => { if (!stopped) finishModelPlay(options.documentId); },
|
|
144
|
+
instanceContainer: (id) => !id && !stopped && game !== null ? options.container : null,
|
|
145
|
+
startedAt: () => startedAt,
|
|
146
|
+
endedAt: () => endedAt,
|
|
147
|
+
surface: () => 'three',
|
|
148
|
+
});
|
|
133
149
|
let pending: { game: ModelPlayGame; composition: PlayComposition | null } | null = null;
|
|
134
150
|
// A failed mount/update still owns a dependency graph whose next save retries it.
|
|
135
151
|
let retryEntries: readonly string[] = [];
|
|
@@ -144,6 +160,8 @@ export function runPlayScript(options: {
|
|
|
144
160
|
dispose(game, composition);
|
|
145
161
|
game = null;
|
|
146
162
|
composition = null;
|
|
163
|
+
if (startedAt !== null) endedAt = Date.now();
|
|
164
|
+
live.notifyChanged();
|
|
147
165
|
};
|
|
148
166
|
const start = async (): Promise<void> => {
|
|
149
167
|
const mine = ++attempt;
|
|
@@ -155,7 +173,7 @@ export function runPlayScript(options: {
|
|
|
155
173
|
if (!project) throw new Error('No project is open.');
|
|
156
174
|
const epoch = beginProjectMountEpoch();
|
|
157
175
|
const container = document.createElement('div');
|
|
158
|
-
container.dataset
|
|
176
|
+
container.dataset['mountEpoch'] = String(epoch);
|
|
159
177
|
Object.assign(container.style, { position: 'absolute', inset: '0', visibility: 'hidden', pointerEvents: 'none' });
|
|
160
178
|
options.container.appendChild(container);
|
|
161
179
|
let layers;
|
|
@@ -210,7 +228,10 @@ export function runPlayScript(options: {
|
|
|
210
228
|
next.game.update(deltaSeconds);
|
|
211
229
|
end();
|
|
212
230
|
game = next.game;
|
|
231
|
+
startedAt = Date.now();
|
|
232
|
+
endedAt = null;
|
|
213
233
|
composition = next.composition;
|
|
234
|
+
live.notifyChanged();
|
|
214
235
|
composition?.reveal();
|
|
215
236
|
replacementUpdated = true;
|
|
216
237
|
} catch (error) {
|
|
@@ -229,6 +250,7 @@ export function runPlayScript(options: {
|
|
|
229
250
|
report(`${modulePath} failed`, error instanceof Error ? error.message : String(error));
|
|
230
251
|
return;
|
|
231
252
|
}
|
|
253
|
+
keys.clear();
|
|
232
254
|
root.updateMatrixWorld(true);
|
|
233
255
|
});
|
|
234
256
|
const onKeyDown = (event: KeyboardEvent): void => {
|
|
@@ -237,14 +259,22 @@ export function runPlayScript(options: {
|
|
|
237
259
|
if (transition.acceptingKeys()) keys.add(event.code);
|
|
238
260
|
};
|
|
239
261
|
const onKeyUp = (event: KeyboardEvent): void => {
|
|
240
|
-
|
|
262
|
+
// Preserve a between-frame tap until one game update has observed it.
|
|
241
263
|
heldKeys.delete(event.code);
|
|
242
264
|
};
|
|
243
265
|
const onBlur = (): void => { keys.clear(); heldKeys.clear(); };
|
|
244
266
|
window.addEventListener('keydown', onKeyDown, true);
|
|
245
267
|
window.addEventListener('keyup', onKeyUp, true);
|
|
246
268
|
window.addEventListener('blur', onBlur);
|
|
247
|
-
const stopChanges = subscribeProjectModuleChange((changed, affected) => {
|
|
269
|
+
const stopChanges = subscribeProjectModuleChange((changed, affected, type) => {
|
|
270
|
+
// Removing this run's entry ends its lifetime. A rename can remove it
|
|
271
|
+
// before the model document switches; importing that vanished entry
|
|
272
|
+
// would manufacture a mount failure while the old game is still alive.
|
|
273
|
+
// A missing dependency continues through the ordinary error-reporting path.
|
|
274
|
+
if (type === 'delete' && projectModuleChangeMatches(changed, modulePath)) {
|
|
275
|
+
finishModelPlay(options.documentId);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
248
278
|
// A composed HUD and script must remount together, including shared-store
|
|
249
279
|
// edits. A fresh epoch is threaded to both through the UI tool door.
|
|
250
280
|
const entries = [modulePath, ...retryEntries, ...(composition?.entries ?? []), ...(pending?.composition?.entries ?? [])];
|
|
@@ -253,6 +283,7 @@ export function runPlayScript(options: {
|
|
|
253
283
|
void start();
|
|
254
284
|
return () => {
|
|
255
285
|
stopped = true;
|
|
286
|
+
unregisterLive();
|
|
256
287
|
stopRequest();
|
|
257
288
|
finishModelPlay(options.documentId);
|
|
258
289
|
stopFrames();
|