react-three-game 0.0.45 → 0.0.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/.gitmodules +3 -0
  2. package/README.md +5 -0
  3. package/assets/architecture.png +0 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/tools/prefabeditor/PrefabEditor.js +4 -13
  6. package/dist/tools/prefabeditor/PrefabRoot.js +8 -3
  7. package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +2 -0
  8. package/dist/tools/prefabeditor/components/ComponentRegistry.js +5 -0
  9. package/dist/tools/prefabeditor/components/GeometryComponent.js +1 -0
  10. package/dist/tools/prefabeditor/components/MaterialComponent.js +3 -2
  11. package/dist/tools/prefabeditor/components/ModelComponent.js +1 -0
  12. package/dist/tools/prefabeditor/components/PhysicsComponent.js +1 -0
  13. package/dist/tools/prefabeditor/components/TextComponent.d.ts +3 -0
  14. package/dist/tools/prefabeditor/components/TextComponent.js +103 -0
  15. package/dist/tools/prefabeditor/components/TransformComponent.js +1 -0
  16. package/dist/tools/prefabeditor/components/index.js +3 -1
  17. package/dist/tools/prefabeditor/utils.d.ts +20 -0
  18. package/dist/tools/prefabeditor/utils.js +51 -0
  19. package/package.json +3 -2
  20. package/react-three-game-skill/.gitattributes +2 -0
  21. package/react-three-game-skill/react-three-game/SKILL.md +609 -0
  22. package/src/index.ts +1 -0
  23. package/src/tools/prefabeditor/PrefabEditor.tsx +4 -19
  24. package/src/tools/prefabeditor/PrefabRoot.tsx +11 -2
  25. package/src/tools/prefabeditor/components/ComponentRegistry.ts +8 -0
  26. package/src/tools/prefabeditor/components/GeometryComponent.tsx +1 -0
  27. package/src/tools/prefabeditor/components/MaterialComponent.tsx +1 -2
  28. package/src/tools/prefabeditor/components/ModelComponent.tsx +1 -0
  29. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +1 -0
  30. package/src/tools/prefabeditor/components/TextComponent.tsx +136 -0
  31. package/src/tools/prefabeditor/components/TransformComponent.tsx +1 -0
  32. package/src/tools/prefabeditor/components/index.ts +3 -1
  33. package/src/tools/prefabeditor/utils.ts +69 -0
package/.gitmodules ADDED
@@ -0,0 +1,3 @@
1
+ [submodule "react-three-game-skill"]
2
+ path = react-three-game-skill
3
+ url = https://github.com/prnthh/react-three-game-skill
package/README.md CHANGED
@@ -9,6 +9,11 @@ npm i react-three-game @react-three/fiber @react-three/rapier three
9
9
  ![Prefab Editor](assets/editor.gif)
10
10
  ![Architecture](assets/architecture.png)
11
11
 
12
+ ## Agent Skill
13
+ ```bash
14
+ npx skills add https://github.com/prnthh/react-three-game-skill
15
+ ```
16
+
12
17
  ## Usage
13
18
 
