react-three-game 0.0.55 → 0.0.56

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 (37) hide show
  1. package/dist/shared/ContactShadow.d.ts +8 -0
  2. package/dist/shared/ContactShadow.js +32 -0
  3. package/dist/tools/assetviewer/page.js +1 -1
  4. package/dist/tools/dragdrop/DragDropLoader.js +17 -40
  5. package/dist/tools/dragdrop/modelLoader.d.ts +5 -0
  6. package/dist/tools/dragdrop/modelLoader.js +39 -0
  7. package/dist/tools/prefabeditor/EditorTree.js +3 -16
  8. package/dist/tools/prefabeditor/EditorUI.js +5 -10
  9. package/dist/tools/prefabeditor/PrefabEditor.js +57 -1
  10. package/dist/tools/prefabeditor/PrefabRoot.d.ts +2 -0
  11. package/dist/tools/prefabeditor/PrefabRoot.js +17 -2
  12. package/dist/tools/prefabeditor/components/Input.js +27 -26
  13. package/dist/tools/prefabeditor/components/MaterialComponent.js +9 -2
  14. package/dist/tools/prefabeditor/components/ModelComponent.js +2 -2
  15. package/dist/tools/prefabeditor/components/SpotLightComponent.js +3 -0
  16. package/dist/tools/prefabeditor/components/TransformComponent.js +13 -11
  17. package/dist/tools/prefabeditor/styles.d.ts +12 -2
  18. package/dist/tools/prefabeditor/styles.js +63 -30
  19. package/dist/tools/prefabeditor/utils.d.ts +4 -0
  20. package/dist/tools/prefabeditor/utils.js +39 -1
  21. package/package.json +1 -1
  22. package/react-three-game-skill/react-three-game/rules/LIGHTING.md +6 -0
  23. package/src/shared/ContactShadow.tsx +74 -0
  24. package/src/tools/assetviewer/page.tsx +1 -1
  25. package/src/tools/dragdrop/DragDropLoader.tsx +7 -39
  26. package/src/tools/dragdrop/modelLoader.ts +36 -0
  27. package/src/tools/prefabeditor/EditorTree.tsx +4 -15
  28. package/src/tools/prefabeditor/EditorUI.tsx +5 -10
  29. package/src/tools/prefabeditor/PrefabEditor.tsx +60 -1
  30. package/src/tools/prefabeditor/PrefabRoot.tsx +21 -2
  31. package/src/tools/prefabeditor/components/Input.tsx +27 -26
  32. package/src/tools/prefabeditor/components/MaterialComponent.tsx +14 -5
  33. package/src/tools/prefabeditor/components/ModelComponent.tsx +2 -2
  34. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +4 -0
  35. package/src/tools/prefabeditor/components/TransformComponent.tsx +17 -11
  36. package/src/tools/prefabeditor/styles.ts +65 -30
  37. package/src/tools/prefabeditor/utils.ts +41 -1
@@ -22,6 +22,8 @@ const IDENTITY = new Matrix4();
22
22
  export interface PrefabRootRef {
23
23
  root: Group | null;
24
24
  rigidBodyRefs: Map<string, any>; // RigidBody refs only populated when using physics
25
+ injectModel: (filename: string, model: Object3D) => void;
26
+ injectTexture: (filename: string, file: File) => void;
25
27
  }
26
28
 
