react-three-game 0.0.19 → 0.0.21

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.
@@ -1,9 +1,10 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { Canvas, useLoader } from "@react-three/fiber";
4
- import { OrbitControls, useGLTF, useFBX, Stage, View, PerspectiveCamera } from "@react-three/drei";
4
+ import { OrbitControls, Stage, View, PerspectiveCamera } from "@react-three/drei";
5
5
  import { Suspense, useEffect, useState, useRef } from "react";
6
6
  import { TextureLoader } from "three";
7
+ import { loadModel } from "../dragdrop/modelLoader";
7
8
  // view models and textures in manifest, onselect callback
8
9
  function getItemsInPath(files, currentPath) {
9
10
  // Remove the leading category folder (e.g., /textures/, /models/, /sounds/)
@@ -100,18 +101,28 @@ function ModelCard({ file, onSelect, basePath = "" }) {
100
101
  return (_jsxs("div", { ref: ref, className: "aspect-square bg-gray-900 cursor-pointer hover:bg-gray-800 flex flex-col", onClick: () => onSelect(file), children: [_jsx("div", { className: "flex-1 relative", children: isInView ? (_jsxs(View, { className: "w-full h-full", children: [_jsx(PerspectiveCamera, { makeDefault: true, position: [0, 1, 3], fov: 50 }), _jsxs(Suspense, { fallback: null, children: [_jsx(Stage, { intensity: 0.5, environment: "city", children: _jsx(ModelPreview, { url: fullPath, onError: () => setError(true) }) }), _jsx(OrbitControls, { enableZoom: false })] })] })) : null }), _jsx("div", { className: "bg-black/60 text-[10px] px-1 truncate text-center", children: file.split('/').pop() })] }));
101
102
  }
102
103
  function ModelPreview({ url, onError }) {
103
- const isFbx = url.toLowerCase().endsWith('.fbx');
104
- if (isFbx)
105
- return _jsx(FBXModel, { url: url, onError: onError });
106
- return _jsx(GLTFModel, { url: url, onError: onError });
107
- }
108
- function GLTFModel({ url, onError }) {
109
- const { scene } = useGLTF(url);
110
- return _jsx("primitive", { object: scene });
111
- }
112
- function FBXModel({ url, onError }) {
113
- const fbx = useFBX(url);
114
- return _jsx("primitive", { object: fbx, scale: 0.01 });
104
+ const [model, setModel] = useState(null);
105
+ const onErrorRef = useRef(onError);
106
+ onErrorRef.current = onError;
107
+ useEffect(() => {
108
+ let cancelled = false;
109
+ setModel(null);
110
+ loadModel(url).then((result) => {
111
+ var _a;
112
+ if (cancelled)
113
+ return;
114
+ if (result.success && result.model) {
115
+ setModel(result.model);
116
+ }
117
+ else {
118
+ (_a = onErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onErrorRef);
119
+ }
120
+ });
121
+ return () => { cancelled = true; };
122
+ }, [url]);
123
+ if (!model)
124
+ return null;
125
+ return _jsx("primitive", { object: model });
115
126
  }
116
127
  export function SoundListViewer({ files, selected, onSelect, basePath = "" }) {
117
128
  return (_jsx(AssetListViewer, { files: files, selected: selected, onSelect: onSelect, renderCard: (file, onSelectHandler) => (_jsx(SoundCard, { file: file, basePath: basePath, onSelect: onSelectHandler })) }));
@@ -108,7 +108,7 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
108
108
  return _jsxs("group", { ref: ref, children: [_jsx(GameInstanceProvider, { models: loadedModels, onSelect: editMode ? onSelect : undefined, registerRef: registerRef, children: _jsx(GameObjectRenderer, { gameObject: data.root, selectedId: selectedId, onSelect: editMode ? onSelect : undefined, registerRef: registerRef, loadedModels: loadedModels, loadedTextures: loadedTextures, editMode: editMode, parentMatrix: new Matrix4() }) }), editMode && _jsxs(_Fragment, { children: [_jsx(MapControls, { makeDefault: true }), selectedId && selectedObject && (_jsx(TransformControls, { object: selectedObject, mode: transformMode, space: "local", onObjectChange: onTransformChange }))] })] });
109
109
  });
