react-three-game 0.0.18 → 0.0.19

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.
@@ -14,28 +14,24 @@ import { useState, useRef, useEffect } from "react";
14
14
  import PrefabRoot from "./PrefabRoot";
15
15
  import { Physics } from "@react-three/rapier";
16
16
  import EditorUI from "./EditorUI";
17
+ import { base, toolbar } from "./styles";
17
18
  const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }) => {
18
19
  const [editMode, setEditMode] = useState(true);
19
20
  const [loadedPrefab, setLoadedPrefab] = useState(initialPrefab !== null && initialPrefab !== void 0 ? initialPrefab : {
20
- "id": "prefab-default",
21
- "name": "New Prefab",
22
- "root": {
23
- "id": "root",
24
- "components": {
25
- "transform": {
26
- "type": "Transform",
27
- "properties": {
28
- "position": [0, 0, 0],
29
- "rotation": [0, 0, 0],
30
- "scale": [1, 1, 1]
31
- }
21
+ id: "prefab-default",
22
+ name: "New Prefab",
23
+ root: {
24
+ id: "root",
25
+ components: {
26
+ transform: {
27
+ type: "Transform",
28
+ properties: { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }
32
29
  }
33
30
  }
34
31
  }
35
32
  });
36
33
  const [selectedId, setSelectedId] = useState(null);
37
34
  const [transformMode, setTransformMode] = useState("translate");
38
- const prefabRef = useRef(null);
39
35
  // Sync internal state with external initialPrefab prop
40
36
  useEffect(() => {
41
37
  if (initialPrefab) {
@@ -48,136 +44,74 @@ const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }) =>
48
44
  const resolved = typeof newPrefab === 'function' ? newPrefab(loadedPrefab) : newPrefab;
49
45
  onPrefabChange === null || onPrefabChange === void 0 ? void 0 : onPrefabChange(resolved);
50
46
  };
51
- return _jsxs(_Fragment, { children: [_jsx(GameCanvas, { children: _jsxs(Physics, { paused: editMode, children: [_jsx("ambientLight", { intensity: 1.5 }), _jsx("gridHelper", { args: [10, 10], position: [0, -1, 0] }), _jsx(PrefabRoot, { data: loadedPrefab, ref: prefabRef,
52
- // props for edit mode
53
- editMode: editMode, onPrefabChange: updatePrefab, selectedId: selectedId, onSelect: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }), children] }) }), _jsx(SaveDataPanel, { currentData: loadedPrefab, onDataChange: updatePrefab, editMode: editMode, onEditModeChange: setEditMode }), editMode && _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: updatePrefab, selectedId: selectedId, setSelectedId: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath })] });
47
+ return _jsxs(_Fragment, { children: [_jsx(GameCanvas, { children: _jsxs(Physics, { paused: editMode, children: [_jsx("ambientLight", { intensity: 1.5 }), _jsx("gridHelper", { args: [10, 10], position: [0, -1, 0] }), _jsx(PrefabRoot, { data: loadedPrefab, editMode: editMode, onPrefabChange: updatePrefab, selectedId: selectedId, onSelect: setSelectedId, transformMode: transformMode, basePath: basePath }), children] }) }), _jsx(SaveDataPanel, { currentData: loadedPrefab, onDataChange: updatePrefab, editMode: editMode, onEditModeChange: setEditMode }), editMode && _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: updatePrefab, selectedId: selectedId, setSelectedId: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath })] });
54
48
  };