27
29
  export const PrefabRoot = forwardRef<PrefabRootRef, {
@@ -48,10 +50,27 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
48
50
  const [selectedObject, setSelectedObject] = useState<Object3D | null>(null);
49
51
  const rootRef = useRef<Group>(null);
50
52
 
53
+ const injectModel = useCallback((filename: string, model: Object3D) => {
54
+ setModels(m => ({ ...m, [filename]: model }));
55
+ }, []);
56
+
57
+ const injectTexture = useCallback((filename: string, file: File) => {
58
+ loading.current.add(filename);
59
+ const url = URL.createObjectURL(file);
60
+ const loader = new TextureLoader();
61
+ loader.load(url, tex => {
62
+ tex.colorSpace = SRGBColorSpace;
63
+ setTextures(t => ({ ...t, [filename]: tex }));
64
+ URL.revokeObjectURL(url);
65
+ }, undefined, () => URL.revokeObjectURL(url));
66
+ }, []);
67
+
51
68
  useImperativeHandle(ref, () => ({
52
69
  root: rootRef.current,
53
- rigidBodyRefs: rigidBodyRefs.current
54
- }), []);
70
+ rigidBodyRefs: rigidBodyRefs.current,
71
+ injectModel,
72
+ injectTexture
73
+ }), [injectModel, injectTexture]);
55
74
 
