react-three-game 0.0.80 → 0.0.82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { default as GameCanvas } from './shared/GameCanvas';
2
2
  export type { GameCanvasProps } from './shared/GameCanvas';
3
+ export { useBeforePhysicsStep, useAfterPhysicsStep, useRapier } from '@react-three/rapier';
3
4
  export { ground } from './helpers';
4
5
  export type { GroundOptions, Vec3 } from './helpers';
5
6
  export { sound as soundManager } from './helpers/SoundManager';
@@ -26,10 +27,11 @@ export type { Component, ComponentViewProps } from './tools/prefabeditor/compone
26
27
  export type { FieldDefinition, FieldType } from './tools/prefabeditor/components/Input';
27
28
  export type { Prefab, GameObject, ComponentData } from './tools/prefabeditor/types';
28
29
  export { findComponent, findComponentEntry, hasComponent } from './tools/prefabeditor/types';
29
- export { gameEvents, useGameEvent, getEntityIdFromRigidBody } from './tools/prefabeditor/GameEvents';
30
+ export { gameEvents, useGameEvent, usePhysicsEvent, useClickEvent, getEntityIdFromRigidBody } from './tools/prefabeditor/GameEvents';
30
31
  export type { GameEventType, GameEventMap, GameEventPayload, PhysicsEventType, InteractionEventType, PhysicsEventPayload, ClickEventPayload } from './tools/prefabeditor/GameEvents';
31
32
  export { loadFiles } from './tools/dragdrop/DragDropLoader';
32
33
  export type { AssetLoadOptions } from './tools/dragdrop/DragDropLoader';
33
34
  export { loadModel, loadSound, loadTexture } from './tools/dragdrop/modelLoader';
34
35
  export type { LoadedModel, LoadedModels, ModelLoadResult, LoadedSound, LoadedSounds, SoundLoadResult, LoadedTexture, LoadedTextures, TextureLoadResult, ProgressCallback, } from './tools/dragdrop/modelLoader';
35
36
  export { ModelListViewer, SoundListViewer, ModelPicker, SoundPicker, TextureListViewer, TexturePicker, SingleModelViewer, SingleSoundViewer, SingleTextureViewer, SharedCanvas, } from './tools/assetviewer/page';
37
+ export type { PrefabRootRef } from './tools/prefabeditor/PrefabRoot';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // Core
2
2
  export { default as GameCanvas } from './shared/GameCanvas';
3
+ export { useBeforePhysicsStep, useAfterPhysicsStep, useRapier } from '@react-three/rapier';
3
4
  // Helpers
4
5
  export { ground } from './helpers';
5
6
  export { sound as soundManager } from './helpers/SoundManager';
@@ -22,7 +23,7 @@ export { createModelNode, createImageNode, } from './tools/prefabeditor/prefab';
22
23
  export { useAssetRuntime, useEntityRuntime, useEntityObjectRef, useEntityRigidBodyRef } from './tools/prefabeditor/runtimeContext';
23
24
  export { findComponent, findComponentEntry, hasComponent } from './tools/prefabeditor/types';
24
25
  // Game Events (physics + custom events)
25
- export { gameEvents, useGameEvent, getEntityIdFromRigidBody } from './tools/prefabeditor/GameEvents';
26
+ export { gameEvents, useGameEvent, usePhysicsEvent, useClickEvent, getEntityIdFromRigidBody } from './tools/prefabeditor/GameEvents';
26
27
  // Asset Loading
27
28
  export { loadFiles } from './tools/dragdrop/DragDropLoader';
28
29
  export { loadModel, loadSound, loadTexture } from './tools/dragdrop/modelLoader';
@@ -40,6 +40,13 @@ export type GameEventType = keyof GameEventMap | (string & {});
40
40
  /** Get payload type for an event, or fallback to generic */
41
41
  export type GameEventPayload<T extends string> = T extends keyof GameEventMap ? GameEventMap[T] : Record<string, unknown>;
42
42
  type EventHandler<T = unknown> = (payload: T) => void;
43
+ type UnknownEventPayload = Record<string, unknown>;
44
+ declare function emitGameEvent<TType extends keyof GameEventMap>(type: TType, payload: GameEventMap[TType]): void;
45
+ declare function emitGameEvent(type: string, payload: UnknownEventPayload): void;
46
+ declare function onGameEvent<TType extends keyof GameEventMap>(type: TType, handler: EventHandler<GameEventMap[TType]>): () => void;
47
+ declare function onGameEvent(type: string, handler: EventHandler<UnknownEventPayload>): () => void;
48
+ declare function offGameEvent<TType extends keyof GameEventMap>(type: TType, handler: EventHandler<GameEventMap[TType]>): void;
49
+ declare function offGameEvent(type: string, handler: EventHandler<UnknownEventPayload>): void;
43
50
  /**
44
51
  * Game event system for all game interactions.
45
52
  *
@@ -66,16 +73,16 @@ export declare const gameEvents: {
66
73
  /**
67
74
  * Emit an event to all subscribers
68
75
  */
69
- emit<T extends string>(type: T, payload: GameEventPayload<T>): void;
76
+ emit: typeof emitGameEvent;
70
77
  /**
71
78
  * Subscribe to an event type
72
79
  * @returns Unsubscribe function
73
80
  */
