react-three-game 0.0.55 → 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 (68) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +1 -1
  3. package/dist/shared/ContactShadow.d.ts +8 -0
  4. package/dist/shared/ContactShadow.js +32 -0
  5. package/dist/shared/GameCanvas.js +1 -3
  6. package/dist/tools/assetviewer/page.js +36 -15
  7. package/dist/tools/dragdrop/DragDropLoader.js +17 -40
  8. package/dist/tools/dragdrop/modelLoader.d.ts +5 -0
  9. package/dist/tools/dragdrop/modelLoader.js +39 -0
  10. package/dist/tools/prefabeditor/Dropdown.d.ts +15 -0
  11. package/dist/tools/prefabeditor/Dropdown.js +82 -0
  12. package/dist/tools/prefabeditor/EditorContext.d.ts +5 -0
  13. package/dist/tools/prefabeditor/EditorTree.js +139 -70
  14. package/dist/tools/prefabeditor/EditorUI.js +5 -10
  15. package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
  16. package/dist/tools/prefabeditor/PrefabEditor.js +70 -3
  17. package/dist/tools/prefabeditor/PrefabRoot.d.ts +3 -0
  18. package/dist/tools/prefabeditor/PrefabRoot.js +136 -35
  19. package/dist/tools/prefabeditor/components/AmbientLightComponent.js +3 -7
  20. package/dist/tools/prefabeditor/components/CameraComponent.d.ts +3 -0
  21. package/dist/tools/prefabeditor/components/CameraComponent.js +25 -0
  22. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +2 -2
  23. package/dist/tools/prefabeditor/components/EnvironmentComponent.d.ts +3 -0
  24. package/dist/tools/prefabeditor/components/EnvironmentComponent.js +15 -0
  25. package/dist/tools/prefabeditor/components/GeometryComponent.js +46 -46
  26. package/dist/tools/prefabeditor/components/Input.d.ts +51 -1
  27. package/dist/tools/prefabeditor/components/Input.js +100 -47
  28. package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +8 -2
  29. package/dist/tools/prefabeditor/components/MaterialComponent.js +129 -14
  30. package/dist/tools/prefabeditor/components/ModelComponent.js +44 -3
  31. package/dist/tools/prefabeditor/components/PhysicsComponent.js +16 -81
  32. package/dist/tools/prefabeditor/components/SpotLightComponent.js +6 -11
  33. package/dist/tools/prefabeditor/components/TextComponent.js +7 -53
  34. package/dist/tools/prefabeditor/components/TransformComponent.js +31 -19
  35. package/dist/tools/prefabeditor/components/index.js +5 -1
  36. package/dist/tools/prefabeditor/styles.d.ts +17 -4
  37. package/dist/tools/prefabeditor/styles.js +69 -32
  38. package/dist/tools/prefabeditor/utils.d.ts +8 -3
  39. package/dist/tools/prefabeditor/utils.js +92 -6
  40. package/package.json +1 -1
  41. package/react-three-game-skill/react-three-game/rules/LIGHTING.md +6 -0
  42. package/src/index.ts +7 -0
  43. package/src/shared/ContactShadow.tsx +74 -0
  44. package/src/shared/GameCanvas.tsx +0 -3
  45. package/src/tools/assetviewer/page.tsx +78 -46
  46. package/src/tools/dragdrop/DragDropLoader.tsx +7 -39
  47. package/src/tools/dragdrop/modelLoader.ts +36 -0
  48. package/src/tools/prefabeditor/Dropdown.tsx +112 -0
  49. package/src/tools/prefabeditor/EditorContext.tsx +5 -0
  50. package/src/tools/prefabeditor/EditorTree.tsx +237 -115
  51. package/src/tools/prefabeditor/EditorUI.tsx +6 -11
  52. package/src/tools/prefabeditor/PrefabEditor.tsx +77 -5
  53. package/src/tools/prefabeditor/PrefabRoot.tsx +228 -59
  54. package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +5 -11
  55. package/src/tools/prefabeditor/components/CameraComponent.tsx +80 -0
  56. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +2 -2
  57. package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +47 -0
  58. package/src/tools/prefabeditor/components/GeometryComponent.tsx +69 -63
  59. package/src/tools/prefabeditor/components/Input.tsx +247 -53
  60. package/src/tools/prefabeditor/components/MaterialComponent.tsx +191 -20
  61. package/src/tools/prefabeditor/components/ModelComponent.tsx +52 -5
  62. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +44 -85
  63. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +14 -16
  64. package/src/tools/prefabeditor/components/TextComponent.tsx +58 -57
  65. package/src/tools/prefabeditor/components/TransformComponent.tsx +78 -20
  66. package/src/tools/prefabeditor/components/index.ts +5 -1
  67. package/src/tools/prefabeditor/styles.ts +71 -32
  68. package/src/tools/prefabeditor/utils.ts +96 -5
