react-three-game 0.0.70 → 0.0.72

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 (34) hide show
  1. package/dist/index.d.ts +9 -5
  2. package/dist/index.js +5 -2
  3. package/dist/tools/prefabeditor/EditorTree.js +2 -12
  4. package/dist/tools/prefabeditor/EditorTreeMenus.js +1 -19
  5. package/dist/tools/prefabeditor/EditorUI.js +2 -1
  6. package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -3
  7. package/dist/tools/prefabeditor/PrefabEditor.js +23 -42
  8. package/dist/tools/prefabeditor/PrefabRoot.d.ts +39 -13
  9. package/dist/tools/prefabeditor/PrefabRoot.js +105 -49
  10. package/dist/tools/prefabeditor/components/AmbientLightComponent.js +10 -7
  11. package/dist/tools/prefabeditor/components/CameraComponent.js +11 -15
  12. package/dist/tools/prefabeditor/components/ClickComponent.js +5 -1
  13. package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +15 -1
  14. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +127 -53
  15. package/dist/tools/prefabeditor/components/EnvironmentComponent.js +5 -3
  16. package/dist/tools/prefabeditor/components/MaterialComponent.js +9 -6
  17. package/dist/tools/prefabeditor/components/ModelComponent.js +4 -2
  18. package/dist/tools/prefabeditor/components/PhysicsComponent.js +6 -3
  19. package/dist/tools/prefabeditor/components/PointLightComponent.d.ts +3 -0
  20. package/dist/tools/prefabeditor/components/PointLightComponent.js +57 -0
  21. package/dist/tools/prefabeditor/components/SoundComponent.js +21 -16
  22. package/dist/tools/prefabeditor/components/SpotLightComponent.js +49 -24
  23. package/dist/tools/prefabeditor/components/index.js +2 -0
  24. package/dist/tools/prefabeditor/components/lightUtils.d.ts +13 -0
  25. package/dist/tools/prefabeditor/components/lightUtils.js +64 -0
  26. package/dist/tools/prefabeditor/prefab.d.ts +37 -0
  27. package/dist/tools/prefabeditor/prefab.js +229 -0
  28. package/dist/tools/prefabeditor/prefabStore.d.ts +3 -16
  29. package/dist/tools/prefabeditor/prefabStore.js +29 -168
  30. package/dist/tools/prefabeditor/{sceneApi.d.ts → scene.d.ts} +6 -2
  31. package/dist/tools/prefabeditor/{sceneApi.js → scene.js} +13 -19
  32. package/dist/tools/prefabeditor/utils.d.ts +0 -4
  33. package/dist/tools/prefabeditor/utils.js +0 -37
  34. package/package.json +1 -1
@@ -11,7 +11,7 @@ var __rest = (this && this.__rest) || function (s, e) {
11
11
  };
12
12
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
13
13
  import { useHelper } from "@react-three/drei";