74
- on<T extends string>(type: T, handler: EventHandler<GameEventPayload<T>>): () => void;
81
+ on: typeof onGameEvent;
75
82
  /**
76
83
  * Unsubscribe from an event type
77
84
  */
78
- off<T extends string>(type: T, handler: EventHandler<GameEventPayload<T>>): void;
85
+ off: typeof offGameEvent;
79
86
  /**
80
87
  * Remove all subscribers (useful for cleanup/reset)
81
88
  */
@@ -97,10 +104,22 @@ export declare const gameEvents: {
97
104
  *
98
105
  * // Custom event
99
106
  * useGameEvent('player:death', (payload) => {
100
- * showGameOver(payload.cause);
107
+ * const cause = typeof payload.cause === 'string' ? payload.cause : 'unknown';
108
+ * showGameOver(cause);
101
109
  * }, []);
102
110
  */
103
- export declare function useGameEvent<T extends string>(type: T, handler: EventHandler<GameEventPayload<T>>, deps?: unknown[]): void;
111
+ export declare function useGameEvent<TType extends keyof GameEventMap>(type: TType, handler: EventHandler<GameEventMap[TType]>, deps?: unknown[]): void;
112
+ export declare function useGameEvent(type: string, handler: EventHandler<UnknownEventPayload>, deps?: unknown[]): void;
113
+ /**
114
+ * React hook to subscribe to any physics event payload.
115
+ * Use this when the event name is dynamic but the payload comes from PhysicsComponent.
116
+ */
117
+ export declare function usePhysicsEvent(type: string, handler: EventHandler<PhysicsEventPayload>, deps?: unknown[]): void;
118
+ /**
119
+ * React hook to subscribe to click event payloads.
120
+ * Use this when the event name is dynamic but the payload comes from ClickComponent.
121
+ */
122
+ export declare function useClickEvent(type: string, handler: EventHandler<ClickEventPayload>, deps?: unknown[]): void;
104
123
  /**
105
124
  * Helper to extract entity ID from Rapier collision data.
106
125
  * Entity IDs are stored in RigidBody userData.
@@ -1,6 +1,40 @@
1
1
  import { useEffect, useCallback } from 'react';
2
2
  // Internal subscriber storage
3
3
  const subscribers = new Map();
4
+ function emitGameEvent(type, payload) {
5
+ const handlers = subscribers.get(type);
6
+ if (handlers) {
7
+ handlers.forEach(handler => {
8
+ try {
9
+ handler(payload);
10
+ }
11
+ catch (e) {
12
+ console.error(`Error in gameEvents handler for ${type}:`, e);
13
+ }
14
+ });
15
+ }
16
+ }
17
+ function onGameEvent(type, handler) {
18
+ if (!subscribers.has(type)) {
19
+ subscribers.set(type, new Set());
20
+ }
21
+ subscribers.get(type).add(handler);
22
+ return () => {
23
+ var _a;
24
+ (_a = subscribers.get(type)) === null || _a === void 0 ? void 0 : _a.delete(handler);
25
+ };
26
+ }
27
+ function offGameEvent(type, handler) {
28
+ var _a;
29
+ (_a = subscribers.get(type)) === null || _a === void 0 ? void 0 : _a.delete(handler);
30
+ }
31
+ function useTypedGameEvent(type, handler, deps = []) {
32
+ // eslint-disable-next-line react-hooks/exhaustive-deps
33
+ const stableHandler = useCallback(handler, deps);
34
+ useEffect(() => {
35
+ return onGameEvent(type, stableHandler);
36
+ }, [type, stableHandler]);
37
+ }
4
38
  /**
5
39
  * Game event system for all game interactions.
6
40
  *
@@ -27,40 +61,16 @@ export const gameEvents = {
27
61
  /**
28
62
  * Emit an event to all subscribers
29
63
  */
30
- emit(type, payload) {
31
- const handlers = subscribers.get(type);
32
- if (handlers) {
33
- handlers.forEach(handler => {
34
- try {
35
- handler(payload);
36
- }
37
- catch (e) {
38
- console.error(`Error in gameEvents handler for ${type}:`, e);
39
- }
40
- });
41
- }
42
- },
64
+ emit: emitGameEvent,
43
65
  /**
44
66
  * Subscribe to an event type
45
67
  * @returns Unsubscribe function
46
68
  */
47
- on(type, handler) {
48
- if (!subscribers.has(type)) {
49
- subscribers.set(type, new Set());
50
- }
51
- subscribers.get(type).add(handler);
52
- return () => {
53
- var _a;
54
- (_a = subscribers.get(type)) === null || _a === void 0 ? void 0 : _a.delete(handler);
55
- };
56
- },
69
+ on: onGameEvent,
57
70
  /**
58
71
  * Unsubscribe from an event type
59
72
  */
60
- off(type, handler) {
61
- var _a;
62
- (_a = subscribers.get(type)) === null || _a === void 0 ? void 0 : _a.delete(handler);
63
- },
73
+ off: offGameEvent,
64
74
  /**
65
75
  * Remove all subscribers (useful for cleanup/reset)
66
76
  */
@@ -75,27 +85,22 @@ export const gameEvents = {
75
85
  return ((_b = (_a = subscribers.get(type)) === null || _a === void 0 ? void 0 : _a.size) !== null && _b !== void 0 ? _b : 0) > 0;
76
86
  }
77
87
  };
