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.
@@ -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 { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter.js";
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
- const exporter = new GLTFExporter();
119
- exporter.parse(
120
- sceneRoot,
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="number"
102
- value={value}
103
- onChange={(e) => onChange(parseFloat(e.target.value))}
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;