|
|
@@ -0,0 +1,796 @@
|
|
|
+"use client";
|
|
|
+
|
|
|
+import { useRef, useEffect } from "react";
|
|
|
+import * as THREE from "three";
|
|
|
+import { RigidBody } from "@react-three/rapier";
|
|
|
+import type { RapierRigidBody } from "@react-three/rapier";
|
|
|
+import { useEditorStore } from "@/lib/store";
|
|
|
+
|
|
|
+interface PhysicsProps {
|
|
|
+ mass?: number | null;
|
|
|
+ isStatic?: boolean | null;
|
|
|
+ restitution?: number | null;
|
|
|
+ friction?: number | null;
|
|
|
+ colliderType?: string | null;
|
|
|
+}
|
|
|
+
|
|
|
+interface DamageProps {
|
|
|
+ amount?: number | null;
|
|
|
+ enabled?: boolean | null;
|
|
|
+}
|
|
|
+
|
|
|
+interface MaterialProps {
|
|
|
+ color?: string | null;
|
|
|
+ metalness?: number | null;
|
|
|
+ roughness?: number | null;
|
|
|
+ emissive?: string | null;
|
|
|
+ emissiveIntensity?: number | null;
|
|
|
+ opacity?: number | null;
|
|
|
+ transparent?: boolean | null;
|
|
|
+ wireframe?: boolean | null;
|
|
|
+}
|
|
|
+
|
|
|
+interface GamePrimitiveProps {
|
|
|
+ position?: [number, number, number] | null;
|
|
|
+ rotation?: [number, number, number] | null;
|
|
|
+ scale?: [number, number, number] | null;
|
|
|
+ castShadow?: boolean | null;
|
|
|
+ receiveShadow?: boolean | null;
|
|
|
+ material?: MaterialProps | null;
|
|
|
+ physics?: PhysicsProps | null;
|
|
|
+ damage?: DamageProps | null;
|
|
|
+ objectId?: string | null;
|
|
|
+ children?: React.ReactNode;
|
|
|
+}
|
|
|
+
|
|
|
+function buildMaterialProps(mat?: MaterialProps | null) {
|
|
|
+ if (!mat) return { color: "#888888" };
|
|
|
+ return {
|
|
|
+ color: mat.color ?? "#888888",
|
|
|
+ metalness: mat.metalness ?? 0,
|
|
|
+ roughness: mat.roughness ?? 0.5,
|
|
|
+ ...(mat.emissive ? { emissive: mat.emissive } : {}),
|
|
|
+ ...(mat.emissiveIntensity
|
|
|
+ ? { emissiveIntensity: mat.emissiveIntensity }
|
|
|
+ : {}),
|
|
|
+ ...(mat.opacity != null ? { opacity: mat.opacity, transparent: true } : {}),
|
|
|
+ ...(mat.wireframe ? { wireframe: true } : {}),
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function PhysicsWrapper({
|
|
|
+ physics,
|
|
|
+ damage,
|
|
|
+ position,
|
|
|
+ rotation,
|
|
|
+ scale,
|
|
|
+ children,
|
|
|
+}: {
|
|
|
+ physics?: PhysicsProps | null;
|
|
|
+ damage?: DamageProps | null;
|
|
|
+ position: [number, number, number];
|
|
|
+ rotation: [number, number, number];
|
|
|
+ scale: [number, number, number];
|
|
|
+ children: React.ReactNode;
|
|
|
+}) {
|
|
|
+ const bodyRef = useRef<RapierRigidBody>(null);
|
|
|
+ const takeDamage = useEditorStore((s) => s.takeDamage);
|
|
|
+ const isPlaying = useEditorStore((s) => s.isPlaying);
|
|
|
+
|
|
|
+ const hasPhysics =
|
|
|
+ physics && physics.colliderType && physics.colliderType !== "none";
|
|
|
+ const hasDamage = damage?.enabled && (damage.amount ?? 0) > 0;
|
|
|
+
|
|
|
+ if (!hasPhysics || !isPlaying) {
|
|
|
+ return (
|
|
|
+ <group position={position} rotation={rotation} scale={scale}>
|
|
|
+ {children}
|
|
|
+ </group>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <RigidBody
|
|
|
+ ref={bodyRef}
|
|
|
+ type={physics.isStatic ? "fixed" : "dynamic"}
|
|
|
+ position={position}
|
|
|
+ rotation={rotation}
|
|
|
+ mass={physics.mass ?? 1}
|
|
|
+ restitution={physics.restitution ?? 0.2}
|
|
|
+ friction={physics.friction ?? 0.5}
|
|
|
+ onCollisionEnter={
|
|
|
+ hasDamage
|
|
|
+ ? () => {
|
|
|
+ takeDamage(damage!.amount!);
|
|
|
+ }
|
|
|
+ : undefined
|
|
|
+ }
|
|
|
+ >
|
|
|
+ <group scale={scale}>{children}</group>
|
|
|
+ </RigidBody>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+export function GameBox({
|
|
|
+ position,
|
|
|
+ rotation,
|
|
|
+ scale,
|
|
|
+ castShadow,
|
|
|
+ receiveShadow,
|
|
|
+ material,
|
|
|
+ physics,
|
|
|
+ damage,
|
|
|
+ width,
|
|
|
+ height,
|
|
|
+ depth,
|
|
|
+}: GamePrimitiveProps & {
|
|
|
+ width?: number | null;
|
|
|
+ height?: number | null;
|
|
|
+ depth?: number | null;
|
|
|
+}) {
|
|
|
+ const pos: [number, number, number] = position ?? [0, 0, 0];
|
|
|
+ const rot: [number, number, number] = rotation ?? [0, 0, 0];
|
|
|
+ const scl: [number, number, number] = scale ?? [1, 1, 1];
|
|
|
+
|
|
|
+ return (
|
|
|
+ <PhysicsWrapper
|
|
|
+ physics={physics}
|
|
|
+ damage={damage}
|
|
|
+ position={pos}
|
|
|
+ rotation={rot}
|
|
|
+ scale={scl}
|
|
|
+ >
|
|
|
+ <mesh
|
|
|
+ castShadow={castShadow ?? false}
|
|
|
+ receiveShadow={receiveShadow ?? false}
|
|
|
+ >
|
|
|
+ <boxGeometry args={[width ?? 1, height ?? 1, depth ?? 1]} />
|
|
|
+ <meshStandardMaterial {...buildMaterialProps(material)} />
|
|
|
+ </mesh>
|
|
|
+ </PhysicsWrapper>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+export function GameSphere({
|
|
|
+ position,
|
|
|
+ rotation,
|
|
|
+ scale,
|
|
|
+ castShadow,
|
|
|
+ receiveShadow,
|
|
|
+ material,
|
|
|
+ physics,
|
|
|
+ damage,
|
|
|
+ radius,
|
|
|
+ widthSegments,
|
|
|
+ heightSegments,
|
|
|
+}: GamePrimitiveProps & {
|
|
|
+ radius?: number | null;
|
|
|
+ widthSegments?: number | null;
|
|
|
+ heightSegments?: number | null;
|
|
|
+}) {
|
|
|
+ const pos: [number, number, number] = position ?? [0, 0, 0];
|
|
|
+ const rot: [number, number, number] = rotation ?? [0, 0, 0];
|
|
|
+ const scl: [number, number, number] = scale ?? [1, 1, 1];
|
|
|
+
|
|
|
+ return (
|
|
|
+ <PhysicsWrapper
|
|
|
+ physics={physics}
|
|
|
+ damage={damage}
|
|
|
+ position={pos}
|
|
|
+ rotation={rot}
|
|
|
+ scale={scl}
|
|
|
+ >
|
|
|
+ <mesh
|
|
|
+ castShadow={castShadow ?? false}
|
|
|
+ receiveShadow={receiveShadow ?? false}
|
|
|
+ >
|
|
|
+ <sphereGeometry
|
|
|
+ args={[radius ?? 1, widthSegments ?? 32, heightSegments ?? 16]}
|
|
|
+ />
|
|
|
+ <meshStandardMaterial {...buildMaterialProps(material)} />
|
|
|
+ </mesh>
|
|
|
+ </PhysicsWrapper>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+export function GameCylinder({
|
|
|
+ position,
|
|
|
+ rotation,
|
|
|
+ scale,
|
|
|
+ castShadow,
|
|
|
+ receiveShadow,
|
|
|
+ material,
|
|
|
+ physics,
|
|
|
+ damage,
|
|
|
+ radiusTop,
|
|
|
+ radiusBottom,
|
|
|
+ height,
|
|
|
+ radialSegments,
|
|
|
+}: GamePrimitiveProps & {
|
|
|
+ radiusTop?: number | null;
|
|
|
+ radiusBottom?: number | null;
|
|
|
+ height?: number | null;
|
|
|
+ radialSegments?: number | null;
|
|
|
+}) {
|
|
|
+ const pos: [number, number, number] = position ?? [0, 0, 0];
|
|
|
+ const rot: [number, number, number] = rotation ?? [0, 0, 0];
|
|
|
+ const scl: [number, number, number] = scale ?? [1, 1, 1];
|
|
|
+
|
|
|
+ return (
|
|
|
+ <PhysicsWrapper
|
|
|
+ physics={physics}
|
|
|
+ damage={damage}
|
|
|
+ position={pos}
|
|
|
+ rotation={rot}
|
|
|
+ scale={scl}
|
|
|
+ >
|
|
|
+ <mesh
|
|
|
+ castShadow={castShadow ?? false}
|
|
|
+ receiveShadow={receiveShadow ?? false}
|
|
|
+ >
|
|
|
+ <cylinderGeometry
|
|
|
+ args={[
|
|
|
+ radiusTop ?? 1,
|
|
|
+ radiusBottom ?? 1,
|
|
|
+ height ?? 1,
|
|
|
+ radialSegments ?? 32,
|
|
|
+ ]}
|
|
|
+ />
|
|
|
+ <meshStandardMaterial {...buildMaterialProps(material)} />
|
|
|
+ </mesh>
|
|
|
+ </PhysicsWrapper>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+export function GameCone({
|
|
|
+ position,
|
|
|
+ rotation,
|
|
|
+ scale,
|
|
|
+ castShadow,
|
|
|
+ receiveShadow,
|
|
|
+ material,
|
|
|
+ physics,
|
|
|
+ damage,
|
|
|
+ radius,
|
|
|
+ height,
|
|
|
+ radialSegments,
|
|
|
+}: GamePrimitiveProps & {
|
|
|
+ radius?: number | null;
|
|
|
+ height?: number | null;
|
|
|
+ radialSegments?: number | null;
|
|
|
+}) {
|
|
|
+ const pos: [number, number, number] = position ?? [0, 0, 0];
|
|
|
+ const rot: [number, number, number] = rotation ?? [0, 0, 0];
|
|
|
+ const scl: [number, number, number] = scale ?? [1, 1, 1];
|
|
|
+
|
|
|
+ return (
|
|
|
+ <PhysicsWrapper
|
|
|
+ physics={physics}
|
|
|
+ damage={damage}
|
|
|
+ position={pos}
|
|
|
+ rotation={rot}
|
|
|
+ scale={scl}
|
|
|
+ >
|
|
|
+ <mesh
|
|
|
+ castShadow={castShadow ?? false}
|
|
|
+ receiveShadow={receiveShadow ?? false}
|
|
|
+ >
|
|
|
+ <coneGeometry args={[radius ?? 1, height ?? 1, radialSegments ?? 32]} />
|
|
|
+ <meshStandardMaterial {...buildMaterialProps(material)} />
|
|
|
+ </mesh>
|
|
|
+ </PhysicsWrapper>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+export function GameTorus({
|
|
|
+ position,
|
|
|
+ rotation,
|
|
|
+ scale,
|
|
|
+ castShadow,
|
|
|
+ receiveShadow,
|
|
|
+ material,
|
|
|
+ physics,
|
|
|
+ damage,
|
|
|
+ radius,
|
|
|
+ tube,
|
|
|
+ radialSegments,
|
|
|
+ tubularSegments,
|
|
|
+}: GamePrimitiveProps & {
|
|
|
+ radius?: number | null;
|
|
|
+ tube?: number | null;
|
|
|
+ radialSegments?: number | null;
|
|
|
+ tubularSegments?: number | null;
|
|
|
+}) {
|
|
|
+ const pos: [number, number, number] = position ?? [0, 0, 0];
|
|
|
+ const rot: [number, number, number] = rotation ?? [0, 0, 0];
|
|
|
+ const scl: [number, number, number] = scale ?? [1, 1, 1];
|
|
|
+
|
|
|
+ return (
|
|
|
+ <PhysicsWrapper
|
|
|
+ physics={physics}
|
|
|
+ damage={damage}
|
|
|
+ position={pos}
|
|
|
+ rotation={rot}
|
|
|
+ scale={scl}
|
|
|
+ >
|
|
|
+ <mesh
|
|
|
+ castShadow={castShadow ?? false}
|
|
|
+ receiveShadow={receiveShadow ?? false}
|
|
|
+ >
|
|
|
+ <torusGeometry
|
|
|
+ args={[
|
|
|
+ radius ?? 1,
|
|
|
+ tube ?? 0.4,
|
|
|
+ radialSegments ?? 16,
|
|
|
+ tubularSegments ?? 48,
|
|
|
+ ]}
|
|
|
+ />
|
|
|
+ <meshStandardMaterial {...buildMaterialProps(material)} />
|
|
|
+ </mesh>
|
|
|
+ </PhysicsWrapper>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+export function GamePlane({
|
|
|
+ position,
|
|
|
+ rotation,
|
|
|
+ scale,
|
|
|
+ castShadow,
|
|
|
+ receiveShadow,
|
|
|
+ material,
|
|
|
+ physics,
|
|
|
+ damage,
|
|
|
+ width,
|
|
|
+ height,
|
|
|
+}: GamePrimitiveProps & { width?: number | null; height?: number | null }) {
|
|
|
+ const pos: [number, number, number] = position ?? [0, 0, 0];
|
|
|
+ const rot: [number, number, number] = rotation ?? [0, 0, 0];
|
|
|
+ const scl: [number, number, number] = scale ?? [1, 1, 1];
|
|
|
+
|
|
|
+ return (
|
|
|
+ <PhysicsWrapper
|
|
|
+ physics={physics}
|
|
|
+ damage={damage}
|
|
|
+ position={pos}
|
|
|
+ rotation={rot}
|
|
|
+ scale={scl}
|
|
|
+ >
|
|
|
+ <mesh
|
|
|
+ castShadow={castShadow ?? false}
|
|
|
+ receiveShadow={receiveShadow ?? true}
|
|
|
+ >
|
|
|
+ <planeGeometry args={[width ?? 1, height ?? 1]} />
|
|
|
+ <meshStandardMaterial
|
|
|
+ {...buildMaterialProps(material)}
|
|
|
+ side={THREE.DoubleSide}
|
|
|
+ />
|
|
|
+ </mesh>
|
|
|
+ </PhysicsWrapper>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+export function GameCapsule({
|
|
|
+ position,
|
|
|
+ rotation,
|
|
|
+ scale,
|
|
|
+ castShadow,
|
|
|
+ receiveShadow,
|
|
|
+ material,
|
|
|
+ physics,
|
|
|
+ damage,
|
|
|
+ radius,
|
|
|
+ length,
|
|
|
+}: GamePrimitiveProps & {
|
|
|
+ radius?: number | null;
|
|
|
+ length?: number | null;
|
|
|
+ capSegments?: number | null;
|
|
|
+ radialSegments?: number | null;
|
|
|
+}) {
|
|
|
+ const pos: [number, number, number] = position ?? [0, 0, 0];
|
|
|
+ const rot: [number, number, number] = rotation ?? [0, 0, 0];
|
|
|
+ const scl: [number, number, number] = scale ?? [1, 1, 1];
|
|
|
+
|
|
|
+ return (
|
|
|
+ <PhysicsWrapper
|
|
|
+ physics={physics}
|
|
|
+ damage={damage}
|
|
|
+ position={pos}
|
|
|
+ rotation={rot}
|
|
|
+ scale={scl}
|
|
|
+ >
|
|
|
+ <mesh
|
|
|
+ castShadow={castShadow ?? false}
|
|
|
+ receiveShadow={receiveShadow ?? false}
|
|
|
+ >
|
|
|
+ <capsuleGeometry args={[radius ?? 0.5, length ?? 1, 16, 32]} />
|
|
|
+ <meshStandardMaterial {...buildMaterialProps(material)} />
|
|
|
+ </mesh>
|
|
|
+ </PhysicsWrapper>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+export function GameKnot({
|
|
|
+ position,
|
|
|
+ rotation,
|
|
|
+ scale,
|
|
|
+ castShadow,
|
|
|
+ receiveShadow,
|
|
|
+ material,
|
|
|
+ physics,
|
|
|
+ damage,
|
|
|
+ radius,
|
|
|
+ tube,
|
|
|
+ tubularSegments,
|
|
|
+ radialSegments,
|
|
|
+ p,
|
|
|
+ q,
|
|
|
+}: GamePrimitiveProps & {
|
|
|
+ radius?: number | null;
|
|
|
+ tube?: number | null;
|
|
|
+ tubularSegments?: number | null;
|
|
|
+ radialSegments?: number | null;
|
|
|
+ p?: number | null;
|
|
|
+ q?: number | null;
|
|
|
+}) {
|
|
|
+ const pos: [number, number, number] = position ?? [0, 0, 0];
|
|
|
+ const rot: [number, number, number] = rotation ?? [0, 0, 0];
|
|
|
+ const scl: [number, number, number] = scale ?? [1, 1, 1];
|
|
|
+
|
|
|
+ return (
|
|
|
+ <PhysicsWrapper
|
|
|
+ physics={physics}
|
|
|
+ damage={damage}
|
|
|
+ position={pos}
|
|
|
+ rotation={rot}
|
|
|
+ scale={scl}
|
|
|
+ >
|
|
|
+ <mesh
|
|
|
+ castShadow={castShadow ?? false}
|
|
|
+ receiveShadow={receiveShadow ?? false}
|
|
|
+ >
|
|
|
+ <torusKnotGeometry
|
|
|
+ args={[
|
|
|
+ radius ?? 1,
|
|
|
+ tube ?? 0.3,
|
|
|
+ tubularSegments ?? 64,
|
|
|
+ radialSegments ?? 8,
|
|
|
+ p ?? 2,
|
|
|
+ q ?? 3,
|
|
|
+ ]}
|
|
|
+ />
|
|
|
+ <meshStandardMaterial {...buildMaterialProps(material)} />
|
|
|
+ </mesh>
|
|
|
+ </PhysicsWrapper>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+function PolyhedronPrimitive({
|
|
|
+ Geometry,
|
|
|
+ ...props
|
|
|
+}: GamePrimitiveProps & {
|
|
|
+ Geometry: "tetrahedron" | "octahedron" | "dodecahedron" | "icosahedron";
|
|
|
+ radius?: number | null;
|
|
|
+}) {
|
|
|
+ const pos: [number, number, number] = props.position ?? [0, 0, 0];
|
|
|
+ const rot: [number, number, number] = props.rotation ?? [0, 0, 0];
|
|
|
+ const scl: [number, number, number] = props.scale ?? [1, 1, 1];
|
|
|
+ const r = props.radius ?? 1;
|
|
|
+
|
|
|
+ const geoElement = {
|
|
|
+ tetrahedron: <tetrahedronGeometry args={[r]} />,
|
|
|
+ octahedron: <octahedronGeometry args={[r]} />,
|
|
|
+ dodecahedron: <dodecahedronGeometry args={[r]} />,
|
|
|
+ icosahedron: <icosahedronGeometry args={[r]} />,
|
|
|
+ }[Geometry];
|
|
|
+
|
|
|
+ return (
|
|
|
+ <PhysicsWrapper
|
|
|
+ physics={props.physics}
|
|
|
+ damage={props.damage}
|
|
|
+ position={pos}
|
|
|
+ rotation={rot}
|
|
|
+ scale={scl}
|
|
|
+ >
|
|
|
+ <mesh
|
|
|
+ castShadow={props.castShadow ?? false}
|
|
|
+ receiveShadow={props.receiveShadow ?? false}
|
|
|
+ >
|
|
|
+ {geoElement}
|
|
|
+ <meshStandardMaterial {...buildMaterialProps(props.material)} />
|
|
|
+ </mesh>
|
|
|
+ </PhysicsWrapper>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+export function GameTetrahedron(
|
|
|
+ props: GamePrimitiveProps & { radius?: number | null },
|
|
|
+) {
|
|
|
+ return <PolyhedronPrimitive {...props} Geometry="tetrahedron" />;
|
|
|
+}
|
|
|
+
|
|
|
+export function GameOctahedron(
|
|
|
+ props: GamePrimitiveProps & { radius?: number | null },
|
|
|
+) {
|
|
|
+ return <PolyhedronPrimitive {...props} Geometry="octahedron" />;
|
|
|
+}
|
|
|
+
|
|
|
+export function GameDodecahedron(
|
|
|
+ props: GamePrimitiveProps & { radius?: number | null },
|
|
|
+) {
|
|
|
+ return <PolyhedronPrimitive {...props} Geometry="dodecahedron" />;
|
|
|
+}
|
|
|
+
|
|
|
+export function GameIcosahedron(
|
|
|
+ props: GamePrimitiveProps & { radius?: number | null },
|
|
|
+) {
|
|
|
+ return <PolyhedronPrimitive {...props} Geometry="icosahedron" />;
|
|
|
+}
|
|
|
+
|
|
|
+export function GameExtrude({
|
|
|
+ position,
|
|
|
+ rotation,
|
|
|
+ scale,
|
|
|
+ castShadow,
|
|
|
+ receiveShadow,
|
|
|
+ material,
|
|
|
+ physics,
|
|
|
+ damage,
|
|
|
+ shapeData,
|
|
|
+ depth,
|
|
|
+}: GamePrimitiveProps & {
|
|
|
+ shapeData?: {
|
|
|
+ points: [number, number][];
|
|
|
+ holes?: [number, number][][];
|
|
|
+ } | null;
|
|
|
+ depth?: number | null;
|
|
|
+}) {
|
|
|
+ const pos: [number, number, number] = position ?? [0, 0, 0];
|
|
|
+ const rot: [number, number, number] = rotation ?? [0, 0, 0];
|
|
|
+ const scl: [number, number, number] = scale ?? [1, 1, 1];
|
|
|
+
|
|
|
+ const shape = new THREE.Shape();
|
|
|
+ const pts = shapeData?.points ?? [
|
|
|
+ [-0.5, -0.5],
|
|
|
+ [0.5, -0.5],
|
|
|
+ [0.5, 0.5],
|
|
|
+ [-0.5, 0.5],
|
|
|
+ ];
|
|
|
+ if (pts.length > 0) {
|
|
|
+ shape.moveTo(pts[0]![0], pts[0]![1]);
|
|
|
+ for (let i = 1; i < pts.length; i++) {
|
|
|
+ shape.lineTo(pts[i]![0], pts[i]![1]);
|
|
|
+ }
|
|
|
+ shape.closePath();
|
|
|
+ }
|
|
|
+ if (shapeData?.holes) {
|
|
|
+ for (const hole of shapeData.holes) {
|
|
|
+ const holePath = new THREE.Path();
|
|
|
+ if (hole.length > 0) {
|
|
|
+ holePath.moveTo(hole[0]![0], hole[0]![1]);
|
|
|
+ for (let i = 1; i < hole.length; i++) {
|
|
|
+ holePath.lineTo(hole[i]![0], hole[i]![1]);
|
|
|
+ }
|
|
|
+ holePath.closePath();
|
|
|
+ shape.holes.push(holePath);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <PhysicsWrapper
|
|
|
+ physics={physics}
|
|
|
+ damage={damage}
|
|
|
+ position={pos}
|
|
|
+ rotation={rot}
|
|
|
+ scale={scl}
|
|
|
+ >
|
|
|
+ <mesh
|
|
|
+ castShadow={castShadow ?? false}
|
|
|
+ receiveShadow={receiveShadow ?? false}
|
|
|
+ >
|
|
|
+ <extrudeGeometry
|
|
|
+ args={[
|
|
|
+ shape as THREE.Shape,
|
|
|
+ { depth: depth ?? 1, bevelEnabled: false },
|
|
|
+ ]}
|
|
|
+ />
|
|
|
+ <meshStandardMaterial {...buildMaterialProps(material)} />
|
|
|
+ </mesh>
|
|
|
+ </PhysicsWrapper>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+export function GameTube({
|
|
|
+ position,
|
|
|
+ rotation,
|
|
|
+ scale,
|
|
|
+ castShadow,
|
|
|
+ receiveShadow,
|
|
|
+ material,
|
|
|
+ physics,
|
|
|
+ damage,
|
|
|
+ radius,
|
|
|
+ tubularSegments,
|
|
|
+ radialSegments,
|
|
|
+}: GamePrimitiveProps & {
|
|
|
+ radius?: number | null;
|
|
|
+ tubularSegments?: number | null;
|
|
|
+ radialSegments?: number | null;
|
|
|
+}) {
|
|
|
+ const pos: [number, number, number] = position ?? [0, 0, 0];
|
|
|
+ const rot: [number, number, number] = rotation ?? [0, 0, 0];
|
|
|
+ const scl: [number, number, number] = scale ?? [1, 1, 1];
|
|
|
+
|
|
|
+ const curve = new THREE.CatmullRomCurve3([
|
|
|
+ new THREE.Vector3(-1, 0, 0),
|
|
|
+ new THREE.Vector3(-0.5, 0.5, 0),
|
|
|
+ new THREE.Vector3(0.5, -0.5, 0),
|
|
|
+ new THREE.Vector3(1, 0, 0),
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <PhysicsWrapper
|
|
|
+ physics={physics}
|
|
|
+ damage={damage}
|
|
|
+ position={pos}
|
|
|
+ rotation={rot}
|
|
|
+ scale={scl}
|
|
|
+ >
|
|
|
+ <mesh
|
|
|
+ castShadow={castShadow ?? false}
|
|
|
+ receiveShadow={receiveShadow ?? false}
|
|
|
+ >
|
|
|
+ <tubeGeometry
|
|
|
+ args={[
|
|
|
+ curve as THREE.Curve<THREE.Vector3>,
|
|
|
+ tubularSegments ?? 64,
|
|
|
+ radius ?? 0.1,
|
|
|
+ radialSegments ?? 8,
|
|
|
+ false,
|
|
|
+ ]}
|
|
|
+ />
|
|
|
+ <meshStandardMaterial {...buildMaterialProps(material)} />
|
|
|
+ </mesh>
|
|
|
+ </PhysicsWrapper>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+export function GameShape({
|
|
|
+ position,
|
|
|
+ rotation,
|
|
|
+ scale,
|
|
|
+ castShadow,
|
|
|
+ receiveShadow,
|
|
|
+ material,
|
|
|
+ physics,
|
|
|
+ damage,
|
|
|
+ shapeData,
|
|
|
+}: GamePrimitiveProps & {
|
|
|
+ shapeData?: {
|
|
|
+ points: [number, number][];
|
|
|
+ holes?: [number, number][][];
|
|
|
+ } | null;
|
|
|
+}) {
|
|
|
+ const pos: [number, number, number] = position ?? [0, 0, 0];
|
|
|
+ const rot: [number, number, number] = rotation ?? [0, 0, 0];
|
|
|
+ const scl: [number, number, number] = scale ?? [1, 1, 1];
|
|
|
+
|
|
|
+ const shape = new THREE.Shape();
|
|
|
+ const pts = shapeData?.points ?? [
|
|
|
+ [-0.5, -0.5],
|
|
|
+ [0.5, -0.5],
|
|
|
+ [0, 0.5],
|
|
|
+ ];
|
|
|
+ if (pts.length > 0) {
|
|
|
+ shape.moveTo(pts[0]![0], pts[0]![1]);
|
|
|
+ for (let i = 1; i < pts.length; i++) {
|
|
|
+ shape.lineTo(pts[i]![0], pts[i]![1]);
|
|
|
+ }
|
|
|
+ shape.closePath();
|
|
|
+ }
|
|
|
+ if (shapeData?.holes) {
|
|
|
+ for (const hole of shapeData.holes) {
|
|
|
+ const holePath = new THREE.Path();
|
|
|
+ if (hole.length > 0) {
|
|
|
+ holePath.moveTo(hole[0]![0], hole[0]![1]);
|
|
|
+ for (let i = 1; i < hole.length; i++) {
|
|
|
+ holePath.lineTo(hole[i]![0], hole[i]![1]);
|
|
|
+ }
|
|
|
+ holePath.closePath();
|
|
|
+ shape.holes.push(holePath);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <PhysicsWrapper
|
|
|
+ physics={physics}
|
|
|
+ damage={damage}
|
|
|
+ position={pos}
|
|
|
+ rotation={rot}
|
|
|
+ scale={scl}
|
|
|
+ >
|
|
|
+ <mesh
|
|
|
+ castShadow={castShadow ?? false}
|
|
|
+ receiveShadow={receiveShadow ?? false}
|
|
|
+ >
|
|
|
+ <shapeGeometry args={[shape as THREE.Shape]} />
|
|
|
+ <meshStandardMaterial
|
|
|
+ {...buildMaterialProps(material)}
|
|
|
+ side={THREE.DoubleSide}
|
|
|
+ />
|
|
|
+ </mesh>
|
|
|
+ </PhysicsWrapper>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+export function GameMesh({
|
|
|
+ position,
|
|
|
+ rotation,
|
|
|
+ scale,
|
|
|
+ castShadow,
|
|
|
+ receiveShadow,
|
|
|
+ material,
|
|
|
+ physics,
|
|
|
+ damage,
|
|
|
+ meshData,
|
|
|
+}: GamePrimitiveProps & {
|
|
|
+ meshData?: {
|
|
|
+ vertices: number[];
|
|
|
+ indices: number[];
|
|
|
+ normals?: number[];
|
|
|
+ uvs?: number[];
|
|
|
+ } | null;
|
|
|
+}) {
|
|
|
+ const pos: [number, number, number] = position ?? [0, 0, 0];
|
|
|
+ const rot: [number, number, number] = rotation ?? [0, 0, 0];
|
|
|
+ const scl: [number, number, number] = scale ?? [1, 1, 1];
|
|
|
+
|
|
|
+ const meshRef = useRef<THREE.Mesh>(null);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (!meshRef.current || !meshData) return;
|
|
|
+ const geometry = new THREE.BufferGeometry();
|
|
|
+ geometry.setAttribute(
|
|
|
+ "position",
|
|
|
+ new THREE.Float32BufferAttribute(meshData.vertices, 3),
|
|
|
+ );
|
|
|
+ if (meshData.indices.length > 0) {
|
|
|
+ geometry.setIndex(meshData.indices);
|
|
|
+ }
|
|
|
+ if (meshData.normals) {
|
|
|
+ geometry.setAttribute(
|
|
|
+ "normal",
|
|
|
+ new THREE.Float32BufferAttribute(meshData.normals, 3),
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ geometry.computeVertexNormals();
|
|
|
+ }
|
|
|
+ if (meshData.uvs) {
|
|
|
+ geometry.setAttribute(
|
|
|
+ "uv",
|
|
|
+ new THREE.Float32BufferAttribute(meshData.uvs, 2),
|
|
|
+ );
|
|
|
+ }
|
|
|
+ meshRef.current.geometry = geometry;
|
|
|
+ }, [meshData]);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <PhysicsWrapper
|
|
|
+ physics={physics}
|
|
|
+ damage={damage}
|
|
|
+ position={pos}
|
|
|
+ rotation={rot}
|
|
|
+ scale={scl}
|
|
|
+ >
|
|
|
+ <mesh
|
|
|
+ ref={meshRef}
|
|
|
+ castShadow={castShadow ?? false}
|
|
|
+ receiveShadow={receiveShadow ?? false}
|
|
|
+ >
|
|
|
+ <boxGeometry args={[1, 1, 1]} />
|
|
|
+ <meshStandardMaterial {...buildMaterialProps(material)} />
|
|
|
+ </mesh>
|
|
|
+ </PhysicsWrapper>
|
|
|
+ );
|
|
|
+}
|