react-three-game 0.0.46 → 0.0.48
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/.github/copilot-instructions.md +1 -1
- package/.gitmodules +3 -0
- package/README.md +5 -0
- package/dist/index.d.ts +1 -0
- package/dist/tools/prefabeditor/PrefabEditor.js +4 -13
- package/dist/tools/prefabeditor/components/AmbientLightComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/AmbientLightComponent.js +23 -0
- package/dist/tools/prefabeditor/components/GeometryComponent.js +7 -0
- package/dist/tools/prefabeditor/components/Input.js +23 -1
- package/dist/tools/prefabeditor/components/index.js +2 -0
- package/dist/tools/prefabeditor/utils.d.ts +20 -0
- package/dist/tools/prefabeditor/utils.js +51 -0
- package/package.json +1 -1
- package/react-three-game-skill/.gitattributes +2 -0
- package/react-three-game-skill/README.md +1 -0
- package/react-three-game-skill/react-three-game/SKILL.md +394 -0
- package/src/index.ts +1 -0
- package/src/tools/prefabeditor/PrefabEditor.tsx +4 -19
- package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +40 -0
- package/src/tools/prefabeditor/components/GeometryComponent.tsx +7 -0
- package/src/tools/prefabeditor/components/Input.tsx +32 -3
- package/src/tools/prefabeditor/components/index.ts +2 -0
- package/src/tools/prefabeditor/utils.ts +69 -0
- package/skill/SKILL.md +0 -491
- package/skill/package.json +0 -17
|
@@ -6,8 +6,7 @@ import { Physics } from "@react-three/rapier";
|
|
|
6
6
|
import EditorUI from "./EditorUI";
|
|
7
7
|
import { base, toolbar } from "./styles";
|
|
8
8
|
import { EditorContext } from "./EditorContext";
|
|
9
|
-
import {
|
|
10
|
-
import { Group } from "three";
|
|
9
|
+
import { exportGLB } from "./utils";
|
|
11
10
|
|
|
12
11
|
export interface PrefabEditorRef {
|
|
13
12
|
screenshot: () => void;
|
|
@@ -115,23 +114,9 @@ const PrefabEditor = forwardRef<PrefabEditorRef, {
|
|
|
115
114
|
const sceneRoot = prefabRootRef.current?.root;
|
|
116
115
|
if (!sceneRoot) return;
|
|
117
116
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
(result) => {
|
|
122
|
-
const blob = new Blob([result as ArrayBuffer], { type: 'application/octet-stream' });
|
|
123
|
-
const url = URL.createObjectURL(blob);
|
|
124
|
-
const a = document.createElement('a');
|
|
125
|
-
a.href = url;
|
|
126
|
-
a.download = `${loadedPrefab.name || 'scene'}.glb`;
|
|
127
|
-
a.click();
|
|
128
|
-
URL.revokeObjectURL(url);
|
|
129
|
-
},
|
|
130
|
-
(error) => {
|
|
131
|
-
console.error('Error exporting GLB:', error);
|
|
132
|
-
},
|
|
133
|
-
{ binary: true }
|
|
134
|
-
);
|
|
117
|
+
exportGLB(sceneRoot, {
|
|
118
|
+
filename: `${loadedPrefab.name || 'scene'}.glb`
|
|
119
|
+
});
|
|
135
120
|
};
|
|
136
121
|
|
|
137
122
|
useEffect(() => {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Component } from "./ComponentRegistry";
|
|
2
|
+
import { FieldRenderer, FieldDefinition } from "./Input";
|
|
3
|
+
|
|
4
|
+
const ambientLightFields: FieldDefinition[] = [
|
|
5
|
+
{ name: 'color', type: 'color', label: 'Color' },
|
|
6
|
+
{ name: 'intensity', type: 'number', label: 'Intensity', step: 0.1, min: 0 },
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
function AmbientLightComponentEditor({
|
|
10
|
+
component,
|
|
11
|
+
onUpdate,
|
|
12
|
+
}: {
|
|
13
|
+
component: any;
|
|
14
|
+
onUpdate: (newProps: any) => void;
|
|
15
|
+
}) {
|
|
16
|
+
return (
|
|
17
|
+
<FieldRenderer
|
|
18
|
+
fields={ambientLightFields}
|
|
19
|
+
values={component.properties}
|
|
20
|
+
onChange={onUpdate}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function AmbientLightComponentView({ properties }: { properties: any }) {
|
|
26
|
+
const { color = '#ffffff', intensity = 1 } = properties;
|
|
27
|
+
return <ambientLight color={color} intensity={intensity} />;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const AmbientLightComponent: Component = {
|
|
31
|
+
name: 'AmbientLight',
|
|
32
|
+
Editor: AmbientLightComponentEditor,
|
|
33
|
+
View: AmbientLightComponentView,
|
|
34
|
+
defaultProperties: {
|
|
35
|
+
color: '#ffffff',
|
|
36
|
+
intensity: 1,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default AmbientLightComponent;
|
|
@@ -17,6 +17,10 @@ const GEOMETRY_ARGS: Record<string, {
|
|
|
17
17
|
labels: ["Width", "Height"],
|
|
18
18
|
defaults: [1, 1],
|
|
19
19
|
},
|
|
20
|
+
cylinder: {
|
|
21
|
+
labels: ["Radius Top", "Radius Bottom", "Height", "Radial Segments"],
|
|
22
|
+
defaults: [1, 1, 1, 32],
|
|
23
|
+
},
|
|
20
24
|
};
|
|
21
25
|
|
|
22
26
|
function GeometryComponentEditor({
|
|
@@ -38,6 +42,7 @@ function GeometryComponentEditor({
|
|
|
38
42
|
{ value: 'box', label: 'Box' },
|
|
39
43
|
{ value: 'sphere', label: 'Sphere' },
|
|
40
44
|
{ value: 'plane', label: 'Plane' },
|
|
45
|
+
{ value: 'cylinder', label: 'Cylinder' },
|
|
41
46
|
],
|
|
42
47
|
},
|
|
43
48
|
{
|
|
@@ -101,6 +106,8 @@ function GeometryComponentView({ properties, children }: { properties: any, chil
|
|
|
101
106
|
return <sphereGeometry args={args as [number, number?, number?]} />;
|
|
102
107
|
case "plane":
|
|
103
108
|
return <planeGeometry args={args as [number, number]} />;
|
|
109
|
+
case "cylinder":
|
|
110
|
+
return <cylinderGeometry args={args as [number, number, number, number?]} />;
|
|
104
111
|
default:
|
|
105
112
|
return <boxGeometry args={[1, 1, 1]} />;
|
|
106
113
|
}
|
|
@@ -96,11 +96,40 @@ interface InputProps {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
export function Input({ value, onChange, step, min, max, style }: InputProps) {
|
|
99
|
+
const [draft, setDraft] = useState<string>(() => value.toString());
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
setDraft(value.toString());
|
|
103
|
+
}, [value]);
|
|
104
|
+
|
|
105
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
106
|
+
const inputValue = e.target.value;
|
|
107
|
+
setDraft(inputValue);
|
|
108
|
+
|
|
109
|
+
const num = parseFloat(inputValue);
|
|
110
|
+
if (Number.isFinite(num)) {
|
|
111
|
+
onChange(num);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const handleBlur = () => {
|
|
116
|
+
const num = parseFloat(draft);
|
|
117
|
+
if (!Number.isFinite(num)) {
|
|
118
|
+
setDraft(value.toString());
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
99
122
|
return (
|
|
100
123
|
<input
|
|
101
|
-
type="
|
|
102
|
-
value={
|
|
103
|
-
onChange={
|
|
124
|
+
type="text"
|
|
125
|
+
value={draft}
|
|
126
|
+
onChange={handleChange}
|
|
127
|
+
onBlur={handleBlur}
|
|
128
|
+
onKeyDown={e => {
|
|
129
|
+
if (e.key === 'Enter') {
|
|
130
|
+
(e.target as HTMLInputElement).blur();
|
|
131
|
+
}
|
|
132
|
+
}}
|
|
104
133
|
step={step}
|
|
105
134
|
min={min}
|
|
106
135
|
max={max}
|
|
@@ -4,6 +4,7 @@ import MaterialComponent from './MaterialComponent';
|
|
|
4
4
|
import PhysicsComponent from './PhysicsComponent';
|
|
5
5
|
import SpotLightComponent from './SpotLightComponent';
|
|
6
6
|
import DirectionalLightComponent from './DirectionalLightComponent';
|
|
7
|
+
import AmbientLightComponent from './AmbientLightComponent';
|
|
7
8
|
import ModelComponent from './ModelComponent';
|
|
8
9
|
import TextComponent from './TextComponent';
|
|
9
10
|
|
|
@@ -14,6 +15,7 @@ export default [
|
|
|
14
15
|
PhysicsComponent,
|
|
15
16
|
SpotLightComponent,
|
|
16
17
|
DirectionalLightComponent,
|
|
18
|
+
AmbientLightComponent,
|
|
17
19
|
ModelComponent,
|
|
18
20
|
TextComponent
|
|
19
21
|
];
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { GameObject, Prefab } from "./types";
|
|
2
|
+
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
|
|
3
|
+
import { Object3D } from 'three';
|
|
4
|
+
|
|
5
|
+
export interface ExportGLBOptions {
|
|
6
|
+
filename?: string;
|
|
7
|
+
binary?: boolean;
|
|
8
|
+
onComplete?: (result: ArrayBuffer | object) => void;
|
|
9
|
+
onError?: (error: any) => void;
|
|
10
|
+
}
|
|
2
11
|
|
|
3
12
|
/** Save a prefab as JSON file */
|
|
4
13
|
export function saveJson(data: Prefab, filename: string) {
|
|
@@ -33,6 +42,66 @@ export function loadJson(): Promise<Prefab | undefined> {
|
|
|
33
42
|
});
|
|
34
43
|
}
|
|
35
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Export a Three.js scene or object to GLB format
|
|
47
|
+
* @param sceneRoot - The Three.js Object3D to export
|
|
48
|
+
* @param options - Export options
|
|
49
|
+
* @returns Promise that resolves when export is complete
|
|
50
|
+
*/
|
|
51
|
+
export function exportGLB(
|
|
52
|
+
sceneRoot: Object3D,
|
|
53
|
+
options: ExportGLBOptions = {}
|
|
54
|
+
): Promise<ArrayBuffer | object> {
|
|
55
|
+
const {
|
|
56
|
+
filename = 'scene.glb',
|
|
57
|
+
binary = true,
|
|
58
|
+
onComplete,
|
|
59
|
+
onError
|
|
60
|
+
} = options;
|
|
61
|
+
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const exporter = new GLTFExporter();
|
|
64
|
+
|
|
65
|
+
exporter.parse(
|
|
66
|
+
sceneRoot,
|
|
67
|
+
(result) => {
|
|
68
|
+
onComplete?.(result);
|
|
69
|
+
resolve(result);
|
|
70
|
+
|
|
71
|
+
// Trigger download if filename is provided
|
|
72
|
+
if (filename) {
|
|
73
|
+
const blob = new Blob(
|
|
74
|
+
[result as ArrayBuffer],
|
|
75
|
+
{ type: binary ? 'application/octet-stream' : 'application/json' }
|
|
76
|
+
);
|
|
77
|
+
const url = URL.createObjectURL(blob);
|
|
78
|
+
const a = document.createElement('a');
|
|
79
|
+
a.href = url;
|
|
80
|
+
a.download = filename;
|
|
81
|
+
a.click();
|
|
82
|
+
URL.revokeObjectURL(url);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
(error) => {
|
|
86
|
+
console.error('Error exporting GLB:', error);
|
|
87
|
+
onError?.(error);
|
|
88
|
+
reject(error);
|
|
89
|
+
},
|
|
90
|
+
{ binary }
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Export a Three.js scene to GLB and return the ArrayBuffer without downloading
|
|
97
|
+
* @param sceneRoot - The Three.js Object3D to export
|
|
98
|
+
* @returns Promise that resolves with the GLB data as ArrayBuffer
|
|
99
|
+
*/
|
|
100
|
+
export async function exportGLBData(sceneRoot: Object3D): Promise<ArrayBuffer> {
|
|
101
|
+
const result = await exportGLB(sceneRoot, { filename: '', binary: true });
|
|
102
|
+
return result as ArrayBuffer;
|
|
103
|
+
}
|
|
104
|
+
|
|
36
105
|
/** Find a node by ID in the tree */
|
|
37
106
|
export function findNode(root: GameObject, id: string): GameObject | null {
|
|
38
107
|
if (root.id === id) return root;
|