react-three-game 0.0.59 → 0.0.61
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/tools/dragdrop/DragDropLoader.d.ts +8 -8
- package/dist/tools/dragdrop/DragDropLoader.js +33 -15
- package/dist/tools/dragdrop/index.d.ts +3 -3
- package/dist/tools/dragdrop/index.js +1 -1
- package/dist/tools/dragdrop/modelLoader.d.ts +10 -1
- package/dist/tools/dragdrop/modelLoader.js +39 -0
- package/dist/tools/prefabeditor/PrefabEditor.js +17 -26
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -1
- package/dist/tools/prefabeditor/PrefabRoot.js +2 -8
- package/package.json +9 -3
- package/.gitattributes +0 -2
- package/.github/copilot-instructions.md +0 -83
- package/.github/workflows/nextjs.yml +0 -99
- package/.gitmodules +0 -3
- package/assets/architecture.png +0 -0
- package/assets/editor.gif +0 -0
- package/assets/favicon.ico +0 -0
- package/assets/react-three-game-logo.png +0 -0
- package/dist/tools/dragdrop/page.d.ts +0 -1
- package/dist/tools/dragdrop/page.js +0 -11
- package/dist/tools/prefabeditor/EntityEvents.d.ts +0 -54
- package/dist/tools/prefabeditor/EntityEvents.js +0 -85
- package/dist/tools/prefabeditor/page.d.ts +0 -1
- package/dist/tools/prefabeditor/page.js +0 -5
- package/react-three-game-skill/.gitattributes +0 -2
- package/react-three-game-skill/README.md +0 -7
- package/react-three-game-skill/react-three-game/SKILL.md +0 -514
- package/react-three-game-skill/react-three-game/rules/ADVANCED_PHYSICS.md +0 -472
- package/react-three-game-skill/react-three-game/rules/LIGHTING.md +0 -6
- package/src/helpers/SoundManager.ts +0 -130
- package/src/helpers/index.ts +0 -91
- package/src/index.ts +0 -59
- package/src/shared/ContactShadow.tsx +0 -74
- package/src/shared/GameCanvas.tsx +0 -52
- package/src/tools/assetviewer/page.tsx +0 -425
- package/src/tools/dragdrop/DragDropLoader.tsx +0 -136
- package/src/tools/dragdrop/index.ts +0 -4
- package/src/tools/dragdrop/modelLoader.ts +0 -145
- package/src/tools/dragdrop/page.tsx +0 -45
- package/src/tools/prefabeditor/Dropdown.tsx +0 -112
- package/src/tools/prefabeditor/EditorContext.tsx +0 -25
- package/src/tools/prefabeditor/EditorTree.tsx +0 -452
- package/src/tools/prefabeditor/EditorTreeMenus.tsx +0 -307
- package/src/tools/prefabeditor/EditorUI.tsx +0 -204
- package/src/tools/prefabeditor/EventSystem.tsx +0 -36
- package/src/tools/prefabeditor/GameEvents.ts +0 -191
- package/src/tools/prefabeditor/InstanceProvider.tsx +0 -466
- package/src/tools/prefabeditor/PrefabEditor.tsx +0 -262
- package/src/tools/prefabeditor/PrefabRoot.tsx +0 -773
- package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +0 -34
- package/src/tools/prefabeditor/components/CameraComponent.tsx +0 -117
- package/src/tools/prefabeditor/components/ComponentRegistry.ts +0 -40
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +0 -210
- package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +0 -47
- package/src/tools/prefabeditor/components/GeometryComponent.tsx +0 -133
- package/src/tools/prefabeditor/components/Input.tsx +0 -820
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +0 -431
- package/src/tools/prefabeditor/components/ModelComponent.tsx +0 -176
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +0 -188
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +0 -109
- package/src/tools/prefabeditor/components/TextComponent.tsx +0 -137
- package/src/tools/prefabeditor/components/TransformComponent.tsx +0 -173
- package/src/tools/prefabeditor/components/index.ts +0 -26
- package/src/tools/prefabeditor/page.tsx +0 -10
- package/src/tools/prefabeditor/styles.ts +0 -235
- package/src/tools/prefabeditor/types.ts +0 -20
- package/src/tools/prefabeditor/utils.ts +0 -312
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import { RigidBody, RapierRigidBody, useRapier } from "@react-three/rapier";
|
|
2
|
-
import type { RigidBodyOptions, CollisionPayload, IntersectionEnterPayload, IntersectionExitPayload } from "@react-three/rapier";
|
|
3
|
-
import type { ReactNode } from 'react';
|
|
4
|
-
import { useRef, useEffect, useCallback } from 'react';
|
|
5
|
-
import { Component } from "./ComponentRegistry";
|
|
6
|
-
import { BooleanField, FieldGroup, NumberField, SelectField } from "./Input";
|
|
7
|
-
import { ComponentData } from "../types";
|
|
8
|
-
import { gameEvents, getEntityIdFromRigidBody } from "../GameEvents";
|
|
9
|
-
|
|
10
|
-
export type PhysicsProps = RigidBodyOptions & {
|
|
11
|
-
activeCollisionTypes?: 'all' | undefined;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
function PhysicsComponentEditor({ component, onUpdate }: { component: ComponentData; onUpdate: (newComp: any) => void }) {
|
|
15
|
-
return (
|
|
16
|
-
<FieldGroup>
|
|
17
|
-
<SelectField
|
|
18
|
-
name="type"
|
|
19
|
-
label="Type"
|
|
20
|
-
values={component.properties}
|
|
21
|
-
onChange={onUpdate}
|
|
22
|
-
options={[
|
|
23
|
-
{ value: 'dynamic', label: 'Dynamic' },
|
|
24
|
-
{ value: 'fixed', label: 'Fixed' },
|
|
25
|
-
{ value: 'kinematicPosition', label: 'Kinematic Position' },
|
|
26
|
-
{ value: 'kinematicVelocity', label: 'Kinematic Velocity' },
|
|
27
|
-
]}
|
|
28
|
-
/>
|
|
29
|
-
<SelectField
|
|
30
|
-
name="colliders"
|
|
31
|
-
label="Collider"
|
|
32
|
-
values={component.properties}
|
|
33
|
-
onChange={onUpdate}
|
|
34
|
-
options={[
|
|
35
|
-
{ value: 'hull', label: 'Hull (convex)' },
|
|
36
|
-
{ value: 'trimesh', label: 'Trimesh (exact)' },
|
|
37
|
-
{ value: 'cuboid', label: 'Cuboid (box)' },
|
|
38
|
-
{ value: 'ball', label: 'Ball (sphere)' },
|
|
39
|
-
]}
|
|
40
|
-
/>
|
|
41
|
-
<NumberField name="mass" label="Mass" values={component.properties} onChange={onUpdate} fallback={1} step={0.1} min={0} />
|
|
42
|
-
<NumberField name="restitution" label="Restitution (Bounciness)" values={component.properties} onChange={onUpdate} fallback={0} min={0} max={1} step={0.1} />
|
|
43
|
-
<NumberField name="friction" label="Friction" values={component.properties} onChange={onUpdate} fallback={0.5} min={0} step={0.1} />
|
|
44
|
-
<NumberField name="linearDamping" label="Linear Damping" values={component.properties} onChange={onUpdate} fallback={0} min={0} step={0.1} />
|
|
45
|
-
<NumberField name="angularDamping" label="Angular Damping" values={component.properties} onChange={onUpdate} fallback={0} min={0} step={0.1} />
|
|
46
|
-
<NumberField name="gravityScale" label="Gravity Scale" values={component.properties} onChange={onUpdate} fallback={1} step={0.1} />
|
|
47
|
-
<BooleanField name="sensor" label="Sensor (Trigger Only)" values={component.properties} onChange={onUpdate} fallback={false} />
|
|
48
|
-
<SelectField
|
|
49
|
-
name="activeCollisionTypes"
|
|
50
|
-
label="Collision Detection"
|
|
51
|
-
values={component.properties}
|
|
52
|
-
onChange={onUpdate}
|
|
53
|
-
options={[
|
|
54
|
-
{ value: '', label: 'Default (Dynamic only)' },
|
|
55
|
-
{ value: 'all', label: 'All (includes kinematic & fixed)' },
|
|
56
|
-
]}
|
|
57
|
-
/>
|
|
58
|
-
</FieldGroup>
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
interface PhysicsViewProps {
|
|
63
|
-
properties: PhysicsProps;
|
|
64
|
-
editMode?: boolean;
|
|
65
|
-
children?: ReactNode;
|
|
66
|
-
position?: [number, number, number];
|
|
67
|
-
rotation?: [number, number, number];
|
|
68
|
-
scale?: [number, number, number];
|
|
69
|
-
nodeId?: string;
|
|
70
|
-
registerRigidBodyRef?: (id: string, rb: RapierRigidBody | null) => void;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function PhysicsComponentView({ properties, children, position, rotation, scale, editMode, nodeId, registerRigidBodyRef }: PhysicsViewProps) {
|
|
74
|
-
const { type, colliders, sensor, activeCollisionTypes, ...otherProps } = properties;
|
|
75
|
-
const colliderType = colliders || (type === 'fixed' ? 'trimesh' : 'hull');
|
|
76
|
-
const rigidBodyRef = useRef<RapierRigidBody>(null);
|
|
77
|
-
|
|
78
|
-
// Try to get rapier context - will be null if not inside <Physics>
|
|
79
|
-
let rapier: any = null;
|
|
80
|
-
try {
|
|
81
|
-
const rapierContext = useRapier();
|
|
82
|
-
rapier = rapierContext.rapier;
|
|
83
|
-
} catch (e) {
|
|
84
|
-
// Not inside Physics context - that's ok, just won't have rapier features
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Register RigidBody ref when it's available
|
|
88
|
-
useEffect(() => {
|
|
89
|
-
if (nodeId && registerRigidBodyRef && rigidBodyRef.current) {
|
|
90
|
-
registerRigidBodyRef(nodeId, rigidBodyRef.current);
|
|
91
|
-
}
|
|
92
|
-
return () => {
|
|
93
|
-
if (nodeId && registerRigidBodyRef) {
|
|
94
|
-
registerRigidBodyRef(nodeId, null);
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
}, [nodeId, registerRigidBodyRef]);
|
|
98
|
-
|
|
99
|
-
// Configure active collision types for kinematic/sensor bodies
|
|
100
|
-
useEffect(() => {
|
|
101
|
-
if (activeCollisionTypes === 'all' && rigidBodyRef.current && rapier) {
|
|
102
|
-
const rb = rigidBodyRef.current;
|
|
103
|
-
// Apply to all colliders on this rigid body
|
|
104
|
-
for (let i = 0; i < rb.numColliders(); i++) {
|
|
105
|
-
const collider = rb.collider(i);
|
|
106
|
-
collider.setActiveCollisionTypes(
|
|
107
|
-
rapier.ActiveCollisionTypes.DEFAULT |
|
|
108
|
-
rapier.ActiveCollisionTypes.KINEMATIC_FIXED |
|
|
109
|
-
rapier.ActiveCollisionTypes.KINEMATIC_KINEMATIC
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}, [activeCollisionTypes, rapier, type, colliders]);
|
|
114
|
-
|
|
115
|
-
// Event handlers for physics interactions
|
|
116
|
-
const handleIntersectionEnter = useCallback((payload: IntersectionEnterPayload) => {
|
|
117
|
-
if (!nodeId) return;
|
|
118
|
-
gameEvents.emit('sensor:enter', {
|
|
119
|
-
sourceEntityId: nodeId,
|
|
120
|
-
targetEntityId: getEntityIdFromRigidBody(payload.other.rigidBody),
|
|
121
|
-
targetRigidBody: payload.other.rigidBody,
|
|
122
|
-
});
|
|
123
|
-
}, [nodeId]);
|
|
124
|
-
|
|
125
|
-
const handleIntersectionExit = useCallback((payload: IntersectionExitPayload) => {
|
|
126
|
-
if (!nodeId) return;
|
|
127
|
-
gameEvents.emit('sensor:exit', {
|
|
128
|
-
sourceEntityId: nodeId,
|
|
129
|
-
targetEntityId: getEntityIdFromRigidBody(payload.other.rigidBody),
|
|
130
|
-
targetRigidBody: payload.other.rigidBody,
|
|
131
|
-
});
|
|
132
|
-
}, [nodeId]);
|
|
133
|
-
|
|
134
|
-
const handleCollisionEnter = useCallback((payload: CollisionPayload) => {
|
|
135
|
-
if (!nodeId) return;
|
|
136
|
-
gameEvents.emit('collision:enter', {
|
|
137
|
-
sourceEntityId: nodeId,
|
|
138
|
-
targetEntityId: getEntityIdFromRigidBody(payload.other.rigidBody),
|
|
139
|
-
targetRigidBody: payload.other.rigidBody,
|
|
140
|
-
});
|
|
141
|
-
}, [nodeId]);
|
|
142
|
-
|
|
143
|
-
const handleCollisionExit = useCallback((payload: CollisionPayload) => {
|
|
144
|
-
if (!nodeId) return;
|
|
145
|
-
gameEvents.emit('collision:exit', {
|
|
146
|
-
sourceEntityId: nodeId,
|
|
147
|
-
targetEntityId: getEntityIdFromRigidBody(payload.other.rigidBody),
|
|
148
|
-
targetRigidBody: payload.other.rigidBody,
|
|
149
|
-
});
|
|
150
|
-
}, [nodeId]);
|
|
151
|
-
|
|
152
|
-
// In edit mode, include position/rotation in key to force remount when transform changes
|
|
153
|
-
// This ensures the RigidBody debug visualization updates even when physics is paused
|
|
154
|
-
const rbKey = editMode
|
|
155
|
-
? `${type || 'dynamic'}_${colliderType}_${position?.join(',')}_${rotation?.join(',')}`
|
|
156
|
-
: `${type || 'dynamic'}_${colliderType}`;
|
|
157
|
-
|
|
158
|
-
return (
|
|
159
|
-
<RigidBody
|
|
160
|
-
key={rbKey}
|
|
161
|
-
ref={rigidBodyRef}
|
|
162
|
-
type={type}
|
|
163
|
-
colliders={colliderType as any}
|
|
164
|
-
position={position}
|
|
165
|
-
rotation={rotation}
|
|
166
|
-
scale={scale}
|
|
167
|
-
sensor={sensor}
|
|
168
|
-
userData={{ entityId: nodeId }}
|
|
169
|
-
onIntersectionEnter={handleIntersectionEnter}
|
|
170
|
-
onIntersectionExit={handleIntersectionExit}
|
|
171
|
-
onCollisionEnter={handleCollisionEnter}
|
|
172
|
-
onCollisionExit={handleCollisionExit}
|
|
173
|
-
{...otherProps}
|
|
174
|
-
>
|
|
175
|
-
{children}
|
|
176
|
-
</RigidBody>
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const PhysicsComponent: Component = {
|
|
181
|
-
name: 'Physics',
|
|
182
|
-
Editor: PhysicsComponentEditor,
|
|
183
|
-
View: PhysicsComponentView,
|
|
184
|
-
nonComposable: true,
|
|
185
|
-
defaultProperties: { type: 'dynamic', colliders: 'hull' }
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
export default PhysicsComponent;
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { Component } from "./ComponentRegistry";
|
|
2
|
-
import { useRef, useEffect, useMemo, useState } from "react";
|
|
3
|
-
import { BooleanField, ColorField, FieldGroup, NumberField } from "./Input";
|
|
4
|
-
import { SpotLight, SpotLightHelper } from "three";
|
|
5
|
-
import { useFrame } from "@react-three/fiber";
|
|
6
|
-
|
|
7
|
-
const spotLightDefaults = {
|
|
8
|
-
color: '#ffffff',
|
|
9
|
-
intensity: 1,
|
|
10
|
-
angle: Math.PI / 6,
|
|
11
|
-
penumbra: 0.5,
|
|
12
|
-
distance: 100,
|
|
13
|
-
castShadow: true,
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
function SpotLightComponentEditor({ component, onUpdate }: { component: any; onUpdate: (newComp: any) => void }) {
|
|
17
|
-
const values = { ...spotLightDefaults, ...component.properties };
|
|
18
|
-
|
|
19
|
-
return (
|
|
20
|
-
<FieldGroup>
|
|
21
|
-
<ColorField name="color" label="Color" values={values} onChange={onUpdate} />
|
|
22
|
-
<NumberField name="intensity" label="Intensity" values={values} onChange={onUpdate} min={0} step={0.1} fallback={1} />
|
|
23
|
-
<NumberField name="angle" label="Angle" values={values} onChange={onUpdate} min={0} max={Math.PI} step={0.05} fallback={Math.PI / 6} />
|
|
24
|
-
<NumberField name="penumbra" label="Penumbra" values={values} onChange={onUpdate} min={0} max={1} step={0.05} fallback={0.5} />
|
|
25
|
-
<NumberField name="distance" label="Distance" values={values} onChange={onUpdate} min={0} step={1} fallback={100} />
|
|
26
|
-
<BooleanField name="castShadow" label="Cast Shadow" values={values} onChange={onUpdate} fallback={true} />
|
|
27
|
-
</FieldGroup>
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function SpotLightView({ properties, editMode, isSelected }: { properties: any; editMode?: boolean; isSelected?: boolean }) {
|
|
32
|
-
const merged = { ...spotLightDefaults, ...properties };
|
|
33
|
-
const color = merged.color;
|
|
34
|
-
const intensity = merged.intensity;
|
|
35
|
-
const angle = merged.angle;
|
|
36
|
-
const penumbra = merged.penumbra;
|
|
37
|
-
const distance = merged.distance;
|
|
38
|
-
const castShadow = merged.castShadow;
|
|
39
|
-
|
|
40
|
-
const spotLightRef = useRef<SpotLight>(null);
|
|
41
|
-
const targetRef = useRef<any>(null);
|
|
42
|
-
const [spotLight, setSpotLight] = useState<SpotLight | null>(null);
|
|
43
|
-
const spotLightHelper = useMemo(
|
|
44
|
-
() => spotLight ? new SpotLightHelper(spotLight, color) : null,
|
|
45
|
-
[spotLight, color]
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
useEffect(() => {
|
|
49
|
-
return () => {
|
|
50
|
-
spotLightHelper?.dispose();
|
|
51
|
-
};
|
|
52
|
-
}, [spotLightHelper]);
|
|
53
|
-
|
|
54
|
-
useEffect(() => {
|
|
55
|
-
if (spotLightRef.current && targetRef.current) {
|
|
56
|
-
spotLightRef.current.target = targetRef.current;
|
|
57
|
-
setSpotLight(spotLightRef.current);
|
|
58
|
-
}
|
|
59
|
-
}, []);
|
|
60
|
-
|
|
61
|
-
useFrame(() => {
|
|
62
|
-
if (spotLightHelper && editMode && isSelected) {
|
|
63
|
-
spotLightHelper.update();
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
return (
|
|
68
|
-
<>
|
|
69
|
-
<spotLight
|
|
70
|
-
ref={spotLightRef}
|
|
71
|
-
color={color}
|
|
72
|
-
intensity={intensity}
|
|
73
|
-
angle={angle}
|
|
74
|
-
penumbra={penumbra}
|
|
75
|
-
distance={distance}
|
|
76
|
-
castShadow={castShadow}
|
|
77
|
-
shadow-mapSize-width={1024}
|
|
78
|
-
shadow-mapSize-height={1024}
|
|
79
|
-
shadow-bias={-0.0001}
|
|
80
|
-
shadow-normalBias={0.02}
|
|
81
|
-
/>
|
|
82
|
-
{editMode && isSelected && spotLightHelper && (
|
|
83
|
-
<primitive object={spotLightHelper} />
|
|
84
|
-
)}
|
|
85
|
-
<object3D ref={targetRef} position={[0, -5, 0]} />
|
|
86
|
-
{editMode && (
|
|
87
|
-
<>
|
|
88
|
-
<mesh>
|
|
89
|
-
<sphereGeometry args={[0.2, 8, 6]} />
|
|
90
|
-
<meshBasicMaterial color={color} wireframe />
|
|
91
|
-
</mesh>
|
|
92
|
-
<mesh position={[0, -5, 0]}>
|
|
93
|
-
<sphereGeometry args={[0.15, 8, 6]} />
|
|
94
|
-
<meshBasicMaterial color={color} wireframe opacity={0.5} transparent />
|
|
95
|
-
</mesh>
|
|
96
|
-
</>
|
|
97
|
-
)}
|
|
98
|
-
</>
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const SpotLightComponent: Component = {
|
|
103
|
-
name: 'SpotLight',
|
|
104
|
-
Editor: SpotLightComponentEditor,
|
|
105
|
-
View: SpotLightView,
|
|
106
|
-
defaultProperties: spotLightDefaults
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
export default SpotLightComponent;
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { Component } from "./ComponentRegistry";
|
|
2
|
-
import { ColorField, FieldGroup, NumberField, SelectField, StringField } from "./Input";
|
|
3
|
-
import { Text } from 'three-text/three/react';
|
|
4
|
-
import { useRef, useState, useCallback } from 'react';
|
|
5
|
-
import { BufferGeometry, Mesh } from "three";
|
|
6
|
-
|
|
7
|
-
// Initialize HarfBuzz path for font shaping
|
|
8
|
-
Text.setHarfBuzzPath('/fonts/hb.wasm');
|
|
9
|
-
|
|
10
|
-
function TextComponentEditor({
|
|
11
|
-
component,
|
|
12
|
-
onUpdate,
|
|
13
|
-
}: {
|
|
14
|
-
component: any;
|
|
15
|
-
onUpdate: (newProps: any) => void;
|
|
16
|
-
}) {
|
|
17
|
-
return (
|
|
18
|
-
<FieldGroup>
|
|
19
|
-
<StringField
|
|
20
|
-
name="text"
|
|
21
|
-
label="Text"
|
|
22
|
-
values={component.properties}
|
|
23
|
-
onChange={onUpdate}
|
|
24
|
-
placeholder="Enter text..."
|
|
25
|
-
/>
|
|
26
|
-
<ColorField
|
|
27
|
-
name="color"
|
|
28
|
-
label="Color"
|
|
29
|
-
values={component.properties}
|
|
30
|
-
onChange={onUpdate}
|
|
31
|
-
/>
|
|
32
|
-
<StringField
|
|
33
|
-
name="font"
|
|
34
|
-
label="Font"
|
|
35
|
-
values={component.properties}
|
|
36
|
-
onChange={onUpdate}
|
|
37
|
-
placeholder="/fonts/NotoSans-Regular.ttf"
|
|
38
|
-
/>
|
|
39
|
-
<NumberField
|
|
40
|
-
name="size"
|
|
41
|
-
label="Size"
|
|
42
|
-
values={component.properties}
|
|
43
|
-
onChange={onUpdate}
|
|
44
|
-
min={0.01}
|
|
45
|
-
step={0.1}
|
|
46
|
-
/>
|
|
47
|
-
<NumberField
|
|
48
|
-
name="depth"
|
|
49
|
-
label="Depth"
|
|
50
|
-
values={component.properties}
|
|
51
|
-
onChange={onUpdate}
|
|
52
|
-
min={0}
|
|
53
|
-
step={0.1}
|
|
54
|
-
/>
|
|
55
|
-
<NumberField
|
|
56
|
-
name="width"
|
|
57
|
-
label="Width"
|
|
58
|
-
values={component.properties}
|
|
59
|
-
onChange={onUpdate}
|
|
60
|
-
min={0}
|
|
61
|
-
step={0.5}
|
|
62
|
-
/>
|
|
63
|
-
<SelectField
|
|
64
|
-
name="align"
|
|
65
|
-
label="Align"
|
|
66
|
-
values={component.properties}
|
|
67
|
-
onChange={onUpdate}
|
|
68
|
-
options={[
|
|
69
|
-
{ value: 'left', label: 'Left' },
|
|
70
|
-
{ value: 'center', label: 'Center' },
|
|
71
|
-
{ value: 'right', label: 'Right' },
|
|
72
|
-
]}
|
|
73
|
-
/>
|
|
74
|
-
</FieldGroup>
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function TextComponentView({ properties }: { properties: any }) {
|
|
79
|
-
const { text = '', font, size, depth, width, align, color } = properties;
|
|
80
|
-
const textContent = String(text || '');
|
|
81
|
-
const meshRef = useRef<Mesh>(null);
|
|
82
|
-
const [offset, setOffset] = useState<[number, number, number]>([0, 0, 0]);
|
|
83
|
-
|
|
84
|
-
const handleLoad = useCallback((_geometry: BufferGeometry, info: any) => {
|
|
85
|
-
if (info?.planeBounds) {
|
|
86
|
-
const bounds = info.planeBounds;
|
|
87
|
-
// Calculate X offset based on alignment
|
|
88
|
-
let centerX = 0;
|
|
89
|
-
if (align === 'center') {
|
|
90
|
-
centerX = -(bounds.min.x + bounds.max.x) / 2;
|
|
91
|
-
} else if (align === 'right') {
|
|
92
|
-
centerX = -bounds.max.x;
|
|
93
|
-
} else {
|
|
94
|
-
// left alignment
|
|
95
|
-
centerX = -bounds.min.x;
|
|
96
|
-
}
|
|
97
|
-
const centerY = -(bounds.min.y + bounds.max.y) / 2;
|
|
98
|
-
setOffset([centerX, centerY, 0]);
|
|
99
|
-
}
|
|
100
|
-
}, [align]);
|
|
101
|
-
|
|
102
|
-
if (!textContent) return null;
|
|
103
|
-
|
|
104
|
-
return (
|
|
105
|
-
<group position={offset}>
|
|
106
|
-
<Text
|
|
107
|
-
ref={meshRef}
|
|
108
|
-
font={font}
|
|
109
|
-
size={size}
|
|
110
|
-
depth={depth}
|
|
111
|
-
layout={{ align, width }}
|
|
112
|
-
color={color}
|
|
113
|
-
onLoad={handleLoad}
|
|
114
|
-
>
|
|
115
|
-
{textContent}
|
|
116
|
-
</Text>
|
|
117
|
-
</group>
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const TextComponent: Component = {
|
|
122
|
-
name: 'Text',
|
|
123
|
-
Editor: TextComponentEditor,
|
|
124
|
-
View: TextComponentView,
|
|
125
|
-
nonComposable: true,
|
|
126
|
-
defaultProperties: {
|
|
127
|
-
text: 'Hello World',
|
|
128
|
-
color: '#888888',
|
|
129
|
-
font: '/fonts/NotoSans-Regular.ttf',
|
|
130
|
-
size: 0.5,
|
|
131
|
-
depth: 0,
|
|
132
|
-
width: 5,
|
|
133
|
-
align: 'center',
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
export default TextComponent;
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import { Component } from "./ComponentRegistry";
|
|
2
|
-
import { Label, Vector3Field, Vector3Input } from "./Input";
|
|
3
|
-
import { useEditorContext } from "../EditorContext";
|
|
4
|
-
import { colors } from "../styles";
|
|
5
|
-
|
|
6
|
-
const buttonStyle = {
|
|
7
|
-
padding: '4px 8px',
|
|
8
|
-
background: colors.bgSurface,
|
|
9
|
-
color: colors.text,
|
|
10
|
-
border: `1px solid ${colors.border}`,
|
|
11
|
-
borderRadius: 3,
|
|
12
|
-
cursor: 'pointer',
|
|
13
|
-
font: 'inherit',
|
|
14
|
-
fontSize: 11,
|
|
15
|
-
flex: 1,
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
function TransformModeSelector({
|
|
19
|
-
transformMode,
|
|
20
|
-
setTransformMode,
|
|
21
|
-
snapResolution,
|
|
22
|
-
setSnapResolution
|
|
23
|
-
}: {
|
|
24
|
-
transformMode: "translate" | "rotate" | "scale";
|
|
25
|
-
setTransformMode: (m: "translate" | "rotate" | "scale") => void;
|
|
26
|
-
snapResolution: number;
|
|
27
|
-
setSnapResolution: (v: number) => void;
|
|
28
|
-
}) {
|
|
29
|
-
return (
|
|
30
|
-
<div style={{ marginBottom: 8 }}>
|
|
31
|
-
<Label>Transform Mode {snapResolution > 0 && `(Snap: ${snapResolution})`}</Label>
|
|
32
|
-
<div style={{ display: 'flex', gap: 6 }}>
|
|
33
|
-
{["translate", "rotate", "scale"].map(mode => {
|
|
34
|
-
const isActive = transformMode === mode;
|
|
35
|
-
return (
|
|
36
|
-
<button
|
|
37
|
-
key={mode}
|
|
38
|
-
onClick={() => setTransformMode(mode as any)}
|
|
39
|
-
style={{
|
|
40
|
-
...buttonStyle,
|
|
41
|
-
background: isActive ? colors.accentBg : colors.bgSurface,
|
|
42
|
-
borderColor: isActive ? colors.accentBorder : colors.border,
|
|
43
|
-
color: isActive ? colors.accent : colors.text,
|
|
44
|
-
}}
|
|
45
|
-
onPointerEnter={(e) => {
|
|
46
|
-
if (!isActive) e.currentTarget.style.background = colors.bgHover;
|
|
47
|
-
}}
|
|
48
|
-
onPointerLeave={(e) => {
|
|
49
|
-
if (!isActive) e.currentTarget.style.background = colors.bgSurface;
|
|
50
|
-
}}
|
|
51
|
-
>
|
|
52
|
-
{mode}
|
|
53
|
-
</button>
|
|
54
|
-
);
|
|
55
|
-
})}
|
|
56
|
-
</div>
|
|
57
|
-
<div style={{ marginTop: 6 }}>
|
|
58
|
-
<button
|
|
59
|
-
onClick={() => setSnapResolution(snapResolution > 0 ? 0 : 0.1)}
|
|
60
|
-
style={{
|
|
61
|
-
...buttonStyle,
|
|
62
|
-
background: snapResolution > 0 ? colors.accentBg : colors.bgSurface,
|
|
63
|
-
borderColor: snapResolution > 0 ? colors.accentBorder : colors.border,
|
|
64
|
-
color: snapResolution > 0 ? colors.accent : colors.text,
|
|
65
|
-
width: '100%',
|
|
66
|
-
}}
|
|
67
|
-
onPointerEnter={(e) => {
|
|
68
|
-
if (snapResolution === 0) e.currentTarget.style.background = colors.bgHover;
|
|
69
|
-
}}
|
|
70
|
-
onPointerLeave={(e) => {
|
|
71
|
-
if (snapResolution === 0) e.currentTarget.style.background = colors.bgSurface;
|
|
72
|
-
}}
|
|
73
|
-
>
|
|
74
|
-
Snap: {snapResolution > 0 ? `ON (${snapResolution})` : 'OFF'}
|
|
75
|
-
</button>
|
|
76
|
-
</div>
|
|
77
|
-
</div>
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const snapLockBtnStyle: React.CSSProperties = {
|
|
82
|
-
background: 'none',
|
|
83
|
-
border: 'none',
|
|
84
|
-
cursor: 'pointer',
|
|
85
|
-
padding: '0 2px',
|
|
86
|
-
fontSize: 12,
|
|
87
|
-
lineHeight: 1,
|
|
88
|
-
color: colors.textMuted,
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
function SnapLockButton({ locked, onToggle, title }: { locked: boolean; onToggle: () => void; title: string }) {
|
|
92
|
-
return (
|
|
93
|
-
<button style={snapLockBtnStyle} onClick={onToggle} title={title}>
|
|
94
|
-
{locked ? '🔒' : '🔓'}
|
|
95
|
-
</button>
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function TransformComponentEditor({ component, onUpdate }: {
|
|
100
|
-
component: any;
|
|
101
|
-
onUpdate: (newComp: any) => void;
|
|
102
|
-
}) {
|
|
103
|
-
const {
|
|
104
|
-
transformMode,
|
|
105
|
-
setTransformMode,
|
|
106
|
-
snapResolution,
|
|
107
|
-
setSnapResolution,
|
|
108
|
-
positionSnap,
|
|
109
|
-
setPositionSnap,
|
|
110
|
-
rotationSnap,
|
|
111
|
-
setRotationSnap
|
|
112
|
-
} = useEditorContext();
|
|
113
|
-
|
|
114
|
-
const positionSnapped = positionSnap > 0;
|
|
115
|
-
const rotationSnapped = rotationSnap > 0;
|
|
116
|
-
|
|
117
|
-
return (
|
|
118
|
-
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
|
119
|
-
<TransformModeSelector
|
|
120
|
-
transformMode={transformMode}
|
|
121
|
-
setTransformMode={setTransformMode}
|
|
122
|
-
snapResolution={snapResolution}
|
|
123
|
-
setSnapResolution={setSnapResolution}
|
|
124
|
-
/>
|
|
125
|
-
<Vector3Input
|
|
126
|
-
label="Position"
|
|
127
|
-
value={component.properties.position ?? [0, 0, 0]}
|
|
128
|
-
onChange={v => onUpdate({ position: v })}
|
|
129
|
-
snap={positionSnap}
|
|
130
|
-
labelExtra={
|
|
131
|
-
<SnapLockButton
|
|
132
|
-
locked={positionSnapped}
|
|
133
|
-
onToggle={() => setPositionSnap(positionSnapped ? 0 : 0.5)}
|
|
134
|
-
title={positionSnapped ? `Snap ON (0.5) — click to disable` : `Snap OFF — click to enable (0.5)`}
|
|
135
|
-
/>
|
|
136
|
-
}
|
|
137
|
-
/>
|
|
138
|
-
<Vector3Input
|
|
139
|
-
label="Rotation"
|
|
140
|
-
value={component.properties.rotation ?? [0, 0, 0]}
|
|
141
|
-
onChange={v => onUpdate({ rotation: v })}
|
|
142
|
-
snap={rotationSnap}
|
|
143
|
-
labelExtra={
|
|
144
|
-
<SnapLockButton
|
|
145
|
-
locked={rotationSnapped}
|
|
146
|
-
onToggle={() => setRotationSnap(rotationSnapped ? 0 : Math.PI / 4)}
|
|
147
|
-
title={rotationSnapped ? `Snap ON (π/4) — click to disable` : `Snap OFF — click to enable (π/4)`}
|
|
148
|
-
/>
|
|
149
|
-
}
|
|
150
|
-
/>
|
|
151
|
-
<Vector3Field
|
|
152
|
-
name="scale"
|
|
153
|
-
label="Scale"
|
|
154
|
-
values={component.properties}
|
|
155
|
-
onChange={onUpdate}
|
|
156
|
-
fallback={[1, 1, 1]}
|
|
157
|
-
/>
|
|
158
|
-
</div>
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const TransformComponent: Component = {
|
|
163
|
-
name: 'Transform',
|
|
164
|
-
Editor: TransformComponentEditor,
|
|
165
|
-
nonComposable: true,
|
|
166
|
-
defaultProperties: {
|
|
167
|
-
position: [0, 0, 0],
|
|
168
|
-
rotation: [0, 0, 0],
|
|
169
|
-
scale: [1, 1, 1]
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
export default TransformComponent;
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import GeometryComponent from './GeometryComponent';
|
|
2
|
-
import TransformComponent from './TransformComponent';
|
|
3
|
-
import MaterialComponent from './MaterialComponent';
|
|
4
|
-
import PhysicsComponent from './PhysicsComponent';
|
|
5
|
-
import SpotLightComponent from './SpotLightComponent';
|
|
6
|
-
import DirectionalLightComponent from './DirectionalLightComponent';
|
|
7
|
-
import AmbientLightComponent from './AmbientLightComponent';
|
|
8
|
-
import ModelComponent from './ModelComponent';
|
|
9
|
-
import TextComponent from './TextComponent';
|
|
10
|
-
import EnvironmentComponent from './EnvironmentComponent';
|
|
11
|
-
import CameraComponent from './CameraComponent';
|
|
12
|
-
|
|
13
|
-
export default [
|
|
14
|
-
GeometryComponent,
|
|
15
|
-
TransformComponent,
|
|
16
|
-
MaterialComponent,
|
|
17
|
-
PhysicsComponent,
|
|
18
|
-
SpotLightComponent,
|
|
19
|
-
DirectionalLightComponent,
|
|
20
|
-
AmbientLightComponent,
|
|
21
|
-
ModelComponent,
|
|
22
|
-
TextComponent,
|
|
23
|
-
EnvironmentComponent,
|
|
24
|
-
CameraComponent,
|
|
25
|
-
];
|
|
26
|
-
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import PrefabEditor from "./PrefabEditor";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export default function PrefabEditorPage() {
|
|
5
|
-
return <div style={{ width: '100%', height: '100%' }}>
|
|
6
|
-
<PrefabEditor>
|
|
7
|
-
<directionalLight position={[5, 10, 7.5]} intensity={1} castShadow />
|
|
8
|
-
</PrefabEditor>
|
|
9
|
-
</div>
|
|
10
|
-
}
|