@@ -1,30 +1,54 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
+ import { colors } from '../styles';
3
4
  // ============================================================================
4
- // Shared Styles
5
+ // Shared Styles (derived from shared color tokens)
5
6
  // ============================================================================
6
- // Shared styles
7
7
  const styles = {
8
8
  input: {
9
9
  width: '80px',
10
- backgroundColor: 'rgba(0, 0, 0, 0.4)',
11
- border: '1px solid rgba(34, 211, 238, 0.3)',
12
- padding: '2px 4px',
13
- fontSize: '10px',
14
- color: 'rgba(165, 243, 252, 1)',
10
+ backgroundColor: colors.bgInput,
11
+ border: `1px solid ${colors.border}`,
12
+ padding: '3px 6px',
13
+ fontSize: '11px',
14
+ color: colors.text,
15
15
  fontFamily: 'monospace',
16
16
  outline: 'none',
17
17
  textAlign: 'right',
18
+ borderRadius: 3,
18
19
  },
19
20
  label: {
20
21
  display: 'block',
21
- fontSize: '9px',
22
- color: 'rgba(34, 211, 238, 0.9)',
22
+ fontSize: '10px',
23
+ color: colors.textMuted,
23
24
  textTransform: 'uppercase',
24
25
  letterSpacing: '0.05em',
25
26
  marginBottom: 2,
27
+ fontWeight: 500,
26
28
  },
27
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
+ }
28
52
  export function Input({ value, onChange, step, min, max, style, label }) {
29
53
  const [draft, setDraft] = useState(() => value.toString());
30
54
  useEffect(() => {
@@ -46,14 +70,11 @@ export function Input({ value, onChange, step, min, max, style, label }) {
46
70
  };
47
71
  const dragState = useRef(null);
48
72
  const startScrub = (e) => {
49
- if (!label)
50
- return;
51
- e.preventDefault();
52
73
  dragState.current = {
53
74
  startX: e.clientX,
54
75
  startValue: value
55
76
  };
56
- e.target.setPointerCapture(e.pointerId);
77
+ e.currentTarget.setPointerCapture(e.pointerId);
57
78
  document.body.style.cursor = "ew-resize";
58
79
  };
59
80
  const onScrubMove = (e) => {
@@ -61,18 +82,21 @@ export function Input({ value, onChange, step, min, max, style, label }) {
61
82
  return;
62
83
  const { startX, startValue } = dragState.current;
63
84
  const dx = e.clientX - startX;
64
- let speed = 0.02;
85
+ const baseStep = getNumericStep(step, 0.1);
86
+ let scrubStep = baseStep;
65
87
  if (e.shiftKey)
66
- speed *= 0.1; // fine
88
+ scrubStep /= 10;
67
89
  if (e.altKey)
68
- speed *= 5; // coarse
69
- 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;
70
94
  // Apply min/max constraints
71
95
  if (min !== undefined && nextValue < min)
72
96
  nextValue = min;
73
97
  if (max !== undefined && nextValue > max)
74
98
  nextValue = max;
75
- setDraft(nextValue.toFixed(3));
99
+ setDraft(nextValue.toFixed(precision));
76
100
  onChange(nextValue);
77
101
  };
78
102
  const endScrub = (e) => {
@@ -80,29 +104,29 @@ export function Input({ value, onChange, step, min, max, style, label }) {
80
104
  return;
81
105
  dragState.current = null;
82
106
  document.body.style.cursor = "";
83
- e.target.releasePointerCapture(e.pointerId);
107
+ e.currentTarget.releasePointerCapture(e.pointerId);
84
108
  };
85
109
  if (label) {
86
110
  return (_jsxs("div", { style: {
87
111
  display: 'flex',
88
112
  alignItems: 'center',
89
113
  justifyContent: 'space-between',
90
- }, 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 => {
91
115
  if (e.key === 'Enter') {
92
116
  e.target.blur();
93
117
  }
94
- }, 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 })] }));
95
119
  }
96
120
  return (_jsx("input", { type: "text", value: draft, onChange: handleChange, onBlur: handleBlur, onKeyDown: e => {
97
121
  if (e.key === 'Enter') {
98
122
  e.target.blur();
99
123
  }
100
- }, 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 }));
101
125
  }
102
126
  export function Label({ children }) {
103
127
  return _jsx("label", { style: styles.label, children: children });
104
128
  }
105
- export function Vector3Input({ label, value, onChange, snap }) {
129
+ export function Vector3Input({ label, value, onChange, snap, labelExtra }) {
106
130
  const snapValue = (num) => {
107
131
  if (!snap)
108
132
  return num;
@@ -123,13 +147,12 @@ export function Vector3Input({ label, value, onChange, snap }) {
123
147
  }
124
148
  };
125
149
  const startScrub = (e, index) => {
126
- e.preventDefault();
127
150
  dragState.current = {
128
151
  index,
129
152
  startX: e.clientX,
130
153
  startValue: value[index]
131
154
  };
132
- e.target.setPointerCapture(e.pointerId);
155
+ e.currentTarget.setPointerCapture(e.pointerId);
133
156
  document.body.style.cursor = "ew-resize";
134
157
  };
135
158
  const onScrubMove = (e) => {
@@ -158,40 +181,41 @@ export function Vector3Input({ label, value, onChange, snap }) {
158
181
  return;
159
182
  dragState.current = null;
160
183
  document.body.style.cursor = "";
161
- e.target.releasePointerCapture(e.pointerId);
184
+ e.currentTarget.releasePointerCapture(e.pointerId);
162
185
  };
163
186
  const axes = [
164
- { key: "x", color: 'rgba(248, 113, 113, 1)', index: 0 },
165
- { key: "y", color: 'rgba(134, 239, 172, 1)', index: 1 },
166
- { key: "z", color: 'rgba(96, 165, 250, 1)', index: 2 }
187
+ { key: "x", color: '#e06c75', index: 0 },
188
+ { key: "y", color: '#98c379', index: 1 },
189
+ { key: "z", color: '#61afef', index: 2 }
167
190
  ];
168
- 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: {
169
192
  flex: 1,
170
193
  display: 'flex',
171
194
  alignItems: 'center',
172
195
  gap: 4,
173
- backgroundColor: 'rgba(0, 0, 0, 0.3)',
174
- border: '1px solid rgba(34, 211, 238, 0.2)',
175
- borderRadius: 4,
196
+ backgroundColor: colors.bgInput,
197
+ border: `1px solid ${colors.border}`,
198
+ borderRadius: 3,
176
199
  padding: '4px 6px',
177
- minHeight: 32,
178
- }, children: [_jsx("span", { style: {
179
- fontSize: '12px',
180
- fontWeight: 'bold',
200
+ minHeight: 28,
201
+ cursor: 'ew-resize',
202
+ }, onPointerDown: e => startScrub(e, index), onPointerMove: onScrubMove, onPointerUp: endScrub, children: [_jsx("span", { style: {
203
+ fontSize: 11,
204
+ fontWeight: 600,
181
205
  color,
182
206
  width: 12,
183
- cursor: 'ew-resize',
184
207
  userSelect: 'none',
185
- }, onPointerDown: e => startScrub(e, index), onPointerMove: onScrubMove, onPointerUp: endScrub, children: key.toUpperCase() }), _jsx("input", { style: {
208
+ }, children: key.toUpperCase() }), _jsx("input", { style: {
186
209
  flex: 1,
187
210
  backgroundColor: 'transparent',
188
211
  border: 'none',
189
- fontSize: '12px',
190
- color: 'rgba(165, 243, 252, 1)',
212
+ fontSize: 11,
213
+ color: colors.text,
191
214
  fontFamily: 'monospace',
192
215
  outline: 'none',
193
216
  width: '100%',
194
217
  minWidth: 0,
218
+ cursor: 'inherit',
195
219
  }, type: "text", value: draft[index], onChange: e => {
196
220
  const next = [...draft];
197
221
  next[index] = e.target.value;
@@ -209,11 +233,11 @@ export function ColorInput({ label, value, onChange }) {
209
233
  return (_jsxs("div", { children: [label && _jsx(Label, { children: label }), _jsxs("div", { style: { display: 'flex', gap: 4, justifyContent: 'space-between' }, children: [_jsx("input", { type: "color", style: {
210
234
  height: 32,
211
235
  width: 48,
212
- backgroundColor: 'transparent',
213
- border: '1px solid rgba(34, 211, 238, 0.3)',
214
- borderRadius: 4,
236
+ backgroundColor: colors.bgInput,
237
+ border: `1px solid ${colors.border}`,
238
+ borderRadius: 3,
215
239
  cursor: 'pointer',
216
- padding: 0,
240
+ padding: 2,
217
241
  flexShrink: 0,
218
242
  }, value: value, onChange: e => onChange(e.target.value) }), _jsx("input", { type: "text", style: Object.assign({}, styles.input), value: value, onChange: e => onChange(e.target.value) })] })] }));
219
243
  }
@@ -224,14 +248,43 @@ export function BooleanInput({ label, value, onChange }) {
224
248
  return (_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between' }, children: [label && _jsx(Label, { children: label }), _jsx("input", { type: "checkbox", style: {
225
249
  height: 16,
226
250
  width: 16,
227
- backgroundColor: 'rgba(0, 0, 0, 0.4)',
228
- border: '1px solid rgba(34, 211, 238, 0.3)',
251
+ accentColor: colors.accent,
229
252
  cursor: 'pointer',
230
253
  }, checked: value, onChange: e => onChange(e.target.checked) })] }));
231
254
  }
232
255
  export function SelectInput({ label, value, onChange, options }) {
233
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))) })] }));
234
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
+ }
235
288
  export function FieldRenderer({ fields, values, onChange }) {
236
289
  const updateField = (name, value) => {
237
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,39 +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';
17
+ import { colors } from '../styles';
16
18
  import { useMemo } from 'react';
17
- 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;
18
22
  function TexturePicker({ value, onChange, basePath }) {
19
23
  const [textureFiles, setTextureFiles] = useState([]);
20
24
  const [showPicker, setShowPicker] = useState(false);
25
+ const [popupStyle, setPopupStyle] = useState(null);
26
+ const triggerRef = useRef(null);
21
27
  useEffect(() => {
22
28
  fetch(`${basePath}/textures/manifest.json`)
23
29
  .then(r => r.json())
24
30
  .then(data => setTextureFiles(Array.isArray(data) ? data : data.files || []))
25
31
  .catch(console.error);
26
32
  }, [basePath]);
27
- return (_jsxs("div", { style: { maxHeight: 128, overflowY: 'auto', position: 'relative', display: 'flex', alignItems: 'center' }, children: [_jsx(SingleTextureViewer, { file: 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: () => {
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]);
69
+ // Only show 3D preview for server-hosted textures (starting with / or http)
70
+ const canPreview = value && (value.startsWith('/') || value.startsWith('http'));
71
+ return (_jsxs("div", { style: { maxHeight: 128, overflow: 'visible', position: 'relative', display: 'flex', alignItems: 'center' }, children: [canPreview
72
+ ? _jsx(SingleTextureViewer, { file: value, basePath: basePath })
73
+ : value
74
+ ? _jsx("span", { style: { fontSize: 10, opacity: 0.6, maxWidth: 100, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: value })
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: () => {
28
76
  onChange(undefined);
29
- }, 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', left: '-10px', top: '50%', transform: 'translate(-100%, -50%)', background: 'rgba(0,0,0,0.9)', padding: 16, border: '1px solid rgba(34, 211, 238, 0.3)', maxHeight: '80vh', overflowY: 'auto', zIndex: 1000 }, 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) => {
30
78
  onChange(file);
31
79
  setShowPicker(false);
32
- }, basePath: basePath }) }))] }));
80
+ }, basePath: basePath }) }), document.body)] }));
33
81
  }
