react-three-game 0.0.56 → 0.0.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +16 -3
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/shared/GameCanvas.js +1 -3
  5. package/dist/tools/assetviewer/page.js +35 -14
  6. package/dist/tools/prefabeditor/Dropdown.d.ts +15 -0
  7. package/dist/tools/prefabeditor/Dropdown.js +82 -0
  8. package/dist/tools/prefabeditor/EditorContext.d.ts +5 -0
  9. package/dist/tools/prefabeditor/EditorTree.js +149 -91
  10. package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +33 -0
  11. package/dist/tools/prefabeditor/EditorTreeMenus.js +136 -0
  12. package/dist/tools/prefabeditor/EditorUI.js +1 -1
  13. package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
  14. package/dist/tools/prefabeditor/PrefabEditor.js +13 -2
  15. package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -0
  16. package/dist/tools/prefabeditor/PrefabRoot.js +120 -34
  17. package/dist/tools/prefabeditor/components/AmbientLightComponent.js +3 -7
  18. package/dist/tools/prefabeditor/components/CameraComponent.d.ts +3 -0
  19. package/dist/tools/prefabeditor/components/CameraComponent.js +45 -0
  20. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +50 -24
  21. package/dist/tools/prefabeditor/components/EnvironmentComponent.d.ts +3 -0
  22. package/dist/tools/prefabeditor/components/EnvironmentComponent.js +15 -0
  23. package/dist/tools/prefabeditor/components/GeometryComponent.js +46 -46
  24. package/dist/tools/prefabeditor/components/Input.d.ts +51 -1
  25. package/dist/tools/prefabeditor/components/Input.js +73 -21
  26. package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +16 -2
  27. package/dist/tools/prefabeditor/components/MaterialComponent.js +129 -15
  28. package/dist/tools/prefabeditor/components/ModelComponent.js +44 -3
  29. package/dist/tools/prefabeditor/components/PhysicsComponent.js +16 -81
  30. package/dist/tools/prefabeditor/components/SpotLightComponent.js +36 -23
  31. package/dist/tools/prefabeditor/components/TextComponent.js +7 -53
  32. package/dist/tools/prefabeditor/components/TransformComponent.js +18 -8
  33. package/dist/tools/prefabeditor/components/index.js +5 -1
  34. package/dist/tools/prefabeditor/styles.d.ts +5 -2
  35. package/dist/tools/prefabeditor/styles.js +7 -3
  36. package/dist/tools/prefabeditor/utils.d.ts +4 -3
  37. package/dist/tools/prefabeditor/utils.js +53 -5
  38. package/package.json +1 -1
  39. package/react-three-game-skill/react-three-game/SKILL.md +4 -1
  40. package/src/index.ts +7 -0
  41. package/src/shared/GameCanvas.tsx +0 -3
  42. package/src/tools/assetviewer/page.tsx +77 -45
  43. package/src/tools/prefabeditor/Dropdown.tsx +112 -0
  44. package/src/tools/prefabeditor/EditorContext.tsx +5 -0
  45. package/src/tools/prefabeditor/EditorTree.tsx +242 -178
  46. package/src/tools/prefabeditor/EditorTreeMenus.tsx +307 -0
  47. package/src/tools/prefabeditor/EditorUI.tsx +1 -1
  48. package/src/tools/prefabeditor/PrefabEditor.tsx +17 -4
  49. package/src/tools/prefabeditor/PrefabRoot.tsx +208 -58
  50. package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +5 -11
  51. package/src/tools/prefabeditor/components/CameraComponent.tsx +117 -0
  52. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +61 -30
  53. package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +47 -0
  54. package/src/tools/prefabeditor/components/GeometryComponent.tsx +69 -63
  55. package/src/tools/prefabeditor/components/Input.tsx +220 -27
  56. package/src/tools/prefabeditor/components/MaterialComponent.tsx +189 -18
  57. package/src/tools/prefabeditor/components/ModelComponent.tsx +51 -4
  58. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +44 -85
  59. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +52 -27
  60. package/src/tools/prefabeditor/components/TextComponent.tsx +58 -57
  61. package/src/tools/prefabeditor/components/TransformComponent.tsx +61 -9
  62. package/src/tools/prefabeditor/components/index.ts +5 -1
  63. package/src/tools/prefabeditor/styles.ts +7 -3
  64. package/src/tools/prefabeditor/utils.ts +55 -4
