@scenoco-three/editor 0.5.0 → 0.5.4
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/app/globals.d.ts +9 -0
- package/app/main.ts +10 -7
- package/app/session.ts +46 -6
- package/dist/server.js +3 -0
- package/package.json +4 -4
package/app/globals.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The websocket path, substituted into the page at serve time by the editor's Vite config.
|
|
3
|
+
*
|
|
4
|
+
* A constant rather than an import because the page and the server share `src/protocol.ts`, and
|
|
5
|
+
* `src/` is not published — the package ships `dist` and `app`. Everything else the page borrows
|
|
6
|
+
* from that module is a type, which erases; this was the only value, so it comes in through
|
|
7
|
+
* `define` and the import can be type-only.
|
|
8
|
+
*/
|
|
9
|
+
declare const __SCENOCO_WS_PATH__: string;
|
package/app/main.ts
CHANGED
|
@@ -9,12 +9,15 @@ import { pickAt } from './picking.js';
|
|
|
9
9
|
import { EditorSession, type FocusTarget } from './session.js';
|
|
10
10
|
import { mountEditorUI } from './ui-react/mount.js';
|
|
11
11
|
import type { EditorUIHandle } from './ui-react/types.js';
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
// Type-only, and deliberately so: `import type` is erased before the browser ever sees this, so
|
|
13
|
+
// the page never asks for a module out of `src/` — which the published package does not ship.
|
|
14
|
+
// The one *value* the page needed from there, the websocket path, arrives as a build-time
|
|
15
|
+
// constant instead. See `__SCENOCO_WS_PATH__` in app/globals.d.ts.
|
|
16
|
+
import type {
|
|
17
|
+
EditorCommand,
|
|
18
|
+
EditorPush,
|
|
19
|
+
EditorResponse,
|
|
20
|
+
EditorResult,
|
|
18
21
|
} from '../src/protocol.js';
|
|
19
22
|
|
|
20
23
|
/**
|
|
@@ -315,7 +318,7 @@ async function save(): Promise<void> {
|
|
|
315
318
|
* write path into someone's source is a write their editor, diff and undo never saw.
|
|
316
319
|
*/
|
|
317
320
|
function connect(): void {
|
|
318
|
-
const socket = new WebSocket(`ws://${location.host}${
|
|
321
|
+
const socket = new WebSocket(`ws://${location.host}${__SCENOCO_WS_PATH__}`);
|
|
319
322
|
socket.addEventListener('message', async (event) => {
|
|
320
323
|
const message = JSON.parse(String(event.data)) as EditorCommand | EditorPush;
|
|
321
324
|
if ('push' in message) {
|
package/app/session.ts
CHANGED
|
@@ -98,6 +98,14 @@ export class EditorSession {
|
|
|
98
98
|
this.sceneClass = SceneClass ?? stageFor(PrefabClass!);
|
|
99
99
|
this.scene = await SceneManager.load(this.sceneClass);
|
|
100
100
|
this.resize(); // the scene's camera only exists now, and it is the one play mode renders through
|
|
101
|
+
|
|
102
|
+
// One draw to make the scene's render settings current — which is what asks for the post
|
|
103
|
+
// chain — then wait for it. Without this the first `screenshot` after opening shows no post
|
|
104
|
+
// passes, because the composer is a dynamic import that has not landed yet, and "no bloom"
|
|
105
|
+
// is indistinguishable from "the <Bloom> I just wrote does not work".
|
|
106
|
+
this.render();
|
|
107
|
+
await this.engine.effectsReady();
|
|
108
|
+
|
|
101
109
|
this.onChange?.();
|
|
102
110
|
}
|
|
103
111
|
|
|
@@ -200,10 +208,16 @@ export class EditorSession {
|
|
|
200
208
|
this.mode = 'play';
|
|
201
209
|
this.playing = true;
|
|
202
210
|
SceneManager.resetFrameClock();
|
|
211
|
+
// The toolbar reads `mode` and `playing`, and the React chrome only re-reads on this call.
|
|
212
|
+
// `stop` always raised it and these two never did, so a scene driven into play mode from the
|
|
213
|
+
// MCP kept a toolbar insisting it was stopped: Play still offered, Pause and Step still
|
|
214
|
+
// hidden, while the scene ran behind them. Whoever is watching has to be able to trust it.
|
|
215
|
+
this.onChange?.();
|
|
203
216
|
}
|
|
204
217
|
|
|
205
218
|
pause(): void {
|
|
206
219
|
this.playing = false;
|
|
220
|
+
this.onChange?.();
|
|
207
221
|
}
|
|
208
222
|
|
|
209
223
|
/**
|
|
@@ -255,7 +269,10 @@ export class EditorSession {
|
|
|
255
269
|
|
|
256
270
|
render(camera: Camera = this.renderCamera()): void {
|
|
257
271
|
if (!this.scene) return;
|
|
258
|
-
|
|
272
|
+
// Through the engine, not `renderer.render`: the scene's render document — tone mapping,
|
|
273
|
+
// bloom, vignette, fog — is applied there, and a viewport that skips it is not showing the
|
|
274
|
+
// scene the game draws.
|
|
275
|
+
this.engine.renderScene(this.scene, camera);
|
|
259
276
|
// Only in edit mode: play renders through the scene's own camera, where the UI is already
|
|
260
277
|
// in front of it, and a run has to show exactly what the game shows — once.
|
|
261
278
|
if (this.showUi && this.mode === 'edit') this.renderUiOverlay();
|
|
@@ -358,11 +375,13 @@ export class EditorSession {
|
|
|
358
375
|
camera.aspect = frame.width / frame.height;
|
|
359
376
|
camera.updateProjectionMatrix();
|
|
360
377
|
|
|
378
|
+
// Buffer *and* effect chain move together, or the post passes run at the wrong scale. The
|
|
379
|
+
// canvas is read while it is still that size, then everything goes back.
|
|
361
380
|
const { width, height } = this.renderer.domElement;
|
|
362
|
-
this.
|
|
363
|
-
this.
|
|
381
|
+
this.engine.setFrameSize(frame.width, frame.height);
|
|
382
|
+
this.engine.renderScene(this.scene, camera);
|
|
364
383
|
const dataUrl = this.canvas.toDataURL('image/png');
|
|
365
|
-
this.
|
|
384
|
+
this.engine.setFrameSize(width, height);
|
|
366
385
|
|
|
367
386
|
// Put the camera back: a capture is an observation, and an observation that moves the thing
|
|
368
387
|
// it observed is not one.
|
|
@@ -429,11 +448,11 @@ export class EditorSession {
|
|
|
429
448
|
await this.stop();
|
|
430
449
|
break;
|
|
431
450
|
case 'step':
|
|
432
|
-
|
|
451
|
+
await this.stepVisibly(step.frames ?? 1, step.dt ?? 1 / 60);
|
|
433
452
|
break;
|
|
434
453
|
case 'press':
|
|
435
454
|
this.key('keydown', step.key);
|
|
436
|
-
this.
|
|
455
|
+
await this.stepVisibly(1);
|
|
437
456
|
this.key('keyup', step.key);
|
|
438
457
|
break;
|
|
439
458
|
case 'keyDown':
|
|
@@ -460,6 +479,27 @@ export class EditorSession {
|
|
|
460
479
|
return out;
|
|
461
480
|
}
|
|
462
481
|
|
|
482
|
+
/**
|
|
483
|
+
* Advance the scene where someone can watch it happen.
|
|
484
|
+
*
|
|
485
|
+
* `step` only ticks; the editor's own loop is what draws, and it does not get a turn while a
|
|
486
|
+
* batch is running — the whole batch is one synchronous task. Left that way, a person watching
|
|
487
|
+
* the page sees a frozen canvas and then a jump to the final state, which is indistinguishable
|
|
488
|
+
* from the editor having hung. So this draws each frame and hands the browser a chance to paint.
|
|
489
|
+
*
|
|
490
|
+
* Yielding every fourth frame rather than every frame: painting more often than that buys no
|
|
491
|
+
* legibility and a long batch would otherwise run at wall-clock speed. Determinism is untouched
|
|
492
|
+
* either way — the step is a fixed `dt`, so when the frames are drawn changes nothing about what
|
|
493
|
+
* they contain.
|
|
494
|
+
*/
|
|
495
|
+
private async stepVisibly(frames: number, dt = 1 / 60): Promise<void> {
|
|
496
|
+
for (let i = 0; i < frames; i++) {
|
|
497
|
+
this.step(dt);
|
|
498
|
+
this.render();
|
|
499
|
+
if (i % 4 === 3) await new Promise((resolve) => requestAnimationFrame(() => resolve(null)));
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
463
503
|
/**
|
|
464
504
|
* Synthetic input is a real DOM event.
|
|
465
505
|
*
|
package/dist/server.js
CHANGED
|
@@ -66,6 +66,9 @@ export async function startEditorServer(options = {}) {
|
|
|
66
66
|
// One `three` across the editor and the project, or `instanceof` quietly fails across two
|
|
67
67
|
// copies and every raycast hit looks like nothing at all.
|
|
68
68
|
resolve: { dedupe: ['three'] },
|
|
69
|
+
// The page cannot import this from `src/`: that directory is not published. See
|
|
70
|
+
// app/globals.d.ts.
|
|
71
|
+
define: { __SCENOCO_WS_PATH__: JSON.stringify(WS_PATH) },
|
|
69
72
|
optimizeDeps: { include: ['react', 'react-dom/client'] },
|
|
70
73
|
});
|
|
71
74
|
let vite = await serve(chosen.port);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scenoco-three/editor",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "Local visual editor for SceNoCo scenes, with an observation-only MCP server so an agent can see and drive the same live scene.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
48
|
-
"@scenoco-three/compiler": "^0.5.
|
|
49
|
-
"@scenoco-three/core": "^0.5.
|
|
48
|
+
"@scenoco-three/compiler": "^0.5.4",
|
|
49
|
+
"@scenoco-three/core": "^0.5.4",
|
|
50
50
|
"@vitejs/plugin-react": "^6.0.3",
|
|
51
51
|
"lucide-react": "^0.468.0",
|
|
52
52
|
"react": "^18.3.1",
|
|
@@ -65,4 +65,4 @@
|
|
|
65
65
|
"@types/react-dom": "^18.3.7",
|
|
66
66
|
"@types/ws": "^8.5.13"
|
|
67
67
|
}
|
|
68
|
-
}
|
|
68
|
+
}
|