@volter/editor-model-play 0.5.187 → 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, subscribe: subscribeModelPlay,
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.187",
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.187"
30
+ "@volter/editor-sdk": "0.5.189"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "react": "^19.0.0",
@@ -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.mountEpoch = String(epoch);
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
- keys.delete(event.code);
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();