@@ -58,11 +58,12 @@ export declare function Input({ value, onChange, step, min, max, style, label }:
58
58
  export declare function Label({ children }: {
59
59
  children: React.ReactNode;
60
60
  }): import("react/jsx-runtime").JSX.Element;
61
- export declare function Vector3Input({ label, value, onChange, snap }: {
61
+ export declare function Vector3Input({ label, value, onChange, snap, labelExtra }: {
62
62
  label: string;
63
63
  value: [number, number, number];
64
64
  onChange: (v: [number, number, number]) => void;
65
65
  snap?: number;
66
+ labelExtra?: React.ReactNode;
66
67
  }): import("react/jsx-runtime").JSX.Element;
67
68
  export declare function ColorInput({ label, value, onChange }: {
68
69
  label?: string;
@@ -89,6 +90,55 @@ export declare function SelectInput({ label, value, onChange, options }: {
89
90
  label: string;
90
91
  }[];
91
92
  }): import("react/jsx-runtime").JSX.Element;
93
+ interface BoundFieldProps {
94
+ name: string;
95
+ values: Record<string, any>;
96
+ onChange: (values: Record<string, any>) => void;
97
+ }
98
+ interface BoundNumberFieldProps extends BoundFieldProps {
99
+ label: string;
100
+ fallback?: number;
101
+ step?: string | number;
102
+ min?: number;
103
+ max?: number;
104
+ style?: React.CSSProperties;
105
+ }
106
+ interface BoundStringFieldProps extends BoundFieldProps {
107
+ label: string;
108
+ fallback?: string;
109
+ placeholder?: string;
110
+ }
111
+ interface BoundColorFieldProps extends BoundFieldProps {
112
+ label: string;
113
+ fallback?: string;
114
+ }
115
+ interface BoundBooleanFieldProps extends BoundFieldProps {
116
+ label: string;
117
+ fallback?: boolean;
118
+ }
119
+ interface BoundSelectFieldProps extends BoundFieldProps {
120
+ label: string;
121
+ fallback?: string;
122
+ options: {
123
+ value: string;
124
+ label: string;
125
+ }[];
126
+ }
127
+ interface BoundVector3FieldProps extends BoundFieldProps {
128
+ label: string;
129
+ fallback?: [number, number, number];
130
+ snap?: number;
131
+ labelExtra?: React.ReactNode;
132
+ }
133
+ export declare function FieldGroup({ children }: {
134
+ children: React.ReactNode;
135
+ }): import("react/jsx-runtime").JSX.Element;
136
+ export declare function NumberField({ name, label, values, onChange, fallback, step, min, max, style, }: BoundNumberFieldProps): import("react/jsx-runtime").JSX.Element;
137
+ export declare function StringField({ name, label, values, onChange, fallback, placeholder, }: BoundStringFieldProps): import("react/jsx-runtime").JSX.Element;
138
+ export declare function ColorField({ name, label, values, onChange, fallback, }: BoundColorFieldProps): import("react/jsx-runtime").JSX.Element;
139
+ export declare function BooleanField({ name, label, values, onChange, fallback, }: BoundBooleanFieldProps): import("react/jsx-runtime").JSX.Element;
140
+ export declare function SelectField({ name, label, values, onChange, fallback, options, }: BoundSelectFieldProps): import("react/jsx-runtime").JSX.Element;
141
+ export declare function Vector3Field({ name, label, values, onChange, fallback, snap, labelExtra, }: BoundVector3FieldProps): import("react/jsx-runtime").JSX.Element;
92
142
  interface FieldRendererProps {
93
143
  fields: FieldDefinition[];
94
144
  values: Record<string, any>;
@@ -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,26 @@
1
+ import type { ThreeElement } from '@react-three/fiber';
1
2
  import { Component } from './ComponentRegistry';
2
- import { MeshStandardMaterialProperties } from 'three';
3
- export interface MaterialProps extends Omit<MeshStandardMaterialProperties, 'args'> {
3
+ import { MeshBasicNodeMaterial, MeshStandardNodeMaterial } from 'three/webgpu';
4
+ import { MeshBasicMaterialProperties, MeshStandardMaterialProperties } from 'three';
5
+ declare module '@react-three/fiber' {
6
+ interface ThreeElements {
7
+ meshBasicNodeMaterial: ThreeElement<typeof MeshBasicNodeMaterial>;
8
+ meshStandardNodeMaterial: ThreeElement<typeof MeshStandardNodeMaterial>;
9
+ }
10
+ }
11
+ export interface MaterialProps extends Omit<MeshStandardMaterialProperties & MeshBasicMaterialProperties, 'args' | 'normalScale'> {
12
+ materialType?: 'standard' | 'basic';
13
+ transmission?: number;
14
+ thickness?: number;
15
+ ior?: number;
4
16
  texture?: string;
5
17
  repeat?: boolean;
6
18
  repeatCount?: [number, number];
7
19
  generateMipmaps?: boolean;
8
20
  minFilter?: string;
9
21
  magFilter?: string;
22
+ normalMapTexture?: string;
23
+ normalScale?: [number, number];
10
24
  }
11
25
  declare const MaterialComponent: Component;
12
26
  export default MaterialComponent;
@@ -11,46 +11,118 @@ 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 { extend } from '@react-three/fiber';
15
+ import { useEffect, useLayoutEffect, useRef, useState } from 'react';
16
+ import { createPortal } from 'react-dom';
15
17
  import { FieldRenderer, Input } from './Input';
16
18
  import { colors } from '../styles';
17
19
  import { useMemo } from 'react';
18
- import { RepeatWrapping, ClampToEdgeWrapping, SRGBColorSpace, NearestFilter, LinearFilter, NearestMipmapNearestFilter, NearestMipmapLinearFilter, LinearMipmapNearestFilter, LinearMipmapLinearFilter } from 'three';
20
+ import { MeshBasicNodeMaterial, MeshStandardNodeMaterial } from 'three/webgpu';
21
+ import { RepeatWrapping, ClampToEdgeWrapping, SRGBColorSpace, LinearSRGBColorSpace, Vector2, NearestFilter, LinearFilter, NearestMipmapNearestFilter, NearestMipmapLinearFilter, LinearMipmapNearestFilter, LinearMipmapLinearFilter, FrontSide, BackSide, DoubleSide, } from 'three';
22
+ const PICKER_POPUP_WIDTH = 260;
23
+ const PICKER_POPUP_HEIGHT = 360;
24
+ extend({
25
+ MeshBasicNodeMaterial,
26
+ MeshStandardNodeMaterial,
27
+ });
19
28
  function TexturePicker({ value, onChange, basePath }) {
20
29
  const [textureFiles, setTextureFiles] = useState([]);
21
30
  const [showPicker, setShowPicker] = useState(false);
31
+ const [popupStyle, setPopupStyle] = useState(null);
32
+ const triggerRef = useRef(null);
22
33
  useEffect(() => {
23
34
  fetch(`${basePath}/textures/manifest.json`)
24
35
  .then(r => r.json())
25
36
  .then(data => setTextureFiles(Array.isArray(data) ? data : data.files || []))
26
37
  .catch(console.error);
27
38
  }, [basePath]);
39
+ useLayoutEffect(() => {
40
+ if (!showPicker || !triggerRef.current || typeof window === 'undefined')
41
+ return;
42
+ const updatePosition = () => {
43
+ var _a;
44
+ const rect = (_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
45
+ if (!rect)
46
+ return;
47
+ const preferredLeft = rect.left - PICKER_POPUP_WIDTH - 8;
48
+ const fallbackLeft = rect.right + 8;
49
+ const fitsLeft = preferredLeft >= 8;
50
+ const left = fitsLeft ? preferredLeft : Math.min(fallbackLeft, window.innerWidth - PICKER_POPUP_WIDTH - 8);
51
+ const top = Math.min(Math.max(8, rect.top), window.innerHeight - PICKER_POPUP_HEIGHT - 8);
52
+ setPopupStyle({
53
+ position: 'fixed',
54
+ left,
55
+ top,
56
+ background: colors.bg,
57
+ padding: 12,
58
+ border: `1px solid ${colors.border}`,
59
+ borderRadius: 6,
60
+ width: PICKER_POPUP_WIDTH,
61
+ height: PICKER_POPUP_HEIGHT,
62
+ overflow: 'hidden',
63
+ zIndex: 1000,
64
+ boxShadow: '0 4px 16px rgba(0,0,0,0.6)',
65
+ });
66
+ };
67
+ updatePosition();
68
+ window.addEventListener('resize', updatePosition);
69
+ window.addEventListener('scroll', updatePosition, true);
70
+ return () => {
71
+ window.removeEventListener('resize', updatePosition);
72
+ window.removeEventListener('scroll', updatePosition, true);
73
+ };
74
+ }, [showPicker]);
28
75
  // Only show 3D preview for server-hosted textures (starting with / or http)
29
76
  const canPreview = value && (value.startsWith('/') || value.startsWith('http'));
30
77
  return (_jsxs("div", { style: { maxHeight: 128, overflow: 'visible', position: 'relative', display: 'flex', alignItems: 'center' }, children: [canPreview
31
78
  ? _jsx(SingleTextureViewer, { file: value, basePath: basePath })
32
79
  : value
33
80
  ? _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: () => {
81
+ : 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
82
  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) => {
83
+ }, 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
84
  onChange(file);
38
85
  setShowPicker(false);
39
- }, basePath: basePath }) }))] }));
86
+ }, basePath: basePath }) }), document.body)] }));
40
87
  }
