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.
- package/dist/commands/clone-scene.d.ts +1 -5
- package/dist/commands/clone-scene.js +124 -31
- package/dist/commands/init-scene.d.ts +1 -0
- package/dist/commands/init-scene.js +16 -0
- package/dist/commands/watch-scene.d.ts +2 -1
- package/dist/commands/watch-scene.js +116 -83
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/index.js +14 -18
- package/dist/lib/api/index.d.ts +1 -2
- package/dist/lib/api/index.js +1 -2
- package/dist/lib/api/public-env.d.ts +0 -1
- package/dist/lib/api/public-env.js +0 -1
- package/dist/lib/scene/file-system.d.ts +2 -1
- package/dist/lib/scene/file-system.js +5 -4
- package/dist/lib/scene/first-run-setup.d.ts +5 -1
- package/dist/lib/scene/first-run-setup.js +10 -4
- package/dist/lib/scene/index.d.ts +1 -2
- package/dist/lib/scene/index.js +1 -2
- package/dist/lib/scene/scene-packaging.d.ts +9 -15
- package/dist/lib/scene/scene-packaging.js +124 -265
- package/dist/lib/scene/scene-state.d.ts +42 -0
- package/dist/lib/scene/scene-state.js +125 -0
- package/dist/lib/scene/script-watcher.d.ts +1 -1
- package/dist/lib/scene/script-watcher.js +13 -94
- package/dist/lib/ws/index.d.ts +1 -0
- package/dist/lib/ws/index.js +1 -0
- package/dist/lib/ws/ws-server.d.ts +15 -0
- package/dist/lib/ws/ws-server.js +56 -0
- package/dist/scene-setup/agents-md.d.ts +1 -1
- package/dist/scene-setup/agents-md.js +35 -21
- package/dist/scene-setup/npm-package.d.ts +1 -1
- package/dist/scene-setup/npm-package.js +2 -2
- package/dist/scene-setup/setup.d.ts +1 -1
- package/dist/scene-setup/setup.js +6 -6
- 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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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 (
|
|
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()
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
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 ?? {
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
169
|
-
const parsed = parseSceneJson(content);
|
|
129
|
+
const parsed = parseSceneJson(sceneJson);
|
|
170
130
|
const appDir = getSceneAppDir(sceneDir);
|
|
171
|
-
const filePath = path.join(appDir,
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
137
|
+
parsed.sceneScriptStore.lastModifiedSceneScript = Date.now();
|
|
138
|
+
return JSON.stringify(parsed);
|
|
187
139
|
}
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
367
|
-
const
|
|
368
|
-
if (
|
|
369
|
-
transform.position = JSON.stringify(
|
|
370
|
-
if (
|
|
371
|
-
transform.rotation = JSON.stringify(
|
|
372
|
-
if (
|
|
373
|
-
transform.scale = JSON.stringify(
|
|
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
|
-
|
|
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:
|
|
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(
|
|
411
|
-
|
|
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
|
+
}
|