88
+ export function useGameEvent(type, handler, deps = []) {
89
+ useTypedGameEvent(type, handler, deps);
90
+ }
78
91
  /**
79
- * React hook to subscribe to game events.
80
- * Automatically cleans up on unmount.
81
- *
82
- * @example
83
- * // Physics event
84
- * useGameEvent('sensor:enter', (payload) => {
85
- * if (payload.sourceEntityId === 'coin') collectCoin();
86
- * }, []);
87
- *
88
- * // Custom event
89
- * useGameEvent('player:death', (payload) => {
90
- * showGameOver(payload.cause);
91
- * }, []);
92
+ * React hook to subscribe to any physics event payload.
93
+ * Use this when the event name is dynamic but the payload comes from PhysicsComponent.
92
94
  */
93
- export function useGameEvent(type, handler, deps = []) {
94
- // eslint-disable-next-line react-hooks/exhaustive-deps
95
- const stableHandler = useCallback(handler, deps);
96
- useEffect(() => {
97
- return gameEvents.on(type, stableHandler);
98
- }, [type, stableHandler]);
95
+ export function usePhysicsEvent(type, handler, deps = []) {
96
+ useTypedGameEvent(type, handler, deps);
97
+ }
98
+ /**
99
+ * React hook to subscribe to click event payloads.
100
+ * Use this when the event name is dynamic but the payload comes from ClickComponent.
101
+ */
102
+ export function useClickEvent(type, handler, deps = []) {
103
+ useTypedGameEvent(type, handler, deps);
99
104
  }
100
105
  // ============================================================================
101
106
  // Helpers
@@ -373,17 +373,21 @@ function renderCompositionNode(gameObject, ctx, childNodes) {
373
373
  return applyNodeComposition(gameObject, _jsxs(_Fragment, { children: [primaryContent, childNodes] }));
374
374
  }
375
375
  function renderNodePrimaryContent(gameObject, ctx) {
376
- var _a;
376
+ var _a, _b;
377
377
  const geometry = findComponent(gameObject, "Geometry");
378
378
  const material = findComponent(gameObject, "Material");
379
379
  const model = findComponent(gameObject, "Model");
380
380
  const geometryDef = geometry && getComponentDef(geometry.type);
381
381
  const materialDef = material && getComponentDef(material.type);
382
382
  const modelDef = model && getComponentDef(model.type);
383
+ const geometryProperties = (_a = geometry === null || geometry === void 0 ? void 0 : geometry.properties) !== null && _a !== void 0 ? _a : {};
384
+ const meshVisible = geometryProperties.visible !== false;
385
+ const meshCastShadow = meshVisible && geometryProperties.castShadow !== false;
386
+ const meshReceiveShadow = meshVisible && geometryProperties.receiveShadow !== false;
383
387
  if ((geometry === null || geometry === void 0 ? void 0 : geometry.type) && (geometryDef === null || geometryDef === void 0 ? void 0 : geometryDef.View)) {
384
- 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
+ return (_jsxs("mesh", { visible: meshVisible, castShadow: meshCastShadow, receiveShadow: meshReceiveShadow, children: [_jsx(geometryDef.View, { properties: geometry.properties }), material && (materialDef === null || materialDef === void 0 ? void 0 : materialDef.View) && (_jsx(materialDef.View, { properties: material.properties }, "material"))] }));
385
389
  }
386
- if ((model === null || model === void 0 ? void 0 : model.type) && (modelDef === null || modelDef === void 0 ? void 0 : modelDef.View) && !((_a = model.properties) === null || _a === void 0 ? void 0 : _a.instanced) && isNodeReady(gameObject, ctx.loadedModels)) {
390
+ if ((model === null || model === void 0 ? void 0 : model.type) && (modelDef === null || modelDef === void 0 ? void 0 : modelDef.View) && !((_b = model.properties) === null || _b === void 0 ? void 0 : _b.instanced) && isNodeReady(gameObject, ctx.loadedModels)) {
387
391
  return _jsx(modelDef.View, { properties: model.properties });
388
392
  }
389
393
  return null;
@@ -1,36 +1,66 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { PerspectiveCamera, useHelper } from '@react-three/drei';
2
+ import { OrthographicCamera, PerspectiveCamera, useHelper } from '@react-three/drei';
3
3
  import { useRef } from 'react';
4
4
  import { CameraHelper } from 'three';
5
- import { useFrame } from '@react-three/fiber';
5
+ import { useFrame, useThree } from '@react-three/fiber';
6
6
  import { useEntityRuntime } from '../runtimeContext';
7
- import { FieldGroup, NumberField } from './Input';
7
+ import { FieldGroup, NumberField, SelectField } from './Input';
8
+ const CAMERA_PROJECTION_OPTIONS = [
9
+ { value: 'perspective', label: 'Perspective' },
10
+ { value: 'orthographic', label: 'Orthographic' },
11
+ ];
8
12
  const cameraDefaults = {
13
+ projection: 'perspective',
9
14
  fov: 50,
10
15
  near: 0.1,
11
16
  zoom: 1,
12
17
  far: 1000,
18
+ orthographicSize: 10,
13
19
  };
14
20
  function CameraComponentEditor({ component, onUpdate }) {
21
+ var _a;
15
22
  const values = Object.assign(Object.assign({}, cameraDefaults), component.properties);
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 })] }));
23
+ const projection = (_a = values.projection) !== null && _a !== void 0 ? _a : cameraDefaults.projection;
24
+ return (_jsxs(FieldGroup, { children: [_jsx(SelectField, { name: "projection", label: "Projection", values: values, onChange: onUpdate, fallback: cameraDefaults.projection, options: [...CAMERA_PROJECTION_OPTIONS] }), projection === 'perspective' ? (_jsx(NumberField, { name: "fov", label: "FOV", values: values, onChange: onUpdate, fallback: 50, min: 1, max: 179, step: 1 })) : null, projection === 'orthographic' ? (_jsx(NumberField, { name: "orthographicSize", label: "Ortho Size", values: values, onChange: onUpdate, fallback: cameraDefaults.orthographicSize, min: 0.01, step: 0.1 })) : null, _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 })] }));
17
25
  }
