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 parent directory.
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. */
@@ -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
- await fs.mkdir(sceneDir, { recursive: true });
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(sceneDir, { withFileTypes: true });
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(sceneDir, ent.name));
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(sceneDir, "scene.tsx"), parsed.sceneScriptStore?.sceneScript ?? "", "utf8");
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(sceneDir, fileName), entity.script, "utf8");
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 filePath = path.join(sceneDir, changedFile);
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 filePath = path.join(sceneDir, fileName);
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(sceneDir, { ignoreInitial: true });
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## File Layout After Clone\n\n- `scene.json`: Full scene state (from the app); used by the CLI, not by the runtime.\n- `scene.tsx`: Scene script (single `SceneRender`).\n- `entity-<engineId>.tsx`: One file per entity; each exports a `Render<engineId>` component by convention.\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\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";
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phibelle-kit",
3
- "version": "1.0.8",
3
+ "version": "1.0.11",
4
4
  "description": "CLI tool for interacting with the Phibelle engine",
5
5
  "type": "module",
6
6
  "bin": {