react-three-game 0.0.68 → 0.0.70

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 (52) hide show
  1. package/dist/helpers/SoundManager.d.ts +2 -0
  2. package/dist/helpers/SoundManager.js +6 -0
  3. package/dist/index.d.ts +10 -7
  4. package/dist/index.js +8 -4
  5. package/dist/shared/GameCanvas.js +0 -2
  6. package/dist/tools/assetviewer/page.d.ts +5 -0
  7. package/dist/tools/assetviewer/page.js +3 -0
  8. package/dist/tools/dragdrop/DragDropLoader.d.ts +3 -2
  9. package/dist/tools/dragdrop/DragDropLoader.js +18 -3
  10. package/dist/tools/dragdrop/index.d.ts +2 -2
  11. package/dist/tools/dragdrop/index.js +1 -1
  12. package/dist/tools/dragdrop/modelLoader.d.ts +10 -0
  13. package/dist/tools/dragdrop/modelLoader.js +60 -0
  14. package/dist/tools/prefabeditor/EditorTree.js +6 -30
  15. package/dist/tools/prefabeditor/EditorTreeMenus.js +3 -3
  16. package/dist/tools/prefabeditor/EditorUI.js +6 -4
  17. package/dist/tools/prefabeditor/InstanceProvider.d.ts +2 -0
  18. package/dist/tools/prefabeditor/InstanceProvider.js +54 -52
  19. package/dist/tools/prefabeditor/PrefabEditor.d.ts +22 -0
  20. package/dist/tools/prefabeditor/PrefabEditor.js +68 -27
  21. package/dist/tools/prefabeditor/PrefabRoot.d.ts +5 -1
  22. package/dist/tools/prefabeditor/PrefabRoot.js +148 -145
  23. package/dist/tools/prefabeditor/components/ClickComponent.js +10 -7
  24. package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +10 -4
  25. package/dist/tools/prefabeditor/components/ComponentRegistry.js +6 -6
  26. package/dist/tools/prefabeditor/components/GeometryComponent.js +1 -1
  27. package/dist/tools/prefabeditor/components/Input.d.ts +16 -0
  28. package/dist/tools/prefabeditor/components/Input.js +33 -0
  29. package/dist/tools/prefabeditor/components/MaterialComponent.js +10 -2
  30. package/dist/tools/prefabeditor/components/ModelComponent.js +35 -43
  31. package/dist/tools/prefabeditor/components/PhysicsComponent.d.ts +10 -1
  32. package/dist/tools/prefabeditor/components/PhysicsComponent.js +122 -28
  33. package/dist/tools/prefabeditor/components/SoundComponent.d.ts +3 -0
  34. package/dist/tools/prefabeditor/components/SoundComponent.js +240 -0
  35. package/dist/tools/prefabeditor/components/SpotLightComponent.js +6 -1
  36. package/dist/tools/prefabeditor/components/TransformComponent.js +2 -2
  37. package/dist/tools/prefabeditor/components/index.js +2 -0
  38. package/dist/tools/prefabeditor/prefabStore.d.ts +1 -0
  39. package/dist/tools/prefabeditor/prefabStore.js +11 -13
  40. package/dist/tools/prefabeditor/sceneApi.d.ts +15 -1
  41. package/dist/tools/prefabeditor/sceneApi.js +77 -32
  42. package/dist/tools/prefabeditor/styles.d.ts +1 -0
  43. package/dist/tools/prefabeditor/styles.js +9 -0
  44. package/dist/tools/prefabeditor/types.d.ts +13 -0
  45. package/dist/tools/prefabeditor/types.js +28 -1
  46. package/dist/tools/prefabeditor/useClickValid.d.ts +13 -0
  47. package/dist/tools/prefabeditor/useClickValid.js +21 -0
  48. package/dist/tools/prefabeditor/utils.d.ts +2 -0
  49. package/dist/tools/prefabeditor/utils.js +34 -35
  50. package/package.json +1 -1
  51. package/dist/tools/prefabeditor/EditorContext.d.ts +0 -16
  52. package/dist/tools/prefabeditor/EditorContext.js +0 -9
@@ -198,7 +198,7 @@ const MaterialComponent = {
198
198
  name: 'Material',
199
199
  Editor: MaterialComponentEditor,
200
200
  View: MaterialComponentView,
201
- nonComposable: true,
201
+ isWrapper: true,
202
202
  defaultProperties: {
203
203
  materialType: 'standard',
204
204
  color: '#ffffff',
@@ -208,6 +208,14 @@ const MaterialComponent = {
208
208
  opacity: 1,
209
209
  metalness: 0,
210
210
  roughness: 1
211
- }
211
+ },
212
+ getAssetRefs: (properties) => {
213
+ const refs = [];
214
+ if (properties.texture)
215
+ refs.push({ type: 'texture', path: properties.texture });
216
+ if (properties.normalMapTexture)
217
+ refs.push({ type: 'texture', path: properties.normalMapTexture });
218
+ return refs;
219
+ },
212
220
  };
213
221
  export default MaterialComponent;
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { ModelPicker } from '../../assetviewer/page';
3
3
  import { useContext, useMemo } from 'react';
4
- import { BooleanField, FieldGroup, Label, NumberInput, SelectInput } from './Input';
5
- import { EditorContext } from '../EditorContext';
4
+ import { BooleanField, FieldGroup, Label, ListEditor, NumberInput, SelectInput } from './Input';
5
+ import { EditorContext } from '../PrefabEditor';
6
6
  import { DEFAULT_REPEAT_AXES, getRepeatAxesFromModelProperties, normalizeRepeatAxes } from '../InstanceProvider';
7
7
  import { colors } from '../styles';