14
19
  ```jsx
Binary file
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
6
6
  export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
7
7
  export { FieldRenderer, Input, Label, Vector3Input, ColorInput, StringInput, BooleanInput, SelectInput, } from './tools/prefabeditor/components/Input';
8
8
  export * from './tools/prefabeditor/utils';
9
+ export type { ExportGLBOptions } from './tools/prefabeditor/utils';
9
10
  export type { PrefabEditorRef } from './tools/prefabeditor/PrefabEditor';
10
11
  export type { PrefabRootRef } from './tools/prefabeditor/PrefabRoot';
11
12
  export type { Component } from './tools/prefabeditor/components/ComponentRegistry';
@@ -6,7 +6,7 @@ import { Physics } from "@react-three/rapier";
6
6
  import EditorUI from "./EditorUI";
7
7
  import { base, toolbar } from "./styles";
8
8
  import { EditorContext } from "./EditorContext";
9
- import { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter.js";
9
+ import { exportGLB } from "./utils";
10
10
  const DEFAULT_PREFAB = {
11
11
  id: "prefab-default",
12
12
  name: "New Prefab",
@@ -102,18 +102,9 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, onPrefabChange, chil
102
102
  const sceneRoot = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.root;
103
103
  if (!sceneRoot)
104
104
  return;
105
- const exporter = new GLTFExporter();
106
- exporter.parse(sceneRoot, (result) => {
107
- const blob = new Blob([result], { type: 'application/octet-stream' });
108
- const url = URL.createObjectURL(blob);
109
- const a = document.createElement('a');
110
- a.href = url;
111
- a.download = `${loadedPrefab.name || 'scene'}.glb`;
112
- a.click();
113
- URL.revokeObjectURL(url);
114
- }, (error) => {
115
- console.error('Error exporting GLB:', error);
116
- }, { binary: true });
105
+ exportGLB(sceneRoot, {
106
+ filename: `${loadedPrefab.name || 'scene'}.glb`
107
+ });
117
108
  };
118
109
  useEffect(() => {
119
110
  const canvas = document.querySelector('canvas');
@@ -11,7 +11,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
11
11
  import { MapControls, TransformControls, useHelper } from "@react-three/drei";
12
12
  import { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useRef, useState } from "react";
13
13
  import { BoxHelper, Euler, Matrix4, Quaternion, SRGBColorSpace, TextureLoader, Vector3, } from "three";
14
- import { getComponent, registerComponent } from "./components/ComponentRegistry";
14
+ import { getComponent, registerComponent, getNonComposableKeys } from "./components/ComponentRegistry";
15
15
  import components from "./components";
16
16
  import { loadModel } from "../dragdrop/modelLoader";
17
17
  import { GameInstance, GameInstanceProvider, useInstanceCheck } from "./InstanceProvider";
@@ -245,13 +245,15 @@ function computeParentWorldMatrix(root, targetId) {
245
245
  return result !== null && result !== void 0 ? result : IDENTITY;
246
246
  }
247
247
  function renderCoreNode(gameObject, ctx, parentMatrix) {
248
- var _a, _b, _c;
248
+ var _a, _b, _c, _d;
249
249
  const geometry = (_a = gameObject.components) === null || _a === void 0 ? void 0 : _a.geometry;
250
250
  const material = (_b = gameObject.components) === null || _b === void 0 ? void 0 : _b.material;
251
251
  const model = (_c = gameObject.components) === null || _c === void 0 ? void 0 : _c.model;
252
+ const text = (_d = gameObject.components) === null || _d === void 0 ? void 0 : _d.text;
252
253
  const geometryDef = geometry && getComponent("Geometry");
253
254
  const materialDef = material && getComponent("Material");
254
255
  const modelDef = model && getComponent("Model");
256
+ const textDef = text && getComponent("Text");
255
257
  const contextProps = {
256
258
  loadedModels: ctx.loadedModels,
257
259
  loadedTextures: ctx.loadedTextures,
@@ -263,7 +265,7 @@ function renderCoreNode(gameObject, ctx, parentMatrix) {
263
265
  const leaves = [];
264
266
  if (gameObject.components) {
265
267
  Object.entries(gameObject.components)
266
- .filter(([k]) => !["geometry", "material", "model", "transform", "physics"].includes(k))
268
+ .filter(([k]) => !getNonComposableKeys().includes(k))
267
269
  .forEach(([key, comp]) => {
268
270
  if (!(comp === null || comp === void 0 ? void 0 : comp.type))
269
271
  return;
@@ -285,6 +287,9 @@ function renderCoreNode(gameObject, ctx, parentMatrix) {
285
287
  else if (geometry && (geometryDef === null || geometryDef === void 0 ? void 0 : geometryDef.View)) {
286
288
  core = (_jsxs("mesh", { castShadow: true, receiveShadow: true, children: [_jsx(geometryDef.View, Object.assign({ properties: geometry.properties }, contextProps)), material && (materialDef === null || materialDef === void 0 ? void 0 : materialDef.View) && (_jsx(materialDef.View, Object.assign({ properties: material.properties }, contextProps), "material")), leaves] }));
287
289
  }
290
+ else if (text && (textDef === null || textDef === void 0 ? void 0 : textDef.View)) {
291
+ core = (_jsxs(_Fragment, { children: [_jsx(textDef.View, Object.assign({ properties: text.properties }, contextProps)), leaves] }));
292
+ }
288
293
  else {
289
294
  core = _jsx(_Fragment, { children: leaves });
290
295
  }
@@ -10,7 +10,9 @@ export interface Component {
10
10
  }>;
11
11
  defaultProperties: any;
12
12
  View?: FC<any>;
13
+ nonComposable?: boolean;
13
14
  }
14
15
  export declare function registerComponent(component: Component): void;
15
16
  export declare function getComponent(name: string): Component | undefined;
16
17
  export declare function getAllComponents(): Record<string, Component>;
18
+ export declare function getNonComposableKeys(): string[];
@@ -11,3 +11,8 @@ export function getComponent(name) {
11
11
  export function getAllComponents() {
12
12
  return Object.assign({}, REGISTRY);
13
13
  }
14
+ export function getNonComposableKeys() {
15
+ return Object.values(REGISTRY)
16
+ .filter(c => c.nonComposable)
17
+ .map(c => c.name.toLowerCase());
18
+ }
@@ -77,6 +77,7 @@ const GeometryComponent = {
77
77
  name: 'Geometry',
78
78
  Editor: GeometryComponentEditor,
79
79
  View: GeometryComponentView,
80
+ nonComposable: true,
80
81
  defaultProperties: {
81
82
  geometryType: 'box',
82
83
  args: GEOMETRY_ARGS.box.defaults,
@@ -3,7 +3,7 @@ import { SingleTextureViewer, TextureListViewer } from '../../assetviewer/page';
3
3
  import { useEffect, useState } from 'react';
4
4
  import { FieldRenderer, Input } from './Input';
5
5
  import { useMemo } from 'react';
6
- import { DoubleSide, RepeatWrapping, ClampToEdgeWrapping, SRGBColorSpace, NearestFilter, LinearFilter, NearestMipmapNearestFilter, NearestMipmapLinearFilter, LinearMipmapNearestFilter, LinearMipmapLinearFilter } from 'three';
6
+ import { RepeatWrapping, ClampToEdgeWrapping, SRGBColorSpace, NearestFilter, LinearFilter, NearestMipmapNearestFilter, NearestMipmapLinearFilter, LinearMipmapNearestFilter, LinearMipmapLinearFilter } from 'three';
7
7
  function TexturePicker({ value, onChange, basePath }) {
8
8
  const [textureFiles, setTextureFiles] = useState([]);
9
9
  const [showPicker, setShowPicker] = useState(false);
@@ -117,12 +117,13 @@ function MaterialComponentView({ properties, loadedTextures }) {
117
117
  return _jsx("meshStandardMaterial", { color: "red", wireframe: true });
118
118
  }
119
119
  const { color, wireframe = false } = properties;
120
- return (_jsx("meshStandardMaterial", { color: color, wireframe: wireframe, map: finalTexture, transparent: !!finalTexture, side: DoubleSide }, (_a = finalTexture === null || finalTexture === void 0 ? void 0 : finalTexture.uuid) !== null && _a !== void 0 ? _a : 'no-texture'));
120
+ return (_jsx("meshStandardMaterial", { color: color, wireframe: wireframe, map: finalTexture, transparent: !!finalTexture }, (_a = finalTexture === null || finalTexture === void 0 ? void 0 : finalTexture.uuid) !== null && _a !== void 0 ? _a : 'no-texture'));
121
121
  }
122
122
  const MaterialComponent = {
123
123
  name: 'Material',
124
124
  Editor: MaterialComponentEditor,
125
125
  View: MaterialComponentView,
126
+ nonComposable: true,
126
127
  defaultProperties: {
127
128
  color: '#ffffff',
128
129
  wireframe: false
@@ -58,6 +58,7 @@ const ModelComponent = {
58
58
  name: 'Model',
59
59
  Editor: ModelComponentEditor,
60
60
  View: ModelComponentView,
61
+ nonComposable: true,
61
62
  defaultProperties: {
62
63
  filename: '',
63
64
  instanced: false
@@ -39,6 +39,7 @@ const PhysicsComponent = {
39
39
  name: 'Physics',
40
40
  Editor: PhysicsComponentEditor,
41
41
  View: PhysicsComponentView,
42
+ nonComposable: true,
42
43
  defaultProperties: { type: 'dynamic', collider: 'hull' }
43
44
  };
44
45
  export default PhysicsComponent;
@@ -0,0 +1,3 @@
1
+ import { Component } from "./ComponentRegistry";
2
+ declare const TextComponent: Component;
3
+ export default TextComponent;
@@ -0,0 +1,103 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { FieldRenderer } from "./Input";
3
+ import { Text } from 'three-text/three/react';
4
+ import { useRef, useState, useCallback } from 'react';
5
+ // Initialize HarfBuzz path for font shaping
6
+ Text.setHarfBuzzPath('/fonts/hb.wasm');
7
+ function TextComponentEditor({ component, onUpdate, }) {
8
+ const fields = [
9
+ {
10
+ name: 'text',
11
+ type: 'string',
12
+ label: 'Text',
13
+ placeholder: 'Enter text...',
14
+ },
15
+ {
16
+ name: 'color',
17
+ type: 'color',
18
+ label: 'Color',
19
+ },
20
+ {
21
+ name: 'font',
22
+ type: 'string',
23
+ label: 'Font',
24
+ placeholder: '/fonts/NotoSans-Regular.ttf',
25
+ },
26
+ {
27
+ name: 'size',
28
+ type: 'number',
29
+ label: 'Size',
30
+ min: 0.01,
31
+ step: 0.1,
32
+ },
33
+ {
34
+ name: 'depth',
35
+ type: 'number',
36
+ label: 'Depth',
37
+ min: 0,
38
+ step: 0.1,
39
+ },
40
+ {
41
+ name: 'width',
42
+ type: 'number',
43
+ label: 'Width',
44
+ min: 0,
45
+ step: 0.5,
46
+ },
47
+ {
48
+ name: 'align',
49
+ type: 'select',
50
+ label: 'Align',
51
+ options: [
52
+ { value: 'left', label: 'Left' },
53
+ { value: 'center', label: 'Center' },
54
+ { value: 'right', label: 'Right' },
55
+ ],
56
+ },
57
+ ];
58
+ return (_jsx(FieldRenderer, { fields: fields, values: component.properties, onChange: onUpdate }));
59
+ }
60
+ function TextComponentView({ properties }) {
61
+ const { text = '', font, size, depth, width, align, color } = properties;
62
+ const textContent = String(text || '');
63
+ const meshRef = useRef(null);
64
+ const [offset, setOffset] = useState([0, 0, 0]);
65
+ const handleLoad = useCallback((_geometry, info) => {
66
+ if (info === null || info === void 0 ? void 0 : info.planeBounds) {
67
+ const bounds = info.planeBounds;
68
+ // Calculate X offset based on alignment
69
+ let centerX = 0;
70
+ if (align === 'center') {
71
+ centerX = -(bounds.min.x + bounds.max.x) / 2;
72
+ }
73
+ else if (align === 'right') {
74
+ centerX = -bounds.max.x;
75
+ }
76
+ else {
77
+ // left alignment
78
+ centerX = -bounds.min.x;
79
+ }
80
+ const centerY = -(bounds.min.y + bounds.max.y) / 2;
81
+ setOffset([centerX, centerY, 0]);
82
+ }
83
+ }, [align]);
84
+ if (!textContent)
85
+ return null;
86
+ return (_jsx("group", { position: offset, children: _jsx(Text, { ref: meshRef, font: font, size: size, depth: depth, layout: { align, width }, color: color, onLoad: handleLoad, children: textContent }) }));
87
+ }
88
+ const TextComponent = {
89
+ name: 'Text',
90
+ Editor: TextComponentEditor,
91
+ View: TextComponentView,
92
+ nonComposable: true,
93
+ defaultProperties: {
94
+ text: 'Hello World',
95
+ color: '#888888',
96
+ font: '/fonts/NotoSans-Regular.ttf',
97
+ size: 0.5,
98
+ depth: 0,
99
+ width: 5,
100
+ align: 'center',
101
+ }
102
+ };
103
+ export default TextComponent;
@@ -41,6 +41,7 @@ function TransformComponentEditor({ component, onUpdate }) {
41
41
  const TransformComponent = {
42
42
  name: 'Transform',
43
43
  Editor: TransformComponentEditor,
44
+ nonComposable: true,
44
45
  defaultProperties: {
45
46
  position: [0, 0, 0],
46
47
  rotation: [0, 0, 0],
@@ -5,6 +5,7 @@ import PhysicsComponent from './PhysicsComponent';
5
5
  import SpotLightComponent from './SpotLightComponent';
6
6
  import DirectionalLightComponent from './DirectionalLightComponent';
7
7
  import ModelComponent from './ModelComponent';
8
+ import TextComponent from './TextComponent';
8
9
  export default [
9
10
  GeometryComponent,
10
11
  TransformComponent,
@@ -12,5 +13,6 @@ export default [
12
13
  PhysicsComponent,
13
14
  SpotLightComponent,
14
15
  DirectionalLightComponent,
15
- ModelComponent
16
+ ModelComponent,
17
+ TextComponent
16
18
  ];
@@ -1,8 +1,28 @@
1
1
  import { GameObject, Prefab } from "./types";
2
+ import { Object3D } from 'three';
3
+ export interface ExportGLBOptions {
4
+ filename?: string;
5
+ binary?: boolean;
6
+ onComplete?: (result: ArrayBuffer | object) => void;
7
+ onError?: (error: any) => void;
8
+ }
2
9
  /** Save a prefab as JSON file */
3
10
  export declare function saveJson(data: Prefab, filename: string): void;
4
11
  /** Load a prefab from JSON file */
5
12
  export declare function loadJson(): Promise<Prefab | undefined>;
13
+ /**
14
+ * Export a Three.js scene or object to GLB format
15
+ * @param sceneRoot - The Three.js Object3D to export
16
+ * @param options - Export options
17
+ * @returns Promise that resolves when export is complete
18
+ */
19
+ export declare function exportGLB(sceneRoot: Object3D, options?: ExportGLBOptions): Promise<ArrayBuffer | object>;
20
+ /**
21
+ * Export a Three.js scene to GLB and return the ArrayBuffer without downloading
22
+ * @param sceneRoot - The Three.js Object3D to export
23
+ * @returns Promise that resolves with the GLB data as ArrayBuffer
24
+ */
25
+ export declare function exportGLBData(sceneRoot: Object3D): Promise<ArrayBuffer>;
6
26
  /** Find a node by ID in the tree */
7
27
  export declare function findNode(root: GameObject, id: string): GameObject | null;
8
28
  /** Find the parent of a node by ID */
@@ -1,3 +1,13 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
1
11
  /** Save a prefab as JSON file */
2
12
  export function saveJson(data, filename) {
3
13
  const a = document.createElement('a');
@@ -34,6 +44,47 @@ export function loadJson() {
34
44
  input.click();
35
45
  });
36
46
  }
47
+ /**
48
+ * Export a Three.js scene or object to GLB format
49
+ * @param sceneRoot - The Three.js Object3D to export
50
+ * @param options - Export options
51
+ * @returns Promise that resolves when export is complete
52
+ */
53
+ export function exportGLB(sceneRoot, options = {}) {
54
+ const { filename = 'scene.glb', binary = true, onComplete, onError } = options;
55
+ return new Promise((resolve, reject) => {
56
+ const exporter = new GLTFExporter();
57
+ exporter.parse(sceneRoot, (result) => {
58
+ onComplete === null || onComplete === void 0 ? void 0 : onComplete(result);
59
+ resolve(result);
60
+ // Trigger download if filename is provided
61
+ if (filename) {
62
+ const blob = new Blob([result], { type: binary ? 'application/octet-stream' : 'application/json' });
63
+ const url = URL.createObjectURL(blob);
64
+ const a = document.createElement('a');
65
+ a.href = url;
66
+ a.download = filename;
67
+ a.click();
68
+ URL.revokeObjectURL(url);
69
+ }
70
+ }, (error) => {
71
+ console.error('Error exporting GLB:', error);
72
+ onError === null || onError === void 0 ? void 0 : onError(error);
73
+ reject(error);
74
+ }, { binary });
75
+ });
76
+ }
77
+ /**
78
+ * Export a Three.js scene to GLB and return the ArrayBuffer without downloading
79
+ * @param sceneRoot - The Three.js Object3D to export
80
+ * @returns Promise that resolves with the GLB data as ArrayBuffer
81
+ */
82
+ export function exportGLBData(sceneRoot) {
83
+ return __awaiter(this, void 0, void 0, function* () {
84
+ const result = yield exportGLB(sceneRoot, { filename: '', binary: true });
85
+ return result;
86
+ });
87
+ }
37
88
  /** Find a node by ID in the tree */
38
89
  export function findNode(root, id) {
39
90
  var _a;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.45",
3
+ "version": "0.0.47",
4
4
  "description": "Batteries included React Three Fiber game engine",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -24,7 +24,8 @@
24
24
  "@react-three/rapier": ">=2.0.0",
25
25
  "react": ">=18.0.0",
26
26
  "react-dom": ">=18.0.0",
27
- "three": ">=0.182.0"
27
+ "three": ">=0.182.0",
28
+ "three-text": ">=0.4.4"
28
29
  },
29
30
  "devDependencies": {
30
31
  "@react-three/drei": "^10.7.7",
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto