Skip to content

Commit

Permalink
Artifact Playground
Browse files Browse the repository at this point in the history
  • Loading branch information
ivntsng committed Nov 8, 2024
1 parent 68aa7e0 commit ec0bf69
Show file tree
Hide file tree
Showing 6 changed files with 420 additions and 11 deletions.
2 changes: 2 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import ResearchPage from "./components/pages/ResearchPage";
import SellerOnboarding from "./components/pages/SellerOnboarding";
import Terminal from "./components/pages/Terminal";
import TermsOfService from "./components/pages/TermsOfService";
import ArtifactPlayground from "@/components/pages/ArtifactPlayground";

const App = () => {
return (
Expand All @@ -59,6 +60,7 @@ const App = () => {
<Route path="/" element={<Home />} />

<Route path="/playground" element={<Playground />} />
<Route path="/playground/:artifactId" element={<ArtifactPlayground />} />

<Route path="/about" element={<About />} />
<Route path="/downloads" element={<DownloadsPage />} />
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/components/files/FileRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import MJCFRenderer from "./MJCFRenderer";
import STLRenderer from "./STLRenderer";
import { UntarredFile } from "./Tarfile";
import URDFRenderer from "./URDFRenderer";

const isMJCFFile = (content: string, filename: string): boolean => {
const extension = filename.split(".").pop()?.toLowerCase();
if (extension !== "xml" && extension !== "mjcf") {
return false;
}

return content.includes("<mujoco") && content.includes("</mujoco>");
};

const FileRenderer: React.FC<{
file: UntarredFile;
allFiles: UntarredFile[];
}> = ({ file, allFiles }) => {
const fileExtension = file.name.split(".").pop()?.toLowerCase();
const fileContent = new TextDecoder().decode(file.content);

switch (fileExtension) {
case "urdf":
Expand All @@ -19,6 +30,17 @@ const FileRenderer: React.FC<{
);
case "stl":
return <STLRenderer stlContent={file.content.buffer} />;
case "xml":
case "mjcf":
if (isMJCFFile(fileContent, file.name)) {
return <MJCFRenderer mjcfContent={fileContent} height="100%" />;
} else {
return (
<div className="h-full w-full flex items-center justify-center">
<p>Invalid MJCF file format</p>
</div>
);
}
default:
return (
<div className="h-full w-full flex items-center justify-center">
Expand Down
220 changes: 220 additions & 0 deletions frontend/src/components/files/MJCFRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import React, { forwardRef, useEffect, useRef, useState } from "react";
import { UntarredFile } from "./Tarfile";
import load_mujoco, { mujoco } from "@/lib/mujoco/mujoco_wasm.js";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

interface MJCFRendererProps {
mjcfContent: string;
files?: UntarredFile[];
width?: string | number;
height?: string | number;
useControls?: boolean;
showWireframe?: boolean;
}

const MJCFRenderer = forwardRef<any, MJCFRendererProps>(({
mjcfContent,
files = [],
width = "100%",
height = "100%",
useControls = true,
showWireframe = false,
}, ref) => {
const containerRef = useRef<HTMLDivElement>(null);
const sceneRef = useRef<THREE.Scene | null>(null);
const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
const controlsRef = useRef<OrbitControls | null>(null);
const robotRef = useRef<THREE.Object3D | null>(null);

const [isMujocoReady, setIsMujocoReady] = useState(false);
const [error, setError] = useState<string | null>(null);

const mujocoRef = useRef<mujoco | null>(null);
const modelRef = useRef<InstanceType<mujoco["Model"]> | null>(null);
const stateRef = useRef<InstanceType<mujoco["State"]> | null>(null);
const simulationRef = useRef<InstanceType<mujoco["Simulation"]> | null>(null);

useEffect(() => {
const initializeScene = async () => {
if (!containerRef.current) return;

try {
mujocoRef.current = await load_mujoco();
console.log("MuJoCo loaded successfully");

// Set up file system
mujocoRef.current.FS.mkdir("/working");
mujocoRef.current.FS.mount(
mujocoRef.current.MEMFS,
{ root: "." },
"/working"
);

// Create a simple test model without meshes
const testModel = `
<mujoco>
<compiler angle="radian"/>
<option gravity="0 0 -9.81"/>
<worldbody>
<light diffuse=".5 .5 .5" pos="0 0 3" dir="0 0 -1"/>
<geom type="plane" size="5 5 0.1" rgba=".9 .9 .9 1"/>
<body name="torso" pos="0 0 0.5">
<freejoint name="root"/>
<inertial pos="0 0 0" mass="1" diaginertia="0.1 0.1 0.1"/>
<geom type="box" size="0.15 0.1 0.1" rgba="0.5 0.5 1 1"/>
</body>
</worldbody>
</mujoco>`;

const modelPath = "/working/model.xml";
console.log("Writing model file to:", modelPath);
mujocoRef.current.FS.writeFile(modelPath, testModel);

// Load model
console.log("Loading model...");
modelRef.current = new mujocoRef.current.Model(modelPath);
console.log("Model properties:", {
nq: modelRef.current.nq,
nv: modelRef.current.nv,
nbody: modelRef.current.nbody,
ngeom: modelRef.current.ngeom
});

stateRef.current = new mujocoRef.current.State(modelRef.current);
simulationRef.current = new mujocoRef.current.Simulation(
modelRef.current,
stateRef.current
);

const nq = modelRef.current.nq;
if (nq !== 7) {
throw new Error(`Unexpected number of position coordinates: ${nq}`);
}

const initialQPos = new Float64Array(7);
initialQPos[0] = 0; // x
initialQPos[1] = 0; // y
initialQPos[2] = 1; // z
initialQPos[3] = 1; // qw
initialQPos[4] = 0; // qx
initialQPos[5] = 0; // qy
initialQPos[6] = 0; // qz

const qpos = simulationRef.current.qpos;
for (let i = 0; i < 7; i++) {
qpos[i] = initialQPos[i];
}

// Initialize Three.js scene
const scene = new THREE.Scene();
sceneRef.current = scene;
scene.background = new THREE.Color(0xf0f0f0);

// Initialize camera
const camera = new THREE.PerspectiveCamera(
45,
containerRef.current.clientWidth / containerRef.current.clientHeight,
0.1,
1000,
);
cameraRef.current = camera;
camera.position.set(2, 2, 2);
camera.lookAt(0, 0, 0);

// Initialize renderer
const renderer = new THREE.WebGLRenderer({ antialias: true });
rendererRef.current = renderer;
renderer.setSize(
containerRef.current.clientWidth,
containerRef.current.clientHeight,
);
renderer.shadowMap.enabled = true;
containerRef.current.appendChild(renderer.domElement);

// Initialize controls
const controls = new OrbitControls(camera, renderer.domElement);
controlsRef.current = controls;
controls.enableDamping = true;
controls.dampingFactor = 0.05;

// Add lights
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);

const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);

// Add grid helper
const gridHelper = new THREE.GridHelper(10, 10);
scene.add(gridHelper);

// Create visual representation
const robot = new THREE.Group();
robotRef.current = robot;
scene.add(robot);

// Animation loop
const animate = () => {
requestAnimationFrame(animate);

// Update robot position from simulation state
if (simulationRef.current && robotRef.current) {
const qpos = simulationRef.current.qpos;

// Update root body position and orientation
robotRef.current.position.set(qpos[0], qpos[1], qpos[2]);

// Convert quaternion to Three.js format (w, x, y, z)
robotRef.current.quaternion.set(qpos[4], qpos[5], qpos[6], qpos[3]);

// Step the simulation
try {
simulationRef.current.step();
} catch (error) {
console.error("Simulation step error:", error);
}
}

controls.update();
renderer.render(scene, camera);
};
animate();

setIsMujocoReady(true);
} catch (error) {
console.error("Error initializing scene:", error);
setError(error instanceof Error ? error.message : "Unknown error");
}
};

initializeScene();

return () => {
if (rendererRef.current?.domElement) {
rendererRef.current.domElement.remove();
}
};
}, [files]);

return (
<div
ref={containerRef}
style={{
width: width,
height: height,
position: "relative",
}}
>
{!isMujocoReady ? (
<div className="text-gray-600">Loading MuJoCo...</div>
) : error ? (
<div className="text-red-600">Error: {error}</div>
) : null}
</div>
);
});

export default MJCFRenderer;
30 changes: 30 additions & 0 deletions frontend/src/components/listing/ListingPlaygroundButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { FaPlay } from "react-icons/fa";
import { useNavigate } from "react-router-dom";

import { Button } from "@/components/ui/button";
import { Artifact } from "@/components/listing/types";

interface Props {
artifacts: Artifact[];
}

const ListingPlaygroundButton = ({ artifacts }: Props) => {
const navigate = useNavigate();

const tgzArtifact = artifacts.find(a => a.artifact_type === 'tgz');

if (!tgzArtifact?.artifact_id) return null;

return (
<Button
variant="primary"
className="flex items-center"
onClick={() => navigate(`/playground/${tgzArtifact.artifact_id}`)}
>
<FaPlay className="mr-2 h-4 w-4" />
<span className="mr-2">View on Playground</span>
</Button>
);
};

export default ListingPlaygroundButton;
16 changes: 5 additions & 11 deletions frontend/src/components/listing/ListingRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ListingName from "@/components/listing/ListingName";
import ListingOnshape from "@/components/listing/ListingOnshape";
import ListingRegisterRobot from "@/components/listing/ListingRegisterRobot";
import { Artifact, ListingResponse } from "@/components/listing/types";
import ListingPlaygroundButton from "./ListingPlaygroundButton";

const ListingRenderer = ({ listing }: { listing: ListingResponse }) => {
const {
Expand All @@ -31,14 +32,6 @@ const ListingRenderer = ({ listing }: { listing: ListingResponse }) => {
const [artifacts, setArtifacts] = useState(initialArtifacts);
const [currentImageIndex, setCurrentImageIndex] = useState(0);

const addArtifacts = (newArtifacts: Artifact[]) => {
setArtifacts((prevArtifacts) =>
[...newArtifacts, ...prevArtifacts].sort((a, b) =>
a.is_main ? -1 : b.is_main ? 1 : 0,
),
);
};

return (
<div className="max-w-6xl mx-auto p-4 pt-12">
{/* Main content area - flex column on mobile, row on desktop */}
Expand Down Expand Up @@ -72,12 +65,13 @@ const ListingRenderer = ({ listing }: { listing: ListingResponse }) => {
<hr className="border-gray-200 my-4" />

{/* Build this robot */}
<div className="flex items-center gap-4">
<div className="flex items-baseline gap-4">
<ListingRegisterRobot listingId={listingId} />
<ListingFeatureButton
listingId={listingId}
initialFeatured={isFeatured}
/>
<ListingPlaygroundButton artifacts={artifacts} />
</div>

<hr className="border-gray-200 my-4" />
Expand Down Expand Up @@ -107,7 +101,7 @@ const ListingRenderer = ({ listing }: { listing: ListingResponse }) => {
dropzoneOptions={{
accept: { "image/*": [".png", ".jpg", ".jpeg"] },
}}
addArtifacts={addArtifacts}
addArtifacts={setArtifacts}
/>
)}

Expand All @@ -117,7 +111,7 @@ const ListingRenderer = ({ listing }: { listing: ListingResponse }) => {
listingId={listingId}
onshapeUrl={onshapeUrl}
canEdit={canEdit}
addArtifacts={addArtifacts}
addArtifacts={setArtifacts}
/>
)}
</div>
Expand Down
Loading

0 comments on commit ec0bf69

Please sign in to comment.