react-three-game 0.0.56 → 0.0.57

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 (59) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +1 -1
  3. package/dist/shared/GameCanvas.js +1 -3
  4. package/dist/tools/assetviewer/page.js +35 -14
  5. package/dist/tools/prefabeditor/Dropdown.d.ts +15 -0
  6. package/dist/tools/prefabeditor/Dropdown.js +82 -0
  7. package/dist/tools/prefabeditor/EditorContext.d.ts +5 -0
  8. package/dist/tools/prefabeditor/EditorTree.js +138 -56
  9. package/dist/tools/prefabeditor/EditorUI.js +1 -1
  10. package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
  11. package/dist/tools/prefabeditor/PrefabEditor.js +13 -2
  12. package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -0
  13. package/dist/tools/prefabeditor/PrefabRoot.js +120 -34
  14. package/dist/tools/prefabeditor/components/AmbientLightComponent.js +3 -7
  15. package/dist/tools/prefabeditor/components/CameraComponent.d.ts +3 -0
  16. package/dist/tools/prefabeditor/components/CameraComponent.js +25 -0
  17. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +2 -2
  18. package/dist/tools/prefabeditor/components/EnvironmentComponent.d.ts +3 -0
  19. package/dist/tools/prefabeditor/components/EnvironmentComponent.js +15 -0
  20. package/dist/tools/prefabeditor/components/GeometryComponent.js +46 -46
  21. package/dist/tools/prefabeditor/components/Input.d.ts +51 -1
  22. package/dist/tools/prefabeditor/components/Input.js +73 -21
  23. package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +8 -2
  24. package/dist/tools/prefabeditor/components/MaterialComponent.js +122 -14
  25. package/dist/tools/prefabeditor/components/ModelComponent.js +44 -3
  26. package/dist/tools/prefabeditor/components/PhysicsComponent.js +16 -81
  27. package/dist/tools/prefabeditor/components/SpotLightComponent.js +4 -12
  28. package/dist/tools/prefabeditor/components/TextComponent.js +7 -53
  29. package/dist/tools/prefabeditor/components/TransformComponent.js +18 -8
  30. package/dist/tools/prefabeditor/components/index.js +5 -1
  31. package/dist/tools/prefabeditor/styles.d.ts +5 -2
  32. package/dist/tools/prefabeditor/styles.js +7 -3
  33. package/dist/tools/prefabeditor/utils.d.ts +4 -3
  34. package/dist/tools/prefabeditor/utils.js +53 -5
  35. package/package.json +1 -1
  36. package/src/index.ts +7 -0
  37. package/src/shared/GameCanvas.tsx +0 -3
  38. package/src/tools/assetviewer/page.tsx +77 -45
  39. package/src/tools/prefabeditor/Dropdown.tsx +112 -0
  40. package/src/tools/prefabeditor/EditorContext.tsx +5 -0
  41. package/src/tools/prefabeditor/EditorTree.tsx +234 -101
  42. package/src/tools/prefabeditor/EditorUI.tsx +1 -1
  43. package/src/tools/prefabeditor/PrefabEditor.tsx +17 -4
  44. package/src/tools/prefabeditor/PrefabRoot.tsx +208 -58
  45. package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +5 -11
  46. package/src/tools/prefabeditor/components/CameraComponent.tsx +80 -0
  47. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +2 -2
  48. package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +47 -0
  49. package/src/tools/prefabeditor/components/GeometryComponent.tsx +69 -63
  50. package/src/tools/prefabeditor/components/Input.tsx +220 -27
  51. package/src/tools/prefabeditor/components/MaterialComponent.tsx +178 -16
  52. package/src/tools/prefabeditor/components/ModelComponent.tsx +51 -4
  53. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +44 -85
  54. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +11 -17
  55. package/src/tools/prefabeditor/components/TextComponent.tsx +58 -57
  56. package/src/tools/prefabeditor/components/TransformComponent.tsx +61 -9
  57. package/src/tools/prefabeditor/components/index.ts +5 -1
  58. package/src/tools/prefabeditor/styles.ts +7 -3
  59. package/src/tools/prefabeditor/utils.ts +55 -4
@@ -27,6 +27,28 @@ const styles = {
27
27
  fontWeight: 500,
28
28
  },
29
29
  };