55
49
  const SaveDataPanel = ({ currentData, onDataChange, editMode, onEditModeChange }) => {
56
50
  const [history, setHistory] = useState([currentData]);
57
51
  const [historyIndex, setHistoryIndex] = useState(0);
58
- const throttleTimeoutRef = useRef(null);
59
- const lastSavedDataRef = useRef(JSON.stringify(currentData));
60
- // Define undo/redo handlers
61
- const handleUndo = () => {
52
+ const throttleRef = useRef(null);
53
+ const lastDataRef = useRef(JSON.stringify(currentData));
54
+ const undo = () => {
62
55
  if (historyIndex > 0) {
63
56
  const newIndex = historyIndex - 1;
64
57
  setHistoryIndex(newIndex);
65
- lastSavedDataRef.current = JSON.stringify(history[newIndex]);
58
+ lastDataRef.current = JSON.stringify(history[newIndex]);
66
59
  onDataChange(history[newIndex]);
67
60
  }
68
61
  };
69
- const handleRedo = () => {
62
+ const redo = () => {
70
63
  if (historyIndex < history.length - 1) {
71
64
  const newIndex = historyIndex + 1;
72
65
  setHistoryIndex(newIndex);
73
- lastSavedDataRef.current = JSON.stringify(history[newIndex]);
66
+ lastDataRef.current = JSON.stringify(history[newIndex]);
74
67
  onDataChange(history[newIndex]);
75
68
  }
76
69
  };
77
- // Keyboard shortcuts for undo/redo
78
70
  useEffect(() => {
79
71
  const handleKeyDown = (e) => {
80
- // Undo: Ctrl+Z (Cmd+Z on Mac)
81
72
  if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
82
73
  e.preventDefault();
83
- handleUndo();
74
+ undo();
84
75
  }
85
- // Redo: Ctrl+Shift+Z or Ctrl+Y (Cmd+Shift+Z or Cmd+Y on Mac)
86
76
  else if ((e.ctrlKey || e.metaKey) && (e.shiftKey && e.key === 'z' || e.key === 'y')) {
87
77
  e.preventDefault();
88
- handleRedo();
78
+ redo();
89
79
  }
90
80
  };
91
81
  window.addEventListener('keydown', handleKeyDown);
92
82
  return () => window.removeEventListener('keydown', handleKeyDown);
93
83
  }, [historyIndex, history]);
94
- // Throttled history update when currentData changes
95
84
  useEffect(() => {
96
- const currentDataStr = JSON.stringify(currentData);
97
- // Skip if data hasn't actually changed
98
- if (currentDataStr === lastSavedDataRef.current) {
85
+ const currentStr = JSON.stringify(currentData);
86
+ if (currentStr === lastDataRef.current)
99
87
  return;
100
- }
101
- // Clear existing throttle timeout
102
- if (throttleTimeoutRef.current) {
103
- clearTimeout(throttleTimeoutRef.current);
104
- }
105
- // Set new throttled update
106
- throttleTimeoutRef.current = setTimeout(() => {
107
- lastSavedDataRef.current = currentDataStr;
88
+ if (throttleRef.current)
89
+ clearTimeout(throttleRef.current);
90
+ throttleRef.current = setTimeout(() => {
91
+ lastDataRef.current = currentStr;
108
92
  setHistory(prev => {
109
- // Slice history at current index (discard future states)
110
- const newHistory = prev.slice(0, historyIndex + 1);
111
- // Add new state
112
- newHistory.push(currentData);
113
- // Limit history size to 50 states
114
- if (newHistory.length > 50) {
115
- newHistory.shift();
116
- return newHistory;
117
- }
118
- return newHistory;
93
+ const newHistory = [...prev.slice(0, historyIndex + 1), currentData];
94
+ return newHistory.length > 50 ? newHistory.slice(1) : newHistory;
119
95
  });
120
- setHistoryIndex(prev => {
121
- const newHistory = history.slice(0, prev + 1);
122
- newHistory.push(currentData);
123
- return Math.min(newHistory.length - 1, 49);
124
- });
125
- }, 500); // 500ms throttle
96
+ setHistoryIndex(prev => Math.min(prev + 1, 49));
97
+ }, 500);
126
98
  return () => {
127
- if (throttleTimeoutRef.current) {
128
- clearTimeout(throttleTimeoutRef.current);
129
- }
99
+ if (throttleRef.current)
100
+ clearTimeout(throttleRef.current);
130
101
  };
131
- }, [currentData, historyIndex, history]);
102
+ }, [currentData]);
132
103
  const handleLoad = () => __awaiter(void 0, void 0, void 0, function* () {
133
104
  const prefab = yield loadJson();
134
105
  if (prefab) {
135
106
  onDataChange(prefab);
136
- // Reset history when loading new file
137
107
  setHistory([prefab]);
138
108
  setHistoryIndex(0);
139
- lastSavedDataRef.current = JSON.stringify(prefab);
109
+ lastDataRef.current = JSON.stringify(prefab);
140
110
  }
141
111
  });
