volodymyrsvirhun commited on
Commit
f0369c0
Β·
verified Β·
1 Parent(s): 6cfcbc8

Take-it-home: engine selector (Isaac/MuJoCo/usdview/Blender) + OS-aware commands, auto-detect, and agent prompt

Browse files
Files changed (1) hide show
  1. index.html +276 -35
index.html CHANGED
@@ -94,6 +94,53 @@
94
  #panel .copy:hover { background: var(--accent); }
95
  #panel .copy:active { transform: scale(.95); }
96
  #panel .copy.ok { background: var(--success); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  </style>
98
  </head>
99
  <body>
@@ -119,10 +166,26 @@
119
  <button id="anim" class="ghost">β–Ά Loop</button>
120
  </div>
121
  <div id="joints"></div>
122
- <h2 style="margin-top:14px">Take it home</h2>
123
- <div class="meta">Same asset, full 4K, real physics:</div>
124
- <pre id="oneliner"></pre>
125
- <button class="copy" id="copy">Copy commands</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  </aside>
127
  </main>
128
  <script type="importmap">
@@ -271,6 +334,189 @@ addEventListener("pointerup", () => {
271
  renderer.domElement.style.cursor = ""; }
272
  });
273
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  // ---- load one item -----------------------------------------------------------
275
  async function loadItem(cls, item) {
276
  clearModel();
@@ -281,12 +527,8 @@ async function loadItem(cls, item) {
281
  `${d[0].toFixed(2)} Γ— ${d[1].toFixed(2)} Γ— ${d[2].toFixed(2)} m Β· ` +
282
  `${item.mass_kg.toFixed(1)} kg Β· ${item.tris} tris Β· ` +
283
  `${item.joints.length} joints`;
284
- document.getElementById("oneliner").textContent =
285
- `mkdir -p "$PWD/projectsim-assets"\ncd "$PWD/projectsim-assets"\n\n` +
286
- `curl --fail --location --progress-bar \\\n` +
287
- ` "${HFDS}/${cls.dataset}/resolve/main/${cls.items_dir}/${item.id}/` +
288
- `${item.id}.blend?download=true" \\\n --output ${item.id}.blend\n\n` +
289
- `open -a Blender ${item.id}.blend`;
290
  const gltf = await loader.loadAsync(dsUrl(cls.dataset, item.glb));
291
  if (gen !== loadGen) return; // user already switched away β€” drop this load
292
  model = new THREE.Group();
@@ -351,31 +593,30 @@ document.getElementById("closeall").onclick = () => {
351
  rig.forEach(r => setJoint(r, r.lo));
352
  };
353
  document.getElementById("anim").onclick = () => { animating = !animating; };
354
- document.getElementById("copy").onclick = async (e) => {
355
- const btn = e.currentTarget;
356
- const text = document.getElementById("oneliner").textContent;
357
- let ok = false;
358
- try {
359
- await navigator.clipboard.writeText(text);
360
- ok = true;
361
- } catch (err) {
362
- // clipboard API blocked (embedded iframe / permission): legacy fallback
363
- const ta = document.createElement("textarea");
364
- ta.value = text;
365
- ta.style.position = "fixed"; ta.style.opacity = "0";
366
- document.body.appendChild(ta);
367
- ta.select();
368
- try { ok = document.execCommand("copy"); } catch (e2) {}
369
- ta.remove();
370
- }
371
- btn.textContent = ok ? "Copied βœ“" : "Copy failed";
372
- btn.classList.toggle("ok", ok);
373
- clearTimeout(btn._t);
374
- btn._t = setTimeout(() => {
375
- btn.textContent = "Copy commands";
376
- btn.classList.remove("ok");
377
- }, 1600);
378
- };
379
 
380
  const smooth = t => t * t * (3 - 2 * t);
381
  function tick() {
 
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;
133
+ height: 26px; padding: 0; border: 0; border-radius: 8px;
134
+ display: inline-flex; align-items: center; justify-content: center;
135
+ cursor: pointer; background: rgba(255,255,255,.10);
136
+ color: var(--accent-soft); transition: background .15s, color .15s; }
137
+ #panel .copybtn:hover { background: rgba(255,255,255,.22); color: #fff; }
138
+ #panel .copybtn:active { transform: scale(.92); }
139
+ #panel .copybtn .i-ok { display: none; }
140
+ #panel .copybtn.ok { color: var(--success); background: rgba(255,255,255,.16); }
141
+ #panel .copybtn.ok .i-copy { display: none; }
142
+ #panel .copybtn.ok .i-ok { display: inline; }
143
+ [hidden] { display: none !important; }
144
  </style>
145
  </head>
146
  <body>
 
166
  <button id="anim" class="ghost">β–Ά Loop</button>
167
  </div>
168
  <div id="joints"></div>
169
+ <div class="hdr"><h2>Take it home</h2><span class="fmt" id="fmt"></span></div>
170
+ <div class="modetabs" id="modetabs">
171
+ <button class="modetab active" data-mode="manual">Manual commands</button>
172
+ <button class="modetab" data-mode="agent">Agent prompt</button>
173
+ </div>
174
+ <div class="meta" id="takeMeta"></div>
175
+ <div class="envtabs" id="envtabs"></div>
176
+ <div class="prereq" id="submeta"></div>
177
+ <div class="codewrap">
178
+ <pre id="output"></pre>
179
+ <button class="copybtn" id="copy" title="Copy" aria-label="Copy">
180
+ <svg class="i-copy" viewBox="0 0 24 24" width="14" height="14" fill="none"
181
+ stroke="currentColor" stroke-width="2" stroke-linecap="round"
182
+ stroke-linejoin="round"><rect x="9" y="9" width="12" height="12" rx="2"/><path
183
+ d="M6 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1"/></svg>
184
+ <svg class="i-ok" viewBox="0 0 24 24" width="15" height="15" fill="none"
185
+ stroke="currentColor" stroke-width="2.5" stroke-linecap="round"
186
+ stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
187
+ </button>
188
+ </div>
189
  </aside>
190
  </main>
191
  <script type="importmap">
 
334
  renderer.domElement.style.cursor = ""; }
335
  });
