react-three-game 0.0.1 → 0.0.3
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/.gitattributes +2 -0
- package/.github/copilot-instructions.md +207 -0
- package/LICENSE +661 -0
- package/README.md +664 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/shared/GameCanvas.d.ts +6 -0
- package/dist/shared/GameCanvas.js +48 -0
- package/dist/shared/extend-three.d.ts +1 -0
- package/dist/shared/extend-three.js +13 -0
- package/dist/tools/assetviewer/page.d.ts +21 -0
- package/dist/tools/assetviewer/page.js +153 -0
- package/dist/tools/dragdrop/DragDropLoader.d.ts +9 -0
- package/dist/tools/dragdrop/DragDropLoader.js +78 -0
- package/dist/tools/dragdrop/modelLoader.d.ts +7 -0
- package/dist/tools/dragdrop/modelLoader.js +53 -0
- package/dist/tools/dragdrop/page.d.ts +1 -0
- package/dist/tools/dragdrop/page.js +11 -0
- package/dist/tools/prefabeditor/EditorTree.d.ts +10 -0
- package/dist/tools/prefabeditor/EditorTree.js +182 -0
- package/dist/tools/prefabeditor/EditorUI.d.ts +11 -0
- package/dist/tools/prefabeditor/EditorUI.js +96 -0
- package/dist/tools/prefabeditor/EventSystem.d.ts +7 -0
- package/dist/tools/prefabeditor/EventSystem.js +23 -0
- package/dist/tools/prefabeditor/InstanceProvider.d.ts +30 -0
- package/dist/tools/prefabeditor/InstanceProvider.js +172 -0
- package/dist/tools/prefabeditor/PrefabEditor.d.ts +4 -0
- package/dist/tools/prefabeditor/PrefabEditor.js +89 -0
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +12 -0
- package/dist/tools/prefabeditor/PrefabRoot.js +273 -0
- package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +13 -0
- package/dist/tools/prefabeditor/components/ComponentRegistry.js +13 -0
- package/dist/tools/prefabeditor/components/GeometryComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/GeometryComponent.js +28 -0
- package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/MaterialComponent.js +66 -0
- package/dist/tools/prefabeditor/components/ModelComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/ModelComponent.js +39 -0
- package/dist/tools/prefabeditor/components/PhysicsComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/PhysicsComponent.js +19 -0
- package/dist/tools/prefabeditor/components/SpotLightComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +19 -0
- package/dist/tools/prefabeditor/components/TransformComponent.d.ts +8 -0
- package/dist/tools/prefabeditor/components/TransformComponent.js +22 -0
- package/dist/tools/prefabeditor/components/index.d.ts +2 -0
- package/dist/tools/prefabeditor/components/index.js +14 -0
- package/dist/tools/prefabeditor/page.d.ts +1 -0
- package/dist/tools/prefabeditor/page.js +5 -0
- package/dist/tools/prefabeditor/types.d.ts +29 -0
- package/dist/tools/prefabeditor/types.js +1 -0
- package/package.json +16 -4
- package/tsconfig.json +2 -1
- package/dist/GameCanvas.d.ts +0 -6
- package/dist/GameCanvas.js +0 -5
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import EditorTree from './EditorTree';
|
|
4
|
+
import { getAllComponents } from './components/ComponentRegistry';
|
|
5
|
+
function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transformMode, setTransformMode }) {
|
|
6
|
+
const [isInspectorCollapsed, setIsInspectorCollapsed] = useState(false);
|
|
7
|
+
const updateNode = (updater) => {
|
|
8
|
+
if (!prefabData || !setPrefabData || !selectedId)
|
|
9
|
+
return;
|
|
10
|
+
setPrefabData(prev => (Object.assign(Object.assign({}, prev), { root: updatePrefabNode(prev.root, selectedId, updater) })));
|
|
11
|
+
};
|
|
12
|
+
const deleteNode = () => {
|
|
13
|
+
if (!prefabData || !setPrefabData || !selectedId)
|
|
14
|
+
return;
|
|
15
|
+
if (selectedId === prefabData.root.id) {
|
|
16
|
+
alert("Cannot delete root node");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
setPrefabData(prev => {
|
|
20
|
+
const newRoot = deletePrefabNode(prev.root, selectedId);
|
|
21
|
+
return Object.assign(Object.assign({}, prev), { root: newRoot });
|
|
22
|
+
});
|
|
23
|
+
setSelectedId(null);
|
|
24
|
+
};
|
|
25
|
+
const selectedNode = selectedId && prefabData ? findNode(prefabData.root, selectedId) : null;
|
|
26
|
+
// if (!selectedNode) return null;
|
|
27
|
+
return _jsxs(_Fragment, { children: [_jsxs("div", { style: { position: 'absolute', top: "0.5rem", right: "0.5rem", zIndex: 20, backgroundColor: "rgba(0,0,0,0.7)", backdropFilter: "blur(4px)", color: "white", border: "1px solid rgba(0,255,255,0.3)" }, children: [_jsxs("div", { className: "px-1.5 py-1 font-mono text-[10px] bg-cyan-500/10 border-b border-cyan-500/30 sticky top-0 uppercase tracking-wider text-cyan-400/80 cursor-pointer hover:bg-cyan-500/20 flex items-center justify-between", onClick: () => setIsInspectorCollapsed(!isInspectorCollapsed), children: [_jsx("span", { children: "Inspector" }), _jsx("span", { className: "text-[8px]", children: isInspectorCollapsed ? '◀' : '▶' })] }), !isInspectorCollapsed && selectedNode && (_jsx(NodeInspector, { node: selectedNode, updateNode: updateNode, deleteNode: deleteNode, transformMode: transformMode, setTransformMode: setTransformMode }))] }), _jsx("div", { style: { position: 'absolute', top: "0.5rem", left: "0.5rem", zIndex: 20 }, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId }) })] });
|
|
28
|
+
}
|
|
29
|
+
function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransformMode }) {
|
|
30
|
+
const ALL_COMPONENTS = getAllComponents();
|
|
31
|
+
const allComponentKeys = Object.keys(ALL_COMPONENTS);
|
|
32
|
+
const [addComponentType, setAddComponentType] = useState(allComponentKeys[0]);
|
|
33
|
+
const componentKeys = Object.keys(node.components || {}).join(',');
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
// Components stored on nodes use lowercase keys (e.g. 'geometry'),
|
|
36
|
+
// while the registry keys are the component names (e.g. 'Geometry').
|
|
37
|
+
const available = allComponentKeys.filter(k => { var _a; return !((_a = node.components) === null || _a === void 0 ? void 0 : _a[k.toLowerCase()]); });
|
|
38
|
+
if (!available.includes(addComponentType)) {
|
|
39
|
+
setAddComponentType(available[0] || "");
|
|
40
|
+
}
|
|
41
|
+
}, [componentKeys, addComponentType, node.components, allComponentKeys]);
|
|
42
|
+
return _jsxs("div", { className: "flex flex-col gap-1 text-[11px] max-w-[250px] max-h-[80vh] overflow-y-auto", children: [_jsx("div", { className: "border-b border-cyan-500/20 pb-1 px-1.5 pt-1", children: _jsx("input", { className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[11px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: node.id, onChange: e => updateNode(n => (Object.assign(Object.assign({}, n), { id: e.target.value }))) }) }), _jsxs("div", { className: "flex justify-between items-center px-1.5 py-0.5 border-b border-cyan-500/20", children: [_jsx("label", { className: "text-[10px] font-mono text-cyan-400/80 uppercase tracking-wider", children: "Components" }), _jsx("button", { onClick: deleteNode, className: "text-[10px] text-red-400/80 hover:text-red-400", children: "\u2715" })] }), _jsxs("div", { className: "px-1.5 py-1 border-b border-cyan-500/20", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Mode" }), _jsx("div", { className: "flex gap-0.5", children: ["translate", "rotate", "scale"].map(mode => (_jsx("button", { onClick: () => setTransformMode(mode), className: `flex-1 px-1 py-0.5 text-[10px] font-mono border ${transformMode === mode ? 'bg-cyan-500/30 border-cyan-400/50 text-cyan-200' : 'bg-black/30 border-cyan-500/20 text-cyan-400/60 hover:border-cyan-400/30'}`, children: mode[0].toUpperCase() }, mode))) })] }), node.components && Object.entries(node.components).map(([key, comp]) => {
|
|
43
|
+
if (!comp)
|
|
44
|
+
return null;
|
|
45
|
+
const componentDef = ALL_COMPONENTS[comp.type];
|
|
46
|
+
if (!componentDef)
|
|
47
|
+
return _jsxs("div", { className: "px-1 py-0.5 text-red-400 text-[10px]", children: ["Unknown component type: ", comp.type, _jsx("textarea", { defaultValue: JSON.stringify(comp) })] }, key);
|
|
48
|
+
const EditorComp = componentDef.Editor;
|
|
49
|
+
return (_jsxs("div", { className: 'px-1', children: [_jsxs("div", { className: "flex justify-between items-center py-0.5 border-b border-cyan-500/20 bg-cyan-500/5", children: [_jsx("span", { className: "font-mono text-[10px] text-cyan-300 uppercase", children: key }), _jsx("button", { onClick: () => updateNode(n => {
|
|
50
|
+
const components = Object.assign({}, n.components);
|
|
51
|
+
delete components[key];
|
|
52
|
+
return Object.assign(Object.assign({}, n), { components });
|
|
53
|
+
}), className: "text-[9px] text-red-400/60 hover:text-red-400", children: "\u2715" })] }), EditorComp ? (_jsx(EditorComp, { component: comp, onUpdate: (newProps) => updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [key]: Object.assign(Object.assign({}, comp), { properties: Object.assign(Object.assign({}, comp.properties), newProps) }) }) }))) })) : null] }, key));
|
|
54
|
+
}), _jsxs("div", { className: "px-1.5 py-1 border-t border-cyan-500/20", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Add Component" }), _jsxs("div", { className: "flex gap-0.5", children: [_jsx("select", { className: "bg-black/40 border border-cyan-500/30 px-1 py-0.5 flex-1 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: addComponentType, onChange: e => setAddComponentType(e.target.value), children: allComponentKeys.filter(k => { var _a; return !((_a = node.components) === null || _a === void 0 ? void 0 : _a[k.toLowerCase()]); }).map(k => (_jsx("option", { value: k, children: k }, k))) }), _jsx("button", { className: "bg-cyan-500/20 hover:bg-cyan-500/30 border border-cyan-500/30 px-2 py-0.5 text-[10px] text-cyan-300 font-mono disabled:opacity-30", disabled: !addComponentType, onClick: () => {
|
|
55
|
+
var _a;
|
|
56
|
+
if (!addComponentType)
|
|
57
|
+
return;
|
|
58
|
+
const def = ALL_COMPONENTS[addComponentType];
|
|
59
|
+
if (def && !((_a = node.components) === null || _a === void 0 ? void 0 : _a[addComponentType.toLowerCase()])) {
|
|
60
|
+
const key = addComponentType.toLowerCase();
|
|
61
|
+
updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [key]: { type: def.name, properties: def.defaultProperties } }) })));
|
|
62
|
+
}
|
|
63
|
+
}, children: "+" })] })] })] });
|
|
64
|
+
}
|
|
65
|
+
function findNode(root, id) {
|
|
66
|
+
if (root.id === id)
|
|
67
|
+
return root;
|
|
68
|
+
if (root.children) {
|
|
69
|
+
for (const child of root.children) {
|
|
70
|
+
const found = findNode(child, id);
|
|
71
|
+
if (found)
|
|
72
|
+
return found;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
function updatePrefabNode(root, id, update) {
|
|
78
|
+
if (root.id === id) {
|
|
79
|
+
return update(root);
|
|
80
|
+
}
|
|
81
|
+
if (root.children) {
|
|
82
|
+
return Object.assign(Object.assign({}, root), { children: root.children.map(child => updatePrefabNode(child, id, update)) });
|
|
83
|
+
}
|
|
84
|
+
return root;
|
|
85
|
+
}
|
|
86
|
+
function deletePrefabNode(root, id) {
|
|
87
|
+
if (root.id === id)
|
|
88
|
+
return null;
|
|
89
|
+
if (root.children) {
|
|
90
|
+
return Object.assign(Object.assign({}, root), { children: root.children
|
|
91
|
+
.map(child => deletePrefabNode(child, id))
|
|
92
|
+
.filter((child) => child !== null) });
|
|
93
|
+
}
|
|
94
|
+
return root;
|
|
95
|
+
}
|
|
96
|
+
export default EditorUI;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useRef, useImperativeHandle, forwardRef, useCallback } from 'react';
|
|
2
|
+
const EventSystemHook = forwardRef(({ entityId }, ref) => {
|
|
3
|
+
const targetRef = useRef(typeof window !== 'undefined' ? window : null);
|
|
4
|
+
// Fire a global JS event with entityId as source
|
|
5
|
+
const fire = useCallback((eventType, data) => {
|
|
6
|
+
if (!targetRef.current)
|
|
7
|
+
return;
|
|
8
|
+
const event = new CustomEvent(eventType, {
|
|
9
|
+
detail: {
|
|
10
|
+
entityId,
|
|
11
|
+
data,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
targetRef.current.dispatchEvent(event);
|
|
15
|
+
}, [entityId]);
|
|
16
|
+
// Expose ref API
|
|
17
|
+
useImperativeHandle(ref, () => ({
|
|
18
|
+
fire,
|
|
19
|
+
}), [fire]);
|
|
20
|
+
return null;
|
|
21
|
+
});
|
|
22
|
+
EventSystemHook.displayName = 'EventSystemHook';
|
|
23
|
+
export default EventSystemHook;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import * as THREE from 'three';
|
|
3
|
+
export type InstanceData = {
|
|
4
|
+
id: string;
|
|
5
|
+
position: [number, number, number];
|
|
6
|
+
rotation: [number, number, number];
|
|
7
|
+
scale: [number, number, number];
|
|
8
|
+
meshPath: string;
|
|
9
|
+
physics?: {
|
|
10
|
+
type: 'dynamic' | 'fixed';
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
export declare function GameInstanceProvider({ children, models, onSelect, registerRef }: {
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
models: {
|
|
16
|
+
[filename: string]: THREE.Object3D;
|
|
17
|
+
};
|
|
18
|
+
onSelect?: (id: string | null) => void;
|
|
19
|
+
registerRef?: (id: string, obj: THREE.Object3D | null) => void;
|
|
20
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
export declare const GameInstance: React.ForwardRefExoticComponent<{
|
|
22
|
+
id: string;
|
|
23
|
+
modelUrl: string;
|
|
24
|
+
position: [number, number, number];
|
|
25
|
+
rotation: [number, number, number];
|
|
26
|
+
scale: [number, number, number];
|
|
27
|
+
physics?: {
|
|
28
|
+
type: "dynamic" | "fixed";
|
|
29
|
+
};
|
|
30
|
+
} & React.RefAttributes<THREE.Group<THREE.Object3DEventMap>>>;
|
|
@@ -0,0 +1,172 @@
|
|
|
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";
|
|
3
|
+
import { Merged } from '@react-three/drei';
|
|
4
|
+
import * as THREE from 'three';
|
|
5
|
+
import { InstancedRigidBodies } from "@react-three/rapier";
|
|
6
|
+
function arrayEquals(a, b) {
|
|
7
|
+
if (a === b)
|
|
8
|
+
return true;
|
|
9
|
+
if (a.length !== b.length)
|
|
10
|
+
return false;
|
|
11
|
+
for (let i = 0; i < a.length; i++) {
|
|
12
|
+
if (a[i] !== b[i])
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
function instanceEquals(a, b) {
|
|
18
|
+
var _a, _b;
|
|
19
|
+
return a.id === b.id &&
|
|
20
|
+
a.meshPath === b.meshPath &&
|
|
21
|
+
arrayEquals(a.position, b.position) &&
|
|
22
|
+
arrayEquals(a.rotation, b.rotation) &&
|
|
23
|
+
arrayEquals(a.scale, b.scale) &&
|
|
24
|
+
((_a = a.physics) === null || _a === void 0 ? void 0 : _a.type) === ((_b = b.physics) === null || _b === void 0 ? void 0 : _b.type);
|
|
25
|
+
}
|
|
26
|
+
const GameInstanceContext = createContext(null);
|
|
27
|
+
export function GameInstanceProvider({ children, models, onSelect, registerRef }) {
|
|
28
|
+
const [instances, setInstances] = useState([]);
|
|
29
|
+
const addInstance = useCallback((instance) => {
|
|
30
|
+
setInstances(prev => {
|
|
31
|
+
const idx = prev.findIndex(i => i.id === instance.id);
|
|
32
|
+
if (idx !== -1) {
|
|
33
|
+
if (instanceEquals(prev[idx], instance)) {
|
|
34
|
+
return prev;
|
|
35
|
+
}
|
|
36
|
+
const copy = [...prev];
|
|
37
|
+
copy[idx] = instance;
|
|
38
|
+
return copy;
|
|
39
|
+
}
|
|
40
|
+
return [...prev, instance];
|
|
41
|
+
});
|
|
42
|
+
}, []);
|
|
43
|
+
const removeInstance = useCallback((id) => {
|
|
44
|
+
setInstances(prev => {
|
|
45
|
+
if (!prev.find(i => i.id === id))
|
|
46
|
+
return prev;
|
|
47
|
+
return prev.filter(i => i.id !== id);
|
|
48
|
+
});
|
|
49
|
+
}, []);
|
|
50
|
+
// Flatten all model meshes once
|
|
51
|
+
const { flatMeshes, modelParts } = useMemo(() => {
|
|
52
|
+
const flatMeshes = {};
|
|
53
|
+
const modelParts = {};
|
|
54
|
+
Object.entries(models).forEach(([modelKey, model]) => {
|
|
55
|
+
const root = model;
|
|
56
|
+
root.updateWorldMatrix(false, true);
|
|
57
|
+
const rootInverse = new THREE.Matrix4().copy(root.matrixWorld).invert();
|
|
58
|
+
let partIndex = 0;
|
|
59
|
+
root.traverse((obj) => {
|
|
60
|
+
if (obj.isMesh) {
|
|
61
|
+
const geom = obj.geometry.clone();
|
|
62
|
+
const relativeTransform = obj.matrixWorld.clone().premultiply(rootInverse);
|
|
63
|
+
geom.applyMatrix4(relativeTransform);
|
|
64
|
+
const partKey = `${modelKey}__${partIndex}`;
|
|
65
|
+
flatMeshes[partKey] = new THREE.Mesh(geom, obj.material);
|
|
66
|
+
partIndex++;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
modelParts[modelKey] = partIndex;
|
|
70
|
+
});
|
|
71
|
+
return { flatMeshes, modelParts };
|
|
72
|
+
}, [models]);
|
|
73
|
+
// Group instances by meshPath + physics type
|
|
74
|
+
const grouped = useMemo(() => {
|
|
75
|
+
var _a;
|
|
76
|
+
const groups = {};
|
|
77
|
+
for (const inst of instances) {
|
|
78
|
+
const type = ((_a = inst.physics) === null || _a === void 0 ? void 0 : _a.type) || 'none';
|
|
79
|
+
const key = `${inst.meshPath}__${type}`;
|
|
80
|
+
if (!groups[key])
|
|
81
|
+
groups[key] = { physicsType: type, instances: [] };
|
|
82
|
+
groups[key].instances.push(inst);
|
|
83
|
+
}
|
|
84
|
+
return groups;
|
|
85
|
+
}, [instances]);
|
|
86
|
+
return (_jsxs(GameInstanceContext.Provider, { value: {
|
|
87
|
+
addInstance,
|
|
88
|
+
removeInstance,
|
|
89
|
+
instances,
|
|
90
|
+
meshes: flatMeshes,
|
|
91
|
+
modelParts
|
|
92
|
+
}, children: [children, Object.entries(grouped).map(([key, group]) => {
|
|
93
|
+
if (group.physicsType === 'none')
|
|
94
|
+
return null;
|
|
95
|
+
const modelKey = group.instances[0].meshPath;
|
|
96
|
+
const partCount = modelParts[modelKey] || 0;
|
|
97
|
+
if (partCount === 0)
|
|
98
|
+
return null;
|
|
99
|
+
return (_jsx(InstancedRigidGroup, { group: group, modelKey: modelKey, partCount: partCount, flatMeshes: flatMeshes }, key));
|
|
100
|
+
}), Object.entries(grouped).map(([key, group]) => {
|
|
101
|
+
if (group.physicsType !== 'none')
|
|
102
|
+
return null;
|
|
103
|
+
const modelKey = group.instances[0].meshPath;
|
|
104
|
+
const partCount = modelParts[modelKey] || 0;
|
|
105
|
+
if (partCount === 0)
|
|
106
|
+
return null;
|
|
107
|
+
// Restrict meshes to just this model's parts for this Merged
|
|
108
|
+
const meshesForModel = {};
|
|
109
|
+
for (let i = 0; i < partCount; i++) {
|
|
110
|
+
const partKey = `${modelKey}__${i}`;
|
|
111
|
+
meshesForModel[partKey] = flatMeshes[partKey];
|
|
112
|
+
}
|
|
113
|
+
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));
|
|
114
|
+
})] }));
|
|
115
|
+
}
|
|
116
|
+
// Physics instancing stays the same
|
|
117
|
+
function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes }) {
|
|
118
|
+
const instances = useMemo(() => group.instances.map(inst => ({
|
|
119
|
+
key: inst.id,
|
|
120
|
+
position: inst.position,
|
|
121
|
+
rotation: inst.rotation,
|
|
122
|
+
scale: inst.scale,
|
|
123
|
+
})), [group.instances]);
|
|
124
|
+
return (_jsx(InstancedRigidBodies, { instances: instances, colliders: group.physicsType === 'fixed' ? 'trimesh' : 'hull', type: group.physicsType, children: Array.from({ length: partCount }).map((_, i) => {
|
|
125
|
+
const mesh = flatMeshes[`${modelKey}__${i}`];
|
|
126
|
+
return (_jsx("instancedMesh", { args: [mesh.geometry, mesh.material, group.instances.length], castShadow: true, receiveShadow: true, frustumCulled: false }, i));
|
|
127
|
+
}) }));
|
|
128
|
+
}
|
|
129
|
+
// Non-physics instanced visuals: per-instance group using Merged's Instance components
|
|
130
|
+
function NonPhysicsInstancedGroup({ modelKey, group, partCount, instancesMap, onSelect, registerRef }) {
|
|
131
|
+
const clickValid = useRef(false);
|
|
132
|
+
const handlePointerDown = (e) => { e.stopPropagation(); clickValid.current = true; };
|
|
133
|
+
const handlePointerMove = () => { if (clickValid.current)
|
|
134
|
+
clickValid.current = false; };
|
|
135
|
+
const handlePointerUp = (e, id) => {
|
|
136
|
+
if (clickValid.current) {
|
|
137
|
+
e.stopPropagation();
|
|
138
|
+
onSelect === null || onSelect === void 0 ? void 0 : onSelect(id);
|
|
139
|
+
}
|
|
140
|
+
clickValid.current = false;
|
|
141
|
+
};
|
|
142
|
+
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) => {
|
|
143
|
+
const Instance = instancesMap[`${modelKey}__${i}`];
|
|
144
|
+
if (!Instance)
|
|
145
|
+
return null;
|
|
146
|
+
return _jsx(Instance, {}, i);
|
|
147
|
+
}) }, inst.id))) }));
|
|
148
|
+
}
|
|
149
|
+
// --- GameInstance: just registers an instance, renders nothing ---
|
|
150
|
+
export const GameInstance = React.forwardRef(({ id, modelUrl, position, rotation, scale, physics = undefined, }, ref) => {
|
|
151
|
+
const ctx = useContext(GameInstanceContext);
|
|
152
|
+
const addInstance = ctx === null || ctx === void 0 ? void 0 : ctx.addInstance;
|
|
153
|
+
const removeInstance = ctx === null || ctx === void 0 ? void 0 : ctx.removeInstance;
|
|
154
|
+
const instance = useMemo(() => ({
|
|
155
|
+
id,
|
|
156
|
+
meshPath: modelUrl,
|
|
157
|
+
position,
|
|
158
|
+
rotation,
|
|
159
|
+
scale,
|
|
160
|
+
physics,
|
|
161
|
+
}), [id, modelUrl, position, rotation, scale, physics]);
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (!addInstance || !removeInstance)
|
|
164
|
+
return;
|
|
165
|
+
addInstance(instance);
|
|
166
|
+
return () => {
|
|
167
|
+
removeInstance(instance.id);
|
|
168
|
+
};
|
|
169
|
+
}, [addInstance, removeInstance, instance]);
|
|
170
|
+
// No visual here – provider will render visuals for all instances
|
|
171
|
+
return null;
|
|
172
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
12
|
+
import GameCanvas from "../../shared/GameCanvas";
|
|
13
|
+
import { useState, useRef, } from "react";
|
|
14
|
+
import PrefabRoot from "./PrefabRoot";
|
|
15
|
+
import { Physics } from "@react-three/rapier";
|
|
16
|
+
// import testPrefab from "./samples/test.json";
|
|
17
|
+
import EditorUI from "./EditorUI";
|
|
18
|
+
const PrefabEditor = ({ children }) => {
|
|
19
|
+
const [editMode, setEditMode] = useState(true);
|
|
20
|
+
const [loadedPrefab, setLoadedPrefab] = useState({
|
|
21
|
+
"id": "prefab-default",
|
|
22
|
+
"name": "New Prefab",
|
|
23
|
+
"root": {
|
|
24
|
+
"id": "root",
|
|
25
|
+
"enabled": true,
|
|
26
|
+
"visible": true,
|
|
27
|
+
"components": {
|
|
28
|
+
"transform": {
|
|
29
|
+
"type": "Transform",
|
|
30
|
+
"properties": {
|
|
31
|
+
"position": [0, 0, 0],
|
|
32
|
+
"rotation": [0, 0, 0],
|
|
33
|
+
"scale": [1, 1, 1]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
const [selectedId, setSelectedId] = useState(null);
|
|
40
|
+
const [transformMode, setTransformMode] = useState("translate");
|
|
41
|
+
const prefabRef = useRef(null);
|
|
42
|
+
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,
|
|
43
|
+
// props for edit mode
|
|
44
|
+
editMode: editMode, onPrefabChange: setLoadedPrefab, selectedId: selectedId, onSelect: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode }), children] }) }), _jsxs("div", { style: { position: "absolute", top: "0.5rem", left: "50%", transform: "translateX(-50%)" }, className: "bg-black/70 backdrop-blur-sm border border-cyan-500/30 px-2 py-1 flex items-center gap-1", children: [_jsx("button", { className: "px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30", onClick: () => setEditMode(!editMode), children: editMode ? "▶" : "⏸" }), _jsx("span", { className: "text-cyan-500/30 text-[10px]", children: "|" }), _jsx("button", { className: "px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30", onClick: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
45
|
+
const prefab = yield loadJson();
|
|
46
|
+
if (prefab)
|
|
47
|
+
setLoadedPrefab(prefab);
|
|
48
|
+
}), children: "\uD83D\uDCE5" }), _jsx("button", { className: "px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30", onClick: () => saveJson(loadedPrefab, "prefab"), children: "\uD83D\uDCBE" })] }), editMode && _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: setLoadedPrefab, selectedId: selectedId, setSelectedId: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode })] });
|
|
49
|
+
};
|
|
50
|
+
const saveJson = (data, filename) => {
|
|
51
|
+
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
|
|
52
|
+
const downloadAnchorNode = document.createElement('a');
|
|
53
|
+
downloadAnchorNode.setAttribute("href", dataStr);
|
|
54
|
+
downloadAnchorNode.setAttribute("download", (filename || 'prefab') + ".json");
|
|
55
|
+
document.body.appendChild(downloadAnchorNode);
|
|
56
|
+
downloadAnchorNode.click();
|
|
57
|
+
downloadAnchorNode.remove();
|
|
58
|
+
};
|
|
59
|
+
const loadJson = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
const input = document.createElement('input');
|
|
62
|
+
input.type = 'file';
|
|
63
|
+
input.accept = '.json,application/json';
|
|
64
|
+
input.onchange = e => {
|
|
65
|
+
var _a;
|
|
66
|
+
const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
|
|
67
|
+
if (!file)
|
|
68
|
+
return resolve(undefined);
|
|
69
|
+
const reader = new FileReader();
|
|
70
|
+
reader.onload = e => {
|
|
71
|
+
var _a;
|
|
72
|
+
try {
|
|
73
|
+
const text = (_a = e.target) === null || _a === void 0 ? void 0 : _a.result;
|
|
74
|
+
if (typeof text === 'string') {
|
|
75
|
+
const json = JSON.parse(text);
|
|
76
|
+
resolve(json);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
console.error('Error parsing prefab JSON:', err);
|
|
81
|
+
resolve(undefined);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
reader.readAsText(file);
|
|
85
|
+
};
|
|
86
|
+
input.click();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
export default PrefabEditor;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Group } from "three";
|
|
2
|
+
import { Prefab } from "./types";
|
|
3
|
+
export declare const PrefabRoot: import("react").ForwardRefExoticComponent<{
|
|
4
|
+
editMode?: boolean;
|
|
5
|
+
data: Prefab;
|
|
6
|
+
onPrefabChange?: (data: Prefab) => void;
|
|
7
|
+
selectedId?: string | null;
|
|
8
|
+
onSelect?: (id: string | null) => void;
|
|
9
|
+
transformMode?: "translate" | "rotate" | "scale";
|
|
10
|
+
setTransformMode?: (mode: "translate" | "rotate" | "scale") => void;
|
|
11
|
+
} & import("react").RefAttributes<Group<import("three").Object3DEventMap>>>;
|
|
12
|
+
export default PrefabRoot;
|