30
+ function getNumericStep(step, fallback) {
31
+ if (typeof step === 'number' && Number.isFinite(step) && step > 0)
32
+ return step;
33
+ if (typeof step === 'string') {
34
+ const parsed = parseFloat(step);
35
+ if (Number.isFinite(parsed) && parsed > 0)
36
+ return parsed;
37
+ }
38
+ return fallback;
39
+ }
40
+ function getStepPrecision(step) {
41
+ var _a;
42
+ if (!Number.isFinite(step) || step <= 0)
43
+ return 3;
44
+ const stepString = step.toString();
45
+ if (stepString.includes('e-')) {
46
+ const exponent = stepString.split('e-')[1];
47
+ return exponent ? parseInt(exponent, 10) : 3;
48
+ }
49
+ const decimal = stepString.split('.')[1];
50
+ return (_a = decimal === null || decimal === void 0 ? void 0 : decimal.length) !== null && _a !== void 0 ? _a : 0;
51
+ }
30
52
  export function Input({ value, onChange, step, min, max, style, label }) {
31
53
  const [draft, setDraft] = useState(() => value.toString());
32
54
  useEffect(() => {
@@ -48,14 +70,11 @@ export function Input({ value, onChange, step, min, max, style, label }) {
48
70
  };
49
71
  const dragState = useRef(null);
50
72
  const startScrub = (e) => {
51
- if (!label)
52
- return;
53
- e.preventDefault();
54
73
  dragState.current = {
55
74
  startX: e.clientX,
56
75
  startValue: value
57
76
  };
58
- e.target.setPointerCapture(e.pointerId);
77
+ e.currentTarget.setPointerCapture(e.pointerId);
59
78
  document.body.style.cursor = "ew-resize";
60
79
  };
61
80
  const onScrubMove = (e) => {
@@ -63,18 +82,21 @@ export function Input({ value, onChange, step, min, max, style, label }) {
63
82
  return;
64
83
  const { startX, startValue } = dragState.current;
65
84
  const dx = e.clientX - startX;
66
- let speed = 0.02;
85
+ const baseStep = getNumericStep(step, 0.1);
86
+ let scrubStep = baseStep;
67
87
  if (e.shiftKey)
68
- speed *= 0.1; // fine
88
+ scrubStep /= 10;
69
89
  if (e.altKey)
70
- speed *= 5; // coarse
71
- let nextValue = startValue + dx * speed;
90
+ scrubStep *= 10;
91
+ const precision = getStepPrecision(scrubStep);
92
+ const deltaSteps = Math.round(dx / 8);
93
+ let nextValue = startValue + deltaSteps * scrubStep;
72
94
  // Apply min/max constraints
73
95
  if (min !== undefined && nextValue < min)
74
96
  nextValue = min;
75
97
  if (max !== undefined && nextValue > max)
76
98
  nextValue = max;
77
- setDraft(nextValue.toFixed(3));
99
+ setDraft(nextValue.toFixed(precision));
78
100
  onChange(nextValue);
79
101
  };
80
102
  const endScrub = (e) => {
@@ -82,29 +104,29 @@ export function Input({ value, onChange, step, min, max, style, label }) {
82
104
  return;
83
105
  dragState.current = null;
84
106
  document.body.style.cursor = "";
85
- e.target.releasePointerCapture(e.pointerId);
107
+ e.currentTarget.releasePointerCapture(e.pointerId);
86
108
  };
87
109
  if (label) {
88
110
  return (_jsxs("div", { style: {
89
111
  display: 'flex',
90
112
  alignItems: 'center',
91
113
  justifyContent: 'space-between',
92
- }, children: [_jsx("span", { style: Object.assign(Object.assign({}, styles.label), { marginBottom: 0, cursor: 'ew-resize', userSelect: 'none', flex: '0 0 auto', minWidth: 20 }), onPointerDown: startScrub, onPointerMove: onScrubMove, onPointerUp: endScrub, children: label }), _jsx("input", { type: "text", value: draft, onChange: handleChange, onBlur: handleBlur, onKeyDown: e => {
114
+ }, children: [_jsx("span", { style: Object.assign(Object.assign({}, styles.label), { marginBottom: 0, userSelect: 'none', flex: '0 0 auto', minWidth: 20 }), children: label }), _jsx("input", { type: "text", value: draft, onChange: handleChange, onBlur: handleBlur, onKeyDown: e => {
93
115
  if (e.key === 'Enter') {
94
116
  e.target.blur();
95
117
  }
96
- }, step: step, min: min, max: max, style: Object.assign(Object.assign({}, styles.input), style) })] }));
118
+ }, step: step, min: min, max: max, style: Object.assign(Object.assign(Object.assign({}, styles.input), { cursor: 'ew-resize' }), style), onPointerDown: startScrub, onPointerMove: onScrubMove, onPointerUp: endScrub })] }));
97
119
  }
98
120
  return (_jsx("input", { type: "text", value: draft, onChange: handleChange, onBlur: handleBlur, onKeyDown: e => {
99
121
  if (e.key === 'Enter') {
100
122
  e.target.blur();
101
123
  }
102
- }, step: step, min: min, max: max, style: Object.assign(Object.assign({}, styles.input), style) }));
124
+ }, step: step, min: min, max: max, style: Object.assign(Object.assign(Object.assign({}, styles.input), { cursor: 'ew-resize' }), style), onPointerDown: startScrub, onPointerMove: onScrubMove, onPointerUp: endScrub }));
103
125
  }
104
126
  export function Label({ children }) {
105
127
  return _jsx("label", { style: styles.label, children: children });
106
128
  }