110
110
  function GameObjectRenderer({ gameObject, selectedId, onSelect, registerRef, loadedModels, loadedTextures, editMode, parentMatrix = new Matrix4(), }) {
111
- var _a, _b, _c, _d;
111
+ var _a, _b, _c, _d, _e;
112
112
  // Early return if gameObject is null or undefined
113
113
  if (!gameObject)
114
114
  return null;
@@ -144,12 +144,28 @@ function GameObjectRenderer({ gameObject, selectedId, onSelect, registerRef, loa
144
144
  }
145
145
  // --- 4. Render core content using component system ---
146
146
  const core = renderCoreNode(gameObject, ctx, parentMatrix);
147
- // --- 5. Wrap with physics if needed (except in edit mode) ---
148
- const physicsWrapped = wrapPhysicsIfNeeded(gameObject, core, ctx);
149
- // --- 6. Render children recursively (always relative transforms) ---
147
+ // --- 5. Render children recursively (always relative transforms) ---
150
148
  const children = ((_d = gameObject.children) !== null && _d !== void 0 ? _d : []).map((child) => (_jsx(GameObjectRenderer, { gameObject: child, selectedId: selectedId, onSelect: onSelect, registerRef: registerRef, loadedModels: loadedModels, loadedTextures: loadedTextures, editMode: editMode, parentMatrix: worldMatrix }, child.id)));
151
- // --- 7. Final group wrapper with local transform ---
152
- return (_jsxs("group", { ref: (el) => registerRef(gameObject.id, el), position: transformProps.position, rotation: transformProps.rotation, scale: transformProps.scale, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, children: [physicsWrapped, children] }));
149
+ // --- 6. Common props for the wrapper element ---
150
+ const wrapperProps = {
151
+ position: transformProps.position,
152
+ rotation: transformProps.rotation,
153
+ onPointerDown: handlePointerDown,
154
+ onPointerMove: handlePointerMove,
155
+ onPointerUp: handlePointerUp,
156
+ };
157
+ // --- 7. Check if physics is needed ---
158
+ const physics = (_e = gameObject.components) === null || _e === void 0 ? void 0 : _e.physics;
159
+ const hasPhysics = physics && !editMode;
160
+ // --- 8. Final structure: RigidBody outside with position/rotation, scale inside ---
161
+ if (hasPhysics) {
162
+ const physicsDef = getComponent('Physics');
163
+ if (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View) {
164
+ return (_jsx(physicsDef.View, Object.assign({ properties: physics.properties, ref: (obj) => registerRef(gameObject.id, obj) }, wrapperProps, { children: _jsxs("group", { scale: transformProps.scale, children: [core, children] }) })));
165
+ }
166
+ }
167
+ // --- 9. No physics - standard group wrapper ---
168
+ return (_jsxs("group", Object.assign({ ref: (el) => registerRef(gameObject.id, el), scale: transformProps.scale }, wrapperProps, { children: [core, children] })));
153
169
  }
154
170
  // Helper: render an instanced GameInstance (terminal node)
155
171
  function renderInstancedNode(gameObject, worldMatrix, ctx) {
@@ -223,17 +239,6 @@ function renderCoreNode(gameObject, ctx, parentMatrix) {
223
239
  return _jsx(View, Object.assign({ properties: properties }, contextProps, { children: content }), key);
224
240
  }, coreContent);
225
241
  }
226
- // Helper: wrap core content with physics component when necessary
227
- function wrapPhysicsIfNeeded(gameObject, content, ctx) {
228
- var _a;
229
- const physics = (_a = gameObject.components) === null || _a === void 0 ? void 0 : _a.physics;
230
- if (!physics)
231
- return content;
232
- const physicsDef = getComponent('Physics');
233
- if (!physicsDef || !physicsDef.View)
234
- return content;
235
- return (_jsx(physicsDef.View, { properties: Object.assign(Object.assign({}, physics.properties), { id: gameObject.id }), editMode: ctx.editMode, children: content }));
236
- }
237
242
  export default PrefabRoot;