8
8
  const AXIS_OPTIONS = [
@@ -18,12 +18,10 @@ function quantize(value, step) {
18
18
  return Math.round(value / step) * step;
19
19
  }
20
20
  function RepeatAxisEditor({ axes, onChange, positionSnap, }) {
21
- const addAxis = () => {
22
- const used = new Set(axes.map(axis => axis.axis));
23
- const nextAxis = AXIS_OPTIONS.find(option => !used.has(option.value));
24
- if (!nextAxis)
21
+ const addAxis = (axisValue) => {
22
+ if (!axisValue)
25
23
  return;
26
- onChange([...axes, { axis: nextAxis.value, count: 1, offset: 1 }]);
24
+ onChange([...axes, { axis: axisValue, count: 1, offset: 1 }]);
27
25
  };
28
26
  const updateAxis = (index, patch) => {
29
27
  const nextAxes = axes.map((axis, axisIndex) => axisIndex === index ? Object.assign(Object.assign({}, axis), patch) : axis);
@@ -32,41 +30,30 @@ function RepeatAxisEditor({ axes, onChange, positionSnap, }) {
32
30
  const removeAxis = (index) => {
33
31
  onChange(axes.filter((_, axisIndex) => axisIndex !== index));
34
32
  };
35
- const canAddAxis = axes.length < AXIS_OPTIONS.length;
36
- return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 8 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }, children: [_jsx(Label, { children: "Repeat Axes" }), _jsx("button", { type: "button", onClick: addAxis, disabled: !canAddAxis, style: {
37
- width: 22,
38
- height: 22,
39
- borderRadius: 3,
40
- border: `1px solid ${canAddAxis ? colors.accentBorder : colors.border}`,
41
- background: canAddAxis ? colors.accentBg : colors.bgSurface,
42
- color: canAddAxis ? colors.accent : colors.textMuted,
43
- cursor: canAddAxis ? 'pointer' : 'not-allowed',
44
- fontSize: 14,
45
- lineHeight: 1,
46
- padding: 0,
47
- }, title: canAddAxis ? 'Add repeat axis' : 'All axes already in use', children: "+" })] }), axes.map((axisConfig, index) => {
48
- const usedByOthers = new Set(axes.filter((_, axisIndex) => axisIndex !== index).map(axis => axis.axis));
49
- const axisOptions = AXIS_OPTIONS.filter(option => option.value === axisConfig.axis || !usedByOthers.has(option.value));
50
- return (_jsxs("div", { style: {
51
- display: 'flex',
52
- flexDirection: 'column',
53
- gap: 6,
54
- padding: 8,
55
- border: `1px solid ${colors.border}`,
56
- borderRadius: 4,
57
- background: colors.bgSurface,
58
- }, children: [_jsxs("div", { style: { display: 'flex', gap: 6, alignItems: 'end' }, children: [_jsx("div", { style: { flex: 1, minWidth: 0 }, children: _jsx(SelectInput, { label: "Axis", value: axisConfig.axis, onChange: (axis) => updateAxis(index, { axis: axis }), options: axisOptions }) }), index > 0 ? (_jsx("button", { type: "button", onClick: () => removeAxis(index), style: {
59
- height: 24,
60
- width: 28,
61
- borderRadius: 3,
62
- border: `1px solid ${colors.border}`,
63
- background: colors.bgInput,
64
- color: colors.text,
65
- cursor: 'pointer',
66
- padding: 0,
67
- flexShrink: 0,
68
- }, title: "Remove repeat axis", children: "\u00D7" })) : null] }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 6 }, children: [_jsxs("div", { children: [_jsx(Label, { children: "Count" }), _jsx(NumberInput, { value: axisConfig.count, onChange: (count) => updateAxis(index, { count: Math.max(1, Math.floor(count)) }), step: 1, min: 1, style: { width: '100%', minWidth: 0, boxSizing: 'border-box' } })] }), _jsxs("div", { children: [_jsx(Label, { children: "Offset" }), _jsx(NumberInput, { value: axisConfig.offset, onChange: (offset) => updateAxis(index, { offset: quantize(offset, positionSnap) }), step: positionSnap > 0 ? positionSnap : 0.1, style: { width: '100%', minWidth: 0, boxSizing: 'border-box' } })] })] })] }, `${axisConfig.axis}-${index}`));
69
- })] }));
33
+ const availableAxisOptions = AXIS_OPTIONS.filter(option => !axes.some(axis => axis.axis === option.value));
34
+ return (_jsx(ListEditor, { label: "Repeat Axes", items: axes, onAdd: addAxis, addOptions: availableAxisOptions, canAdd: availableAxisOptions.length > 0, emptyMessage: "No repeat axes added.", addButtonTitle: "Add repeat axis", addDisabledTitle: "All axes already in use", renderItem: (axisConfig, index) => {
35
+ const usedByOthers = new Set(axes.filter((_, axisIndex) => axisIndex !== index).map(axis => axis.axis));
36
+ const axisOptions = AXIS_OPTIONS.filter(option => option.value === axisConfig.axis || !usedByOthers.has(option.value));
37
+ return (_jsxs("div", { style: {
38
+ display: 'flex',
39
+ flexDirection: 'column',
40
+ gap: 6,
41
+ padding: 8,
42
+ border: `1px solid ${colors.border}`,
43
+ borderRadius: 4,
44
+ background: colors.bgSurface,
45
+ }, children: [_jsxs("div", { style: { display: 'flex', gap: 6, alignItems: 'end' }, children: [_jsx("div", { style: { flex: 1, minWidth: 0 }, children: _jsx(SelectInput, { label: "Axis", value: axisConfig.axis, onChange: (axis) => updateAxis(index, { axis: axis }), options: axisOptions }) }), _jsx("button", { type: "button", onClick: () => removeAxis(index), style: {
46
+ height: 24,
47
+ width: 28,
48
+ borderRadius: 3,
49
+ border: `1px solid ${colors.border}`,
50
+ background: colors.bgInput,
51
+ color: colors.text,
52
+ cursor: 'pointer',
53
+ padding: 0,
54
+ flexShrink: 0,
55
+ }, title: "Remove repeat axis", children: "\u00D7" })] }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 6 }, children: [_jsxs("div", { children: [_jsx(Label, { children: "Count" }), _jsx(NumberInput, { value: axisConfig.count, onChange: (count) => updateAxis(index, { count: Math.max(1, Math.floor(count)) }), step: 1, min: 1, style: { width: '100%', minWidth: 0, boxSizing: 'border-box' } })] }), _jsxs("div", { children: [_jsx(Label, { children: "Offset" }), _jsx(NumberInput, { value: axisConfig.offset, onChange: (offset) => updateAxis(index, { offset: quantize(offset, positionSnap) }), step: positionSnap > 0 ? positionSnap : 0.1, style: { width: '100%', minWidth: 0, boxSizing: 'border-box' } })] })] })] }, `${axisConfig.axis}-${index}`));
56
+ } }));
70
57
  }