34
82
  function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
83
+ var _a;
84
+ const materialType = (_a = component.properties.materialType) !== null && _a !== void 0 ? _a : 'standard';
35
85
  const hasTexture = !!component.properties.texture;
36
86
  const hasRepeat = component.properties.repeat;
87
+ const isStandardMaterial = materialType === 'standard';
37
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
+ },
38
98
  { name: 'color', type: 'color', label: 'Color' },
99
+ { name: 'toneMapped', type: 'boolean', label: 'Tone Mapped' },
39
100
  { name: 'wireframe', type: 'boolean', label: 'Wireframe' },
40
101
  { name: 'transparent', type: 'boolean', label: 'Transparent' },
41
102
  { name: 'opacity', type: 'number', label: 'Opacity', min: 0, max: 1, step: 0.01 },
42
- { name: 'metalness', type: 'number', label: 'Metalness', min: 0, max: 1, step: 0.01 },
43
- { name: 'roughness', type: 'number', label: 'Roughness', min: 0, max: 1, step: 0.01 },
44
- { name: 'transmission', type: 'number', label: 'Transmission', min: 0, max: 1, step: 0.01 },
45
- { name: 'thickness', type: 'number', label: 'Thickness', min: 0, step: 0.1 },
46
- { 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
+ },
47
120
  {
48
121
  name: 'texture',
49
122
  type: 'custom',
@@ -62,6 +135,21 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
62
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 })] }));
63
136
  },