56
75
  const registerRef = useCallback((id: string, obj: Object3D | null) => {
57
76
  objectRefs.current[id] = obj;
@@ -1,4 +1,5 @@
1
1
  import React, { useEffect, useRef, useState } from 'react';
2
+ import { colors } from '../styles';
2
3
 
3
4
  // ============================================================================
4
5
  // Field Definition Types
@@ -61,29 +62,30 @@ export type FieldDefinition =
61
62
  | CustomFieldDefinition;
62
63
 
63
64
  // ============================================================================
64
- // Shared Styles
65
+ // Shared Styles (derived from shared color tokens)
65
66
  // ============================================================================
66
67
 
67
- // Shared styles
68
68
  const styles = {
69
69
  input: {
70
70
  width: '80px',
71
- backgroundColor: 'rgba(0, 0, 0, 0.4)',
72
- border: '1px solid rgba(34, 211, 238, 0.3)',
73
- padding: '2px 4px',
74
- fontSize: '10px',
75
- color: 'rgba(165, 243, 252, 1)',
71
+ backgroundColor: colors.bgInput,
72
+ border: `1px solid ${colors.border}`,
73
+ padding: '3px 6px',
74
+ fontSize: '11px',
75
+ color: colors.text,
76
76
  fontFamily: 'monospace',
77
77
  outline: 'none',
78
78
  textAlign: 'right',
79
+ borderRadius: 3,
79
80
  } as React.CSSProperties,
80
81
  label: {
81
82
  display: 'block',
82
- fontSize: '9px',
83
- color: 'rgba(34, 211, 238, 0.9)',
83
+ fontSize: '10px',
84
+ color: colors.textMuted,
84
85
  textTransform: 'uppercase',
85
86
  letterSpacing: '0.05em',
86
87
  marginBottom: 2,
88
+ fontWeight: 500,
87
89
  } as React.CSSProperties,
88
90
  };
89
91
 
@@ -317,9 +319,9 @@ export function Vector3Input({
317
319
  };
318
320
 
319
321
  const axes = [
320
- { key: "x", color: 'rgba(248, 113, 113, 1)', index: 0 },
321
- { key: "y", color: 'rgba(134, 239, 172, 1)', index: 1 },
322
- { key: "z", color: 'rgba(96, 165, 250, 1)', index: 2 }
322
+ { key: "x", color: '#e06c75', index: 0 },
323
+ { key: "y", color: '#98c379', index: 1 },
324
+ { key: "z", color: '#61afef', index: 2 }
323
325
  ] as const;
324
326
 
325
327
  return (
@@ -334,17 +336,17 @@ export function Vector3Input({
334
336
  display: 'flex',
335
337
  alignItems: 'center',
336
338
  gap: 4,
337
- backgroundColor: 'rgba(0, 0, 0, 0.3)',
338
- border: '1px solid rgba(34, 211, 238, 0.2)',
339
- borderRadius: 4,
339
+ backgroundColor: colors.bgInput,
340
+ border: `1px solid ${colors.border}`,
341
+ borderRadius: 3,
340
342
  padding: '4px 6px',
341
- minHeight: 32,
343
+ minHeight: 28,
342
344
  }}
343
345
  >
344
346
  <span
345
347
  style={{
346
- fontSize: '12px',
347
- fontWeight: 'bold',
348
+ fontSize: 11,
349
+ fontWeight: 600,
348
350
  color,
349
351
  width: 12,
350
352
  cursor: 'ew-resize',
@@ -361,8 +363,8 @@ export function Vector3Input({
361
363
  flex: 1,
362
364
  backgroundColor: 'transparent',
363
365
  border: 'none',
364
- fontSize: '12px',
365
- color: 'rgba(165, 243, 252, 1)',
366
+ fontSize: 11,
367
+ color: colors.text,
366
368
  fontFamily: 'monospace',
367
369
  outline: 'none',
368
370
  width: '100%',
@@ -411,11 +413,11 @@ export function ColorInput({
411
413
  style={{
412
414
  height: 32,
413
415
  width: 48,
414
- backgroundColor: 'transparent',
415
- border: '1px solid rgba(34, 211, 238, 0.3)',
416
- borderRadius: 4,
416
+ backgroundColor: colors.bgInput,
417
+ border: `1px solid ${colors.border}`,
418
+ borderRadius: 3,
417
419
  cursor: 'pointer',
418
- padding: 0,
420
+ padding: 2,
419
421
  flexShrink: 0,
420
422
  }}
421
423
  value={value}
@@ -474,8 +476,7 @@ export function BooleanInput({
474
476
  style={{
475
477
  height: 16,
476
478
  width: 16,
477
- backgroundColor: 'rgba(0, 0, 0, 0.4)',
478
- border: '1px solid rgba(34, 211, 238, 0.3)',
479
+ accentColor: colors.accent,
479
480
  cursor: 'pointer',
480
481
  }}
481
482
  checked={value}
@@ -2,6 +2,7 @@ import { SingleTextureViewer, TextureListViewer } from '../../assetviewer/page';
2
2
  import { useEffect, useState } from 'react';
3
3
  import { Component } from './ComponentRegistry';
4
4
  import { FieldRenderer, FieldDefinition, Input } from './Input';
5
+ import { colors } from '../styles';
5
6
  import { useMemo } from 'react';
6
7
  import {
7
8
  RepeatWrapping,
@@ -47,12 +48,20 @@ function TexturePicker({
47
48
  .catch(console.error);
48
49
  }, [basePath]);
49
50
 
51
+ // Only show 3D preview for server-hosted textures (starting with / or http)
52
+ const canPreview = value && (value.startsWith('/') || value.startsWith('http'));
53
+
50
54
  return (
51
- <div style={{ maxHeight: 128, overflowY: 'auto', position: 'relative', display: 'flex', alignItems: 'center' }}>
52
- <SingleTextureViewer file={value || undefined} basePath={basePath} />
55
+ <div style={{ maxHeight: 128, overflow: 'visible', position: 'relative', display: 'flex', alignItems: 'center' }}>
56
+ {canPreview
57
+ ? <SingleTextureViewer file={value} basePath={basePath} />
58
+ : value
59
+ ? <span style={{ fontSize: 10, opacity: 0.6, maxWidth: 100, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{value}</span>
60
+ : null
61
+ }
53
62
  <button
54
63
  onClick={() => setShowPicker(!showPicker)}
55
- style={{ padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4 }}
64
+ style={{ padding: '4px 8px', backgroundColor: colors.bgLight, color: 'inherit', fontSize: 10, cursor: 'pointer', border: `1px solid ${colors.border}`, borderRadius: 3, marginTop: 4 }}
56
65
  >
57
66
  {showPicker ? 'Cancel' : 'Change'}
58
67
  </button>
@@ -60,12 +69,12 @@ function TexturePicker({
60
69
  onClick={() => {
61
70
  onChange(undefined as any);
62
71
  }}
63
- 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 }}
72
+ style={{ padding: '4px 8px', backgroundColor: colors.bgLight, color: 'inherit', fontSize: 10, cursor: 'pointer', border: `1px solid ${colors.border}`, borderRadius: 3, marginTop: 4, marginLeft: 4 }}
64
73
  >
65
74
  Clear
66
75
  </button>
67
76
  {showPicker && (
68
- <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 }}>
77
+ <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)' }}>
69
78
  <TextureListViewer
70
79
  files={textureFiles}
71
80
  selected={value || undefined}
@@ -32,7 +32,7 @@ function ModelPicker({
32
32
  };
33
33
 
34
34
  return (
35
- <div style={{ maxHeight: 128, overflowY: 'auto', position: 'relative', display: 'flex', alignItems: 'center' }}>
35
+ <div style={{ maxHeight: 128, overflow: 'visible', position: 'relative', display: 'flex', alignItems: 'center' }}>
36
36
  <SingleModelViewer file={value ? `/${value}` : undefined} basePath={basePath} />
37
37
  <button
38
38
  onClick={() => setShowPicker(!showPicker)}
@@ -49,7 +49,7 @@ function ModelPicker({
49
49
  Clear
50
50
  </button>
51
51
  {showPicker && (
52
- <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 }}>
52
+ <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 }}>
53
53
  <ModelListViewer
54
54
  key={nodeId}
55
55
  files={modelFiles}
@@ -1,6 +1,8 @@
1
1
  import { Component } from "./ComponentRegistry";
2
2
  import { useRef, useEffect } from "react";
3
3
  import { FieldRenderer, FieldDefinition } from "./Input";
4
+ import { useHelper } from "@react-three/drei";
5
+ import { SpotLightHelper } from "three";
4
6
 
5
7
  const spotLightFields: FieldDefinition[] = [
6
8
  { name: 'color', type: 'color', label: 'Color' },
@@ -32,6 +34,8 @@ function SpotLightView({ properties, editMode }: { properties: any; editMode?: b
32
34
  const spotLightRef = useRef<any>(null);
33
35
  const targetRef = useRef<any>(null);
34
36
 
37
+ useHelper(editMode ? spotLightRef : null, SpotLightHelper, color);
38
+
35
39
  useEffect(() => {
36
40
  if (spotLightRef.current && targetRef.current) {
37
41
  spotLightRef.current.target = targetRef.current;
@@ -1,15 +1,17 @@
1
1
  import { Component } from "./ComponentRegistry";
2
2
  import { FieldRenderer, FieldDefinition, Label } from "./Input";
3
3
  import { useEditorContext } from "../EditorContext";
4
+ import { colors } from "../styles";
4
5
 
5
6
  const buttonStyle = {
6
- padding: '2px 6px',
7
- background: 'transparent',
8
- color: 'rgba(255,255,255,0.9)',
9
- border: '1px solid rgba(255,255,255,0.14)',
10
- borderRadius: 4,
7
+ padding: '4px 8px',
8
+ background: colors.bgSurface,
9
+ color: colors.text,
10
+ border: `1px solid ${colors.border}`,
11
+ borderRadius: 3,
11
12
  cursor: 'pointer',
12
13
  font: 'inherit',
14
+ fontSize: 11,
13
15
  flex: 1,
14
16
  };
15
17
 
@@ -36,13 +38,15 @@ function TransformModeSelector({
36
38
  onClick={() => setTransformMode(mode as any)}
37
39
  style={{
38
40
  ...buttonStyle,
39
- background: isActive ? 'rgba(255,255,255,0.10)' : 'transparent',
41
+ background: isActive ? colors.accentBg : colors.bgSurface,
42
+ borderColor: isActive ? colors.accentBorder : colors.border,
43
+ color: isActive ? colors.accent : colors.text,
40
44
  }}
41
45
  onPointerEnter={(e) => {
42
- if (!isActive) e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
46
+ if (!isActive) e.currentTarget.style.background = colors.bgHover;
43
47
  }}
44
48
  onPointerLeave={(e) => {
45
- if (!isActive) e.currentTarget.style.background = 'transparent';
49
+ if (!isActive) e.currentTarget.style.background = colors.bgSurface;
46
50
  }}
47
51
  >
48
52
  {mode}
@@ -55,14 +59,16 @@ function TransformModeSelector({
55
59
  onClick={() => setSnapResolution(snapResolution > 0 ? 0 : 0.1)}
56
60
  style={{
57
61
  ...buttonStyle,
58
- background: snapResolution > 0 ? 'rgba(255,255,255,0.10)' : 'transparent',
62
+ background: snapResolution > 0 ? colors.accentBg : colors.bgSurface,
63
+ borderColor: snapResolution > 0 ? colors.accentBorder : colors.border,
64
+ color: snapResolution > 0 ? colors.accent : colors.text,
59
65
  width: '100%',
60
66
  }}
61
67
  onPointerEnter={(e) => {
62
- if (snapResolution === 0) e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
68
+ if (snapResolution === 0) e.currentTarget.style.background = colors.bgHover;
63
69
  }}
64
70
  onPointerLeave={(e) => {
65
- if (snapResolution === 0) e.currentTarget.style.background = 'transparent';
71
+ if (snapResolution === 0) e.currentTarget.style.background = colors.bgSurface;
66
72
  }}
67
73
  >
68
74
  Snap: {snapResolution > 0 ? `ON (${snapResolution})` : 'OFF'}
@@ -1,21 +1,27 @@
1
1
  // Shared editor styles - single source of truth for all prefab editor UI
2
2
 
3
3
  export const colors = {
4
- bg: 'rgba(0,0,0,0.6)',
5
- bgLight: 'rgba(255,255,255,0.06)',
6
- bgHover: 'rgba(255,255,255,0.1)',
7
- border: 'rgba(255,255,255,0.15)',
8
- borderLight: 'rgba(255,255,255,0.1)',
9
- borderFaint: 'rgba(255,255,255,0.05)',
10
- text: '#fff',
11
- textMuted: 'rgba(255,255,255,0.7)',
12
- danger: '#ffaaaa',
13
- dangerBg: 'rgba(255,80,80,0.2)',
14
- dangerBorder: 'rgba(255,80,80,0.4)',
4
+ bg: '#1e1e1e',
5
+ bgSurface: '#252526',
6
+ bgLight: '#2d2d2d',
7
+ bgHover: '#2a2d2e',
8
+ bgInput: '#1a1a1a',
9
+ border: '#3c3c3c',
10
+ borderLight: '#333333',
11
+ borderFaint: '#2a2a2a',
12
+ text: '#cccccc',
13
+ textMuted: '#999999',
14
+ textDim: '#666666',
15
+ accent: '#4c9eff',
16
+ accentBg: 'rgba(76, 158, 255, 0.12)',
17
+ accentBorder: 'rgba(76, 158, 255, 0.4)',
18
+ danger: '#f44747',
19
+ dangerBg: 'rgba(244, 71, 71, 0.12)',
20
+ dangerBorder: 'rgba(244, 71, 71, 0.35)',
15
21
  };
16
22
 
17
23
  export const fonts = {
18
- family: 'system-ui, sans-serif',
24
+ family: 'system-ui, -apple-system, sans-serif',
19
25
  size: 11,
20
26
  sizeSm: 10,
21
27
  };
@@ -27,13 +33,13 @@ export const base = {
27
33
  color: colors.text,
28
34
  border: `1px solid ${colors.border}`,
29
35
  borderRadius: 4,
30
- backdropFilter: 'blur(8px)',
31
36
  fontFamily: fonts.family,
32
37
  fontSize: fonts.size,
38
+ boxShadow: '0 2px 8px rgba(0,0,0,0.4)',
33
39
  } as React.CSSProperties,
34
40
 
35
41
  header: {
36
- padding: '6px 8px',
42
+ padding: '7px 10px',
37
43
  display: 'flex',
38
44
  alignItems: 'center',
39
45
  justifyContent: 'space-between',
@@ -41,24 +47,25 @@ export const base = {
41
47
  background: colors.bgLight,
42
48
  borderBottom: `1px solid ${colors.borderLight}`,
43
49
  fontSize: fonts.size,
44
- fontWeight: 500,
50
+ fontWeight: 600,
45
51
  textTransform: 'uppercase',
46
- letterSpacing: 0.5,
52
+ letterSpacing: 0.8,
53
+ color: colors.text,
47
54
  } as React.CSSProperties,
48
55
 
49
56
  input: {
50
57
  width: '100%',
51
- background: colors.bgHover,
58
+ background: colors.bgInput,
52
59
  border: `1px solid ${colors.border}`,
53
60
  borderRadius: 3,
54
- padding: '4px 6px',
61
+ padding: '5px 8px',
55
62
  color: colors.text,
56
63
  fontSize: fonts.size,
57
64
  outline: 'none',
58
65
  } as React.CSSProperties,
59
66
 
60
67
  btn: {
61
- background: colors.bgHover,
68
+ background: colors.bgLight,
62
69
  border: `1px solid ${colors.border}`,
63
70
  borderRadius: 3,
64
71
  padding: '4px 8px',
@@ -76,10 +83,11 @@ export const base = {
76
83
 
77
84
  label: {
78
85
  fontSize: fonts.sizeSm,
79
- opacity: 0.7,
86
+ color: colors.textMuted,
80
87
  marginBottom: 4,
81
88
  textTransform: 'uppercase',
82
89
  letterSpacing: 0.5,
90
+ fontWeight: 500,
83
91
  } as React.CSSProperties,
84
92
 
85
93
  row: {
@@ -125,18 +133,22 @@ export const tree = {
125
133
  overflowY: 'auto' as const,
126
134
  padding: 4,
127
135
  scrollbarWidth: 'thin' as const,
128
- scrollbarColor: 'rgba(255,255,255,0.06) transparent',
136
+ scrollbarColor: `${colors.bgLight} transparent`,
129
137
  } as React.CSSProperties,
130
138
  row: {
131
139
  display: 'flex',
132
140
  alignItems: 'center',
133
141
  padding: '3px 6px',
134
- borderBottom: `1px solid ${colors.borderFaint}`,
142
+ borderBottomWidth: 1,
143
+ borderBottomStyle: 'solid',
144
+ borderBottomColor: colors.borderFaint,
135
145
  cursor: 'pointer',
136
146
  whiteSpace: 'nowrap' as const,
147
+ borderRadius: 2,
137
148
  } as React.CSSProperties,
138
149
  selected: {
139
- background: 'rgba(255,255,255,0.12)',
150
+ background: colors.accentBg,
151
+ borderBottomColor: colors.accentBorder,
140
152
  },
141
153
  };
142
154
 
@@ -144,18 +156,17 @@ export const menu = {
144
156
  container: {
145
157
  position: 'fixed' as const,
146
158
  zIndex: 50,
147
- minWidth: 120,
148
- background: 'rgba(0,0,0,0.85)',
159
+ minWidth: 140,
160
+ background: colors.bgSurface,
149
161
  border: `1px solid ${colors.border}`,
150
162
  borderRadius: 4,
151
163
  overflow: 'hidden',
152
- boxShadow: '0 8px 24px rgba(0,0,0,0.5)',
153
- backdropFilter: 'blur(8px)',
164
+ boxShadow: '0 4px 16px rgba(0,0,0,0.6)',
154
165
  },
155
166
  item: {
156
167
  width: '100%',
157
168
  textAlign: 'left' as const,
158
- padding: '6px 8px',
169
+ padding: '7px 12px',
159
170
  background: 'transparent',
160
171
  border: 'none',
161
172
  color: colors.text,
@@ -183,14 +194,38 @@ export const toolbar = {
183
194
  color: colors.text,
184
195
  fontFamily: fonts.family,
185
196
  fontSize: fonts.size,
186
- backdropFilter: 'blur(8px)',
197
+ boxShadow: '0 2px 8px rgba(0,0,0,0.4)',
187
198
  },
188
199
  divider: {
189
200
  width: 1,
190
- background: 'rgba(255,255,255,0.2)',
201
+ background: colors.borderLight,
191
202
  },
192
203
  disabled: {
193
204
  opacity: 0.4,
194
205
  cursor: 'not-allowed',
195
206
  },
196
207
  };
208
+
209
+ // Shared scrollbar CSS (inject via <style> tag since CSS can't be bundled)
210
+ export const scrollbarCSS = `
211
+ .prefab-scroll::-webkit-scrollbar,
212
+ .tree-scroll::-webkit-scrollbar { width: 6px; height: 6px; }
213
+ .prefab-scroll::-webkit-scrollbar-track,
214
+ .tree-scroll::-webkit-scrollbar-track { background: transparent; }
215
+ .prefab-scroll::-webkit-scrollbar-thumb,
216
+ .tree-scroll::-webkit-scrollbar-thumb { background: ${colors.border}; border-radius: 3px; }
217
+ .prefab-scroll::-webkit-scrollbar-thumb:hover,
218
+ .tree-scroll::-webkit-scrollbar-thumb:hover { background: #555; }
219
+ .prefab-scroll { scrollbar-width: thin; scrollbar-color: ${colors.border} transparent; }
220
+ `;
221
+
222
+ // Reusable component card style for inspector sections
223
+ export const componentCard = {
224
+ container: {
225
+ marginBottom: 8,
226
+ backgroundColor: colors.bgSurface,
227
+ padding: 8,
228
+ borderRadius: 4,
229
+ border: `1px solid ${colors.border}`,
230
+ } as React.CSSProperties,
231
+ };
@@ -171,7 +171,7 @@ export function cloneNode(node: GameObject): GameObject {
171
171
  return {
172
172
  ...node,
173
173
  id: crypto.randomUUID(),
174
- name: `${node.name ?? "Node"} Copy`,
174
+ name: `${node.name ?? node.id} Copy`,
175
175
  children: node.children?.map(cloneNode)
176
176
  };
177
177
  }
@@ -219,3 +219,43 @@ export function updateNodeById(
219
219
  children: newChildren
220
220
  };
221
221
  }
222
+
223
+ /** Create a GameObject node for a 3D model file */
224
+ export function createModelNode(filename: string, name?: string): GameObject {
225
+ return {
226
+ id: crypto.randomUUID(),
227
+ name: name ?? filename.replace(/^.*[\/]/, '').replace(/\.[^.]+$/, ''),
228
+ components: {
229
+ transform: {
230
+ type: 'Transform',
231
+ properties: { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }
232
+ },
233
+ model: {
234
+ type: 'Model',
235
+ properties: { filename, instanced: false }
236
+ }
237
+ }
238
+ };
239
+ }
240
+
241
+ /** Create a GameObject node for an image as a textured plane */
242
+ export function createImageNode(texturePath: string, name?: string): GameObject {
243
+ return {
244
+ id: crypto.randomUUID(),
245
+ name: name ?? texturePath.replace(/^.*[\/]/, '').replace(/\.[^.]+$/, ''),
246
+ components: {
247
+ transform: {
248
+ type: 'Transform',
249
+ properties: { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }
250
+ },
251
+ geometry: {
252
+ type: 'Geometry',
253
+ properties: { geometryType: 'plane', args: [1, 1] }
254
+ },
255
+ material: {
256
+ type: 'Material',
257
+ properties: { color: '#ffffff', texture: texturePath }
258
+ }
259
+ }
260
+ };
261
+ }