phibelle-kit 1.0.9 → 1.0.12
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/README.md +1 -1
- package/dist/commands/watch-scene.js +2 -2
- package/dist/lib/file-system.d.ts +2 -0
- package/dist/lib/file-system.js +4 -0
- package/dist/lib/first-run-setup.d.ts +1 -1
- package/dist/lib/first-run-setup.js +1 -1
- package/dist/lib/scene-packaging.d.ts +10 -3
- package/dist/lib/scene-packaging.js +47 -14
- package/dist/lib/script-watcher.d.ts +2 -2
- package/dist/lib/script-watcher.js +6 -4
- package/dist/package/agents-md.d.ts +1 -1
- package/dist/package/agents-md.js +12 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,5 +23,5 @@ npx phibelle-kit watch <sceneId>
|
|
|
23
23
|
|
|
24
24
|
1. `cd <scene-folder>` to enter the scene directory.
|
|
25
25
|
2. `npm install` to install dependencies (for types and intellisense).
|
|
26
|
-
3. `npm run watch` in the scene folder to push edits, or run `npx phibelle-kit watch <sceneId>` from the
|
|
26
|
+
3. `npm run watch` in the scene folder to push edits, or run `npx phibelle-kit watch <sceneId>` from the project root.
|
|
27
27
|
|
|
@@ -13,7 +13,7 @@ import { BASE_URL } from "../lib/public-env.js";
|
|
|
13
13
|
function createWatchCallbacks() {
|
|
14
14
|
return {
|
|
15
15
|
onPushed: (file) => console.log(chalk.green(" ✓ Pushed " + file + " to Convex")),
|
|
16
|
-
onNewEntity: (engineId) => console.log(chalk.green(" ✓ Created new entity " + engineId + " (
|
|
16
|
+
onNewEntity: (engineId, fileName) => console.log(chalk.green(" ✓ Created new entity " + engineId + (fileName ? " (" + fileName + ")" : ""))),
|
|
17
17
|
onError: (file, err) => console.log(chalk.red(" ✖ Failed to push " + file + ": " + err.message)),
|
|
18
18
|
};
|
|
19
19
|
}
|
|
@@ -71,7 +71,7 @@ export async function watchSceneCommand(sceneId) {
|
|
|
71
71
|
scriptWatcher = null;
|
|
72
72
|
try {
|
|
73
73
|
await unpackageScene(scene._id, sceneDir);
|
|
74
|
-
console.log(chalk.green(" ✓ Re-unpackaged scene scripts (scene.tsx, entity
|
|
74
|
+
console.log(chalk.green(" ✓ Re-unpackaged scene scripts (scene.tsx, <entity-name>-<id>.tsx, manifest.json)"));
|
|
75
75
|
scriptWatcher = startScriptWatcher(scene, sceneDir);
|
|
76
76
|
}
|
|
77
77
|
catch (e) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export declare const SCENE_APP_DIR = "app";
|
|
2
|
+
export declare function getSceneAppDir(sceneDir: string): string;
|
|
1
3
|
export declare function folderExists(folderPath: string): boolean;
|
|
2
4
|
export declare function createFolder(folderPath: string): void;
|
|
3
5
|
/** Scene name to folder name: lowercased, spaces to hyphens, safe for filesystem. */
|
package/dist/lib/file-system.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import * as fs from "fs";
|
|
3
|
+
export const SCENE_APP_DIR = "app";
|
|
4
|
+
export function getSceneAppDir(sceneDir) {
|
|
5
|
+
return path.join(sceneDir, SCENE_APP_DIR);
|
|
6
|
+
}
|
|
3
7
|
export function folderExists(folderPath) {
|
|
4
8
|
return fs.existsSync(folderPath);
|
|
5
9
|
}
|
|
@@ -2,7 +2,7 @@ import { type ScriptWatcher } from "./script-watcher.js";
|
|
|
2
2
|
import type { Scene } from "./types.js";
|
|
3
3
|
export type WatchCallbacks = {
|
|
4
4
|
onPushed: (file: string) => void;
|
|
5
|
-
onNewEntity: (engineId: number) => void;
|
|
5
|
+
onNewEntity: (engineId: number, fileName?: string) => void;
|
|
6
6
|
onError: (file: string, err: Error) => void;
|
|
7
7
|
};
|
|
8
8
|
/** Create scene dir, write scene.json, setup package.json/global.d.ts; log install hint if needed. */
|
|
@@ -23,7 +23,7 @@ export function setupSceneFiles(scene, sceneDir, ignoreHint = false) {
|
|
|
23
23
|
export async function unpackageWithLogging(sceneId, sceneDir, failHint) {
|
|
24
24
|
try {
|
|
25
25
|
await unpackageScene(sceneId, sceneDir);
|
|
26
|
-
console.log(chalk.green(" ✓ Unpacked scene scripts (scene.tsx, entity
|
|
26
|
+
console.log(chalk.green(" ✓ Unpacked scene scripts (scene.tsx, <entity-name>-<id>.tsx, manifest.json)"));
|
|
27
27
|
}
|
|
28
28
|
catch (e) {
|
|
29
29
|
console.log(chalk.yellow(" ⚠ Could not unpackage scene scripts: " + toErrorMessage(e)));
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Convert entity display name to slug: "Main Enemy" → "main-enemy". */
|
|
2
|
+
export declare function entityNameToSlug(name: string): string;
|
|
3
|
+
/** Convert slug to display name: "main-enemy" → "Main Enemy". */
|
|
4
|
+
export declare function slugToEntityName(slug: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Matches entity script filenames: <entity-name>-<engineId>.tsx (e.g. main-enemy-1.tsx).
|
|
7
|
+
* Group 1 = slug, group 2 = engine id.
|
|
8
|
+
*/
|
|
2
9
|
export declare const ENTITY_FILE_RE: RegExp;
|
|
3
10
|
export declare function unpackageScene(sceneId: string, sceneDir: string): Promise<void>;
|
|
4
11
|
export declare function pushScriptChange(sceneId: string, sceneDir: string, changedFile: string): Promise<void>;
|
|
5
|
-
/** Add a new entity to the scene when user creates entity
|
|
6
|
-
export declare function pushNewEntity(sceneId: string, sceneDir: string, engineId: number): Promise<void>;
|
|
12
|
+
/** Add a new entity to the scene when user creates <entity-name>-<engineId>.tsx locally. */
|
|
13
|
+
export declare function pushNewEntity(sceneId: string, sceneDir: string, engineId: number, fileName: string): Promise<void>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "fs/promises";
|
|
2
2
|
import * as path from "path";
|
|
3
|
+
import { getSceneAppDir } from "./file-system.js";
|
|
3
4
|
import { queryOnce, mutateOnce } from "./convex-client.js";
|
|
4
5
|
import { api } from "../convex/_generated/api.js";
|
|
5
6
|
import { getSessionId } from "./conf.js";
|
|
@@ -22,8 +23,28 @@ function parseSceneJson(content) {
|
|
|
22
23
|
throw new Error("Invalid scene file: not valid JSON");
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
|
-
/**
|
|
26
|
-
export
|
|
26
|
+
/** Convert entity display name to slug: "Main Enemy" → "main-enemy". */
|
|
27
|
+
export function entityNameToSlug(name) {
|
|
28
|
+
const slug = name
|
|
29
|
+
.toLowerCase()
|
|
30
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
31
|
+
.replace(/^-|-$/g, "");
|
|
32
|
+
return slug || "entity";
|
|
33
|
+
}
|
|
34
|
+
/** Convert slug to display name: "main-enemy" → "Main Enemy". */
|
|
35
|
+
export function slugToEntityName(slug) {
|
|
36
|
+
if (!slug || slug === "entity")
|
|
37
|
+
return "Entity";
|
|
38
|
+
return slug
|
|
39
|
+
.split("-")
|
|
40
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
41
|
+
.join(" ");
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Matches entity script filenames: <entity-name>-<engineId>.tsx (e.g. main-enemy-1.tsx).
|
|
45
|
+
* Group 1 = slug, group 2 = engine id.
|
|
46
|
+
*/
|
|
47
|
+
export const ENTITY_FILE_RE = /^([a-z0-9]+(?:-[a-z0-9]+)*)-(\d+)\.tsx$/;
|
|
27
48
|
async function uploadSceneJsonAndUpdate(sceneId, json) {
|
|
28
49
|
const uploadUrl = await mutateOnce(api.storage.generateUploadUrl, {});
|
|
29
50
|
if (typeof uploadUrl !== "string") {
|
|
@@ -53,13 +74,14 @@ async function uploadSceneJsonAndUpdate(sceneId, json) {
|
|
|
53
74
|
export async function unpackageScene(sceneId, sceneDir) {
|
|
54
75
|
const content = await getSceneFileContent(sceneId);
|
|
55
76
|
const parsed = parseSceneJson(content);
|
|
56
|
-
|
|
77
|
+
const appDir = getSceneAppDir(sceneDir);
|
|
78
|
+
await fs.mkdir(appDir, { recursive: true });
|
|
57
79
|
// Remove existing entity files so entities deleted in the editor are removed from disk
|
|
58
80
|
try {
|
|
59
|
-
const entries = await fs.readdir(
|
|
81
|
+
const entries = await fs.readdir(appDir, { withFileTypes: true });
|
|
60
82
|
for (const ent of entries) {
|
|
61
83
|
if (ent.isFile() && ENTITY_FILE_RE.test(ent.name)) {
|
|
62
|
-
await fs.unlink(path.join(
|
|
84
|
+
await fs.unlink(path.join(appDir, ent.name));
|
|
63
85
|
}
|
|
64
86
|
}
|
|
65
87
|
}
|
|
@@ -71,16 +93,23 @@ export async function unpackageScene(sceneId, sceneDir) {
|
|
|
71
93
|
sceneScript: "scene.tsx",
|
|
72
94
|
entities: {},
|
|
73
95
|
};
|
|
74
|
-
await fs.writeFile(path.join(
|
|
96
|
+
await fs.writeFile(path.join(appDir, "scene.tsx"), parsed.sceneScriptStore?.sceneScript ?? "", "utf8");
|
|
75
97
|
for (const idStr of Object.keys(entities)) {
|
|
76
98
|
const entity = entities[idStr];
|
|
77
99
|
if (!entity || typeof entity.script !== "string")
|
|
78
100
|
continue;
|
|
79
101
|
const engineId = String(entity.engineId ?? idStr);
|
|
80
|
-
|
|
102
|
+
let slug = entityNameToSlug(entity.name ?? "Entity");
|
|
103
|
+
const redundantPrefix = `entity-${engineId}`;
|
|
104
|
+
if (slug === redundantPrefix)
|
|
105
|
+
slug = "entity";
|
|
106
|
+
else if (slug.startsWith(redundantPrefix + "-"))
|
|
107
|
+
slug = slug.slice((redundantPrefix + "-").length);
|
|
108
|
+
const fileName = `${slug}-${engineId}.tsx`;
|
|
81
109
|
manifest.entities[engineId] = fileName;
|
|
82
|
-
await fs.writeFile(path.join(
|
|
110
|
+
await fs.writeFile(path.join(appDir, fileName), entity.script, "utf8");
|
|
83
111
|
}
|
|
112
|
+
await fs.mkdir(sceneDir, { recursive: true });
|
|
84
113
|
await fs.writeFile(path.join(sceneDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
85
114
|
}
|
|
86
115
|
export async function pushScriptChange(sceneId, sceneDir, changedFile) {
|
|
@@ -95,7 +124,8 @@ export async function pushScriptChange(sceneId, sceneDir, changedFile) {
|
|
|
95
124
|
const manifest = JSON.parse(manifestRaw);
|
|
96
125
|
const content = await getSceneFileContent(sceneId);
|
|
97
126
|
const parsed = parseSceneJson(content);
|
|
98
|
-
const
|
|
127
|
+
const appDir = getSceneAppDir(sceneDir);
|
|
128
|
+
const filePath = path.join(appDir, changedFile);
|
|
99
129
|
let fileContent;
|
|
100
130
|
try {
|
|
101
131
|
fileContent = await fs.readFile(filePath, "utf8");
|
|
@@ -131,8 +161,8 @@ export async function pushScriptChange(sceneId, sceneDir, changedFile) {
|
|
|
131
161
|
const DEFAULT_POSITION = '{"x":0,"y":0,"z":0}';
|
|
132
162
|
const DEFAULT_ROTATION = '{"isEuler":true,"_x":0,"_y":0,"_z":0,"_order":"XYZ"}';
|
|
133
163
|
const DEFAULT_SCALE = '{"x":1,"y":1,"z":1}';
|
|
134
|
-
/** Add a new entity to the scene when user creates entity
|
|
135
|
-
export async function pushNewEntity(sceneId, sceneDir, engineId) {
|
|
164
|
+
/** Add a new entity to the scene when user creates <entity-name>-<engineId>.tsx locally. */
|
|
165
|
+
export async function pushNewEntity(sceneId, sceneDir, engineId, fileName) {
|
|
136
166
|
const manifestPath = path.join(sceneDir, "manifest.json");
|
|
137
167
|
let manifestRaw;
|
|
138
168
|
try {
|
|
@@ -148,8 +178,8 @@ export async function pushNewEntity(sceneId, sceneDir, engineId) {
|
|
|
148
178
|
}
|
|
149
179
|
const content = await getSceneFileContent(sceneId);
|
|
150
180
|
const parsed = parseSceneJson(content);
|
|
151
|
-
const
|
|
152
|
-
const filePath = path.join(
|
|
181
|
+
const appDir = getSceneAppDir(sceneDir);
|
|
182
|
+
const filePath = path.join(appDir, fileName);
|
|
153
183
|
let script;
|
|
154
184
|
try {
|
|
155
185
|
script = await fs.readFile(filePath, "utf8");
|
|
@@ -157,6 +187,9 @@ export async function pushNewEntity(sceneId, sceneDir, engineId) {
|
|
|
157
187
|
catch {
|
|
158
188
|
throw new Error(`Could not read file: ${fileName}`);
|
|
159
189
|
}
|
|
190
|
+
const match = fileName.match(ENTITY_FILE_RE);
|
|
191
|
+
const slug = match ? match[1] : "entity";
|
|
192
|
+
const entityDisplayName = slugToEntityName(slug);
|
|
160
193
|
if (!parsed.entityStore) {
|
|
161
194
|
parsed.entityStore = {
|
|
162
195
|
_currId: 0,
|
|
@@ -173,7 +206,7 @@ export async function pushNewEntity(sceneId, sceneDir, engineId) {
|
|
|
173
206
|
store.rootEntities.push(engineId);
|
|
174
207
|
}
|
|
175
208
|
store.entities[idStr] = {
|
|
176
|
-
name:
|
|
209
|
+
name: entityDisplayName,
|
|
177
210
|
engineId,
|
|
178
211
|
childrenIdsSet: [],
|
|
179
212
|
childrenIds: [],
|
|
@@ -3,7 +3,7 @@ export type ScriptWatcher = {
|
|
|
3
3
|
};
|
|
4
4
|
export type WatchCallbacks = {
|
|
5
5
|
onPushed: (file: string) => void;
|
|
6
|
-
onNewEntity: (engineId: number) => void;
|
|
6
|
+
onNewEntity: (engineId: number, fileName?: string) => void;
|
|
7
7
|
onError: (file: string, err: Error) => void;
|
|
8
8
|
};
|
|
9
|
-
export declare function createScriptWatcher(sceneId: string, sceneDir: string, onPushed: (file: string) => void, onNewEntity: (engineId: number) => void, onError: (file: string, err: Error) => void): ScriptWatcher;
|
|
9
|
+
export declare function createScriptWatcher(sceneId: string, sceneDir: string, onPushed: (file: string) => void, onNewEntity: (engineId: number, fileName?: string) => void, onError: (file: string, err: Error) => void): ScriptWatcher;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import * as path from "path";
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import chokidar from "chokidar";
|
|
4
|
+
import { getSceneAppDir } from "./file-system.js";
|
|
4
5
|
import { pushScriptChange, pushNewEntity, ENTITY_FILE_RE } from "./scene-packaging.js";
|
|
5
6
|
const DEBOUNCE_MS = 600;
|
|
6
7
|
export function createScriptWatcher(sceneId, sceneDir, onPushed, onNewEntity, onError) {
|
|
7
8
|
const manifestPath = path.join(sceneDir, "manifest.json");
|
|
9
|
+
const appDir = getSceneAppDir(sceneDir);
|
|
8
10
|
function readManifest() {
|
|
9
11
|
try {
|
|
10
12
|
const raw = fs.readFileSync(manifestPath, "utf8");
|
|
@@ -42,12 +44,12 @@ export function createScriptWatcher(sceneId, sceneDir, onPushed, onNewEntity, on
|
|
|
42
44
|
}
|
|
43
45
|
const entityMatch = basename.match(ENTITY_FILE_RE);
|
|
44
46
|
if (entityMatch) {
|
|
45
|
-
const engineId = parseInt(entityMatch[
|
|
47
|
+
const engineId = parseInt(entityMatch[2], 10);
|
|
46
48
|
const isNew = !manifest.entities[String(engineId)];
|
|
47
49
|
if (isNew) {
|
|
48
50
|
try {
|
|
49
|
-
await pushNewEntity(sceneId, sceneDir, engineId);
|
|
50
|
-
onNewEntity(engineId);
|
|
51
|
+
await pushNewEntity(sceneId, sceneDir, engineId, basename);
|
|
52
|
+
onNewEntity(engineId, basename);
|
|
51
53
|
}
|
|
52
54
|
catch (err) {
|
|
53
55
|
onError(basename, err instanceof Error ? err : new Error(String(err)));
|
|
@@ -65,7 +67,7 @@ export function createScriptWatcher(sceneId, sceneDir, onPushed, onNewEntity, on
|
|
|
65
67
|
}
|
|
66
68
|
}, DEBOUNCE_MS);
|
|
67
69
|
}
|
|
68
|
-
const watcher = chokidar.watch(
|
|
70
|
+
const watcher = chokidar.watch(appDir, { ignoreInitial: true });
|
|
69
71
|
watcher.on("change", (filePath) => {
|
|
70
72
|
const basename = path.basename(filePath);
|
|
71
73
|
if (basename === "manifest.json")
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* AGENTS.md content written into the scene directory when cloning.
|
|
3
3
|
* Explains how the Phibelle engine works for AI agents and developers.
|
|
4
4
|
*/
|
|
5
|
-
export declare const AGENTS_MD = "# Phibelle Engine \u2014 How It Works\n\nThis document describes how the Phibelle 3D engine runs your scene and entity scripts. Use it when editing `scene.tsx` and
|
|
5
|
+
export declare const AGENTS_MD = "# Phibelle Engine \u2014 How It Works\n\nThis document describes how the Phibelle 3D engine runs your scene and entity scripts. Use it when editing `scene.tsx` and `<entity-name>-<id>.tsx` files or when an AI agent needs to understand the runtime.\n\n## Overview\n\n- **Phibelle** is a React Three Fiber based 3D engine used in the [Phibelle Studio](https://phibelle.studio) editor.\n- A **scene** has one **scene script** (`scene.tsx`) and zero or more **entity scripts** (`<entity-name>-<engineId>.tsx`, e.g. `main-enemy-1.tsx`). Entity names are lowercased with hyphens (e.g. \"Main Enemy\" \u2192 `main-enemy-1.tsx`).\n- You can **create new entities** by adding new entity script files (`<entity-name>-<engineId>.tsx`); when the watcher is running, they are synced and the new entities appear in the scene.\n- The engine **concatenates** these scripts and runs them in a single live-editing environment with pre-injected globals (no `import`/`export`).\n\n## Runtime Model\n\n1. **Scene script** must define a component named `SceneRender` that receives `sceneProperties` and `children` and renders an `R3F.Canvas` (or equivalent). It **must** render `{children}` inside the canvas; that is where entity trees are rendered.\n2. **Entity scripts** each define a component named `Render<engineId>` (e.g. `Render1`, `Render42`). The engine builds a registry mapping `engineId` \u2192 render component and mounts entities in a tree (root entities first, then children).\n3. **Live code** is produced by: (1) scene script, (2) all entity scripts, (3) a small bootstrap that wraps the scene in an error boundary, passes `entityRenderRegistry` into `PhibelleRoot`, and calls `render(<PhibelleScene />)`. Your scripts run inside that bootstrap with no direct imports.\n\n## What You Can Use in Scripts (Namespaces)\n\nScripts run with these globals; **do not** use `import` or `export`.\n\n| Namespace | Purpose |\n|-----------|---------|\n| `React` | Hooks and utilities: `React.useState`, `React.useEffect`, `React.useRef`, `React.memo`, etc. |\n| `THREE` | Three.js: `THREE.Vector3`, `THREE.Euler`, `THREE.Mesh`, `THREE.Color`, etc. |\n| `R3F` | React Three Fiber: `R3F.Canvas`, `R3F.useThree`, `R3F.useFrame`, `R3F.extend` |\n| `DREI` | @react-three/drei: `DREI.Environment`, `DREI.OrbitControls`, `DREI.Text`, `DREI.Html`, `DREI.Float`, `DREI.MeshTransmissionMaterial`, etc. |\n| `UIKIT` | @react-three/uikit for 3D UI |\n| `RAPIER` | @react-three/rapier for physics: `RAPIER.Physics`, `RAPIER.RigidBody`, `RAPIER.Collider`, etc. |\n| `PHI` | Engine-specific: `PHI.useEngineMode`, `PHI.usePlayModeFrame`, `PHI.useGamepad`, `PHI.useEntityThreeObject`, `PHI.phibelleResetSelectedEntityIds`, `PHI.globalStore` |\n\n## Scene Script (`scene.tsx`)\n\n- **Component name**: Must be `SceneRender`.\n- **Props**: `sceneProperties: PHI.Property[]`, `children: React.ReactNode`.\n- **Responsibility**: Create the single `R3F.Canvas`, set up lights/environment, and render `{children}`. Attach `onPointerMissed={PHI.phibelleResetSelectedEntityIds}` on the Canvas to clear selection when clicking empty space.\n\nExample:\n\n```tsx\nconst SceneRender = ({\n sceneProperties,\n children,\n}: {\n sceneProperties: PHI.Property[];\n children: React.ReactNode;\n}) => (\n <R3F.Canvas\n shadows\n frameloop=\"always\"\n gl={{ preserveDrawingBuffer: true }}\n onPointerMissed={PHI.phibelleResetSelectedEntityIds}\n >\n <DREI.Environment preset=\"city\" />\n {children}\n </R3F.Canvas>\n);\n```\n\n## Entity Scripts (`<entity-name>-<engineId>.tsx`)\n\n- **File name**: `<entity-name>-<engineId>.tsx` where entity name is lowercased with hyphens (e.g. `main-enemy-1.tsx` for an entity named \"Main Enemy\" with engineId 1).\n- **Component name**: Must be `Render<engineId>` (e.g. `Render1` for `main-enemy-1.tsx`).\n- **Props**: `entityData: PHI.EntityData`.\n- **entityData** contains: `name`, `engineId`, `threeId`, `parentId`, `childrenIds`, `transform`, `properties`.\n- **Properties**: Use `entityData.properties` **by index** (order is fixed in the editor), e.g. `const [colorProp, speedProp] = entityData.properties;`.\n- **Edit vs Play**: Use `PHI.useEngineMode()` \u2192 `{ editMode, playMode }`. For per-frame game logic use **`PHI.usePlayModeFrame(callback)`** only (not `R3F.useFrame`), so logic does not run in edit mode. Show helpers/gizmos only when `editMode === true`.\n\nExample:\n\n```tsx\nconst Render1 = ({ entityData }: { entityData: PHI.EntityData }) => {\n const [colorProp] = entityData.properties;\n const { editMode, playMode } = PHI.useEngineMode();\n\n PHI.usePlayModeFrame((state, delta) => {\n // Game logic here; runs only in play mode\n });\n\n return (\n <group>\n <mesh>\n <boxGeometry args={[1, 1, 1]} />\n <meshStandardMaterial color={colorProp.value} />\n </mesh>\n {editMode && <DREI.Helper type={THREE.BoxHelper} args={[\"yellow\"]} />}\n </group>\n );\n};\n```\n\n## Creating New Entities\n\nYou can add new entities to the scene by creating new entity script files. Add a file named `<entity-name>-<engineId>.tsx` (e.g. `player-2.tsx`, `main-enemy-3.tsx`) with a component `Render<engineId>` (e.g. `Render2`, `Render3`). The entity name in the filename is lowercased with hyphens (e.g. \"Main Enemy\" \u2192 `main-enemy`). When **watch** is running (`npm run watch` or `npx phibelle-kit watch <sceneId>`), the watcher detects the new file and creates the corresponding entity in the Phibelle Studio scene. Use a unique `engineId` that does not already exist in `manifest.json`.\n\n## File Layout After Clone\n\n- `scene.json`: Full scene state (from the app); used by the CLI, not by the runtime.\n- `manifest.json`: Maps engine IDs to filenames for the watch/sync process.\n- `package.json`: Scripts: `watch` (sync edits to the app), and dependencies for types/IntelliSense.\n- `global.d.ts`: Declares global `PHI`, `THREE`, `R3F`, `DREI`, `UIKIT`, `RAPIER` for type checking.\n- `app/scene.tsx`: Scene script (single `SceneRender`).\n- `app/<entity-name>-<engineId>.tsx`: One file per entity (e.g. `main-enemy-1.tsx`); each exports a `Render<engineId>` component by convention.\n\n## Sync Flow (phibelle-kit)\n\n- **clone**: Fetches scene from the app, creates the folder, writes `scene.json`, `package.json`, `global.d.ts`, `AGENTS.md`, and unpacks `scene.tsx`, `<entity-name>-<id>.tsx`, `manifest.json`.\n- **watch**: Watches the scene directory; when you change a script file, it pushes that file\u2019s content back to the scene in the app (and can create new entities when you add `<entity-name>-<id>.tsx`).\n\nEdits you make in `scene.tsx` or `<entity-name>-<id>.tsx` are synced to the Phibelle Studio scene when the watcher is running (`npm run watch` or `npx phibelle-kit watch <sceneId>`).\n";
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export const AGENTS_MD = `# Phibelle Engine — How It Works
|
|
6
6
|
|
|
7
|
-
This document describes how the Phibelle 3D engine runs your scene and entity scripts. Use it when editing \`scene.tsx\` and
|
|
7
|
+
This document describes how the Phibelle 3D engine runs your scene and entity scripts. Use it when editing \`scene.tsx\` and \`<entity-name>-<id>.tsx\` files or when an AI agent needs to understand the runtime.
|
|
8
8
|
|
|
9
9
|
## Overview
|
|
10
10
|
|
|
11
11
|
- **Phibelle** is a React Three Fiber based 3D engine used in the [Phibelle Studio](https://phibelle.studio) editor.
|
|
12
|
-
- A **scene** has one **scene script** (\`scene.tsx\`) and zero or more **entity scripts** (
|
|
13
|
-
- You can **create new entities** by adding new entity script files (
|
|
12
|
+
- A **scene** has one **scene script** (\`scene.tsx\`) and zero or more **entity scripts** (\`<entity-name>-<engineId>.tsx\`, e.g. \`main-enemy-1.tsx\`). Entity names are lowercased with hyphens (e.g. "Main Enemy" → \`main-enemy-1.tsx\`).
|
|
13
|
+
- You can **create new entities** by adding new entity script files (\`<entity-name>-<engineId>.tsx\`); when the watcher is running, they are synced and the new entities appear in the scene.
|
|
14
14
|
- The engine **concatenates** these scripts and runs them in a single live-editing environment with pre-injected globals (no \`import\`/\`export\`).
|
|
15
15
|
|
|
16
16
|
## Runtime Model
|
|
@@ -61,9 +61,10 @@ const SceneRender = ({
|
|
|
61
61
|
);
|
|
62
62
|
\`\`\`
|
|
63
63
|
|
|
64
|
-
## Entity Scripts (
|
|
64
|
+
## Entity Scripts (\`<entity-name>-<engineId>.tsx\`)
|
|
65
65
|
|
|
66
|
-
- **
|
|
66
|
+
- **File name**: \`<entity-name>-<engineId>.tsx\` where entity name is lowercased with hyphens (e.g. \`main-enemy-1.tsx\` for an entity named "Main Enemy" with engineId 1).
|
|
67
|
+
- **Component name**: Must be \`Render<engineId>\` (e.g. \`Render1\` for \`main-enemy-1.tsx\`).
|
|
67
68
|
- **Props**: \`entityData: PHI.EntityData\`.
|
|
68
69
|
- **entityData** contains: \`name\`, \`engineId\`, \`threeId\`, \`parentId\`, \`childrenIds\`, \`transform\`, \`properties\`.
|
|
69
70
|
- **Properties**: Use \`entityData.properties\` **by index** (order is fixed in the editor), e.g. \`const [colorProp, speedProp] = entityData.properties;\`.
|
|
@@ -94,21 +95,21 @@ const Render1 = ({ entityData }: { entityData: PHI.EntityData }) => {
|
|
|
94
95
|
|
|
95
96
|
## Creating New Entities
|
|
96
97
|
|
|
97
|
-
You can add new entities to the scene by creating new entity script files. Add a file named
|
|
98
|
+
You can add new entities to the scene by creating new entity script files. Add a file named \`<entity-name>-<engineId>.tsx\` (e.g. \`player-2.tsx\`, \`main-enemy-3.tsx\`) with a component \`Render<engineId>\` (e.g. \`Render2\`, \`Render3\`). The entity name in the filename is lowercased with hyphens (e.g. "Main Enemy" → \`main-enemy\`). When **watch** is running (\`npm run watch\` or \`npx phibelle-kit watch <sceneId>\`), the watcher detects the new file and creates the corresponding entity in the Phibelle Studio scene. Use a unique \`engineId\` that does not already exist in \`manifest.json\`.
|
|
98
99
|
|
|
99
100
|
## File Layout After Clone
|
|
100
101
|
|
|
101
102
|
- \`scene.json\`: Full scene state (from the app); used by the CLI, not by the runtime.
|
|
102
|
-
- \`scene.tsx\`: Scene script (single \`SceneRender\`).
|
|
103
|
-
- \`entity-<engineId>.tsx\`: One file per entity; each exports a \`Render<engineId>\` component by convention.
|
|
104
103
|
- \`manifest.json\`: Maps engine IDs to filenames for the watch/sync process.
|
|
105
104
|
- \`package.json\`: Scripts: \`watch\` (sync edits to the app), and dependencies for types/IntelliSense.
|
|
106
105
|
- \`global.d.ts\`: Declares global \`PHI\`, \`THREE\`, \`R3F\`, \`DREI\`, \`UIKIT\`, \`RAPIER\` for type checking.
|
|
106
|
+
- \`app/scene.tsx\`: Scene script (single \`SceneRender\`).
|
|
107
|
+
- \`app/<entity-name>-<engineId>.tsx\`: One file per entity (e.g. \`main-enemy-1.tsx\`); each exports a \`Render<engineId>\` component by convention.
|
|
107
108
|
|
|
108
109
|
## Sync Flow (phibelle-kit)
|
|
109
110
|
|
|
110
|
-
- **clone**: Fetches scene from the app, creates the folder, writes \`scene.json\`, \`package.json\`, \`global.d.ts\`, \`AGENTS.md\`, and unpacks \`scene.tsx\`,
|
|
111
|
-
- **watch**: Watches the scene directory; when you change a script file, it pushes that file’s content back to the scene in the app (and can create new entities when you add
|
|
111
|
+
- **clone**: Fetches scene from the app, creates the folder, writes \`scene.json\`, \`package.json\`, \`global.d.ts\`, \`AGENTS.md\`, and unpacks \`scene.tsx\`, \`<entity-name>-<id>.tsx\`, \`manifest.json\`.
|
|
112
|
+
- **watch**: Watches the scene directory; when you change a script file, it pushes that file’s content back to the scene in the app (and can create new entities when you add \`<entity-name>-<id>.tsx\`).
|
|
112
113
|
|
|
113
|
-
Edits you make in \`scene.tsx\` or
|
|
114
|
+
Edits you make in \`scene.tsx\` or \`<entity-name>-<id>.tsx\` are synced to the Phibelle Studio scene when the watcher is running (\`npm run watch\` or \`npx phibelle-kit watch <sceneId>\`).
|
|
114
115
|
`;
|