64
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
+ }] : []),
65
153
  { name: 'generateMipmaps', type: 'boolean', label: 'Generate Mipmaps' },
66
154
  {
67
155
  name: 'minFilter',
@@ -91,7 +179,8 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
91
179
  }
92
180
  // View for Material component
93
181
  function MaterialComponentView({ properties, loadedTextures }) {
94
- 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';
95
184
  const textureName = properties === null || properties === void 0 ? void 0 : properties.texture;
96
185
  const repeat = properties === null || properties === void 0 ? void 0 : properties.repeat;
97
186
  const repeatCount = properties === null || properties === void 0 ? void 0 : properties.repeatCount;
@@ -99,9 +188,14 @@ function MaterialComponentView({ properties, loadedTextures }) {
99
188
  const minFilter = (properties === null || properties === void 0 ? void 0 : properties.minFilter) || 'LinearMipmapLinearFilter';
100
189
  const magFilter = (properties === null || properties === void 0 ? void 0 : properties.magFilter) || 'LinearFilter';
101
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 : {};
102
195
  // Destructure all material props and separate custom texture handling props
103
- 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
104
- 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;
105
199
  const minFilterMap = {
106
200
  NearestFilter,
107
201
  LinearFilter,
@@ -135,10 +229,29 @@ function MaterialComponentView({ properties, loadedTextures }) {
135
229
  t.needsUpdate = true;
136
230
  return t;
137
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]]);
138
246
  if (!properties) {
139
247
  return _jsx("meshStandardMaterial", { color: "red", wireframe: true });
140
248
  }
141
- 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));
142
255
  }
143
256
  const MaterialComponent = {
144
257
  name: 'Material',
@@ -146,7 +259,9 @@ const MaterialComponent = {
146
259
  View: MaterialComponentView,
147
260
  nonComposable: true,
148
261
  defaultProperties: {
262
+ materialType: 'standard',
149
263
  color: '#ffffff',
264
+ toneMapped: true,
150
265
  wireframe: false,
151
266
  transparent: false,
152
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, overflowY: 'auto', 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', left: '-10px', top: '50%', transform: 'translate(-100%, -50%)', background: 'rgba(0,0,0,0.9)', padding: 16, border: '1px solid rgba(34, 211, 238, 0.3)', maxHeight: '80vh', overflowY: 'auto', 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 = [