41
88
  function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
89
+ var _a;
90
+ const materialType = (_a = component.properties.materialType) !== null && _a !== void 0 ? _a : 'standard';
42
91
  const hasTexture = !!component.properties.texture;
43
92
  const hasRepeat = component.properties.repeat;
93
+ const isStandardMaterial = materialType === 'standard';
44
94
  const fields = [
95
+ {
96
+ name: 'materialType',
97
+ type: 'select',
98
+ label: 'Material Type',
99
+ options: [
100
+ { value: 'standard', label: 'Standard' },
101
+ { value: 'basic', label: 'Basic' },
102
+ ],
103
+ },
45
104
  { name: 'color', type: 'color', label: 'Color' },
105
+ { name: 'toneMapped', type: 'boolean', label: 'Tone Mapped' },
46
106
  { name: 'wireframe', type: 'boolean', label: 'Wireframe' },
47
107
  { name: 'transparent', type: 'boolean', label: 'Transparent' },
48
108
  { 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 },
109
+ ...(isStandardMaterial ? [
110
+ { name: 'metalness', type: 'number', label: 'Metalness', min: 0, max: 1, step: 0.01 },
111
+ { name: 'roughness', type: 'number', label: 'Roughness', min: 0, max: 1, step: 0.01 },
112
+ { name: 'transmission', type: 'number', label: 'Transmission', min: 0, max: 1, step: 0.01 },
113
+ { name: 'thickness', type: 'number', label: 'Thickness', min: 0, step: 0.1 },
114
+ { name: 'ior', type: 'number', label: 'IOR (Index of Refraction)', min: 1, max: 2.333, step: 0.01 },
115
+ ] : []),
116
+ {
117
+ name: 'side',
118
+ type: 'select',
119
+ label: 'Side',
120
+ options: [
121
+ { value: 'FrontSide', label: 'Front' },
122
+ { value: 'BackSide', label: 'Back' },
123
+ { value: 'DoubleSide', label: 'Double' },
124
+ ],
125
+ },
54
126
  {
55
127
  name: 'texture',
56
128
  type: 'custom',
@@ -69,6 +141,21 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
69
141
  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
142
  },
71
143
  }] : []),