18
26
  function CameraComponentView({ properties, children }) {
27
+ var _a;
19
28
  const { editMode, isSelected } = useEntityRuntime();
29
+ const { size } = useThree();
20
30
  const merged = Object.assign(Object.assign({}, cameraDefaults), properties);
31
+ const projection = (_a = merged.projection) !== null && _a !== void 0 ? _a : cameraDefaults.projection;
21
32
  const fov = merged.fov;
22
33
  const near = merged.near;
23
34
  const zoom = merged.zoom;
24
35
  const far = merged.far;
25
- const cameraRef = useRef(null);
26
- useHelper(editMode && isSelected ? cameraRef : null, CameraHelper);
36
+ const orthographicSize = merged.orthographicSize;
37
+ const aspect = size.height > 0 ? size.width / size.height : 1;
38
+ const halfHeight = orthographicSize / 2;
39
+ const halfWidth = halfHeight * aspect;
40
+ const perspectiveCameraRef = useRef(null);
41
+ const orthographicCameraRef = useRef(null);
42
+ const activeCameraRef = projection === 'orthographic'
43
+ ? orthographicCameraRef
44
+ : perspectiveCameraRef;
45
+ useHelper(editMode && isSelected ? activeCameraRef : null, CameraHelper);
27
46
  useFrame(() => {
28
- if (cameraRef.current && editMode && isSelected) {
29
- cameraRef.current.updateProjectionMatrix();
30
- cameraRef.current.updateMatrixWorld();
47
+ if (!editMode || !isSelected)
48
+ return;
49
+ if (projection === 'orthographic' && orthographicCameraRef.current) {
50
+ orthographicCameraRef.current.updateProjectionMatrix();
51
+ orthographicCameraRef.current.updateMatrixWorld();
52
+ return;
53
+ }
54
+ if (perspectiveCameraRef.current) {
55
+ perspectiveCameraRef.current.updateProjectionMatrix();
56
+ perspectiveCameraRef.current.updateMatrixWorld();
31
57
  }
32
58
  });
33
- return (_jsxs(PerspectiveCamera, { ref: cameraRef, makeDefault: !editMode, fov: fov, near: near, zoom: zoom, far: far, children: [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] }));
59
+ const helperContent = 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;
60
+ if (projection === 'orthographic') {
61
+ return (_jsxs(OrthographicCamera, { ref: orthographicCameraRef, makeDefault: !editMode, near: near, zoom: zoom, far: far, left: -halfWidth, right: halfWidth, top: halfHeight, bottom: -halfHeight, children: [helperContent, children] }));
62
+ }
63
+ return (_jsxs(PerspectiveCamera, { ref: perspectiveCameraRef, makeDefault: !editMode, fov: fov, near: near, zoom: zoom, far: far, children: [helperContent, children] }));
34
64
  }
35
65
  const CameraComponent = {
36
66
  name: 'Camera',
@@ -2,9 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useRef } from 'react';
3
3
  import { gameEvents } from '../GameEvents';
4
4
  import { useEntityRuntime } from '../runtimeContext';
5
- import { FieldGroup, StringField } from './Input';
5
+ import { EventField, FieldGroup } from './Input';
6
6
  function ClickComponentEditor({ component, onUpdate }) {
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
+ 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(EventField, { name: "eventName", label: "Emit Event", values: component.properties, onChange: onUpdate, placeholder: "click" })] }));
8
8
  }
9
9
  function ClickComponentView({ children, properties }) {
10
10
  const clickValid = useRef(false);
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { FieldGroup, NumberField, SelectField } from "./Input";
2
+ import { BooleanField, FieldGroup, NumberField, SelectField } from "./Input";
3
3
  const GEOMETRY_ARGS = {
4
4
  box: {
5
5
  fields: [
@@ -61,7 +61,7 @@ function GeometryComponentEditor({ component, onUpdate, }) {
61
61
  ] }), schema.fields.map((field, index) => {
62
62
  var _a;
63
63
  return (_jsx(NumberField, { name: field.name, label: field.label, values: { [field.name]: (_a = args[index]) !== null && _a !== void 0 ? _a : field.defaultValue }, onChange: (next) => updateArg(index, next[field.name]), fallback: field.defaultValue, min: field.min, step: field.step }, field.name));
64
- })] }));
64
+ }), _jsx(BooleanField, { name: "visible", label: "Visible", values: component.properties, onChange: handleChange, fallback: true }), _jsx(BooleanField, { name: "castShadow", label: "Cast Shadow", values: component.properties, onChange: handleChange, fallback: true }), _jsx(BooleanField, { name: "receiveShadow", label: "Receive Shadow", values: component.properties, onChange: handleChange, fallback: true })] }));
65
65
  }