71
58
  function ModelComponentEditor({ component, node, onUpdate, basePath = "" }) {
72
59
  var _a;
@@ -107,6 +94,11 @@ const ModelComponent = {
107
94
  instanced: false,
108
95
  repeat: false,
109
96
  repeatAxes: DEFAULT_REPEAT_AXES
110
- }
97
+ },
98
+ getAssetRefs: (properties) => {
99
+ if (properties.filename)
100
+ return [{ type: 'model', path: properties.filename }];
101
+ return [];
102
+ },
111
103
  };
112
104
  export default ModelComponent;
@@ -1,9 +1,18 @@
1
1
  import type { RigidBodyOptions } from "@react-three/rapier";
2
2
  import { Component } from "./ComponentRegistry";
3
- export type PhysicsProps = RigidBodyOptions & {
3
+ type PhysicsColliderType = NonNullable<RigidBodyOptions['colliders']> | 'capsule';
4
+ export type PhysicsProps = Omit<RigidBodyOptions, 'colliders'> & {
5
+ colliders?: PhysicsColliderType;
4
6
  activeCollisionTypes?: 'all' | undefined;
5
7
  linearVelocity?: [number, number, number];
6
8
  angularVelocity?: [number, number, number];
9
+ capsuleRadius?: number;
10
+ capsuleHalfHeight?: number;
11
+ sensorEnterEventName?: string;
12
+ sensorExitEventName?: string;
13
+ collisionEnterEventName?: string;
14
+ collisionExitEventName?: string;
7
15
  };
16
+ export declare function isPhysicsProps(v: any): v is PhysicsProps;
8
17
  declare const PhysicsComponent: Component;
9
18
  export default PhysicsComponent;
@@ -9,22 +9,111 @@ var __rest = (this && this.__rest) || function (s, e) {
9
9
  }
10
10
  return t;
11
11
  };
12
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
- import { RigidBody, useRapier } from "@react-three/rapier";
12
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
13
+ import { CapsuleCollider, RigidBody, useRapier } from "@react-three/rapier";
14
14
  import { useRef, useEffect, useCallback } from 'react';
15
- import { BooleanField, FieldGroup, NumberField, SelectField, Vector3Field } from "./Input";
15
+ import { BooleanField, FieldGroup, ListEditor, NumberField, SelectField, SelectInput, StringInput, Vector3Field } from "./Input";
16
16
  import { gameEvents, getEntityIdFromRigidBody } from "../GameEvents";
17
17
  import { colors } from "../styles";
18
+ export function isPhysicsProps(v) {
19
+ return (v === null || v === void 0 ? void 0 : v.type) === "fixed" || (v === null || v === void 0 ? void 0 : v.type) === "dynamic" || (v === null || v === void 0 ? void 0 : v.type) === "kinematicPosition" || (v === null || v === void 0 ? void 0 : v.type) === "kinematicVelocity";
20
+ }
18
21
  const enabledAxesFallback = [true, true, true];
