funnel: API banner, clearer 'Create your own via API', API snippet, UTMs, team@projectsim.ai

#3
by volodymyrsvirhun - opened
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-static #panel .hdr,
159
- body.class-static #panel .modetabs,
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" target="_blank"
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 class="hdr"><h2>Take it home</h2><span class="fmt" id="fmt"></span></div>
199
- <div class="modetabs" id="modetabs">
200
- <button class="modetab active" data-mode="manual">Manual commands</button>
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: OS-aware engine selector + agent prompt ------------------
384
- function detectOS() {
385
- const p = ((navigator.userAgentData && navigator.userAgentData.platform) ||
386
- navigator.platform || navigator.userAgent || "").toLowerCase();
387
- if (p.includes("mac")) return "mac";
388
- if (p.includes("win")) return "win";
389
- return "linux";
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
- // the open step, aware of OS (macOS needs mjpython; Blender launcher differs)
442
- function openCmd(env, cls, item) {
443
- const id = item.id;
444
- const usda = assetPath(cls, item, `${id}.usda`);
445
- const xml = assetPath(cls, item, `mjcf/${id}.xml`);
446
- const glb = assetPath(cls, item, `${id}.glb`);
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 cmdFor(env, cls, item) {
462
- if (env === "auto") return autoScript(cls, item);
463
- const head = `mkdir -p projectsim-assets && cd projectsim-assets\npip install -q huggingface_hub\n\n` + auth(cls);
464
- const naNote = naOn(envById(env))
465
- ? `# ${envById(env).label} doesn't run on ${OS_LABEL} — run this on a\n` +
466
- `# Linux/Windows box with an NVIDIA RTX GPU (or a cloud instance).\n\n`
467
- : "";
468
- const patterns = {
469
- isaac: [`${item.id}.usda`, "textures/**"],
470
- usd: [`${item.id}.usda`, "textures/**"],
471
- mujoco: ["mjcf/**"],
472
- blender: [`${item.id}.glb`],
473
- }[env];
474
- const preOpen = env === "isaac"
475
- ? `\n# open the play-ready scene in Isaac Sim, then press Play\n` +
476
- `# (set ISAAC_SIM to your install, e.g. ~/isaacsim)\n`
477
- : env === "mujoco"
478
- ? `\n# self-contained model (visuals + colliders + joints + latches)\n`
479
- : "\n";
480
- return naNote + head + hfDl(cls, item, patterns) + preOpen + openCmd(env, cls, item);
481
- }
482
-
483
- // The browser can't read your disk — so this script does the detection itself,
484
- // at run time, and launches whichever engine you actually have installed.
485
- function autoScript(cls, item) {
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
- const agent = curMode === "agent";
538
- const out = document.getElementById("output");
539
- const sub = document.getElementById("submeta");
540
- document.getElementById("envtabs").hidden = agent; // software picker: manual only
541
- document.getElementById("fmt").hidden = agent;
542
- document.getElementById("takeMeta").textContent = agent
543
- ? "The agent detects your setup and opens it wherever it can:"
544
- : "Same asset, full-res + real physics — pick your engine:";
545
- if (agent) {
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");