14
- import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
14
+ import { forwardRef, createContext, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
15
15
  import { BoxHelper, Euler, Matrix4, } from "three";
16
16
  import { useStore } from "zustand";
17
17
  import { useClickValid } from "./useClickValid";
@@ -22,13 +22,62 @@ import { loadModel, loadSound, loadTexture } from "../dragdrop";
22
22
  import { GameInstance, GameInstanceProvider, getRepeatAxesFromModelProperties, useInstanceCheck } from "./InstanceProvider";
23
23
  import { composeTransform, decompose } from "./utils";
24
24
  import { isPhysicsProps } from "./components/PhysicsComponent";
25
- import { createPrefabStore, PrefabStoreProvider, prefabStoreToPrefab, useOptionalPrefabStoreApi, usePrefabChildIds, usePrefabNode, usePrefabRootId } from "./prefabStore";
25
+ import { denormalizePrefab } from "./prefab";
26
+ import { createPrefabStore, PrefabStoreProvider, useOptionalPrefabStoreApi, usePrefabChildIds, usePrefabNode, usePrefabRootId } from "./prefabStore";
26
27
  import { sound as soundManager } from "../../helpers/SoundManager";
27
28
  components.forEach(registerComponent);
28
29
  const IDENTITY = new Matrix4();
29
30
  const EMPTY_MODELS = {};
30
31
  const EMPTY_TEXTURES = {};
31
32
  const EMPTY_SOUNDS = {};
33
+ const AssetRuntimeContext = createContext(null);
34
+ const EntityRuntimeContext = createContext(null);
35
+ /** Access scene-wide shared services from within a component View. */
36
+ export function useAssetRuntime() {
37
+ const ctx = useContext(AssetRuntimeContext);
38
+ if (!ctx)
39
+ throw new Error("useAssetRuntime must be used inside <PrefabRoot>");
40
+ return ctx;
41
+ }
42
+ /** Access the current node's runtime from within a component View. */
43
+ export function useEntityRuntime() {
44
+ const ctx = useContext(EntityRuntimeContext);
45
+ if (!ctx)
46
+ throw new Error("useEntityRuntime must be used inside a component View rendered by <PrefabRoot>");
47
+ return ctx;
48
+ }
49
+ /** Read the current component's Object3D through a live ref-like accessor. */
50
+ export function useEntityObjectRef() {
51
+ const { getObject } = useEntityRuntime();
52
+ return useMemo(() => ({
53
+ get current() {
54
+ return getObject();
55
+ },
56
+ }), [getObject]);
57
+ }
58
+ /** Read the current component's rigid body through a live ref-like accessor. */
59
+ export function useEntityRigidBodyRef() {
60
+ const { getRigidBody } = useEntityRuntime();
61
+ return useMemo(() => ({
62
+ get current() {
63
+ return getRigidBody();
64
+ },
65
+ }), [getRigidBody]);
66
+ }
67
+ function EntityRuntimeScope({ nodeId, editMode, isSelected, children, }) {
68
+ const assetRuntime = useContext(AssetRuntimeContext);
69
+ if (!assetRuntime)
70
+ throw new Error("EntityRuntimeScope must be used inside <PrefabRoot>");
71
+ const { getObject, getRigidBody } = assetRuntime;
72
+ const value = useMemo(() => ({
73
+ nodeId,
74
+ editMode,
75
+ isSelected,
76
+ getObject: () => getObject(nodeId),
77
+ getRigidBody: () => getRigidBody(nodeId),
78
+ }), [editMode, getObject, getRigidBody, isSelected, nodeId]);
79
+ return _jsx(EntityRuntimeContext.Provider, { value: value, children: children });
80
+ }
32
81
  /** Resolve a relative or absolute asset file path against a base path. */
33
82
  function resolveAssetPath(basePath, file) {
34
83
  if (file.startsWith("http://") || file.startsWith("https://"))
@@ -43,11 +92,14 @@ function isNodeReady(node, loadedModels) {
43
92
  return true;
44
93
  return Boolean(loadedModels[model.properties.filename]);
45
94
  }
46
- export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSelect, onClick, onSelectedObjectChange, onFocusNode, basePath = "", injectedModels = EMPTY_MODELS, injectedTextures = EMPTY_TEXTURES, injectedSounds = EMPTY_SOUNDS }, ref) => {
95
+ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSelect, onClick, basePath = "" }, ref) => {
47
96
  var _a;
48
97
  const [models, setModels] = useState({});
49
98
  const [textures, setTextures] = useState({});
50
99
  const [sounds, setSounds] = useState({});
100
+ const [injectedModels, setInjectedModels] = useState(EMPTY_MODELS);
101
+ const [injectedTextures, setInjectedTextures] = useState(EMPTY_TEXTURES);
102
+ const [injectedSounds, setInjectedSounds] = useState(EMPTY_SOUNDS);
51
103
  const loading = useRef(new Set());
52
104
  const failedModels = useRef(new Set());
53
105
  const failedTextures = useRef(new Set());
@@ -56,7 +108,7 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
56
108
  const rigidBodyRefs = useRef(new Map());
57
109
  const rootRef = useRef(null);
58
110
  const parentStore = useOptionalPrefabStoreApi();
59
- const [ownedStore] = useState(() => { var _a, _b; return createPrefabStore(data !== null && data !== void 0 ? data : prefabStoreToPrefab((_b = (_a = store === null || store === void 0 ? void 0 : store.getState()) !== null && _a !== void 0 ? _a : parentStore === null || parentStore === void 0 ? void 0 : parentStore.getState()) !== null && _b !== void 0 ? _b : missingStoreState())); });
111
+ const [ownedStore] = useState(() => { var _a, _b; return createPrefabStore(data !== null && data !== void 0 ? data : denormalizePrefab((_b = (_a = store === null || store === void 0 ? void 0 : store.getState()) !== null && _a !== void 0 ? _a : parentStore === null || parentStore === void 0 ? void 0 : parentStore.getState()) !== null && _b !== void 0 ? _b : missingStoreState())); });
60
112
  const resolvedStore = (_a = store !== null && store !== void 0 ? store : parentStore) !== null && _a !== void 0 ? _a : ownedStore;
61
113
  const usesOwnedStore = resolvedStore === ownedStore;
62
114
  const shouldProvideStoreContext = !parentStore || parentStore !== resolvedStore;
@@ -64,19 +116,24 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
64
116
  const availableModels = useMemo(() => (Object.assign(Object.assign({}, models), injectedModels)), [models, injectedModels]);
65
117
  const availableTextures = useMemo(() => (Object.assign(Object.assign({}, textures), injectedTextures)), [textures, injectedTextures]);
66
118
  const availableSounds = useMemo(() => (Object.assign(Object.assign({}, sounds), injectedSounds)), [sounds, injectedSounds]);
119
+ const getObject = useCallback((id) => {
120
+ var _a;
121
+ return (_a = objectRefs.current[id]) !== null && _a !== void 0 ? _a : null;
122
+ }, []);
67
123
  useImperativeHandle(ref, () => ({
68
124
  root: rootRef.current,
69
- rigidBodyRefs: rigidBodyRefs.current,
70
- getObject: (nodeId) => { var _a; return (_a = objectRefs.current[nodeId]) !== null && _a !== void 0 ? _a : null; },
125
+ getObject,
71
126
  getRigidBody: (nodeId) => { var _a; return (_a = rigidBodyRefs.current.get(nodeId)) !== null && _a !== void 0 ? _a : null; },
72
- focusNode: (nodeId) => onFocusNode === null || onFocusNode === void 0 ? void 0 : onFocusNode(nodeId),
73
- }), [onFocusNode]);
127
+ addModel: (path, model) => setInjectedModels(prev => (Object.assign(Object.assign({}, prev), { [path]: model }))),
128
+ addTexture: (path, texture) => setInjectedTextures(prev => (Object.assign(Object.assign({}, prev), { [path]: texture }))),
129
+ addSound: (path, sound) => {
130
+ soundManager.setBuffer(path, sound);
131
+ setInjectedSounds(prev => (Object.assign(Object.assign({}, prev), { [path]: sound })));
132
+ },
133
+ }), [getObject]);
74
134
  const registerRef = useCallback((id, obj) => {
75
135
  objectRefs.current[id] = obj;
76
- if (id === selectedId) {
77
- onSelectedObjectChange === null || onSelectedObjectChange === void 0 ? void 0 : onSelectedObjectChange(obj);
78
- }
79
- }, [onSelectedObjectChange, selectedId]);
136
+ }, []);
80
137
  const registerRigidBodyRef = useCallback((id, rb) => {
81
138
  rigidBodyRefs.current.set(id, rb);
82
139
  }, []);
@@ -155,11 +212,31 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
155
212
  };
156
213
  syncAssets();
157
214
  }, [resolvedStore, assetManifestKey, basePath, injectedModels, injectedSounds, injectedTextures, models, sounds, textures]);
