Spaces:
Running
Running
funnel: API banner, clearer 'Create your own via API', API snippet, UTMs, team@projectsim.ai
#3
by volodymyrsvirhun - opened
- README.md +4 -0
- badges/create-your-own-xl-dark.svg +7 -6
- badges/made-with-api-banner.svg +20 -0
- classes.json +51 -10
- index.html +88 -248
README.md
CHANGED
|
@@ -17,6 +17,10 @@ license: mit
|
|
| 17 |
|
| 18 |
# projectsim Lab
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
In-browser 3D playground for the Kaedim class datasets — boxes, cages, desks,
|
| 21 |
ovens, wardrobes: pick a class tab, pick an item, orbit it, and articulate
|
| 22 |
every joint — grab a flap/door/drawer/wheel and drag it, or use the sliders
|
|
|
|
| 17 |
|
| 18 |
# projectsim Lab
|
| 19 |
|
| 20 |
+
> Every asset in this lab was generated by the **projectsim API** —
|
| 21 |
+
> [generate your own →](https://docs.projectsim.ai/getting-started/quickstart?utm_source=hf_space&utm_medium=readme&utm_content=lab)
|
| 22 |
+
> Each item's **Take it home** panel shows the exact API call that orders a set like it.
|
| 23 |
+
|
| 24 |
In-browser 3D playground for the Kaedim class datasets — boxes, cages, desks,
|
| 25 |
ovens, wardrobes: pick a class tab, pick an item, orbit it, and articulate
|
| 26 |
every joint — grab a flap/door/drawer/wheel and drag it, or use the sliders
|
badges/create-your-own-xl-dark.svg
CHANGED
|
|
|
|
badges/made-with-api-banner.svg
ADDED
|
|
classes.json
CHANGED
|
@@ -3,65 +3,106 @@
|
|
| 3 |
"class": "cardboard_box",
|
| 4 |
"label": "Boxes",
|
| 5 |
"dataset": "warehouse-manipulation-and-states",
|
| 6 |
-
"catalog_path": "viewer/cardboard_box.json"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
},
|
| 8 |
{
|
| 9 |
"class": "logistics_cage",
|
| 10 |
"label": "Cages",
|
| 11 |
"dataset": "warehouse-manipulation-and-states",
|
| 12 |
-
"catalog_path": "viewer/logistics_cage.json"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
},
|
| 14 |
{
|
| 15 |
"class": "desk_with_drawers",
|
| 16 |
"label": "Desks",
|
| 17 |
"dataset": "articulated-storage-and-appliances",
|
| 18 |
-
"catalog_path": "viewer/desk_with_drawers.json"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
},
|
| 20 |
{
|
| 21 |
"class": "oven",
|
| 22 |
"label": "Ovens",
|
| 23 |
"dataset": "articulated-storage-and-appliances",
|
| 24 |
-
"catalog_path": "viewer/oven.json"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
},
|
| 26 |
{
|
| 27 |
"class": "wardrobe",
|
| 28 |
"label": "Wardrobes",
|
| 29 |
"dataset": "articulated-storage-and-appliances",
|
| 30 |
-
"catalog_path": "viewer/wardrobe.json"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
},
|
| 32 |
{
|
| 33 |
"class": "pallet_boxes",
|
| 34 |
"label": "Pallet Boxes",
|
| 35 |
"dataset": "warehouse-manipulation-and-states",
|
| 36 |
"catalog_path": "viewer/pallet_boxes.json",
|
| 37 |
-
"static": true
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
},
|
| 39 |
{
|
| 40 |
"class": "gas_cylinder",
|
| 41 |
"label": "Gas Cylinders",
|
| 42 |
"dataset": "industrial-parts-and-packaging",
|
| 43 |
"catalog_path": "viewer/gas_cylinder.json",
|
| 44 |
-
"static": true
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
},
|
| 46 |
{
|
| 47 |
"class": "pallet_racking",
|
| 48 |
"label": "Pallet Racking",
|
| 49 |
"dataset": "industrial-parts-and-packaging",
|
| 50 |
"catalog_path": "viewer/pallet_racking.json",
|
| 51 |
-
"static": true
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
},
|
| 53 |
{
|
| 54 |
"class": "containers",
|
| 55 |
"label": "Containers",
|
| 56 |
"dataset": "bottles-and-containers",
|
| 57 |
"catalog_path": "viewer/containers.json",
|
| 58 |
-
"static": true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
},
|
| 60 |
{
|
| 61 |
"class": "scenes",
|
| 62 |
"label": "Scenes",
|
| 63 |
"dataset": "scenes-and-worlds",
|
| 64 |
"catalog_path": "viewer/scenes.json",
|
| 65 |
-
"static": true
|
|
|
|
| 66 |
}
|
| 67 |
]
|
|
|
|
| 3 |
"class": "cardboard_box",
|
| 4 |
"label": "Boxes",
|
| 5 |
"dataset": "warehouse-manipulation-and-states",
|
| 6 |
+
"catalog_path": "viewer/cardboard_box.json",
|
| 7 |
+
"api": {
|
| 8 |
+
"articulated": true,
|
| 9 |
+
"seed": "I need an articulated cardboard box, opened on top with flat bottom."
|
| 10 |
+
}
|
| 11 |
},
|
| 12 |
{
|
| 13 |
"class": "logistics_cage",
|
| 14 |
"label": "Cages",
|
| 15 |
"dataset": "warehouse-manipulation-and-states",
|
| 16 |
+
"catalog_path": "viewer/logistics_cage.json",
|
| 17 |
+
"api": {
|
| 18 |
+
"articulated": true,
|
| 19 |
+
"seed": "I need an articulated wire roll cage with four swivel casters."
|
| 20 |
+
}
|
| 21 |
},
|
| 22 |
{
|
| 23 |
"class": "desk_with_drawers",
|
| 24 |
"label": "Desks",
|
| 25 |
"dataset": "articulated-storage-and-appliances",
|
| 26 |
+
"catalog_path": "viewer/desk_with_drawers.json",
|
| 27 |
+
"api": {
|
| 28 |
+
"articulated": true,
|
| 29 |
+
"seed": "I need an articulated wooden table with one sliding drawer."
|
| 30 |
+
}
|
| 31 |
},
|
| 32 |
{
|
| 33 |
"class": "oven",
|
| 34 |
"label": "Ovens",
|
| 35 |
"dataset": "articulated-storage-and-appliances",
|
| 36 |
+
"catalog_path": "viewer/oven.json",
|
| 37 |
+
"api": {
|
| 38 |
+
"articulated": true,
|
| 39 |
+
"seed": "I need an articulated kitchen oven with a drop-down front door."
|
| 40 |
+
}
|
| 41 |
},
|
| 42 |
{
|
| 43 |
"class": "wardrobe",
|
| 44 |
"label": "Wardrobes",
|
| 45 |
"dataset": "articulated-storage-and-appliances",
|
| 46 |
+
"catalog_path": "viewer/wardrobe.json",
|
| 47 |
+
"api": {
|
| 48 |
+
"articulated": true,
|
| 49 |
+
"seed": "I need an articulated two-door wardrobe."
|
| 50 |
+
}
|
| 51 |
},
|
| 52 |
{
|
| 53 |
"class": "pallet_boxes",
|
| 54 |
"label": "Pallet Boxes",
|
| 55 |
"dataset": "warehouse-manipulation-and-states",
|
| 56 |
"catalog_path": "viewer/pallet_boxes.json",
|
| 57 |
+
"static": true,
|
| 58 |
+
"api": {
|
| 59 |
+
"articulated": false,
|
| 60 |
+
"seed": "I need a wooden pallet stacked with taped cardboard boxes."
|
| 61 |
+
}
|
| 62 |
},
|
| 63 |
{
|
| 64 |
"class": "gas_cylinder",
|
| 65 |
"label": "Gas Cylinders",
|
| 66 |
"dataset": "industrial-parts-and-packaging",
|
| 67 |
"catalog_path": "viewer/gas_cylinder.json",
|
| 68 |
+
"static": true,
|
| 69 |
+
"api": {
|
| 70 |
+
"articulated": false,
|
| 71 |
+
"seed": "I need a steel gas cylinder with a valve guard."
|
| 72 |
+
}
|
| 73 |
},
|
| 74 |
{
|
| 75 |
"class": "pallet_racking",
|
| 76 |
"label": "Pallet Racking",
|
| 77 |
"dataset": "industrial-parts-and-packaging",
|
| 78 |
"catalog_path": "viewer/pallet_racking.json",
|
| 79 |
+
"static": true,
|
| 80 |
+
"api": {
|
| 81 |
+
"articulated": false,
|
| 82 |
+
"seed": "I need a steel warehouse pallet racking bay."
|
| 83 |
+
}
|
| 84 |
},
|
| 85 |
{
|
| 86 |
"class": "containers",
|
| 87 |
"label": "Containers",
|
| 88 |
"dataset": "bottles-and-containers",
|
| 89 |
"catalog_path": "viewer/containers.json",
|
| 90 |
+
"static": true,
|
| 91 |
+
"api": {
|
| 92 |
+
"articulated": false,
|
| 93 |
+
"seeds": {
|
| 94 |
+
"beverage_can": "I need an aluminium beverage can.",
|
| 95 |
+
"glass_jar": "I need a glass jar with a screw lid.",
|
| 96 |
+
"mug": "I need a ceramic coffee mug with a handle."
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
},
|
| 100 |
{
|
| 101 |
"class": "scenes",
|
| 102 |
"label": "Scenes",
|
| 103 |
"dataset": "scenes-and-worlds",
|
| 104 |
"catalog_path": "viewer/scenes.json",
|
| 105 |
+
"static": true,
|
| 106 |
+
"scene": true
|
| 107 |
}
|
| 108 |
]
|
index.html
CHANGED
|
@@ -94,39 +94,9 @@
|
|
| 94 |
#panel .copy:hover { background: var(--accent); }
|
| 95 |
#panel .copy:active { transform: scale(.95); }
|
| 96 |
#panel .copy.ok { background: var(--success); }
|
| 97 |
-
#panel .envtabs { display: flex; flex-wrap: wrap; gap: 5px; margin: 6px 0 8px; }
|
| 98 |
-
#panel .envtab { padding: 4px 11px; border: 1px solid var(--border);
|
| 99 |
-
border-radius: 999px; background: var(--surface);
|
| 100 |
-
cursor: pointer; font: 500 11.5px var(--sans); color: var(--ink);
|
| 101 |
-
transition: background .15s, border-color .15s; }
|
| 102 |
-
#panel .envtab:hover { border-color: var(--accent); }
|
| 103 |
-
#panel .envtab.active { background: var(--accent-strong); color: #fff;
|
| 104 |
-
border-color: var(--accent-strong); }
|
| 105 |
-
#panel .envtab.active:hover { background: var(--accent); border-color: var(--accent); }
|
| 106 |
-
#panel .envtab .soon { opacity: .55; font-size: 8.5px; margin-left: 4px;
|
| 107 |
-
text-transform: uppercase; letter-spacing: .05em;
|
| 108 |
-
vertical-align: middle; }
|
| 109 |
#panel .hdr { display: flex; align-items: baseline; gap: 8px; margin-top: 14px; }
|
| 110 |
#panel .hdr h2 { margin: 0; }
|
| 111 |
#panel .fmt { margin-left: auto; font: 500 10px var(--code); color: var(--body); }
|
| 112 |
-
#panel .prereq { margin: -2px 0 7px; font: 500 10.5px var(--sans);
|
| 113 |
-
color: var(--body); }
|
| 114 |
-
#panel .prereq.na { color: var(--accent-strong); }
|
| 115 |
-
#panel .envtab.na { opacity: .7; }
|
| 116 |
-
#panel .envtab .tag { margin-left: 4px; padding: 1px 5px; font-size: 8px;
|
| 117 |
-
border-radius: 6px; background: var(--subtle);
|
| 118 |
-
color: var(--body); letter-spacing: .04em;
|
| 119 |
-
text-transform: uppercase; vertical-align: middle; }
|
| 120 |
-
#panel .envtab.active .tag { background: rgba(255,255,255,.24); color: #fff; }
|
| 121 |
-
#panel .modetabs { display: flex; gap: 6px; margin: 11px 0 6px; }
|
| 122 |
-
#panel .modetab { padding: 5px 13px; border: 1px solid var(--border);
|
| 123 |
-
border-radius: 999px; background: var(--surface);
|
| 124 |
-
cursor: pointer; font: 600 11.5px var(--sans);
|
| 125 |
-
color: var(--ink); transition: background .15s, border-color .15s; }
|
| 126 |
-
#panel .modetab:hover { border-color: var(--accent); }
|
| 127 |
-
#panel .modetab.active { background: var(--accent-strong); color: #fff;
|
| 128 |
-
border-color: var(--accent-strong); }
|
| 129 |
-
#panel .modetab.active:hover { background: var(--accent); border-color: var(--accent); }
|
| 130 |
#panel .codewrap { position: relative; }
|
| 131 |
#panel .codewrap pre { margin: 0; padding-right: 36px; }
|
| 132 |
#panel .copybtn { position: absolute; top: 7px; right: 7px; width: 26px;
|
|
@@ -141,6 +111,24 @@
|
|
| 141 |
#panel .copybtn.ok .i-copy { display: none; }
|
| 142 |
#panel .copybtn.ok .i-ok { display: inline; }
|
| 143 |
[hidden] { display: none !important; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
</style>
|
| 145 |
<style id="scene-loader-style">
|
| 146 |
#scene-loader { position: absolute; inset: 0; display: none;
|
|
@@ -155,13 +143,8 @@
|
|
| 155 |
<style id="scene-links-style">
|
| 156 |
body.class-static #panel .row,
|
| 157 |
body.class-static #panel #joints,
|
| 158 |
-
body.class-
|
| 159 |
-
body.class-
|
| 160 |
-
body.class-static #panel #takeMeta,
|
| 161 |
-
body.class-static #panel .envtabs,
|
| 162 |
-
body.class-static #panel .prereq,
|
| 163 |
-
body.class-static #panel .codewrap { display: none !important; }
|
| 164 |
-
body:not(.class-static) #scene-links { display: none; }
|
| 165 |
#scene-links { padding: 4px 0; }
|
| 166 |
#scene-links h2 { margin: 12px 0 6px; font-size: 15px; }
|
| 167 |
#scene-links a { display: block; padding: 8px 12px; margin: 4px 0;
|
|
@@ -173,14 +156,19 @@ body:not(.class-static) #scene-links { display: none; }
|
|
| 173 |
</head>
|
| 174 |
<body>
|
| 175 |
<div id="scene-loader"><div class="spin"></div><span id="scene-loader-text">loading…</span></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
<header>
|
| 177 |
<h1>projectsim Lab</h1>
|
| 178 |
<div id="tabs"></div>
|
| 179 |
<nav class="links">
|
| 180 |
<a id="dslink" href="https://huggingface.co/projectsim"
|
| 181 |
target="_blank" rel="noopener">Dataset</a>
|
| 182 |
-
<a href="https://docs.projectsim.ai
|
| 183 |
-
rel="noopener">API docs</a>
|
| 184 |
</nav>
|
| 185 |
</header>
|
| 186 |
<main>
|
|
@@ -195,14 +183,9 @@ body:not(.class-static) #scene-links { display: none; }
|
|
| 195 |
<button id="anim" class="ghost">▶ Loop</button>
|
| 196 |
</div>
|
| 197 |
<div id="joints"></div>
|
| 198 |
-
<div
|
| 199 |
-
<div class="
|
| 200 |
-
|
| 201 |
-
<button class="modetab" data-mode="agent">Agent prompt</button>
|
| 202 |
-
</div>
|
| 203 |
-
<div class="meta" id="takeMeta"></div>
|
| 204 |
-
<div class="envtabs" id="envtabs"></div>
|
| 205 |
-
<div class="prereq" id="submeta"></div>
|
| 206 |
<div class="codewrap">
|
| 207 |
<pre id="output"></pre>
|
| 208 |
<button class="copybtn" id="copy" title="Copy" aria-label="Copy">
|
|
@@ -215,11 +198,19 @@ body:not(.class-static) #scene-links { display: none; }
|
|
| 215 |
stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
|
| 216 |
</button>
|
| 217 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
<div id="scene-links">
|
| 219 |
<h2>Take it home</h2>
|
| 220 |
<a id="scene-usd-link" href="#" target="_blank" rel="noopener">Full USD (with Gaussian splat) on Hugging Face →</a>
|
| 221 |
<a id="scene-glb-link" href="#" target="_blank" rel="noopener">This scene as GLB →</a>
|
| 222 |
<a id="scene-dataset-link" href="https://huggingface.co/datasets/projectsim/scenes-and-worlds" target="_blank" rel="noopener">Browse the scenes-and-worlds dataset →</a>
|
|
|
|
| 223 |
<div class="note" id="scene-note">USD ships the full room including the Gaussian splat backdrop; the GLB is a lightweight preview (splat dropped, textures baked).</div>
|
| 224 |
</div>
|
| 225 |
</aside>
|
|
@@ -247,8 +238,6 @@ const dsUrl = (ds, path) => LOCAL_BASE
|
|
| 247 |
? `${LOCAL_BASE}/${ds}/${path}`
|
| 248 |
: GATED.has(ds) ? `${PREVIEW}/${ds}/${path}`
|
| 249 |
: `${HFDS}/${ds}/resolve/main/${path}`;
|
| 250 |
-
const auth = cls => GATED.has(cls.dataset)
|
| 251 |
-
? `# gated: accept access at ${HFDS}/${cls.dataset} first\nhf auth login\n\n` : "";
|
| 252 |
const stage = document.getElementById("stage");
|
| 253 |
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
| 254 |
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
|
|
@@ -380,214 +369,64 @@ addEventListener("pointerup", () => {
|
|
| 380 |
renderer.domElement.style.cursor = ""; }
|
| 381 |
});
|
| 382 |
|
| 383 |
-
// ---- Take-it-home:
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
}
|
| 391 |
-
const OS = detectOS();
|
| 392 |
-
const OS_LABEL = { mac: "macOS", win: "Windows", linux: "Linux" }[OS];
|
| 393 |
-
|
| 394 |
let curCls = null, curItem = null;
|
| 395 |
-
let curEnv = (() => { try { return localStorage.getItem("ps_env"); }
|
| 396 |
-
catch (e) { return null; } })();
|
| 397 |
-
let curMode = (() => { try { return localStorage.getItem("ps_mode"); }
|
| 398 |
-
catch (e) { return null; } })() || "manual";
|
| 399 |
-
|
| 400 |
-
// software options for MANUAL mode. "auto" self-detects at run time; os =
|
| 401 |
-
// platforms each runs on natively (used to flag Isaac on macOS).
|
| 402 |
-
const ENVS = [
|
| 403 |
-
{ id: "auto", label: "Auto-detect", fmt: "detects engine",
|
| 404 |
-
os: ["mac", "win", "linux"],
|
| 405 |
-
prereq: "Runs whichever engine you have installed (detected at run time)." },
|
| 406 |
-
{ id: "isaac", label: "Isaac Sim", fmt: "USD + textures",
|
| 407 |
-
os: ["win", "linux"],
|
| 408 |
-
prereq: "Isaac Sim — Linux/Windows + an NVIDIA RTX GPU." },
|
| 409 |
-
{ id: "mujoco", label: "MuJoCo", fmt: "MJCF + meshes",
|
| 410 |
-
os: ["mac", "win", "linux"],
|
| 411 |
-
prereq: "pip install mujoco · any OS." },
|
| 412 |
-
{ id: "usd", label: "usdview / Omniverse", fmt: "USD + textures",
|
| 413 |
-
os: ["mac", "win", "linux"],
|
| 414 |
-
prereq: "usdview (pip install usd-core) — Omniverse needs Linux/Win + RTX." },
|
| 415 |
-
{ id: "blender", label: "Blender", fmt: ".glb",
|
| 416 |
-
os: ["mac", "win", "linux"],
|
| 417 |
-
prereq: "Blender 4.0+ — free at blender.org/download." },
|
| 418 |
-
];
|
| 419 |
-
const envById = id => ENVS.find(e => e.id === id);
|
| 420 |
-
const naOn = e => e.id !== "auto" && !e.os.includes(OS);
|
| 421 |
-
if (!envById(curEnv)) curEnv = "auto"; // default: self-detect
|
| 422 |
-
|
| 423 |
-
const classLabel = cls =>
|
| 424 |
-
cls.dataset.replace(/^articulated-/, "")
|
| 425 |
-
.replace(/ies$/, "y").replace(/([sxz]|ch|sh)es$/, "$1").replace(/s$/, "");
|
| 426 |
-
|
| 427 |
-
// One-shot snapshot download of a subtree from the dataset via huggingface_hub.
|
| 428 |
-
// Files land at the same relative path they have on the hub.
|
| 429 |
-
function hfDl(cls, item, patterns) {
|
| 430 |
-
const inc = patterns.map(p => `"assets/${cls.class}/${item.id}/${p}"`).join(", ");
|
| 431 |
-
return `python - <<'PY'\n` +
|
| 432 |
-
`from huggingface_hub import snapshot_download\n` +
|
| 433 |
-
`snapshot_download("projectsim/${cls.dataset}", repo_type="dataset",\n` +
|
| 434 |
-
` allow_patterns=[${inc}], local_dir=".")\n` +
|
| 435 |
-
`PY\n`;
|
| 436 |
-
}
|
| 437 |
-
|
| 438 |
-
// The path a downloaded file lives at, relative to projectsim-assets/.
|
| 439 |
-
const assetPath = (cls, item, file) => `assets/${cls.class}/${item.id}/${file}`;
|
| 440 |
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
const
|
| 446 |
-
|
| 447 |
-
if (env === "mujoco")
|
| 448 |
-
return OS === "mac"
|
| 449 |
-
? `mjpython -m mujoco.viewer --mjcf=${xml}`
|
| 450 |
-
: `python -m mujoco.viewer --mjcf=${xml}`;
|
| 451 |
-
if (env === "blender")
|
| 452 |
-
return OS === "mac" ? `open -a Blender ${glb}` : `blender ${glb}`;
|
| 453 |
-
if (env === "usd")
|
| 454 |
-
return `usdview ${usda}`;
|
| 455 |
-
return `"$ISAAC_SIM/python.sh" - <<PY\n` + // isaac
|
| 456 |
-
`from isaacsim import SimulationApp\n` +
|
| 457 |
-
`app = SimulationApp({"open_usd": "$PWD/${usda}"})\n` +
|
| 458 |
-
`while app.is_running():\n app.update()\nPY`;
|
| 459 |
}
|
| 460 |
|
| 461 |
-
function
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
const
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
:
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
}
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
const id = item.id;
|
| 487 |
-
const usda = assetPath(cls, item, `${id}.usda`);
|
| 488 |
-
const xml = assetPath(cls, item, `mjcf/${id}.xml`);
|
| 489 |
-
const glb = assetPath(cls, item, `${id}.glb`);
|
| 490 |
-
const dl = (patterns) =>
|
| 491 |
-
`python - <<PY\n` +
|
| 492 |
-
`from huggingface_hub import snapshot_download\n` +
|
| 493 |
-
`snapshot_download("projectsim/${cls.dataset}", repo_type="dataset",\n` +
|
| 494 |
-
` allow_patterns=[${patterns.map(p=>`"assets/${cls.class}/${id}/${p}"`).join(", ")}], local_dir=".")\n` +
|
| 495 |
-
`PY`;
|
| 496 |
-
return `# Paste in a macOS/Linux terminal — finds your installed engine.\n` +
|
| 497 |
-
`mkdir -p projectsim-assets && cd projectsim-assets\n` +
|
| 498 |
-
`pip install -q huggingface_hub\n\n` + auth(cls) +
|
| 499 |
-
`if [ -n "$ISAAC_SIM" ] && [ -x "$ISAAC_SIM/python.sh" ]; then\n` +
|
| 500 |
-
` echo "→ Isaac Sim"\n` +
|
| 501 |
-
` ${dl([`${id}.usda`, "textures/**"])}\n` +
|
| 502 |
-
` "$ISAAC_SIM/python.sh" - <<PY\n` +
|
| 503 |
-
`from isaacsim import SimulationApp\n` +
|
| 504 |
-
`app = SimulationApp({"open_usd": "$PWD/${usda}"})\n` +
|
| 505 |
-
`while app.is_running(): app.update()\nPY\n` +
|
| 506 |
-
`elif command -v mjpython >/dev/null 2>&1 || python -c "import mujoco" 2>/dev/null; then\n` +
|
| 507 |
-
` echo "→ MuJoCo"\n` +
|
| 508 |
-
` ${dl(["mjcf/**"])}\n` +
|
| 509 |
-
` { command -v mjpython >/dev/null 2>&1 && mjpython -m mujoco.viewer --mjcf=${xml}; } \\\n` +
|
| 510 |
-
` || python -m mujoco.viewer --mjcf=${xml}\n` +
|
| 511 |
-
`elif command -v usdview >/dev/null 2>&1; then\n` +
|
| 512 |
-
` echo "→ usdview"\n` +
|
| 513 |
-
` ${dl([`${id}.usda`, "textures/**"])}\n` +
|
| 514 |
-
` usdview ${usda}\n` +
|
| 515 |
-
`elif command -v blender >/dev/null 2>&1 || [ -d /Applications/Blender.app ]; then\n` +
|
| 516 |
-
` echo "→ Blender"\n` +
|
| 517 |
-
` ${dl([`${id}.glb`])}\n` +
|
| 518 |
-
` { command -v blender >/dev/null 2>&1 && blender ${glb}; } || open -a Blender ${glb}\n` +
|
| 519 |
-
`else\n` +
|
| 520 |
-
` echo "No engine found — easiest is: pip install mujoco (then re-run)."\n` +
|
| 521 |
-
`fi`;
|
| 522 |
-
}
|
| 523 |
-
|
| 524 |
-
function agentFor(cls, item) {
|
| 525 |
-
return `Download the projectsim asset "${item.id}" — a sim-ready articulated ` +
|
| 526 |
-
`${classLabel(cls)} (${item.mass_kg.toFixed(1)} kg, ` +
|
| 527 |
-
`${item.joints.length} joints) — and open it in whichever simulator is ` +
|
| 528 |
-
`installed here (Isaac Sim, MuJoCo, usdview, or Blender). Detect what's ` +
|
| 529 |
-
`available, download the matching file, and open it; if none are installed, ` +
|
| 530 |
-
"`pip install mujoco` and use that.\n\n" +
|
| 531 |
-
`A script that does exactly this — run it or adapt it:\n\n` +
|
| 532 |
-
autoScript(cls, item);
|
| 533 |
}
|
| 534 |
|
| 535 |
function renderCmd() {
|
| 536 |
-
if (!curItem) return;
|
| 537 |
-
|
| 538 |
-
const
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
document.getElementById("
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
out.textContent = agentFor(curCls, curItem);
|
| 547 |
-
sub.textContent = "Paste into Claude Code, Cursor, or any coding agent.";
|
| 548 |
-
sub.classList.remove("na");
|
| 549 |
-
} else {
|
| 550 |
-
const e = envById(curEnv), na = naOn(e);
|
| 551 |
-
document.getElementById("fmt").textContent = e.fmt;
|
| 552 |
-
out.textContent = cmdFor(curEnv, curCls, curItem);
|
| 553 |
-
sub.textContent = (na ? `Not on ${OS_LABEL} · ` : "") + e.prereq;
|
| 554 |
-
sub.classList.toggle("na", na);
|
| 555 |
-
}
|
| 556 |
}
|
| 557 |
|
| 558 |
-
(function initEnvTabs() {
|
| 559 |
-
const box = document.getElementById("envtabs");
|
| 560 |
-
ENVS.forEach(e => {
|
| 561 |
-
const b = document.createElement("button");
|
| 562 |
-
const na = naOn(e);
|
| 563 |
-
b.className = "envtab" + (e.id === curEnv ? " active" : "") + (na ? " na" : "");
|
| 564 |
-
b.dataset.env = e.id;
|
| 565 |
-
b.innerHTML = e.label + (na ? ` <span class="tag">Linux/Win</span>` : "");
|
| 566 |
-
b.onclick = () => {
|
| 567 |
-
curEnv = e.id;
|
| 568 |
-
try { localStorage.setItem("ps_env", curEnv); } catch (err) {}
|
| 569 |
-
box.querySelectorAll(".envtab").forEach(x =>
|
| 570 |
-
x.classList.toggle("active", x.dataset.env === curEnv));
|
| 571 |
-
renderCmd();
|
| 572 |
-
};
|
| 573 |
-
box.appendChild(b);
|
| 574 |
-
});
|
| 575 |
-
})();
|
| 576 |
-
|
| 577 |
-
(function initModeTabs() {
|
| 578 |
-
const box = document.getElementById("modetabs");
|
| 579 |
-
box.querySelectorAll(".modetab").forEach(b => {
|
| 580 |
-
b.classList.toggle("active", b.dataset.mode === curMode);
|
| 581 |
-
b.onclick = () => {
|
| 582 |
-
curMode = b.dataset.mode;
|
| 583 |
-
try { localStorage.setItem("ps_mode", curMode); } catch (err) {}
|
| 584 |
-
box.querySelectorAll(".modetab").forEach(x =>
|
| 585 |
-
x.classList.toggle("active", x.dataset.mode === curMode));
|
| 586 |
-
renderCmd();
|
| 587 |
-
};
|
| 588 |
-
});
|
| 589 |
-
})();
|
| 590 |
-
|
| 591 |
// ---- load one item -----------------------------------------------------------
|
| 592 |
async function loadItem(cls, item) {
|
| 593 |
clearModel();
|
|
@@ -762,6 +601,7 @@ fetch("classes.json")
|
|
| 762 |
history.replaceState(null, "", `?${q}`);
|
| 763 |
}
|
| 764 |
document.body.classList.toggle("class-static", !!cls.static);
|
|
|
|
| 765 |
list.innerHTML = "";
|
| 766 |
cls.items.forEach((item, i) => {
|
| 767 |
const btn = document.createElement("button");
|
|
|
|
| 94 |
#panel .copy:hover { background: var(--accent); }
|
| 95 |
#panel .copy:active { transform: scale(.95); }
|
| 96 |
#panel .copy.ok { background: var(--success); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
#panel .hdr { display: flex; align-items: baseline; gap: 8px; margin-top: 14px; }
|
| 98 |
#panel .hdr h2 { margin: 0; }
|
| 99 |
#panel .fmt { margin-left: auto; font: 500 10px var(--code); color: var(--body); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
#panel .codewrap { position: relative; }
|
| 101 |
#panel .codewrap pre { margin: 0; padding-right: 36px; }
|
| 102 |
#panel .copybtn { position: absolute; top: 7px; right: 7px; width: 26px;
|
|
|
|
| 111 |
#panel .copybtn.ok .i-copy { display: none; }
|
| 112 |
#panel .copybtn.ok .i-ok { display: inline; }
|
| 113 |
[hidden] { display: none !important; }
|
| 114 |
+
#apibanner { display: flex; align-items: center; justify-content: center;
|
| 115 |
+
gap: 10px; padding: 7px 16px; flex-wrap: wrap;
|
| 116 |
+
background: linear-gradient(90deg, var(--accent), var(--accent-strong));
|
| 117 |
+
color: #fff; font: 500 13px var(--sans); text-align: center; }
|
| 118 |
+
#apibanner b { font-weight: 700; }
|
| 119 |
+
#apibanner a { padding: 3px 12px; border-radius: 999px; background: #fff;
|
| 120 |
+
color: var(--accent-strong); font: 700 12px var(--sans);
|
| 121 |
+
text-decoration: none; white-space: nowrap;
|
| 122 |
+
transition: background .15s; }
|
| 123 |
+
#apibanner a:hover { background: var(--accent-soft); }
|
| 124 |
+
#panel .apinote { margin: 6px 0 8px; font: 500 10.5px var(--sans); color: var(--body); }
|
| 125 |
+
#panel .apilinks a { display: block; margin: 6px 0; padding: 7px 12px;
|
| 126 |
+
border-radius: 999px; background: var(--accent-strong);
|
| 127 |
+
color: #fff; text-decoration: none; text-align: center;
|
| 128 |
+
font: 600 12px var(--sans); transition: background .15s; }
|
| 129 |
+
#panel .apilinks a:hover { background: var(--accent); }
|
| 130 |
+
#panel .apilinks a.ghost { background: var(--subtle); color: var(--ink); }
|
| 131 |
+
#panel .apilinks a.ghost:hover { background: var(--page); }
|
| 132 |
</style>
|
| 133 |
<style id="scene-loader-style">
|
| 134 |
#scene-loader { position: absolute; inset: 0; display: none;
|
|
|
|
| 143 |
<style id="scene-links-style">
|
| 144 |
body.class-static #panel .row,
|
| 145 |
body.class-static #panel #joints,
|
| 146 |
+
body.class-scene #panel #apiblock { display: none !important; }
|
| 147 |
+
body:not(.class-scene) #scene-links { display: none; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
#scene-links { padding: 4px 0; }
|
| 149 |
#scene-links h2 { margin: 12px 0 6px; font-size: 15px; }
|
| 150 |
#scene-links a { display: block; padding: 8px 12px; margin: 4px 0;
|
|
|
|
| 156 |
</head>
|
| 157 |
<body>
|
| 158 |
<div id="scene-loader"><div class="spin"></div><span id="scene-loader-text">loading…</span></div>
|
| 159 |
+
<div id="apibanner">
|
| 160 |
+
<span>Every asset in this lab was generated by the <b>projectsim API</b> — make a set of your own.</span>
|
| 161 |
+
<a href="https://docs.projectsim.ai/getting-started/quickstart?utm_source=hf_space&utm_medium=banner&utm_content=lab"
|
| 162 |
+
target="_blank" rel="noopener">Generate your own →</a>
|
| 163 |
+
</div>
|
| 164 |
<header>
|
| 165 |
<h1>projectsim Lab</h1>
|
| 166 |
<div id="tabs"></div>
|
| 167 |
<nav class="links">
|
| 168 |
<a id="dslink" href="https://huggingface.co/projectsim"
|
| 169 |
target="_blank" rel="noopener">Dataset</a>
|
| 170 |
+
<a href="https://docs.projectsim.ai/?utm_source=hf_space&utm_medium=header&utm_content=lab"
|
| 171 |
+
target="_blank" rel="noopener">API docs</a>
|
| 172 |
</nav>
|
| 173 |
</header>
|
| 174 |
<main>
|
|
|
|
| 183 |
<button id="anim" class="ghost">▶ Loop</button>
|
| 184 |
</div>
|
| 185 |
<div id="joints"></div>
|
| 186 |
+
<div id="apiblock">
|
| 187 |
+
<div class="hdr"><h2>Take it home</h2><span class="fmt">projectsim API</span></div>
|
| 188 |
+
<div class="meta">Generate a set like this one with the projectsim API:</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
<div class="codewrap">
|
| 190 |
<pre id="output"></pre>
|
| 191 |
<button class="copybtn" id="copy" title="Copy" aria-label="Copy">
|
|
|
|
| 198 |
stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
|
| 199 |
</button>
|
| 200 |
</div>
|
| 201 |
+
<div class="apinote">USD output · 10–500 variants per call · 1 credit per variant</div>
|
| 202 |
+
<div class="apilinks">
|
| 203 |
+
<a id="api-token" href="https://dashboard.projectsim.ai/?utm_source=hf_space&utm_medium=take_it_home&utm_content=lab" target="_blank" rel="noopener">Get an API token →</a>
|
| 204 |
+
<a id="api-docs" href="https://docs.projectsim.ai/creating-assets/procedural-variation-set?utm_source=hf_space&utm_medium=take_it_home&utm_content=lab" target="_blank" rel="noopener">Read the API docs →</a>
|
| 205 |
+
<a id="api-files" class="ghost" href="https://huggingface.co/projectsim" target="_blank" rel="noopener">Or download this asset from the dataset →</a>
|
| 206 |
+
</div>
|
| 207 |
+
</div>
|
| 208 |
<div id="scene-links">
|
| 209 |
<h2>Take it home</h2>
|
| 210 |
<a id="scene-usd-link" href="#" target="_blank" rel="noopener">Full USD (with Gaussian splat) on Hugging Face →</a>
|
| 211 |
<a id="scene-glb-link" href="#" target="_blank" rel="noopener">This scene as GLB →</a>
|
| 212 |
<a id="scene-dataset-link" href="https://huggingface.co/datasets/projectsim/scenes-and-worlds" target="_blank" rel="noopener">Browse the scenes-and-worlds dataset →</a>
|
| 213 |
+
<a id="scene-request-link" href="https://www.projectsim.ai/access?dataset=scenes-and-worlds&utm_source=hf_space&utm_medium=take_it_home&utm_content=scenes" target="_blank" rel="noopener">Need scenes at scale? Request a larger dataset →</a>
|
| 214 |
<div class="note" id="scene-note">USD ships the full room including the Gaussian splat backdrop; the GLB is a lightweight preview (splat dropped, textures baked).</div>
|
| 215 |
</div>
|
| 216 |
</aside>
|
|
|
|
| 238 |
? `${LOCAL_BASE}/${ds}/${path}`
|
| 239 |
: GATED.has(ds) ? `${PREVIEW}/${ds}/${path}`
|
| 240 |
: `${HFDS}/${ds}/resolve/main/${path}`;
|
|
|
|
|
|
|
| 241 |
const stage = document.getElementById("stage");
|
| 242 |
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
| 243 |
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
|
|
|
|
| 369 |
renderer.domElement.style.cursor = ""; }
|
| 370 |
});
|
| 371 |
|
| 372 |
+
// ---- Take-it-home: the projectsim API call that generates a set like this ----
|
| 373 |
+
// Every tab's items came out of the procedural pipeline, so "take it home" is the
|
| 374 |
+
// POST /v1/variation-sets/procedural call that makes more of them. Seeds live in
|
| 375 |
+
// classes.json (api.seed, or api.seeds keyed by an item-id substring when one tab
|
| 376 |
+
// mixes object types). Scenes have no endpoint yet and keep their file links.
|
| 377 |
+
const API = "https://api.projectsim.ai";
|
| 378 |
+
const utm = (medium, content) =>
|
| 379 |
+
`utm_source=hf_space&utm_medium=${medium}&utm_content=${encodeURIComponent(content)}`;
|
|
|
|
|
|
|
|
|
|
| 380 |
let curCls = null, curItem = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
|
| 382 |
+
function seedFor(cls, item) {
|
| 383 |
+
const a = cls.api;
|
| 384 |
+
if (!a) return null;
|
| 385 |
+
if (a.seed) return a.seed;
|
| 386 |
+
const k = Object.keys(a.seeds || {}).find(k => item.id.includes(k));
|
| 387 |
+
return k ? a.seeds[k] : null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
}
|
| 389 |
|
| 390 |
+
function apiFor(cls, item) {
|
| 391 |
+
const seed = seedFor(cls, item);
|
| 392 |
+
if (!seed) return null;
|
| 393 |
+
// Same shape as robotics-server's CreateProceduralVariationSetRequest: only
|
| 394 |
+
// what changes the result. `params.physics.articulated` defaults to true
|
| 395 |
+
// (body rigid, simulator isaac_sim_physx, collision auto), so it's sent
|
| 396 |
+
// only for jointless props.
|
| 397 |
+
const body = { name: cls.class.replace(/_/g, " "), count: 10, seed: { text: seed } };
|
| 398 |
+
if (!cls.api.articulated) body.params = { physics: { articulated: false } };
|
| 399 |
+
// the body sits in a single-quoted shell string: escape any ' in it
|
| 400 |
+
const json = JSON.stringify(body, null, 2).replace(/'/g, `'\\''`);
|
| 401 |
+
return `# export PROJECTSIM_TOKEN=kps_live_... (from dashboard.projectsim.ai)\n\n` +
|
| 402 |
+
`# 1. order 10 variants like this one (10 credits)\n` +
|
| 403 |
+
`curl -X POST "${API}/v1/variation-sets/procedural" \\\n` +
|
| 404 |
+
` -H "Authorization: Bearer $PROJECTSIM_TOKEN" \\\n` +
|
| 405 |
+
` -H "Content-Type: application/json" \\\n` +
|
| 406 |
+
` -H "Idempotency-Key: $(uuidgen)" \\\n` +
|
| 407 |
+
` -d '${json}'\n\n` +
|
| 408 |
+
`# 2. poll the job_id from the response until "complete"\n` +
|
| 409 |
+
`curl -H "Authorization: Bearer $PROJECTSIM_TOKEN" \\\n` +
|
| 410 |
+
` "${API}/v1/jobs/$JOB_ID"\n\n` +
|
| 411 |
+
`# 3. list the set (its id is in step 1's response); each\n` +
|
| 412 |
+
`# files[].url is a presigned USD download\n` +
|
| 413 |
+
`curl -H "Authorization: Bearer $PROJECTSIM_TOKEN" \\\n` +
|
| 414 |
+
` "${API}/v1/variation-sets/$SET_ID/assets?limit=100"`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
}
|
| 416 |
|
| 417 |
function renderCmd() {
|
| 418 |
+
if (!curItem || !curCls) return;
|
| 419 |
+
document.getElementById("output").textContent = apiFor(curCls, curItem) || "";
|
| 420 |
+
const c = curCls.class;
|
| 421 |
+
document.getElementById("api-token").href =
|
| 422 |
+
`https://dashboard.projectsim.ai/?${utm("take_it_home", c)}`;
|
| 423 |
+
document.getElementById("api-docs").href =
|
| 424 |
+
`https://docs.projectsim.ai/creating-assets/procedural-variation-set?${utm("take_it_home", c)}`;
|
| 425 |
+
const dir = curItem.glb ? curItem.glb.replace(/\/[^/]*$/, "")
|
| 426 |
+
: (curCls.items_dir || `assets/${c}`);
|
| 427 |
+
document.getElementById("api-files").href = `${HFDS}/${curCls.dataset}/tree/main/${dir}`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
}
|
| 429 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
// ---- load one item -----------------------------------------------------------
|
| 431 |
async function loadItem(cls, item) {
|
| 432 |
clearModel();
|
|
|
|
| 601 |
history.replaceState(null, "", `?${q}`);
|
| 602 |
}
|
| 603 |
document.body.classList.toggle("class-static", !!cls.static);
|
| 604 |
+
document.body.classList.toggle("class-scene", !!cls.scene);
|
| 605 |
list.innerHTML = "";
|
| 606 |
cls.items.forEach((item, i) => {
|
| 607 |
const btn = document.createElement("button");
|