66
66
  // View for Geometry component
67
67
  function GeometryComponentView({ properties, children }) {
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- export type FieldType = 'vector3' | 'number' | 'string' | 'color' | 'boolean' | 'select';
2
+ export type FieldType = 'vector3' | 'number' | 'string' | 'color' | 'boolean' | 'select' | 'node' | 'event';
3
3
  interface BaseFieldDefinition {
4
4
  name: string;
5
5
  label: string;
@@ -31,6 +31,15 @@ interface SelectFieldDefinition extends BaseFieldDefinition {
31
31
  label: string;
32
32
  }[];
33
33
  }
34
+ interface NodeFieldDefinition extends BaseFieldDefinition {
35
+ type: 'node';
36
+ placeholder?: string;
37
+ includeRoot?: boolean;
38
+ }
39
+ interface EventFieldDefinition extends BaseFieldDefinition {
40
+ type: 'event';
41
+ placeholder?: string;
42
+ }
34
43
  interface CustomFieldDefinition extends BaseFieldDefinition {
35
44
  type: 'custom';
36
45
  render: (props: {
@@ -40,7 +49,7 @@ interface CustomFieldDefinition extends BaseFieldDefinition {
40
49
  onChangeMultiple: (values: Record<string, any>) => void;
41
50
  }) => React.ReactNode;
42
51
  }
43
- export type FieldDefinition = Vector3FieldDefinition | NumberFieldDefinition | StringFieldDefinition | ColorFieldDefinition | BooleanFieldDefinition | SelectFieldDefinition | CustomFieldDefinition;
52
+ export type FieldDefinition = Vector3FieldDefinition | NumberFieldDefinition | StringFieldDefinition | ColorFieldDefinition | BooleanFieldDefinition | SelectFieldDefinition | NodeFieldDefinition | EventFieldDefinition | CustomFieldDefinition;
44
53
  declare const styles: {
45
54
  input: React.CSSProperties;
46
55
  label: React.CSSProperties;
@@ -79,6 +88,19 @@ export declare function StringInput({ label, value, onChange, placeholder }: {
79
88
  onChange: (value: string) => void;
80
89
  placeholder?: string;
81
90
  }): import("react/jsx-runtime").JSX.Element;
91
+ export declare function NodeInput({ label, value, onChange, placeholder, includeRoot, }: {
92
+ label: string;
93
+ value: string;
94
+ onChange: (value: string) => void;
95
+ placeholder?: string;
96
+ includeRoot?: boolean;
97
+ }): import("react/jsx-runtime").JSX.Element;
98
+ export declare function EventInput({ label, value, onChange, placeholder, }: {
99
+ label: string;
100
+ value: string;
101
+ onChange: (value: string) => void;
102
+ placeholder?: string;
103
+ }): import("react/jsx-runtime").JSX.Element;
82
104
  export declare function BooleanInput({ label, value, onChange }: {
83
105
  label?: string;
84
106
  value: boolean;
@@ -157,6 +179,10 @@ export declare function StringField({ name, label, values, onChange, fallback, p
157
179
  export declare function ColorField({ name, label, values, onChange, fallback, }: BoundColorFieldProps): import("react/jsx-runtime").JSX.Element;
158
180
  export declare function BooleanField({ name, label, values, onChange, fallback, }: BoundBooleanFieldProps): import("react/jsx-runtime").JSX.Element;
159
181
  export declare function SelectField({ name, label, values, onChange, fallback, options, }: BoundSelectFieldProps): import("react/jsx-runtime").JSX.Element;
182
+ export declare function NodeField({ name, label, values, onChange, fallback, }: BoundStringFieldProps & {
183
+ fallback?: string;
184
+ }): import("react/jsx-runtime").JSX.Element;
185
+ export declare function EventField({ name, label, values, onChange, fallback, placeholder, }: BoundStringFieldProps): import("react/jsx-runtime").JSX.Element;
160
186
  export declare function Vector3Field({ name, label, values, onChange, fallback, snap, labelExtra, }: BoundVector3FieldProps): import("react/jsx-runtime").JSX.Element;
161
187
  interface FieldRendererProps {
162
188
  fields: FieldDefinition[];
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState } from 'react';
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { colors } from '../styles';
4
+ import { useOptionalPrefabStoreApi } from '../prefabStore';
4
5
  // ============================================================================
5
6
  // Shared Styles (derived from shared color tokens)
6
7
  // ============================================================================
@@ -277,6 +278,124 @@ export function ColorInput({ label, value, onChange }) {
277
278
  export function StringInput({ label, value, onChange, placeholder }) {
278
279
  return (_jsxs("div", { children: [label && _jsx(Label, { children: label }), _jsx("input", { type: "text", style: styles.input, value: value, onChange: e => onChange(e.target.value), placeholder: placeholder })] }));
279
280
  }
281
+ function useOptionalPrefabSnapshot() {
282
+ const store = useOptionalPrefabStoreApi();
283
+ const [state, setState] = useState(() => { var _a; return (_a = store === null || store === void 0 ? void 0 : store.getState()) !== null && _a !== void 0 ? _a : null; });
284
+ useEffect(() => {
285
+ if (!store) {
286
+ setState(null);
287
+ return;
288
+ }
289
+ setState(store.getState());
290
+ return store.subscribe(nextState => setState(nextState));
291
+ }, [store]);
292
+ return state;
293
+ }
294
+ function SearchSuggestionList({ query, options, onSelect, emptyMessage, }) {
295
+ const normalizedQuery = query.trim().toLowerCase();
296
+ const filtered = useMemo(() => {
297
+ if (!normalizedQuery)
298
+ return options.slice(0, 8);
299
+ return options.filter(option => option.searchText.includes(normalizedQuery)).slice(0, 8);
300
+ }, [normalizedQuery, options]);
301
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, children: [_jsx("input", { type: "text", style: Object.assign(Object.assign({}, styles.input), { width: '100%', textAlign: 'left' }), value: query, onChange: () => undefined, readOnly: true, "aria-hidden": true, tabIndex: -1, hidden: true }), _jsx("div", { style: {
302
+ display: 'flex',
303
+ flexDirection: 'column',
304
+ gap: 4,
305
+ maxHeight: 160,
306
+ overflowY: 'auto',
307
+ border: `1px solid ${colors.border}`,
308
+ borderRadius: 3,
309
+ background: colors.bgSurface,
310
+ padding: 4,
311
+ }, children: filtered.length === 0 ? (_jsx("div", { style: { fontSize: 11, color: colors.textMuted, padding: '4px 6px' }, children: emptyMessage })) : filtered.map(option => (_jsxs("button", { type: "button", onClick: () => onSelect(option.value), style: {
312
+ display: 'flex',
313
+ flexDirection: 'column',
314
+ alignItems: 'flex-start',
315
+ gap: 2,
316
+ border: `1px solid ${colors.border}`,
317
+ borderRadius: 3,
318
+ background: colors.bgInput,
319
+ color: colors.text,
320
+ padding: '6px 8px',
321
+ cursor: 'pointer',
322
+ textAlign: 'left',
323
+ }, children: [_jsx("span", { style: { fontSize: 11, fontWeight: 500 }, children: option.label }), option.description ? (_jsx("span", { style: { fontSize: 10, color: colors.textMuted, fontFamily: 'monospace' }, children: option.description })) : null] }, option.value))) })] }));
324
+ }
325
+ export function NodeInput({ label, value, onChange, placeholder, includeRoot = true, }) {
326
+ const prefabState = useOptionalPrefabSnapshot();
327
+ const [query, setQuery] = useState('');
328
+ const options = useMemo(() => {
329
+ var _a;
330
+ const nodesById = (_a = prefabState === null || prefabState === void 0 ? void 0 : prefabState.nodesById) !== null && _a !== void 0 ? _a : {};
331
+ const rootId = prefabState === null || prefabState === void 0 ? void 0 : prefabState.rootId;
332
+ return Object.values(nodesById)
333
+ .filter(node => includeRoot || node.id !== rootId)
334
+ .map(node => {
335
+ const nodeName = typeof node.name === 'string' && node.name.trim().length > 0 ? node.name.trim() : '(unnamed)';
336
+ return {
337
+ value: node.id,
338
+ label: nodeName,
339
+ description: node.id,
340
+ searchText: `${nodeName} ${node.id}`.toLowerCase(),
341
+ };
342
+ })
343
+ .sort((left, right) => left.label.localeCompare(right.label) || left.value.localeCompare(right.value));
344
+ }, [includeRoot, prefabState === null || prefabState === void 0 ? void 0 : prefabState.nodesById, prefabState === null || prefabState === void 0 ? void 0 : prefabState.rootId]);
345
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 6 }, children: [_jsx(StringInput, { label: label, value: value, onChange: onChange, placeholder: placeholder !== null && placeholder !== void 0 ? placeholder : 'Node id' }), options.length > 0 ? (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, children: [_jsx("input", { type: "text", style: Object.assign(Object.assign({}, styles.input), { width: '100%', textAlign: 'left' }), value: query, onChange: e => setQuery(e.target.value), placeholder: "Search nodes by name or id" }), _jsx(SearchSuggestionList, { query: query, options: options, onSelect: (nextValue) => {
346
+ onChange(nextValue);
347
+ setQuery('');
348
+ }, emptyMessage: "No matching nodes." })] })) : null] }));
349
+ }
350
+ const BUILT_IN_EVENT_OPTIONS = [
351
+ 'sensor:enter',
352
+ 'sensor:exit',
353
+ 'collision:enter',
354
+ 'collision:exit',
355
+ 'click',
356
+ ].map(eventName => ({
357
+ value: eventName,
358
+ label: eventName,
359
+ searchText: eventName.toLowerCase(),
360
+ }));
361
+ export function EventInput({ label, value, onChange, placeholder, }) {
362
+ const prefabState = useOptionalPrefabSnapshot();
363
+ const [query, setQuery] = useState('');
364
+ const options = useMemo(() => {
365
+ var _a;
366
+ const authoredEvents = new Map();
367
+ Object.values((_a = prefabState === null || prefabState === void 0 ? void 0 : prefabState.nodesById) !== null && _a !== void 0 ? _a : {}).forEach(node => {
368
+ var _a;
369
+ Object.values((_a = node.components) !== null && _a !== void 0 ? _a : {}).forEach(component => {
370
+ var _a;
371
+ Object.entries((_a = component === null || component === void 0 ? void 0 : component.properties) !== null && _a !== void 0 ? _a : {}).forEach(([key, entry]) => {
372
+ var _a, _b;
373
+ if (typeof entry !== 'string')
374
+ return;
375
+ if (!(key === 'eventName' || key.endsWith('EventName')))
376
+ return;
377
+ const eventName = entry.trim();
378
+ if (!eventName)
379
+ return;
380
+ authoredEvents.set(eventName, {
381
+ value: eventName,
382
+ label: eventName,
383
+ description: `${(_a = component === null || component === void 0 ? void 0 : component.type) !== null && _a !== void 0 ? _a : 'Component'} -> ${key}`,
384
+ searchText: `${eventName} ${(_b = component === null || component === void 0 ? void 0 : component.type) !== null && _b !== void 0 ? _b : ''} ${key}`.toLowerCase(),
385
+ });
386
+ });
387
+ });
388
+ });
389
+ const merged = new Map();
390
+ BUILT_IN_EVENT_OPTIONS.forEach(option => merged.set(option.value, option));
391
+ authoredEvents.forEach((option, key) => merged.set(key, option));
392
+ return [...merged.values()].sort((left, right) => left.value.localeCompare(right.value));
393
+ }, [prefabState === null || prefabState === void 0 ? void 0 : prefabState.nodesById]);
394
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 6 }, children: [_jsx(StringInput, { label: label, value: value, onChange: onChange, placeholder: placeholder !== null && placeholder !== void 0 ? placeholder : 'Event name' }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, children: [_jsx("input", { type: "text", style: Object.assign(Object.assign({}, styles.input), { width: '100%', textAlign: 'left' }), value: query, onChange: e => setQuery(e.target.value), placeholder: "Search built-in and authored events" }), _jsx(SearchSuggestionList, { query: query, options: options, onSelect: (nextValue) => {
395
+ onChange(nextValue);
396
+ setQuery('');
397
+ }, emptyMessage: "No matching events." })] })] }));
398
+ }
280
399
  export function BooleanInput({ label, value, onChange }) {
281
400
  return (_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between' }, children: [label && _jsx(Label, { children: label }), _jsx("input", { type: "checkbox", style: {
282
401
  height: 16,
@@ -347,6 +466,14 @@ export function SelectField({ name, label, values, onChange, fallback, options,
347
466
  var _a, _b, _c, _d;
348
467
  return (_jsx(SelectInput, { label: label, value: (_d = (_b = (_a = values[name]) !== null && _a !== void 0 ? _a : fallback) !== null && _b !== void 0 ? _b : (_c = options[0]) === null || _c === void 0 ? void 0 : _c.value) !== null && _d !== void 0 ? _d : '', onChange: bindFieldChange(name, onChange), options: options }));
349
468
  }
469
+ export function NodeField({ name, label, values, onChange, fallback = '', }) {
470
+ var _a;
471
+ return (_jsx(NodeInput, { label: label, value: (_a = values[name]) !== null && _a !== void 0 ? _a : fallback, onChange: bindFieldChange(name, onChange) }));
472
+ }
473
+ export function EventField({ name, label, values, onChange, fallback = '', placeholder, }) {
474
+ var _a;
475
+ return (_jsx(EventInput, { label: label, value: (_a = values[name]) !== null && _a !== void 0 ? _a : fallback, onChange: bindFieldChange(name, onChange), placeholder: placeholder }));
476
+ }
350
477
  export function Vector3Field({ name, label, values, onChange, fallback = [0, 0, 0], snap, labelExtra, }) {
351
478
  var _a;
352
479
  return (_jsx(Vector3Input, { label: label, value: (_a = values[name]) !== null && _a !== void 0 ? _a : fallback, onChange: bindFieldChange(name, onChange), snap: snap, labelExtra: labelExtra }));
@@ -371,6 +498,10 @@ export function FieldRenderer({ fields, values, onChange }) {
371
498
  return (_jsx(BooleanInput, { label: field.label, value: value !== null && value !== void 0 ? value : false, onChange: v => updateField(field.name, v) }, field.name));
372
499
  case 'select':
373
500
  return (_jsx(SelectInput, { label: field.label, value: (_b = value !== null && value !== void 0 ? value : (_a = field.options[0]) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : '', onChange: v => updateField(field.name, v), options: field.options }, field.name));
501
+ case 'node':
502
+ return (_jsx(NodeInput, { label: field.label, value: value !== null && value !== void 0 ? value : '', onChange: v => updateField(field.name, v), placeholder: field.placeholder, includeRoot: field.includeRoot }, field.name));
503
+ case 'event':
504
+ return (_jsx(EventInput, { label: field.label, value: value !== null && value !== void 0 ? value : '', onChange: v => updateField(field.name, v), placeholder: field.placeholder }, field.name));
374
505
  case 'custom':
375
506
  return (_jsxs("div", { children: [field.label && _jsx(Label, { children: field.label }), field.render({
376
507
  value,
@@ -4,7 +4,7 @@ import { useContext, useMemo } from 'react';
4
4
  import { BooleanField, FieldGroup, Label, ListEditor, NumberInput, SelectInput } from './Input';
5
5
  import { useAssetRuntime } from '../runtimeContext';
6
6
  import { EditorContext } from '../PrefabEditor';
7
- import { DEFAULT_REPEAT_AXES, getRepeatAxesFromModelProperties, normalizeRepeatAxes } from '../InstanceProvider';
7
+ import { getRepeatAxesFromModelProperties, normalizeRepeatAxes } from '../InstanceProvider';
8
8
  import { colors } from '../styles';
9
9
  const AXIS_OPTIONS = [
10
10
  { value: 'x', label: 'X' },
@@ -91,12 +91,7 @@ const ModelComponent = {
91
91
  name: 'Model',
92
92
  Editor: ModelComponentEditor,
93
93
  View: ModelComponentView,
94
- defaultProperties: {
95
- filename: '',
96
- instanced: false,
97
- repeat: false,
98
- repeatAxes: DEFAULT_REPEAT_AXES
99
- },
94
+ defaultProperties: {},
100
95
  getAssetRefs: (properties) => {
101
96
  if (properties.filename)
102
97
  return [{ type: 'model', path: properties.filename }];
@@ -13,7 +13,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
13
13
  import { CapsuleCollider, RigidBody, useRapier } from "@react-three/rapier";
14
14
  import { useRef, useEffect, useCallback } from 'react';
15
15
  import { useAssetRuntime, useEntityRuntime } from "../runtimeContext";
16
- import { BooleanField, FieldGroup, ListEditor, NumberField, SelectField, SelectInput, StringInput, Vector3Field } from "./Input";
16
+ import { BooleanField, EventInput, FieldGroup, ListEditor, NumberField, SelectField, SelectInput, Vector3Field } from "./Input";
17
17
  import { gameEvents, getEntityIdFromRigidBody } from "../GameEvents";
18
18
  import { colors } from "../styles";
19
19
  export function isPhysicsProps(v) {
@@ -103,7 +103,7 @@ function PhysicsEventBindingsEditor({ values, onChange, }) {
103
103
  cursor: 'pointer',
104
104
  padding: 0,
105
105
  flexShrink: 0,
106
- }, title: "Remove physics event", children: "\u00D7" })] }), _jsx(StringInput, { label: "Event Name", value: (_a = values[option.key]) !== null && _a !== void 0 ? _a : option.defaultName, onChange: (eventName) => updateEventName(option.key, eventName), placeholder: option.defaultName })] }, option.key));
106
+ }, title: "Remove physics event", children: "\u00D7" })] }), _jsx(EventInput, { label: "Event Name", value: (_a = values[option.key]) !== null && _a !== void 0 ? _a : option.defaultName, onChange: (eventName) => updateEventName(option.key, eventName), placeholder: option.defaultName })] }, option.key));
107
107
  } }));
