react-three-game 0.0.57 → 0.0.59

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.
Files changed (39) hide show
  1. package/.github/copilot-instructions.md +1 -1
  2. package/README.md +59 -35
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/tools/assetviewer/page.js +1 -1
  6. package/dist/tools/dragdrop/DragDropLoader.d.ts +19 -6
  7. package/dist/tools/dragdrop/DragDropLoader.js +77 -40
  8. package/dist/tools/dragdrop/index.d.ts +4 -0
  9. package/dist/tools/dragdrop/index.js +2 -0
  10. package/dist/tools/dragdrop/modelLoader.d.ts +5 -6
  11. package/dist/tools/dragdrop/modelLoader.js +62 -49
  12. package/dist/tools/dragdrop/page.js +3 -3
  13. package/dist/tools/prefabeditor/EditorTree.js +24 -48
  14. package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +33 -0
  15. package/dist/tools/prefabeditor/EditorTreeMenus.js +136 -0
  16. package/dist/tools/prefabeditor/PrefabEditor.js +1 -1
  17. package/dist/tools/prefabeditor/PrefabRoot.js +5 -3
  18. package/dist/tools/prefabeditor/components/CameraComponent.js +32 -12
  19. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +49 -23
  20. package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +8 -0
  21. package/dist/tools/prefabeditor/components/MaterialComponent.js +11 -5
  22. package/dist/tools/prefabeditor/components/SpotLightComponent.js +34 -13
  23. package/package.json +2 -2
  24. package/react-three-game-skill/react-three-game/SKILL.md +63 -5
  25. package/react-three-game-skill/react-three-game/rules/ADVANCED_PHYSICS.md +7 -5
  26. package/src/index.ts +1 -1
  27. package/src/tools/assetviewer/page.tsx +1 -1
  28. package/src/tools/dragdrop/DragDropLoader.tsx +118 -55
  29. package/src/tools/dragdrop/index.ts +4 -0
  30. package/src/tools/dragdrop/modelLoader.ts +95 -50
  31. package/src/tools/dragdrop/page.tsx +7 -4
  32. package/src/tools/prefabeditor/EditorTree.tsx +56 -125
  33. package/src/tools/prefabeditor/EditorTreeMenus.tsx +307 -0
  34. package/src/tools/prefabeditor/PrefabEditor.tsx +1 -1
  35. package/src/tools/prefabeditor/PrefabRoot.tsx +6 -3
  36. package/src/tools/prefabeditor/components/CameraComponent.tsx +51 -14
  37. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +59 -28
  38. package/src/tools/prefabeditor/components/MaterialComponent.tsx +18 -9
  39. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +49 -18
