phibelle-kit 1.0.22 → 1.0.24

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.
Files changed (36) hide show
  1. package/dist/commands/clone-scene.d.ts +1 -5
  2. package/dist/commands/clone-scene.js +124 -31
  3. package/dist/commands/init-scene.d.ts +1 -0
  4. package/dist/commands/init-scene.js +16 -0
  5. package/dist/commands/watch-scene.d.ts +2 -1
  6. package/dist/commands/watch-scene.js +116 -83
  7. package/dist/constants.d.ts +2 -0
  8. package/dist/constants.js +2 -0
  9. package/dist/index.js +14 -18
  10. package/dist/lib/api/index.d.ts +1 -2
  11. package/dist/lib/api/index.js +1 -2
  12. package/dist/lib/api/public-env.d.ts +0 -1
  13. package/dist/lib/api/public-env.js +0 -1
  14. package/dist/lib/scene/file-system.d.ts +2 -1
  15. package/dist/lib/scene/file-system.js +5 -4
  16. package/dist/lib/scene/first-run-setup.d.ts +5 -1
  17. package/dist/lib/scene/first-run-setup.js +10 -4
  18. package/dist/lib/scene/index.d.ts +1 -2
  19. package/dist/lib/scene/index.js +1 -2
  20. package/dist/lib/scene/scene-packaging.d.ts +9 -15
  21. package/dist/lib/scene/scene-packaging.js +124 -265
  22. package/dist/lib/scene/scene-state.d.ts +42 -0
  23. package/dist/lib/scene/scene-state.js +125 -0
  24. package/dist/lib/scene/script-watcher.d.ts +1 -1
  25. package/dist/lib/scene/script-watcher.js +13 -94
  26. package/dist/lib/ws/index.d.ts +1 -0
  27. package/dist/lib/ws/index.js +1 -0
  28. package/dist/lib/ws/ws-server.d.ts +15 -0
  29. package/dist/lib/ws/ws-server.js +56 -0
  30. package/dist/scene-setup/agents-md.d.ts +1 -1
  31. package/dist/scene-setup/agents-md.js +35 -21
  32. package/dist/scene-setup/npm-package.d.ts +1 -1
  33. package/dist/scene-setup/npm-package.js +2 -2
  34. package/dist/scene-setup/setup.d.ts +1 -1
  35. package/dist/scene-setup/setup.js +6 -6
  36. package/package.json +3 -4
@@ -1,24 +1,10 @@
1
1
  import * as fs from "fs/promises";
2
2
  import * as path from "path";
3
3
  import { getSceneAppDir } from "./file-system.js";
4
- import { queryOnce, mutateOnce } from "../api/convex-client.js";
5
- import { api } from "../../convex/_generated/api.js";
6
- import { getSessionId } from "../auth/conf.js";
7
4
  /** Scene-level file names in app/. */
8
5
  export const SCENE_SCRIPT_FILE = "scene-script.tsx";
9
6
  export const SCENE_PROPERTIES_FILE = "scene-properties.json";
