phibelle-kit 1.0.8 → 1.0.11
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
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
|
|
|
@@ -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
|
}
|
|
@@ -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";
|
|
@@ -53,13 +54,14 @@ async function uploadSceneJsonAndUpdate(sceneId, json) {
|
|
|
53
54
|
export async function unpackageScene(sceneId, sceneDir) {
|
|
54
55
|
const content = await getSceneFileContent(sceneId);
|
|
55
56
|
const parsed = parseSceneJson(content);
|
|
56
|
-
|
|
57
|
+
const appDir = getSceneAppDir(sceneDir);
|
|
58
|
+
await fs.mkdir(appDir, { recursive: true });
|
|
57
59
|
// Remove existing entity files so entities deleted in the editor are removed from disk
|
|
58
60
|
try {
|
|
59
|
-
const entries = await fs.readdir(
|
|
61
|
+
const entries = await fs.readdir(appDir, { withFileTypes: true });
|
|
60
62
|
for (const ent of entries) {
|
|
61
63
|
if (ent.isFile() && ENTITY_FILE_RE.test(ent.name)) {
|
|
62
|
-
await fs.unlink(path.join(
|
|
64
|
+
await fs.unlink(path.join(appDir, ent.name));
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
67
|
}
|
|
@@ -71,7 +73,7 @@ export async function unpackageScene(sceneId, sceneDir) {
|
|
|
71
73
|
sceneScript: "scene.tsx",
|
|
72
74
|
entities: {},
|
|
73
75
|
};
|
|
74
|
-
await fs.writeFile(path.join(
|
|
76
|
+
await fs.writeFile(path.join(appDir, "scene.tsx"), parsed.sceneScriptStore?.sceneScript ?? "", "utf8");
|
|
75
77
|
for (const idStr of Object.keys(entities)) {
|
|
76
78
|
const entity = entities[idStr];
|
|
77
79
|
if (!entity || typeof entity.script !== "string")
|
|
@@ -79,8 +81,9 @@ export async function unpackageScene(sceneId, sceneDir) {
|
|
|
79
81
|
const engineId = String(entity.engineId ?? idStr);
|
|
80
82
|
const fileName = `entity-${engineId}.tsx`;
|
|
81
83
|
manifest.entities[engineId] = fileName;
|
|
82
|
-
await fs.writeFile(path.join(
|
|
84
|
+
await fs.writeFile(path.join(appDir, fileName), entity.script, "utf8");
|
|
83
85
|
}
|
|
86
|
+
await fs.mkdir(sceneDir, { recursive: true });
|
|
84
87
|
await fs.writeFile(path.join(sceneDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
85
88
|
}
|
|
86
89
|
export async function pushScriptChange(sceneId, sceneDir, changedFile) {
|
|
@@ -95,7 +98,8 @@ export async function pushScriptChange(sceneId, sceneDir, changedFile) {
|
|
|
95
98
|
const manifest = JSON.parse(manifestRaw);
|
|
96
99
|
const content = await getSceneFileContent(sceneId);
|
|
97
100
|
const parsed = parseSceneJson(content);
|
|
98
|
-
const
|
|
101
|
+
const appDir = getSceneAppDir(sceneDir);
|
|
102
|
+
const filePath = path.join(appDir, changedFile);
|
|
99
103
|
let fileContent;
|
|
100
104
|
try {
|
|
101
105
|
fileContent = await fs.readFile(filePath, "utf8");
|
|
@@ -149,7 +153,8 @@ export async function pushNewEntity(sceneId, sceneDir, engineId) {
|
|
|
149
153
|
const content = await getSceneFileContent(sceneId);
|
|
150
154
|
const parsed = parseSceneJson(content);
|
|
151
155
|
const fileName = `entity-${engineId}.tsx`;
|
|
152
|
-
const
|
|
156
|
+
const appDir = getSceneAppDir(sceneDir);
|
|
157
|
+
const filePath = path.join(appDir, fileName);
|
|
153
158
|
let script;
|
|
154
159
|
try {
|
|
155
160
|
script = await fs.readFile(filePath, "utf8");
|
|
@@ -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");
|
|
@@ -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 `entity-*.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-<engineId>.tsx`).\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-<engineId>.tsx`)\n\n- **Component name**: Must be `Render<engineId>` (e.g. `Render1` for `entity-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##
|
|
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-*.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-<engineId>.tsx`).\n- You can **create new entities** by adding new entity script files (`entity-<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-<engineId>.tsx`)\n\n- **Component name**: Must be `Render<engineId>` (e.g. `Render1` for `entity-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-<engineId>.tsx` (e.g. `entity-2.tsx`, `entity-3.tsx`) with a component `Render<engineId>` (e.g. `Render2`, `Render3`). 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-<engineId>.tsx`: One file per entity; 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-*.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-<id>.tsx`).\n\nEdits you make in `scene.tsx` or `entity-*.tsx` are synced to the Phibelle Studio scene when the watcher is running (`npm run watch` or `npx phibelle-kit watch <sceneId>`).\n";
|
|
@@ -10,6 +10,7 @@ This document describes how the Phibelle 3D engine runs your scene and entity sc
|
|
|
10
10
|
|
|
11
11
|
- **Phibelle** is a React Three Fiber based 3D engine used in the [Phibelle Studio](https://phibelle.studio) editor.
|
|
12
12
|
- A **scene** has one **scene script** (\`scene.tsx\`) and zero or more **entity scripts** (\`entity-<engineId>.tsx\`).
|
|
13
|
+
- You can **create new entities** by adding new entity script files (\`entity-<engineId>.tsx\`); when the watcher is running, they are synced and the new entities appear in the scene.
|
|
13
14
|
- The engine **concatenates** these scripts and runs them in a single live-editing environment with pre-injected globals (no \`import\`/\`export\`).
|
|
14
15
|
|
|
15
16
|
## Runtime Model
|
|
@@ -91,14 +92,18 @@ const Render1 = ({ entityData }: { entityData: PHI.EntityData }) => {
|
|
|
91
92
|
};
|
|
92
93
|
\`\`\`
|
|
93
94
|
|
|
95
|
+
## Creating New Entities
|
|
96
|
+
|
|
97
|
+
You can add new entities to the scene by creating new entity script files. Add a file named \`entity-<engineId>.tsx\` (e.g. \`entity-2.tsx\`, \`entity-3.tsx\`) with a component \`Render<engineId>\` (e.g. \`Render2\`, \`Render3\`). 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
|
+
|
|
94
99
|
## File Layout After Clone
|
|
95
100
|
|
|
96
101
|
- \`scene.json\`: Full scene state (from the app); used by the CLI, not by the runtime.
|
|
97
|
-
- \`scene.tsx\`: Scene script (single \`SceneRender\`).
|
|
98
|
-
- \`entity-<engineId>.tsx\`: One file per entity; each exports a \`Render<engineId>\` component by convention.
|
|
99
102
|
- \`manifest.json\`: Maps engine IDs to filenames for the watch/sync process.
|
|
100
103
|
- \`package.json\`: Scripts: \`watch\` (sync edits to the app), and dependencies for types/IntelliSense.
|
|
101
104
|
- \`global.d.ts\`: Declares global \`PHI\`, \`THREE\`, \`R3F\`, \`DREI\`, \`UIKIT\`, \`RAPIER\` for type checking.
|
|
105
|
+
- \`app/scene.tsx\`: Scene script (single \`SceneRender\`).
|
|
106
|
+
- \`app/entity-<engineId>.tsx\`: One file per entity; each exports a \`Render<engineId>\` component by convention.
|
|
102
107
|
|
|
103
108
|
## Sync Flow (phibelle-kit)
|
|
104
109
|
|