144
+ {
145
+ name: 'normalMapTexture',
146
+ type: 'custom',
147
+ label: 'Normal Map',
148
+ render: ({ value, onChange }) => (_jsx(TexturePicker, { value: value, onChange: onChange, basePath: basePath })),
149
+ },
150
+ ...(component.properties.normalMapTexture ? [{
151
+ name: 'normalScale',
152
+ type: 'custom',
153
+ label: 'Normal Scale (X, Y)',
154
+ render: ({ value, onChange }) => {
155
+ var _a, _b;
156
+ 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 })] }));
157
+ },
158
+ }] : []),
72
159
  { name: 'generateMipmaps', type: 'boolean', label: 'Generate Mipmaps' },
73
160
  {
74
161
  name: 'minFilter',
@@ -98,7 +185,8 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
98
185
  }
99
186
  // View for Material component
100
187
  function MaterialComponentView({ properties, loadedTextures }) {
101
- var _a;
188
+ var _a, _b, _c;
189
+ const materialType = (_a = properties === null || properties === void 0 ? void 0 : properties.materialType) !== null && _a !== void 0 ? _a : 'standard';
102
190
  const textureName = properties === null || properties === void 0 ? void 0 : properties.texture;
103
191
  const repeat = properties === null || properties === void 0 ? void 0 : properties.repeat;
104
192
  const repeatCount = properties === null || properties === void 0 ? void 0 : properties.repeatCount;
@@ -106,9 +194,14 @@ function MaterialComponentView({ properties, loadedTextures }) {
106
194
  const minFilter = (properties === null || properties === void 0 ? void 0 : properties.minFilter) || 'LinearMipmapLinearFilter';
107
195
  const magFilter = (properties === null || properties === void 0 ? void 0 : properties.magFilter) || 'LinearFilter';
108
196
  const texture = textureName && loadedTextures ? loadedTextures[textureName] : undefined;
197
+ const normalMapTextureName = properties === null || properties === void 0 ? void 0 : properties.normalMapTexture;
198
+ const normalScaleProp = properties === null || properties === void 0 ? void 0 : properties.normalScale;
199
+ const normalMapTexture = normalMapTextureName && loadedTextures ? loadedTextures[normalMapTextureName] : undefined;
200
+ const materialSource = properties !== null && properties !== void 0 ? properties : {};
109
201
  // 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"]);
202
+ 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 } = materialSource, materialProps = __rest(materialSource, ["texture", "repeat", "repeatCount", "generateMipmaps", "minFilter", "magFilter", "map", "materialType", "normalMapTexture", "normalScale", "normalMap", "side"]);
203
+ const sideMap = { FrontSide, BackSide, DoubleSide };
204
+ const resolvedSide = sideProp ? ((_b = sideMap[sideProp]) !== null && _b !== void 0 ? _b : FrontSide) : FrontSide;
112
205
  const minFilterMap = {
113
206
  NearestFilter,
114
207
  LinearFilter,
@@ -142,10 +235,29 @@ function MaterialComponentView({ properties, loadedTextures }) {
142
235
  t.needsUpdate = true;
143
236
  return t;
144
237
  }, [texture, repeat, repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[0], repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[1], generateMipmaps, minFilter, magFilter]);
238
+ const finalNormalMap = useMemo(() => {
239
+ if (!normalMapTexture)
240
+ return undefined;
241
+ const t = normalMapTexture.clone();
242
+ t.colorSpace = LinearSRGBColorSpace;
243
+ t.needsUpdate = true;
244
+ return t;
245
+ }, [normalMapTexture]);
246
+ const normalScaleVec = useMemo(() => {
247
+ var _a, _b;
248
+ if (!finalNormalMap)
249
+ return undefined;
250
+ 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);
251
+ }, [finalNormalMap, normalScaleProp === null || normalScaleProp === void 0 ? void 0 : normalScaleProp[0], normalScaleProp === null || normalScaleProp === void 0 ? void 0 : normalScaleProp[1]]);
145
252
  if (!properties) {
146
- return _jsx("meshStandardMaterial", { color: "red", wireframe: true });
253
+ return _jsx("meshStandardNodeMaterial", { color: "red", wireframe: true });
254
+ }
255
+ const materialKey = `${(_c = finalTexture === null || finalTexture === void 0 ? void 0 : finalTexture.uuid) !== null && _c !== void 0 ? _c : 'no-texture'}:${materialProps.transparent ? 'transparent' : 'opaque'}`;
256
+ const sharedProps = Object.assign({ map: finalTexture, side: resolvedSide }, materialProps);
257
+ if (materialType === 'basic') {
258
+ return _jsx("meshBasicNodeMaterial", Object.assign({}, sharedProps), materialKey);
147
259
  }
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'));
260
+ return (_jsx("meshStandardNodeMaterial", Object.assign({}, sharedProps, { normalMap: finalNormalMap, normalScale: normalScaleVec }), materialKey));
149
261
  }
150
262
  const MaterialComponent = {
151
263
  name: 'Material',
@@ -153,7 +265,9 @@ const MaterialComponent = {
153
265
  View: MaterialComponentView,
154
266
  nonComposable: true,
155
267
  defaultProperties: {
268
+ materialType: 'standard',
156
269
  color: '#ffffff',
270
+ toneMapped: true,
157
271
  wireframe: false,
158
272
  transparent: false,
159
273
  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 = [