@@ -0,0 +1,307 @@
1
+ import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { Prefab } from './types';
4
+ import { menu } from './styles';
5
+ import { useEditorContext } from './EditorContext';
6
+ import { getComponent } from './components/ComponentRegistry';
7
+ import { loadJson, saveJson, regenerateIds, updateNodeById } from './utils';
8
+
9
+ export type TreeContextMenuState = { nodeId: string; x: number; y: number } | null;
10
+
11
+ function createEmptyPrefab(): Prefab {
12
+ return {
13
+ id: crypto.randomUUID(),
14
+ name: 'New Scene',
15
+ root: {
16
+ id: crypto.randomUUID(),
17
+ name: 'Scene',
18
+ components: {
19
+ transform: {
20
+ type: 'Transform',
21
+ properties: { ...getComponent('Transform')?.defaultProperties }
22
+ }
23
+ },
24
+ children: []
25
+ }
26
+ };
27
+ }
28
+
29
+ function MenuPanel({
30
+ children,
31
+ style,
32
+ }: {
33
+ children: React.ReactNode;
34
+ style?: React.CSSProperties;
35
+ }) {
36
+ return (
37
+ <div style={{ ...menu.container, position: 'static', ...style }} onClick={(e) => e.stopPropagation()}>
38
+ {children}
39
+ </div>
40
+ );
41
+ }
42
+
43
+ function MenuItemButton({
44
+ children,
45
+ onClick,
46
+ danger = false,
47
+ style,
48
+ }: {
49
+ children: React.ReactNode;
50
+ onClick: () => void;
51
+ danger?: boolean;
52
+ style?: React.CSSProperties;
53
+ }) {
54
+ return (
55
+ <button
56
+ style={danger ? { ...menu.item, ...menu.danger, ...style } : { ...menu.item, ...style }}
57
+ onClick={onClick}
58
+ >
59
+ {children}
60
+ </button>
61
+ );
62
+ }
63
+
64
+ function MenuSubmenu({
65
+ label,
66
+ children,
67
+ }: {
68
+ label: string;
69
+ children: React.ReactNode;
70
+ }) {
71
+ const [isOpen, setIsOpen] = useState(false);
72
+
73
+ return (
74
+ <div
75
+ style={{ position: 'relative' }}
76
+ onMouseEnter={() => setIsOpen(true)}
77
+ onMouseLeave={() => setIsOpen(false)}
78
+ >
79
+ <MenuItemButton
80
+ onClick={() => setIsOpen(open => !open)}
81
+ style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}
82
+ >
83
+ <span>{label}</span>
84
+ <span aria-hidden="true">›</span>
85
+ </MenuItemButton>
86
+ {isOpen && (
87
+ <div
88
+ style={{
89
+ position: 'absolute',
90
+ top: 0,
91
+ left: '100%',
92
+ zIndex: 1,
93
+ }}
94
+ >
95
+ <MenuPanel>{children}</MenuPanel>
96
+ </div>
97
+ )}
98
+ </div>
99
+ );
100
+ }
101
+
102
+ export function MenuTriggerButton({
103
+ buttonRef,
104
+ onToggle,
105
+ title,
106
+ style,
107
+ children,
108
+ }: {
109
+ buttonRef: React.RefObject<HTMLButtonElement | null>;
110
+ onToggle: () => void;
111
+ title: string;
112
+ style: React.CSSProperties;
113
+ children: React.ReactNode;
114
+ }) {
115
+ return (
116
+ <button
117
+ ref={buttonRef}
118
+ style={style}
119
+ onClick={(e) => {
120
+ e.stopPropagation();
121
+ onToggle();
122
+ }}
123
+ title={title}
124
+ >
125
+ {children}
126
+ </button>
127
+ );
128
+ }
129
+
130
+ export function TreeNodeMenu({
131
+ isRoot,
132
+ nodeId,
133
+ onAddChild,
134
+ onFocus,
135
+ onDuplicate,
136
+ onDelete,
137
+ onClose,
138
+ }: {
139
+ isRoot: boolean;
140
+ nodeId: string;
141
+ onAddChild: (parentId: string) => void;
142
+ onFocus: (nodeId: string) => void;
143
+ onDuplicate?: (nodeId: string) => void;
144
+ onDelete?: (nodeId: string) => void;
145
+ onClose: () => void;
146
+ }) {
147
+ return (
148
+ <MenuPanel>
149
+ <MenuItemButton onClick={() => { onAddChild(nodeId); onClose(); }}>
150
+ Add Child
151
+ </MenuItemButton>
152
+ <MenuItemButton onClick={() => { onFocus(nodeId); onClose(); }}>
153
+ Focus Camera
154
+ </MenuItemButton>
155
+ {!isRoot && onDuplicate && (
156
+ <MenuItemButton onClick={() => { onDuplicate(nodeId); onClose(); }}>
157
+ Duplicate
158
+ </MenuItemButton>
159
+ )}
160
+ {!isRoot && onDelete && (
161
+ <MenuItemButton danger onClick={() => { onDelete(nodeId); onClose(); }}>
162
+ Delete
163
+ </MenuItemButton>
164
+ )}
165
+ </MenuPanel>
166
+ );
167
+ }
168
+
169
+ export function TreeContextMenu({
170
+ contextMenu,
171
+ onClose,
172
+ children,
173
+ }: {
174
+ contextMenu: TreeContextMenuState;
175
+ onClose: () => void;
176
+ children: (nodeId: string, onClose: () => void) => React.ReactNode;
177
+ }) {
178
+ const panelRef = useRef<HTMLDivElement>(null);
179
+ const [position, setPosition] = useState<{ left: number; top: number } | null>(null);
180
+
181
+ useEffect(() => {
182
+ if (!contextMenu) return;
183
+
184
+ const handlePointerDown = (event: PointerEvent) => {
185
+ const target = event.target as Node | null;
186
+ if (!target) return;
187
+ if (panelRef.current?.contains(target)) return;
188
+ onClose();
189
+ };
190
+
191
+ const handleKeyDown = (event: KeyboardEvent) => {
192
+ if (event.key === 'Escape') onClose();
193
+ };
194
+
195
+ document.addEventListener('pointerdown', handlePointerDown);
196
+ document.addEventListener('keydown', handleKeyDown);
197
+
198
+ return () => {
199
+ document.removeEventListener('pointerdown', handlePointerDown);
200
+ document.removeEventListener('keydown', handleKeyDown);
201
+ };
202
+ }, [contextMenu, onClose]);
203
+
204
+ useEffect(() => {
205
+ if (!contextMenu || !panelRef.current || typeof window === 'undefined') return;
206
+
207
+ const panelRect = panelRef.current.getBoundingClientRect();
208
+ const left = Math.max(8, Math.min(contextMenu.x, window.innerWidth - panelRect.width - 8));
209
+ const top = Math.max(8, Math.min(contextMenu.y, window.innerHeight - panelRect.height - 8));
210
+ setPosition({ left, top });
211
+ }, [contextMenu]);
212
+
213
+ useEffect(() => {
214
+ if (!contextMenu) {
215
+ setPosition(null);
216
+ }
217
+ }, [contextMenu]);
218
+
219
+ if (!contextMenu || typeof document === 'undefined') return null;
220
+
221
+ return createPortal(
222
+ <div
223
+ ref={panelRef}
224
+ style={{
225
+ position: 'fixed',
226
+ left: position?.left ?? contextMenu.x,
227
+ top: position?.top ?? contextMenu.y,
228
+ zIndex: 1000,
229
+ }}
230
+ onMouseLeave={onClose}
231
+ onContextMenu={(e) => e.preventDefault()}
232
+ >
233
+ {children(contextMenu.nodeId, onClose)}
234
+ </div>,
235
+ document.body
236
+ );
237
+ }
238
+
239
+ export function FileMenu({
240
+ prefabData,
241
+ setPrefabData,
242
+ onClose
243
+ }: {
244
+ prefabData: Prefab;
245
+ setPrefabData: Dispatch<SetStateAction<Prefab>>;
246
+ onClose: () => void;
247
+ }) {
248
+ const { onScreenshot, onExportGLB } = useEditorContext();
249
+
250
+ const handleNewScene = () => {
251
+ setPrefabData(createEmptyPrefab());
252
+ onClose();
253
+ };
254
+
255
+ const handleNewSceneFromPrefab = async () => {
256
+ const loadedPrefab = await loadJson();
257
+ if (!loadedPrefab) return;
258
+ setPrefabData(loadedPrefab);
259
+ onClose();
260
+ };
261
+
262
+ const handleSave = () => {
263
+ saveJson(prefabData, 'prefab');
264
+ onClose();
265
+ };
266
+
267
+ const handleLoadIntoScene = async () => {
268
+ const loadedPrefab = await loadJson();
269
+ if (!loadedPrefab) return;
270
+
271
+ setPrefabData(prev => ({
272
+ ...prev,
273
+ root: updateNodeById(prev.root, prev.root.id, root => ({
274
+ ...root,
275
+ children: [...(root.children ?? []), regenerateIds(loadedPrefab.root)]
276
+ }))
277
+ }));
278
+ onClose();
279
+ };
280
+
281
+ return (
282
+ <MenuPanel style={{ overflow: 'visible' }}>
283
+ <MenuSubmenu label="File">
284
+ <MenuItemButton onClick={handleNewScene}>
285
+ New Scene
286
+ </MenuItemButton>
287
+ <MenuItemButton onClick={handleNewSceneFromPrefab}>
288
+ New Scene from Prefab
289
+ </MenuItemButton>
290
+ <MenuItemButton onClick={handleLoadIntoScene}>
291
+ Load Prefab into Scene
292
+ </MenuItemButton>
293
+ <MenuItemButton onClick={handleSave}>
294
+ Save Prefab
295
+ </MenuItemButton>
296
+ </MenuSubmenu>
297
+ <MenuSubmenu label="Export">
298
+ <MenuItemButton onClick={() => { onExportGLB?.(); onClose(); }}>
299
+ GLB
300
+ </MenuItemButton>
301
+ <MenuItemButton onClick={() => { onScreenshot?.(); onClose(); }}>
302
+ PNG
303
+ </MenuItemButton>
304
+ </MenuSubmenu>
305
+ </MenuPanel>
306
+ );
307
+ }
@@ -7,7 +7,7 @@ import EditorUI from "./EditorUI";
7
7
  import { base, toolbar } from "./styles";
