react-three-game 0.0.99 → 0.0.101

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 (34) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +1 -0
  3. package/dist/tools/assetviewer/page.js +70 -58
  4. package/dist/tools/dragdrop/DragDropLoader.d.ts +3 -0
  5. package/dist/tools/dragdrop/DragDropLoader.js +183 -44
  6. package/dist/tools/dragdrop/index.d.ts +1 -1
  7. package/dist/tools/dragdrop/index.js +1 -1
  8. package/dist/tools/dragdrop/modelLoader.js +2 -0
  9. package/dist/tools/prefabeditor/EditorUI.js +7 -8
  10. package/dist/tools/prefabeditor/PrefabEditor.d.ts +3 -0
  11. package/dist/tools/prefabeditor/PrefabEditor.js +28 -11
  12. package/dist/tools/prefabeditor/PrefabRoot.d.ts +2 -0
  13. package/dist/tools/prefabeditor/PrefabRoot.js +51 -35
  14. package/dist/tools/prefabeditor/components/BufferGeometryComponent.js +20 -0
  15. package/dist/tools/prefabeditor/components/CameraComponent.js +1 -14
  16. package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +5 -0
  17. package/dist/tools/prefabeditor/components/ComponentRegistry.js +31 -0
  18. package/dist/tools/prefabeditor/components/DataComponent.js +1 -0
  19. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +31 -119
  20. package/dist/tools/prefabeditor/components/EnvironmentComponent.js +1 -0
  21. package/dist/tools/prefabeditor/components/GeometryComponent.js +1 -0
  22. package/dist/tools/prefabeditor/components/Input.d.ts +1 -0
  23. package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +1 -0
  24. package/dist/tools/prefabeditor/components/MaterialComponent.js +89 -52
  25. package/dist/tools/prefabeditor/components/ModelComponent.js +45 -3
  26. package/dist/tools/prefabeditor/components/PointLightComponent.js +19 -25
  27. package/dist/tools/prefabeditor/components/SpotLightComponent.js +27 -42
  28. package/dist/tools/prefabeditor/components/SpriteComponent.js +1 -0
  29. package/dist/tools/prefabeditor/components/TransformComponent.js +1 -0
  30. package/dist/tools/prefabeditor/modelPrefab.d.ts +16 -0
  31. package/dist/tools/prefabeditor/modelPrefab.js +180 -0
  32. package/dist/tools/prefabeditor/prefabStore.d.ts +1 -0
  33. package/dist/tools/prefabeditor/prefabStore.js +75 -42
  34. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -18,6 +18,8 @@ export { FieldRenderer, FieldGroup, ListEditor, Label, Vector3Input, Vector3Fiel
18
18
  export { loadJson, saveJson, exportGLB, exportGLBData, regenerateIds, computeParentWorldMatrix, } from './tools/prefabeditor/utils';
19
19
  export type { ExportGLBOptions } from './tools/prefabeditor/utils';
20
20
  export { createModelNode, createImageNode, } from './tools/prefabeditor/prefab';
21
+ export { decomposeModelToPrefabNodes } from './tools/prefabeditor/modelPrefab';
22
+ export type { DecomposeModelOptions } from './tools/prefabeditor/modelPrefab';
21
23
  export type { PrefabEditorProps, PrefabNode, PrefabEditorRef, } from './tools/prefabeditor/PrefabEditor';
22
24
  export type { PrefabRootProps } from './tools/prefabeditor/PrefabRoot';
23
25
  export type { AssetRuntime, NodeApi, LiveRef } from './tools/prefabeditor/assetRuntime';
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ export { FieldRenderer, FieldGroup, ListEditor, Label, Vector3Input, Vector3Fiel
19
19
  // Prefab Editor - Utils
20
20
  export { loadJson, saveJson, exportGLB, exportGLBData, regenerateIds, computeParentWorldMatrix, } from './tools/prefabeditor/utils';
21
21
  export { createModelNode, createImageNode, } from './tools/prefabeditor/prefab';
22
+ export { decomposeModelToPrefabNodes } from './tools/prefabeditor/modelPrefab';
22
23
  export { useAssetRuntime, useNode, useNodeHandle, useNodeObject } from './tools/prefabeditor/assetRuntime';
23
24
  export { useScene } from './tools/prefabeditor/PrefabRoot';
24
25
  export { MaterialOverridesProvider, useMaterialOverrides } from './tools/prefabeditor/components/MaterialComponent';
@@ -1,45 +1,71 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Canvas } from "@react-three/fiber";
2
+ import { Canvas, useLoader } from "@react-three/fiber";
3
3
  import { OrbitControls, View, PerspectiveCamera } from "@react-three/drei";
4
4
  import { Suspense, useEffect, useLayoutEffect, useState, useRef } from "react";
5
5
  import { createPortal } from 'react-dom';
6
- import { TextureLoader } from "three";
6
+ import { Mesh, TextureLoader } from "three";
7
7
  import { loadModel } from "../dragdrop/modelLoader";
8
+ import { resolvePrefabAssetPath } from "../prefabeditor/PrefabEditor";
9
+ import { base, colors, fonts } from "../prefabeditor/styles";
8
10
  const styles = {
9
- errorIcon: { color: '#fca5a5', fontSize: 12 }, // text-red-400 text-xs
11
+ errorIcon: { color: colors.danger, fontSize: 12 },
10
12
  flexFillRelative: { flex: 1, position: 'relative' },
11
- bottomLabel: { backgroundColor: 'rgba(0,0,0,0.6)', color: '#f9fafb', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' },
12
- textLight: { color: '#f9fafb' },
13
+ bottomLabel: { backgroundColor: colors.bgLight, color: colors.text, fontSize: fonts.sizeSm, padding: '1px 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center', borderTop: `1px solid ${colors.borderFaint}` },
14
+ textLight: { color: colors.text, fontFamily: fonts.family, fontSize: fonts.size },
13
15
  iconLarge: { fontSize: 20 }
14
16
  };
15
17
  const assetViewerColors = {
16
- panelBg: '#111827',
17
- controlBg: '#1f2937',
18
- text: '#f9fafb',
19
- border: 'rgba(255,255,255,0.12)',
20
- accentBorder: 'rgba(34, 211, 238, 0.3)',
18
+ panelBg: colors.bg,
19
+ controlBg: colors.bgSurface,
20
+ previewBg: colors.bgLight,
21
+ text: colors.text,
22
+ border: colors.border,
23
+ borderFaint: colors.borderFaint,
24
+ accentBorder: colors.accentBorder,
25
+ errorBg: colors.dangerBg,
21
26
  };
22
27
  const assetPickerPopupBaseStyle = {
23
28
  background: assetViewerColors.panelBg,
24
29
  border: `1px solid ${assetViewerColors.border}`,
25
30
  borderRadius: 0,
26
- boxShadow: '0 4px 16px rgba(0,0,0,0.6)',
27
- };
28
- const assetPickerButtonBaseStyle = {
29
- backgroundColor: assetViewerColors.controlBg,
30
- color: 'inherit',
31
- fontSize: 10,
32
- cursor: 'pointer',
33
- border: `1px solid ${assetViewerColors.border}`,
34
- borderRadius: 0,
31
+ boxShadow: 'none',
32
+ color: colors.text,
33
+ fontFamily: fonts.family,
34
+ fontSize: fonts.size,
35
35
  };
36
+ const assetPickerButtonBaseStyle = Object.assign(Object.assign({}, base.btn), { background: assetViewerColors.controlBg, color: colors.text, fontSize: fonts.size, cursor: 'pointer' });
36
37
  const assetPickerWideButtonStyle = Object.assign(Object.assign({}, assetPickerButtonBaseStyle), { width: '100%', padding: '6px 8px' });
38
+ function disposeMaterial(material) {
39
+ if (Array.isArray(material)) {
40
+ material.forEach(entry => entry.dispose());
41
+ return;
42
+ }
43
+ material.dispose();
44
+ }
45
+ function disposeObject3D(object) {
46
+ object.traverse(child => {
47
+ var _a;
48
+ if (!(child instanceof Mesh))
49
+ return;
50
+ (_a = child.geometry) === null || _a === void 0 ? void 0 : _a.dispose();
51
+ disposeMaterial(child.material);
52
+ });
53
+ }
37
54
  const assetPickerSmallButtonStyle = Object.assign(Object.assign({}, assetPickerButtonBaseStyle), { padding: '4px 8px' });
38
55
  const assetPickerEmptyPreviewStyle = {
39
- backgroundColor: assetViewerColors.controlBg,
56
+ backgroundColor: assetViewerColors.previewBg,
40
57
  border: `1px dashed ${assetViewerColors.border}`,
41
58
  borderRadius: 0,
42
59
  };
60
+ const assetTileStyle = {
61
+ backgroundColor: assetViewerColors.previewBg,
62
+ color: assetViewerColors.text,
63
+ border: `1px solid ${assetViewerColors.borderFaint}`,
64
+ cursor: 'pointer',
65
+ display: 'flex',
66
+ flexDirection: 'column',
67
+ boxSizing: 'border-box',
68
+ };
43
69
  function getItemsInPath(files, currentPath) {
44
70
  // Remove the leading category folder (e.g., /textures/, /models/, /sounds/)
45
71
  const filesWithoutCategory = files.map(file => {
@@ -64,17 +90,7 @@ function getItemsInPath(files, currentPath) {
64
90
  return { folders: Array.from(folders), filesInCurrentPath };
65
91
  }
66
92
  function FolderTile({ name, onClick }) {
67
- return (_jsxs("div", { onClick: onClick, style: {
68
- maxWidth: 60,
69
- aspectRatio: '1 / 1',
70
- backgroundColor: assetViewerColors.controlBg,
71
- color: assetViewerColors.text,
72
- cursor: 'pointer',
73
- display: 'flex',
74
- flexDirection: 'column',
75
- alignItems: 'center',
76
- justifyContent: 'center'
77
- }, children: [_jsx("div", { style: { fontSize: 24 }, children: "\uD83D\uDCC1" }), _jsx("div", { style: { fontSize: 10, textAlign: 'center', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', width: '100%', padding: '0 4px', marginTop: 4 }, children: name })] }));
93
+ return (_jsxs("div", { onClick: onClick, style: Object.assign(Object.assign({ maxWidth: 60, aspectRatio: '1 / 1' }, assetTileStyle), { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }), children: [_jsx("div", { style: { fontSize: 24 }, children: "\uD83D\uDCC1" }), _jsx("div", { style: { fontSize: 10, textAlign: 'center', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', width: '100%', padding: '0 4px', marginTop: 4 }, children: name })] }));
78
94
  }
79
95
  function useInView() {
80
96
  const [isInView, setIsInView] = useState(false);
@@ -101,33 +117,19 @@ function AssetListViewer({ files, selected, onSelect, renderCard }) {
101
117
  const pathParts = currentPath.split('/').filter(Boolean);
102
118
  pathParts.pop();
103
119
  setCurrentPath(pathParts.join('/'));
104
- }, style: Object.assign(Object.assign({}, assetPickerSmallButtonStyle), { marginBottom: 4, fontSize: 12, border: 'none' }), children: "\u2190 Back" })), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 4 }, children: [folders.map((folder) => (_jsx(FolderTile, { name: folder, onClick: () => setCurrentPath(currentPath ? `${currentPath}/${folder}` : folder) }, folder))), filesInCurrentPath.map((file) => (_jsx("div", { children: renderCard(file, onSelect) }, file)))] })] }));
120
+ }, style: Object.assign(Object.assign({}, assetPickerSmallButtonStyle), { marginBottom: 4 }), children: "\u2190 Back" })), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 4 }, children: [folders.map((folder) => (_jsx(FolderTile, { name: folder, onClick: () => setCurrentPath(currentPath ? `${currentPath}/${folder}` : folder) }, folder))), filesInCurrentPath.map((file) => (_jsx("div", { children: renderCard(file, onSelect) }, file)))] })] }));
105
121
  }
