@scenoco-three/editor 0.3.1 → 0.4.0
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/document.ts +1 -1
- package/app/main.ts +16 -5
- package/app/session.ts +145 -33
- package/app/vite-env.d.ts +4 -2
- package/app/wsClient.ts +1 -1
- package/dist/cli/index.js +0 -0
- package/dist/server.js +47 -9
- package/package.json +6 -6
package/app/document.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type SceneGraph,
|
|
13
13
|
} from '@scenoco-three/core';
|
|
14
14
|
import type { NodeOrigin, SceneProvenance } from '@scenoco-three/compiler';
|
|
15
|
-
import type { EditOp, EditTarget } from '../
|
|
15
|
+
import type { EditOp, EditTarget } from '../dist/protocol.js';
|
|
16
16
|
import { UndoStack } from './commands.js';
|
|
17
17
|
|
|
18
18
|
export type TransformChannel = 'position' | 'rotation' | 'scale';
|
package/app/main.ts
CHANGED
|
@@ -217,6 +217,9 @@ let reloading = false;
|
|
|
217
217
|
let conflictBanner: HTMLElement | undefined;
|
|
218
218
|
|
|
219
219
|
session.onExternalChange = (_file, role) => void handleExternalChange(role);
|
|
220
|
+
// A gameplay frame that throws surfaces in the same banner; playback is already
|
|
221
|
+
// paused by the session, so the editor stays alive until the code is fixed.
|
|
222
|
+
session.onRuntimeError = (message) => setComponentError(message);
|
|
220
223
|
|
|
221
224
|
async function handleExternalChange(role: 'open' | 'dep'): Promise<void> {
|
|
222
225
|
if (reloading) return;
|
|
@@ -322,11 +325,19 @@ async function openFile(path: string): Promise<void> {
|
|
|
322
325
|
}
|
|
323
326
|
|
|
324
327
|
void (async () => {
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
|
|
328
|
+
// Core tags + tag packages first, as their own import — this is the registry
|
|
329
|
+
// every core scene decodes against, so it must NOT depend on project components
|
|
330
|
+
// loading (a broken component previously cascaded into "Background not registered").
|
|
331
|
+
try {
|
|
332
|
+
await import('virtual:scenoco-runtime');
|
|
333
|
+
} catch (e) {
|
|
334
|
+
console.error('[editor] runtime tags failed to load:', e);
|
|
335
|
+
setComponentError(e instanceof Error ? e.message : String(e));
|
|
336
|
+
}
|
|
337
|
+
// Project components, imported separately + caught so a broken component file
|
|
338
|
+
// can't abort page startup or wipe the core tags above: the WS bridge (hence
|
|
339
|
+
// MCP) and the error banner still come up, and core scenes still preview. A full
|
|
340
|
+
// reload on the next successful HMR update revives components once they're fixed.
|
|
330
341
|
try {
|
|
331
342
|
await import('virtual:scenoco-components');
|
|
332
343
|
} catch (e) {
|
package/app/session.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AmbientLight, Box3, Color, DirectionalLight, PerspectiveCamera, Sphere, Vector3, type Object3D,
|
|
3
3
|
} from 'three';
|
|
4
|
-
import { Engine, Registry, bundleKind, type Bundle, type Component } from '@scenoco-three/core';
|
|
4
|
+
import { Engine, PrefabRef, Registry, bundleKind, type Bundle, type Component } from '@scenoco-three/core';
|
|
5
5
|
import type { SceneProvenance } from '@scenoco-three/compiler';
|
|
6
|
-
import type { EditOp, EditorResult, SimStep } from '../
|
|
6
|
+
import type { EditOp, EditorResult, SimStep } from '../dist/protocol.js';
|
|
7
7
|
|
|
8
8
|
export type EditorMode = 'edit' | 'play';
|
|
9
9
|
|
|
@@ -19,10 +19,19 @@ export class EditorSession {
|
|
|
19
19
|
playing = false;
|
|
20
20
|
/** The editor camera (edit mode only). */
|
|
21
21
|
editorCamera?: PerspectiveCamera;
|
|
22
|
+
/** A reused framing camera for play-mode previews of scenes/prefabs that have no camera of their own. */
|
|
23
|
+
private previewCamera?: PerspectiveCamera;
|
|
22
24
|
/** Fired after the engine/scene changes, so the host rebinds camera + panels. */
|
|
23
25
|
onChange?: () => void;
|
|
24
26
|
/** Fired when a watched file changes on disk (the open file or a `src=` dep). */
|
|
25
27
|
onExternalChange?: (file: string, role: 'open' | 'dep') => void;
|
|
28
|
+
/**
|
|
29
|
+
* Fired when a gameplay frame throws (a component hook / system error). The host
|
|
30
|
+
* shows it in the same "component code has an error" banner; the session stays
|
|
31
|
+
* alive (playback paused) so a fix + HMR reload recovers, rather than the RAF
|
|
32
|
+
* loop dying or the MCP session wedging.
|
|
33
|
+
*/
|
|
34
|
+
onRuntimeError?: (message: string) => void;
|
|
26
35
|
|
|
27
36
|
private bundle?: Bundle;
|
|
28
37
|
/** Source path of the open scene/prefab — the target of `save`. */
|
|
@@ -86,7 +95,7 @@ export class EditorSession {
|
|
|
86
95
|
/** Advance gameplay by a single frame while paused (no-op outside play mode). */
|
|
87
96
|
step(dt = 1 / 60): void {
|
|
88
97
|
if (this.mode !== 'play') return;
|
|
89
|
-
this.
|
|
98
|
+
this.runTick(dt); // bypass the play+paused gate in tick()
|
|
90
99
|
}
|
|
91
100
|
stop(): void {
|
|
92
101
|
this.toEdit();
|
|
@@ -94,18 +103,45 @@ export class EditorSession {
|
|
|
94
103
|
|
|
95
104
|
/** Advance one frame: edit always renders; play advances only while playing. */
|
|
96
105
|
tick(dt: number): void {
|
|
97
|
-
this.
|
|
106
|
+
this.runTick(this.mode === 'play' && !this.playing ? 0 : dt);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Drive the engine one frame, catching any error a component hook / system
|
|
111
|
+
* throws so a single bad frame can't kill the RAF loop or the MCP session.
|
|
112
|
+
* On failure it pauses playback (so the failing frame isn't re-run every RAF)
|
|
113
|
+
* and surfaces the error to the host banner. Returns the error message, if any.
|
|
114
|
+
*/
|
|
115
|
+
private runTick(dt: number): string | undefined {
|
|
116
|
+
try {
|
|
117
|
+
this.engine.tick(dt);
|
|
118
|
+
return undefined;
|
|
119
|
+
} catch (e) {
|
|
120
|
+
this.playing = false;
|
|
121
|
+
const message = e instanceof Error ? (e.stack ?? e.message) : String(e);
|
|
122
|
+
this.onRuntimeError?.(message);
|
|
123
|
+
return message;
|
|
124
|
+
}
|
|
98
125
|
}
|
|
99
126
|
|
|
100
127
|
/** Capture the current view as a PNG data URL (optionally re-framing the editor camera). */
|
|
101
128
|
preview(opts: { camera?: string; lookAt?: string }): string {
|
|
102
|
-
if (this.mode === 'edit'
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
129
|
+
if (this.mode === 'edit') {
|
|
130
|
+
if (this.editorCamera) {
|
|
131
|
+
const cam = vec3(opts.camera);
|
|
132
|
+
const look = vec3(opts.lookAt);
|
|
133
|
+
if (cam) this.editorCamera.position.copy(cam);
|
|
134
|
+
if (look) this.editorCamera.lookAt(look);
|
|
135
|
+
}
|
|
136
|
+
// Particles only emit under their play-mode render system — render a short
|
|
137
|
+
// throwaway play burst so a VFX prefab isn't previewed empty while authoring.
|
|
138
|
+
if (this.hasParticles()) return this.particlePreview(opts);
|
|
139
|
+
} else {
|
|
140
|
+
// Play mode: honor the camera override (P2) and give a camera-less lone prefab
|
|
141
|
+
// a default framed camera + backdrop, so it isn't a blank white frame.
|
|
142
|
+
this.framePlayPreview(opts);
|
|
107
143
|
}
|
|
108
|
-
this.
|
|
144
|
+
this.runTick(0); // render the current frame (guarded — a bad frame won't wedge the session)
|
|
109
145
|
return this.canvas.toDataURL('image/png');
|
|
110
146
|
}
|
|
111
147
|
|
|
@@ -156,7 +192,12 @@ export class EditorSession {
|
|
|
156
192
|
case 'step': {
|
|
157
193
|
const frames = Math.max(0, Math.floor(step.frames ?? 1));
|
|
158
194
|
const dt = step.dt ?? 1 / 60;
|
|
159
|
-
for (let i = 0; i < frames; i++)
|
|
195
|
+
for (let i = 0; i < frames; i++) {
|
|
196
|
+
// A throwing frame surfaces as a result (not a bare transport error)
|
|
197
|
+
// and stops the batch, leaving the session alive for the agent to inspect.
|
|
198
|
+
const error = this.runTick(dt);
|
|
199
|
+
if (error) { out.push({ kind: 'json', data: { error } }); return out; }
|
|
200
|
+
}
|
|
160
201
|
break;
|
|
161
202
|
}
|
|
162
203
|
case 'tap':
|
|
@@ -212,35 +253,100 @@ export class EditorSession {
|
|
|
212
253
|
}
|
|
213
254
|
|
|
214
255
|
private applyBundle(): void {
|
|
215
|
-
if (
|
|
216
|
-
if (bundleKind(this.bundle) === 'prefab') {
|
|
217
|
-
this.engine.instantiate(this.bundle);
|
|
218
|
-
// Editor-only preview lights — flagged so the Hierarchy/picking ignore them.
|
|
219
|
-
const ambient = new AmbientLight(0xffffff, 0.6);
|
|
220
|
-
ambient.userData.scenocoEditorOnly = true;
|
|
221
|
-
this.engine.scene.add(ambient);
|
|
222
|
-
const key = new DirectionalLight(0xffffff, 2);
|
|
223
|
-
key.position.set(3, 5, 4);
|
|
224
|
-
key.userData.scenocoEditorOnly = true;
|
|
225
|
-
this.engine.scene.add(key);
|
|
226
|
-
if (!this.engine.scene.background) this.engine.scene.background = new Color(0x15181f);
|
|
227
|
-
} else {
|
|
228
|
-
this.engine.loadScene(this.bundle);
|
|
229
|
-
}
|
|
256
|
+
if (this.bundle) applyBundleToEngine(this.engine, this.bundle);
|
|
230
257
|
}
|
|
231
258
|
|
|
232
259
|
/** Editor camera framing the scene from a 3/4 angle. */
|
|
233
260
|
private frameCamera(): PerspectiveCamera {
|
|
234
261
|
const cam = new PerspectiveCamera(50, this.canvas.clientWidth / this.canvas.clientHeight || 1, 0.01, 50000);
|
|
235
|
-
|
|
236
|
-
const sphere = box.isEmpty() ? new Sphere(new Vector3(), 1) : box.getBoundingSphere(new Sphere());
|
|
237
|
-
const center = sphere.center;
|
|
238
|
-
const radius = sphere.radius || 1;
|
|
239
|
-
cam.position.copy(center).addScaledVector(new Vector3(0.6, 0.45, 1).normalize(), (radius / Math.sin((50 * Math.PI) / 180 / 2)) * 1.2);
|
|
240
|
-
cam.lookAt(center);
|
|
262
|
+
frameCameraTo(cam, this.engine.scene);
|
|
241
263
|
this.engine.mainCamera = cam;
|
|
242
264
|
return cam;
|
|
243
265
|
}
|
|
266
|
+
|
|
267
|
+
/** Any live `ParticleSystem` component in the scene — VFX that only emits under its play-mode render system. */
|
|
268
|
+
private hasParticles(): boolean {
|
|
269
|
+
const ctor = Registry.getComponent('ParticleSystem')?.ctor;
|
|
270
|
+
return ctor ? this.engine.getComponentsOfType(ctor).length > 0 : false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Frame a preview for the current play scene: honor a `camera`/`lookAt` override
|
|
275
|
+
* (P2 — worked only in edit mode before), and give a scene with no camera of its
|
|
276
|
+
* own (a lone prefab) a default framed camera + neutral backdrop so it isn't a
|
|
277
|
+
* blank white frame. Left untouched when the scene has its own camera and no override.
|
|
278
|
+
*/
|
|
279
|
+
private framePlayPreview(opts: { camera?: string; lookAt?: string }): void {
|
|
280
|
+
const camPos = vec3(opts.camera);
|
|
281
|
+
const lookAt = vec3(opts.lookAt);
|
|
282
|
+
if (this.engine.mainCamera && !camPos && !lookAt) return; // scene's own camera, no override — leave it
|
|
283
|
+
const cam = (this.previewCamera ??= new PerspectiveCamera(50, 1, 0.01, 50000));
|
|
284
|
+
cam.aspect = this.canvas.clientWidth / this.canvas.clientHeight || 1;
|
|
285
|
+
const center = frameCameraTo(cam, this.engine.scene); // auto-frame, then apply any override
|
|
286
|
+
if (camPos) cam.position.copy(camPos);
|
|
287
|
+
if (lookAt) cam.lookAt(lookAt);
|
|
288
|
+
else if (camPos) cam.lookAt(center);
|
|
289
|
+
cam.updateProjectionMatrix();
|
|
290
|
+
this.engine.mainCamera = cam; // not one of the scene's cameras → a render override
|
|
291
|
+
if (!this.engine.scene.background) this.engine.scene.background = new Color(0x15181f);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Render a particle-bearing file in a throwaway play engine advanced a few
|
|
296
|
+
* frames, so its VFX is actually visible when previewed while authoring (in edit
|
|
297
|
+
* mode the particle render system never runs, so the effect shows empty). The
|
|
298
|
+
* edit session's own engine is left untouched; the next frame re-renders it.
|
|
299
|
+
*/
|
|
300
|
+
private particlePreview(opts: { camera?: string; lookAt?: string }): string {
|
|
301
|
+
const engine = new Engine({ canvas: this.canvas, autoStart: false, editor: false, preserveDrawingBuffer: true });
|
|
302
|
+
try {
|
|
303
|
+
if (this.bundle) applyBundleToEngine(engine, this.bundle);
|
|
304
|
+
const cam = new PerspectiveCamera(50, this.canvas.clientWidth / this.canvas.clientHeight || 1, 0.01, 50000);
|
|
305
|
+
const center = frameCameraTo(cam, engine.scene);
|
|
306
|
+
const camPos = vec3(opts.camera);
|
|
307
|
+
const lookAt = vec3(opts.lookAt);
|
|
308
|
+
if (camPos) cam.position.copy(camPos);
|
|
309
|
+
if (lookAt || camPos) cam.lookAt(lookAt ?? center);
|
|
310
|
+
cam.updateProjectionMatrix();
|
|
311
|
+
engine.mainCamera = cam;
|
|
312
|
+
for (let i = 0; i < PARTICLE_PREVIEW_FRAMES; i++) engine.tick(1 / 60);
|
|
313
|
+
return this.canvas.toDataURL('image/png');
|
|
314
|
+
} finally {
|
|
315
|
+
engine.dispose();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Frames to advance a particle preview so the effect has visibly emitted. */
|
|
321
|
+
const PARTICLE_PREVIEW_FRAMES = 30;
|
|
322
|
+
|
|
323
|
+
/** Instantiate a bundle into `engine`: a prefab gets neutral preview lights + backdrop; a scene loads as-is. */
|
|
324
|
+
function applyBundleToEngine(engine: Engine, bundle: Bundle): void {
|
|
325
|
+
if (bundleKind(bundle) === 'prefab') {
|
|
326
|
+
engine.instantiate(bundle);
|
|
327
|
+
// Editor-only preview lights — flagged so the Hierarchy/picking ignore them.
|
|
328
|
+
const ambient = new AmbientLight(0xffffff, 0.6);
|
|
329
|
+
ambient.userData.scenocoEditorOnly = true;
|
|
330
|
+
engine.scene.add(ambient);
|
|
331
|
+
const key = new DirectionalLight(0xffffff, 2);
|
|
332
|
+
key.position.set(3, 5, 4);
|
|
333
|
+
key.userData.scenocoEditorOnly = true;
|
|
334
|
+
engine.scene.add(key);
|
|
335
|
+
if (!engine.scene.background) engine.scene.background = new Color(0x15181f);
|
|
336
|
+
} else {
|
|
337
|
+
engine.loadScene(bundle);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Position `cam` to frame `scene` from a 3/4 angle; returns the scene center it looks at. */
|
|
342
|
+
function frameCameraTo(cam: PerspectiveCamera, scene: Object3D): Vector3 {
|
|
343
|
+
const box = new Box3().setFromObject(scene);
|
|
344
|
+
const sphere = box.isEmpty() ? new Sphere(new Vector3(), 1) : box.getBoundingSphere(new Sphere());
|
|
345
|
+
const center = sphere.center;
|
|
346
|
+
const radius = sphere.radius || 1;
|
|
347
|
+
cam.position.copy(center).addScaledVector(new Vector3(0.6, 0.45, 1).normalize(), (radius / Math.sin((50 * Math.PI) / 180 / 2)) * 1.2);
|
|
348
|
+
cam.lookAt(center);
|
|
349
|
+
return center;
|
|
244
350
|
}
|
|
245
351
|
|
|
246
352
|
// ── value helpers (telemetry → JSON) ─────────────────────────────────────────────
|
|
@@ -255,13 +361,19 @@ function propValues(component: Component): Record<string, unknown> {
|
|
|
255
361
|
function jsonValue(v: unknown): unknown {
|
|
256
362
|
if (v == null || typeof v === 'number' || typeof v === 'boolean' || typeof v === 'string') return v ?? null;
|
|
257
363
|
if (Array.isArray(v)) return v.map(jsonValue);
|
|
364
|
+
if (v instanceof PrefabRef) return v.describe(); // a `prefab` prop → its label, not "[object Object]"
|
|
258
365
|
const o = v as { isVector3?: boolean; isVector2?: boolean; isColor?: boolean; isEuler?: boolean; isObject3D?: boolean; x?: number; y?: number; z?: number; name?: string; getHexString?: () => string };
|
|
259
366
|
if (o.isVector3) return [o.x, o.y, o.z];
|
|
260
367
|
if (o.isVector2) return [o.x, o.y];
|
|
261
368
|
if (o.isEuler) return [deg(o.x!), deg(o.y!), deg(o.z!)];
|
|
262
369
|
if (o.isColor) return `#${o.getHexString!()}`;
|
|
263
370
|
if (o.isObject3D) return o.name || '(node)';
|
|
264
|
-
|
|
371
|
+
// A component reference → its scene #id (what the agent correlates on), falling
|
|
372
|
+
// back to the class name only when the target has no id.
|
|
373
|
+
if ('node' in (o as object) && 'engine' in (o as object)) {
|
|
374
|
+
const c = v as Component;
|
|
375
|
+
return c.id !== undefined ? `#${c.id}` : (componentName(c) ?? '(component)');
|
|
376
|
+
}
|
|
265
377
|
return String(v);
|
|
266
378
|
}
|
|
267
379
|
const arr = (v: { x: number; y: number; z: number }) => [round(v.x), round(v.y), round(v.z)];
|
package/app/vite-env.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/// <reference types="vite/client" />
|
|
2
2
|
|
|
3
|
-
// The editor server's plugin emits
|
|
4
|
-
//
|
|
3
|
+
// The editor server's plugin emits two modules: `scenoco-runtime` registers the
|
|
4
|
+
// built-in node tags + tag packages, and `scenoco-components` imports every project
|
|
5
|
+
// component. They're split so a broken component can't wipe the core tag registry.
|
|
6
|
+
declare module 'virtual:scenoco-runtime';
|
|
5
7
|
declare module 'virtual:scenoco-components';
|
package/app/wsClient.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WS_PATH, type EditorCommand, type EditorPush, type EditorResponse, type EditorResult } from '../
|
|
1
|
+
import { WS_PATH, type EditorCommand, type EditorPush, type EditorResponse, type EditorResult } from '../dist/protocol.js';
|
|
2
2
|
import type { EditorSession } from './session.js';
|
|
3
3
|
|
|
4
4
|
/**
|
package/dist/cli/index.js
CHANGED
|
File without changes
|
package/dist/server.js
CHANGED
|
@@ -149,22 +149,60 @@ function createFileWatcher() {
|
|
|
149
149
|
close: () => w.close(),
|
|
150
150
|
};
|
|
151
151
|
}
|
|
152
|
-
/**
|
|
152
|
+
/**
|
|
153
|
+
* Two virtual modules, split so a broken project component can't wipe the core
|
|
154
|
+
* tag registry (the P1 "Background not registered" cascade):
|
|
155
|
+
* - `virtual:scenoco-runtime` → `@scenoco-three/core/nodes` + tag packages
|
|
156
|
+
* (the infrastructure every core scene decodes against; imported first).
|
|
157
|
+
* - `virtual:scenoco-components` → only the project's component files (fragile —
|
|
158
|
+
* a broken one throws here, isolated from core/package registration).
|
|
159
|
+
* The components module is invalidated when a component file is added/removed, so
|
|
160
|
+
* a deleted file never lingers as a stale import (the client-cache half of P1).
|
|
161
|
+
*/
|
|
153
162
|
function allComponentsPlugin(roots, packages, cwd) {
|
|
154
|
-
const
|
|
155
|
-
const
|
|
163
|
+
const runtimeId = 'virtual:scenoco-runtime';
|
|
164
|
+
const componentsId = 'virtual:scenoco-components';
|
|
165
|
+
const resolvedRuntime = '\0' + runtimeId;
|
|
166
|
+
const resolvedComponents = '\0' + componentsId;
|
|
167
|
+
const absRoots = roots.map((r) => resolve(cwd, r));
|
|
168
|
+
const isComponentFile = (file) => SOURCE_EXT_RE.test(file) && absRoots.some((r) => resolve(file).startsWith(r));
|
|
156
169
|
return {
|
|
157
170
|
name: 'scenoco:all-components',
|
|
158
|
-
resolveId: (source) =>
|
|
171
|
+
resolveId: (source) => source === runtimeId ? resolvedRuntime : source === componentsId ? resolvedComponents : null,
|
|
159
172
|
load(source) {
|
|
160
|
-
if (source
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
173
|
+
if (source === resolvedRuntime) {
|
|
174
|
+
// Core nodes first (so core tags register even if a package throws next), then packages.
|
|
175
|
+
return ['import "@scenoco-three/core/nodes";', ...packages.map((p) => `import ${JSON.stringify(p)};`)].join('\n');
|
|
176
|
+
}
|
|
177
|
+
if (source === resolvedComponents) {
|
|
178
|
+
return [...scanComponents(roots, cwd).files.keys()].map((f) => `import ${JSON.stringify(`/@fs${f}`)};`).join('\n') || 'export {};';
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
},
|
|
182
|
+
configureServer(server) {
|
|
183
|
+
// Vite's watcher is rooted at the editor app dir, so it only sees component
|
|
184
|
+
// files once they're in the module graph (already imported). Watch the project
|
|
185
|
+
// component roots explicitly, so a brand-NEW file's `add` fires too — not just
|
|
186
|
+
// edits/deletes of already-imported ones.
|
|
187
|
+
server.watcher.add(absRoots);
|
|
188
|
+
// Adding/removing a component file changes the import list — rebuild the
|
|
189
|
+
// components module (not the runtime one) and reload, so a new file gets
|
|
190
|
+
// imported and a just-deleted one isn't imported from a stale graph.
|
|
191
|
+
const invalidate = (file) => {
|
|
192
|
+
if (!isComponentFile(file))
|
|
193
|
+
return;
|
|
194
|
+
const mod = server.moduleGraph.getModuleById(resolvedComponents);
|
|
195
|
+
if (mod)
|
|
196
|
+
server.moduleGraph.invalidateModule(mod);
|
|
197
|
+
server.ws.send({ type: 'full-reload' });
|
|
198
|
+
};
|
|
199
|
+
server.watcher.on('add', invalidate);
|
|
200
|
+
server.watcher.on('unlink', invalidate);
|
|
165
201
|
},
|
|
166
202
|
};
|
|
167
203
|
}
|
|
204
|
+
/** Component source extensions the editor imports (mirrors the compiler's scan). */
|
|
205
|
+
const SOURCE_EXT_RE = /\.[cm]?[jt]sx?$/;
|
|
168
206
|
/** `GET /@scenoco/bundle?file=<abs>` → the compiled runtime bundle (JSON). */
|
|
169
207
|
function bundleEndpointPlugin(roots, packages, cwd, watcher) {
|
|
170
208
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scenoco-three/editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Local preview + headless frame render for SceNoCo scenes/prefabs (for multimodal LLMs)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,18 +32,18 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
|
-
"build": "tsc -p tsconfig.build.json",
|
|
36
|
-
"typecheck": "npm --prefix ../core run build && npm --prefix ../compiler run build && npm --prefix ../vite run build && tsc --noEmit -p tsconfig.json"
|
|
35
|
+
"build": "tsc -p tsconfig.build.json && chmod +x dist/cli/index.js",
|
|
36
|
+
"typecheck": "npm --prefix ../core run build && npm --prefix ../compiler run build && npm --prefix ../vite run build && npm run build && tsc --noEmit -p tsconfig.json"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
40
|
-
"@scenoco-three/compiler": "^0.
|
|
41
|
-
"@scenoco-three/vite": "^0.
|
|
40
|
+
"@scenoco-three/compiler": "^0.4.0",
|
|
41
|
+
"@scenoco-three/vite": "^0.4.0",
|
|
42
42
|
"chokidar": "^4.0.3",
|
|
43
43
|
"ws": "^8.18.0"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"@scenoco-three/core": "^0.
|
|
46
|
+
"@scenoco-three/core": "^0.4.0",
|
|
47
47
|
"three": ">=0.160",
|
|
48
48
|
"vite": ">=5"
|
|
49
49
|
},
|