lecodes-3d-editor 0.1.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.
@@ -0,0 +1,26 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>LeCodes Scene Editor</title>
7
+ <style>
8
+ /* Theme tokens (--bg, --accent, --font-ui, …) come from ui-kit, imported in main.ts. */
9
+ * { box-sizing: border-box; margin: 0; }
10
+ html, body, #app { height: 100%; }
11
+ body {
12
+ background: var(--bg);
13
+ color: var(--text);
14
+ font-family: var(--font-ui);
15
+ font-size: 14px;
16
+ overflow: hidden;
17
+ }
18
+ #app { display: flex; flex-direction: column; }
19
+ </style>
20
+ <script type="module" crossorigin src="/assets/index-DZc_N5Kk.js"></script>
21
+ <link rel="stylesheet" crossorigin href="/assets/index-BuGYQX6Z.css">
22
+ </head>
23
+ <body>
24
+ <div id="app"></div>
25
+ </body>
26
+ </html>
package/dist/server.js ADDED
@@ -0,0 +1,503 @@
1
+ // src/server/index.ts
2
+ import { createServer } from "node:http";
3
+ import { existsSync as existsSync2, readFileSync as readFileSync2, statSync as statSync2, watch } from "node:fs";
4
+ import { dirname, extname, join as join2, normalize, resolve as resolve2, sep as sep2 } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ // src/server/handlers.ts
8
+ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
9
+ import { join, resolve, sep } from "node:path";
10
+
11
+ // src/sceneDocUtils.ts
12
+ var ASPECT_RE = /(?:export\s+)?class\s+([A-Za-z_$][\w$]*)\s+extends\s+Aspect\s*</g;
13
+ var classBody = (text, from) => {
14
+ let angle = 1;
15
+ let i = from;
16
+ for (;i < text.length && angle > 0; i++) {
17
+ if (text[i] === "<")
18
+ angle++;
19
+ else if (text[i] === ">")
20
+ angle--;
21
+ }
22
+ const open = text.indexOf("{", i);
23
+ if (open < 0)
24
+ return "";
25
+ let depth = 0;
26
+ for (let j = open;j < text.length; j++) {
27
+ if (text[j] === "{")
28
+ depth++;
29
+ else if (text[j] === "}" && --depth === 0)
30
+ return text.slice(open + 1, j);
31
+ }
32
+ return "";
33
+ };
34
+ var PROP_LINE_RE = /^\s*(?:readonly\s+)?([A-Za-z_$][\w$]*)\s*[?!]?\s*(?::\s*([^=;]+?))?\s*(?:=(?!=)|;|$)/;
35
+ var extractFieldHints = (body) => {
36
+ const hints = [];
37
+ let depth = 0;
38
+ let doc = null;
39
+ let pendingDoc;
40
+ for (const line of body.split(`
41
+ `)) {
42
+ const trimmed = line.trim();
43
+ if (doc) {
44
+ const end = trimmed.indexOf("*/");
45
+ doc.push(end >= 0 ? trimmed.slice(0, end) : trimmed);
46
+ if (end >= 0) {
47
+ pendingDoc = doc.map((l) => l.replace(/^[/*\s]+/, "").trim()).filter(Boolean).join(" ");
48
+ doc = null;
49
+ }
50
+ continue;
51
+ }
52
+ if (depth === 0) {
53
+ if (trimmed.startsWith("/**")) {
54
+ const oneLine = trimmed.indexOf("*/");
55
+ if (oneLine >= 0)
56
+ pendingDoc = trimmed.slice(3, oneLine).trim();
57
+ else
58
+ doc = [trimmed.slice(3)];
59
+ continue;
60
+ }
61
+ const m = trimmed.length > 0 && !trimmed.includes("(") ? PROP_LINE_RE.exec(line) : null;
62
+ if (m) {
63
+ const [, key, typeText] = m;
64
+ const parts = typeText?.split("|").map((p) => p.trim()) ?? [];
65
+ const literals = parts.map((p) => /^['"]([^'"]*)['"]$/.exec(p)?.[1]);
66
+ const options = parts.length >= 2 && literals.every((l) => l !== undefined) ? literals : undefined;
67
+ const node = parts.length > 0 && parts.every((p) => p === "Node" || p === "null") && parts.includes("Node") ? true : undefined;
68
+ if (options || pendingDoc || node)
69
+ hints.push({ key, options, tooltip: pendingDoc, node });
70
+ }
71
+ if (trimmed.length > 0 && !trimmed.startsWith("//"))
72
+ pendingDoc = undefined;
73
+ }
74
+ for (const ch of line) {
75
+ if (ch === "{")
76
+ depth++;
77
+ else if (ch === "}")
78
+ depth = Math.max(0, depth - 1);
79
+ }
80
+ }
81
+ return hints;
82
+ };
83
+ var scanUserAspects = (files) => {
84
+ const out = [];
85
+ for (const file of files) {
86
+ if (!file.path.endsWith(".ts") || file.path.endsWith(".scene.ts"))
87
+ continue;
88
+ if (!file.text.includes("extends Aspect"))
89
+ continue;
90
+ for (const m of file.text.matchAll(ASPECT_RE)) {
91
+ const hints = extractFieldHints(classBody(file.text, m.index + m[0].length));
92
+ out.push({ className: m[1], path: file.path, hints: hints.length > 0 ? hints : undefined });
93
+ }
94
+ }
95
+ return out;
96
+ };
97
+
98
+ // src/server/handlers.ts
99
+ var TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i;
100
+ var listProjectFiles = (dir, prefix = "") => {
101
+ const out = [];
102
+ for (const name of readdirSync(dir)) {
103
+ if (name === "node_modules" || name.startsWith("."))
104
+ continue;
105
+ const full = join(dir, name);
106
+ const rel = prefix ? `${prefix}/${name}` : name;
107
+ if (statSync(full).isDirectory())
108
+ out.push(...listProjectFiles(full, rel));
109
+ else
110
+ out.push(rel);
111
+ }
112
+ return out;
113
+ };
114
+ var projectOverview = (projectDir) => {
115
+ const files = listProjectFiles(projectDir);
116
+ const scenes = files.filter((f) => f.endsWith(".scene.ts")).map((f) => "./" + f);
117
+ const models = files.filter((f) => /\.(glb|gltf)$/i.test(f)).map((f) => "./" + f);
118
+ const aspects = scanUserAspects(files.filter((f) => TEXT_EXT.test(f)).map((f) => ({
119
+ path: "./" + f,
120
+ text: readFileSync(join(projectDir, f), "utf8")
121
+ })));
122
+ return { dir: projectDir, scenes, aspects, models };
123
+ };
124
+ var resolveInside = (projectDir, path) => {
125
+ const rel = path.replace(/^\.?[/\\]/, "").replace(/\\/g, "/");
126
+ const full = resolve(projectDir, rel);
127
+ if (full !== projectDir && !full.startsWith(resolve(projectDir) + sep))
128
+ return null;
129
+ return full;
130
+ };
131
+ var readBody = (req) => new Promise((res, rej) => {
132
+ let body = "";
133
+ req.on("data", (c) => {
134
+ body += c;
135
+ });
136
+ req.on("end", () => res(body));
137
+ req.on("error", rej);
138
+ });
139
+ var sendJson = (res, data, status = 200) => {
140
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
141
+ res.end(JSON.stringify(data));
142
+ };
143
+ var handleSceneApi = async (host, req, res) => {
144
+ const { projectDir } = host;
145
+ const url = new URL(req.url ?? "/", "http://localhost");
146
+ const path = url.pathname;
147
+ try {
148
+ if (path === "/api/project" && req.method === "GET") {
149
+ sendJson(res, projectOverview(projectDir));
150
+ return true;
151
+ }
152
+ if (path === "/api/file" && req.method === "GET") {
153
+ const full = resolveInside(projectDir, url.searchParams.get("path") ?? "");
154
+ if (!full || !existsSync(full)) {
155
+ sendJson(res, { error: "not found" }, 404);
156
+ return true;
157
+ }
158
+ sendJson(res, { text: readFileSync(full, "utf8") });
159
+ return true;
160
+ }
161
+ if (path === "/api/file" && req.method === "POST") {
162
+ const { path: rel, text } = JSON.parse(await readBody(req));
163
+ const full = resolveInside(projectDir, rel);
164
+ if (!full || typeof text !== "string") {
165
+ sendJson(res, { error: "bad request" }, 400);
166
+ return true;
167
+ }
168
+ writeFileSync(full, text, "utf8");
169
+ sendJson(res, { ok: true });
170
+ return true;
171
+ }
172
+ if (path === "/api/compile" && req.method === "POST") {
173
+ const { path: scenePath } = JSON.parse(await readBody(req));
174
+ if (typeof scenePath !== "string") {
175
+ sendJson(res, { js: null, error: "no scene path" });
176
+ return true;
177
+ }
178
+ try {
179
+ sendJson(res, await host.compile(scenePath));
180
+ } catch (e) {
181
+ sendJson(res, { js: null, error: e?.message?.replace(/mem:/g, "") ?? String(e) });
182
+ }
183
+ return true;
184
+ }
185
+ if (path.startsWith("/project/") && req.method === "GET") {
186
+ const full = resolveInside(projectDir, decodeURIComponent(path.slice("/project/".length)));
187
+ if (!full || !existsSync(full)) {
188
+ res.statusCode = 404;
189
+ res.end("Not found");
190
+ return true;
191
+ }
192
+ res.writeHead(200, { "content-type": "application/octet-stream", "cache-control": "no-store" });
193
+ res.end(readFileSync(full));
194
+ return true;
195
+ }
196
+ } catch (e) {
197
+ sendJson(res, { error: e?.message ?? String(e) }, 500);
198
+ return true;
199
+ }
200
+ return false;
201
+ };
202
+
203
+ // src/server/mcp.ts
204
+ var PROTOCOL_VERSION = "2025-06-18";
205
+ var KNOWN_PROTOCOLS = new Set(["2024-11-05", "2025-03-26", "2025-06-18"]);
206
+ var ok = (id, result) => ({ jsonrpc: "2.0", id, result });
207
+ var err = (id, code, message) => ({ jsonrpc: "2.0", id, error: { code, message } });
208
+ var text = (s) => ({ type: "text", text: s });
209
+ var asStringArray = (value) => Array.isArray(value) && value.every((v) => typeof v === "string") ? value : undefined;
210
+ var TOOLS = [
211
+ {
212
+ name: "scene_state",
213
+ description: "The live project the editor sees: every `.scene.ts` file, every GLB/glTF model, and every " + "user aspect class (extends Aspect) with its syntactic field hints — string-literal-union " + "fields → select options, a JSDoc → the field tooltip, a `Node | null` field → a node " + "reference. Read this before adding nodes/aspects to a scene: it tells you which aspects and " + "models exist and what each aspect's editable fields are, which re-reading the source doesn't " + "hand you pre-synthesized. Editing a scene's nodes stays with your own Read/Edit of the " + "`.scene.ts` (a declarative defineScene).",
214
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
215
+ run: (_args, ctx) => {
216
+ const { dir: _dir, ...rest } = ctx.overview();
217
+ return { content: [text(JSON.stringify(rest, null, 2))] };
218
+ }
219
+ },
220
+ {
221
+ name: "check_scene",
222
+ description: "Compile scenes through the editor's pipeline and report ok / compile-error per scene — the fast " + "'did I break anything?' loop after editing a .scene.ts or an aspect. No rendering. Omit " + "`paths` to check every scene. A compile error's message points at the offending source.",
223
+ inputSchema: {
224
+ type: "object",
225
+ properties: {
226
+ paths: {
227
+ type: "array",
228
+ items: { type: "string" },
229
+ description: 'Scene paths to check, e.g. "./city.scene.ts" (default: all scenes).'
230
+ }
231
+ },
232
+ additionalProperties: false
233
+ },
234
+ run: async (args, ctx) => {
235
+ const all = ctx.listScenes();
236
+ const requested = asStringArray(args.paths)?.map((p) => p.startsWith("./") ? p : "./" + p.replace(/^\//, ""));
237
+ const unknown = requested?.filter((p) => !all.includes(p)) ?? [];
238
+ const paths = requested ? requested.filter((p) => all.includes(p)) : all;
239
+ const scenes = await Promise.all(paths.map(async (path) => {
240
+ try {
241
+ const compiled = await ctx.compile(path);
242
+ return compiled.js !== null && !compiled.error ? { path, ok: true } : { path, ok: false, error: compiled.error ?? "compile produced no output" };
243
+ } catch (e) {
244
+ return { path, ok: false, error: e instanceof Error ? e.message : String(e) };
245
+ }
246
+ }));
247
+ const summary = {
248
+ ok: scenes.every((s) => s.ok),
249
+ checked: scenes.length,
250
+ failed: scenes.filter((s) => !s.ok).length,
251
+ scenes,
252
+ ...unknown.length ? { unknownPaths: unknown } : {}
253
+ };
254
+ return { content: [text(JSON.stringify(summary, null, 2))], isError: !summary.ok };
255
+ }
256
+ }
257
+ ];
258
+ var listToolsResult = () => ({
259
+ tools: TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema }))
260
+ });
261
+ var callTool = async (params, ctx) => {
262
+ const { name, arguments: args } = params ?? {};
263
+ const tool = TOOLS.find((t) => t.name === name);
264
+ if (!tool)
265
+ return { content: [text(`Unknown tool: ${String(name)}`)], isError: true };
266
+ try {
267
+ return await tool.run(args ?? {}, ctx);
268
+ } catch (e) {
269
+ return { content: [text(e instanceof Error ? e.message : String(e))], isError: true };
270
+ }
271
+ };
272
+ var handleOne = async (msg, ctx) => {
273
+ const id = msg && typeof msg === "object" && "id" in msg ? msg.id : undefined;
274
+ const isNotification = id === undefined;
275
+ if (!msg || typeof msg !== "object" || typeof msg.method !== "string") {
276
+ return isNotification ? undefined : err(id ?? null, -32600, "Invalid Request");
277
+ }
278
+ const method = msg.method;
279
+ if (isNotification)
280
+ return;
281
+ switch (method) {
282
+ case "initialize": {
283
+ const requested = msg.params?.protocolVersion;
284
+ const protocolVersion = typeof requested === "string" && KNOWN_PROTOCOLS.has(requested) ? requested : PROTOCOL_VERSION;
285
+ return ok(id, {
286
+ protocolVersion,
287
+ capabilities: { tools: {} },
288
+ serverInfo: { name: ctx.serverName, version: ctx.version }
289
+ });
290
+ }
291
+ case "ping":
292
+ return ok(id, {});
293
+ case "tools/list":
294
+ return ok(id, listToolsResult());
295
+ case "tools/call":
296
+ return ok(id, await callTool(msg.params, ctx));
297
+ default:
298
+ return err(id, -32601, `Method not found: ${method}`);
299
+ }
300
+ };
301
+ var handleMcpMessage = async (message, ctx) => {
302
+ if (Array.isArray(message)) {
303
+ if (message.length === 0)
304
+ return err(null, -32600, "Invalid Request");
305
+ const responses = [];
306
+ for (const m of message) {
307
+ const r = await handleOne(m, ctx);
308
+ if (r !== undefined)
309
+ responses.push(r);
310
+ }
311
+ return responses.length ? responses : undefined;
312
+ }
313
+ return handleOne(message, ctx);
314
+ };
315
+
316
+ // src/server/index.ts
317
+ var MCP_VERSION = "1.0.0";
318
+ var readBody2 = (req) => new Promise((resolveBody, rejectBody) => {
319
+ let data = "";
320
+ req.on("data", (chunk) => {
321
+ data += chunk;
322
+ });
323
+ req.on("end", () => {
324
+ try {
325
+ resolveBody(JSON.parse(data || "{}"));
326
+ } catch (e) {
327
+ rejectBody(e);
328
+ }
329
+ });
330
+ req.on("error", rejectBody);
331
+ });
332
+ var MIME = {
333
+ ".html": "text/html; charset=utf-8",
334
+ ".js": "text/javascript; charset=utf-8",
335
+ ".mjs": "text/javascript; charset=utf-8",
336
+ ".css": "text/css; charset=utf-8",
337
+ ".json": "application/json; charset=utf-8",
338
+ ".svg": "image/svg+xml",
339
+ ".png": "image/png",
340
+ ".jpg": "image/jpeg",
341
+ ".jpeg": "image/jpeg",
342
+ ".gif": "image/gif",
343
+ ".webp": "image/webp",
344
+ ".avif": "image/avif",
345
+ ".ico": "image/x-icon",
346
+ ".wasm": "application/wasm",
347
+ ".woff": "font/woff",
348
+ ".woff2": "font/woff2",
349
+ ".ttf": "font/ttf",
350
+ ".otf": "font/otf",
351
+ ".bin": "application/octet-stream"
352
+ };
353
+ var sendFile = (res, baseDir, relPath) => {
354
+ const abs = resolve2(baseDir, "." + sep2 + normalize(relPath).replace(/^([/\\])+/, ""));
355
+ if (!abs.startsWith(resolve2(baseDir) + sep2) && abs !== resolve2(baseDir))
356
+ return false;
357
+ if (!existsSync2(abs) || !statSync2(abs).isFile())
358
+ return false;
359
+ res.writeHead(200, {
360
+ "content-type": MIME[extname(abs).toLowerCase()] ?? "application/octet-stream",
361
+ "cache-control": "no-store"
362
+ });
363
+ res.end(readFileSync2(abs));
364
+ return true;
365
+ };
366
+ var startSceneServer = async (options) => {
367
+ const port = options.port ?? 4488;
368
+ const log = options.log ?? (() => {});
369
+ const appDir = options.appDir ?? join2(dirname(fileURLToPath(import.meta.url)), "app");
370
+ const host = { projectDir: resolve2(options.root), compile: options.compile };
371
+ const clients = new Set;
372
+ const broadcast = (event) => {
373
+ const payload = `data: ${JSON.stringify(event)}
374
+
375
+ `;
376
+ for (const res of clients)
377
+ res.write(payload);
378
+ };
379
+ let pending = new Map;
380
+ let timer = null;
381
+ const flush = () => {
382
+ timer = null;
383
+ const events = [...pending];
384
+ pending = new Map;
385
+ for (const [path, type] of events)
386
+ broadcast({ type, path });
387
+ log(`change → ${events.map(([p]) => p).join(", ")}`);
388
+ };
389
+ const onFsEvent = (file) => {
390
+ if (!file) {
391
+ pending.set("./", "project");
392
+ } else {
393
+ const rel = "./" + file.replace(/\\/g, "/");
394
+ const type = /\.ts$/.test(rel) ? "scene" : "project";
395
+ pending.set(rel, type);
396
+ }
397
+ if (timer)
398
+ clearTimeout(timer);
399
+ timer = setTimeout(flush, 120);
400
+ };
401
+ let watcher = null;
402
+ try {
403
+ watcher = watch(host.projectDir, { recursive: true }, (_type, file) => onFsEvent(file));
404
+ } catch {
405
+ log("fs.watch(recursive) unavailable — hot reload disabled (refresh the browser manually).");
406
+ }
407
+ const mcpContext = {
408
+ serverName: "lecodes-scene",
409
+ version: MCP_VERSION,
410
+ overview: () => projectOverview(host.projectDir),
411
+ listScenes: () => projectOverview(host.projectDir).scenes,
412
+ compile: host.compile
413
+ };
414
+ const server = createServer(async (req, res) => {
415
+ const url = new URL(req.url ?? "/", "http://localhost");
416
+ const path = decodeURIComponent(url.pathname);
417
+ try {
418
+ if (await handleSceneApi(host, req, res))
419
+ return;
420
+ if (path === "/mcp") {
421
+ if (req.method !== "POST") {
422
+ res.writeHead(405, { allow: "POST", "content-type": "text/plain" });
423
+ res.end("MCP endpoint accepts POST only.");
424
+ return;
425
+ }
426
+ const origin = req.headers.origin;
427
+ if (origin && !/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin)) {
428
+ res.writeHead(403, { "content-type": "text/plain" });
429
+ res.end("Cross-origin requests are not allowed.");
430
+ return;
431
+ }
432
+ let message;
433
+ try {
434
+ message = await readBody2(req);
435
+ } catch {
436
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
437
+ res.end(JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } }));
438
+ return;
439
+ }
440
+ const response = await handleMcpMessage(message, mcpContext);
441
+ if (response === undefined) {
442
+ res.writeHead(202);
443
+ res.end();
444
+ return;
445
+ }
446
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
447
+ res.end(JSON.stringify(response));
448
+ return;
449
+ }
450
+ if (path === "/events") {
451
+ res.writeHead(200, {
452
+ "content-type": "text/event-stream",
453
+ "cache-control": "no-store",
454
+ connection: "keep-alive"
455
+ });
456
+ res.write(`retry: 1000
457
+
458
+ `);
459
+ clients.add(res);
460
+ const ping = setInterval(() => res.write(`: ping
461
+
462
+ `), 30000);
463
+ req.on("close", () => {
464
+ clients.delete(res);
465
+ clearInterval(ping);
466
+ });
467
+ return;
468
+ }
469
+ if (path !== "/" && sendFile(res, appDir, path))
470
+ return;
471
+ if (!sendFile(res, appDir, "index.html")) {
472
+ res.writeHead(503, { "content-type": "text/plain" });
473
+ res.end("The scene editor app is not built (missing dist/app). Reinstall scene-editor or run its build.");
474
+ }
475
+ } catch (e) {
476
+ res.writeHead(500, { "content-type": "application/json" });
477
+ res.end(JSON.stringify({ error: e instanceof Error ? e.message : String(e) }));
478
+ }
479
+ });
480
+ await new Promise((resolvePromise, reject) => {
481
+ server.once("error", reject);
482
+ server.listen(port, "127.0.0.1", () => resolvePromise());
483
+ });
484
+ return {
485
+ port,
486
+ url: `http://localhost:${port}`,
487
+ close: () => {
488
+ watcher?.close();
489
+ for (const res of clients)
490
+ res.end();
491
+ clients.clear();
492
+ server.close();
493
+ }
494
+ };
495
+ };
496
+ export {
497
+ startSceneServer,
498
+ resolveInside,
499
+ projectOverview,
500
+ listProjectFiles,
501
+ handleSceneApi,
502
+ TEXT_EXT
503
+ };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "lecodes-3d-editor",
3
+ "version": "0.1.0",
4
+ "description": "LeCodes Scene Editor — the visual .scene.ts editor (Vue components + a standalone host). Embedded by the platform frontend as raw source; hosted on local files by the lecodes CLI (`lecodes scene`), like lecodes-design.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "main": "src/index.ts",
8
+ "exports": {
9
+ ".": "./src/index.ts",
10
+ "./server": "./dist/server.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "dev": "bunx --bun vite",
18
+ "build": "vite build && bun build src/server/index.ts --target=node --outfile dist/server.js",
19
+ "typecheck": "bunx vue-tsc --noEmit",
20
+ "test": "bun test",
21
+ "prepublishOnly": "bun run build"
22
+ },
23
+ "devDependencies": {
24
+ "@vitejs/plugin-vue": "^5.2.3",
25
+ "scene-doc": "workspace:*",
26
+ "sdk": "workspace:*",
27
+ "typescript": "~5.8.3",
28
+ "ui-kit": "workspace:*",
29
+ "viewer": "workspace:*",
30
+ "vite": "^8.0.16",
31
+ "vue": "^3.5.13",
32
+ "vue-tsc": "^2.2.8"
33
+ }
34
+ }
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ // scene-editor: the visual .scene.ts editor as an embeddable Vue component. The host supplies a
2
+ // SceneSource (file text + write + edit-mode compile + aspect list); everything else — document
3
+ // round-trip (scene-doc), GL host lifecycle (viewer), harness wiring, tree/inspector UI — is
4
+ // self-contained. `bun run dev` in this package runs the standalone auth-free playground.
5
+
6
+ export { default as SceneEditor } from "./SceneEditor.vue"
7
+ export { useSceneEditor, type SceneEditorCtl, type ScenePhase } from "./useSceneEditor"
8
+ export { scanUserAspects, docRows, findDocNode, resolveScenePath, BUILTIN_ASPECTS, type SceneRow } from "./sceneDocUtils"
9
+ export {
10
+ sceneGlobals,
11
+ type AspectClassInfo,
12
+ type CompiledScene,
13
+ type FieldDescriptor,
14
+ type FieldEditor,
15
+ type SceneHarness,
16
+ type SceneSource,
17
+ type UserAspect,
18
+ type Vec3Tuple,
19
+ } from "./types"