"use client";
import { useState } from "react";
import { ChevronDown, ChevronRight, Eye, EyeOff, Trash2 } from "lucide-react";
import { useEditorStore } from "@/lib/store";
import { useIsMobile } from "@/lib/use-mobile";
import type { SceneObject, Physics, LightType } from "@/lib/types";
function Section({
label,
defaultOpen = true,
children,
}: {
label: string;
defaultOpen?: boolean;
children: React.ReactNode;
}) {
const [open, setOpen] = useState(defaultOpen);
return (
{open && (
{children}
)}
);
}
function Field({
label,
children,
}: {
label: string;
children: React.ReactNode;
}) {
return (
);
}
function NumberInput({
value,
onChange,
step = 0.1,
}: {
value: number;
onChange: (v: number) => void;
step?: number;
}) {
return (
onChange(parseFloat(e.target.value) || 0)}
step={step}
className="w-full bg-[#1a1a1a] border border-[#2a2a2a] rounded px-1.5 py-1 sm:py-0.5 text-xs sm:text-[11px] text-[#ccc] outline-none focus:border-[#555]"
/>
);
}
function Vec3Input({
value,
onChange,
labels = ["X", "Y", "Z"],
}: {
value: [number, number, number];
onChange: (v: [number, number, number]) => void;
labels?: [string, string, string];
}) {
return (
{[0, 1, 2].map((i) => (
{labels[i]}
{
const next = [...value] as [number, number, number];
next[i] = v;
onChange(next);
}}
/>
))}
);
}
function ColorInput({
value,
onChange,
}: {
value: string;
onChange: (v: string) => void;
}) {
return (
onChange(e.target.value)}
className="w-8 h-8 sm:w-6 sm:h-6 rounded border border-[#2a2a2a] cursor-pointer bg-transparent"
/>
onChange(e.target.value)}
className="flex-1 bg-[#1a1a1a] border border-[#2a2a2a] rounded px-1.5 py-1 sm:py-0.5 text-xs sm:text-[11px] text-[#ccc] outline-none focus:border-[#555]"
/>
);
}
function SelectInput({
value,
onChange,
options,
}: {
value: string;
onChange: (v: string) => void;
options: { value: string; label: string }[];
}) {
return (
);
}
function CheckboxInput({
value,
onChange,
label,
}: {
value: boolean;
onChange: (v: boolean) => void;
label: string;
}) {
return (
);
}
function ObjectListItem({ obj }: { obj: SceneObject }) {
const {
selectedObjectId,
selectObject,
removeObject,
toggleObjectVisibility,
} = useEditorStore();
const isSelected = selectedObjectId === obj.id;
const isMobile = useIsMobile();
return (
selectObject(obj.id)}
>
{obj.name}
);
}
function TransformEditor({ obj }: { obj: SceneObject }) {
const updateObjectTransform = useEditorStore((s) => s.updateObjectTransform);
const saveToHistory = useEditorStore((s) => s.saveToHistory);
return (
{
updateObjectTransform(obj.id, { position: v });
saveToHistory();
}}
/>
{
updateObjectTransform(obj.id, { rotation: v });
saveToHistory();
}}
/>
{
updateObjectTransform(obj.id, { scale: v });
saveToHistory();
}}
/>
);
}
function MaterialEditor({ obj }: { obj: SceneObject }) {
const updateObjectMaterial = useEditorStore((s) => s.updateObjectMaterial);
return (
updateObjectMaterial(obj.id, { color: v })}
/>
updateObjectMaterial(obj.id, { metalness: v })}
step={0.05}
/>
updateObjectMaterial(obj.id, { roughness: v })}
step={0.05}
/>
updateObjectMaterial(obj.id, { emissive: v })}
/>
updateObjectMaterial(obj.id, { emissiveIntensity: v })
}
step={0.1}
/>
);
}
function PhysicsEditor({ obj }: { obj: SceneObject }) {
const updateObjectPhysics = useEditorStore((s) => s.updateObjectPhysics);
return (
updateObjectPhysics(obj.id, {
colliderType: v as Physics["colliderType"],
})
}
options={[
{ value: "none", label: "None" },
{ value: "cuboid", label: "Cuboid" },
{ value: "ball", label: "Ball" },
{ value: "capsule", label: "Capsule" },
]}
/>
{obj.physics.colliderType !== "none" && (
<>
updateObjectPhysics(obj.id, { mass: v })}
/>
updateObjectPhysics(obj.id, { isStatic: v })}
label="Static"
/>
updateObjectPhysics(obj.id, { restitution: v })}
step={0.05}
/>
updateObjectPhysics(obj.id, { friction: v })}
step={0.05}
/>
>
)}
);
}
function DamageEditor({ obj }: { obj: SceneObject }) {
const updateDamage = useEditorStore((s) => s.updateDamage);
const dmg = obj.damage || { amount: 0, enabled: false };
return (
updateDamage(obj.id, { enabled: v })}
label="Enabled"
/>
{dmg.enabled && (
updateDamage(obj.id, { amount: v })}
step={1}
/>
)}
);
}
function LightEditor({ obj }: { obj: SceneObject }) {
const updateObjectGeneral = useEditorStore((s) => s.updateObjectGeneral);
return (
updateObjectGeneral(obj.id, { lightType: v as LightType })
}
options={[
{ value: "ambient", label: "Ambient" },
{ value: "directional", label: "Directional" },
{ value: "point", label: "Point" },
{ value: "spot", label: "Spot" },
]}
/>
updateObjectGeneral(obj.id, { intensity: v })}
step={0.1}
/>
{(obj.lightType === "point" || obj.lightType === "spot") && (
updateObjectGeneral(obj.id, { distance: v })}
/>
)}
{obj.lightType === "spot" && (
<>
updateObjectGeneral(obj.id, { angle: v })}
step={0.05}
/>
updateObjectGeneral(obj.id, { penumbra: v })}
step={0.05}
/>
>
)}
);
}
function TextInput({
value,
onChange,
placeholder,
}: {
value: string;
onChange: (v: string) => void;
placeholder?: string;
}) {
return (
onChange(e.target.value)}
placeholder={placeholder}
className="w-full bg-[#1a1a1a] border border-[#2a2a2a] rounded px-1.5 py-1 sm:py-0.5 text-xs sm:text-[11px] text-[#ccc] outline-none focus:border-[#555] placeholder:text-[#444]"
/>
);
}
function ModelUrlEditor({ obj }: { obj: SceneObject }) {
const updateObjectGeneral = useEditorStore((s) => s.updateObjectGeneral);
return (
updateObjectGeneral(obj.id, { modelUrl: v })}
placeholder="/models/example.glb"
/>
Enter a URL to a .glb or .gltf file. Place files in public/models/ to
use local paths like /models/file.glb
);
}
function CharacterEditor({ obj }: { obj: SceneObject }) {
const updateCharacter = useEditorStore((s) => s.updateCharacter);
const role = obj.character?.role || "";
return (
updateCharacter(obj.id, { role: v })}
placeholder="e.g. village guard, merchant"
/>
);
}
function EnvironmentEditor() {
const scenes = useEditorStore((s) => s.scenes);
const activeSceneId = useEditorStore((s) => s.activeSceneId);
const updateEnvironmentSettings = useEditorStore(
(s) => s.updateEnvironmentSettings,
);
const activeScene = scenes.find((s) => s.id === activeSceneId);
if (!activeScene) return null;
const env = activeScene.environmentSettings;
return (
updateEnvironmentSettings({ preset: v })}
options={[
{ value: "apartment", label: "Apartment" },
{ value: "city", label: "City" },
{ value: "dawn", label: "Dawn" },
{ value: "forest", label: "Forest" },
{ value: "lobby", label: "Lobby" },
{ value: "night", label: "Night" },
{ value: "park", label: "Park" },
{ value: "studio", label: "Studio" },
{ value: "sunset", label: "Sunset" },
{ value: "warehouse", label: "Warehouse" },
]}
/>
updateEnvironmentSettings({ intensity: v })}
step={0.1}
/>
updateEnvironmentSettings({ blur: v })}
step={0.1}
/>
);
}
function FogEditor() {
const scenes = useEditorStore((s) => s.scenes);
const activeSceneId = useEditorStore((s) => s.activeSceneId);
const updateSceneSettings = useEditorStore((s) => s.updateSceneSettings);
const activeScene = scenes.find((s) => s.id === activeSceneId);
if (!activeScene) return null;
const fog = activeScene.sceneSettings.fog;
return (
updateSceneSettings({ fog: { ...fog, enabled: v } })}
label="Enabled"
/>
{fog.enabled && (
<>
updateSceneSettings({ fog: { ...fog, color: v } })
}
/>
updateSceneSettings({ fog: { ...fog, near: v } })
}
/>
updateSceneSettings({ fog: { ...fog, far: v } })}
/>
>
)}
);
}
function GridEditor() {
const scenes = useEditorStore((s) => s.scenes);
const activeSceneId = useEditorStore((s) => s.activeSceneId);
const updateSceneSettings = useEditorStore((s) => s.updateSceneSettings);
const activeScene = scenes.find((s) => s.id === activeSceneId);
if (!activeScene) return null;
const grid = activeScene.sceneSettings.grid;
return (
updateSceneSettings({ grid: { ...grid, visible: v } })}
label="Visible"
/>
{grid.visible && (
<>
updateSceneSettings({ grid: { ...grid, size: v } })
}
step={0.5}
/>
updateSceneSettings({
grid: { ...grid, divisions: Math.max(1, Math.round(v)) },
})
}
step={1}
/>
updateSceneSettings({ grid: { ...grid, fadeDistance: v } })
}
step={10}
/>
updateSceneSettings({ grid: { ...grid, fadeStrength: v } })
}
step={0.1}
/>
>
)}
);
}
export function SceneInspector() {
const scenes = useEditorStore((s) => s.scenes);
const activeSceneId = useEditorStore((s) => s.activeSceneId);
const selectedObjectId = useEditorStore((s) => s.selectedObjectId);
const activeScene = scenes.find((s) => s.id === activeSceneId);
const objects = activeScene?.objects ?? [];
const selectedObj = selectedObjectId
? objects.find((o) => o.id === selectedObjectId)
: null;
return (
{/* Object list */}
{objects.map((obj) => (
))}
{/* Selected object editors */}
{selectedObj && (
<>
{selectedObj.type !== "light" &&
selectedObj.type !== "player" &&
selectedObj.type !== "sound" && (
)}
{selectedObj.type !== "light" && selectedObj.type !== "player" && (
)}
{selectedObj.type !== "light" &&
selectedObj.type !== "player" &&
selectedObj.type !== "sound" &&
}
{selectedObj.type === "light" &&
}
{(selectedObj.type === "model" ||
selectedObj.type === "character") && (
)}
{selectedObj.type === "character" && (
)}
>
)}
{/* Scene-level editors */}
);
}