react-three-game 0.0.21 → 0.0.23

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,30 +1,29 @@
1
1
  import React from "react";
2
- import { Object3D, Group } from "three";
2
+ import { RigidBodyProps } from "@react-three/rapier";
3
+ import { Object3D } from "three";
3
4
  export type InstanceData = {
4
5
  id: string;
6
+ meshPath: string;
5
7
  position: [number, number, number];
6
8
  rotation: [number, number, number];
7
9
  scale: [number, number, number];
8
- meshPath: string;
9
10
  physics?: {
10
- type: 'dynamic' | 'fixed';
11
+ type: RigidBodyProps['type'];
11
12
  };
12
13
  };
13
14
  export declare function GameInstanceProvider({ children, models, onSelect, registerRef }: {
14
15
  children: React.ReactNode;
15
- models: {
16
- [filename: string]: Object3D;
17
- };
16
+ models: Record<string, Object3D>;
18
17
  onSelect?: (id: string | null) => void;
19
18
  registerRef?: (id: string, obj: Object3D | null) => void;
20
19
  }): import("react/jsx-runtime").JSX.Element;
21
- export declare const GameInstance: React.ForwardRefExoticComponent<{
20
+ export declare function GameInstance({ id, modelUrl, position, rotation, scale, physics }: {
22
21
  id: string;
23
22
  modelUrl: string;
24
23
  position: [number, number, number];
25
24
  rotation: [number, number, number];
26
25
  scale: [number, number, number];
27
26
  physics?: {
28
- type: "dynamic" | "fixed";
27
+ type: RigidBodyProps['type'];
29
28
  };
30
- } & React.RefAttributes<Group<import("three").Object3DEventMap>>>;
29
+ }): null;
@@ -1,164 +1,137 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import React, { createContext, useContext, useMemo, useRef, useState, useEffect, useCallback } from "react";
2
+ import { createContext, useContext, useMemo, useRef, useState, useEffect, useCallback } from "react";
3
3
  import { Merged } from '@react-three/drei';
4
4
  import { InstancedRigidBodies } from "@react-three/rapier";