8
8
  import { EditorContext } from "./EditorContext";
9
9
  import { exportGLB, createModelNode, createImageNode } from "./utils";
10
- import { parseModelFromFile } from "../dragdrop/modelLoader";
10
+ import { parseModelFromFile } from "../dragdrop";
11
11
 
12
12
  export interface PrefabEditorRef {
13
13
  screenshot: () => void;
@@ -6,7 +6,7 @@ import { ThreeEvent } from "@react-three/fiber";
6
6
  import { Prefab, GameObject as GameObjectType } from "./types";
7
7
  import { getComponent, registerComponent, getNonComposableKeys } from "./components/ComponentRegistry";
8
8
  import components from "./components";
9
- import { loadModel } from "../dragdrop/modelLoader";
9
+ import { loadModel } from "../dragdrop";
10
10
  import { GameInstance, GameInstanceProvider, useInstanceCheck } from "./InstanceProvider";
11
11
  import { focusCameraOnObject, updateNode } from "./utils";
12
12
  import { PhysicsProps } from "./components/PhysicsComponent";
@@ -155,8 +155,11 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
155
155
  : `${basePath}/${file}`;
156
156
 
157
157
  const res = await loadModel(path);
158
- res.success && res.model &&
159
- setModels(m => ({ ...m, [file]: res.model }));
158
+ const model = res.model;
159
+
160
+ if (res.success && model) {
161
+ setModels(m => ({ ...m, [file]: model }));
162
+ }
160
163
  });