158
- const content = (_jsx("group", { ref: rootRef, children: _jsx(GameInstanceProvider, { models: availableModels, selectedId: selectedId, editMode: editMode, onSelect: editMode ? onSelect : undefined, registerRef: registerRef, children: _jsx(StoreRootNode, { selectedId: selectedId, onSelect: editMode ? onSelect : undefined, onClick: onClick, registerRef: registerRef, registerRigidBodyRef: registerRigidBodyRef, getRigidBody: getRigidBody, loadedModels: availableModels, loadedSounds: availableSounds, loadedTextures: availableTextures, editMode: editMode, parentMatrix: IDENTITY }) }) }));
215
+ // Keep refs current so context getters are always fresh without changing context identity
216
+ const availableModelsRef = useRef(availableModels);
217
+ availableModelsRef.current = availableModels;
218
+ const availableTexturesRef = useRef(availableTextures);
219
+ availableTexturesRef.current = availableTextures;
220
+ const availableSoundsRef = useRef(availableSounds);
221
+ availableSoundsRef.current = availableSounds;
222
+ const assetRuntime = useMemo(() => ({
223
+ getObject,
224
+ getRigidBody,
225
+ registerRigidBodyRef,
226
+ getModel: (path) => { var _a; return (_a = availableModelsRef.current[path]) !== null && _a !== void 0 ? _a : null; },
227
+ getTexture: (path) => { var _a; return (_a = availableTexturesRef.current[path]) !== null && _a !== void 0 ? _a : null; },
228
+ getSound: (path) => { var _a; return (_a = availableSoundsRef.current[path]) !== null && _a !== void 0 ? _a : null; },
229
+ getAssetRevision: () => {
230
+ const modelKeys = Object.keys(availableModelsRef.current).sort().join('|');
231
+ const textureKeys = Object.keys(availableTexturesRef.current).sort().join('|');
232
+ return `${textureKeys}::${modelKeys}`;
233
+ },
234
+ }), [getObject, getRigidBody, registerRigidBodyRef]);
235
+ const content = (_jsx("group", { ref: rootRef, children: _jsx(GameInstanceProvider, { models: availableModels, selectedId: selectedId, editMode: editMode, onSelect: editMode ? onSelect : undefined, registerRef: registerRef, children: _jsx(StoreRootNode, { selectedId: selectedId, onSelect: editMode ? onSelect : undefined, onClick: onClick, registerRef: registerRef, loadedModels: availableModels, editMode: editMode, parentMatrix: IDENTITY }) }) }));
159
236
  if (!shouldProvideStoreContext) {
160
- return content;
237
+ return _jsx(AssetRuntimeContext.Provider, { value: assetRuntime, children: content });
161
238
  }
162
- return _jsx(PrefabStoreProvider, { store: resolvedStore, children: content });
239
+ return _jsx(PrefabStoreProvider, { store: resolvedStore, children: _jsx(AssetRuntimeContext.Provider, { value: assetRuntime, children: content }) });
163
240
  });
164
241
  function StoreRootNode(props) {
165
242
  const rootId = usePrefabRootId();
@@ -218,7 +295,7 @@ function InstancedNode({ nodeId, parentMatrix = IDENTITY, editMode, registerRef,
218
295
  }
219
296
  return (_jsx(_Fragment, { children: instances.map(instance => (_jsx(GameInstance, { id: instance.id, sourceId: gameObject.id, clickable: clickable, clickEventName: clickEventName, modelUrl: instance.modelUrl, position: instance.position, rotation: instance.rotation, scale: instance.scale, locked: isLocked, physics: instance.physics }, instance.id))) }));
220
297
  }
221
- function StandardNode({ nodeId, selectedId, onSelect, onClick, registerRef, registerRigidBodyRef, getRigidBody, loadedModels, loadedSounds, loadedTextures, editMode, parentMatrix = IDENTITY, }) {
298
+ function StandardNode({ nodeId, selectedId, onSelect, onClick, registerRef, loadedModels, editMode, parentMatrix = IDENTITY, }) {
222
299
  var _a, _b;
223
300
  const gameObject = usePrefabNode(nodeId);
224
301
  const childIds = usePrefabChildIds(nodeId);
@@ -246,18 +323,12 @@ function StandardNode({ nodeId, selectedId, onSelect, onClick, registerRef, regi
246
323
  const physicsDef = hasPhysics ? getComponentDef(physics.type) : null;
247
324
  const isInstanced = (_b = (_a = findComponent(gameObject, "Model")) === null || _a === void 0 ? void 0 : _a.properties) === null || _b === void 0 ? void 0 : _b.instanced;
248
325
  const physicsKey = `physics_${nodeId}_${isInstanced ? 'instanced' : 'standard'}`;
249
- const renderCtx = { loadedModels, loadedSounds, loadedTextures, editMode, registerRef, getRigidBody };
326
+ const renderCtx = { loadedModels, editMode, registerRef };
250
327
  const childNodes = getChildHostComponents(gameObject).length > 0
251
328
  ? _jsx(CompositionChildren, { childIds: childIds, selectedId: selectedId, ctx: renderCtx, parentMatrix: world })
252
- : _jsx(ChildNodes, { childIds: childIds, parentMatrix: world, selectedId: selectedId, onSelect: onSelect, onClick: onClick, registerRef: registerRef, registerRigidBodyRef: registerRigidBodyRef, getRigidBody: getRigidBody, loadedModels: loadedModels, loadedSounds: loadedSounds, loadedTextures: loadedTextures, editMode: editMode });
253
- const inner = (_jsx("group", Object.assign({}, clickHandlers, { children: renderCompositionNode(gameObject, renderCtx, isSelected, parentMatrix, childNodes) })));
254
- if (editMode) {
255
- return (_jsxs(_Fragment, { children: [_jsx("group", { ref: groupRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: _jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) }) }), _jsx("group", { ref: helperRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: inner }), hasPhysics && (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View) ? (_jsx(physicsDef.View, { properties: physics.properties, position: transform.position, rotation: transform.rotation, scale: transform.scale, editMode: editMode, nodeId: nodeId, registerRigidBodyRef: registerRigidBodyRef, children: inner }, physicsKey)) : null] }));
256
- }
257
- if (hasPhysics && (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View)) {
258
- return (_jsx(physicsDef.View, { properties: physics.properties, position: transform.position, rotation: transform.rotation, scale: transform.scale, editMode: editMode, nodeId: nodeId, registerRigidBodyRef: registerRigidBodyRef, children: inner }, physicsKey));
259
- }
260
- return (_jsx("group", { ref: groupRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: inner }));
329
+ : _jsx(ChildNodes, { childIds: childIds, parentMatrix: world, selectedId: selectedId, onSelect: onSelect, onClick: onClick, registerRef: registerRef, loadedModels: loadedModels, editMode: editMode });
330
+ const inner = (_jsx("group", Object.assign({}, clickHandlers, { children: renderCompositionNode(gameObject, renderCtx, childNodes) })));
331
+ return (_jsx(EntityRuntimeScope, { nodeId: nodeId, editMode: editMode, isSelected: isSelected, children: editMode ? (_jsxs(_Fragment, { children: [_jsx("group", { ref: groupRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: _jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) }) }), _jsx("group", { ref: helperRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: inner }), hasPhysics && (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View) ? (_jsx(physicsDef.View, { properties: physics.properties, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: inner }, physicsKey)) : null] })) : hasPhysics && (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View) ? (_jsx(physicsDef.View, { properties: physics.properties, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: inner }, physicsKey)) : (_jsx("group", { ref: groupRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: inner })) }));
261
332
  }