5
- import { Mesh, Matrix4 } from "three";
6
- // Helper functions for comparison
7
- function arrayEquals(a, b) {
8
- if (a === b)
9
- return true;
10
- if (a.length !== b.length)
11
- return false;
12
- for (let i = 0; i < a.length; i++) {
13
- if (a[i] !== b[i])
14
- return false;
15
- }
16
- return true;
17
- }
18
- function instanceEquals(a, b) {
5
+ import { useFrame } from "@react-three/fiber";
6
+ import { Mesh, Matrix4, Euler, Quaternion, Vector3 } from "three";
7
+ // --- Helpers ---
8
+ const arraysEqual = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
9
+ const instancesEqual = (a, b) => {
19
10
  var _a, _b;
20
11
  return a.id === b.id &&
21
12
  a.meshPath === b.meshPath &&
22
- arrayEquals(a.position, b.position) &&
23
- arrayEquals(a.rotation, b.rotation) &&
24
- arrayEquals(a.scale, b.scale) &&
25
- ((_a = a.physics) === null || _a === void 0 ? void 0 : _a.type) === ((_b = b.physics) === null || _b === void 0 ? void 0 : _b.type);
13
+ ((_a = a.physics) === null || _a === void 0 ? void 0 : _a.type) === ((_b = b.physics) === null || _b === void 0 ? void 0 : _b.type) &&
14
+ arraysEqual(a.position, b.position) &&
15
+ arraysEqual(a.rotation, b.rotation) &&
16
+ arraysEqual(a.scale, b.scale);
17
+ };
18
+ // Reusable objects for matrix computation (avoid allocations in hot paths)
19
+ const _matrix = new Matrix4();
20
+ const _position = new Vector3();
21
+ const _quaternion = new Quaternion();
22
+ const _euler = new Euler();
23
+ const _scale = new Vector3();
24
+ function composeMatrix(position, rotation, scale, target = _matrix) {
25
+ _position.set(...position);
26
+ _quaternion.setFromEuler(_euler.set(...rotation));
27
+ _scale.set(...scale);
28
+ return target.compose(_position, _quaternion, _scale);
26
29
  }
30
+ // --- Context ---
27
31
  const GameInstanceContext = createContext(null);
32
+ // --- Provider ---
28
33
  export function GameInstanceProvider({ children, models, onSelect, registerRef }) {
29
34
  const [instances, setInstances] = useState([]);
30
35
  const addInstance = useCallback((instance) => {
31
36
  setInstances(prev => {
32
37
  const idx = prev.findIndex(i => i.id === instance.id);
33
- if (idx !== -1) {
34
- // Update existing if changed
35
- if (instanceEquals(prev[idx], instance)) {
36
- return prev;
37
- }
38
- const copy = [...prev];
39
- copy[idx] = instance;
40
- return copy;
41
- }
42
- // Add new
43
- return [...prev, instance];
38
+ if (idx === -1)
39
+ return [...prev, instance];
40
+ if (instancesEqual(prev[idx], instance))
41
+ return prev;
42
+ const updated = [...prev];
43
+ updated[idx] = instance;
44
+ return updated;
44
45
  });
45
46
  }, []);
46
47
  const removeInstance = useCallback((id) => {
47
- setInstances(prev => {
48
- if (!prev.find(i => i.id === id))
49
- return prev;
50
- return prev.filter(i => i.id !== id);
51
- });
48
+ setInstances(prev => prev.filter(i => i.id !== id));
52
49
  }, []);
53
- // Flatten all model meshes once (models flat mesh parts)
54
- const { flatMeshes, modelParts } = useMemo(() => {
55
- const flatMeshes = {};
56
- const modelParts = {};
50
+ // Extract mesh parts from models with baked local transforms
51
+ const { meshParts, partCounts } = useMemo(() => {
52
+ const meshParts = {};
53
+ const partCounts = {};
57
54
  Object.entries(models).forEach(([modelKey, model]) => {
58
- const root = model;
59
- root.updateWorldMatrix(false, true);
60
- const rootInverse = new Matrix4().copy(root.matrixWorld).invert();
55
+ model.updateWorldMatrix(false, true);
56
+ const rootInverse = new Matrix4().copy(model.matrixWorld).invert();
61
57
  let partIndex = 0;
62
- root.traverse((obj) => {
63
- if (obj.isMesh) {
64
- const geom = obj.geometry.clone();
65
- const relativeTransform = obj.matrixWorld.clone().premultiply(rootInverse);
66
- geom.applyMatrix4(relativeTransform);
67
- const partKey = `${modelKey}__${partIndex}`;
68
- flatMeshes[partKey] = new Mesh(geom, obj.material);
58
+ model.traverse((child) => {
59
+ if (child.isMesh) {
60
+ const mesh = child;
61
+ const geometry = mesh.geometry.clone();
62
+ geometry.applyMatrix4(mesh.matrixWorld.clone().premultiply(rootInverse));
63
+ meshParts[`${modelKey}__${partIndex}`] = new Mesh(geometry, mesh.material);
69
64
  partIndex++;
70
65
  }
71
66
  });
72
- modelParts[modelKey] = partIndex;
67
+ partCounts[modelKey] = partIndex;
73
68
  });
74
- return { flatMeshes, modelParts };
69
+ return { meshParts, partCounts };
75
70
  }, [models]);
76
- // Group instances by meshPath + physics type for batch rendering
71
+ // Cleanup cloned geometries
72
+ useEffect(() => () => {
73
+ Object.values(meshParts).forEach(mesh => mesh.geometry.dispose());
74
+ }, [meshParts]);
75
+ // Group instances by model + physics type
77
76
  const grouped = useMemo(() => {
78
- var _a;
79
77
  const groups = {};
80
- for (const inst of instances) {
81
- const type = ((_a = inst.physics) === null || _a === void 0 ? void 0 : _a.type) || 'none';
82
- const key = `${inst.meshPath}__${type}`;
83
- if (!groups[key])
84
- groups[key] = { physicsType: type, instances: [] };
78
+ instances.forEach(inst => {
79
+ var _a, _b, _c;
80
+ const physicsType = (_b = (_a = inst.physics) === null || _a === void 0 ? void 0 : _a.type) !== null && _b !== void 0 ? _b : 'none';
81
+ const key = `${inst.meshPath}__${physicsType}`;
82
+ (_c = groups[key]) !== null && _c !== void 0 ? _c : (groups[key] = { physicsType, instances: [] });
85
83
  groups[key].instances.push(inst);
86
- }
84
+ });
87
85
  return groups;
88
86
  }, [instances]);
89
- return (_jsxs(GameInstanceContext.Provider, { value: {
90
- addInstance,
91
- removeInstance,
92
- instances,
93
- meshes: flatMeshes,
94
- modelParts
95
- }, children: [children, Object.entries(grouped).map(([key, group]) => {
96
- if (group.physicsType === 'none')
97
- return null;
87
+ const contextValue = useMemo(() => ({ addInstance, removeInstance }), [addInstance, removeInstance]);
88
+ return (_jsxs(GameInstanceContext.Provider, { value: contextValue, children: [children, Object.entries(grouped).map(([key, group]) => {
89
+ var _a;
98
90
  const modelKey = group.instances[0].meshPath;
99
- const partCount = modelParts[modelKey] || 0;
91
+ const partCount = (_a = partCounts[modelKey]) !== null && _a !== void 0 ? _a : 0;
100
92
  if (partCount === 0)
101
93
  return null;
102
- return (_jsx(InstancedRigidGroup, { group: group, modelKey: modelKey, partCount: partCount, flatMeshes: flatMeshes }, key));
103
- }), Object.entries(grouped).map(([key, group]) => {
104
- if (group.physicsType !== 'none')
105
- return null;
106
- const modelKey = group.instances[0].meshPath;
107
- const partCount = modelParts[modelKey] || 0;
108
- if (partCount === 0)
109
- return null;
110
- // Create mesh subset for this specific model
111
- const meshesForModel = {};
112
- for (let i = 0; i < partCount; i++) {
113
- const partKey = `${modelKey}__${i}`;
114
- meshesForModel[partKey] = flatMeshes[partKey];
94
+ if (group.physicsType !== 'none') {
95
+ return (_jsx(PhysicsInstances, { instances: group.instances, physicsType: group.physicsType, modelKey: modelKey, partCount: partCount, meshParts: meshParts }, key));
115
96
  }
116
- return (_jsx(Merged, { meshes: meshesForModel, castShadow: true, receiveShadow: true, children: (instancesMap) => (_jsx(NonPhysicsInstancedGroup, { modelKey: modelKey, group: group, partCount: partCount, instancesMap: instancesMap, onSelect: onSelect, registerRef: registerRef })) }, key));
97
+ const modelMeshes = Object.fromEntries(Array.from({ length: partCount }, (_, i) => [`${modelKey}__${i}`, meshParts[`${modelKey}__${i}`]]));
98
+ return (_jsx(Merged, { meshes: modelMeshes, castShadow: true, receiveShadow: true, children: (Components) => (_jsx(StaticInstances, { instances: group.instances, modelKey: modelKey, partCount: partCount, Components: Components, onSelect: onSelect, registerRef: registerRef })) }, key));
117
99
  })] }));
118
100
  }
119
- // Render physics-enabled instances using InstancedRigidBodies
120
- function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes }) {
121
- const instances = useMemo(() => group.instances.map(inst => ({
122
- key: inst.id,
123
- position: inst.position,
124
- rotation: inst.rotation,
125
- scale: inst.scale,
126
- })), [group.instances]);
127
- return (_jsx(InstancedRigidBodies, { instances: instances, colliders: group.physicsType === 'fixed' ? 'trimesh' : 'hull', type: group.physicsType, children: Array.from({ length: partCount }).map((_, i) => {
128
- const mesh = flatMeshes[`${modelKey}__${i}`];
129
- return (_jsx("instancedMesh", { args: [mesh.geometry, mesh.material, group.instances.length], castShadow: true, receiveShadow: true, frustumCulled: false }, i));
101
+ // --- Physics Instances ---
102
+ function PhysicsInstances({ instances, physicsType, modelKey, partCount, meshParts }) {
103
+ const meshRefs = useRef([]);
104
+ const rigidBodyInstances = useMemo(() => instances.map(({ id, position, rotation, scale }) => ({ key: id, position, rotation, scale })), [instances]);
105
+ // Sync visual matrices each frame (physics updates position/rotation, we need to apply scale)
106
+ useFrame(() => {
107
+ meshRefs.current.forEach(mesh => {
108
+ if (!mesh)
109
+ return;
110
+ instances.forEach((inst, i) => {
111
+ mesh.setMatrixAt(i, composeMatrix(inst.position, inst.rotation, inst.scale));
112
+ });
113
+ mesh.instanceMatrix.needsUpdate = true;
114
+ });
115
+ });
116
+ return (_jsx(InstancedRigidBodies, { instances: rigidBodyInstances, type: physicsType, colliders: physicsType === 'fixed' ? 'trimesh' : 'hull', children: Array.from({ length: partCount }, (_, i) => {
117
+ const mesh = meshParts[`${modelKey}__${i}`];
118
+ return mesh ? (_jsx("instancedMesh", { ref: el => { meshRefs.current[i] = el; }, args: [mesh.geometry, mesh.material, instances.length], frustumCulled: false, castShadow: true, receiveShadow: true }, i)) : null;
130
119
  }) }));
131
120
  }
132
- // Render non-physics instances using Merged's per-instance groups
133
- function NonPhysicsInstancedGroup({ modelKey, group, partCount, instancesMap, onSelect, registerRef }) {
134
- const clickValid = useRef(false);
135
- const handlePointerDown = (e) => {
136
- e.stopPropagation();
137
- clickValid.current = true;
138
- };
139
- const handlePointerMove = () => {
140
- if (clickValid.current)
141
- clickValid.current = false;
142
- };
143
- const handlePointerUp = (e, id) => {
144
- if (clickValid.current) {
145
- e.stopPropagation();
146
- onSelect === null || onSelect === void 0 ? void 0 : onSelect(id);
147
- }
148
- clickValid.current = false;
149
- };
150
- return (_jsx(_Fragment, { children: group.instances.map(inst => (_jsx("group", { ref: (el) => { registerRef === null || registerRef === void 0 ? void 0 : registerRef(inst.id, el); }, position: inst.position, rotation: inst.rotation, scale: inst.scale, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: (e) => handlePointerUp(e, inst.id), children: Array.from({ length: partCount }).map((_, i) => {
151
- const Instance = instancesMap[`${modelKey}__${i}`];
152
- if (!Instance)
153
- return null;
154
- return _jsx(Instance, {}, i);
155
- }) }, inst.id))) }));
121
+ // --- Static Instances (non-physics) ---
122
+ function StaticInstances({ instances, modelKey, partCount, Components, onSelect, registerRef }) {
123
+ const Parts = useMemo(() => Array.from({ length: partCount }, (_, i) => Components[`${modelKey}__${i}`]).filter(Boolean), [Components, modelKey, partCount]);
124
+ return (_jsx(_Fragment, { children: instances.map(inst => (_jsx(InstanceItem, { instance: inst, Parts: Parts, onSelect: onSelect, registerRef: registerRef }, inst.id))) }));
156
125
  }
157
- // GameInstance component: registers an instance for batch rendering (renders nothing itself)
158
- export const GameInstance = React.forwardRef(({ id, modelUrl, position, rotation, scale, physics = undefined, }, ref) => {
126
+ // --- Single Instance ---
127
+ function InstanceItem({ instance, Parts, onSelect, registerRef }) {
128
+ const moved = useRef(false);
129
+ return (_jsx("group", { ref: el => registerRef === null || registerRef === void 0 ? void 0 : registerRef(instance.id, el), position: instance.position, rotation: instance.rotation, scale: instance.scale, onPointerDown: e => { e.stopPropagation(); moved.current = false; }, onPointerMove: () => { moved.current = true; }, onPointerUp: e => { e.stopPropagation(); if (!moved.current)
130
+ onSelect === null || onSelect === void 0 ? void 0 : onSelect(instance.id); }, children: Parts.map((Part, i) => _jsx(Part, {}, i)) }));
131
+ }
132
+ // --- GameInstance (declarative registration) ---
133
+ export function GameInstance({ id, modelUrl, position, rotation, scale, physics }) {
159
134
  const ctx = useContext(GameInstanceContext);
160
- const addInstance = ctx === null || ctx === void 0 ? void 0 : ctx.addInstance;
161
- const removeInstance = ctx === null || ctx === void 0 ? void 0 : ctx.removeInstance;
162
135
  const instance = useMemo(() => ({
163
136
  id,
164
137
  meshPath: modelUrl,
@@ -168,13 +141,10 @@ export const GameInstance = React.forwardRef(({ id, modelUrl, position, rotation
168
141
  physics,
169
142
  }), [id, modelUrl, position, rotation, scale, physics]);
170
143
  useEffect(() => {
171
- if (!addInstance || !removeInstance)
144
+ if (!ctx)
172
145
  return;
173
- addInstance(instance);
174
- return () => {
175
- removeInstance(instance.id);
176
- };
177
- }, [addInstance, removeInstance, instance]);
178
- // No visual rendering - provider handles all instanced visuals
146
+ ctx.addInstance(instance);
147
+ return () => ctx.removeInstance(id);
148
+ }, [ctx, instance, id]);
179
149
  return null;
180
- });
150
+ }
@@ -146,26 +146,17 @@ function GameObjectRenderer({ gameObject, selectedId, onSelect, registerRef, loa
146
146
  const core = renderCoreNode(gameObject, ctx, parentMatrix);
147
147
  // --- 5. Render children recursively (always relative transforms) ---
148
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)));
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 ---
149
+ // --- 6. Inner content group with full transform ---
150
+ const innerGroup = (_jsxs("group", { ref: (el) => registerRef(gameObject.id, el), position: transformProps.position, rotation: transformProps.rotation, scale: transformProps.scale, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, children: [core, children] }));
151
+ // --- 7. Wrap with physics if needed (RigidBody as outer parent, no transform) ---
158
152
  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) {
153
+ if (physics && !editMode) {
162
154
  const physicsDef = getComponent('Physics');
163
155
  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] }) })));
156
+ return (_jsx(physicsDef.View, { properties: physics.properties, children: innerGroup }));
165
157
  }
166
158
  }
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] })));
159
+ return innerGroup;
169
160
  }
170
161
  // Helper: render an instanced GameInstance (terminal node)
171
162
  function renderInstancedNode(gameObject, worldMatrix, ctx) {
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useRef, useEffect } from "react";
3
- import { useFrame, useThree } from "@react-three/fiber";
4
- import { CameraHelper, Object3D, Vector3 } from "three";
3
+ import { useFrame } from "@react-three/fiber";
4
+ import { Vector3 } from "three";
5
5
  function DirectionalLightComponentEditor({ component, onUpdate }) {
6
6
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
7
7
  const props = {
@@ -32,60 +32,24 @@ function DirectionalLightView({ properties, editMode }) {
32
32
  const shadowCameraLeft = (_j = properties.shadowCameraLeft) !== null && _j !== void 0 ? _j : -30;
33
33
  const shadowCameraRight = (_k = properties.shadowCameraRight) !== null && _k !== void 0 ? _k : 30;
34
34
  const targetOffset = (_l = properties.targetOffset) !== null && _l !== void 0 ? _l : [0, -5, 0];
35
- const { scene } = useThree();
36
35
  const directionalLightRef = useRef(null);
37
- const targetRef = useRef(new Object3D());
38
- const cameraHelperRef = useRef(null);
39
- // Add target to scene once
36
+ const targetRef = useRef(null);
37
+ // Set up light target reference when both refs are ready
40
38
  useEffect(() => {
41
- const target = targetRef.current;
42
- scene.add(target);
43
- return () => {
44
- scene.remove(target);
45
- };
46
- }, [scene]);
47
- // Set up light target reference once
48
- useEffect(() => {
49
- if (directionalLightRef.current) {
39
+ if (directionalLightRef.current && targetRef.current) {
50
40
  directionalLightRef.current.target = targetRef.current;
51
41
  }
52
42
  }, []);
53
- // Update target position and mark shadow for update when light moves or offset changes
43
+ // Update target world position based on light position + offset
54
44
  useFrame(() => {
55
- if (!directionalLightRef.current)
45
+ if (!directionalLightRef.current || !targetRef.current)
56
46
  return;
57
47
  const lightWorldPos = new Vector3();
58
48
  directionalLightRef.current.getWorldPosition(lightWorldPos);
59
- const newTargetPos = new Vector3(lightWorldPos.x + targetOffset[0], lightWorldPos.y + targetOffset[1], lightWorldPos.z + targetOffset[2]);
60
- // Only update if position actually changed
61
- if (!targetRef.current.position.equals(newTargetPos)) {
62
- targetRef.current.position.copy(newTargetPos);
63
- if (directionalLightRef.current.shadow) {
64
- directionalLightRef.current.shadow.needsUpdate = true;
65
- }
66
- }
67
- // Update camera helper in edit mode
68
- if (editMode && cameraHelperRef.current) {
69
- cameraHelperRef.current.update();
70
- }
49
+ // Target is positioned relative to light's world position
50
+ targetRef.current.position.set(lightWorldPos.x + targetOffset[0], lightWorldPos.y + targetOffset[1], lightWorldPos.z + targetOffset[2]);
71
51
  });
72
- // Create/destroy camera helper for edit mode
73
- useEffect(() => {
74
- var _a;
75
- if (editMode && ((_a = directionalLightRef.current) === null || _a === void 0 ? void 0 : _a.shadow.camera)) {
76
- const helper = new CameraHelper(directionalLightRef.current.shadow.camera);
77
- cameraHelperRef.current = helper;
78
- scene.add(helper);
79
- return () => {
80
- if (cameraHelperRef.current) {
81
- scene.remove(cameraHelperRef.current);
82
- cameraHelperRef.current.dispose();
83
- cameraHelperRef.current = null;
84
- }
85
- };
86
- }
87
- }, [editMode, scene]);
88
- return (_jsxs(_Fragment, { children: [_jsx("directionalLight", { ref: directionalLightRef, color: color, intensity: intensity, castShadow: castShadow, "shadow-mapSize": [shadowMapSize, shadowMapSize], "shadow-bias": -0.001, "shadow-normalBias": 0.02, children: _jsx("orthographicCamera", { attach: "shadow-camera", near: shadowCameraNear, far: shadowCameraFar, top: shadowCameraTop, bottom: shadowCameraBottom, left: shadowCameraLeft, right: shadowCameraRight }) }), editMode && (_jsxs(_Fragment, { children: [_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [0.3, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true })] }), _jsxs("mesh", { position: targetOffset, children: [_jsx("sphereGeometry", { args: [0.2, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, opacity: 0.5, transparent: true })] }), _jsxs("line", { children: [_jsx("bufferGeometry", { onUpdate: (geo) => {
52
+ return (_jsxs(_Fragment, { children: [_jsx("directionalLight", { ref: directionalLightRef, color: color, intensity: intensity, castShadow: castShadow, "shadow-mapSize-width": shadowMapSize, "shadow-mapSize-height": shadowMapSize, "shadow-camera-near": shadowCameraNear, "shadow-camera-far": shadowCameraFar, "shadow-camera-top": shadowCameraTop, "shadow-camera-bottom": shadowCameraBottom, "shadow-camera-left": shadowCameraLeft, "shadow-camera-right": shadowCameraRight, "shadow-bias": -0.001, "shadow-normalBias": 0.02 }), _jsx("object3D", { ref: targetRef }), editMode && (_jsxs(_Fragment, { children: [_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [0.3, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true })] }), _jsxs("mesh", { position: targetOffset, children: [_jsx("sphereGeometry", { args: [0.2, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, opacity: 0.5, transparent: true })] }), _jsxs("line", { children: [_jsx("bufferGeometry", { onUpdate: (geo) => {
89
53
  const points = [
90
54
  new Vector3(0, 0, 0),
91
55
  new Vector3(targetOffset[0], targetOffset[1], targetOffset[2])
@@ -53,7 +53,7 @@ function MaterialComponentView({ properties, loadedTextures, isSelected }) {
53
53
  }
54
54
  const { color, wireframe = false } = properties;
55
55
  const displayColor = isSelected ? "cyan" : color;
56
- return _jsx("meshStandardMaterial", { color: displayColor, 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');
56
+ return (_jsx("meshStandardMaterial", { color: displayColor, 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'));
57
57
  }
58
58
  const MaterialComponent = {
59
59
  name: 'Material',
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { ModelListViewer } from '../../assetviewer/page';
3
- import { useEffect, useState } from 'react';
3
+ import { useEffect, useState, useMemo } from 'react';
4
4
  function ModelComponentEditor({ component, onUpdate, basePath = "" }) {
5
5
  const [modelFiles, setModelFiles] = useState([]);
6
6
  useEffect(() => {
@@ -22,19 +22,23 @@ function ModelComponentView({ properties, loadedModels, children }) {
22
22
  // Instanced models are handled elsewhere (GameInstance), so only render non-instanced here
23
23
  if (!properties.filename || properties.instanced)
24
24
  return _jsx(_Fragment, { children: children });
25
- if (loadedModels && loadedModels[properties.filename]) {
26
- const clonedModel = loadedModels[properties.filename].clone();
27
- // Enable shadows on all meshes in the model
28
- clonedModel.traverse((obj) => {
25
+ const sourceModel = loadedModels === null || loadedModels === void 0 ? void 0 : loadedModels[properties.filename];
26
+ // Clone model once and set up shadows - memoized to avoid cloning on every render
27
+ const clonedModel = useMemo(() => {
28
+ if (!sourceModel)
29
+ return null;
30
+ const clone = sourceModel.clone();
31
+ clone.traverse((obj) => {
29
32
  if (obj.isMesh) {
30
33
  obj.castShadow = true;
31
34
  obj.receiveShadow = true;
32
35
  }
33
36
  });
34
- return _jsx("primitive", { object: clonedModel, children: children });
35
- }
36
- // Model not loaded yet - render children only
37
- return _jsx(_Fragment, { children: children });
37
+ return clone;
38
+ }, [sourceModel]);
39
+ if (!clonedModel)
40
+ return _jsx(_Fragment, { children: children });
41
+ return _jsx("primitive", { object: clonedModel, children: children });
38
42
  }
39
43
  const ModelComponent = {
40
44
  name: 'Model',
@@ -1,14 +1,3 @@
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
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
13
2
  import { RigidBody } from "@react-three/rapier";
14
3
  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";
@@ -17,12 +6,11 @@ function PhysicsComponentEditor({ component, onUpdate }) {
17
6
  const { type, collider = 'hull' } = component.properties;
18
7
  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)" })] })] }));
19
8
  }
20
- function PhysicsComponentView(_a) {
21
- var { properties, editMode, children } = _a, rigidBodyProps = __rest(_a, ["properties", "editMode", "children"]);
9
+ function PhysicsComponentView({ properties, editMode, children }) {
22
10
  if (editMode)
23
11
  return _jsx(_Fragment, { children: children });
24
12
  const colliders = properties.collider || (properties.type === 'fixed' ? 'trimesh' : 'hull');
25
- return (_jsx(RigidBody, Object.assign({ type: properties.type, colliders: colliders }, rigidBodyProps, { children: children })));
13
+ return (_jsx(RigidBody, { type: properties.type, colliders: colliders, children: children }));
26
14
  }
27
15
  const PhysicsComponent = {
28
16
  name: 'Physics',
@@ -27,7 +27,7 @@ function SpotLightView({ properties, editMode }) {
27
27
  spotLightRef.current.target = targetRef.current;
28
28
  }
29
29
  }, []);
30
- return (_jsxs(_Fragment, { children: [_jsx("spotLight", { ref: spotLightRef, color: color, intensity: intensity, angle: angle, penumbra: penumbra, distance: distance, castShadow: castShadow, "shadow-bias": -0.0001, "shadow-normalBias": 0.02 }), _jsx("object3D", { ref: targetRef, position: [0, -5, 0] }), editMode && (_jsxs(_Fragment, { children: [_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [0.2, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true })] }), _jsxs("mesh", { position: [0, -5, 0], children: [_jsx("sphereGeometry", { args: [0.15, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, opacity: 0.5, transparent: true })] })] }))] }));
30
+ return (_jsxs(_Fragment, { children: [_jsx("spotLight", { ref: spotLightRef, color: color, intensity: intensity, angle: angle, penumbra: penumbra, distance: distance, castShadow: castShadow, "shadow-mapSize-width": 1024, "shadow-mapSize-height": 1024, "shadow-bias": -0.0001, "shadow-normalBias": 0.02 }), _jsx("object3D", { ref: targetRef, position: [0, -5, 0] }), editMode && (_jsxs(_Fragment, { children: [_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [0.2, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true })] }), _jsxs("mesh", { position: [0, -5, 0], children: [_jsx("sphereGeometry", { args: [0.15, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, opacity: 0.5, transparent: true })] })] }))] }));
31
31
  }
32
32
  const SpotLightComponent = {
33
33
  name: 'SpotLight',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "description": "Batteries included React Three Fiber game engine",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",