336
 
337
+ // ---- Take-it-home: OS-aware engine selector + agent prompt ------------------
338
+ function detectOS() {
339
+ const p = ((navigator.userAgentData && navigator.userAgentData.platform) ||
340
+ navigator.platform || navigator.userAgent || "").toLowerCase();
341
+ if (p.includes("mac")) return "mac";
342
+ if (p.includes("win")) return "win";
343
+ return "linux";
344
+ }
345
+ const OS = detectOS();
346
+ const OS_LABEL = { mac: "macOS", win: "Windows", linux: "Linux" }[OS];
347
+
348
+ let curCls = null, curItem = null;
349
+ let curEnv = (() => { try { return localStorage.getItem("ps_env"); }
350
+ catch (e) { return null; } })();
351
+ let curMode = (() => { try { return localStorage.getItem("ps_mode"); }
352
+ catch (e) { return null; } })() || "manual";
353
+
354
+ // software options for MANUAL mode. "auto" self-detects at run time; os =
355
+ // platforms each runs on natively (used to flag Isaac on macOS).
356
+ const ENVS = [
357
+ { id: "auto", label: "Auto-detect", fmt: "detects engine",
358
+ os: ["mac", "win", "linux"],
359
+ prereq: "Runs whichever engine you have installed (detected at run time)." },
360
+ { id: "isaac", label: "Isaac Sim", fmt: "USD",
361
+ os: ["win", "linux"],
362
+ prereq: "Isaac Sim β€” Linux/Windows + an NVIDIA RTX GPU." },
363
+ { id: "mujoco", label: "MuJoCo", fmt: "MJCF",
364
+ os: ["mac", "win", "linux"],
365
+ prereq: "pip install mujoco Β· any OS." },
366
+ { id: "usd", label: "usdview / Omniverse", fmt: "USD",
367
+ os: ["mac", "win", "linux"],
368
+ prereq: "usdview (pip install usd-core) β€” Omniverse needs Linux/Win + RTX." },
369
+ { id: "blender", label: "Blender", fmt: ".blend",
370
+ os: ["mac", "win", "linux"],
371
+ prereq: "Blender β€” free at blender.org/download." },
372
+ ];
373
+ const envById = id => ENVS.find(e => e.id === id);
374
+ const naOn = e => e.id !== "auto" && !e.os.includes(OS);
375
+ if (!envById(curEnv)) curEnv = "auto"; // default: self-detect
376
+
377
+ const resUrl = (cls, item, file) =>
378
+ `${HFDS}/${cls.dataset}/resolve/main/${cls.items_dir}/${item.id}/${file}`;
379
+ const classLabel = cls =>
380
+ cls.dataset.replace(/^articulated-/, "")
381
+ .replace(/ies$/, "y").replace(/([sxz]|ch|sh)es$/, "$1").replace(/s$/, "");
382
+
383
+ // the open step, aware of OS (macOS needs mjpython; Blender launcher differs)
384
+ function openCmd(env, id) {
385
+ if (env === "mujoco")
386
+ return OS === "mac"
387
+ ? `mjpython -m mujoco.viewer --mjcf=${id}.xml` // macOS: mjpython, not python
388
+ : `python -m mujoco.viewer --mjcf=${id}.xml`;
389
+ if (env === "blender")
390
+ return OS === "mac" ? `open -a Blender ${id}.blend` : `blender ${id}.blend`;
391
+ if (env === "usd")
392
+ return `usdview ${id}/${id}_scene.usda`;
393
+ return `"$ISAAC_SIM/python.sh" - <<PY\n` + // isaac
394
+ `from isaacsim import SimulationApp\n` +
395
+ `app = SimulationApp({"open_usd": "$PWD/${id}/${id}_scene.usda"})\n` +
396
+ `while app.is_running():\n app.update()\nPY`;
397
+ }
398
+
399
+ function cmdFor(env, cls, item) {
400
+ const id = item.id;
401
+ if (env === "auto") return autoScript(cls, item);
402
+ const head = `mkdir -p projectsim-assets && cd projectsim-assets\n\n`;
403
+ const grab = (file) =>
404
+ `curl --fail --location --progress-bar \\\n` +
405
+ ` "${resUrl(cls, item, file)}?download=true" \\\n` +
406
+ ` --output ${file}\n`;
407
+ const naNote = naOn(envById(env))
408
+ ? `# ${envById(env).label} doesn't run on ${OS_LABEL} β€” run this on a\n` +
409
+ `# Linux/Windows box with an NVIDIA RTX GPU (or a cloud instance).\n\n`
410
+ : "";
411
+ if (env === "blender")
412
+ return naNote + head + grab(`${id}.blend`) + `\n${openCmd(env, id)}`;
413
+ if (env === "mujoco")
414
+ return naNote + head + grab(`${id}.xml`) +
415
+ `\n# self-contained model (colliders + joints + a floor) β€” press Play\n` +
416
+ openCmd(env, id);
417
+ const tail = env === "isaac"
418
+ ? `\n# open the play-ready scene in Isaac Sim, then press Play\n` +
419
+ `# (set ISAAC_SIM to your install, e.g. ~/isaacsim)\n${openCmd(env, id)}`
420
+ : `\n${openCmd(env, id)}`;
421
+ return naNote + head + grab(`${id}_usd.zip`) + `unzip -o ${id}_usd.zip\n` + tail;
422
+ }
423
+
424
+ // The browser can't read your disk β€” so this script does the detection itself,
425
+ // at run time, and launches whichever engine you actually have installed.
426
+ function autoScript(cls, item) {
427
+ const id = item.id;
428
+ const u = file => `${resUrl(cls, item, file)}?download=true`;
429
+ return `# Paste in a macOS/Linux terminal β€” finds your installed engine.\n` +
430
+ `mkdir -p projectsim-assets && cd projectsim-assets\n` +
431
+ `get(){ curl --fail -L --progress-bar "$1" -o "$2"; }\n\n` +
432
+ `if [ -n "$ISAAC_SIM" ] && [ -x "$ISAAC_SIM/python.sh" ]; then\n` +
433
+ ` echo "β†’ Isaac Sim"; get "${u(id + '_usd.zip')}" ${id}_usd.zip; unzip -o ${id}_usd.zip\n` +
434
+ ` "$ISAAC_SIM/python.sh" - <<PY\n` +
435
+ `from isaacsim import SimulationApp\n` +
436
+ `app = SimulationApp({"open_usd": "$PWD/${id}/${id}_scene.usda"})\n` +
437
+ `while app.is_running(): app.update()\nPY\n` +
438
+ `elif command -v mjpython >/dev/null 2>&1 || python -c "import mujoco" 2>/dev/null; then\n` +
439
+ ` echo "β†’ MuJoCo"; get "${u(id + '.xml')}" ${id}.xml\n` +
440
+ ` { command -v mjpython >/dev/null 2>&1 && mjpython -m mujoco.viewer --mjcf=${id}.xml; } \\\n` +
441
+ ` || python -m mujoco.viewer --mjcf=${id}.xml\n` +
442
+ `elif command -v usdview >/dev/null 2>&1; then\n` +
443
+ ` echo "β†’ usdview"; get "${u(id + '_usd.zip')}" ${id}_usd.zip; unzip -o ${id}_usd.zip\n` +
444
+ ` usdview ${id}/${id}_scene.usda\n` +
445
+ `elif command -v blender >/dev/null 2>&1 || [ -d /Applications/Blender.app ]; then\n` +
446
+ ` echo "β†’ Blender"; get "${u(id + '.blend')}" ${id}.blend\n` +
447
+ ` { command -v blender >/dev/null 2>&1 && blender ${id}.blend; } || open -a Blender ${id}.blend\n` +
448
+ `else\n` +
449
+ ` echo "No engine found β€” easiest is: pip install mujoco (then re-run)."\n` +
450
+ `fi`;
451
+ }
452
+
453
+ function agentFor(cls, item) {
454
+ return `Download the ProjectSim asset "${item.id}" β€” a sim-ready articulated ` +
455
+ `${classLabel(cls)} (${item.mass_kg.toFixed(1)} kg, ` +
456
+ `${item.joints.length} joints) β€” and open it in whichever simulator is ` +
457
+ `installed here (Isaac Sim, MuJoCo, usdview, or Blender). Detect what's ` +
458
+ `available, download the matching file, and open it; if none are installed, ` +
459
+ "`pip install mujoco` and use that.\n\n" +
460
+ `A script that does exactly this β€” run it or adapt it:\n\n` +
461
+ autoScript(cls, item);
462
+ }
463
+
464
+ function renderCmd() {
465
+ if (!curItem) return;
466
+ const agent = curMode === "agent";
467
+ const out = document.getElementById("output");
468
+ const sub = document.getElementById("submeta");
469
+ document.getElementById("envtabs").hidden = agent; // software picker: manual only
470
+ document.getElementById("fmt").hidden = agent;
471
+ document.getElementById("takeMeta").textContent = agent
472
+ ? "The agent detects your setup and opens it wherever it can:"
473
+ : "Same asset, full-res + real physics β€” pick your engine:";
474
+ if (agent) {
475
+ out.textContent = agentFor(curCls, curItem);
476
+ sub.textContent = "Paste into Claude Code, Cursor, or any coding agent.";
477
+ sub.classList.remove("na");
478
+ } else {
479
+ const e = envById(curEnv), na = naOn(e);
480
+ document.getElementById("fmt").textContent = e.fmt;
481
+ out.textContent = cmdFor(curEnv, curCls, curItem);
482
+ sub.textContent = (na ? `Not on ${OS_LABEL} Β· ` : "") + e.prereq;
483
+ sub.classList.toggle("na", na);
484
+ }
485
+ }
486
+
487
+ (function initEnvTabs() {
488
+ const box = document.getElementById("envtabs");
489
+ ENVS.forEach(e => {
490
+ const b = document.createElement("button");
491
+ const na = naOn(e);
492
+ b.className = "envtab" + (e.id === curEnv ? " active" : "") + (na ? " na" : "");
493
+ b.dataset.env = e.id;
494
+ b.innerHTML = e.label + (na ? ` <span class="tag">Linux/Win</span>` : "");
495
+ b.onclick = () => {
496
+ curEnv = e.id;
497
+ try { localStorage.setItem("ps_env", curEnv); } catch (err) {}
498
+ box.querySelectorAll(".envtab").forEach(x =>
499
+ x.classList.toggle("active", x.dataset.env === curEnv));
500
+ renderCmd();
501
+ };
502
+ box.appendChild(b);
503
+ });
504
+ })();
505
+
506
+ (function initModeTabs() {
507
+ const box = document.getElementById("modetabs");
508
+ box.querySelectorAll(".modetab").forEach(b => {
509
+ b.classList.toggle("active", b.dataset.mode === curMode);
510
+ b.onclick = () => {
511
+ curMode = b.dataset.mode;
512
+ try { localStorage.setItem("ps_mode", curMode); } catch (err) {}
513
+ box.querySelectorAll(".modetab").forEach(x =>
514
+ x.classList.toggle("active", x.dataset.mode === curMode));
515
+ renderCmd();
516
+ };
517
+ });
518
+ })();
519
+
520
  // ---- load one item -----------------------------------------------------------