10
- async function getSceneFileContent(sceneId) {
11
- const fileUrl = await queryOnce(api.scenes.getSceneFileUrl, { sceneId });
12
- if (fileUrl == null) {
13
- throw new Error("Scene file not found (scene may be missing or have no file yet)");
14
- }
15
- const response = await fetch(fileUrl);
16
- if (!response.ok) {
17
- throw new Error(`Failed to download scene file: ${response.status} ${response.statusText}`);
18
- }
19
- return response.text();
20
- }
21
- function parseSceneJson(content) {
7
+ export function parseSceneJson(content) {
22
8
  try {
23
9
  return JSON.parse(content);
24
10
  }
@@ -43,11 +29,6 @@ export function slugToEntityName(slug) {
43
29
  .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
44
30
  .join(" ");
45
31
  }
46
- /**
47
- * Matches entity folder names: <entity-name>-<engineId> (e.g. main-enemy-1).
48
- * Group 1 = slug, group 2 = engine id.
49
- */
50
- export const ENTITY_FOLDER_RE = /^([a-z0-9]+(?:-[a-z0-9]+)*)-(\d+)$/;
51
32
  /** Entity filenames inside an entity folder. */
52
33
  export const ENTITY_SCRIPT_FILE = "script.tsx";
53
34
  export const ENTITY_PROPERTIES_FILE = "properties.json";
@@ -55,57 +36,41 @@ export const ENTITY_TRANSFORMS_FILE = "transforms.json";
55
36
  const DEFAULT_POSITION = '{"x":0,"y":0,"z":0}';
56
37
  const DEFAULT_ROTATION = '{"isEuler":true,"_x":0,"_y":0,"_z":0,"_order":"XYZ"}';
57
38
  const DEFAULT_SCALE = '{"x":1,"y":1,"z":1}';
58
- async function uploadSceneJsonAndUpdate(sceneId, json) {
59
- const uploadUrl = await mutateOnce(api.storage.generateUploadUrl, {});
60
- if (typeof uploadUrl !== "string") {
61
- throw new Error("Invalid upload URL response");
62
- }
63
- const uploadResult = await fetch(uploadUrl, {
64
- method: "POST",
65
- headers: { "Content-Type": "application/json" },
66
- body: json,
67
- });
68
- if (!uploadResult.ok) {
69
- throw new Error(`Failed to upload scene file: ${uploadResult.status} ${uploadResult.statusText}`);
70
- }
71
- const uploadResponse = (await uploadResult.json());
72
- const storageId = uploadResponse.storageId;
73
- if (!storageId) {
74
- throw new Error("Upload response missing storageId");
75
- }
76
- const sceneByteLength = Buffer.byteLength(json, "utf8");
77
- await mutateOnce(api.scenes.update, {
78
- id: sceneId,
79
- sceneFileId: storageId,
80
- sceneByteLength,
81
- lastModifiedSessionId: getSessionId(),
82
- });
83
- }
84
- export async function unpackageScene(sceneId, sceneDir) {
85
- const content = await getSceneFileContent(sceneId);
86
- const parsed = parseSceneJson(content);
39
+ /** Unpackage scene JSON to filesystem. No DB; accepts full scene JSON string. */
40
+ export async function unpackageScene(sceneJson, sceneDir, sceneId) {
41
+ const parsed = parseSceneJson(sceneJson);
42
+ const parsedSceneId = typeof parsed._id === "string"
43
+ ? parsed._id
44
+ : "";
87
45
  const appDir = getSceneAppDir(sceneDir);
88
46
  await fs.mkdir(appDir, { recursive: true });
89
- // Remove existing entity files/folders and legacy scene.tsx so entities deleted in the editor are removed from disk
90
47
  try {
91
48
  const entries = await fs.readdir(appDir, { withFileTypes: true });
92
49
  for (const ent of entries) {
93
50
  if (ent.isFile()) {
94
51
  if (ent.name === "scene.tsx")
95
52
  await fs.unlink(path.join(appDir, ent.name));
96
- else if (ENTITY_FOLDER_RE.test(ent.name.replace(/\.tsx$/, "")))
53
+ else if (ent.name.endsWith(".tsx") && ent.name !== SCENE_SCRIPT_FILE)
97
54
  await fs.unlink(path.join(appDir, ent.name));
98
55
  }
99
- if (ent.isDirectory() && ENTITY_FOLDER_RE.test(ent.name)) {
100
- await fs.rm(path.join(appDir, ent.name), { recursive: true });
56
+ if (ent.isDirectory()) {
57
+ const scriptPath = path.join(appDir, ent.name, ENTITY_SCRIPT_FILE);
58
+ try {
59
+ await fs.access(scriptPath);
60
+ await fs.rm(path.join(appDir, ent.name), { recursive: true });
61
+ }
62
+ catch {
63
+ /* no script.tsx, leave dir alone */
64
+ }
101
65
  }
102
66
  }
103
67
  }
104
68
  catch {
105
- // Directory might not exist or be empty; ignore
69
+ /* directory might not exist or be empty */
106
70
  }
107
71
  const entities = parsed.entityStore?.entities ?? {};
108
72
  const manifest = {
73
+ sceneId: sceneId?.trim() || parsedSceneId,
109
74
  sceneScript: SCENE_SCRIPT_FILE,
110
75
  sceneProperties: SCENE_PROPERTIES_FILE,
111
76
  entities: {},
@@ -113,25 +78,28 @@ export async function unpackageScene(sceneId, sceneDir) {
113
78
  await fs.writeFile(path.join(appDir, SCENE_SCRIPT_FILE), parsed.sceneScriptStore?.sceneScript ?? "", "utf8");
114
79
  const scenePropertiesJson = JSON.stringify(parsed.sceneScriptStore?.sceneProperties ?? [], null, 2);
115
80
  await fs.writeFile(path.join(appDir, SCENE_PROPERTIES_FILE), scenePropertiesJson, "utf8");
81
+ const usedFolderNames = new Set();
116
82
  for (const idStr of Object.keys(entities)) {
117
83
  const entity = entities[idStr];
118
84
  if (!entity || typeof entity.script !== "string")
119
85
  continue;
120
86
  const engineId = String(entity.engineId ?? idStr);
121
- let slug = entityNameToSlug(entity.name ?? "Entity");
122
- const redundantPrefix = `entity-${engineId}`;
123
- if (slug === redundantPrefix)
124
- slug = "entity";
125
- else if (slug.startsWith(redundantPrefix + "-"))
126
- slug = slug.slice((redundantPrefix + "-").length);
127
- const folderName = `${slug}-${engineId}`;
87
+ const baseName = entity.name ?? "Entity";
88
+ let folderName = baseName;
89
+ if (usedFolderNames.has(folderName))
90
+ folderName = `${baseName}-${engineId}`;
91
+ usedFolderNames.add(folderName);
128
92
  const entityDir = path.join(appDir, folderName);
129
93
  await fs.mkdir(entityDir, { recursive: true });
130
94
  manifest.entities[engineId] = folderName;
131
95
  await fs.writeFile(path.join(entityDir, ENTITY_SCRIPT_FILE), entity.script, "utf8");
132
96
  const propertiesJson = JSON.stringify(entity.properties ?? [], null, 2);
133
97
  await fs.writeFile(path.join(entityDir, ENTITY_PROPERTIES_FILE), propertiesJson, "utf8");
134
- const transform = entity.transform ?? { position: DEFAULT_POSITION, rotation: DEFAULT_ROTATION, scale: DEFAULT_SCALE };
98
+ const transform = entity.transform ?? {
99
+ position: DEFAULT_POSITION,
100
+ rotation: DEFAULT_ROTATION,
101
+ scale: DEFAULT_SCALE,
102
+ };
135
103
  const transformsData = {
136
104
  position: JSON.parse(transform.position),
137
105
  rotation: JSON.parse(transform.rotation),
@@ -142,10 +110,6 @@ export async function unpackageScene(sceneId, sceneDir) {
142
110
  await fs.mkdir(sceneDir, { recursive: true });
143
111
  await fs.writeFile(path.join(sceneDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
144
112
  }
145
- /**
146
- * Resolve entity id from a path like "main-enemy-1/script.tsx" or "main-enemy-1/properties.json".
147
- * Accepts both / and \ as separators. Returns the engineId (string) if the path is an entity file, otherwise null.
148
- */
149
113
  function getEntityIdFromRelativePath(manifest, relativePath) {
150
114
  const normalized = relativePath.replace(/\\/g, "/");
151
115
  const match = normalized.match(/^([^/]+)\/(.+)$/);
@@ -155,229 +119,123 @@ function getEntityIdFromRelativePath(manifest, relativePath) {
155
119
  const entityId = Object.entries(manifest.entities).find(([, folder]) => folder === folderName)?.[0];
156
120
  return entityId ?? null;
157
121
  }
158
- export async function pushScriptChange(sceneId, sceneDir, changedFile) {
159
- const manifestPath = path.join(sceneDir, "manifest.json");
160
- let manifestRaw;
161
- try {
162
- manifestRaw = await fs.readFile(manifestPath, "utf8");
163
- }
164
- catch {
165
- throw new Error("manifest.json not found; run watch-scene to unpackage first");
166
- }
122
+ /**
123
+ * Apply a single file change to the in-memory scene and return updated JSON.
124
+ * Used by script watcher to produce scene snapshot to send to FE.
125
+ */
126
+ export async function patchSceneFromFile(sceneJson, sceneDir, relativePath, eventType) {
127
+ const manifestRaw = await fs.readFile(path.join(sceneDir, "manifest.json"), "utf8");
167
128
  const manifest = JSON.parse(manifestRaw);
168
- const content = await getSceneFileContent(sceneId);
169
- const parsed = parseSceneJson(content);
129
+ const parsed = parseSceneJson(sceneJson);
170
130
  const appDir = getSceneAppDir(sceneDir);
171
- const filePath = path.join(appDir, changedFile);
172
- let fileContent;
173
- try {
174
- fileContent = await fs.readFile(filePath, "utf8");
175
- }
176
- catch {
177
- throw new Error(`Could not read file: ${changedFile}`);
178
- }
179
- const isSceneScript = manifest.sceneScript === changedFile;
180
- if (isSceneScript) {
131
+ const filePath = path.join(appDir, relativePath);
132
+ if (relativePath === manifest.sceneScript || relativePath === SCENE_SCRIPT_FILE) {
133
+ const fileContent = await fs.readFile(filePath, "utf8");
181
134
  if (!parsed.sceneScriptStore)
182
135
  parsed.sceneScriptStore = { sceneScript: "", sceneProperties: [], lastModifiedSceneScript: 0 };
183
136
  parsed.sceneScriptStore.sceneScript = fileContent;
184
- if (typeof parsed.sceneScriptStore.lastModifiedSceneScript === "number") {
185
- parsed.sceneScriptStore.lastModifiedSceneScript = Date.now();
186
- }
137
+ parsed.sceneScriptStore.lastModifiedSceneScript = Date.now();
138
+ return JSON.stringify(parsed);
187
139
  }
188
- else {
189
- const entityId = getEntityIdFromRelativePath(manifest, changedFile);
190
- if (entityId == null) {
191
- throw new Error(`Unknown script file: ${changedFile}`);
140
+ if (relativePath === (manifest.sceneProperties ?? SCENE_PROPERTIES_FILE)) {
141
+ const fileContent = await fs.readFile(filePath, "utf8");
142
+ const sceneProperties = JSON.parse(fileContent);
143
+ if (!parsed.sceneScriptStore)
144
+ parsed.sceneScriptStore = { sceneScript: "", sceneProperties: [], lastModifiedSceneScript: 0 };
145
+ parsed.sceneScriptStore.sceneProperties = Array.isArray(sceneProperties) ? sceneProperties : [];
146
+ return JSON.stringify(parsed);
147
+ }
148
+ const scriptMatch = relativePath.match(new RegExp(`^([^/]+)\\/${ENTITY_SCRIPT_FILE.replace(".", "\\.")}$`));
149
+ if (scriptMatch) {
150
+ const folderName = scriptMatch[1];
151
+ const fileContent = await fs.readFile(filePath, "utf8");
152
+ const existingId = Object.entries(manifest.entities).find(([, f]) => f === folderName)?.[0];
153
+ if (existingId != null) {
154
+ const entities = parsed.entityStore?.entities ?? {};
155
+ const entity = entities[existingId];
156
+ if (!entity)
157
+ return sceneJson;
158
+ entity.script = fileContent;
159
+ entity.scriptVersion = (entity.scriptVersion ?? 0) + 1;
160
+ return JSON.stringify(parsed);
192
161
  }
162
+ if (eventType === "add") {
163
+ return addNewEntityFromFolder(parsed, manifest, sceneDir, folderName, appDir);
164
+ }
165
+ return sceneJson;
166
+ }
167
+ const propsMatch = relativePath.match(new RegExp(`^([^/]+)\\/${ENTITY_PROPERTIES_FILE.replace(".", "\\.")}$`));
168
+ if (propsMatch) {
169
+ const entityId = getEntityIdFromRelativePath(manifest, relativePath);
170
+ if (entityId == null)
171
+ return sceneJson;
172
+ const fileContent = await fs.readFile(filePath, "utf8");
173
+ const properties = JSON.parse(fileContent);
193
174
  const entities = parsed.entityStore?.entities ?? {};
194
175
  const entity = entities[entityId];
195
- if (!entity) {
196
- throw new Error(`Entity ${entityId} no longer in scene; skip or sync from app`);
197
- }
198
- entity.script = fileContent;
199
- entity.scriptVersion = (entity.scriptVersion ?? 0) + 1;
200
- }
201
- const json = JSON.stringify(parsed);
202
- await uploadSceneJsonAndUpdate(sceneId, json);
203
- }
204
- /** Push entity properties from app/<folder>/properties.json to the scene. */
205
- export async function pushPropertiesChange(sceneId, sceneDir, relativePath) {
206
- const manifestPath = path.join(sceneDir, "manifest.json");
207
- let manifestRaw;
208
- try {
209
- manifestRaw = await fs.readFile(manifestPath, "utf8");
210
- }
211
- catch {
212
- throw new Error("manifest.json not found; run watch-scene to unpackage first");
213
- }
214
- const manifest = JSON.parse(manifestRaw);
215
- const entityId = getEntityIdFromRelativePath(manifest, relativePath);
216
- if (entityId == null) {
217
- throw new Error(`Unknown properties file: ${relativePath}`);
218
- }
219
- const content = await getSceneFileContent(sceneId);
220
- const parsed = parseSceneJson(content);
221
- const appDir = getSceneAppDir(sceneDir);
222
- const filePath = path.join(appDir, relativePath);
223
- let fileContent;
224
- try {
225
- fileContent = await fs.readFile(filePath, "utf8");
226
- }
227
- catch {
228
- throw new Error(`Could not read file: ${relativePath}`);
229
- }
230
- let properties;
231
- try {
232
- properties = JSON.parse(fileContent);
233
- if (!Array.isArray(properties))
234
- properties = [];
235
- }
236
- catch {
237
- throw new Error(`Invalid JSON in ${relativePath}`);
238
- }
239
- const entities = parsed.entityStore?.entities ?? {};
240
- const entity = entities[entityId];
241
- if (!entity) {
242
- throw new Error(`Entity ${entityId} no longer in scene; skip or sync from app`);
243
- }
244
- entity.properties = properties;
245
- const json = JSON.stringify(parsed);
246
- await uploadSceneJsonAndUpdate(sceneId, json);
247
- }
248
- /** Push scene properties from app/scene-properties.json to the scene. */
249
- export async function pushScenePropertiesChange(sceneId, sceneDir) {
250
- const content = await getSceneFileContent(sceneId);
251
- const parsed = parseSceneJson(content);
252
- const appDir = getSceneAppDir(sceneDir);
253
- const filePath = path.join(appDir, SCENE_PROPERTIES_FILE);
254
- let fileContent;
255
- try {
256
- fileContent = await fs.readFile(filePath, "utf8");
257
- }
258
- catch {
259
- throw new Error(`Could not read ${SCENE_PROPERTIES_FILE}`);
260
- }
261
- let sceneProperties;
262
- try {
263
- const parsedProps = JSON.parse(fileContent);
264
- sceneProperties = Array.isArray(parsedProps) ? parsedProps : [];
265
- }
266
- catch {
267
- throw new Error(`Invalid JSON in ${SCENE_PROPERTIES_FILE}`);
268
- }
269
- if (!parsed.sceneScriptStore) {
270
- parsed.sceneScriptStore = { sceneScript: "", sceneProperties: [], lastModifiedSceneScript: 0 };
271
- }
272
- parsed.sceneScriptStore.sceneProperties = sceneProperties;
273
- const json = JSON.stringify(parsed);
274
- await uploadSceneJsonAndUpdate(sceneId, json);
275
- }
276
- /** Push entity transform from app/<folder>/transforms.json to the scene. */
277
- export async function pushTransformsChange(sceneId, sceneDir, relativePath) {
278
- const manifestPath = path.join(sceneDir, "manifest.json");
279
- let manifestRaw;
280
- try {
281
- manifestRaw = await fs.readFile(manifestPath, "utf8");
282
- }
283
- catch {
284
- throw new Error("manifest.json not found; run watch-scene to unpackage first");
285
- }
286
- const manifest = JSON.parse(manifestRaw);
287
- const entityId = getEntityIdFromRelativePath(manifest, relativePath);
288
- if (entityId == null) {
289
- throw new Error(`Unknown transforms file: ${relativePath}`);
290
- }
291
- const content = await getSceneFileContent(sceneId);
292
- const parsed = parseSceneJson(content);
293
- const appDir = getSceneAppDir(sceneDir);
294
- const filePath = path.join(appDir, relativePath);
295
- let fileContent;
296
- try {
297
- fileContent = await fs.readFile(filePath, "utf8");
298
- }
299
- catch {
300
- throw new Error(`Could not read file: ${relativePath}`);
301
- }
302
- let data;
303
- try {
304
- data = JSON.parse(fileContent);
305
- }
306
- catch {
307
- throw new Error(`Invalid JSON in ${relativePath}`);
308
- }
309
- const entities = parsed.entityStore?.entities ?? {};
310
- const entity = entities[entityId];
311
- if (!entity) {
312
- throw new Error(`Entity ${entityId} no longer in scene; skip or sync from app`);
313
- }
314
- if (!entity.transform) {
315
- entity.transform = { position: DEFAULT_POSITION, rotation: DEFAULT_ROTATION, scale: DEFAULT_SCALE };
316
- }
317
- if (data.position !== undefined)
318
- entity.transform.position = JSON.stringify(data.position);
319
- if (data.rotation !== undefined)
320
- entity.transform.rotation = JSON.stringify(data.rotation);
321
- if (data.scale !== undefined)
322
- entity.transform.scale = JSON.stringify(data.scale);
323
- const json = JSON.stringify(parsed);
324
- await uploadSceneJsonAndUpdate(sceneId, json);
176
+ if (!entity)
177
+ return sceneJson;
178
+ entity.properties = Array.isArray(properties) ? properties : [];
179
+ return JSON.stringify(parsed);
180
+ }
181
+ const transformsMatch = relativePath.match(new RegExp(`^([^/]+)\\/${ENTITY_TRANSFORMS_FILE.replace(".", "\\.")}$`));
182
+ if (transformsMatch) {
183
+ const entityId = getEntityIdFromRelativePath(manifest, relativePath);
184
+ if (entityId == null)
185
+ return sceneJson;
186
+ const fileContent = await fs.readFile(filePath, "utf8");
187
+ const data = JSON.parse(fileContent);
188
+ const entities = parsed.entityStore?.entities ?? {};
189
+ const entity = entities[entityId];
190
+ if (!entity)
191
+ return sceneJson;
192
+ if (!entity.transform)
193
+ entity.transform = { position: DEFAULT_POSITION, rotation: DEFAULT_ROTATION, scale: DEFAULT_SCALE };
194
+ if (data.position !== undefined)
195
+ entity.transform.position = JSON.stringify(data.position);
196
+ if (data.rotation !== undefined)
197
+ entity.transform.rotation = JSON.stringify(data.rotation);
198
+ if (data.scale !== undefined)
199
+ entity.transform.scale = JSON.stringify(data.scale);
200
+ return JSON.stringify(parsed);
201
+ }
202
+ return sceneJson;
325
203
  }
326
- /** Add a new entity to the scene when user creates <entity-name>-<engineId>/ with script.tsx (and optional properties.json, transforms.json). */
327
- export async function pushNewEntity(sceneId, sceneDir, engineId, folderName) {
328
- const manifestPath = path.join(sceneDir, "manifest.json");
329
- let manifestRaw;
330
- try {
331
- manifestRaw = await fs.readFile(manifestPath, "utf8");
332
- }
333
- catch {
334
- throw new Error("manifest.json not found; run watch-scene to unpackage first");
335
- }
336
- const manifest = JSON.parse(manifestRaw);
337
- const idStr = String(engineId);
338
- if (manifest.entities[idStr]) {
339
- throw new Error(`Entity ${engineId} already exists in manifest`);
340
- }
341
- const content = await getSceneFileContent(sceneId);
342
- const parsed = parseSceneJson(content);
343
- const appDir = getSceneAppDir(sceneDir);
204
+ async function addNewEntityFromFolder(parsed, manifest, sceneDir, folderName, appDir) {
205
+ if (Object.values(manifest.entities).includes(folderName))
206
+ return JSON.stringify(parsed);
344
207
  const scriptPath = path.join(appDir, folderName, ENTITY_SCRIPT_FILE);
345
208
  let script;
346
209
  try {
347
210
  script = await fs.readFile(scriptPath, "utf8");
348
211
  }
349
212
  catch {
350
- throw new Error(`Could not read ${folderName}/${ENTITY_SCRIPT_FILE}`);
213
+ return JSON.stringify(parsed);
351
214
  }
352
215
  let properties = [];
353
- const propertiesPath = path.join(appDir, folderName, ENTITY_PROPERTIES_FILE);
354
216
  try {
355
- const raw = await fs.readFile(propertiesPath, "utf8");
217
+ const raw = await fs.readFile(path.join(appDir, folderName, ENTITY_PROPERTIES_FILE), "utf8");
356
218
  const parsedProps = JSON.parse(raw);
357
219
  if (Array.isArray(parsedProps))
358
220
  properties = parsedProps;
359
221
  }
360
222
  catch {
361
- // properties.json optional; default to []
223
+ /* optional */
362
224
  }
363
225
  let transform = { position: DEFAULT_POSITION, rotation: DEFAULT_ROTATION, scale: DEFAULT_SCALE };
364
- const transformsPath = path.join(appDir, folderName, ENTITY_TRANSFORMS_FILE);
365
226
  try {
366
- const raw = await fs.readFile(transformsPath, "utf8");
367
- const parsedTransform = JSON.parse(raw);
368
- if (parsedTransform.position !== undefined)
369
- transform.position = JSON.stringify(parsedTransform.position);
370
- if (parsedTransform.rotation !== undefined)
371
- transform.rotation = JSON.stringify(parsedTransform.rotation);
372
- if (parsedTransform.scale !== undefined)
373
- transform.scale = JSON.stringify(parsedTransform.scale);
227
+ const raw = await fs.readFile(path.join(appDir, folderName, ENTITY_TRANSFORMS_FILE), "utf8");
228
+ const t = JSON.parse(raw);
229
+ if (t.position !== undefined)
230
+ transform.position = JSON.stringify(t.position);
231
+ if (t.rotation !== undefined)
232
+ transform.rotation = JSON.stringify(t.rotation);
233
+ if (t.scale !== undefined)
234
+ transform.scale = JSON.stringify(t.scale);
374
235
  }
375
236
  catch {
376
- // transforms.json optional; use defaults
237
+ /* optional */
377
238
  }
378
- const match = folderName.match(ENTITY_FOLDER_RE);
379
- const slug = match ? match[1] : "entity";
380
- const entityDisplayName = slugToEntityName(slug);
381
239
  if (!parsed.entityStore) {
382
240
  parsed.entityStore = {
383
241
  _currId: 0,
@@ -389,12 +247,14 @@ export async function pushNewEntity(sceneId, sceneDir, engineId, folderName) {
389
247
  };
390
248
  }
391
249
  const store = parsed.entityStore;
250
+ const existingIds = [...Object.keys(manifest.entities).map(Number), store._currId];
251
+ const engineId = existingIds.length ? Math.max(...existingIds) + 1 : 1;
252
+ const idStr = String(engineId);
392
253
  store._currId = Math.max(store._currId, engineId);
393
- if (!store.rootEntities.includes(engineId)) {
254
+ if (!store.rootEntities.includes(engineId))
394
255
  store.rootEntities.push(engineId);
395
- }
396
256
  store.entities[idStr] = {
397
- name: entityDisplayName,
257
+ name: folderName,
398
258
  engineId,
399
259
  childrenIdsSet: [],
400
260
  childrenIds: [],
@@ -407,7 +267,6 @@ export async function pushNewEntity(sceneId, sceneDir, engineId, folderName) {
407
267
  threeId: 0,
408
268
  };
409
269
  manifest.entities[idStr] = folderName;
410
- await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
411
- const json = JSON.stringify(parsed);
412
- await uploadSceneJsonAndUpdate(sceneId, json);
270
+ await fs.writeFile(path.join(sceneDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
271
+ return JSON.stringify(parsed);
413
272
  }
@@ -0,0 +1,42 @@
1
+ export type EntityTransformInput = {
2
+ position?: unknown;
3
+ rotation?: unknown;
4
+ scale?: unknown;
5
+ };
6
+ export type NewEntityData = {
7
+ name: string;
8
+ script: string;
9
+ properties: Array<{
10
+ name: string;
11
+ type: string;
12
+ value: unknown;
13
+ }>;
14
+ transform: {
15
+ position: string;
16
+ rotation: string;
17
+ scale: string;
18
+ };
19
+ };
20
+ /**
21
+ * In-memory scene state for WebSocket mode. Holds a PhibelleScene and provides
22
+ * update methods that mirror the push logic without touching the network.
23
+ */
24
+ export declare class SceneState {
25
+ private scene;
26
+ constructor(initialJson: string);
27
+ getJson(): string;
28
+ /** Current max entity id used in entityStore (for computing next engineId). */
29
+ getCurrId(): number;
30
+ /** Replace in-memory scene with the given JSON (e.g. from FE over WebSocket). */
31
+ replaceScene(json: string): void;
32
+ updateSceneScript(script: string): void;
33
+ updateSceneProperties(properties: unknown[]): void;
34
+ updateEntityScript(entityId: string, script: string): void;
35
+ updateEntityProperties(entityId: string, properties: Array<{
36
+ name: string;
37
+ type: string;
38
+ value: unknown;
39
+ }>): void;
40
+ updateEntityTransforms(entityId: string, data: EntityTransformInput): void;
41
+ addEntity(engineId: number, data: NewEntityData): void;
42
+ }
@@ -0,0 +1,125 @@
1
+ const DEFAULT_POSITION = '{"x":0,"y":0,"z":0}';
2
+ const DEFAULT_ROTATION = '{"isEuler":true,"_x":0,"_y":0,"_z":0,"_order":"XYZ"}';
3
+ const DEFAULT_SCALE = '{"x":1,"y":1,"z":1}';
4
+ /**
5
+ * In-memory scene state for WebSocket mode. Holds a PhibelleScene and provides
6
+ * update methods that mirror the push logic without touching the network.
7
+ */
8
+ export class SceneState {
9
+ scene;
10
+ constructor(initialJson) {
11
+ try {
12
+ this.scene = JSON.parse(initialJson);
13
+ }
14
+ catch {
15
+ throw new Error("Invalid scene file: not valid JSON");
16
+ }
17
+ }
18
+ getJson() {
19
+ return JSON.stringify(this.scene);
20
+ }
21
+ /** Current max entity id used in entityStore (for computing next engineId). */
22
+ getCurrId() {
23
+ return this.scene.entityStore?._currId ?? 0;
24
+ }
25
+ /** Replace in-memory scene with the given JSON (e.g. from FE over WebSocket). */
26
+ replaceScene(json) {
27
+ try {
28
+ this.scene = JSON.parse(json);
29
+ }
30
+ catch {
31
+ throw new Error("Invalid scene file: not valid JSON");
32
+ }
33
+ }
34
+ updateSceneScript(script) {
35
+ if (!this.scene.sceneScriptStore) {
36
+ this.scene.sceneScriptStore = {
37
+ sceneScript: "",
38
+ sceneProperties: [],
39
+ lastModifiedSceneScript: 0,
40
+ };
41
+ }
42
+ this.scene.sceneScriptStore.sceneScript = script;
43
+ if (typeof this.scene.sceneScriptStore.lastModifiedSceneScript === "number") {
44
+ this.scene.sceneScriptStore.lastModifiedSceneScript = Date.now();
45
+ }
46
+ }
47
+ updateSceneProperties(properties) {
48
+ if (!this.scene.sceneScriptStore) {
49
+ this.scene.sceneScriptStore = {
50
+ sceneScript: "",
51
+ sceneProperties: [],
52
+ lastModifiedSceneScript: 0,
53
+ };
54
+ }
55
+ this.scene.sceneScriptStore.sceneProperties = properties;
56
+ }
57
+ updateEntityScript(entityId, script) {
58
+ const entities = this.scene.entityStore?.entities ?? {};
59
+ const entity = entities[entityId];
60
+ if (!entity) {
61
+ throw new Error(`Entity ${entityId} no longer in scene`);
62
+ }
63
+ entity.script = script;
64
+ entity.scriptVersion = (entity.scriptVersion ?? 0) + 1;
65
+ }
66
+ updateEntityProperties(entityId, properties) {
67
+ const entities = this.scene.entityStore?.entities ?? {};
68
+ const entity = entities[entityId];
69
+ if (!entity) {
70
+ throw new Error(`Entity ${entityId} no longer in scene`);
71
+ }
72
+ entity.properties = properties;
73
+ }
74
+ updateEntityTransforms(entityId, data) {
75
+ const entities = this.scene.entityStore?.entities ?? {};
76
+ const entity = entities[entityId];
77
+ if (!entity) {
78
+ throw new Error(`Entity ${entityId} no longer in scene`);
79
+ }
80
+ if (!entity.transform) {
81
+ entity.transform = {
82
+ position: DEFAULT_POSITION,
83
+ rotation: DEFAULT_ROTATION,
84
+ scale: DEFAULT_SCALE,
85
+ };
86
+ }
87
+ if (data.position !== undefined)
88
+ entity.transform.position = JSON.stringify(data.position);
89
+ if (data.rotation !== undefined)
90
+ entity.transform.rotation = JSON.stringify(data.rotation);
91
+ if (data.scale !== undefined)
92
+ entity.transform.scale = JSON.stringify(data.scale);
93
+ }
94
+ addEntity(engineId, data) {
95
+ if (!this.scene.entityStore) {
96
+ this.scene.entityStore = {
97
+ _currId: 0,
98
+ rootEntities: [],
99
+ entities: {},
100
+ rootRenderVersion: 0,
101
+ lastRenderedVersion: 0,
102
+ selectedEntityIds: [],
103
+ };
104
+ }
105
+ const store = this.scene.entityStore;
106
+ const idStr = String(engineId);
107
+ store._currId = Math.max(store._currId, engineId);
108
+ if (!store.rootEntities.includes(engineId)) {
109
+ store.rootEntities.push(engineId);
110
+ }
111
+ store.entities[idStr] = {
112
+ name: data.name,
113
+ engineId,
114
+ childrenIdsSet: [],
115
+ childrenIds: [],
116
+ isNodeOpen: false,
117
+ script: data.script,
118
+ scriptVersion: 0,
119
+ transform: data.transform,
120
+ renderVersion: 0,
121
+ properties: data.properties,
122
+ threeId: 0,
123
+ };
124
+ }
125
+ }