react-three-game 0.0.56 → 0.0.58

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 (64) hide show
  1. package/README.md +16 -3
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/shared/GameCanvas.js +1 -3
  5. package/dist/tools/assetviewer/page.js +35 -14
  6. package/dist/tools/prefabeditor/Dropdown.d.ts +15 -0
  7. package/dist/tools/prefabeditor/Dropdown.js +82 -0
  8. package/dist/tools/prefabeditor/EditorContext.d.ts +5 -0
  9. package/dist/tools/prefabeditor/EditorTree.js +149 -91
  10. package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +33 -0
  11. package/dist/tools/prefabeditor/EditorTreeMenus.js +136 -0
  12. package/dist/tools/prefabeditor/EditorUI.js +1 -1
  13. package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
  14. package/dist/tools/prefabeditor/PrefabEditor.js +13 -2
  15. package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -0
  16. package/dist/tools/prefabeditor/PrefabRoot.js +120 -34
  17. package/dist/tools/prefabeditor/components/AmbientLightComponent.js +3 -7
  18. package/dist/tools/prefabeditor/components/CameraComponent.d.ts +3 -0
  19. package/dist/tools/prefabeditor/components/CameraComponent.js +45 -0
  20. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +50 -24
  21. package/dist/tools/prefabeditor/components/EnvironmentComponent.d.ts +3 -0
  22. package/dist/tools/prefabeditor/components/EnvironmentComponent.js +15 -0
  23. package/dist/tools/prefabeditor/components/GeometryComponent.js +46 -46
  24. package/dist/tools/prefabeditor/components/Input.d.ts +51 -1
  25. package/dist/tools/prefabeditor/components/Input.js +73 -21
  26. package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +16 -2
  27. package/dist/tools/prefabeditor/components/MaterialComponent.js +129 -15
  28. package/dist/tools/prefabeditor/components/ModelComponent.js +44 -3
  29. package/dist/tools/prefabeditor/components/PhysicsComponent.js +16 -81
  30. package/dist/tools/prefabeditor/components/SpotLightComponent.js +36 -23
  31. package/dist/tools/prefabeditor/components/TextComponent.js +7 -53
  32. package/dist/tools/prefabeditor/components/TransformComponent.js +18 -8
  33. package/dist/tools/prefabeditor/components/index.js +5 -1
  34. package/dist/tools/prefabeditor/styles.d.ts +5 -2
  35. package/dist/tools/prefabeditor/styles.js +7 -3
  36. package/dist/tools/prefabeditor/utils.d.ts +4 -3
  37. package/dist/tools/prefabeditor/utils.js +53 -5
  38. package/package.json +1 -1
  39. package/react-three-game-skill/react-three-game/SKILL.md +4 -1
  40. package/src/index.ts +7 -0
  41. package/src/shared/GameCanvas.tsx +0 -3
  42. package/src/tools/assetviewer/page.tsx +77 -45
  43. package/src/tools/prefabeditor/Dropdown.tsx +112 -0
  44. package/src/tools/prefabeditor/EditorContext.tsx +5 -0
  45. package/src/tools/prefabeditor/EditorTree.tsx +242 -178
  46. package/src/tools/prefabeditor/EditorTreeMenus.tsx +307 -0
  47. package/src/tools/prefabeditor/EditorUI.tsx +1 -1
  48. package/src/tools/prefabeditor/PrefabEditor.tsx +17 -4
  49. package/src/tools/prefabeditor/PrefabRoot.tsx +208 -58
  50. package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +5 -11
  51. package/src/tools/prefabeditor/components/CameraComponent.tsx +117 -0
  52. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +61 -30
  53. package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +47 -0
  54. package/src/tools/prefabeditor/components/GeometryComponent.tsx +69 -63
  55. package/src/tools/prefabeditor/components/Input.tsx +220 -27
  56. package/src/tools/prefabeditor/components/MaterialComponent.tsx +189 -18
  57. package/src/tools/prefabeditor/components/ModelComponent.tsx +51 -4
  58. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +44 -85
  59. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +52 -27
  60. package/src/tools/prefabeditor/components/TextComponent.tsx +58 -57
  61. package/src/tools/prefabeditor/components/TransformComponent.tsx +61 -9
  62. package/src/tools/prefabeditor/components/index.ts +5 -1
  63. package/src/tools/prefabeditor/styles.ts +7 -3
  64. package/src/tools/prefabeditor/utils.ts +55 -4