108
108
  }
109
109
  function LockedAxisField({ label, name, values, onChange, }) {
@@ -5,7 +5,7 @@ import { SoundPicker } from '../../assetviewer/page';
5
5
  import { sound as soundManager } from '../../../helpers/SoundManager';
6
6
  import { gameEvents } from '../GameEvents';
7
7
  import { useAssetRuntime, useEntityRuntime } from '../runtimeContext';
8
- import { BooleanField, FieldGroup, FieldRenderer, ListEditor, NumberField, SelectField, StringField } from './Input';
8
+ import { BooleanField, EventField, FieldGroup, FieldRenderer, ListEditor, NumberField, SelectField } from './Input';
9
9
  import { colors } from '../styles';
10
10
  import { AudioListener } from 'three';
11
11
  const CLIP_MODE_OPTIONS = [
@@ -84,7 +84,7 @@ function SoundComponentEditor({ component, onUpdate, basePath = '' }) {
84
84
  const removeClip = (index) => {
85
85
  setClips(clips.filter((_, clipIndex) => clipIndex !== index));
86
86
  };
87
- return (_jsxs(FieldGroup, { children: [_jsx(StringField, { name: "eventName", label: "Listen Event", values: component.properties, onChange: onUpdate, placeholder: "click" }), _jsx(FieldRenderer, { fields: [
87
+ return (_jsxs(FieldGroup, { children: [_jsx(EventField, { name: "eventName", label: "Listen Event", values: component.properties, onChange: onUpdate, placeholder: "click" }), _jsx(FieldRenderer, { fields: [
88
88
  {
89
89
  name: 'clipMode',
90
90
  label: 'Clip Mode',
@@ -48,10 +48,6 @@ function TransformComponentEditor({ component, onUpdate }) {
48
48
  const TransformComponent = {
49
49
  name: 'Transform',
50
50
  Editor: TransformComponentEditor,
51
- defaultProperties: {
52
- position: [0, 0, 0],
53
- rotation: [0, 0, 0],
54
- scale: [1, 1, 1]
55
- }
51
+ defaultProperties: {}
56
52
  };
57
53
  export default TransformComponent;
@@ -12,11 +12,11 @@ export interface AssetRuntime {
12
12
  getTexture: (path: string) => Texture | null;
13
13
  getSound: (path: string) => AudioBuffer | null;
14
14
  getAssetRevision: () => string;
15
- }
16
- export interface AssetRuntimeContextValue extends AssetRuntime {
17
15
  getObject: (id: string) => Object3D | null;
18
16
  getRigidBody: (id: string) => any;
19
17
  }
18
+ export interface AssetRuntimeContextValue extends AssetRuntime {
19
+ }
20
20
  export interface EntityRuntime {
21
21
  nodeId: string;
22
22
  editMode?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.80",
3
+ "version": "0.0.82",
4
4
  "description": "high performance 3D game engine built in React",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",