106
122
  export function TextureListViewer({ files, selected, onSelect, basePath = "" }) {
107
123
  return (_jsxs("div", { style: { position: 'relative', width: '100%', height: '100%' }, children: [_jsx("div", { style: { width: '100%', height: '100%', overflowY: 'auto', overflowX: 'hidden', paddingRight: 4 }, children: _jsx(AssetListViewer, { files: files, selected: selected, onSelect: onSelect, renderCard: (file, onSelectHandler) => (_jsx(TextureCard, { file: file, basePath: basePath, onSelect: onSelectHandler })) }) }), _jsx(SharedCanvas, {})] }));
108
124
  }
109
125
  function TextureCard({ file, onSelect, basePath = "" }) {
110
- const [error, setError] = useState(false);
111
126
  const [isHovered, setIsHovered] = useState(false);
112
127
  const { ref, isInView } = useInView();
113
- const fullPath = basePath ? `/${basePath}${file}` : file;
114
- if (error) {
115
- return (_jsx("div", { ref: ref, style: { aspectRatio: '1 / 1', backgroundColor: '#c30000', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }, onClick: () => onSelect(file), children: _jsx("div", { style: styles.errorIcon, children: "\u2717" }) }));
116
- }
117
- return (_jsxs("div", { ref: ref, style: { maxWidth: 60, aspectRatio: '1 / 1', backgroundColor: '#aeaeae', color: '#f9fafb', cursor: 'pointer', display: 'flex', flexDirection: 'column' }, onClick: () => onSelect(file), onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), children: [_jsx("div", { style: { flex: 1, position: 'relative' }, children: isInView ? (_jsxs(View, { style: { width: '100%', height: '100%' }, children: [_jsx(PerspectiveCamera, { makeDefault: true, position: [0, 0, 2.5], fov: 50 }), _jsx("ambientLight", { intensity: 0.8 }), _jsx("pointLight", { position: [5, 5, 5], intensity: 0.5 }), _jsx(TextureSphere, { url: fullPath, onError: () => setError(true) }), _jsx(OrbitControls, { enableZoom: false, enablePan: false, autoRotate: isHovered, autoRotateSpeed: 2 })] })) : null }), _jsx("div", { style: styles.bottomLabel, children: file.split('/').pop() })] }));
128
+ const fullPath = resolvePrefabAssetPath(basePath, file);
129
+ return (_jsxs("div", { ref: ref, style: Object.assign({ maxWidth: 60, aspectRatio: '1 / 1' }, assetTileStyle), onClick: () => onSelect(file), onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), children: [_jsx("div", { style: { flex: 1, position: 'relative' }, children: isInView ? (_jsxs(View, { style: { width: '100%', height: '100%' }, children: [_jsx(PerspectiveCamera, { makeDefault: true, position: [0, 0, 2.5], fov: 50 }), _jsx("ambientLight", { intensity: 0.8 }), _jsx("pointLight", { position: [5, 5, 5], intensity: 0.5 }), _jsx(TextureSphere, { url: fullPath }), _jsx(OrbitControls, { enableZoom: false, enablePan: false, autoRotate: isHovered, autoRotateSpeed: 2 })] })) : null }), _jsx("div", { style: styles.bottomLabel, children: file.split('/').pop() })] }));
118
130
  }