161
164
 
162
165
  const loader = new TextureLoader();
@@ -1,16 +1,26 @@
1
- import { PerspectiveCamera, useHelper } from '@react-three/drei';
2
- import { useRef } from 'react';
1
+ import { PerspectiveCamera } from '@react-three/drei';
2
+ import { useEffect, useMemo, useState } from 'react';
3
3
  import { CameraHelper, PerspectiveCamera as ThreePerspectiveCamera } from 'three';
4
+ import { useFrame } from '@react-three/fiber';
4
5
  import { Component } from './ComponentRegistry';
5
6
  import { FieldGroup, NumberField } from './Input';
6
7
 
8
+ const cameraDefaults = {
9
+ fov: 50,
10
+ near: 0.1,
11
+ zoom: 1,
12
+ far: 1000,
13
+ };
14
+
7
15
  function CameraComponentEditor({ component, onUpdate }: { component: any; onUpdate: (newComp: any) => void }) {
16
+ const values = { ...cameraDefaults, ...component.properties };
17
+
8
18
  return (
9
19
  <FieldGroup>
10
20
  <NumberField
11
21
  name="fov"
12
22
  label="FOV"
13
- values={component.properties}
23
+ values={values}
14
24
  onChange={onUpdate}
15
25
  fallback={50}
16
26
  min={1}
@@ -20,7 +30,7 @@ function CameraComponentEditor({ component, onUpdate }: { component: any; onUpda
20
30
  <NumberField
21
31
  name="near"
22
32
  label="Near"
23
- values={component.properties}
33
+ values={values}
24
34
  onChange={onUpdate}
25
35
  fallback={0.1}
26
36
  min={0.001}
@@ -29,7 +39,7 @@ function CameraComponentEditor({ component, onUpdate }: { component: any; onUpda
29
39
  <NumberField
30
40
  name="zoom"
31
41
  label="Zoom"
32
- values={component.properties}
42
+ values={values}
33
43
  onChange={onUpdate}
34
44
  fallback={1}
35
45
  min={0.01}
@@ -38,7 +48,7 @@ function CameraComponentEditor({ component, onUpdate }: { component: any; onUpda
38
48
  <NumberField
39
49
  name="far"
40
50
  label="Far"
41
- values={component.properties}
51
+ values={values}
42
52
  onChange={onUpdate}
43
53
  fallback={1000}
44
54
  min={0.1}
@@ -49,17 +59,44 @@ function CameraComponentEditor({ component, onUpdate }: { component: any; onUpda
49
59
  }
50
60
 
51
61
  function CameraComponentView({ properties, editMode, isSelected }: { properties: any; editMode?: boolean; isSelected?: boolean }) {
52
- const fov = properties?.fov ?? 50;
53
- const near = properties?.near ?? 0.1;
54
- const zoom = properties?.zoom ?? 1;
55
- const far = properties?.far ?? 1000;
56
- const cameraRef = useRef<ThreePerspectiveCamera>(null);
62
+ const merged = { ...cameraDefaults, ...properties };
63
+ const fov = merged.fov;
64
+ const near = merged.near;
65
+ const zoom = merged.zoom;
66
+ const far = merged.far;
67
+ const [camera, setCamera] = useState<ThreePerspectiveCamera | null>(null);
68
+ const cameraHelper = useMemo(
69
+ () => camera ? new CameraHelper(camera) : null,
70
+ [camera]
71
+ );
72
+
73
+ useEffect(() => {
74
+ return () => {
75
+ cameraHelper?.dispose();
76
+ };
77
+ }, [cameraHelper]);
57
78
 
58
- useHelper(editMode && isSelected ? (cameraRef as React.RefObject<ThreePerspectiveCamera>) : null, CameraHelper);
79
+ useFrame(() => {
80
+ if (camera && cameraHelper && editMode && isSelected) {
81
+ camera.updateProjectionMatrix();
82
+ camera.updateMatrixWorld();
83
+ cameraHelper.update();
84
+ }
85
+ });
59
86
 
60
87
  return (
61
88
  <>
62
- <PerspectiveCamera ref={cameraRef} makeDefault={!editMode} fov={fov} near={near} zoom={zoom} far={far} />
89
+ <PerspectiveCamera
90
+ ref={(instance) => setCamera(instance)}
91
+ makeDefault={!editMode}
92
+ fov={fov}
93
+ near={near}
94
+ zoom={zoom}
95
+ far={far}
96
+ />
97
+ {editMode && isSelected && cameraHelper && (
98
+ <primitive object={cameraHelper} />
99
+ )}
63
100
  {editMode && !isSelected ? (
64
101
  <mesh>
65
102
  <boxGeometry args={[0.34, 0.22, 0.18]} />
@@ -74,7 +111,7 @@ const CameraComponent: Component = {
74
111
  name: 'Camera',
75
112
  Editor: CameraComponentEditor,
76
113
  View: CameraComponentView,
77
- defaultProperties: {},
114
+ defaultProperties: cameraDefaults,
78
115
  };
79
116
 
80
117
  export default CameraComponent;
@@ -1,11 +1,25 @@
1
1
  import { Component } from "./ComponentRegistry";
2
- import { useRef, useEffect } from "react";
2
+ import { useRef, useEffect, useMemo, useState } from "react";
3
3
  import { useFrame } from "@react-three/fiber";
4
- import { DirectionalLight, Object3D, Vector3 } from "three";
4
+ import { CameraHelper, DirectionalLight, Object3D, OrthographicCamera, Vector3 } from "three";
5
5
  import { FieldRenderer, FieldDefinition, Input } from "./Input";
6
6
 
7
7
  const smallLabel = { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 } as const;
8
8
 
9
+ const directionalLightDefaults = {
10
+ color: '#ffffff',
11
+ intensity: 1,
12
+ castShadow: true,
13
+ shadowMapSize: 1024,
14
+ shadowCameraNear: 0.1,
15
+ shadowCameraFar: 100,
16
+ shadowCameraTop: 30,
17
+ shadowCameraBottom: -30,
18
+ shadowCameraLeft: -30,
19
+ shadowCameraRight: 30,
20
+ targetOffset: [0, -5, 0],
21
+ };
22
+
9
23
  const directionalLightFields: FieldDefinition[] = [
10
24
  { name: 'color', type: 'color', label: 'Color' },
11
25
  { name: 'intensity', type: 'number', label: 'Intensity', step: 0.1, min: 0 },
@@ -71,51 +85,66 @@ const directionalLightFields: FieldDefinition[] = [
71
85
  ];
72
86
 
73
87
  function DirectionalLightComponentEditor({ component, onUpdate }: { component: any; onUpdate: (newComp: any) => void }) {
88
+ const values = { ...directionalLightDefaults, ...component.properties };
89
+ const fields = values.castShadow
90
+ ? directionalLightFields
91
+ : directionalLightFields.filter(field => field.name !== '_shadowCamera');
92
+
74
93
  return (
75
94
  <FieldRenderer
76
- fields={directionalLightFields}
77
- values={component.properties}
95
+ fields={fields}
96
+ values={values}
78
97
  onChange={onUpdate}
79
98
  />
80
99
  );
81
100
  }
82
101
 
83
102
  function DirectionalLightView({ properties, editMode, isSelected }: { properties: any; editMode?: boolean; isSelected?: boolean }) {
84
- const color = properties.color ?? '#ffffff';
85
- const intensity = properties.intensity ?? 1.0;
86
- const castShadow = properties.castShadow ?? true;
87
- const shadowMapSize = properties.shadowMapSize ?? 1024;
88
- const shadowCameraNear = properties.shadowCameraNear ?? 0.1;
89
- const shadowCameraFar = properties.shadowCameraFar ?? 100;
90
- const shadowCameraTop = properties.shadowCameraTop ?? 30;
91
- const shadowCameraBottom = properties.shadowCameraBottom ?? -30;
92
- const shadowCameraLeft = properties.shadowCameraLeft ?? -30;
93
- const shadowCameraRight = properties.shadowCameraRight ?? 30;
94
- const targetOffset = properties.targetOffset ?? [0, -5, 0];
103
+ const merged = { ...directionalLightDefaults, ...properties };
104
+ const color = merged.color;
105
+ const intensity = merged.intensity;
106
+ const castShadow = merged.castShadow;
107
+ const shadowMapSize = merged.shadowMapSize;
108
+ const shadowCameraNear = merged.shadowCameraNear;
109
+ const shadowCameraFar = merged.shadowCameraFar;
110
+ const shadowCameraTop = merged.shadowCameraTop;
111
+ const shadowCameraBottom = merged.shadowCameraBottom;
112
+ const shadowCameraLeft = merged.shadowCameraLeft;
113
+ const shadowCameraRight = merged.shadowCameraRight;
114
+ const targetOffset = merged.targetOffset;
95
115
 
96
116
  const directionalLightRef = useRef<DirectionalLight>(null);
97
117
  const targetRef = useRef<Object3D>(null);
118
+ const [shadowCamera, setShadowCamera] = useState<OrthographicCamera | null>(null);
119
+ const shadowCameraHelper = useMemo(
120
+ () => shadowCamera ? new CameraHelper(shadowCamera) : null,
121
+ [shadowCamera]
122
+ );
98
123
 
99
- // Set up light target reference when both refs are ready
124
+ useEffect(() => {
125
+ return () => {
126
+ shadowCameraHelper?.dispose();
127
+ };
128
+ }, [shadowCameraHelper]);
129
+
130
+ // Use a local target object so node transforms rotate the light direction naturally.
100
131
  useEffect(() => {
101
132
  if (directionalLightRef.current && targetRef.current) {
102
133
  directionalLightRef.current.target = targetRef.current;
134
+ setShadowCamera(directionalLightRef.current.shadow.camera);
103
135
  }
104
136
  }, []);
105
137
 
106
- // Update target world position based on light position + offset
107
138
  useFrame(() => {
108
139
  if (!directionalLightRef.current || !targetRef.current) return;
109
140
 
110
- const lightWorldPos = new Vector3();
111
- directionalLightRef.current.getWorldPosition(lightWorldPos);
141
+ directionalLightRef.current.target.updateMatrixWorld();
112
142
 
113
- // Target is positioned relative to light's world position
114
- targetRef.current.position.set(
115
- lightWorldPos.x + targetOffset[0],
116
- lightWorldPos.y + targetOffset[1],
117
- lightWorldPos.z + targetOffset[2]
118
- );
143
+ if (shadowCamera && shadowCameraHelper && castShadow) {
144
+ shadowCamera.updateProjectionMatrix();
145
+ shadowCamera.updateMatrixWorld();
146
+ shadowCameraHelper.update();
147
+ }
119
148
  });
120
149
 
121
150
  return (
@@ -136,8 +165,10 @@ function DirectionalLightView({ properties, editMode, isSelected }: { properties
136
165
  shadow-bias={-0.001}
137
166
  shadow-normalBias={0.02}
138
167
  />
139
- {/* Target object - rendered declaratively in scene graph */}
140
- <object3D ref={targetRef} />
168
+ <object3D ref={targetRef} position={targetOffset as [number, number, number]} />
169
+ {editMode && isSelected && castShadow && shadowCameraHelper && (
170
+ <primitive object={shadowCameraHelper} />
171
+ )}
141
172
  {editMode && isSelected && (
142
173
  <>
143
174
  {/* Light source indicator */}
@@ -173,7 +204,7 @@ const DirectionalLightComponent: Component = {
173
204
  name: 'DirectionalLight',
174
205
  Editor: DirectionalLightComponentEditor,
175
206
  View: DirectionalLightView,
176
- defaultProperties: {}
207
+ defaultProperties: directionalLightDefaults
177
208
  };
178
209
 
179
210
  export default DirectionalLightComponent;
@@ -1,10 +1,13 @@
1
1
  import { SingleTextureViewer, TextureListViewer } from '../../assetviewer/page';
2
+ import { extend } from '@react-three/fiber';
3
+ import type { ThreeElement } from '@react-three/fiber';
2
4
  import { useEffect, useLayoutEffect, useRef, useState } from 'react';
3
5
  import { createPortal } from 'react-dom';
4
6
  import { Component } from './ComponentRegistry';
5
7
  import { FieldRenderer, FieldDefinition, Input } from './Input';
6
8
  import { colors } from '../styles';
7
9
  import { useMemo } from 'react';
10
+ import { MeshBasicNodeMaterial, MeshStandardNodeMaterial } from 'three/webgpu';
8
11
  import {
9
12
  RepeatWrapping,
10
13
  ClampToEdgeWrapping,
@@ -27,6 +30,13 @@ import {
27
30
  DoubleSide,
28
31
  } from 'three';
29
32
 
33
+ declare module '@react-three/fiber' {
34
+ interface ThreeElements {
35
+ meshBasicNodeMaterial: ThreeElement<typeof MeshBasicNodeMaterial>;
36
+ meshStandardNodeMaterial: ThreeElement<typeof MeshStandardNodeMaterial>;
37
+ }
38
+ }
39
+
30
40
  export interface MaterialProps extends Omit<MeshStandardMaterialProperties & MeshBasicMaterialProperties, 'args' | 'normalScale'> {
31
41
  materialType?: 'standard' | 'basic';
32
42
  transmission?: number;
@@ -44,6 +54,10 @@ export interface MaterialProps extends Omit<MeshStandardMaterialProperties & Mes
44
54
 
45
55
  const PICKER_POPUP_WIDTH = 260;
46
56
  const PICKER_POPUP_HEIGHT = 360;
57
+ extend({
58
+ MeshBasicNodeMaterial,
59
+ MeshStandardNodeMaterial,
60
+ });
47
61
 
48
62
  function TexturePicker({
49
63
  value,
@@ -321,11 +335,6 @@ function MaterialComponentView({ properties, loadedTextures }: { properties: Mat
321
335
  normalScale: _normalScale,
322
336
  normalMap: _normalMap,
323
337
  side: sideProp,
324
- metalness: _metalness,
325
- roughness: _roughness,
326
- transmission: _transmission,
327
- thickness: _thickness,
328
- ior: _ior,
329
338
  ...materialProps
330
339
  } = materialSource;
331
340
 
@@ -378,10 +387,10 @@ function MaterialComponentView({ properties, loadedTextures }: { properties: Mat
378
387
  }, [finalNormalMap, normalScaleProp?.[0], normalScaleProp?.[1]]);
379
388
 
380
389
  if (!properties) {
381
- return <meshStandardMaterial color="red" wireframe />;
390
+ return <meshStandardNodeMaterial color="red" wireframe />;
382
391
  }
383
392
 
384
- const materialKey = finalTexture?.uuid ?? 'no-texture';
393
+ const materialKey = `${finalTexture?.uuid ?? 'no-texture'}:${materialProps.transparent ? 'transparent' : 'opaque'}`;
385
394
  const sharedProps = {
386
395
  map: finalTexture,
387
396
  side: resolvedSide,
@@ -389,11 +398,11 @@ function MaterialComponentView({ properties, loadedTextures }: { properties: Mat
389
398
  };
390
399
 
391
400
  if (materialType === 'basic') {
392
- return <meshBasicMaterial key={materialKey} {...sharedProps} />;
401
+ return <meshBasicNodeMaterial key={materialKey} {...sharedProps} />;
393
402
  }
394
403
 
395
404
  return (
396
- <meshStandardMaterial
405
+ <meshStandardNodeMaterial
397
406
  key={materialKey}
398
407
  {...sharedProps}
399
408
  normalMap={finalNormalMap}