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.
- package/dist/shared/ContactShadow.d.ts +8 -0
- package/dist/shared/ContactShadow.js +32 -0
- package/dist/tools/assetviewer/page.js +1 -1
- package/dist/tools/dragdrop/DragDropLoader.js +17 -40
- package/dist/tools/dragdrop/modelLoader.d.ts +5 -0
- package/dist/tools/dragdrop/modelLoader.js +39 -0
- package/dist/tools/prefabeditor/EditorTree.js +3 -16
- package/dist/tools/prefabeditor/EditorUI.js +5 -10
- package/dist/tools/prefabeditor/PrefabEditor.js +57 -1
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +2 -0
- package/dist/tools/prefabeditor/PrefabRoot.js +17 -2
- package/dist/tools/prefabeditor/components/Input.js +27 -26
- package/dist/tools/prefabeditor/components/MaterialComponent.js +9 -2
- package/dist/tools/prefabeditor/components/ModelComponent.js +2 -2
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +3 -0
- package/dist/tools/prefabeditor/components/TransformComponent.js +13 -11
- package/dist/tools/prefabeditor/styles.d.ts +12 -2
- package/dist/tools/prefabeditor/styles.js +63 -30
- package/dist/tools/prefabeditor/utils.d.ts +4 -0
- package/dist/tools/prefabeditor/utils.js +39 -1
- package/package.json +1 -1
- package/react-three-game-skill/react-three-game/rules/LIGHTING.md +6 -0
- package/src/shared/ContactShadow.tsx +74 -0
- package/src/tools/assetviewer/page.tsx +1 -1
- package/src/tools/dragdrop/DragDropLoader.tsx +7 -39
- package/src/tools/dragdrop/modelLoader.ts +36 -0
- package/src/tools/prefabeditor/EditorTree.tsx +4 -15
- package/src/tools/prefabeditor/EditorUI.tsx +5 -10
- package/src/tools/prefabeditor/PrefabEditor.tsx +60 -1
- package/src/tools/prefabeditor/PrefabRoot.tsx +21 -2
- package/src/tools/prefabeditor/components/Input.tsx +27 -26
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +14 -5
- package/src/tools/prefabeditor/components/ModelComponent.tsx +2 -2
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +4 -0
- package/src/tools/prefabeditor/components/TransformComponent.tsx +17 -11
- package/src/tools/prefabeditor/styles.ts +65 -30
- 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:
|
|
72
|
-
border:
|
|
73
|
-
padding: '
|
|
74
|
-
fontSize: '
|
|
75
|
-
color:
|
|
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: '
|
|
83
|
-
color:
|
|
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: '
|
|
321
|
-
{ key: "y", color: '
|
|
322
|
-
{ key: "z", color: '
|
|
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:
|
|
338
|
-
border:
|
|
339
|
-
borderRadius:
|
|
339
|
+
backgroundColor: colors.bgInput,
|
|
340
|
+
border: `1px solid ${colors.border}`,
|
|
341
|
+
borderRadius: 3,
|
|
340
342
|
padding: '4px 6px',
|
|
341
|
-
minHeight:
|
|
343
|
+
minHeight: 28,
|
|
342
344
|
}}
|
|
343
345
|
>
|
|
344
346
|
<span
|
|
345
347
|
style={{
|
|
346
|
-
fontSize:
|
|
347
|
-
fontWeight:
|
|
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:
|
|
365
|
-
color:
|
|
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:
|
|
415
|
-
border:
|
|
416
|
-
borderRadius:
|
|
416
|
+
backgroundColor: colors.bgInput,
|
|
417
|
+
border: `1px solid ${colors.border}`,
|
|
418
|
+
borderRadius: 3,
|
|
417
419
|
cursor: 'pointer',
|
|
418
|
-
padding:
|
|
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
|
-
|
|
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,
|
|
52
|
-
|
|
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:
|
|
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:
|
|
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',
|
|
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,
|
|
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',
|
|
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: '
|
|
7
|
-
background:
|
|
8
|
-
color:
|
|
9
|
-
border:
|
|
10
|
-
borderRadius:
|
|
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 ?
|
|
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 =
|
|
46
|
+
if (!isActive) e.currentTarget.style.background = colors.bgHover;
|
|
43
47
|
}}
|
|
44
48
|
onPointerLeave={(e) => {
|
|
45
|
-
if (!isActive) e.currentTarget.style.background =
|
|
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 ?
|
|
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 =
|
|
68
|
+
if (snapResolution === 0) e.currentTarget.style.background = colors.bgHover;
|
|
63
69
|
}}
|
|
64
70
|
onPointerLeave={(e) => {
|
|
65
|
-
if (snapResolution === 0) e.currentTarget.style.background =
|
|
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: '
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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: '
|
|
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:
|
|
50
|
+
fontWeight: 600,
|
|
45
51
|
textTransform: 'uppercase',
|
|
46
|
-
letterSpacing: 0.
|
|
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.
|
|
58
|
+
background: colors.bgInput,
|
|
52
59
|
border: `1px solid ${colors.border}`,
|
|
53
60
|
borderRadius: 3,
|
|
54
|
-
padding: '
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
148
|
-
background:
|
|
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
|
|
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: '
|
|
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
|
-
|
|
197
|
+
boxShadow: '0 2px 8px rgba(0,0,0,0.4)',
|
|
187
198
|
},
|
|
188
199
|
divider: {
|
|
189
200
|
width: 1,
|
|
190
|
-
background:
|
|
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 ??
|
|
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
|
+
}
|