238
243
  function getNodeTransformProps(node) {
239
244
  var _a, _b, _c, _d, _e;
@@ -1,19 +1,33 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
13
+ import { RigidBody } from "@react-three/rapier";
14
+ const selectClass = "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50";
15
+ const labelClass = "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5";
2
16
  function PhysicsComponentEditor({ component, onUpdate }) {
3
- return _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Type" }), _jsxs("select", { className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: component.properties.type, onChange: e => onUpdate({ type: e.target.value }), children: [_jsx("option", { value: "dynamic", children: "Dynamic" }), _jsx("option", { value: "fixed", children: "Fixed" })] })] });
17
+ const { type, collider = 'hull' } = component.properties;
18
+ return (_jsxs("div", { children: [_jsx("label", { className: labelClass, children: "Type" }), _jsxs("select", { className: selectClass, value: type, onChange: e => onUpdate({ type: e.target.value }), children: [_jsx("option", { value: "dynamic", children: "Dynamic" }), _jsx("option", { value: "fixed", children: "Fixed" })] }), _jsx("label", { className: `${labelClass} mt-2`, children: "Collider" }), _jsxs("select", { className: selectClass, value: collider, onChange: e => onUpdate({ collider: e.target.value }), children: [_jsx("option", { value: "hull", children: "Hull (convex)" }), _jsx("option", { value: "trimesh", children: "Trimesh (exact)" }), _jsx("option", { value: "cuboid", children: "Cuboid (box)" }), _jsx("option", { value: "ball", children: "Ball (sphere)" })] })] }));
4
19
  }
5
- import { RigidBody } from "@react-three/rapier";
6
- function PhysicsComponentView({ properties, children, editMode }) {
20
+ function PhysicsComponentView(_a) {
21
+ var { properties, editMode, children } = _a, rigidBodyProps = __rest(_a, ["properties", "editMode", "children"]);
7
22
  if (editMode)
8
- return children;
9
- return (_jsx(RigidBody, { type: properties.type, colliders: "cuboid", children: children }));
23
+ return _jsx(_Fragment, { children: children });
24
+ const colliders = properties.collider || (properties.type === 'fixed' ? 'trimesh' : 'hull');
25
+ return (_jsx(RigidBody, Object.assign({ type: properties.type, colliders: colliders }, rigidBodyProps, { children: children })));
10
26
  }
11
27
  const PhysicsComponent = {
12
28
  name: 'Physics',
13
29
  Editor: PhysicsComponentEditor,
14
30
  View: PhysicsComponentView,
15
- defaultProperties: {
16
- type: 'dynamic'
17
- }
31
+ defaultProperties: { type: 'dynamic', collider: 'hull' }
18
32
  };
19
33
  export default PhysicsComponent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.19",
3
+ "version": "0.0.21",
4
4
  "description": "Batteries included React Three Fiber game engine",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -15,10 +15,12 @@
15
15
  "author": "prnth",
16
16
  "license": "VPL",
17
17
  "type": "module",
18
- "workspaces": ["docs"],
18
+ "workspaces": [
19
+ "docs"
20
+ ],
19
21
  "peerDependencies": {
20
- "@react-three/fiber": ">=9.0.0",
21
22
  "@react-three/drei": ">=10.0.0",
23
+ "@react-three/fiber": ">=9.0.0",
22
24
  "@react-three/rapier": ">=2.0.0",
23
25
  "react": ">=18.0.0",
24
26
  "react-dom": ">=18.0.0",
@@ -36,5 +38,8 @@
36
38
  "react-dom": "^19.2.0",
37
39
  "three": "^0.182.0",
38
40
  "typescript": "^5.9.3"
41
+ },
42
+ "dependencies": {
43
+ "react-error-boundary": "^6.0.0"
39
44
  }
40
45
  }
@@ -1,9 +1,10 @@
1
1
  "use client";
2
2
 
3
3
  import { Canvas, useLoader } from "@react-three/fiber";
4
- import { OrbitControls, useGLTF, useFBX, Stage, View, PerspectiveCamera } from "@react-three/drei";
4
+ import { OrbitControls, Stage, View, PerspectiveCamera } from "@react-three/drei";
5
5
  import { Suspense, useEffect, useState, useRef } from "react";
6
6
  import { TextureLoader } from "three";
7
+ import { loadModel } from "../dragdrop/modelLoader";
7
8
 
8
9
  // view models and textures in manifest, onselect callback
9
10
 
@@ -290,19 +291,28 @@ function ModelCard({ file, onSelect, basePath = "" }: { file: string; onSelect:
290
291
  }
291
292
 
292
293
  function ModelPreview({ url, onError }: { url: string; onError?: () => void }) {
293
- const isFbx = url.toLowerCase().endsWith('.fbx');
294
- if (isFbx) return <FBXModel url={url} onError={onError} />;
295
- return <GLTFModel url={url} onError={onError} />;
296
- }
294
+ const [model, setModel] = useState<any>(null);
295
+ const onErrorRef = useRef(onError);
296
+ onErrorRef.current = onError;
297
297
 
298
- function GLTFModel({ url, onError }: { url: string; onError?: () => void }) {
299
- const { scene } = useGLTF(url);
300
- return <primitive object={scene} />;
301
- }
298
+ useEffect(() => {
299
+ let cancelled = false;
300
+ setModel(null);
301
+
302
+ loadModel(url).then((result) => {
303
+ if (cancelled) return;
304
+ if (result.success && result.model) {
305
+ setModel(result.model);
306
+ } else {
307
+ onErrorRef.current?.();
308
+ }
309
+ });
310
+
311
+ return () => { cancelled = true; };
312
+ }, [url]);
302
313
 
303
- function FBXModel({ url, onError }: { url: string; onError?: () => void }) {
304
- const fbx = useFBX(url);
305
- return <primitive object={fbx} scale={0.01} />;
314
+ if (!model) return null;
315
+ return <primitive object={model} />;
306
316
  }
307
317
 
308
318
  interface SoundListViewerProps {
@@ -222,10 +222,7 @@ function GameObjectRenderer({
222
222
  // --- 4. Render core content using component system ---
223
223
  const core = renderCoreNode(gameObject, ctx, parentMatrix);
224
224
 
225
- // --- 5. Wrap with physics if needed (except in edit mode) ---
226
- const physicsWrapped = wrapPhysicsIfNeeded(gameObject, core, ctx);
227
-
228
- // --- 6. Render children recursively (always relative transforms) ---
225
+ // --- 5. Render children recursively (always relative transforms) ---
229
226
  const children = (gameObject.children ?? []).map((child) => (
230
227
  <GameObjectRenderer
231
228
  key={child.id}
@@ -240,18 +237,46 @@ function GameObjectRenderer({
240
237
  />
241
238
  ));
242
239
 
243
- // --- 7. Final group wrapper with local transform ---
240
+ // --- 6. Common props for the wrapper element ---
241
+ const wrapperProps = {
242
+ position: transformProps.position,
243
+ rotation: transformProps.rotation,
244
+ onPointerDown: handlePointerDown,
245
+ onPointerMove: handlePointerMove,
246
+ onPointerUp: handlePointerUp,
247
+ };
248
+
249
+ // --- 7. Check if physics is needed ---
250
+ const physics = gameObject.components?.physics;
251
+ const hasPhysics = physics && !editMode;
252
+
253
+ // --- 8. Final structure: RigidBody outside with position/rotation, scale inside ---
254
+ if (hasPhysics) {
255
+ const physicsDef = getComponent('Physics');
256
+ if (physicsDef?.View) {
257
+ return (
258
+ <physicsDef.View
259
+ properties={physics.properties}
260
+ ref={(obj: Object3D | null) => registerRef(gameObject.id, obj)}
261
+ {...wrapperProps}
262
+ >
263
+ <group scale={transformProps.scale}>
264
+ {core}
265
+ {children}
266
+ </group>
267
+ </physicsDef.View>
268
+ );
269
+ }
270
+ }
271
+
272
+ // --- 9. No physics - standard group wrapper ---
244
273
  return (
245
274
  <group
246
275
  ref={(el) => registerRef(gameObject.id, el)}
247
- position={transformProps.position}
248
- rotation={transformProps.rotation}
249
276
  scale={transformProps.scale}
250
- onPointerDown={handlePointerDown}
251
- onPointerMove={handlePointerMove}
252
- onPointerUp={handlePointerUp}
277
+ {...wrapperProps}
253
278
  >
254
- {physicsWrapped}
279
+ {core}
255
280
  {children}
256
281
  </group>
257
282
  );
@@ -364,22 +389,6 @@ function renderCoreNode(gameObject: GameObjectType, ctx: any, parentMatrix: Matr
364
389
  }, coreContent);
365
390
  }
366
391
 
367
- // Helper: wrap core content with physics component when necessary
368
- function wrapPhysicsIfNeeded(gameObject: GameObjectType, content: React.ReactNode, ctx: any) {
369
- const physics = gameObject.components?.physics;
370
- if (!physics) return content;
371
- const physicsDef = getComponent('Physics');
372
- if (!physicsDef || !physicsDef.View) return content;
373
- return (
374
- <physicsDef.View
375
- properties={{ ...physics.properties, id: gameObject.id }}
376
- editMode={ctx.editMode}
377
- >
378
- {content}
379
- </physicsDef.View>
380
- );
381
- }
382
-
383
392
 
384
393
 
385
394
 
@@ -1,29 +1,42 @@
1
+ import { RigidBody, RigidBodyProps } from "@react-three/rapier";
1
2
  import { Component } from "./ComponentRegistry";
2
3
 
3
- function PhysicsComponentEditor({ component, onUpdate }: { component: any; onUpdate: (newComp: any) => void }) {
4
- return <div>
5
- <label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5">Type</label>
6
- <select
7
- className="w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50"
8
- value={component.properties.type}
9
- onChange={e => onUpdate({ type: e.target.value })}
10
- >
11
- <option value="dynamic">Dynamic</option>
12
- <option value="fixed">Fixed</option>
13
- </select>
14
- </div>;
4
+ const selectClass = "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50";
5
+ const labelClass = "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5";
6
+
7
+ function PhysicsComponentEditor({ component, onUpdate }: { component: any; onUpdate: (props: any) => void }) {
8
+ const { type, collider = 'hull' } = component.properties;
9
+ return (
10
+ <div>
11
+ <label className={labelClass}>Type</label>
12
+ <select className={selectClass} value={type} onChange={e => onUpdate({ type: e.target.value })}>
13
+ <option value="dynamic">Dynamic</option>
14
+ <option value="fixed">Fixed</option>
15
+ </select>
16
+
17
+ <label className={`${labelClass} mt-2`}>Collider</label>
18
+ <select className={selectClass} value={collider} onChange={e => onUpdate({ collider: e.target.value })}>
19
+ <option value="hull">Hull (convex)</option>
20
+ <option value="trimesh">Trimesh (exact)</option>
21
+ <option value="cuboid">Cuboid (box)</option>
22
+ <option value="ball">Ball (sphere)</option>
23
+ </select>
24
+ </div>
25
+ );
26
+ }
27
+
28
+ interface PhysicsViewProps extends Omit<RigidBodyProps, 'type' | 'colliders'> {
29
+ properties: { type: RigidBodyProps['type']; collider?: string };
30
+ editMode?: boolean;
15
31
  }
16
32
 
33
+ function PhysicsComponentView({ properties, editMode, children, ...rigidBodyProps }: PhysicsViewProps) {
34
+ if (editMode) return <>{children}</>;
17
35
 
18
- import { RigidBody } from "@react-three/rapier";
36
+ const colliders = properties.collider || (properties.type === 'fixed' ? 'trimesh' : 'hull');
19
37
 
20
- function PhysicsComponentView({ properties, children, editMode }: any) {
21
- if (editMode) return children;
22
38
  return (
23
- <RigidBody
24
- type={properties.type}
25
- colliders="cuboid"
26
- >
39
+ <RigidBody type={properties.type} colliders={colliders as any} {...rigidBodyProps}>
27
40
  {children}
28
41
  </RigidBody>
29
42
  );
@@ -33,9 +46,7 @@ const PhysicsComponent: Component = {
33
46
  name: 'Physics',
34
47
  Editor: PhysicsComponentEditor,
35
48
  View: PhysicsComponentView,
36
- defaultProperties: {
37
- type: 'dynamic'
38
- }
49
+ defaultProperties: { type: 'dynamic', collider: 'hull' }
39
50
  };
40
51
 
41
52
  export default PhysicsComponent;