19
- function LockedAxisField({ label, values, onChange, }) {
20
- const enabledTranslations = Array.isArray(values.enabledTranslations)
21
- ? values.enabledTranslations
22
+ const capsuleRadiusFallback = 0.35;
23
+ const capsuleHalfHeightFallback = 0.45;
24
+ const PHYSICS_EVENT_OPTIONS = [
25
+ {
26
+ key: 'sensorEnterEventName',
27
+ label: 'Sensor Enter',
28
+ defaultName: 'sensor:enter',
29
+ requiresSensor: true,
30
+ },
31
+ {
32
+ key: 'sensorExitEventName',
33
+ label: 'Sensor Exit',
34
+ defaultName: 'sensor:exit',
35
+ requiresSensor: true,
36
+ },
37
+ {
38
+ key: 'collisionEnterEventName',
39
+ label: 'Collision Enter',
40
+ defaultName: 'collision:enter',
41
+ requiresSensor: false,
42
+ },
43
+ {
44
+ key: 'collisionExitEventName',
45
+ label: 'Collision Exit',
46
+ defaultName: 'collision:exit',
47
+ requiresSensor: false,
48
+ },
49
+ ];
50
+ function getPhysicsEventOption(key) {
51
+ return PHYSICS_EVENT_OPTIONS.find(option => option.key === key);
52
+ }
53
+ function getConfiguredPhysicsEvents(values) {
54
+ return PHYSICS_EVENT_OPTIONS.filter(option => typeof values[option.key] === 'string' && values[option.key].trim().length > 0);
55
+ }
56
+ function getAvailablePhysicsEvents(values, currentKey) {
57
+ const configuredKeys = new Set(getConfiguredPhysicsEvents(values).map(option => option.key));
58
+ return PHYSICS_EVENT_OPTIONS
59
+ .filter(option => option.key === currentKey || !configuredKeys.has(option.key))
60
+ .map(option => ({ value: option.key, label: option.label }));
61
+ }
62
+ function PhysicsEventBindingsEditor({ values, onChange, }) {
63
+ const configuredEvents = getConfiguredPhysicsEvents(values);
64
+ const nextEventOptions = getAvailablePhysicsEvents(values);
65
+ const addEvent = (eventKey) => {
66
+ if (!eventKey)
67
+ return;
68
+ const option = getPhysicsEventOption(eventKey);
69
+ if (!option)
70
+ return;
71
+ onChange(Object.assign({ [option.key]: option.defaultName }, (option.requiresSensor ? { sensor: true } : null)));
72
+ };
73
+ const updateEventKey = (currentKey, nextKey) => {
74
+ const nextOption = getPhysicsEventOption(nextKey);
75
+ if (!nextOption)
76
+ return;
77
+ onChange(Object.assign({ [currentKey]: undefined, [nextOption.key]: values[currentKey] || nextOption.defaultName }, (nextOption.requiresSensor ? { sensor: true } : null)));
78
+ };
79
+ const updateEventName = (key, eventName) => {
80
+ onChange({ [key]: eventName });
81
+ };
82
+ const removeEvent = (key) => {
83
+ onChange({ [key]: undefined });
84
+ };
85
+ return (_jsx(ListEditor, { label: "Events", items: configuredEvents, onAdd: addEvent, addOptions: nextEventOptions, canAdd: nextEventOptions.length > 0, emptyMessage: "No physics events configured.", addButtonTitle: "Add physics event", addDisabledTitle: "All physics events already added", renderItem: (option) => {
86
+ var _a;
87
+ return (_jsxs("div", { style: {
88
+ display: 'flex',
89
+ flexDirection: 'column',
90
+ gap: 6,
91
+ padding: 8,
92
+ border: `1px solid ${colors.border}`,
93
+ borderRadius: 4,
94
+ background: colors.bgSurface,
95
+ }, children: [_jsxs("div", { style: { display: 'flex', gap: 6, alignItems: 'end' }, children: [_jsx("div", { style: { flex: 1, minWidth: 0 }, children: _jsx(SelectInput, { label: "Type", value: option.key, onChange: (nextKey) => updateEventKey(option.key, nextKey), options: getAvailablePhysicsEvents(values, option.key) }) }), _jsx("button", { type: "button", onClick: () => removeEvent(option.key), style: {
96
+ height: 24,
97
+ width: 28,
98
+ borderRadius: 3,
99
+ border: `1px solid ${colors.border}`,
100
+ background: colors.bgInput,
101
+ color: colors.text,
102
+ cursor: 'pointer',
103
+ padding: 0,
104
+ flexShrink: 0,
105
+ }, 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
+ } }));
107
+ }
108
+ function LockedAxisField({ label, name, values, onChange, }) {
109
+ const enabledAxes = Array.isArray(values[name])
110
+ ? values[name]
22
111
  : enabledAxesFallback;
23
112
  const axisLabels = ['X', 'Y', 'Z'];
24
113
  const toggleAxisLock = (index) => {
25
- const nextEnabledTranslations = [...enabledTranslations];
26
- nextEnabledTranslations[index] = !nextEnabledTranslations[index];
27
- onChange({ enabledTranslations: nextEnabledTranslations });
114
+ const nextEnabledAxes = [...enabledAxes];
115
+ nextEnabledAxes[index] = !nextEnabledAxes[index];
116
+ onChange({ [name]: nextEnabledAxes });
28
117
  };
29
118
  return (_jsxs("div", { children: [_jsxs("div", { style: {
30
119
  display: 'flex',
@@ -42,7 +131,7 @@ function LockedAxisField({ label, values, onChange, }) {
42
131
  fontSize: '10px',
43
132
  color: colors.textDim,
44
133
  }, children: "Active means locked" })] }), _jsx("div", { style: { display: 'flex', gap: 4 }, children: axisLabels.map((axisLabel, index) => {
45
- const isLocked = !enabledTranslations[index];
134
+ const isLocked = !enabledAxes[index];
46
135
  return (_jsx("button", { type: "button", onClick: () => toggleAxisLock(index), style: {
47
136
  flex: 1,
48
137
  backgroundColor: isLocked ? colors.dangerBg : colors.bgInput,
@@ -67,20 +156,22 @@ function PhysicsComponentEditor({ component, onUpdate }) {
67
156
  { value: 'trimesh', label: 'Trimesh (exact)' },
68
157
  { value: 'cuboid', label: 'Cuboid (box)' },
69
158
  { value: 'ball', label: 'Ball (sphere)' },
70
- ] }), _jsx(NumberField, { name: "mass", label: "Mass", values: component.properties, onChange: onUpdate, fallback: 1, step: 0.1, min: 0 }), _jsx(NumberField, { name: "restitution", label: "Restitution (Bounciness)", values: component.properties, onChange: onUpdate, fallback: 0, min: 0, max: 1, step: 0.1 }), _jsx(NumberField, { name: "friction", label: "Friction", values: component.properties, onChange: onUpdate, fallback: 0.5, min: 0, step: 0.1 }), _jsx(NumberField, { name: "linearDamping", label: "Linear Damping", values: component.properties, onChange: onUpdate, fallback: 0, min: 0, step: 0.1 }), _jsx(NumberField, { name: "angularDamping", label: "Angular Damping", values: component.properties, onChange: onUpdate, fallback: 0, min: 0, step: 0.1 }), _jsx(NumberField, { name: "gravityScale", label: "Gravity Scale", values: component.properties, onChange: onUpdate, fallback: 1, step: 0.1 }), _jsx(Vector3Field, { name: "linearVelocity", label: "Linear Velocity", values: component.properties, onChange: onUpdate, fallback: [0, 0, 0] }), _jsx(Vector3Field, { name: "angularVelocity", label: "Angular Velocity", values: component.properties, onChange: onUpdate, fallback: [0, 0, 0] }), _jsx(LockedAxisField, { label: "Lock Movement", values: component.properties, onChange: onUpdate }), _jsx(BooleanField, { name: "sensor", label: "Sensor (Trigger Only)", values: component.properties, onChange: onUpdate, fallback: false }), _jsx(SelectField, { name: "activeCollisionTypes", label: "Collision Detection", values: component.properties, onChange: onUpdate, options: [
159
+ { value: 'capsule', label: 'Capsule' },
160
+ ] }), component.properties.colliders === 'capsule' ? (_jsxs(_Fragment, { children: [_jsx(NumberField, { name: "capsuleRadius", label: "Capsule Radius", values: component.properties, onChange: onUpdate, fallback: capsuleRadiusFallback, min: 0.01, step: 0.01 }), _jsx(NumberField, { name: "capsuleHalfHeight", label: "Capsule Half Height", values: component.properties, onChange: onUpdate, fallback: capsuleHalfHeightFallback, min: 0.01, step: 0.01 })] })) : null, _jsx(NumberField, { name: "mass", label: "Mass", values: component.properties, onChange: onUpdate, fallback: 1, step: 0.1, min: 0 }), _jsx(NumberField, { name: "restitution", label: "Restitution (Bounciness)", values: component.properties, onChange: onUpdate, fallback: 0, min: 0, max: 1, step: 0.1 }), _jsx(NumberField, { name: "friction", label: "Friction", values: component.properties, onChange: onUpdate, fallback: 0.5, min: 0, step: 0.1 }), _jsx(NumberField, { name: "linearDamping", label: "Linear Damping", values: component.properties, onChange: onUpdate, fallback: 0, min: 0, step: 0.1 }), _jsx(NumberField, { name: "angularDamping", label: "Angular Damping", values: component.properties, onChange: onUpdate, fallback: 0, min: 0, step: 0.1 }), _jsx(NumberField, { name: "gravityScale", label: "Gravity Scale", values: component.properties, onChange: onUpdate, fallback: 1, step: 0.1 }), _jsx(Vector3Field, { name: "linearVelocity", label: "Linear Velocity", values: component.properties, onChange: onUpdate, fallback: [0, 0, 0] }), _jsx(Vector3Field, { name: "angularVelocity", label: "Angular Velocity", values: component.properties, onChange: onUpdate, fallback: [0, 0, 0] }), _jsx(LockedAxisField, { label: "Lock Movement", name: "enabledTranslations", values: component.properties, onChange: onUpdate }), _jsx(LockedAxisField, { label: "Lock Rotations", name: "enabledRotations", values: component.properties, onChange: onUpdate }), _jsx(BooleanField, { name: "sensor", label: "Sensor (Trigger Only)", values: component.properties, onChange: onUpdate, fallback: false }), _jsx(PhysicsEventBindingsEditor, { values: component.properties, onChange: onUpdate }), _jsx(SelectField, { name: "activeCollisionTypes", label: "Collision Detection", values: component.properties, onChange: onUpdate, options: [
71
161
  { value: '', label: 'Default (Dynamic only)' },
72
162
  { value: 'all', label: 'All (includes kinematic & fixed)' },
73
163
  ] })] }));
74
164
  }
75
165
  function PhysicsComponentView({ properties, children, position, rotation, scale, editMode, nodeId, registerRigidBodyRef }) {
76
- const { type, colliders, sensor, activeCollisionTypes, linearVelocity = [0, 0, 0], angularVelocity = [0, 0, 0], enabledTranslations = enabledAxesFallback } = properties, otherProps = __rest(properties, ["type", "colliders", "sensor", "activeCollisionTypes", "linearVelocity", "angularVelocity", "enabledTranslations"]);
166
+ const { type, colliders, sensor, activeCollisionTypes, linearVelocity = [0, 0, 0], angularVelocity = [0, 0, 0], capsuleRadius = capsuleRadiusFallback, capsuleHalfHeight = capsuleHalfHeightFallback, sensorEnterEventName, sensorExitEventName, collisionEnterEventName, collisionExitEventName, enabledTranslations = enabledAxesFallback, enabledRotations = enabledAxesFallback } = properties, otherProps = __rest(properties, ["type", "colliders", "sensor", "activeCollisionTypes", "linearVelocity", "angularVelocity", "capsuleRadius", "capsuleHalfHeight", "sensorEnterEventName", "sensorExitEventName", "collisionEnterEventName", "collisionExitEventName", "enabledTranslations", "enabledRotations"]);
77
167
  const colliderType = colliders || (type === 'fixed' ? 'trimesh' : 'hull');
168
+ const usesManualCapsuleCollider = colliderType === 'capsule';
78
169
  const rigidBodyRef = useRef(null);
79
170
  const linearVelocityKey = linearVelocity.join(',');
80
171
  const angularVelocityKey = angularVelocity.join(',');
81
172
  const rbKey = editMode
82
- ? `${type || 'dynamic'}_${colliderType}_${position === null || position === void 0 ? void 0 : position.join(',')}_${rotation === null || rotation === void 0 ? void 0 : rotation.join(',')}`
83
- : `${type || 'dynamic'}_${colliderType}`;
173
+ ? `${type || 'dynamic'}_${colliderType}_${capsuleRadius}_${capsuleHalfHeight}_${position === null || position === void 0 ? void 0 : position.join(',')}_${rotation === null || rotation === void 0 ? void 0 : rotation.join(',')}`
174
+ : `${type || 'dynamic'}_${colliderType}_${capsuleRadius}_${capsuleHalfHeight}`;
84
175
  // Try to get rapier context - will be null if not inside <Physics>
85
176
  let rapier = null;
86
177
  try {
@@ -135,54 +226,57 @@ function PhysicsComponentView({ properties, children, position, rotation, scale,
135
226
  }, [rbKey, angularVelocityKey]);
136
227
  // Event handlers for physics interactions
137
228
  const handleIntersectionEnter = useCallback((payload) => {
138
- if (!nodeId)
229
+ if (!nodeId || !sensorEnterEventName)
139
230
  return;
140
- gameEvents.emit('sensor:enter', {
231
+ gameEvents.emit(sensorEnterEventName, {
141
232
  sourceEntityId: nodeId,
142
233
  targetEntityId: getEntityIdFromRigidBody(payload.other.rigidBody),
143
234
  targetRigidBody: payload.other.rigidBody,
144
235
  });
145
- }, [nodeId]);
236
+ }, [nodeId, sensorEnterEventName]);
146
237
  const handleIntersectionExit = useCallback((payload) => {
147
- if (!nodeId)
238
+ if (!nodeId || !sensorExitEventName)
148
239
  return;
149
- gameEvents.emit('sensor:exit', {
240
+ gameEvents.emit(sensorExitEventName, {
150
241
  sourceEntityId: nodeId,
151
242
  targetEntityId: getEntityIdFromRigidBody(payload.other.rigidBody),
152
243
  targetRigidBody: payload.other.rigidBody,
153
244
  });
154
- }, [nodeId]);
245
+ }, [nodeId, sensorExitEventName]);
155
246
  const handleCollisionEnter = useCallback((payload) => {
156
- if (!nodeId)
247
+ if (!nodeId || !collisionEnterEventName)
157
248
  return;
158
- gameEvents.emit('collision:enter', {
249
+ gameEvents.emit(collisionEnterEventName, {
159
250
  sourceEntityId: nodeId,
160
251
  targetEntityId: getEntityIdFromRigidBody(payload.other.rigidBody),
161
252
  targetRigidBody: payload.other.rigidBody,
162
253
  });
163
- }, [nodeId]);
254
+ }, [collisionEnterEventName, nodeId]);
164
255
  const handleCollisionExit = useCallback((payload) => {
165
- if (!nodeId)
256
+ if (!nodeId || !collisionExitEventName)
166
257
  return;
167
- gameEvents.emit('collision:exit', {
258
+ gameEvents.emit(collisionExitEventName, {
168
259
  sourceEntityId: nodeId,
169
260
  targetEntityId: getEntityIdFromRigidBody(payload.other.rigidBody),
170
261
  targetRigidBody: payload.other.rigidBody,
171
262
  });
172
- }, [nodeId]);
173
- return (_jsx(RigidBody, Object.assign({ ref: rigidBodyRef, type: type, colliders: colliderType, position: position, rotation: rotation, scale: scale, sensor: sensor, enabledTranslations: enabledTranslations, userData: { entityId: nodeId }, onIntersectionEnter: handleIntersectionEnter, onIntersectionExit: handleIntersectionExit, onCollisionEnter: handleCollisionEnter, onCollisionExit: handleCollisionExit }, otherProps, { children: children }), rbKey));
263
+ }, [collisionExitEventName, nodeId]);
264
+ return (_jsxs(RigidBody, Object.assign({ ref: rigidBodyRef, type: type, colliders: usesManualCapsuleCollider ? false : colliderType, position: position, rotation: rotation, scale: scale, sensor: sensor, enabledTranslations: enabledTranslations, enabledRotations: enabledRotations, userData: { entityId: nodeId }, onIntersectionEnter: handleIntersectionEnter, onIntersectionExit: handleIntersectionExit, onCollisionEnter: handleCollisionEnter, onCollisionExit: handleCollisionExit }, otherProps, { children: [usesManualCapsuleCollider ? _jsx(CapsuleCollider, { args: [capsuleHalfHeight, capsuleRadius], sensor: sensor }) : null, children] }), rbKey));
174
265
  }
175
266
  const PhysicsComponent = {
176
267
  name: 'Physics',
177
268
  Editor: PhysicsComponentEditor,
178
269
  View: PhysicsComponentView,
179
- nonComposable: true,
270
+ isWrapper: true,
180
271
  defaultProperties: {
181
272
  type: 'dynamic',
182
273
  colliders: 'hull',
274
+ capsuleRadius: capsuleRadiusFallback,
275
+ capsuleHalfHeight: capsuleHalfHeightFallback,
183
276
  linearVelocity: [0, 0, 0],
184
277
  angularVelocity: [0, 0, 0],
185
278
  enabledTranslations: [true, true, true],
279
+ enabledRotations: [true, true, true],
186
280
  }
187
281
  };
188
282
  export default PhysicsComponent;
@@ -0,0 +1,3 @@
1
+ import { Component } from './ComponentRegistry';
2
+ declare const SoundComponent: Component;
3
+ export default SoundComponent;
@@ -0,0 +1,240 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
3
+ import { useThree } from '@react-three/fiber';
4
+ import { SoundPicker } from '../../assetviewer/page';
5
+ import { sound as soundManager } from '../../../helpers/SoundManager';
6
+ import { gameEvents } from '../GameEvents';
7
+ import { BooleanField, FieldGroup, FieldRenderer, ListEditor, NumberField, SelectField, StringField } from './Input';
8
+ import { colors } from '../styles';
9
+ import { AudioListener } from 'three';
10
+ const CLIP_MODE_OPTIONS = [
11
+ { value: 'single', label: 'Single Clip' },
12
+ { value: 'random', label: 'Random Clip' },
13
+ { value: 'sequence', label: 'Sequence' },
14
+ ];
15
+ let sharedAudioListener = null;
16
+ function getSharedAudioListener() {
17
+ if (!sharedAudioListener) {
18
+ sharedAudioListener = new AudioListener();
19
+ }
20
+ return sharedAudioListener;
21
+ }
22
+ function normalizeClips(clips) {
23
+ return (clips !== null && clips !== void 0 ? clips : []).map(clip => clip.trim()).filter(Boolean);
24
+ }
25
+ function clampRange(min, max, fallbackMin, fallbackMax) {
26
+ const safeMin = Number.isFinite(min) ? Number(min) : fallbackMin;
27
+ const safeMax = Number.isFinite(max) ? Number(max) : fallbackMax;
28
+ return safeMin <= safeMax ? [safeMin, safeMax] : [safeMax, safeMin];
29
+ }
30
+ function sampleRange(min, max) {
31
+ return min + Math.random() * (max - min);
32
+ }
33
+ function getPitchValue(properties) {
34
+ if (properties.randomizePitch) {
35
+ const [pitchFloor, pitchCeiling] = clampRange(properties.minPitch, properties.maxPitch, 0.96, 1.04);
36
+ return sampleRange(pitchFloor, pitchCeiling);
37
+ }
38
+ return Number.isFinite(properties.pitch) ? Number(properties.pitch) : 1;
39
+ }
40
+ function getVolumeValue(properties) {
41
+ if (properties.randomizeVolume) {
42
+ const [volumeFloor, volumeCeiling] = clampRange(properties.minVolume, properties.maxVolume, 0.9, 1);
43
+ return sampleRange(volumeFloor, volumeCeiling);
44
+ }
45
+ return Number.isFinite(properties.volume) ? Number(properties.volume) : 1;
46
+ }
47
+ function resolveClipPaths({ path, clips, clipMode }) {
48
+ const normalizedClips = normalizeClips(clips);
49
+ if (normalizedClips.length > 0) {
50
+ return { paths: normalizedClips, mode: clipMode !== null && clipMode !== void 0 ? clipMode : 'random' };
51
+ }
52
+ return path ? { paths: [path], mode: 'single' } : { paths: [], mode: 'single' };
53
+ }
54
+ function pickClip(paths, mode, sequenceIndexRef) {
55
+ if (paths.length <= 1 || mode === 'single') {
56
+ return paths[0];
57
+ }
58
+ if (mode === 'sequence') {
59
+ const clip = paths[sequenceIndexRef.current % paths.length];
60
+ sequenceIndexRef.current += 1;
61
+ return clip;
62
+ }
63
+ return paths[Math.floor(Math.random() * paths.length)];
64
+ }
65
+ function SoundComponentEditor({ component, onUpdate, basePath = '' }) {
66
+ const clips = normalizeClips(component.properties.clips);
67
+ const randomizePitch = Boolean(component.properties.randomizePitch);
68
+ const randomizeVolume = Boolean(component.properties.randomizeVolume);
69
+ const positional = Boolean(component.properties.positional);
70
+ const addClip = () => {
71
+ onUpdate({ clips: [...clips, ''] });
72
+ };
73
+ const updateClip = (index, nextPath) => {
74
+ onUpdate({
75
+ clips: clips.map((clip, clipIndex) => clipIndex === index ? nextPath : clip),
76
+ });
77
+ };
78
+ const removeClip = (index) => {
79
+ onUpdate({ clips: clips.filter((_, clipIndex) => clipIndex !== index) });
80
+ };
81
+ return (_jsxs(FieldGroup, { children: [_jsx(SoundPicker, { value: component.properties.path, onChange: (path) => onUpdate({ path: path !== null && path !== void 0 ? path : '' }), basePath: basePath }), _jsx(StringField, { name: "eventName", label: "Listen Event", values: component.properties, onChange: onUpdate, placeholder: "click" }), _jsx(FieldRenderer, { fields: [
82
+ {
83
+ name: 'clipMode',
84
+ label: 'Clip Mode',
85
+ type: 'select',
86
+ options: CLIP_MODE_OPTIONS.map(option => ({ value: option.value, label: option.label })),
87
+ },
88
+ ], values: component.properties, onChange: onUpdate }), _jsx(ListEditor, { label: "Clips", items: clips, onAdd: addClip, emptyMessage: "No clips added.", addButtonTitle: "Add clip", addDisabledTitle: "Add clip", renderItem: (clip, index) => (_jsxs("div", { style: {
89
+ display: 'flex',
90
+ gap: 6,
91
+ alignItems: 'end',
92
+ padding: 8,
93
+ border: `1px solid ${colors.border}`,
94
+ borderRadius: 4,
95
+ background: colors.bgSurface,
96
+ }, children: [_jsx("div", { style: { flex: 1, minWidth: 0 }, children: _jsx(SoundPicker, { value: clip || undefined, onChange: (nextPath) => updateClip(index, nextPath !== null && nextPath !== void 0 ? nextPath : ''), basePath: basePath }) }), _jsx("button", { type: "button", onClick: () => removeClip(index), style: {
97
+ height: 24,
98
+ width: 28,
99
+ borderRadius: 3,
100
+ border: `1px solid ${colors.border}`,
101
+ background: colors.bgInput,
102
+ color: colors.text,
103
+ cursor: 'pointer',
104
+ padding: 0,
105
+ flexShrink: 0,
106
+ }, title: "Remove clip", children: "\u00D7" })] }, `${clip}-${index}`)) }), _jsx(BooleanField, { name: "positional", label: "Positional", values: component.properties, onChange: onUpdate, fallback: false }), positional ? (_jsxs(_Fragment, { children: [_jsx(NumberField, { name: "refDistance", label: "Ref Distance", values: component.properties, onChange: onUpdate, fallback: 1, min: 0.01, step: 0.1 }), _jsx(NumberField, { name: "maxDistance", label: "Max Distance", values: component.properties, onChange: onUpdate, fallback: 24, min: 0.01, step: 0.1 }), _jsx(NumberField, { name: "rolloffFactor", label: "Rolloff", values: component.properties, onChange: onUpdate, fallback: 1, min: 0, step: 0.1 }), _jsx(SelectField, { name: "distanceModel", label: "Distance Model", values: component.properties, onChange: onUpdate, fallback: "inverse", options: [
107
+ { value: 'inverse', label: 'Inverse' },
108
+ { value: 'linear', label: 'Linear' },
109
+ { value: 'exponential', label: 'Exponential' },
110
+ ] })] })) : null, _jsx(BooleanField, { name: "randomizePitch", label: "Random Pitch", values: component.properties, onChange: onUpdate, fallback: false }), randomizePitch ? (_jsxs(_Fragment, { children: [_jsx(NumberField, { name: "minPitch", label: "Min Pitch", values: component.properties, onChange: onUpdate, fallback: 0.96, step: 0.01, min: 0.1 }), _jsx(NumberField, { name: "maxPitch", label: "Max Pitch", values: component.properties, onChange: onUpdate, fallback: 1.04, step: 0.01, min: 0.1 })] })) : (_jsx(NumberField, { name: "pitch", label: "Pitch", values: component.properties, onChange: onUpdate, fallback: 1, step: 0.01, min: 0.1 })), _jsx(BooleanField, { name: "randomizeVolume", label: "Random Volume", values: component.properties, onChange: onUpdate, fallback: false }), randomizeVolume ? (_jsxs(_Fragment, { children: [_jsx(NumberField, { name: "minVolume", label: "Min Volume", values: component.properties, onChange: onUpdate, fallback: 0.9, step: 0.01, min: 0 }), _jsx(NumberField, { name: "maxVolume", label: "Max Volume", values: component.properties, onChange: onUpdate, fallback: 1, step: 0.01, min: 0 })] })) : (_jsx(NumberField, { name: "volume", label: "Volume", values: component.properties, onChange: onUpdate, fallback: 1, step: 0.01, min: 0 }))] }));
111
+ }
112
+ function payloadMatchesNode(nodeId, payload) {
113
+ if (!nodeId || !payload || typeof payload !== 'object')
114
+ return true;
115
+ const eventPayload = payload;
116
+ const ids = [eventPayload.sourceEntityId, eventPayload.targetEntityId, eventPayload.instanceEntityId];
117
+ const hasEntityIds = ids.some(id => typeof id === 'string');
118
+ return hasEntityIds ? ids.includes(nodeId) : true;
119
+ }
120
+ function SoundComponentView({ properties, editMode, nodeId, children, loadedSounds }) {
121
+ const { camera } = useThree();
122
+ const { eventName, positional = false, refDistance = 1, maxDistance = 24, rolloffFactor = 1, distanceModel = 'inverse' } = properties;
123
+ const sequenceIndexRef = useRef(0);
124
+ const listenerRef = useRef(null);
125
+ const positionalAudioRef = useRef(null);
126
+ const { paths, mode } = resolveClipPaths(properties);
127
+ if (!listenerRef.current) {
128
+ listenerRef.current = getSharedAudioListener();
129
+ }
130
+ useEffect(() => {
131
+ var _a;
132
+ if (!positional) {
133
+ return;
134
+ }
135
+ const listener = listenerRef.current;
136
+ if (!listener) {
137
+ return;
138
+ }
139
+ if (listener.parent !== camera) {
140
+ (_a = listener.parent) === null || _a === void 0 ? void 0 : _a.remove(listener);
141
+ camera.add(listener);
142
+ }
143
+ return () => {
144
+ if (listener.parent === camera) {
145
+ camera.remove(listener);
146
+ }
147
+ };
148
+ }, [camera, positional]);
149
+ useEffect(() => {
150
+ const audio = positionalAudioRef.current;
151
+ if (!audio) {
152
+ return;
153
+ }
154
+ audio.setRefDistance(refDistance);
155
+ audio.setMaxDistance(maxDistance);
156
+ audio.setRolloffFactor(rolloffFactor);
157
+ audio.setDistanceModel(distanceModel);
158
+ }, [distanceModel, maxDistance, refDistance, rolloffFactor]);
159
+ useEffect(() => {
160
+ if (editMode || paths.length === 0 || !eventName) {
161
+ return;
162
+ }
163
+ return gameEvents.on(eventName, (payload) => {
164
+ if (!payloadMatchesNode(nodeId, payload)) {
165
+ return;
166
+ }
167
+ const clip = pickClip(paths, mode, sequenceIndexRef);
168
+ if (!clip)
169
+ return;
170
+ const pitch = getPitchValue(properties);
171
+ const volume = getVolumeValue(properties);
172
+ if (!positional) {
173
+ const loadedBuffer = loadedSounds === null || loadedSounds === void 0 ? void 0 : loadedSounds[clip];
174
+ if (loadedBuffer && !soundManager.hasBuffer(clip)) {
175
+ soundManager.setBuffer(clip, loadedBuffer);
176
+ }
177
+ if (soundManager.hasBuffer(clip)) {
178
+ soundManager.playSync(clip, { pitch, volume });
179
+ return;
180
+ }
181
+ void soundManager.play(clip, { pitch, volume });
182
+ return;
183
+ }
184
+ const audio = positionalAudioRef.current;
185
+ const listener = listenerRef.current;
186
+ const buffer = loadedSounds === null || loadedSounds === void 0 ? void 0 : loadedSounds[clip];
187
+ if (!audio || !listener || !buffer) {
188
+ return;
189
+ }
190
+ void listener.context.resume();
191
+ if (audio.isPlaying) {
192
+ audio.stop();
193
+ }
194
+ audio.setBuffer(buffer);
195
+ audio.setLoop(false);
196
+ audio.setPlaybackRate(pitch);
197
+ audio.setVolume(volume);
198
+ audio.play();
199
+ });
200
+ }, [editMode, eventName, loadedSounds, mode, nodeId, paths, positional, properties]);
201
+ return (_jsxs(_Fragment, { children: [positional && listenerRef.current ? _jsx("positionalAudio", { ref: positionalAudioRef, args: [listenerRef.current] }) : null, children] }));
202
+ }
203
+ const SoundComponent = {
204
+ name: 'Sound',
205
+ Editor: SoundComponentEditor,
206
+ View: SoundComponentView,
207
+ defaultProperties: {
208
+ path: '',
209
+ eventName: '',
210
+ clips: [],
211
+ clipMode: 'single',
212
+ positional: false,
213
+ refDistance: 1,
214
+ maxDistance: 24,
215
+ rolloffFactor: 1,
216
+ distanceModel: 'inverse',
217
+ pitch: 1,
218
+ randomizePitch: false,
219
+ minPitch: 0.96,
220
+ maxPitch: 1.04,
221
+ volume: 1,
222
+ randomizeVolume: false,
223
+ minVolume: 0.9,
224
+ maxVolume: 1,
225
+ },
226
+ getAssetRefs: (properties) => {
227
+ const refs = [];
228
+ if (properties.path)
229
+ refs.push({ type: 'sound', path: properties.path });
230
+ if (Array.isArray(properties.clips)) {
231
+ properties.clips.forEach((clip) => {
232
+ if (typeof clip === 'string' && clip.trim().length > 0) {
233
+ refs.push({ type: 'sound', path: clip });
234
+ }
235
+ });
236
+ }
237
+ return refs;
238
+ },
239
+ };
240
+ export default SoundComponent;