262
333
  function getChildHostComponents(gameObject) {
263
334
  var _a;
@@ -346,7 +417,7 @@ function renderCompositionSubtree(gameObject, ctx, isSelected, childIds, parentM
346
417
  const transform = getNodeTransformProps(gameObject);
347
418
  const world = parentMatrix.clone().multiply(compose(gameObject));
348
419
  const childNodes = _jsx(CompositionChildren, { childIds: childIds, ctx: ctx, parentMatrix: world });
349
- return (_jsx("group", { position: transform.position, rotation: transform.rotation, scale: transform.scale, children: renderCompositionNode(gameObject, ctx, isSelected, parentMatrix, childNodes) }, gameObject.id));
420
+ return (_jsx(EntityRuntimeScope, { nodeId: gameObject.id, editMode: ctx.editMode, isSelected: isSelected, children: _jsx("group", { position: transform.position, rotation: transform.rotation, scale: transform.scale, children: renderCompositionNode(gameObject, ctx, childNodes) }, gameObject.id) }));
350
421
  }
351
422
  function CompositionChildren({ childIds, selectedId, ctx, parentMatrix, }) {
352
423
  return childIds.map(childId => (_jsx(CompositionSubtree, { nodeId: childId, selectedId: selectedId, ctx: ctx, parentMatrix: parentMatrix }, childId)));
@@ -359,37 +430,22 @@ function CompositionSubtree({ nodeId, selectedId, ctx, parentMatrix, }) {
359
430
  return null;
360
431
  return renderCompositionSubtree(gameObject, ctx, isSelected, childIds, parentMatrix);
361
432
  }
362
- function renderCompositionNode(gameObject, ctx, isSelected, parentMatrix, childNodes) {
363
- const ownContent = renderNodeOwnContent(gameObject, ctx, isSelected, parentMatrix);
364
- return wrapWithChildHosts(gameObject, ctx, isSelected, parentMatrix, _jsxs(_Fragment, { children: [ownContent, childNodes] }));
365
- }
366
- function buildContextProps(gameObject, ctx, isSelected, parentMatrix) {
367
- return {
368
- loadedModels: ctx.loadedModels,
369
- loadedSounds: ctx.loadedSounds,
370
- loadedTextures: ctx.loadedTextures,
371
- editMode: ctx.editMode,
372
- isSelected,
373
- nodeId: gameObject.id,
374
- parentMatrix,
375
- registerRef: ctx.registerRef,
376
- getRigidBody: ctx.getRigidBody,
377
- };
433
+ function renderCompositionNode(gameObject, ctx, childNodes) {
434
+ const ownContent = renderNodeOwnContent(gameObject);
435
+ return wrapWithChildHosts(gameObject, _jsxs(_Fragment, { children: [ownContent, childNodes] }));
378
436
  }
379
- function renderNodeOwnContent(gameObject, ctx, isSelected, parentMatrix) {
437
+ function renderNodeOwnContent(gameObject) {
380
438
  const geometry = findComponent(gameObject, "Geometry");
381
439
  const material = findComponent(gameObject, "Material");
382
440
  const geometryDef = geometry && getComponentDef(geometry.type);
383
441
  const materialDef = material && getComponentDef(material.type);
384
442
  if (!geometry || !(geometryDef === null || geometryDef === void 0 ? void 0 : geometryDef.View))
385
443
  return null;
386
- const contextProps = buildContextProps(gameObject, ctx, isSelected, parentMatrix);
387
- return (_jsxs("mesh", { castShadow: true, receiveShadow: true, children: [_jsx(geometryDef.View, Object.assign({ properties: geometry.properties }, contextProps)), material && (materialDef === null || materialDef === void 0 ? void 0 : materialDef.View) && (_jsx(materialDef.View, Object.assign({ properties: material.properties }, contextProps), "material"))] }));
444
+ return (_jsxs("mesh", { castShadow: true, receiveShadow: true, children: [_jsx(geometryDef.View, { properties: geometry.properties }), material && (materialDef === null || materialDef === void 0 ? void 0 : materialDef.View) && (_jsx(materialDef.View, { properties: material.properties }, "material"))] }));
388
445
  }
389
- function wrapWithChildHosts(gameObject, ctx, isSelected, parentMatrix, subtree) {
390
- const contextProps = buildContextProps(gameObject, ctx, isSelected, parentMatrix);
446
+ function wrapWithChildHosts(gameObject, subtree) {
391
447
  const childHosts = getChildHostComponents(gameObject);
392
- return childHosts.reduce((acc, { key, View, properties }) => (_jsx(View, Object.assign({ properties: properties }, contextProps, { children: acc }), key)), subtree);
448
+ return childHosts.reduce((acc, { key, View, properties }) => (_jsx(View, { properties: properties, children: acc }, key)), subtree);
393
449
  }
394
450
  export default PrefabRoot;
395
451
  function missingStoreState() {
@@ -1,19 +1,22 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { ColorField, FieldGroup, NumberField } from "./Input";
2
+ import { ColorField, NumberField } from "./Input";
3
+ import { LightSection, mergeWithDefaults } from "./lightUtils";
4
+ const ambientLightDefaults = {
5
+ color: '#ffffff',
6
+ intensity: 1,
7
+ };
3
8
  function AmbientLightComponentEditor({ component, onUpdate, }) {
4
- return (_jsxs(FieldGroup, { children: [_jsx(ColorField, { name: "color", label: "Color", values: component.properties, onChange: onUpdate }), _jsx(NumberField, { name: "intensity", label: "Intensity", values: component.properties, onChange: onUpdate, min: 0, step: 0.1, fallback: 1 })] }));
9
+ const values = mergeWithDefaults(ambientLightDefaults, component.properties);
10
+ return (_jsxs(LightSection, { title: "Light", children: [_jsx(ColorField, { name: "color", label: "Color", values: values, onChange: onUpdate }), _jsx(NumberField, { name: "intensity", label: "Intensity", values: values, onChange: onUpdate, min: 0, step: 0.1, fallback: 1 })] }));
5
11
  }
6
12
  function AmbientLightComponentView({ properties, children }) {
7
- const { color = '#ffffff', intensity = 1 } = properties;
13
+ const { color, intensity } = mergeWithDefaults(ambientLightDefaults, properties);
8
14
  return (_jsxs(_Fragment, { children: [_jsx("ambientLight", { color: color, intensity: intensity }), children] }));
9
15
  }
10
16
  const AmbientLightComponent = {
11
17
  name: 'AmbientLight',
12
18
  Editor: AmbientLightComponentEditor,
13
19
  View: AmbientLightComponentView,
14
- defaultProperties: {
15
- color: '#ffffff',
16
- intensity: 1,
17
- },
20
+ defaultProperties: {},
18
21
  };
19
22
  export default AmbientLightComponent;
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { PerspectiveCamera } from '@react-three/drei';
3
- import { useEffect, useMemo, useState } from 'react';
2
+ import { PerspectiveCamera, useHelper } from '@react-three/drei';
3
+ import { useRef } from 'react';
4
4
  import { CameraHelper } from 'three';
5
5
  import { useFrame } from '@react-three/fiber';
6
+ import { useEntityRuntime } from '../PrefabRoot';
6
7
  import { FieldGroup, NumberField } from './Input';
7
8
  const cameraDefaults = {
8
9
  fov: 50,
@@ -14,27 +15,22 @@ function CameraComponentEditor({ component, onUpdate }) {
14
15
  const values = Object.assign(Object.assign({}, cameraDefaults), component.properties);
15
16
  return (_jsxs(FieldGroup, { children: [_jsx(NumberField, { name: "fov", label: "FOV", values: values, onChange: onUpdate, fallback: 50, min: 1, max: 179, step: 1 }), _jsx(NumberField, { name: "near", label: "Near", values: values, onChange: onUpdate, fallback: 0.1, min: 0.001, step: 0.1 }), _jsx(NumberField, { name: "zoom", label: "Zoom", values: values, onChange: onUpdate, fallback: 1, min: 0.01, step: 0.1 }), _jsx(NumberField, { name: "far", label: "Far", values: values, onChange: onUpdate, fallback: 1000, min: 0.1, step: 1 })] }));
16
17
  }
17
- function CameraComponentView({ properties, children, editMode, isSelected }) {
18
+ function CameraComponentView({ properties, children }) {
19
+ const { editMode, isSelected } = useEntityRuntime();
18
20
  const merged = Object.assign(Object.assign({}, cameraDefaults), properties);
19
21
  const fov = merged.fov;
20
22
  const near = merged.near;
21
23
  const zoom = merged.zoom;
22
24
  const far = merged.far;
23
- const [camera, setCamera] = useState(null);
24
- const cameraHelper = useMemo(() => camera ? new CameraHelper(camera) : null, [camera]);
25
- useEffect(() => {
26
- return () => {
27
- cameraHelper === null || cameraHelper === void 0 ? void 0 : cameraHelper.dispose();
28
- };
29
- }, [cameraHelper]);
25
+ const cameraRef = useRef(null);
26
+ useHelper(editMode && isSelected ? cameraRef : null, CameraHelper);
30
27
  useFrame(() => {
31
- if (camera && cameraHelper && editMode && isSelected) {
32
- camera.updateProjectionMatrix();
33
- camera.updateMatrixWorld();
34
- cameraHelper.update();
28
+ if (cameraRef.current && editMode && isSelected) {
29
+ cameraRef.current.updateProjectionMatrix();
30
+ cameraRef.current.updateMatrixWorld();
35
31
  }
36
32
  });
37
- return (_jsxs(_Fragment, { children: [_jsx(PerspectiveCamera, { ref: (instance) => setCamera(instance), makeDefault: !editMode, fov: fov, near: near, zoom: zoom, far: far }), editMode && isSelected && cameraHelper && (_jsx("primitive", { object: cameraHelper })), editMode ? (_jsxs("group", { children: [_jsxs("mesh", { children: [_jsx("boxGeometry", { args: [0.3, 0.3, 0.5] }), _jsx("meshBasicMaterial", { color: '#22d3ee', wireframe: true })] }), _jsxs("mesh", { position: [0, 0, -0.25], rotation: [Math.PI / 2, 0, 0], children: [_jsx("coneGeometry", { args: [0.08, 0.16, 16] }), _jsx("meshBasicMaterial", { color: '#22d3ee', wireframe: true })] })] })) : null, children] }));
33
+ return (_jsxs(_Fragment, { children: [_jsx(PerspectiveCamera, { ref: cameraRef, makeDefault: !editMode, fov: fov, near: near, zoom: zoom, far: far }), editMode ? (_jsxs("group", { children: [_jsxs("mesh", { children: [_jsx("boxGeometry", { args: [0.3, 0.3, 0.5] }), _jsx("meshBasicMaterial", { color: '#22d3ee', wireframe: true })] }), _jsxs("mesh", { position: [0, 0, -0.25], rotation: [Math.PI / 2, 0, 0], children: [_jsx("coneGeometry", { args: [0.08, 0.16, 16] }), _jsx("meshBasicMaterial", { color: '#22d3ee', wireframe: true })] })] })) : null, children] }));
38
34
  }
39
35
  const CameraComponent = {
40
36
  name: 'Camera',
@@ -1,12 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useRef } from 'react';
3
3
  import { gameEvents } from '../GameEvents';
4
+ import { useEntityRuntime } from '../PrefabRoot';
4
5
  import { FieldGroup, StringField } from './Input';
5
6
  function ClickComponentEditor({ component, onUpdate }) {
6
7
  return (_jsxs(FieldGroup, { children: [_jsx("div", { style: { fontSize: 12, opacity: 0.8 }, children: "Emits a game event in play mode when this entity is clicked." }), _jsx(StringField, { name: "eventName", label: "Emit Event", values: component.properties, onChange: onUpdate, placeholder: "click" })] }));
7
8
  }
8
- function ClickComponentView({ children, editMode, nodeId, properties }) {
9
+ function ClickComponentView({ children, properties }) {
9
10
  const clickValid = useRef(false);
11
+ const { editMode, nodeId } = useEntityRuntime();
10
12
  const eventName = (properties === null || properties === void 0 ? void 0 : properties.eventName) || 'click';
11
13
  const emitClick = (event) => {
12
14
  if (!nodeId)
@@ -27,6 +29,8 @@ function ClickComponentView({ children, editMode, nodeId, properties }) {
27
29
  return (_jsx("group", { onPointerDown: (event) => {
28
30
  event.stopPropagation();
29
31
  clickValid.current = true;
32
+ }, onClick: (event) => {
33
+ event.stopPropagation();
30
34
  }, onPointerMove: () => {
31
35
  clickValid.current = false;
32
36
  }, onPointerUp: (event) => {
@@ -4,6 +4,19 @@ export type AssetRef = {
4
4
  type: "model" | "texture" | "sound";
5
5
  path: string;
6
6
  };
7
+ /** Props every component View receives from the renderer. */
8
+ export interface ComponentViewProps<P = Record<string, any>> {
9
+ /** This component's own data from the prefab JSON. */
10
+ properties: P;
11
+ /** Children to render (for wrapper / child-host components). */
12
+ children?: React.ReactNode;
13
+ /** Entity local position (passed to wrapper components like Physics). */
14
+ position?: [number, number, number];
15
+ /** Entity local rotation in radians (passed to wrapper components like Physics). */
16
+ rotation?: [number, number, number];
17
+ /** Entity local scale (passed to wrapper components like Physics). */
18
+ scale?: [number, number, number];
19
+ }
7
20
  export interface Component {
8
21
  name: string;
9
22
  Editor: FC<{
@@ -13,9 +26,10 @@ export interface Component {
13
26
  basePath?: string;
14
27
  }>;
15
28
  defaultProperties: any;
16
- View?: FC<any>;
29
+ View?: FC<ComponentViewProps>;
17
30
  /** When true, this component wraps child entities (e.g. Physics wraps children in RigidBody). */
18
31
  isWrapper?: boolean;
32
+ /** Declare which asset paths this component references (for asset loading). */
19
33
  getAssetRefs?: (properties: Record<string, any>) => AssetRef[];
20
34
  }
21
35
  export declare function registerComponent(component: Component): void;
@@ -1,59 +1,116 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useRef, useEffect, useMemo, useState } from "react";
2
+ import { useHelper } from "@react-three/drei";
3
+ import { useRef, useEffect, useState } from "react";
3
4
  import { useFrame } from "@react-three/fiber";
4
5
  import { CameraHelper, Vector3 } from "three";
5
- import { FieldRenderer, NumberInput } from "./Input";
6
- const smallLabel = { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 };
6
+ import { useEntityRuntime } from "../PrefabRoot";
7
+ import { BooleanField, ColorField, NumberField, NumberInput, Vector3Input } from "./Input";
8
+ import { LightSection, ShadowBiasField, mergeWithDefaults } from "./lightUtils";
9
+ import { colors } from "../styles";
7
10
  const directionalLightDefaults = {
8
11
  color: '#ffffff',
9
12
  intensity: 1,
10
- castShadow: true,
11
- shadowMapSize: 1024,
12
- shadowCameraNear: 0.1,
13
- shadowCameraFar: 100,
14
- shadowCameraTop: 30,
15
- shadowCameraBottom: -30,
16
- shadowCameraLeft: -30,
17
- shadowCameraRight: 30,
13
+ castShadow: false,
14
+ shadowMapSize: 512,
15
+ shadowBias: 0,
16
+ shadowNormalBias: 0,
17
+ shadowAutoUpdate: true,
18
+ shadowCameraNear: 0.5,
19
+ shadowCameraFar: 500,
20
+ shadowCameraTop: 5,
21
+ shadowCameraBottom: -5,
22
+ shadowCameraLeft: -5,
23
+ shadowCameraRight: 5,
18
24
  targetOffset: [0, -5, 0],
19
25
  };
20
- const directionalLightFields = [
21
- { name: 'color', type: 'color', label: 'Color' },
22
- { name: 'intensity', type: 'number', label: 'Intensity', step: 0.1, min: 0 },
23
- { name: 'castShadow', type: 'boolean', label: 'Cast Shadow' },
24
- { name: 'shadowMapSize', type: 'number', label: 'Shadow Map Size', step: 256, min: 256 },
25
- {
26
- name: '_shadowCamera',
27
- type: 'custom',
28
- label: 'Shadow Camera',
29
- render: ({ values, onChangeMultiple }) => {
30
- var _a, _b, _c, _d, _e, _f;
31
- return (_jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4 }, children: [_jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Near" }), _jsx(NumberInput, { step: 0.1, value: (_a = values.shadowCameraNear) !== null && _a !== void 0 ? _a : 0.1, onChange: v => onChangeMultiple({ shadowCameraNear: v }) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Far" }), _jsx(NumberInput, { step: 1, value: (_b = values.shadowCameraFar) !== null && _b !== void 0 ? _b : 100, onChange: v => onChangeMultiple({ shadowCameraFar: v }) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Top" }), _jsx(NumberInput, { step: 1, value: (_c = values.shadowCameraTop) !== null && _c !== void 0 ? _c : 30, onChange: v => onChangeMultiple({ shadowCameraTop: v }) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Bottom" }), _jsx(NumberInput, { step: 1, value: (_d = values.shadowCameraBottom) !== null && _d !== void 0 ? _d : -30, onChange: v => onChangeMultiple({ shadowCameraBottom: v }) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Left" }), _jsx(NumberInput, { step: 1, value: (_e = values.shadowCameraLeft) !== null && _e !== void 0 ? _e : -30, onChange: v => onChangeMultiple({ shadowCameraLeft: v }) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Right" }), _jsx(NumberInput, { step: 1, value: (_f = values.shadowCameraRight) !== null && _f !== void 0 ? _f : 30, onChange: v => onChangeMultiple({ shadowCameraRight: v }) })] })] }));
32
- },
33
- },
34
- {
35
- name: 'targetOffset',
36
- type: 'custom',
37
- label: 'Target Offset',
38
- render: ({ value, onChange }) => {
39
- const offset = value !== null && value !== void 0 ? value : [0, -5, 0];
40
- return (_jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 4 }, children: [_jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "X" }), _jsx(NumberInput, { step: 0.5, value: offset[0], onChange: v => onChange([v, offset[1], offset[2]]) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Y" }), _jsx(NumberInput, { step: 0.5, value: offset[1], onChange: v => onChange([offset[0], v, offset[2]]) })] }), _jsxs("div", { children: [_jsx("label", { style: smallLabel, children: "Z" }), _jsx(NumberInput, { step: 0.5, value: offset[2], onChange: v => onChange([offset[0], offset[1], v]) })] })] }));
41
- },
42
- },
43
- ];
26
+ const frustumLabelStyle = {
27
+ fontSize: 10,
28
+ textTransform: 'uppercase',
29
+ letterSpacing: '0.06em',
30
+ color: colors.textMuted,
31
+ textAlign: 'center',
32
+ };
33
+ const frustumCellStyle = {
34
+ display: 'flex',
35
+ alignItems: 'center',
36
+ justifyContent: 'center',
37
+ };
38
+ const frustumInputStyle = {
39
+ width: 62,
40
+ minWidth: 62,
41
+ textAlign: 'center',
42
+ };
43
+ const centerLockButtonStyle = {
44
+ width: 34,
45
+ height: 34,
46
+ borderRadius: 999,
47
+ border: `1px solid ${colors.border}`,
48
+ background: colors.bgInput,
49
+ color: colors.textMuted,
50
+ cursor: 'pointer',
51
+ fontSize: 14,
52
+ lineHeight: 1,
53
+ padding: 0,
54
+ };
55
+ function areFrustumSidesLocked(values) {
56
+ const top = Math.abs(values.shadowCameraTop);
57
+ const bottom = Math.abs(values.shadowCameraBottom);
58
+ const left = Math.abs(values.shadowCameraLeft);
59
+ const right = Math.abs(values.shadowCameraRight);
60
+ return top === bottom && top === left && top === right;
61
+ }
62
+ function ShadowFrustumField({ values, onChange, }) {
63
+ const [locked, setLocked] = useState(() => areFrustumSidesLocked(values));
64
+ const updateSide = (side, nextValue) => {
65
+ if (!locked) {
66
+ onChange({ [side]: nextValue });
67
+ return;
68
+ }
69
+ const magnitude = Math.abs(nextValue);
70
+ onChange({
71
+ shadowCameraTop: magnitude,
72
+ shadowCameraBottom: -magnitude,
73
+ shadowCameraLeft: -magnitude,
74
+ shadowCameraRight: magnitude,
75
+ });
76
+ };
77
+ const toggleLocked = () => {
78
+ setLocked(current => {
79
+ const nextLocked = !current;
80
+ if (nextLocked) {
81
+ const magnitude = Math.max(Math.abs(values.shadowCameraTop), Math.abs(values.shadowCameraBottom), Math.abs(values.shadowCameraLeft), Math.abs(values.shadowCameraRight));
82
+ onChange({
83
+ shadowCameraTop: magnitude,
84
+ shadowCameraBottom: -magnitude,
85
+ shadowCameraLeft: -magnitude,
86
+ shadowCameraRight: magnitude,
87
+ });
88
+ }
89
+ return nextLocked;
90
+ });
91
+ };
92
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 6 }, children: [_jsx("div", { style: Object.assign(Object.assign({}, frustumLabelStyle), { textAlign: 'left' }), children: "Shadow Frustum" }), _jsxs("div", { style: {
93
+ display: 'grid',
94
+ gridTemplateColumns: '1fr auto 1fr',
95
+ gridTemplateRows: 'auto auto auto',
96
+ gap: 8,
97
+ alignItems: 'center',
98
+ }, children: [_jsx("div", {}), _jsx("div", { style: frustumCellStyle, children: _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 4, alignItems: 'center' }, children: [_jsx("div", { style: frustumLabelStyle, children: "Top" }), _jsx(NumberInput, { value: values.shadowCameraTop, onChange: nextValue => updateSide('shadowCameraTop', nextValue), step: 0.5, style: frustumInputStyle })] }) }), _jsx("div", {}), _jsx("div", { style: frustumCellStyle, children: _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 4, alignItems: 'center' }, children: [_jsx("div", { style: frustumLabelStyle, children: "Left" }), _jsx(NumberInput, { value: values.shadowCameraLeft, onChange: nextValue => updateSide('shadowCameraLeft', nextValue), step: 0.5, style: frustumInputStyle })] }) }), _jsx("div", { style: frustumCellStyle, children: _jsx("button", { type: "button", onClick: toggleLocked, style: Object.assign(Object.assign({}, centerLockButtonStyle), { color: locked ? colors.accent : colors.textMuted, borderColor: locked ? colors.accentBorder : colors.border, background: locked ? colors.accentBg : colors.bgInput }), title: locked ? 'Frustum sides locked' : 'Frustum sides unlocked', children: locked ? '🔒' : '🔓' }) }), _jsx("div", { style: frustumCellStyle, children: _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 4, alignItems: 'center' }, children: [_jsx("div", { style: frustumLabelStyle, children: "Right" }), _jsx(NumberInput, { value: values.shadowCameraRight, onChange: nextValue => updateSide('shadowCameraRight', nextValue), step: 0.5, style: frustumInputStyle })] }) }), _jsx("div", {}), _jsx("div", { style: frustumCellStyle, children: _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 4, alignItems: 'center' }, children: [_jsx("div", { style: frustumLabelStyle, children: "Bottom" }), _jsx(NumberInput, { value: values.shadowCameraBottom, onChange: nextValue => updateSide('shadowCameraBottom', nextValue), step: 0.5, style: frustumInputStyle })] }) }), _jsx("div", {})] })] }));
99
+ }
44
100
  function DirectionalLightComponentEditor({ component, onUpdate }) {
45
- const values = Object.assign(Object.assign({}, directionalLightDefaults), component.properties);
46
- const fields = values.castShadow
47
- ? directionalLightFields
48
- : directionalLightFields.filter(field => field.name !== '_shadowCamera');
49
- return (_jsx(FieldRenderer, { fields: fields, values: values, onChange: onUpdate }));
101
+ const values = mergeWithDefaults(directionalLightDefaults, component.properties);
102
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 8 }, children: [_jsxs(LightSection, { title: "Light", children: [_jsx(ColorField, { name: "color", label: "Color", values: values, onChange: onUpdate }), _jsx(NumberField, { name: "intensity", label: "Intensity", values: values, onChange: onUpdate, min: 0, step: 0.1, fallback: 1 }), _jsx(Vector3Input, { label: "Target Offset", value: values.targetOffset, onChange: targetOffset => onUpdate({ targetOffset }), snap: 0.5 })] }), _jsxs(LightSection, { title: "Shadow", children: [_jsx(BooleanField, { name: "castShadow", label: "Cast Shadow", values: values, onChange: onUpdate, fallback: false }), values.castShadow ? (_jsxs(_Fragment, { children: [_jsx(BooleanField, { name: "shadowAutoUpdate", label: "Auto Update", values: values, onChange: onUpdate, fallback: true }), _jsx(NumberField, { name: "shadowMapSize", label: "Map Size", values: values, onChange: onUpdate, min: 128, step: 128, fallback: 512 }), _jsx(ShadowBiasField, { name: "shadowBias", label: "Bias", values: values, onChange: onUpdate, fallback: 0 }), _jsx(ShadowBiasField, { name: "shadowNormalBias", label: "Normal Bias", values: values, onChange: onUpdate, fallback: 0 }), _jsx(NumberField, { name: "shadowCameraNear", label: "Near", values: values, onChange: onUpdate, min: 0.001, step: 0.1, fallback: 0.5 }), _jsx(NumberField, { name: "shadowCameraFar", label: "Far", values: values, onChange: onUpdate, min: 0.1, step: 1, fallback: 500 }), _jsx(ShadowFrustumField, { values: values, onChange: onUpdate })] })) : null] })] }));
50
103
  }