107
- export function Vector3Input({ label, value, onChange, snap }) {
129
+ export function Vector3Input({ label, value, onChange, snap, labelExtra }) {
108
130
  const snapValue = (num) => {
109
131
  if (!snap)
110
132
  return num;
@@ -125,13 +147,12 @@ export function Vector3Input({ label, value, onChange, snap }) {
125
147
  }
126
148
  };
127
149
  const startScrub = (e, index) => {
128
- e.preventDefault();
129
150
  dragState.current = {
130
151
  index,
131
152
  startX: e.clientX,
132
153
  startValue: value[index]
133
154
  };
134
- e.target.setPointerCapture(e.pointerId);
155
+ e.currentTarget.setPointerCapture(e.pointerId);
135
156
  document.body.style.cursor = "ew-resize";
136
157
  };
137
158
  const onScrubMove = (e) => {
@@ -160,14 +181,14 @@ export function Vector3Input({ label, value, onChange, snap }) {
160
181
  return;
161
182
  dragState.current = null;
162
183
  document.body.style.cursor = "";
163
- e.target.releasePointerCapture(e.pointerId);
184
+ e.currentTarget.releasePointerCapture(e.pointerId);
164
185
  };
165
186
  const axes = [
166
187
  { key: "x", color: '#e06c75', index: 0 },
167
188
  { key: "y", color: '#98c379', index: 1 },
168
189
  { key: "z", color: '#61afef', index: 2 }
169
190
  ];
170
- return (_jsxs("div", { style: { marginBottom: 8 }, children: [_jsx("label", { style: Object.assign(Object.assign({}, styles.label), { marginBottom: 4 }), children: label }), _jsx("div", { style: { display: 'flex', gap: 4 }, children: axes.map(({ key, color, index }) => (_jsxs("div", { style: {
191
+ return (_jsxs("div", { style: { marginBottom: 8 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }, children: [_jsx("label", { style: Object.assign(Object.assign({}, styles.label), { marginBottom: 0 }), children: label }), labelExtra] }), _jsx("div", { style: { display: 'flex', gap: 4 }, children: axes.map(({ key, color, index }) => (_jsxs("div", { style: {
171
192
  flex: 1,
172
193
  display: 'flex',
173
194
  alignItems: 'center',
@@ -177,14 +198,14 @@ export function Vector3Input({ label, value, onChange, snap }) {
177
198
  borderRadius: 3,
178
199
  padding: '4px 6px',
179
200
  minHeight: 28,
180
- }, children: [_jsx("span", { style: {
201
+ cursor: 'ew-resize',
202
+ }, onPointerDown: e => startScrub(e, index), onPointerMove: onScrubMove, onPointerUp: endScrub, children: [_jsx("span", { style: {
181
203
  fontSize: 11,
182
204
  fontWeight: 600,
183
205
  color,
184
206
  width: 12,
185
- cursor: 'ew-resize',
186
207
  userSelect: 'none',
187
- }, onPointerDown: e => startScrub(e, index), onPointerMove: onScrubMove, onPointerUp: endScrub, children: key.toUpperCase() }), _jsx("input", { style: {
208
+ }, children: key.toUpperCase() }), _jsx("input", { style: {
188
209
  flex: 1,
189
210
  backgroundColor: 'transparent',
190
211
  border: 'none',
@@ -194,6 +215,7 @@ export function Vector3Input({ label, value, onChange, snap }) {
194
215
  outline: 'none',
195
216
  width: '100%',
196
217
  minWidth: 0,
218
+ cursor: 'inherit',
197
219
  }, type: "text", value: draft[index], onChange: e => {
198
220
  const next = [...draft];
199
221
  next[index] = e.target.value;
@@ -233,6 +255,36 @@ export function BooleanInput({ label, value, onChange }) {
233
255
  export function SelectInput({ label, value, onChange, options }) {
234
256
  return (_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between' }, children: [label && _jsx(Label, { children: label }), _jsx("select", { style: styles.input, value: value, onChange: e => onChange(e.target.value), children: options.map(opt => (_jsx("option", { value: opt.value, children: opt.label }, opt.value))) })] }));
235
257
  }
258
+ function bindFieldChange(name, onChange) {
259
+ return (value) => onChange({ [name]: value });
260
+ }
261
+ export function FieldGroup({ children }) {
262
+ return _jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 8 }, children: children });
263
+ }
264
+ export function NumberField({ name, label, values, onChange, fallback = 0, step, min, max, style, }) {
265
+ var _a;
266
+ return (_jsx(Input, { label: label, value: (_a = values[name]) !== null && _a !== void 0 ? _a : fallback, onChange: bindFieldChange(name, onChange), step: step, min: min, max: max, style: style }));
267
+ }
268
+ export function StringField({ name, label, values, onChange, fallback = '', placeholder, }) {
269
+ var _a;
270
+ return (_jsx(StringInput, { label: label, value: (_a = values[name]) !== null && _a !== void 0 ? _a : fallback, onChange: bindFieldChange(name, onChange), placeholder: placeholder }));
271
+ }
272
+ export function ColorField({ name, label, values, onChange, fallback = '#ffffff', }) {
273
+ var _a;
274
+ return (_jsx(ColorInput, { label: label, value: (_a = values[name]) !== null && _a !== void 0 ? _a : fallback, onChange: bindFieldChange(name, onChange) }));
275
+ }
276
+ export function BooleanField({ name, label, values, onChange, fallback = false, }) {
277
+ var _a;
278
+ return (_jsx(BooleanInput, { label: label, value: (_a = values[name]) !== null && _a !== void 0 ? _a : fallback, onChange: bindFieldChange(name, onChange) }));
279
+ }
280
+ export function SelectField({ name, label, values, onChange, fallback, options, }) {
281
+ var _a, _b, _c, _d;
282
+ 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 }));
283
+ }
284
+ export function Vector3Field({ name, label, values, onChange, fallback = [0, 0, 0], snap, labelExtra, }) {
285
+ var _a;
286
+ return (_jsx(Vector3Input, { label: label, value: (_a = values[name]) !== null && _a !== void 0 ? _a : fallback, onChange: bindFieldChange(name, onChange), snap: snap, labelExtra: labelExtra }));
287
+ }
236
288
  export function FieldRenderer({ fields, values, onChange }) {
237
289
  const updateField = (name, value) => {
238
290
  onChange({ [name]: value });
@@ -1,12 +1,18 @@
1
1
  import { Component } from './ComponentRegistry';
2
- import { MeshStandardMaterialProperties } from 'three';
3
- export interface MaterialProps extends Omit<MeshStandardMaterialProperties, 'args'> {
2
+ import { MeshBasicMaterialProperties, MeshStandardMaterialProperties } from 'three';
3
+ export interface MaterialProps extends Omit<MeshStandardMaterialProperties & MeshBasicMaterialProperties, 'args' | 'normalScale'> {
4
+ materialType?: 'standard' | 'basic';
5
+ transmission?: number;
6
+ thickness?: number;
7
+ ior?: number;
4
8
  texture?: string;
5
9
  repeat?: boolean;
6
10
  repeatCount?: [number, number];
7
11
  generateMipmaps?: boolean;
8
12
  minFilter?: string;
9
13
  magFilter?: string;
14
+ normalMapTexture?: string;
15
+ normalScale?: [number, number];
10
16
  }
11
17
  declare const MaterialComponent: Component;
12
18
  export default MaterialComponent;
@@ -11,46 +11,112 @@ var __rest = (this && this.__rest) || function (s, e) {
11
11
  };
12
12
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
13
  import { SingleTextureViewer, TextureListViewer } from '../../assetviewer/page';
14
- import { useEffect, useState } from 'react';
14
+ import { useEffect, useLayoutEffect, useRef, useState } from 'react';
15
+ import { createPortal } from 'react-dom';
15
16
  import { FieldRenderer, Input } from './Input';
16
17
  import { colors } from '../styles';
17
18
  import { useMemo } from 'react';
18
- import { RepeatWrapping, ClampToEdgeWrapping, SRGBColorSpace, NearestFilter, LinearFilter, NearestMipmapNearestFilter, NearestMipmapLinearFilter, LinearMipmapNearestFilter, LinearMipmapLinearFilter } from 'three';
19
+ import { RepeatWrapping, ClampToEdgeWrapping, SRGBColorSpace, LinearSRGBColorSpace, Vector2, NearestFilter, LinearFilter, NearestMipmapNearestFilter, NearestMipmapLinearFilter, LinearMipmapNearestFilter, LinearMipmapLinearFilter, FrontSide, BackSide, DoubleSide, } from 'three';
20
+ const PICKER_POPUP_WIDTH = 260;
21
+ const PICKER_POPUP_HEIGHT = 360;
19
22
  function TexturePicker({ value, onChange, basePath }) {
20
23
  const [textureFiles, setTextureFiles] = useState([]);
21
24
  const [showPicker, setShowPicker] = useState(false);
25
+ const [popupStyle, setPopupStyle] = useState(null);
26
+ const triggerRef = useRef(null);
22
27
  useEffect(() => {
23
28
  fetch(`${basePath}/textures/manifest.json`)
24
29
  .then(r => r.json())
25
30
  .then(data => setTextureFiles(Array.isArray(data) ? data : data.files || []))
26
31
  .catch(console.error);
27
32
  }, [basePath]);
33
+ useLayoutEffect(() => {
34
+ if (!showPicker || !triggerRef.current || typeof window === 'undefined')
35
+ return;
36
+ const updatePosition = () => {
37
+ var _a;
38
+ const rect = (_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
39
+ if (!rect)
40
+ return;
41
+ const preferredLeft = rect.left - PICKER_POPUP_WIDTH - 8;
42
+ const fallbackLeft = rect.right + 8;
43
+ const fitsLeft = preferredLeft >= 8;
44
+ const left = fitsLeft ? preferredLeft : Math.min(fallbackLeft, window.innerWidth - PICKER_POPUP_WIDTH - 8);
45
+ const top = Math.min(Math.max(8, rect.top), window.innerHeight - PICKER_POPUP_HEIGHT - 8);
46
+ setPopupStyle({
47
+ position: 'fixed',
48
+ left,
49
+ top,
50
+ background: colors.bg,
51
+ padding: 12,
52
+ border: `1px solid ${colors.border}`,
53
+ borderRadius: 6,
54
+ width: PICKER_POPUP_WIDTH,
55
+ height: PICKER_POPUP_HEIGHT,
56
+ overflow: 'hidden',
57
+ zIndex: 1000,
58
+ boxShadow: '0 4px 16px rgba(0,0,0,0.6)',
59
+ });
60
+ };
61
+ updatePosition();
62
+ window.addEventListener('resize', updatePosition);
63
+ window.addEventListener('scroll', updatePosition, true);
64
+ return () => {
65
+ window.removeEventListener('resize', updatePosition);
66
+ window.removeEventListener('scroll', updatePosition, true);
67
+ };
68
+ }, [showPicker]);
28
69
  // Only show 3D preview for server-hosted textures (starting with / or http)
29
70
  const canPreview = value && (value.startsWith('/') || value.startsWith('http'));
30
71
  return (_jsxs("div", { style: { maxHeight: 128, overflow: 'visible', position: 'relative', display: 'flex', alignItems: 'center' }, children: [canPreview
31
72
  ? _jsx(SingleTextureViewer, { file: value, basePath: basePath })
32
73
  : value
33
74
  ? _jsx("span", { style: { fontSize: 10, opacity: 0.6, maxWidth: 100, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: value })
34
- : null, _jsx("button", { onClick: () => setShowPicker(!showPicker), style: { padding: '4px 8px', backgroundColor: colors.bgLight, color: 'inherit', fontSize: 10, cursor: 'pointer', border: `1px solid ${colors.border}`, borderRadius: 3, marginTop: 4 }, children: showPicker ? 'Cancel' : 'Change' }), _jsx("button", { onClick: () => {
75
+ : null, _jsx("button", { ref: triggerRef, onClick: () => setShowPicker(!showPicker), style: { padding: '4px 8px', backgroundColor: colors.bgLight, color: 'inherit', fontSize: 10, cursor: 'pointer', border: `1px solid ${colors.border}`, borderRadius: 3, marginTop: 4 }, children: showPicker ? 'Cancel' : 'Change' }), _jsx("button", { onClick: () => {
35
76
  onChange(undefined);
36
- }, style: { padding: '4px 8px', backgroundColor: colors.bgLight, color: 'inherit', fontSize: 10, cursor: 'pointer', border: `1px solid ${colors.border}`, borderRadius: 3, marginTop: 4, marginLeft: 4 }, children: "Clear" }), showPicker && (_jsx("div", { style: { position: 'fixed', right: 60, top: 60, transform: 'translate(-100%,0%)', background: colors.bg, padding: 16, border: `1px solid ${colors.border}`, borderRadius: 4, maxHeight: '80vh', overflowY: 'auto', overflowX: 'hidden', width: 220, zIndex: 1000, boxShadow: '0 4px 16px rgba(0,0,0,0.6)' }, children: _jsx(TextureListViewer, { files: textureFiles, selected: value || undefined, onSelect: (file) => {
77
+ }, style: { padding: '4px 8px', backgroundColor: colors.bgLight, color: 'inherit', fontSize: 10, cursor: 'pointer', border: `1px solid ${colors.border}`, borderRadius: 3, marginTop: 4, marginLeft: 4 }, children: "Clear" }), showPicker && popupStyle && typeof document !== 'undefined' && createPortal(_jsx("div", { style: popupStyle, onMouseLeave: () => setShowPicker(false), children: _jsx(TextureListViewer, { files: textureFiles, selected: value || undefined, onSelect: (file) => {
37
78
  onChange(file);
38
79
  setShowPicker(false);
39
- }, basePath: basePath }) }))] }));
80
+ }, basePath: basePath }) }), document.body)] }));
40
81
  }
41
82
  function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
83
+ var _a;
84
+ const materialType = (_a = component.properties.materialType) !== null && _a !== void 0 ? _a : 'standard';
42
85
  const hasTexture = !!component.properties.texture;
43
86
  const hasRepeat = component.properties.repeat;
87
+ const isStandardMaterial = materialType === 'standard';
44
88
  const fields = [
89
+ {
90
+ name: 'materialType',
91
+ type: 'select',
92
+ label: 'Material Type',
93
+ options: [
94
+ { value: 'standard', label: 'Standard' },
95
+ { value: 'basic', label: 'Basic' },
96
+ ],
97
+ },
45
98
  { name: 'color', type: 'color', label: 'Color' },
99
+ { name: 'toneMapped', type: 'boolean', label: 'Tone Mapped' },
46
100
  { name: 'wireframe', type: 'boolean', label: 'Wireframe' },
47
101
  { name: 'transparent', type: 'boolean', label: 'Transparent' },
48
102
  { name: 'opacity', type: 'number', label: 'Opacity', min: 0, max: 1, step: 0.01 },
49
- { name: 'metalness', type: 'number', label: 'Metalness', min: 0, max: 1, step: 0.01 },
50
- { name: 'roughness', type: 'number', label: 'Roughness', min: 0, max: 1, step: 0.01 },
51
- { name: 'transmission', type: 'number', label: 'Transmission', min: 0, max: 1, step: 0.01 },
52
- { name: 'thickness', type: 'number', label: 'Thickness', min: 0, step: 0.1 },
53
- { name: 'ior', type: 'number', label: 'IOR (Index of Refraction)', min: 1, max: 2.333, step: 0.01 },
103
+ ...(isStandardMaterial ? [
104
+ { name: 'metalness', type: 'number', label: 'Metalness', min: 0, max: 1, step: 0.01 },
105
+ { name: 'roughness', type: 'number', label: 'Roughness', min: 0, max: 1, step: 0.01 },
106
+ { name: 'transmission', type: 'number', label: 'Transmission', min: 0, max: 1, step: 0.01 },
107
+ { name: 'thickness', type: 'number', label: 'Thickness', min: 0, step: 0.1 },
108
+ { name: 'ior', type: 'number', label: 'IOR (Index of Refraction)', min: 1, max: 2.333, step: 0.01 },
109
+ ] : []),
110
+ {
111
+ name: 'side',
112
+ type: 'select',
113
+ label: 'Side',
114
+ options: [
115
+ { value: 'FrontSide', label: 'Front' },
116
+ { value: 'BackSide', label: 'Back' },
117
+ { value: 'DoubleSide', label: 'Double' },
118
+ ],
119
+ },
54
120
  {
55
121
  name: 'texture',
56
122
  type: 'custom',
@@ -69,6 +135,21 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
69
135
  return (_jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsx(Input, { label: "X", value: (_a = value === null || value === void 0 ? void 0 : value[0]) !== null && _a !== void 0 ? _a : 1, onChange: v => { var _a; return onChange([v, (_a = value === null || value === void 0 ? void 0 : value[1]) !== null && _a !== void 0 ? _a : 1]); }, min: 0.01, max: 100, step: 0.1 }), _jsx(Input, { label: "Y", value: (_b = value === null || value === void 0 ? void 0 : value[1]) !== null && _b !== void 0 ? _b : 1, onChange: v => { var _a; return onChange([(_a = value === null || value === void 0 ? void 0 : value[0]) !== null && _a !== void 0 ? _a : 1, v]); }, min: 0.01, max: 100, step: 0.1 })] }));
70
136
  },
71
137
  }] : []),
138
+ {
139
+ name: 'normalMapTexture',
140
+ type: 'custom',
141
+ label: 'Normal Map',
142
+ render: ({ value, onChange }) => (_jsx(TexturePicker, { value: value, onChange: onChange, basePath: basePath })),
143
+ },
144
+ ...(component.properties.normalMapTexture ? [{
145
+ name: 'normalScale',
146
+ type: 'custom',
147
+ label: 'Normal Scale (X, Y)',
148
+ render: ({ value, onChange }) => {
149
+ var _a, _b;
150
+ return (_jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsx(Input, { label: "X", value: (_a = value === null || value === void 0 ? void 0 : value[0]) !== null && _a !== void 0 ? _a : 1, onChange: v => { var _a; return onChange([v, (_a = value === null || value === void 0 ? void 0 : value[1]) !== null && _a !== void 0 ? _a : 1]); }, min: 0, max: 5, step: 0.01 }), _jsx(Input, { label: "Y", value: (_b = value === null || value === void 0 ? void 0 : value[1]) !== null && _b !== void 0 ? _b : 1, onChange: v => { var _a; return onChange([(_a = value === null || value === void 0 ? void 0 : value[0]) !== null && _a !== void 0 ? _a : 1, v]); }, min: 0, max: 5, step: 0.01 })] }));
151
+ },
152
+ }] : []),
72
153
  { name: 'generateMipmaps', type: 'boolean', label: 'Generate Mipmaps' },
73
154
  {
74
155
  name: 'minFilter',
@@ -98,7 +179,8 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
98
179
  }
99
180
  // View for Material component
100
181
  function MaterialComponentView({ properties, loadedTextures }) {
101
- var _a;
182
+ var _a, _b, _c;
183
+ const materialType = (_a = properties === null || properties === void 0 ? void 0 : properties.materialType) !== null && _a !== void 0 ? _a : 'standard';
102
184
  const textureName = properties === null || properties === void 0 ? void 0 : properties.texture;
103
185
  const repeat = properties === null || properties === void 0 ? void 0 : properties.repeat;
104
186
  const repeatCount = properties === null || properties === void 0 ? void 0 : properties.repeatCount;
@@ -106,9 +188,14 @@ function MaterialComponentView({ properties, loadedTextures }) {
106
188
  const minFilter = (properties === null || properties === void 0 ? void 0 : properties.minFilter) || 'LinearMipmapLinearFilter';
107
189
  const magFilter = (properties === null || properties === void 0 ? void 0 : properties.magFilter) || 'LinearFilter';
108
190
  const texture = textureName && loadedTextures ? loadedTextures[textureName] : undefined;
191
+ const normalMapTextureName = properties === null || properties === void 0 ? void 0 : properties.normalMapTexture;
192
+ const normalScaleProp = properties === null || properties === void 0 ? void 0 : properties.normalScale;
193
+ const normalMapTexture = normalMapTextureName && loadedTextures ? loadedTextures[normalMapTextureName] : undefined;
194
+ const materialSource = properties !== null && properties !== void 0 ? properties : {};
109
195
  // Destructure all material props and separate custom texture handling props
110
- const _b = properties || {}, { texture: _texture, repeat: _repeat, repeatCount: _repeatCount, generateMipmaps: _generateMipmaps, minFilter: _minFilter, magFilter: _magFilter, map: _map } = _b, // Filter out map since we set it explicitly
111
- materialProps = __rest(_b, ["texture", "repeat", "repeatCount", "generateMipmaps", "minFilter", "magFilter", "map"]);
196
+ const { texture: _texture, repeat: _repeat, repeatCount: _repeatCount, generateMipmaps: _generateMipmaps, minFilter: _minFilter, magFilter: _magFilter, map: _map, materialType: _materialType, normalMapTexture: _normalMapTexture, normalScale: _normalScale, normalMap: _normalMap, side: sideProp, metalness: _metalness, roughness: _roughness, transmission: _transmission, thickness: _thickness, ior: _ior } = materialSource, materialProps = __rest(materialSource, ["texture", "repeat", "repeatCount", "generateMipmaps", "minFilter", "magFilter", "map", "materialType", "normalMapTexture", "normalScale", "normalMap", "side", "metalness", "roughness", "transmission", "thickness", "ior"]);
197
+ const sideMap = { FrontSide, BackSide, DoubleSide };
198
+ const resolvedSide = sideProp ? ((_b = sideMap[sideProp]) !== null && _b !== void 0 ? _b : FrontSide) : FrontSide;
112
199
  const minFilterMap = {
113
200
  NearestFilter,
114
201
  LinearFilter,
@@ -142,10 +229,29 @@ function MaterialComponentView({ properties, loadedTextures }) {
142
229
  t.needsUpdate = true;
143
230
  return t;
144
231
  }, [texture, repeat, repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[0], repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[1], generateMipmaps, minFilter, magFilter]);
232
+ const finalNormalMap = useMemo(() => {
233
+ if (!normalMapTexture)
234
+ return undefined;
235
+ const t = normalMapTexture.clone();
236
+ t.colorSpace = LinearSRGBColorSpace;
237
+ t.needsUpdate = true;
238
+ return t;
239
+ }, [normalMapTexture]);
240
+ const normalScaleVec = useMemo(() => {
241
+ var _a, _b;
242
+ if (!finalNormalMap)
243
+ return undefined;
244
+ return new Vector2((_a = normalScaleProp === null || normalScaleProp === void 0 ? void 0 : normalScaleProp[0]) !== null && _a !== void 0 ? _a : 1, (_b = normalScaleProp === null || normalScaleProp === void 0 ? void 0 : normalScaleProp[1]) !== null && _b !== void 0 ? _b : 1);
245
+ }, [finalNormalMap, normalScaleProp === null || normalScaleProp === void 0 ? void 0 : normalScaleProp[0], normalScaleProp === null || normalScaleProp === void 0 ? void 0 : normalScaleProp[1]]);
145
246
  if (!properties) {
146
247
  return _jsx("meshStandardMaterial", { color: "red", wireframe: true });
147
248
  }
148
- return (_jsx("meshStandardMaterial", Object.assign({ map: finalTexture }, materialProps), (_a = finalTexture === null || finalTexture === void 0 ? void 0 : finalTexture.uuid) !== null && _a !== void 0 ? _a : 'no-texture'));
249
+ const materialKey = (_c = finalTexture === null || finalTexture === void 0 ? void 0 : finalTexture.uuid) !== null && _c !== void 0 ? _c : 'no-texture';
250
+ const sharedProps = Object.assign({ map: finalTexture, side: resolvedSide }, materialProps);
251
+ if (materialType === 'basic') {
252
+ return _jsx("meshBasicMaterial", Object.assign({}, sharedProps), materialKey);
253
+ }
254
+ return (_jsx("meshStandardMaterial", Object.assign({}, sharedProps, { normalMap: finalNormalMap, normalScale: normalScaleVec }), materialKey));
149
255
  }
150
256
  const MaterialComponent = {
151
257
  name: 'Material',
@@ -153,7 +259,9 @@ const MaterialComponent = {
153
259
  View: MaterialComponentView,
154
260
  nonComposable: true,
155
261
  defaultProperties: {
262
+ materialType: 'standard',
156
263
  color: '#ffffff',
264
+ toneMapped: true,
157
265
  wireframe: false,
158
266
  transparent: false,
159
267
  opacity: 1,
@@ -1,24 +1,65 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { ModelListViewer, SingleModelViewer } from '../../assetviewer/page';
3
- import { useEffect, useState, useMemo } from 'react';
3
+ import { useEffect, useLayoutEffect, useState, useMemo, useRef } from 'react';
4
+ import { createPortal } from 'react-dom';
4
5
  import { FieldRenderer } from './Input';
6
+ const PICKER_POPUP_WIDTH = 260;
7
+ const PICKER_POPUP_HEIGHT = 360;
5
8
  function ModelPicker({ value, onChange, basePath, nodeId }) {
6
9
  const [modelFiles, setModelFiles] = useState([]);
7
10
  const [showPicker, setShowPicker] = useState(false);
11
+ const [popupStyle, setPopupStyle] = useState(null);
12
+ const triggerRef = useRef(null);
8
13
  useEffect(() => {
9
14
  fetch(`${basePath}/models/manifest.json`)
10
15
  .then(r => r.json())
11
16
  .then(data => setModelFiles(Array.isArray(data) ? data : data.files || []))
12
17
  .catch(console.error);
13
18
  }, [basePath]);
19
+ useLayoutEffect(() => {
20
+ if (!showPicker || !triggerRef.current || typeof window === 'undefined')
21
+ return;
22
+ const updatePosition = () => {
23
+ var _a;
24
+ const rect = (_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
25
+ if (!rect)
26
+ return;
27
+ const preferredLeft = rect.left - PICKER_POPUP_WIDTH - 8;
28
+ const fallbackLeft = rect.right + 8;
29
+ const fitsLeft = preferredLeft >= 8;
30
+ const left = fitsLeft ? preferredLeft : Math.min(fallbackLeft, window.innerWidth - PICKER_POPUP_WIDTH - 8);
31
+ const top = Math.min(Math.max(8, rect.top), window.innerHeight - PICKER_POPUP_HEIGHT - 8);
32
+ setPopupStyle({
33
+ position: 'fixed',
34
+ left,
35
+ top,
36
+ background: 'rgba(0,0,0,0.9)',
37
+ padding: 12,
38
+ border: '1px solid rgba(34, 211, 238, 0.3)',
39
+ borderRadius: 6,
40
+ width: PICKER_POPUP_WIDTH,
41
+ height: PICKER_POPUP_HEIGHT,
42
+ overflow: 'hidden',
43
+ zIndex: 1000,
44
+ boxShadow: '0 4px 16px rgba(0,0,0,0.6)',
45
+ });
46
+ };
47
+ updatePosition();
48
+ window.addEventListener('resize', updatePosition);
49
+ window.addEventListener('scroll', updatePosition, true);
50
+ return () => {
51
+ window.removeEventListener('resize', updatePosition);
52
+ window.removeEventListener('scroll', updatePosition, true);
53
+ };
54
+ }, [showPicker]);
14
55
  const handleModelSelect = (file) => {
15
56
  const filename = file.startsWith('/') ? file.slice(1) : file;
16
57
  onChange(filename);
17
58
  setShowPicker(false);
18
59
  };
19
- return (_jsxs("div", { style: { maxHeight: 128, overflow: 'visible', position: 'relative', display: 'flex', alignItems: 'center' }, children: [_jsx(SingleModelViewer, { file: value ? `/${value}` : undefined, basePath: basePath }), _jsx("button", { onClick: () => setShowPicker(!showPicker), style: { padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4 }, children: showPicker ? 'Cancel' : 'Change' }), _jsx("button", { onClick: () => {
60
+ return (_jsxs("div", { style: { maxHeight: 128, overflow: 'visible', position: 'relative', display: 'flex', alignItems: 'center' }, children: [_jsx(SingleModelViewer, { file: value ? `/${value}` : undefined, basePath: basePath }), _jsx("button", { ref: triggerRef, onClick: () => setShowPicker(!showPicker), style: { padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4 }, children: showPicker ? 'Cancel' : 'Change' }), _jsx("button", { onClick: () => {
20
61
  onChange(undefined);
21
- }, style: { padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4, marginLeft: 4 }, children: "Clear" }), showPicker && (_jsx("div", { style: { position: 'fixed', right: 60, top: 60, transform: 'translate(-100%,0%)', background: 'rgba(0,0,0,0.9)', padding: 16, border: '1px solid rgba(34, 211, 238, 0.3)', maxHeight: '80vh', overflowY: 'auto', overflowX: 'hidden', width: 220, zIndex: 1000 }, children: _jsx(ModelListViewer, { files: modelFiles, selected: value ? `/${value}` : undefined, onSelect: handleModelSelect, basePath: basePath }, nodeId) }))] }));
62
+ }, style: { padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4, marginLeft: 4 }, children: "Clear" }), showPicker && popupStyle && typeof document !== 'undefined' && createPortal(_jsx("div", { style: popupStyle, onMouseLeave: () => setShowPicker(false), children: _jsx(ModelListViewer, { files: modelFiles, selected: value ? `/${value}` : undefined, onSelect: handleModelSelect, basePath: basePath }, nodeId) }), document.body)] }));
22
63
  }
23
64
  function ModelComponentEditor({ component, node, onUpdate, basePath = "" }) {
24
65
  const fields = [
@@ -9,91 +9,26 @@ var __rest = (this && this.__rest) || function (s, e) {
9
9
  }
10
10
  return t;
11
11
  };
12
- import { jsx as _jsx } from "react/jsx-runtime";
12
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
13
  import { RigidBody, useRapier } from "@react-three/rapier";
14
14
  import { useRef, useEffect, useCallback } from 'react';
15
- import { FieldRenderer } from "./Input";
15
+ import { BooleanField, FieldGroup, NumberField, SelectField } from "./Input";
16
16
  import { gameEvents, getEntityIdFromRigidBody } from "../GameEvents";
17
- const physicsFields = [
18
- {
19
- name: 'type',
20
- type: 'select',
21
- label: 'Type',
22
- options: [
23
- { value: 'dynamic', label: 'Dynamic' },
24
- { value: 'fixed', label: 'Fixed' },
25
- { value: 'kinematicPosition', label: 'Kinematic Position' },
26
- { value: 'kinematicVelocity', label: 'Kinematic Velocity' },
27
- ],
28
- },
29
- {
30
- name: 'colliders',
31
- type: 'select',
32
- label: 'Collider',
33
- options: [
34
- { value: 'hull', label: 'Hull (convex)' },
35
- { value: 'trimesh', label: 'Trimesh (exact)' },
36
- { value: 'cuboid', label: 'Cuboid (box)' },
37
- { value: 'ball', label: 'Ball (sphere)' },
38
- ],
39
- },
40
- {
41
- name: 'mass',
42
- type: 'number',
43
- label: 'Mass',
44
- },
45
- {
46
- name: 'restitution',
47
- type: 'number',
48
- label: 'Restitution (Bounciness)',
49
- min: 0,
50
- max: 1,
51
- step: 0.1,
52
- },
53
- {
54
- name: 'friction',
55
- type: 'number',
56
- label: 'Friction',
57
- min: 0,
58
- step: 0.1,
59
- },
60
- {
61
- name: 'linearDamping',
62
- type: 'number',
63
- label: 'Linear Damping',
64
- min: 0,
65
- step: 0.1,
66
- },
67
- {
68
- name: 'angularDamping',
69
- type: 'number',
70
- label: 'Angular Damping',
71
- min: 0,
72
- step: 0.1,
73
- },
74
- {
75
- name: 'gravityScale',
76
- type: 'number',
77
- label: 'Gravity Scale',
78
- step: 0.1,
79
- },
80
- {
81
- name: 'sensor',
82
- type: 'boolean',
83
- label: 'Sensor (Trigger Only)',
84
- },
85
- {
86
- name: 'activeCollisionTypes',
87
- type: 'select',
88
- label: 'Collision Detection',
89
- options: [
90
- { value: '', label: 'Default (Dynamic only)' },
91
- { value: 'all', label: 'All (includes kinematic & fixed)' },
92
- ],
93
- },
94
- ];
95
17
  function PhysicsComponentEditor({ component, onUpdate }) {
96
- return (_jsx(FieldRenderer, { fields: physicsFields, values: component.properties, onChange: onUpdate }));
18
+ return (_jsxs(FieldGroup, { children: [_jsx(SelectField, { name: "type", label: "Type", values: component.properties, onChange: onUpdate, options: [
19
+ { value: 'dynamic', label: 'Dynamic' },
20
+ { value: 'fixed', label: 'Fixed' },
21
+ { value: 'kinematicPosition', label: 'Kinematic Position' },
22
+ { value: 'kinematicVelocity', label: 'Kinematic Velocity' },
23
+ ] }), _jsx(SelectField, { name: "colliders", label: "Collider", values: component.properties, onChange: onUpdate, options: [
24
+ { value: 'hull', label: 'Hull (convex)' },
25
+ { value: 'trimesh', label: 'Trimesh (exact)' },
26
+ { value: 'cuboid', label: 'Cuboid (box)' },
27
+ { value: 'ball', label: 'Ball (sphere)' },
28
+ ] }), _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(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: [
29
+ { value: '', label: 'Default (Dynamic only)' },
30
+ { value: 'all', label: 'All (includes kinematic & fixed)' },
31
+ ] })] }));
97
32
  }
98
33
  function PhysicsComponentView({ properties, children, position, rotation, scale, editMode, nodeId, registerRigidBodyRef }) {
99
34
  const { type, colliders, sensor, activeCollisionTypes } = properties, otherProps = __rest(properties, ["type", "colliders", "sensor", "activeCollisionTypes"]);