521
  async function loadItem(cls, item) {
522
  clearModel();
 
527
  `${d[0].toFixed(2)} Γ— ${d[1].toFixed(2)} Γ— ${d[2].toFixed(2)} m Β· ` +
528
  `${item.mass_kg.toFixed(1)} kg Β· ${item.tris} tris Β· ` +
529
  `${item.joints.length} joints`;
530
+ curCls = cls; curItem = item;
531
+ renderCmd();
 
 
 
 
532
  const gltf = await loader.loadAsync(dsUrl(cls.dataset, item.glb));
533
  if (gen !== loadGen) return; // user already switched away β€” drop this load
534
  model = new THREE.Group();
 
593
  rig.forEach(r => setJoint(r, r.lo));
594
  };
595
  document.getElementById("anim").onclick = () => { animating = !animating; };
596
+ (function initCopy() {
597
+ const btn = document.getElementById("copy");
598
+ btn.onclick = async () => {
599
+ const text = document.getElementById("output").textContent;
600
+ let ok = false;
601
+ try {
602
+ await navigator.clipboard.writeText(text);
603
+ ok = true;
604
+ } catch (err) {
605
+ // clipboard API blocked (embedded iframe / permission): legacy fallback
606
+ const ta = document.createElement("textarea");
607
+ ta.value = text;
608
+ ta.style.position = "fixed"; ta.style.opacity = "0";
609
+ document.body.appendChild(ta);
610
+ ta.select();
611
+ try { ok = document.execCommand("copy"); } catch (e2) {}
612
+ ta.remove();
613
+ }
614
+ btn.classList.toggle("ok", ok);
615
+ btn.title = ok ? "Copied" : "Copy failed";
616
+ clearTimeout(btn._t);
617
+ btn._t = setTimeout(() => { btn.classList.remove("ok"); btn.title = "Copy"; }, 1400);
618
+ };
619
+ })();
 
620
 
621
  const smooth = t => t * t * (3 - 2 * t);
622
  function tick() {