51
- function DirectionalLightView({ properties, children, editMode, isSelected }) {
52
- const merged = Object.assign(Object.assign({}, directionalLightDefaults), properties);
104
+ function DirectionalLightView({ properties, children }) {
105
+ const { editMode, isSelected } = useEntityRuntime();
106
+ const merged = mergeWithDefaults(directionalLightDefaults, properties);
53
107
  const color = merged.color;
54
108
  const intensity = merged.intensity;
55
109
  const castShadow = merged.castShadow;
56
110
  const shadowMapSize = merged.shadowMapSize;
111
+ const shadowBias = merged.shadowBias;
112
+ const shadowNormalBias = merged.shadowNormalBias;
113
+ const shadowAutoUpdate = merged.shadowAutoUpdate;
57
114
  const shadowCameraNear = merged.shadowCameraNear;
58
115
  const shadowCameraFar = merged.shadowCameraFar;
59
116
  const shadowCameraTop = merged.shadowCameraTop;
@@ -63,31 +120,48 @@ function DirectionalLightView({ properties, children, editMode, isSelected }) {
63
120
  const targetOffset = merged.targetOffset;
64
121
  const directionalLightRef = useRef(null);
65
122
  const targetRef = useRef(null);
123
+ const shadowCameraRef = useRef(null);
66
124
  const [shadowCamera, setShadowCamera] = useState(null);
67
- const shadowCameraHelper = useMemo(() => shadowCamera ? new CameraHelper(shadowCamera) : null, [shadowCamera]);
68
- useEffect(() => {
69
- return () => {
70
- shadowCameraHelper === null || shadowCameraHelper === void 0 ? void 0 : shadowCameraHelper.dispose();
71
- };
72
- }, [shadowCameraHelper]);
125
+ useHelper(editMode && isSelected && castShadow ? shadowCameraRef : null, CameraHelper);
73
126
  // Use a local target object so node transforms rotate the light direction naturally.
74
127
  useEffect(() => {
75
128
  if (directionalLightRef.current && targetRef.current) {
76
129
  directionalLightRef.current.target = targetRef.current;
77
- setShadowCamera(directionalLightRef.current.shadow.camera);
130
+ const nextShadowCamera = directionalLightRef.current.shadow.camera;
131
+ shadowCameraRef.current = nextShadowCamera;
132
+ setShadowCamera(nextShadowCamera);
78
133
  }
79
134
  }, []);
135
+ useEffect(() => {
136
+ var _a;
137
+ const shadow = (_a = directionalLightRef.current) === null || _a === void 0 ? void 0 : _a.shadow;
138
+ if (!shadow)
139
+ return;
140
+ shadow.needsUpdate = true;
141
+ shadow.camera.updateProjectionMatrix();
142
+ }, [
143
+ castShadow,
144
+ shadowMapSize,
145
+ shadowBias,
146
+ shadowNormalBias,
147
+ shadowAutoUpdate,
148
+ shadowCameraNear,
149
+ shadowCameraFar,
150
+ shadowCameraTop,
151
+ shadowCameraBottom,
152
+ shadowCameraLeft,
153
+ shadowCameraRight,
154
+ ]);
80
155
  useFrame(() => {
81
156
  if (!directionalLightRef.current || !targetRef.current)
82
157
  return;
83
158
  directionalLightRef.current.target.updateMatrixWorld();
84
- if (shadowCamera && shadowCameraHelper && castShadow) {
159
+ if (shadowCamera && castShadow) {
85
160
  shadowCamera.updateProjectionMatrix();
86
161
  shadowCamera.updateMatrixWorld();
87
- shadowCameraHelper.update();
88
162
  }
89
163
  });
90
- return (_jsxs(_Fragment, { children: [_jsx("directionalLight", { ref: directionalLightRef, color: color, intensity: intensity, castShadow: castShadow, "shadow-mapSize-width": shadowMapSize, "shadow-mapSize-height": shadowMapSize, "shadow-camera-near": shadowCameraNear, "shadow-camera-far": shadowCameraFar, "shadow-camera-top": shadowCameraTop, "shadow-camera-bottom": shadowCameraBottom, "shadow-camera-left": shadowCameraLeft, "shadow-camera-right": shadowCameraRight, "shadow-bias": -0.001, "shadow-normalBias": 0.02 }), _jsx("object3D", { ref: targetRef, position: targetOffset }), editMode && isSelected && castShadow && shadowCameraHelper && (_jsx("primitive", { object: shadowCameraHelper })), editMode && isSelected && (_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) => {
164
+ return (_jsxs(_Fragment, { children: [_jsx("directionalLight", { ref: directionalLightRef, color: color, intensity: intensity, castShadow: castShadow, "shadow-mapSize-width": shadowMapSize, "shadow-mapSize-height": shadowMapSize, "shadow-camera-near": shadowCameraNear, "shadow-camera-far": shadowCameraFar, "shadow-camera-top": shadowCameraTop, "shadow-camera-bottom": shadowCameraBottom, "shadow-camera-left": shadowCameraLeft, "shadow-camera-right": shadowCameraRight, "shadow-bias": shadowBias, "shadow-normalBias": shadowNormalBias, "shadow-autoUpdate": shadowAutoUpdate }), _jsx("object3D", { ref: targetRef, position: targetOffset }), editMode && isSelected && (_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) => {
91
165
  const points = [
92
166
  new Vector3(0, 0, 0),
93
167
  new Vector3(targetOffset[0], targetOffset[1], targetOffset[2])
@@ -99,6 +173,6 @@ const DirectionalLightComponent = {
99
173
  name: 'DirectionalLight',
100
174
  Editor: DirectionalLightComponentEditor,
101
175
  View: DirectionalLightView,
102
- defaultProperties: directionalLightDefaults
176
+ defaultProperties: {}
103
177
  };
104
178
  export default DirectionalLightComponent;