@@ -8,7 +8,7 @@ import { getComponent, registerComponent, getNonComposableKeys } from "./compone
8
8
  import components from "./components";
9
9
  import { loadModel } from "../dragdrop/modelLoader";
10
10
  import { GameInstance, GameInstanceProvider, useInstanceCheck } from "./InstanceProvider";
11
- import { updateNode } from "./utils";
11
+ import { focusCameraOnObject, updateNode } from "./utils";
12
12
  import { PhysicsProps } from "./components/PhysicsComponent";
13
13
  import { EditorContext } from "./EditorContext";
14
14
 
@@ -24,6 +24,7 @@ export interface PrefabRootRef {
24
24
  rigidBodyRefs: Map<string, any>; // RigidBody refs only populated when using physics
25
25
  injectModel: (filename: string, model: Object3D) => void;
26
26
  injectTexture: (filename: string, file: File) => void;
27
+ focusNode: (nodeId: string) => void;
27
28
  }
28
29
 
29
30
  export const PrefabRoot = forwardRef<PrefabRootRef, {
@@ -40,15 +41,19 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
40
41
  const editorContext = useContext(EditorContext);
41
42
  const transformMode = editorContext?.transformMode ?? "translate";
42
43
  const snapResolution = editorContext?.snapResolution ?? 0;
44
+ const positionSnap = editorContext?.positionSnap ?? 0.5;
45
+ const rotationSnap = editorContext?.rotationSnap ?? Math.PI / 4;
43
46
 
44
47
  // prefab root state
45
48
  const [models, setModels] = useState<Record<string, Object3D>>({});
46
49
  const [textures, setTextures] = useState<Record<string, Texture>>({});
47
50
  const loading = useRef(new Set<string>());
51
+ const failedTextures = useRef(new Set<string>());
48
52
  const objectRefs = useRef<Record<string, Object3D | null>>({});
49
53
  const rigidBodyRefs = useRef<Map<string, RapierRigidBody | null>>(new Map());
50
54
  const [selectedObject, setSelectedObject] = useState<Object3D | null>(null);
51
55
  const rootRef = useRef<Group>(null);
56
+ const controlsRef = useRef<any>(null);
52
57
 
53
58
  const injectModel = useCallback((filename: string, model: Object3D) => {
54
59
  setModels(m => ({ ...m, [filename]: model }));
@@ -69,7 +74,16 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
69
74
  root: rootRef.current,
70
75
  rigidBodyRefs: rigidBodyRefs.current,
71
76
  injectModel,
72
- injectTexture
77
+ injectTexture,
78
+ focusNode: (nodeId: string) => {
79
+ const object = objectRefs.current[nodeId];
80
+ const controls = controlsRef.current;
81
+ const camera = controls?.object;
82
+
83
+ if (!object || !controls || !camera) return;
84
+
85
+ focusCameraOnObject(object, camera, controls.target, () => controls.update?.());
86
+ }
73
87
  }), [injectModel, injectTexture]);
74
88
 
75
89
  const registerRef = useCallback((id: string, obj: Object3D | null) => {
@@ -128,6 +142,8 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
128
142
  modelsToLoad.add(node.components.model.properties.filename);
129
143
  node.components?.material?.properties?.texture &&
130
144
  texturesToLoad.add(node.components.material.properties.texture);
145
+ node.components?.material?.properties?.normalMapTexture &&
146
+ texturesToLoad.add(node.components.material.properties.normalMapTexture);
131
147
  });
132
148
 
133
149
  modelsToLoad.forEach(async file => {
@@ -145,7 +161,7 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
145
161
 
146
162
  const loader = new TextureLoader();
147
163
  texturesToLoad.forEach(file => {
148
- if (textures[file] || loading.current.has(file)) return;
164
+ if (textures[file] || loading.current.has(file) || failedTextures.current.has(file)) return;
149
165
  loading.current.add(file);
150
166
 
151
167
  // Handle full URLs (http/https) or regular paths
@@ -159,8 +175,9 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
159
175
  tex.colorSpace = SRGBColorSpace;
160
176
  setTextures(t => ({ ...t, [file]: tex }));
161
177
  }, undefined, (err) => {
162
- console.error(`Failed to load texture: ${path}`, err);
178
+ console.warn(`Failed to load texture: ${path}`, err);
163
179
  loading.current.delete(file);
180
+ failedTextures.current.add(file);
164
181
  });
165
182
  });