142
112
  const canUndo = historyIndex > 0;
143
113
  const canRedo = historyIndex < history.length - 1;
144
- return _jsxs("div", { style: {
145
- position: "absolute",
146
- top: 8,
147
- left: "50%",
148
- transform: "translateX(-50%)",
149
- display: "flex",
150
- alignItems: "center",
151
- gap: 6,
152
- padding: "2px 4px",
153
- background: "rgba(0,0,0,0.55)",
154
- border: "1px solid rgba(255,255,255,0.12)",
155
- borderRadius: 4,
156
- color: "rgba(255,255,255,0.9)",
157
- fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
158
- fontSize: 11,
159
- lineHeight: 1,
160
- WebkitUserSelect: "none",
161
- userSelect: "none",
162
- }, children: [_jsx(PanelButton, { onClick: () => onEditModeChange(!editMode), children: editMode ? "▶" : "⏸" }), _jsx("span", { style: { opacity: 0.35 }, children: "|" }), _jsx(PanelButton, { onClick: handleUndo, disabled: !canUndo, title: "Undo (Ctrl+Z)", children: "\u21B6" }), _jsx(PanelButton, { onClick: handleRedo, disabled: !canRedo, title: "Redo (Ctrl+Shift+Z)", children: "\u21B7" }), _jsx("span", { style: { opacity: 0.35 }, children: "|" }), _jsx(PanelButton, { onClick: handleLoad, title: "Load JSON", children: "\uD83D\uDCE5" }), _jsx(PanelButton, { onClick: () => saveJson(currentData, "prefab"), title: "Save JSON", children: "\uD83D\uDCBE" })] });
163
- };
164
- const PanelButton = ({ onClick, disabled, title, children }) => {
165
- return _jsx("button", { style: {
166
- padding: "2px 6px",
167
- font: "inherit",
168
- background: "transparent",
169
- color: disabled ? "rgba(255,255,255,0.3)" : "inherit",
170
- border: "1px solid rgba(255,255,255,0.18)",
171
- borderRadius: 3,
172
- cursor: disabled ? "not-allowed" : "pointer",
173
- opacity: disabled ? 0.5 : 1,
174
- }, onClick: onClick, disabled: disabled, title: title, onPointerEnter: (e) => {
175
- if (!disabled) {
176
- e.currentTarget.style.background = "rgba(255,255,255,0.08)";
177
- }
178
- }, onPointerLeave: (e) => {
179
- e.currentTarget.style.background = "transparent";
180
- }, children: children });
114
+ return _jsxs("div", { style: toolbar.panel, children: [_jsx("button", { style: base.btn, onClick: () => onEditModeChange(!editMode), children: editMode ? "▶" : "⏸" }), _jsx("div", { style: toolbar.divider }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), (canUndo ? {} : toolbar.disabled)), onClick: undo, disabled: !canUndo, children: "\u21B6" }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), (canRedo ? {} : toolbar.disabled)), onClick: redo, disabled: !canRedo, children: "\u21B7" }), _jsx("div", { style: toolbar.divider }), _jsx("button", { style: base.btn, onClick: handleLoad, children: "\uD83D\uDCE5" }), _jsx("button", { style: base.btn, onClick: () => saveJson(currentData, "prefab"), children: "\uD83D\uDCBE" })] });
181
115
  };