119
- function TextureSphere({ url, onError }) {
120
- const [texture, setTexture] = useState(null);
121
- useEffect(() => {
122
- setTexture(null);
123
- const loader = new TextureLoader();
124
- loader.load(url, (tex) => setTexture(tex), undefined, (err) => {
125
- console.warn('Failed to load texture:', url, err);
126
- onError === null || onError === void 0 ? void 0 : onError();
127
- });
128
- }, [url]);
129
- if (!texture)
130
- return null;
131
+ function TextureSphere({ url }) {
132
+ const texture = useLoader(TextureLoader, url);
131
133
  return (_jsxs("mesh", { position: [0, 0, 0], children: [_jsx("sphereGeometry", { args: [1, 32, 32] }), _jsx("meshStandardMaterial", { map: texture })] }));
132
134
  }
133
135
  export function ModelListViewer({ files, selected, onSelect, basePath = "" }) {
@@ -136,31 +138,41 @@ export function ModelListViewer({ files, selected, onSelect, basePath = "" }) {
136
138
  function ModelCard({ file, onSelect, basePath = "", size = 60, }) {
137
139
  const [error, setError] = useState(false);
138
140
  const { ref, isInView } = useInView();
139
- const fullPath = basePath ? `/${basePath}${file}` : file;
141
+ const fullPath = resolvePrefabAssetPath(basePath, file);
140
142
  if (error) {
141
- return (_jsx("div", { ref: ref, style: { aspectRatio: '1 / 1', backgroundColor: '#c30000', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }, onClick: () => onSelect(file), children: _jsx("div", { style: styles.errorIcon, children: "\u2717" }) }));
143
+ return (_jsx("div", { ref: ref, style: { aspectRatio: '1 / 1', backgroundColor: assetViewerColors.errorBg, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', border: `1px solid ${colors.dangerBorder}` }, onClick: () => onSelect(file), children: _jsx("div", { style: styles.errorIcon, children: "\u2717" }) }));
142
144
  }
143
- return (_jsxs("div", { ref: ref, style: { width: size, aspectRatio: '1 / 1', backgroundColor: '#aeaeae', color: '#f9fafb', cursor: 'pointer', display: 'flex', flexDirection: 'column' }, onClick: () => onSelect(file), children: [_jsx("div", { style: styles.flexFillRelative, children: isInView ? (_jsxs(View, { style: { width: '100%', height: '100%' }, children: [_jsx(PerspectiveCamera, { makeDefault: true, position: [0, 1, 3], fov: 50 }), _jsxs(Suspense, { fallback: null, children: [_jsx("ambientLight", { intensity: 1 }), _jsx("pointLight", { position: [5, 5, 5], intensity: 0.5 }), _jsx(ModelPreview, { url: fullPath, onError: () => setError(true) }), _jsx(OrbitControls, { enableZoom: false })] })] })) : null }), _jsx("div", { style: styles.bottomLabel, children: file.split('/').pop() })] }));
145
+ return (_jsxs("div", { ref: ref, style: Object.assign({ width: size, aspectRatio: '1 / 1' }, assetTileStyle), onClick: () => onSelect(file), children: [_jsx("div", { style: styles.flexFillRelative, children: isInView ? (_jsxs(View, { style: { width: '100%', height: '100%' }, children: [_jsx(PerspectiveCamera, { makeDefault: true, position: [0, 1, 3], fov: 50 }), _jsxs(Suspense, { fallback: null, children: [_jsx("ambientLight", { intensity: 1 }), _jsx("pointLight", { position: [5, 5, 5], intensity: 0.5 }), _jsx(ModelPreview, { url: fullPath, onError: () => setError(true) }), _jsx(OrbitControls, { enableZoom: false })] })] })) : null }), _jsx("div", { style: styles.bottomLabel, children: file.split('/').pop() })] }));
144
146
  }
145
147
  function ModelPreview({ url, onError }) {
146
148
  const [model, setModel] = useState(null);
149
+ const modelRef = useRef(null);
147
150
  const onErrorRef = useRef(onError);
148
151
  onErrorRef.current = onError;
149
152
  useEffect(() => {
150
153
  let cancelled = false;
154
+ modelRef.current && disposeObject3D(modelRef.current);
155
+ modelRef.current = null;
151
156
  setModel(null);
152
157
  loadModel(url).then((result) => {
153
158
  var _a;
154
- if (cancelled)
159
+ if (cancelled) {
160
+ result.model && disposeObject3D(result.model);
155
161
  return;
162
+ }
156
163
  if (result.success && result.model) {
164
+ modelRef.current = result.model;
157
165
  setModel(result.model);
158
166
  }
159
167
  else {
160
168
  (_a = onErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onErrorRef);
161
169
  }
162
170
  });
163
- return () => { cancelled = true; };
171
+ return () => {
172
+ cancelled = true;
173
+ modelRef.current && disposeObject3D(modelRef.current);
174
+ modelRef.current = null;
175
+ };
164
176
  }, [url]);
165
177
  if (!model)
166
178
  return null;
@@ -171,8 +183,8 @@ export function SoundListViewer({ files, selected, onSelect, basePath = "" }) {
171
183
  }
172
184
  function SoundCard({ file, onSelect, basePath = "" }) {
173
185
  const fileName = file.split('/').pop() || '';
174
- const fullPath = basePath ? `/${basePath}${file}` : file;
175
- return (_jsxs("div", { onClick: () => onSelect(file), style: { aspectRatio: '1 / 1', backgroundColor: '#374151', color: '#f9fafb', cursor: 'pointer', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }, children: [_jsx("div", { style: styles.iconLarge, children: "\uD83D\uDD0A" }), _jsx("div", { style: { color: '#f9fafb', fontSize: 12, padding: '0 4px', marginTop: 4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center', width: '100%' }, children: fileName })] }));
186
+ const fullPath = resolvePrefabAssetPath(basePath, file);
187
+ return (_jsxs("div", { onClick: () => onSelect(file), style: Object.assign(Object.assign({ aspectRatio: '1 / 1' }, assetTileStyle), { alignItems: 'center', justifyContent: 'center' }), children: [_jsx("div", { style: styles.iconLarge, children: "\uD83D\uDD0A" }), _jsx("div", { style: { color: colors.text, fontSize: fonts.size, padding: '0 4px', marginTop: 4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center', width: '100%' }, children: fileName })] }));
176
188
  }
177
189
  const PICKER_POPUP_WIDTH = 260;
178
190
  const PICKER_POPUP_HEIGHT = 360;
@@ -225,7 +237,7 @@ export function TexturePicker({ value, onChange, basePath = "" }) {
225
237
  return (_jsx(AssetPicker, { value: value, onChange: onChange, basePath: basePath, manifestFolder: "textures", rootStyle: { maxHeight: 128, overflow: 'visible', position: 'relative', display: 'flex', alignItems: 'center' }, changeButtonStyle: Object.assign(Object.assign({}, assetPickerSmallButtonStyle), { marginTop: 4 }), clearButtonStyle: Object.assign(Object.assign({}, assetPickerSmallButtonStyle), { marginTop: 4, marginLeft: 4 }), preview: _jsx(SingleTextureViewer, { file: value, basePath: basePath }), renderList: ({ files, value: selectedValue, onSelect, basePath: currentBasePath }) => (_jsx(TextureListViewer, { files: files, selected: selectedValue || undefined, onSelect: onSelect, basePath: currentBasePath })) }));
226
238
  }
227
239
  export function ModelPicker({ value, onChange, basePath = "", pickerKey }) {
228
- return (_jsx(AssetPicker, { value: value, onChange: onChange, basePath: basePath, manifestFolder: "models", rootStyle: { maxHeight: 160, overflow: 'visible', position: 'relative', display: 'flex', gap: 8, alignItems: 'center', justifyContent: 'center' }, controlsStyle: { display: 'flex', flexDirection: 'column', gap: 6, flex: '0 0 84px', minWidth: 84, justifyContent: 'flex-end' }, changeButtonStyle: Object.assign(Object.assign({}, assetPickerWideButtonStyle), { border: `1px solid ${assetViewerColors.accentBorder}` }), clearButtonStyle: Object.assign(Object.assign({}, assetPickerWideButtonStyle), { border: `1px solid ${assetViewerColors.accentBorder}` }), popupStyle: { background: 'rgba(0,0,0,0.9)', border: `1px solid ${assetViewerColors.accentBorder}` }, preview: _jsx("div", { style: { flex: '0 0 auto' }, children: _jsx(SingleModelViewer, { file: value ? `/${value}` : undefined, basePath: basePath }) }), renderList: ({ files, value: selectedValue, onSelect, basePath: currentBasePath }) => (_jsx(ModelListViewer, { files: files, selected: selectedValue ? `/${selectedValue}` : undefined, onSelect: (file) => onSelect(file.startsWith('/') ? file.slice(1) : file), basePath: currentBasePath }, pickerKey)) }));
240
+ return (_jsx(AssetPicker, { value: value, onChange: onChange, basePath: basePath, manifestFolder: "models", rootStyle: { maxHeight: 160, overflow: 'visible', position: 'relative', display: 'flex', gap: 8, alignItems: 'center', justifyContent: 'center' }, controlsStyle: { display: 'flex', flexDirection: 'column', gap: 6, flex: '0 0 84px', minWidth: 84, justifyContent: 'flex-end' }, changeButtonStyle: Object.assign(Object.assign({}, assetPickerWideButtonStyle), { border: `1px solid ${assetViewerColors.accentBorder}` }), clearButtonStyle: Object.assign(Object.assign({}, assetPickerWideButtonStyle), { border: `1px solid ${assetViewerColors.accentBorder}` }), popupStyle: { border: `1px solid ${assetViewerColors.accentBorder}` }, preview: _jsx("div", { style: { flex: '0 0 auto' }, children: _jsx(SingleModelViewer, { file: value, basePath: basePath }) }), renderList: ({ files, value: selectedValue, onSelect, basePath: currentBasePath }) => (_jsx(ModelListViewer, { files: files, selected: selectedValue ? `/${selectedValue}` : undefined, onSelect: (file) => onSelect(file.startsWith('/') ? file.slice(1) : file), basePath: currentBasePath }, pickerKey)) }));
229
241
  }
230
242
  export function SoundPicker({ value, onChange, basePath = "" }) {
231
243
  return (_jsx(AssetPicker, { value: value, onChange: onChange, basePath: basePath, manifestFolder: "sound", rootStyle: { maxHeight: 76, overflow: 'visible', position: 'relative', display: 'flex', gap: 8, alignItems: 'center', justifyContent: 'center' }, controlsStyle: { display: 'flex', flexDirection: 'column', gap: 6, flex: '0 0 84px', minWidth: 84, justifyContent: 'flex-end' }, changeButtonStyle: assetPickerWideButtonStyle, clearButtonStyle: assetPickerWideButtonStyle, preview: _jsx("div", { style: { flex: '0 0 auto', minWidth: 84 }, children: value ? _jsx(SingleSoundViewer, { file: value, basePath: basePath }) : _jsx("div", { style: Object.assign({ width: 84, height: 60 }, assetPickerEmptyPreviewStyle) }) }), renderList: ({ files, value: selectedValue, onSelect, basePath: currentBasePath }) => (_jsx(SoundListViewer, { files: files, selected: selectedValue || undefined, onSelect: onSelect, basePath: currentBasePath })) }));
@@ -17,6 +17,9 @@ export interface FilePickerProps extends AssetLoadOptions, DivProps {
17
17
  children?: ReactNode;
18
18
  multiple?: boolean;
19
19
  }
20
+ export declare function loadDroppedAssets(dataTransfer: DataTransfer | null, options: AssetLoadOptions): Promise<void>;
21
+ export declare function loadUrls(urls: string[], options: AssetLoadOptions): Promise<void>;
22
+ export declare function loadUrl(url: string, options: AssetLoadOptions): Promise<void>;
20
23
  export declare function loadFiles(files: File[], { onModelLoaded, onTextureLoaded, onSoundLoaded, onUnhandledFile, onFilesLoaded, onLoadError }: AssetLoadOptions): Promise<void>;
21
24
  export declare function DragDropLoader({ children, ...divProps }: DragDropLoaderProps): import("react/jsx-runtime").JSX.Element;
22
25
  export declare function FilePicker({ accept, children, multiple, ...divProps }: FilePickerProps): import("react/jsx-runtime").JSX.Element;
@@ -25,56 +25,196 @@ const DEFAULT_ACCEPT = ".glb,.gltf,.fbx,.png,.jpg,.jpeg,.webp,.gif,.bmp,.svg,.mp
25
25
  function getFiles(fileList) {
26
26
  return fileList ? Array.from(fileList) : [];
27
27
  }
28
- export function loadFiles(files_1, _a) {
29
- return __awaiter(this, arguments, void 0, function* (files, { onModelLoaded, onTextureLoaded, onSoundLoaded, onUnhandledFile, onFilesLoaded, onLoadError }) {
30
- yield Promise.all(files.map((file) => __awaiter(this, void 0, void 0, function* () {
31
- const shouldParseModel = canParseModelFile(file);
32
- const shouldParseTexture = canParseTextureFile(file);
33
- const shouldParseSound = canParseSoundFile(file);
34
- if (shouldParseModel) {
35
- const result = yield parseModelFromFile(file);
36
- if (result.success && result.model) {
37
- yield (onModelLoaded === null || onModelLoaded === void 0 ? void 0 : onModelLoaded(result.model, file.name, file));
38
- return;
28
+ function getStringFromItem(item) {
29
+ return new Promise(resolve => item.getAsString(value => resolve(value !== null && value !== void 0 ? value : "")));
30
+ }
31
+ function parseUriList(value) {
32
+ return value
33
+ .split(/\r?\n/)
34
+ .map(line => line.trim())
35
+ .filter(line => line && !line.startsWith("#"));
36
+ }
37
+ function parseDroppedUrls(value) {
38
+ const urls = new Set();
39
+ const addUrl = (candidate) => {
40
+ try {
41
+ const url = new URL(candidate.trim());
42
+ if (url.protocol === "http:" || url.protocol === "https:") {
43
+ urls.add(url.href);
44
+ }
45
+ }
46
+ catch (_a) {
47
+ // Ignore non-URL dropped text.
48
+ }
49
+ };
50
+ parseUriList(value).forEach(addUrl);
51
+ if (typeof DOMParser !== "undefined" && value.includes("<")) {
52
+ const document = new DOMParser().parseFromString(value, "text/html");
53
+ document.querySelectorAll("[href], [src]").forEach(element => {
54
+ var _a;
55
+ const rawUrl = (_a = element.getAttribute("href")) !== null && _a !== void 0 ? _a : element.getAttribute("src");
56
+ if (rawUrl)
57
+ addUrl(rawUrl);
58
+ });
59
+ }
60
+ return Array.from(urls);
61
+ }
62
+ function getDroppedUrls(dataTransfer) {
63
+ return __awaiter(this, void 0, void 0, function* () {
64
+ var _a;
65
+ if (!dataTransfer)
66
+ return [];
67
+ const urls = new Set();
68
+ const itemValues = yield Promise.all(Array.from((_a = dataTransfer.items) !== null && _a !== void 0 ? _a : [])
69
+ .filter(item => item.kind === "string")
70
+ .map(getStringFromItem));
71
+ itemValues.forEach(value => parseDroppedUrls(value).forEach(url => urls.add(url)));
72
+ const uriList = dataTransfer.getData("text/uri-list");
73
+ const plainText = dataTransfer.getData("text/plain");
74
+ const html = dataTransfer.getData("text/html");
75
+ [uriList, plainText, html].forEach(value => {
76
+ if (value)
77
+ parseDroppedUrls(value).forEach(url => urls.add(url));
78
+ });
79
+ return Array.from(urls);
80
+ });
81
+ }
82
+ function getExtensionFromMimeType(mimeType) {
83
+ var _a;
84
+ const type = mimeType.split(";", 1)[0].toLowerCase();
85
+ const extensions = {
86
+ "image/jpeg": ".jpg",
87
+ "image/png": ".png",
88
+ "image/webp": ".webp",
89
+ "image/gif": ".gif",
90
+ "image/bmp": ".bmp",
91
+ "image/svg+xml": ".svg",
92
+ "model/gltf-binary": ".glb",
93
+ "model/gltf+json": ".gltf",
94
+ "audio/mpeg": ".mp3",
95
+ "audio/wav": ".wav",
96
+ "audio/ogg": ".ogg",
97
+ "audio/mp4": ".m4a",
98
+ };
99
+ return (_a = extensions[type]) !== null && _a !== void 0 ? _a : "";
100
+ }
101
+ function getFilenameFromUrl(url, mimeType) {
102
+ const parsedUrl = new URL(url);
103
+ const rawName = parsedUrl.pathname.split("/").filter(Boolean).pop() || "asset";
104
+ const decodedName = decodeURIComponent(rawName).replace(/[^\w.\-]+/g, "-");
105
+ const hasExtension = /\.[a-z0-9]+$/i.test(decodedName);
106
+ return hasExtension ? decodedName : `${decodedName}${getExtensionFromMimeType(mimeType)}`;
107
+ }
108
+ function fetchUrlAsFile(url) {
109
+ return __awaiter(this, void 0, void 0, function* () {
110
+ const response = yield fetch(url);
111
+ if (!response.ok) {
112
+ throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
113
+ }
114
+ const blob = yield response.blob();
115
+ return new File([blob], getFilenameFromUrl(url, blob.type), { type: blob.type });
116
+ });
117
+ }
118
+ export function loadDroppedAssets(dataTransfer, options) {
119
+ return __awaiter(this, void 0, void 0, function* () {
120
+ const localFiles = getFiles(dataTransfer === null || dataTransfer === void 0 ? void 0 : dataTransfer.files);
121
+ const remoteUrls = yield getDroppedUrls(dataTransfer);
122
+ const localNames = new Set(localFiles.map(file => file.name));
123
+ yield Promise.all(remoteUrls.map((url) => __awaiter(this, void 0, void 0, function* () {
124
+ var _a;
125
+ try {
126
+ const file = yield fetchUrlAsFile(url);
127
+ if (!localNames.has(file.name)) {
128
+ yield loadFile(file, options, url);
39
129
  }
40
- if (onLoadError) {
41
- yield onLoadError(result.error, file.name, file);
42
- return;
130
+ }
131
+ catch (error) {
132
+ const fallbackName = getFilenameFromUrl(url, "");
133
+ const fallbackFile = new File([], fallbackName);
134
+ yield ((_a = options.onLoadError) === null || _a === void 0 ? void 0 : _a.call(options, error, url, fallbackFile));
135
+ if (!options.onLoadError) {
136
+ console.error("URL load error:", error);
43
137
  }
44
- console.error("Model parse error:", result.error);
138
+ }
139
+ })));
140
+ yield loadFiles(localFiles, options);
141
+ });
142
+ }
143
+ export function loadUrls(urls, options) {
144
+ return __awaiter(this, void 0, void 0, function* () {
145
+ yield Promise.all(urls.map(url => loadUrl(url, options)));
146
+ });
147
+ }
148
+ export function loadUrl(url, options) {
149
+ return __awaiter(this, void 0, void 0, function* () {
150
+ var _a;
151
+ try {
152
+ const file = yield fetchUrlAsFile(url);
153
+ yield loadFile(file, options, url);
154
+ }
155
+ catch (error) {
156
+ const fallbackName = getFilenameFromUrl(url, "");
157
+ const fallbackFile = new File([], fallbackName);
158
+ yield ((_a = options.onLoadError) === null || _a === void 0 ? void 0 : _a.call(options, error, url, fallbackFile));
159
+ if (!options.onLoadError) {
160
+ console.error("URL load error:", error);
161
+ }
162
+ }
163
+ });
164
+ }
165
+ export function loadFiles(files_1, _a) {
166
+ return __awaiter(this, arguments, void 0, function* (files, { onModelLoaded, onTextureLoaded, onSoundLoaded, onUnhandledFile, onFilesLoaded, onLoadError }) {
167
+ yield Promise.all(files.map(file => loadFile(file, { onModelLoaded, onTextureLoaded, onSoundLoaded, onUnhandledFile, onLoadError })));
168
+ yield (onFilesLoaded === null || onFilesLoaded === void 0 ? void 0 : onFilesLoaded(files));
169
+ });
170
+ }
171
+ function loadFile(file_1, _a) {
172
+ return __awaiter(this, arguments, void 0, function* (file, { onModelLoaded, onTextureLoaded, onSoundLoaded, onUnhandledFile, onLoadError }, assetRef = file.name) {
173
+ const shouldParseModel = canParseModelFile(file);
174
+ const shouldParseTexture = canParseTextureFile(file);
175
+ const shouldParseSound = canParseSoundFile(file);
176
+ if (shouldParseModel) {
177
+ const result = yield parseModelFromFile(file);
178
+ if (result.success && result.model) {
179
+ yield (onModelLoaded === null || onModelLoaded === void 0 ? void 0 : onModelLoaded(result.model, assetRef, file));
45
180
  return;
46
181
  }
47
- if (shouldParseTexture) {
48
- const result = yield parseTextureFromFile(file);
49
- if (result.success && result.texture) {
50
- yield (onTextureLoaded === null || onTextureLoaded === void 0 ? void 0 : onTextureLoaded(result.texture, file.name, file));
51
- return;
52
- }
53
- if (onLoadError) {
54
- yield onLoadError(result.error, file.name, file);
55
- return;
56
- }
57
- console.error("Texture parse error:", result.error);
182
+ if (onLoadError) {
183
+ yield onLoadError(result.error, assetRef, file);
58
184
  return;
59
185
  }
60
- if (shouldParseSound) {
61
- const result = yield parseSoundFromFile(file);
62
- if (result.success && result.sound) {
63
- yield (onSoundLoaded === null || onSoundLoaded === void 0 ? void 0 : onSoundLoaded(result.sound, file.name, file));
64
- return;
65
- }
66
- if (onLoadError) {
67
- yield onLoadError(result.error, file.name, file);
68
- return;
69
- }
70
- console.error("Sound parse error:", result.error);
186
+ console.error("Model parse error:", result.error);
187
+ return;
188
+ }
189
+ if (shouldParseTexture) {
190
+ const result = yield parseTextureFromFile(file);
191
+ if (result.success && result.texture) {
192
+ yield (onTextureLoaded === null || onTextureLoaded === void 0 ? void 0 : onTextureLoaded(result.texture, assetRef, file));
71
193
  return;
72
194
  }
73
- if (onUnhandledFile) {
74
- yield onUnhandledFile(file);
195
+ if (onLoadError) {
196
+ yield onLoadError(result.error, assetRef, file);
197
+ return;
75
198
  }
76
- })));
77
- yield (onFilesLoaded === null || onFilesLoaded === void 0 ? void 0 : onFilesLoaded(files));
199
+ console.error("Texture parse error:", result.error);
200
+ return;
201
+ }
202
+ if (shouldParseSound) {
203
+ const result = yield parseSoundFromFile(file);
204
+ if (result.success && result.sound) {
205
+ yield (onSoundLoaded === null || onSoundLoaded === void 0 ? void 0 : onSoundLoaded(result.sound, assetRef, file));
206
+ return;
207
+ }
208
+ if (onLoadError) {
209
+ yield onLoadError(result.error, assetRef, file);
210
+ return;
211
+ }
212
+ console.error("Sound parse error:", result.error);
213
+ return;
214
+ }
215
+ if (onUnhandledFile) {
216
+ yield onUnhandledFile(file);
217
+ }
78
218
  });
79
219
  }
80
220
  function reportFileLoadError(error) {
@@ -94,16 +234,15 @@ export function DragDropLoader(_a) {
94
234
  var { children } = _a, divProps = __rest(_a, ["children"]);
95
235
  const loadOptions = createLoadHandlers(divProps);
96
236
  function handleDrop(event) {
97
- var _a;
98
237
  event.preventDefault();
99
238
  event.stopPropagation();
100
- void loadFiles(getFiles((_a = event.dataTransfer) === null || _a === void 0 ? void 0 : _a.files), loadOptions).catch(reportFileLoadError);
239
+ void loadDroppedAssets(event.dataTransfer, loadOptions).catch(reportFileLoadError);
101
240
  }
102
241
  function handleDragOver(event) {
103
242
  event.preventDefault();
104
243
  event.stopPropagation();
105
244
  }
106
- return (_jsx("div", Object.assign({}, divProps, { onDrop: handleDrop, onDragOver: handleDragOver, children: children })));
245
+ return (_jsx("div", Object.assign({ role: "application" }, divProps, { onDrop: handleDrop, onDragOver: handleDragOver, children: children })));
107
246
  }
108
247
  export function FilePicker(_a) {
109
248
  var { accept = DEFAULT_ACCEPT, children, multiple = true } = _a, divProps = __rest(_a, ["accept", "children", "multiple"]);
@@ -1,4 +1,4 @@
1
- export { DragDropLoader, FilePicker, loadFiles } from "./DragDropLoader";
1
+ export { DragDropLoader, FilePicker, loadDroppedAssets, loadFiles, loadUrl, loadUrls } from "./DragDropLoader";
2
2
  export type { AssetLoadOptions, DragDropLoaderProps, FilePickerProps } from "./DragDropLoader";
3
3
  export { loadModel, loadSound, loadTexture, parseModelFromFile, parseSoundFromFile, parseTextureFromFile } from "./modelLoader";
4
4
  export type { LoadedModel, LoadedSound, LoadedTexture, LoadedModels, LoadedSounds, LoadedTextures, ModelLoadResult, ProgressCallback, SoundLoadResult, TextureLoadResult } from "./modelLoader";
@@ -1,2 +1,2 @@
1
- export { DragDropLoader, FilePicker, loadFiles } from "./DragDropLoader";
1
+ export { DragDropLoader, FilePicker, loadDroppedAssets, loadFiles, loadUrl, loadUrls } from "./DragDropLoader";
2
2
  export { loadModel, loadSound, loadTexture, parseModelFromFile, parseSoundFromFile, parseTextureFromFile } from "./modelLoader";
@@ -50,6 +50,8 @@ export function canParseModelFile(file) {
50
50
  }
51
51
  export function canParseTextureFile(file) {
52
52
  const filename = typeof file === "string" ? file : file.name;
53
+ if (filename.startsWith("data:image/"))
54
+ return true;
53
55
  const normalizedName = normalizeModelPath(filename);
54
56
  return TEXTURE_FILE_EXTENSIONS.some(extension => normalizedName.endsWith(extension));
55
57
  }
@@ -11,9 +11,8 @@ var __rest = (this && this.__rest) || function (s, e) {
11
11
  };
12
12
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
13
13
  import { useState } from 'react';
14
- import { hasComponent } from "./types";
15
14
  import EditorTree from './EditorTree';
16
- import { getAllComponentDefs } from './components/ComponentRegistry';
15
+ import { canAddComponentToNode, getAllComponentDefs, getNextComponentKey } from './components/ComponentRegistry';
17
16
  import { createComponentData } from './prefab';
18
17
  import { useEditorRef } from './PrefabEditor';
19
18
  import { base, colors, inspector, componentCard } from './styles';
@@ -34,32 +33,32 @@ function EditorUI({ selectedId, setSelectedId, getPrefab, onReplacePrefab, onImp
34
33
  editor.remove(selectedId);
35
34
  setSelectedId(null);
36
35
  };
37
- return _jsxs(_Fragment, { children: [_jsxs("div", { style: inspector.panel, children: [_jsxs("div", { style: base.header, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: "Inspector" }), _jsx("span", { children: collapsed ? '◀' : '▼' })] }), !collapsed && selectedNode && (_jsx(NodeInspector, { node: selectedNode, updateNode: updateNodeHandler, deleteNode: deleteNodeHandler, basePath: basePath }))] }), _jsx("div", { style: { position: 'absolute', top: 8, left: 8, zIndex: 20 }, children: _jsx(EditorTree, { selectedId: selectedId, setSelectedId: setSelectedId, getPrefab: getPrefab, onReplacePrefab: onReplacePrefab, onImportPrefab: onImportPrefab, onUndo: onUndo, onRedo: onRedo, canUndo: canUndo, canRedo: canRedo }) })] });
36
+ return _jsxs(_Fragment, { children: [_jsxs("div", { style: inspector.panel, children: [_jsxs("button", { type: "button", style: base.header, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: "Inspector" }), _jsx("span", { children: collapsed ? '◀' : '▼' })] }), !collapsed && selectedNode && (_jsx(NodeInspector, { node: selectedNode, updateNode: updateNodeHandler, deleteNode: deleteNodeHandler, basePath: basePath }))] }), _jsx("div", { style: { position: 'absolute', top: 8, left: 8, zIndex: 20 }, children: _jsx(EditorTree, { selectedId: selectedId, setSelectedId: setSelectedId, getPrefab: getPrefab, onReplacePrefab: onReplacePrefab, onImportPrefab: onImportPrefab, onUndo: onUndo, onRedo: onRedo, canUndo: canUndo, canRedo: canRedo }) })] });
38
37
  }
39
38
  function NodeInspector({ node, updateNode, deleteNode, basePath }) {
40
39
  var _a;
41
40
  const ALL_COMPONENTS = getAllComponentDefs();
42
41
  const allKeys = Object.keys(ALL_COMPONENTS);
43
- const available = allKeys.filter(k => !hasComponent(node, k));
42
+ const available = allKeys.filter(k => canAddComponentToNode(node, ALL_COMPONENTS[k], ALL_COMPONENTS));
44
43
  const [preferredAddType, setAddType] = useState(available[0] || "");
45
44
  const addType = available.includes(preferredAddType) ? preferredAddType : (available[0] || "");
46
- return _jsxs("div", { style: inspector.content, children: [_jsxs("div", { style: base.section, children: [_jsxs("div", { style: { display: "flex", marginBottom: 4, alignItems: 'center', gap: 4 }, children: [_jsx("div", { style: { fontSize: 10, color: colors.textDim, wordBreak: 'break-all', background: colors.bgLight, padding: '2px 4px', flex: 1, fontFamily: 'monospace', minHeight: 18, boxSizing: 'border-box' }, children: node.id }), _jsx("button", { style: Object.assign(Object.assign(Object.assign({}, base.btn), base.btnDanger), { minWidth: 22, padding: '2px 4px' }), title: "Delete Node", onClick: deleteNode, children: "\u2715" })] }), _jsx("input", { style: base.input, value: (_a = node.name) !== null && _a !== void 0 ? _a : "", placeholder: 'Node name', onChange: e => updateNode(n => (Object.assign(Object.assign({}, n), { name: e.target.value }))) })] }), _jsxs("div", { style: base.section, children: [_jsx("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }, children: _jsx("div", { style: base.label, children: "Components" }) }), node.components && Object.entries(node.components).map(([key, comp]) => {
45
+ return _jsxs("div", { style: inspector.content, children: [_jsxs("div", { style: base.section, children: [_jsxs("div", { style: { display: "flex", marginBottom: 4, alignItems: 'center', gap: 4 }, children: [_jsx("div", { style: { fontSize: 10, color: colors.textDim, wordBreak: 'break-all', background: colors.bgLight, padding: '2px 4px', flex: 1, fontFamily: 'monospace', minHeight: 18, boxSizing: 'border-box' }, children: node.id }), _jsx("button", { style: Object.assign(Object.assign(Object.assign({}, base.btn), base.btnDanger), { minWidth: 22, padding: '2px 4px' }), type: "button", title: "Delete Node", onClick: deleteNode, children: "\u2715" })] }), _jsx("input", { style: base.input, value: (_a = node.name) !== null && _a !== void 0 ? _a : "", placeholder: 'Node name', onChange: e => updateNode(n => (Object.assign(Object.assign({}, n), { name: e.target.value }))) })] }), _jsxs("div", { style: base.section, children: [_jsx("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }, children: _jsx("div", { style: base.label, children: "Components" }) }), node.components && Object.entries(node.components).map(([key, comp]) => {
47
46
  if (!comp)
48
47
  return null;
49
48
  const def = ALL_COMPONENTS[comp.type];
50
49
  if (!def)
51
50
  return _jsxs("div", { style: { color: colors.danger, fontSize: 11 }, children: ["Unknown: ", comp.type] }, key);
52
- return (_jsxs("div", { style: componentCard.container, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 3 }, children: [_jsx("div", { style: { fontSize: 11, fontWeight: 500 }, children: key }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 4px', minWidth: 20 }), title: "Remove Component", onClick: () => updateNode(n => {
51
+ return (_jsxs("div", { style: componentCard.container, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 3 }, children: [_jsx("div", { style: { fontSize: 11, fontWeight: 500 }, children: key }), _jsx("button", { type: "button", style: Object.assign(Object.assign({}, base.btn), { padding: '2px 4px', minWidth: 20 }), title: "Remove Component", onClick: () => updateNode(n => {
53
52
  var _a;
54
53
  const _b = (_a = n.components) !== null && _a !== void 0 ? _a : {}, _c = key, _ = _b[_c], rest = __rest(_b, [typeof _c === "symbol" ? _c : _c + ""]);
55
54
  return Object.assign(Object.assign({}, n), { components: rest });
56
55
  }), children: "\u2715" })] }), def.Editor && (_jsx(def.Editor, { component: comp, node: node, onUpdate: (newProps) => updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [key]: Object.assign(Object.assign({}, comp), { properties: Object.assign(Object.assign({}, comp.properties), newProps) }) }) }))), basePath: basePath }))] }, key));
57
- })] }), available.length > 0 && (_jsx("div", { children: _jsxs("div", { style: base.row, children: [_jsx("select", { style: Object.assign(Object.assign({}, base.input), { flex: 1, background: colors.bgInput, border: `1px solid ${colors.border}`, minHeight: 22 }), value: addType, onChange: e => setAddType(e.target.value), children: available.map(k => _jsx("option", { value: k, children: k }, k)) }), _jsx("button", { style: base.btn, disabled: !addType, onClick: () => {
56
+ })] }), available.length > 0 && (_jsx("div", { children: _jsxs("div", { style: base.row, children: [_jsx("select", { style: Object.assign(Object.assign({}, base.input), { flex: 1, background: colors.bgInput, border: `1px solid ${colors.border}`, minHeight: 22 }), value: addType, onChange: e => setAddType(e.target.value), children: available.map(k => _jsx("option", { value: k, children: k }, k)) }), _jsx("button", { type: "button", style: base.btn, disabled: !addType, onClick: () => {
58
57
  if (!addType)
59
58
  return;
60
59
  const def = ALL_COMPONENTS[addType];
61
60
  if (def) {
62
- updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [addType.toLowerCase()]: createComponentData(def.name) }) })));
61
+ updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [getNextComponentKey(n, def.name)]: createComponentData(def.name) }) })));
63
62
  }
64
63
  }, title: "Add Component", children: "+" })] }) }))] });
65
64
  }
@@ -2,6 +2,9 @@ import GameCanvas from "../../shared/GameCanvas";
2
2
  import type { Prefab } from "./types";
3
3
  import { PrefabEditorMode, type Scene } from "./PrefabRoot";
4
4
  import type { ExportGLBOptions } from "./utils";
5
+ export declare function isAbsoluteAssetPath(path: string): boolean;
6
+ export declare function resolvePrefabAssetPath(basePath: string, file: string): string;
7
+ export declare function getPrefabAssetRef(assetRef: string, folder: "models" | "textures" | "sound"): string;
5
8
  export interface PrefabEditorRef extends Scene {
6
9
  save: () => Prefab;
7
10
  load: (prefab: Prefab, options?: {