166
183
  }, [data, models, textures]);
@@ -190,16 +207,16 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
190
207
 
191
208
  {editMode && (
192
209
  <>
193
- <MapControls makeDefault />
210
+ <MapControls ref={controlsRef} makeDefault />
194
211
  {selectedObject && (
195
212
  <TransformControls
196
- key={`transform-${snapResolution}`}
213
+ key={`transform-${transformMode}-${positionSnap}-${rotationSnap}-${snapResolution}`}
197
214
  object={selectedObject}
198
215
  mode={transformMode}
199
216
  space="local"
200
217
  onObjectChange={onTransformChange}
201
- translationSnap={snapResolution > 0 ? snapResolution : undefined}
202
- rotationSnap={snapResolution > 0 ? snapResolution : undefined}
218
+ translationSnap={positionSnap > 0 ? positionSnap : undefined}
219
+ rotationSnap={rotationSnap > 0 ? rotationSnap : undefined}
203
220
  scaleSnap={snapResolution > 0 ? snapResolution : undefined}
204
221
  />
205
222
  )}
@@ -362,6 +379,19 @@ function StandardNode({
362
379
  const physicsDef = hasPhysics ? getComponent("Physics") : null;
363
380
  const isInstanced = gameObject.components?.model?.properties?.instanced;
364
381
  const physicsKey = `physics_${gameObject.id}_${isInstanced ? 'instanced' : 'standard'}`;
382
+ const renderCtx = createRenderContext(loadedModels, loadedTextures, editMode, selectedId, registerRef);
383
+ const childNodes = getChildHostComponents(gameObject).length > 0
384
+ ? renderHostedChildren(gameObject, renderCtx, world)
385
+ : renderSceneChildren(gameObject, world, {
386
+ selectedId,
387
+ onSelect,
388
+ onClick,
389
+ registerRef,
390
+ registerRigidBodyRef,
391
+ loadedModels,
392
+ loadedTextures,
393
+ editMode,
394
+ });
365
395
 
366
396
  const inner = (
367
397
  <group
@@ -369,22 +399,7 @@ function StandardNode({
369
399
  onPointerMove={editMode ? () => (clickValid.current = false) : undefined}
370
400
  onPointerUp={editMode ? onUp : undefined}
371
401
  >
372
- {renderCoreNode(gameObject, { loadedModels, loadedTextures, editMode, registerRef }, parentMatrix)}
373
- {gameObject.children?.map(child => (
374
- <GameObjectRenderer
375
- key={child.id}
376
- gameObject={child}
377
- selectedId={selectedId}
378
- onSelect={onSelect}
379
- onClick={onClick}
380
- registerRef={registerRef}
381
- registerRigidBodyRef={registerRigidBodyRef}
382
- loadedModels={loadedModels}
383
- loadedTextures={loadedTextures}
384
- editMode={editMode}
385
- parentMatrix={world}
386
- />
387
- ))}
402
+ {renderCompositionNode(gameObject, renderCtx, parentMatrix, childNodes)}
388
403
  </group>
389
404
  );
390
405
 
@@ -468,6 +483,74 @@ interface RendererProps {
468
483
  parentMatrix?: Matrix4;
469
484
  }
470
485
 
486
+ const CHILD_HOST_COMPONENT_TYPES = new Set(["Environment"]);
487
+
488
+ function isChildHostType(type: string) {
489
+ return CHILD_HOST_COMPONENT_TYPES.has(type);
490
+ }
491
+
492
+ function getChildHostComponents(gameObject: GameObjectType) {
493
+ return Object.entries(gameObject.components ?? {}).flatMap(([key, comp]) => {
494
+ if (!comp?.type || !isChildHostType(comp.type)) return [];
495
+
496
+ const def = getComponent(comp.type);
497
+ if (!def?.View) return [];
498
+
499
+ return { key, View: def.View, properties: comp.properties };
500
+ });
501
+ }
502
+
503
+ interface RenderContext {
504
+ loadedModels: Record<string, Object3D>;
505
+ loadedTextures: Record<string, Texture>;
506
+ editMode?: boolean;
507
+ selectedId?: string | null;
508
+ registerRef: (id: string, obj: Object3D | null) => void;
509
+ }
510
+
511
+ interface RuntimeChildRendererProps {
512
+ selectedId?: string | null;
513
+ onSelect?: (id: string) => void;
514
+ onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
515
+ registerRef: (id: string, obj: Object3D | null) => void;
516
+ registerRigidBodyRef: (id: string, rb: any) => void;
517
+ loadedModels: Record<string, Object3D>;
518
+ loadedTextures: Record<string, Texture>;
519
+ editMode?: boolean;
520
+ }
521
+
522
+ function createRenderContext(
523
+ loadedModels: Record<string, Object3D>,
524
+ loadedTextures: Record<string, Texture>,
525
+ editMode: boolean | undefined,
526
+ selectedId: string | null | undefined,
527
+ registerRef: (id: string, obj: Object3D | null) => void,
528
+ ): RenderContext {
529
+ return { loadedModels, loadedTextures, editMode, selectedId, registerRef };
530
+ }
531
+
532
+ function renderSceneChildren(
533
+ gameObject: GameObjectType,
534
+ parentMatrix: Matrix4,
535
+ props: RuntimeChildRendererProps,
536
+ ) {
537
+ return gameObject.children?.map(child =>
538
+ <GameObjectRenderer
539
+ key={child.id}
540
+ gameObject={child}
541
+ selectedId={props.selectedId}
542
+ onSelect={props.onSelect}
543
+ onClick={props.onClick}
544
+ registerRef={props.registerRef}
545
+ registerRigidBodyRef={props.registerRigidBodyRef}
546
+ loadedModels={props.loadedModels}
547
+ loadedTextures={props.loadedTextures}
548
+ editMode={props.editMode}
549
+ parentMatrix={parentMatrix}
550
+ />
551
+ );
552
+ }
553
+
471
554
  function walk(node: GameObjectType, fn: (n: GameObjectType) => void) {
472
555
  fn(node);
473
556
  node.children?.forEach(c => walk(c, fn));
@@ -518,14 +601,55 @@ function computeParentWorldMatrix(root: GameObjectType, targetId: string): Matri
518
601
  return result ?? IDENTITY;
519
602
  }
520
603
 
521
- function renderCoreNode(
604
+ function renderCompositionSubtree(
605
+ gameObject: GameObjectType,
606
+ ctx: RenderContext,
607
+ parentMatrix = IDENTITY
608
+ ): React.ReactNode {
609
+ if (!gameObject || gameObject.disabled) return null;
610
+
611
+ const transform = getNodeTransformProps(gameObject);
612
+ const world = parentMatrix.clone().multiply(compose(gameObject));
613
+ const childNodes = renderHostedChildren(gameObject, ctx, world);
614
+
615
+ return (
616
+ <group
617
+ key={gameObject.id}
618
+ position={transform.position}
619
+ rotation={transform.rotation}
620
+ scale={transform.scale}
621
+ >
622
+ {renderCompositionNode(gameObject, ctx, parentMatrix, childNodes)}
623
+ </group>
624
+ );
625
+ }
626
+
627
+ function renderHostedChildren(
628
+ gameObject: GameObjectType,
629
+ ctx: RenderContext,
630
+ parentMatrix: Matrix4,
631
+ ) {
632
+ return gameObject.children?.map(child =>
633
+ renderCompositionSubtree(child, ctx, parentMatrix)
634
+ );
635
+ }
636
+
637
+ function renderCompositionNode(
522
638
  gameObject: GameObjectType,
523
- ctx: {
524
- loadedModels: Record<string, Object3D>;
525
- loadedTextures: Record<string, Texture>;
526
- editMode?: boolean;
527
- registerRef: (id: string, obj: Object3D | null) => void;
528
- },
639
+ ctx: RenderContext,
640
+ parentMatrix?: Matrix4,
641
+ childNodes?: React.ReactNode
642
+ ) {
643
+ const ownContent = renderNodeOwnContent(gameObject, ctx, parentMatrix);
644
+ const siblingContent = renderNodeSiblingComponents(gameObject, ctx, parentMatrix);
645
+ const subtree = <>{ownContent}{siblingContent}{childNodes}</>;
646
+
647
+ return wrapWithChildHosts(gameObject, ctx, parentMatrix, subtree);
648
+ }
649
+
650
+ function renderNodeOwnContent(
651
+ gameObject: GameObjectType,
652
+ ctx: RenderContext,
529
653
  parentMatrix?: Matrix4
530
654
  ) {
531
655
  const geometry = gameObject.components?.geometry;
@@ -542,31 +666,12 @@ function renderCoreNode(
542
666
  loadedModels: ctx.loadedModels,
543
667
  loadedTextures: ctx.loadedTextures,
544
668
  editMode: ctx.editMode,
669
+ isSelected: ctx.selectedId === gameObject.id,
670
+ nodeId: gameObject.id,
545
671
  parentMatrix,
546
672
  registerRef: ctx.registerRef,
547
673
  };
548
674
 
549
- const wrappers: Array<{ key: string; View: any; properties: any }> = [];
550
- const leaves: React.ReactNode[] = [];
551
-
552
- if (gameObject.components) {
553
- Object.entries(gameObject.components)
554
- .filter(([k]) => !getNonComposableKeys().includes(k))
555
- .forEach(([key, comp]) => {
556
- if (!comp?.type) return;
557
- const def = getComponent(comp.type);
558
- if (!def?.View) return;
559
-
560
- if (def.View.toString().includes("children")) {
561
- wrappers.push({ key, View: def.View, properties: comp.properties });
562
- } else {
563
- leaves.push(
564
- <def.View key={key} properties={comp.properties} {...contextProps} />
565
- );
566
- }
567
- });
568
- }
569
-
570
675
  let core: React.ReactNode;
571
676
 
572
677
  if (model && modelDef?.View) {
@@ -579,7 +684,6 @@ function renderCoreNode(
579
684
  {...contextProps}
580
685
  />
581
686
  )}
582
- {leaves}
583
687
  </modelDef.View>
584
688
  );
585
689
  } else if (geometry && geometryDef?.View) {
@@ -593,27 +697,73 @@ function renderCoreNode(
593
697
  {...contextProps}
594
698
  />
595
699
  )}
596
- {leaves}
597
700
  </mesh>
598
701
  );
599
702
  } else if (text && textDef?.View) {
600
703
  core = (
601
704
  <>
602
705
  <textDef.View properties={text.properties} {...contextProps} />
603
- {leaves}
604
706
  </>
605
707
  );
606
708
  } else {
607
- core = <>{leaves}</>;
709
+ core = null;
608
710
  }
609
711
 
610
- return wrappers.reduce(
712
+ return core;
713
+ }
714
+
715
+ function renderNodeSiblingComponents(
716
+ gameObject: GameObjectType,
717
+ ctx: RenderContext,
718
+ parentMatrix?: Matrix4
719
+ ) {
720
+ const contextProps = {
721
+ loadedModels: ctx.loadedModels,
722
+ loadedTextures: ctx.loadedTextures,
723
+ editMode: ctx.editMode,
724
+ isSelected: ctx.selectedId === gameObject.id,
725
+ nodeId: gameObject.id,
726
+ parentMatrix,
727
+ registerRef: ctx.registerRef,
728
+ };
729
+
730
+ return Object.entries(gameObject.components ?? {})
731
+ .filter(([key]) => !getNonComposableKeys().includes(key))
732
+ .flatMap(([key, comp]) => {
733
+ if (!comp?.type || isChildHostType(comp.type)) return [];
734
+
735
+ const def = getComponent(comp.type);
736
+ if (!def?.View) return [];
737
+
738
+ return <def.View key={key} properties={comp.properties} {...contextProps} />;
739
+ });
740
+ }
741
+
742
+ function wrapWithChildHosts(
743
+ gameObject: GameObjectType,
744
+ ctx: RenderContext,
745
+ parentMatrix: Matrix4 | undefined,
746
+ subtree: React.ReactNode
747
+ ) {
748
+ const contextProps = {
749
+ loadedModels: ctx.loadedModels,
750
+ loadedTextures: ctx.loadedTextures,
751
+ editMode: ctx.editMode,
752
+ isSelected: ctx.selectedId === gameObject.id,
753
+ nodeId: gameObject.id,
754
+ parentMatrix,
755
+ registerRef: ctx.registerRef,
756
+ };
757
+
758
+ const childHosts = getChildHostComponents(gameObject);
759
+
760
+ return childHosts.reduce(
611
761
  (acc, { key, View, properties }) => (
612
762
  <View key={key} properties={properties} {...contextProps}>
613
763
  {acc}
614
764
  </View>
615
765
  ),
616
- core
766
+ subtree
617
767
  );
618
768
  }
619
769
 
@@ -1,10 +1,5 @@
1
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
- ];
2
+ import { ColorField, FieldGroup, NumberField } from "./Input";
8
3
 
9
4
  function AmbientLightComponentEditor({
10
5
  component,
@@ -14,11 +9,10 @@ function AmbientLightComponentEditor({
14
9
  onUpdate: (newProps: any) => void;
15
10
  }) {
16
11
  return (
17
- <FieldRenderer
18
- fields={ambientLightFields}
19
- values={component.properties}
20
- onChange={onUpdate}
21
- />
12
+ <FieldGroup>
13
+ <ColorField name="color" label="Color" values={component.properties} onChange={onUpdate} />
14
+ <NumberField name="intensity" label="Intensity" values={component.properties} onChange={onUpdate} min={0} step={0.1} fallback={1} />
15
+ </FieldGroup>
22
16
  );
23
17
  }
24
18
 
@@ -0,0 +1,117 @@
1
+ import { PerspectiveCamera } from '@react-three/drei';
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { CameraHelper, PerspectiveCamera as ThreePerspectiveCamera } from 'three';
4
+ import { useFrame } from '@react-three/fiber';
5
+ import { Component } from './ComponentRegistry';
6
+ import { FieldGroup, NumberField } from './Input';
7
+
8
+ const cameraDefaults = {
9
+ fov: 50,
10
+ near: 0.1,
11
+ zoom: 1,
12
+ far: 1000,
13
+ };
14
+
15
+ function CameraComponentEditor({ component, onUpdate }: { component: any; onUpdate: (newComp: any) => void }) {
16
+ const values = { ...cameraDefaults, ...component.properties };
17
+
18
+ return (
19
+ <FieldGroup>
20
+ <NumberField
21
+ name="fov"
22
+ label="FOV"
23
+ values={values}
24
+ onChange={onUpdate}
25
+ fallback={50}
26
+ min={1}
27
+ max={179}
28
+ step={1}
29
+ />
30
+ <NumberField
31
+ name="near"
32
+ label="Near"
33
+ values={values}
34
+ onChange={onUpdate}
35
+ fallback={0.1}
36
+ min={0.001}
37
+ step={0.1}
38
+ />
39
+ <NumberField
40
+ name="zoom"
41
+ label="Zoom"
42
+ values={values}
43
+ onChange={onUpdate}
44
+ fallback={1}
45
+ min={0.01}
46
+ step={0.1}
47
+ />
48
+ <NumberField
49
+ name="far"
50
+ label="Far"
51
+ values={values}
52
+ onChange={onUpdate}
53
+ fallback={1000}
54
+ min={0.1}
55
+ step={1}
56
+ />
57
+ </FieldGroup>
58
+ );
59
+ }
60
+
61
+ function CameraComponentView({ properties, editMode, isSelected }: { properties: any; editMode?: boolean; isSelected?: boolean }) {
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]);
78
+
79
+ useFrame(() => {
80
+ if (camera && cameraHelper && editMode && isSelected) {
81
+ camera.updateProjectionMatrix();
82
+ camera.updateMatrixWorld();
83
+ cameraHelper.update();
84
+ }
85
+ });
86
+
87
+ return (
88
+ <>
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
+ )}
100
+ {editMode && !isSelected ? (
101
+ <mesh>
102
+ <boxGeometry args={[0.34, 0.22, 0.18]} />
103
+ <meshBasicMaterial color="#22d3ee" wireframe />
104
+ </mesh>
105
+ ) : null}
106
+ </>
107
+ );
108
+ }
109
+
110
+ const CameraComponent: Component = {
111
+ name: 'Camera',
112
+ Editor: CameraComponentEditor,
113
+ View: CameraComponentView,
114
+ defaultProperties: cameraDefaults,
115
+ };
116
+
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
- function DirectionalLightView({ properties, editMode }: { properties: any; editMode?: 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];
102
+ function DirectionalLightView({ properties, editMode, isSelected }: { properties: any; editMode?: boolean; isSelected?: boolean }) {
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,9 +165,11 @@ function DirectionalLightView({ properties, editMode }: { properties: any; editM
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} />
141
- {editMode && (
168
+ <object3D ref={targetRef} position={targetOffset as [number, number, number]} />
169
+ {editMode && isSelected && castShadow && shadowCameraHelper && (
170
+ <primitive object={shadowCameraHelper} />
171
+ )}
172
+ {editMode && isSelected && (
142
173
  <>
143
174
  {/* Light source indicator */}
144
175
  <mesh>
@@ -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;