182
116
  const saveJson = (data, filename) => {
183
117
  const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
@@ -7,7 +7,6 @@ export declare const PrefabRoot: import("react").ForwardRefExoticComponent<{
7
7
  selectedId?: string | null;
8
8
  onSelect?: (id: string | null) => void;
9
9
  transformMode?: "translate" | "rotate" | "scale";
10
- setTransformMode?: (mode: "translate" | "rotate" | "scale") => void;
11
10
  basePath?: string;
12
11
  } & import("react").RefAttributes<Group<import("three").Object3DEventMap>>>;
13
12
  export default PrefabRoot;
@@ -12,42 +12,26 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
12
12
  import { MapControls, TransformControls } from "@react-three/drei";
13
13
  import { useState, useRef, useEffect, forwardRef, useCallback } from "react";
14
14
  import { Vector3, Euler, Quaternion, SRGBColorSpace, TextureLoader, Matrix4 } from "three";
15
- import { getComponent } from "./components/ComponentRegistry";
15
+ import { getComponent, registerComponent } from "./components/ComponentRegistry";
16
16
  import { loadModel } from "../dragdrop/modelLoader";
17
17
  import { GameInstance, GameInstanceProvider } from "./InstanceProvider";
18
- // register all components
19
- import { registerComponent } from './components/ComponentRegistry';
18
+ import { updateNode } from "./utils";
20
19
  import components from './components/';
20
+ // Register all components
21
21
  components.forEach(registerComponent);
22
- function updatePrefabNode(root, id, update) {
23
- if (root.id === id) {
24
- return update(root);
25
- }
26
- if (root.children) {
27
- return Object.assign(Object.assign({}, root), { children: root.children.map(child => updatePrefabNode(child, id, update)) });
28
- }
29
- return root;
30
- }
31
- export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selectedId, onSelect, transformMode, setTransformMode, basePath = "" }, ref) => {
22
+ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selectedId, onSelect, transformMode, basePath = "" }, ref) => {
32
23
  const [loadedModels, setLoadedModels] = useState({});
33
24
  const [loadedTextures, setLoadedTextures] = useState({});
34
- // const [prefabRoot, setPrefabRoot] = useState<Prefab>(data); // Removed local state
35
25
  const loadingRefs = useRef(new Set());
36
26
  const objectRefs = useRef({});
37
27
  const [selectedObject, setSelectedObject] = useState(null);
38
28
  const registerRef = useCallback((id, obj) => {
39
29
  objectRefs.current[id] = obj;
40
- if (id === selectedId) {
30
+ if (id === selectedId)
41
31
  setSelectedObject(obj);
42
- }
43
32
  }, [selectedId]);
44
33
  useEffect(() => {
45
- if (selectedId) {
46
- setSelectedObject(objectRefs.current[selectedId] || null);
47
- }
48
- else {
49
- setSelectedObject(null);
50
- }
34
+ setSelectedObject(selectedId ? objectRefs.current[selectedId] || null : null);
51
35
  }, [selectedId]);
52
36
  const onTransformChange = () => {
53
37
  if (!selectedId || !onPrefabChange)
@@ -68,7 +52,7 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
68
52
  localMatrix.decompose(lp, lq, ls);
69
53
  const le = new Euler().setFromQuaternion(lq);
70
54
  // 4. Write back LOCAL transform into the prefab node
71
- const newRoot = updatePrefabNode(data.root, selectedId, (node) => (Object.assign(Object.assign({}, node), { components: Object.assign(Object.assign({}, node === null || node === void 0 ? void 0 : node.components), { transform: {
55
+ const newRoot = updateNode(data.root, selectedId, (node) => (Object.assign(Object.assign({}, node), { components: Object.assign(Object.assign({}, node.components), { transform: {
72
56
  type: "Transform",
73
57
  properties: {
74
58
  position: [lp.x, lp.y, lp.z],
@@ -35,30 +35,44 @@ function DirectionalLightView({ properties, editMode }) {
35
35
  const { scene } = useThree();
36
36
  const directionalLightRef = useRef(null);
37
37
  const targetRef = useRef(new Object3D());
38
- const lastUpdate = useRef(0);
39
38
  const cameraHelperRef = useRef(null);
40
- const lastPositionRef = useRef(new Vector3());
41
- // Add target to scene
39
+ // Add target to scene once
42
40
  useEffect(() => {
43
- if (targetRef.current) {
44
- scene.add(targetRef.current);
45
- return () => {
46
- scene.remove(targetRef.current);
47
- };
48
- }
41
+ const target = targetRef.current;
42
+ scene.add(target);
43
+ return () => {
44
+ scene.remove(target);
45
+ };
49
46
  }, [scene]);
50
- // Update target position when light position or offset changes
47
+ // Set up light target reference once
51
48
  useEffect(() => {
52
- if (directionalLightRef.current && targetRef.current) {
53
- const lightWorldPos = new Vector3();
54
- directionalLightRef.current.getWorldPosition(lightWorldPos);
55
- targetRef.current.position.set(lightWorldPos.x + targetOffset[0], lightWorldPos.y + targetOffset[1], lightWorldPos.z + targetOffset[2]);
49
+ if (directionalLightRef.current) {
56
50
  directionalLightRef.current.target = targetRef.current;
57
51
  }
52
+ }, []);
53
+ // Update target position and mark shadow for update when light moves or offset changes
54
+ useFrame(() => {
55
+ if (!directionalLightRef.current)
56
+ return;
57
+ const lightWorldPos = new Vector3();
58
+ 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
+ }
58
71
  });
72
+ // Create/destroy camera helper for edit mode
59
73
  useEffect(() => {
60
- // Create camera helper for edit mode and add to scene
61
- if (editMode && directionalLightRef.current && directionalLightRef.current.shadow.camera) {
74
+ var _a;
75
+ if (editMode && ((_a = directionalLightRef.current) === null || _a === void 0 ? void 0 : _a.shadow.camera)) {
62
76
  const helper = new CameraHelper(directionalLightRef.current.shadow.camera);
63
77
  cameraHelperRef.current = helper;
64
78
  scene.add(helper);
@@ -66,37 +80,11 @@ function DirectionalLightView({ properties, editMode }) {
66
80
  if (cameraHelperRef.current) {
67
81
  scene.remove(cameraHelperRef.current);
68
82
  cameraHelperRef.current.dispose();
83
+ cameraHelperRef.current = null;
69
84
  }
70
85
  };
71
86
  }
72
87
  }, [editMode, scene]);
73
- useFrame(({ clock }) => {
74
- if (!directionalLightRef.current || !directionalLightRef.current.shadow)
75
- return;
76
- // Disable auto-update for shadows
77
- if (directionalLightRef.current.shadow.autoUpdate) {
78
- directionalLightRef.current.shadow.autoUpdate = false;
79
- directionalLightRef.current.shadow.needsUpdate = true;
80
- }
81
- // Check if position has changed
82
- const currentPosition = new Vector3();
83
- directionalLightRef.current.getWorldPosition(currentPosition);
84
- const positionChanged = !currentPosition.equals(lastPositionRef.current);
85
- if (positionChanged) {
86
- lastPositionRef.current.copy(currentPosition);
87
- directionalLightRef.current.shadow.needsUpdate = true;
88
- lastUpdate.current = clock.elapsedTime; // Reset timer on position change
89
- }
90
- // Update shadow map infrequently (every 5 seconds) if position hasn't changed
91
- if (!editMode && !positionChanged && clock.elapsedTime - lastUpdate.current > 5) {
92
- lastUpdate.current = clock.elapsedTime;
93
- directionalLightRef.current.shadow.needsUpdate = true;
94
- }
95
- // Update camera helper in edit mode
96
- if (editMode && cameraHelperRef.current) {
97
- cameraHelperRef.current.update();
98
- }
99
- });
100
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) => {
101
89
  const points = [
102
90
  new Vector3(0, 0, 0),