Spaces:
Running
Running
Take-it-home: engine selector (Isaac/MuJoCo/usdview/Blender) + OS-aware commands, auto-detect, and agent prompt
Browse files- 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 |
-
<
|
| 123 |
-
<div class="
|
| 124 |
-
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 285 |
-
|
| 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 |
-
|
| 355 |
-
const btn =
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
btn.
|
| 376 |
-
|
| 377 |
-
|
| 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() {
|