Spaces:
Running
Running
v0.1.0
Browse files- .gitattributes +1 -35
- README.md +16 -6
- classes.json +22 -0
- index.html +374 -17
.gitattributes
CHANGED
|
@@ -1,35 +1 @@
|
|
| 1 |
-
*.
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
*.glb filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -1,10 +1,20 @@
|
|
| 1 |
---
|
| 2 |
-
title: Procedural Lab
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
| 7 |
-
pinned:
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: ProjectSim Procedural Lab
|
| 3 |
+
emoji: 📦
|
| 4 |
+
colorFrom: gray
|
| 5 |
+
colorTo: yellow
|
| 6 |
sdk: static
|
| 7 |
+
pinned: true
|
| 8 |
+
license: mit
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# ProjectSim Procedural Lab
|
| 12 |
+
|
| 13 |
+
In-browser 3D playground for the Kaedim class datasets — boxes, desks, cages, wardrobes: pick a class tab, pick an item, orbit it,
|
| 14 |
+
and articulate every joint — grab a flap/door/drawer/wheel and drag it, or
|
| 15 |
+
use the sliders (revolute and prismatic, joint chains like wheels on swivel
|
| 16 |
+
casters). Web-optimized GLBs (1K textures) in `assets/<class>/`; the full
|
| 17 |
+
joint metadata in `catalog.json`. three.js from CDN — no build step, no
|
| 18 |
+
backend.
|
| 19 |
+
|
| 20 |
+
Code mirror: [github.com/Kaedim/robotics-procedural-lab](https://github.com/Kaedim/robotics-procedural-lab) · Code & loaders: [github.com/Kaedim/robotics-sim-ready](https://github.com/Kaedim/robotics-sim-ready)
|
classes.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"class": "boxes",
|
| 4 |
+
"label": "Boxes",
|
| 5 |
+
"dataset": "articulated-boxes"
|
| 6 |
+
},
|
| 7 |
+
{
|
| 8 |
+
"class": "desks",
|
| 9 |
+
"label": "Desks",
|
| 10 |
+
"dataset": "articulated-desks"
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
"class": "cages",
|
| 14 |
+
"label": "Cages",
|
| 15 |
+
"dataset": "articulated-logistics-cages"
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"class": "articulated-wardrobes",
|
| 19 |
+
"label": "Wardrobes",
|
| 20 |
+
"dataset": "articulated-wardrobes"
|
| 21 |
+
}
|
| 22 |
+
]
|
index.html
CHANGED
|
@@ -1,19 +1,376 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</html>
|
|
|
|
| 1 |
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>ProjectSim Procedural Lab</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root { color-scheme: light; }
|
| 9 |
+
* { box-sizing: border-box; }
|
| 10 |
+
body { margin: 0; font: 14px/1.5 -apple-system, "Segoe UI", sans-serif;
|
| 11 |
+
background: #ececef; color: #1c1c1e; height: 100vh; display: flex;
|
| 12 |
+
flex-direction: column; overflow: hidden; }
|
| 13 |
+
header { padding: 10px 20px; display: flex; align-items: center; gap: 16px;
|
| 14 |
+
background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.08); z-index: 2;
|
| 15 |
+
flex-wrap: wrap; }
|
| 16 |
+
header h1 { margin: 0; font-size: 17px; white-space: nowrap; }
|
| 17 |
+
#tabs { display: flex; gap: 6px; }
|
| 18 |
+
.tab { padding: 6px 14px; border: 1px solid #ddd; border-radius: 18px;
|
| 19 |
+
background: #fff; cursor: pointer; font-size: 13px; }
|
| 20 |
+
.tab.active { background: #1c1c1e; color: #fff; border-color: #1c1c1e; }
|
| 21 |
+
header .links { margin-left: auto; font-size: 12.5px; }
|
| 22 |
+
header .links a { margin-left: 14px; text-decoration: none; }
|
| 23 |
+
main { flex: 1; display: flex; min-height: 0; }
|
| 24 |
+
#itemlist { width: 172px; overflow-y: auto; padding: 10px; background: #f6f6f8; }
|
| 25 |
+
.itembtn { display: block; width: 100%; text-align: left; margin-bottom: 6px;
|
| 26 |
+
padding: 8px 10px; border: 1px solid #ddd; border-radius: 8px;
|
| 27 |
+
background: #fff; cursor: pointer; font: 12px ui-monospace, monospace; }
|
| 28 |
+
.itembtn small { display: block; color: #888; font: 11px -apple-system, sans-serif; }
|
| 29 |
+
.itembtn.active { border-color: #1c1c1e; background: #1c1c1e; color: #fff; }
|
| 30 |
+
.itembtn.active small { color: #bbb; }
|
| 31 |
+
#stage { flex: 1; position: relative; min-width: 0; }
|
| 32 |
+
#stage canvas { display: block; }
|
| 33 |
+
#hint { position: absolute; left: 12px; bottom: 10px; color: #999;
|
| 34 |
+
font-size: 11.5px; pointer-events: none; }
|
| 35 |
+
#panel { width: 274px; padding: 14px; background: #fff; overflow-y: auto;
|
| 36 |
+
box-shadow: -1px 0 3px rgba(0,0,0,.06); }
|
| 37 |
+
#panel h2 { margin: 0 0 4px; font-size: 14px; }
|
| 38 |
+
#panel .meta { color: #666; font-size: 12px; margin-bottom: 10px; }
|
| 39 |
+
.joint { margin-bottom: 10px; }
|
| 40 |
+
.joint label { display: flex; justify-content: space-between;
|
| 41 |
+
font-size: 12px; color: #444; gap: 6px; }
|
| 42 |
+
.joint label span:first-child { overflow: hidden; text-overflow: ellipsis;
|
| 43 |
+
white-space: nowrap; }
|
| 44 |
+
.joint input { width: 100%; accent-color: #1c1c1e; }
|
| 45 |
+
.row { display: flex; gap: 8px; margin: 12px 0; }
|
| 46 |
+
.row button { flex: 1; padding: 7px 0; border: 0; border-radius: 7px;
|
| 47 |
+
background: #1c1c1e; color: #fff; cursor: pointer; }
|
| 48 |
+
.row button.ghost { background: #e6e6ea; color: #1c1c1e; }
|
| 49 |
+
#panel pre { background: #16161a; color: #e8e8ea; padding: 9px;
|
| 50 |
+
border-radius: 6px; font-size: 10px; white-space: pre-wrap;
|
| 51 |
+
word-break: break-all; max-height: 130px; overflow-y: auto; }
|
| 52 |
+
#panel .copy { margin-top: 2px; padding: 5px 10px; border: 0; border-radius: 6px;
|
| 53 |
+
background: #1c1c1e; color: #fff; cursor: pointer; font-size: 12px; }
|
| 54 |
+
</style>
|
| 55 |
+
</head>
|
| 56 |
+
<body>
|
| 57 |
+
<header>
|
| 58 |
+
<h1>ProjectSim Procedural Lab</h1>
|
| 59 |
+
<div id="tabs"></div>
|
| 60 |
+
<nav class="links">
|
| 61 |
+
<a id="dslink" href="https://huggingface.co/projectsim">Dataset</a>
|
| 62 |
+
<a href="https://docs.projectsim.ai">API docs</a>
|
| 63 |
+
<a href="https://www.projectsim.ai">www.projectsim.ai</a>
|
| 64 |
+
</nav>
|
| 65 |
+
</header>
|
| 66 |
+
<main>
|
| 67 |
+
<div id="itemlist"></div>
|
| 68 |
+
<div id="stage"><div id="hint">grab a part to articulate it · drag background to orbit · scroll zoom</div></div>
|
| 69 |
+
<aside id="panel">
|
| 70 |
+
<h2 id="title">loading…</h2>
|
| 71 |
+
<div class="meta" id="meta"></div>
|
| 72 |
+
<div class="row">
|
| 73 |
+
<button id="openall">Open all</button>
|
| 74 |
+
<button id="closeall" class="ghost">Close</button>
|
| 75 |
+
<button id="anim" class="ghost">▶ Loop</button>
|
| 76 |
+
</div>
|
| 77 |
+
<div id="joints"></div>
|
| 78 |
+
<h2 style="margin-top:14px">Take it home</h2>
|
| 79 |
+
<div class="meta">Same asset, full 4K, real physics — Blender one paste:</div>
|
| 80 |
+
<pre id="oneliner"></pre>
|
| 81 |
+
<button class="copy" id="copy">Copy one-liner</button>
|
| 82 |
+
</aside>
|
| 83 |
+
</main>
|
| 84 |
+
<script type="importmap">
|
| 85 |
+
{ "imports": {
|
| 86 |
+
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
|
| 87 |
+
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
|
| 88 |
+
} }
|
| 89 |
+
</script>
|
| 90 |
+
<script type="module">
|
| 91 |
+
import * as THREE from "three";
|
| 92 |
+
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
| 93 |
+
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
| 94 |
+
|
| 95 |
+
// Data comes from the class DATASETS (viewer/ GLBs + catalog). ?base=URL
|
| 96 |
+
// overrides for local testing against a plain directory layout.
|
| 97 |
+
const LOCAL_BASE = new URLSearchParams(location.search).get("base");
|
| 98 |
+
const HFDS = "https://huggingface.co/datasets/projectsim";
|
| 99 |
+
const dsUrl = (ds, path) => LOCAL_BASE
|
| 100 |
+
? `${LOCAL_BASE}/${ds}/${path}`
|
| 101 |
+
: `${HFDS}/${ds}/resolve/main/${path}`;
|
| 102 |
+
const stage = document.getElementById("stage");
|
| 103 |
+
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
| 104 |
+
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
|
| 105 |
+
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
| 106 |
+
stage.appendChild(renderer.domElement);
|
| 107 |
+
const scene = new THREE.Scene();
|
| 108 |
+
scene.background = new THREE.Color(0xececef);
|
| 109 |
+
const camera = new THREE.PerspectiveCamera(40, 1, 0.01, 100);
|
| 110 |
+
const controls = new OrbitControls(camera, renderer.domElement);
|
| 111 |
+
controls.enableDamping = true;
|
| 112 |
+
scene.add(new THREE.HemisphereLight(0xffffff, 0x8a8a92, 1.1));
|
| 113 |
+
const sun = new THREE.DirectionalLight(0xffffff, 1.6);
|
| 114 |
+
sun.position.set(2, 4, 3);
|
| 115 |
+
scene.add(sun);
|
| 116 |
+
scene.add(new THREE.GridHelper(6, 30, 0xbbbbc2, 0xd8d8de));
|
| 117 |
+
|
| 118 |
+
function resize() {
|
| 119 |
+
const w = stage.clientWidth, h = stage.clientHeight;
|
| 120 |
+
renderer.setSize(w, h); camera.aspect = w / h;
|
| 121 |
+
camera.updateProjectionMatrix();
|
| 122 |
+
}
|
| 123 |
+
addEventListener("resize", resize);
|
| 124 |
+
|
| 125 |
+
const loader = new GLTFLoader();
|
| 126 |
+
let model = null;
|
| 127 |
+
let rig = [];
|
| 128 |
+
let grabMeshes = [];
|
| 129 |
+
let drag = null;
|
| 130 |
+
let animating = false, tAnim = 0;
|
| 131 |
+
|
| 132 |
+
function clearModel() {
|
| 133 |
+
if (model) { scene.remove(model); model = null; }
|
| 134 |
+
rig = []; grabMeshes = []; drag = null;
|
| 135 |
+
document.getElementById("joints").innerHTML = "";
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
function fmtVal(r, v) {
|
| 139 |
+
return r.unit === "deg" ? `${Math.round(v)}°` : `${(v * 100).toFixed(1)} cm`;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
function setJoint(r, v) {
|
| 143 |
+
v = Math.min(r.hi, Math.max(r.lo, v));
|
| 144 |
+
r.val = v;
|
| 145 |
+
if (r.type === "revolute") {
|
| 146 |
+
r.group.quaternion.setFromAxisAngle(r.axis, THREE.MathUtils.degToRad(v));
|
| 147 |
+
} else {
|
| 148 |
+
r.group.position.copy(r.basePos).addScaledVector(r.axis, v);
|
| 149 |
+
}
|
| 150 |
+
r.slider.value = v;
|
| 151 |
+
r.out.textContent = fmtVal(r, v);
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
// ---- pointer: grab a part, swing (revolute) or pull (prismatic) --------------
|
| 155 |
+
const ray = new THREE.Raycaster();
|
| 156 |
+
const ptr = new THREE.Vector2();
|
| 157 |
+
function ptrRay(e) {
|
| 158 |
+
const rect = renderer.domElement.getBoundingClientRect();
|
| 159 |
+
ptr.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
| 160 |
+
ptr.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
|
| 161 |
+
ray.setFromCamera(ptr, camera);
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
renderer.domElement.addEventListener("pointerdown", e => {
|
| 165 |
+
if (!grabMeshes.length) return;
|
| 166 |
+
ptrRay(e);
|
| 167 |
+
const hits = ray.intersectObjects(grabMeshes, false);
|
| 168 |
+
if (!hits.length) return;
|
| 169 |
+
const r = hits[0].object.userData.rigEntry;
|
| 170 |
+
const pWorld = r.group.getWorldPosition(new THREE.Vector3());
|
| 171 |
+
const aWorld = r.axis.clone()
|
| 172 |
+
.transformDirection(r.group.parent.matrixWorld).normalize();
|
| 173 |
+
animating = false;
|
| 174 |
+
if (r.type === "revolute") {
|
| 175 |
+
const v0 = hits[0].point.clone().sub(pWorld).projectOnPlane(aWorld);
|
| 176 |
+
if (v0.lengthSq() < 1e-8) return;
|
| 177 |
+
drag = { kind: "rev", r, pWorld, aWorld, v0: v0.normalize(),
|
| 178 |
+
start: r.val || 0 };
|
| 179 |
+
} else {
|
| 180 |
+
const n = camera.getWorldDirection(new THREE.Vector3());
|
| 181 |
+
drag = { kind: "pris", r, aWorld, start: r.val || 0,
|
| 182 |
+
plane: new THREE.Plane().setFromNormalAndCoplanarPoint(
|
| 183 |
+
n, hits[0].point),
|
| 184 |
+
p0: hits[0].point.clone() };
|
| 185 |
+
}
|
| 186 |
+
controls.enabled = false;
|
| 187 |
+
renderer.domElement.setPointerCapture(e.pointerId);
|
| 188 |
+
renderer.domElement.style.cursor = "grabbing";
|
| 189 |
+
});
|
| 190 |
+
|
| 191 |
+
renderer.domElement.addEventListener("pointermove", e => {
|
| 192 |
+
if (!drag) {
|
| 193 |
+
if (!grabMeshes.length) return;
|
| 194 |
+
ptrRay(e);
|
| 195 |
+
renderer.domElement.style.cursor =
|
| 196 |
+
ray.intersectObjects(grabMeshes, false).length ? "grab" : "";
|
| 197 |
+
return;
|
| 198 |
+
}
|
| 199 |
+
ptrRay(e);
|
| 200 |
+
const hit = new THREE.Vector3();
|
| 201 |
+
if (drag.kind === "rev") {
|
| 202 |
+
const plane = new THREE.Plane().setFromNormalAndCoplanarPoint(
|
| 203 |
+
drag.aWorld, drag.pWorld);
|
| 204 |
+
if (!ray.ray.intersectPlane(plane, hit)) return;
|
| 205 |
+
const v1 = hit.sub(drag.pWorld).projectOnPlane(drag.aWorld);
|
| 206 |
+
if (v1.lengthSq() < 1e-8) return;
|
| 207 |
+
v1.normalize();
|
| 208 |
+
const cross = new THREE.Vector3().crossVectors(drag.v0, v1);
|
| 209 |
+
const delta = Math.atan2(cross.dot(drag.aWorld), drag.v0.dot(v1));
|
| 210 |
+
setJoint(drag.r, drag.start + THREE.MathUtils.radToDeg(delta));
|
| 211 |
+
} else {
|
| 212 |
+
if (!ray.ray.intersectPlane(drag.plane, hit)) return;
|
| 213 |
+
const delta = hit.sub(drag.p0).dot(drag.aWorld);
|
| 214 |
+
setJoint(drag.r, drag.start + delta);
|
| 215 |
+
}
|
| 216 |
+
});
|
| 217 |
+
|
| 218 |
+
window.__lab = { camera, ray, ptrRay, scene,
|
| 219 |
+
get drag() { return drag; },
|
| 220 |
+
get rig() { return rig; },
|
| 221 |
+
get grabMeshes() { return grabMeshes; },
|
| 222 |
+
setJoint };
|
| 223 |
+
|
| 224 |
+
addEventListener("pointerup", () => {
|
| 225 |
+
if (drag) { drag = null; controls.enabled = true;
|
| 226 |
+
renderer.domElement.style.cursor = ""; }
|
| 227 |
+
});
|
| 228 |
+
|
| 229 |
+
// ---- load one item -----------------------------------------------------------
|
| 230 |
+
async function loadItem(cls, item) {
|
| 231 |
+
clearModel();
|
| 232 |
+
document.getElementById("title").textContent = item.id;
|
| 233 |
+
const d = item.dims_m;
|
| 234 |
+
document.getElementById("meta").textContent =
|
| 235 |
+
`${d[0].toFixed(2)} × ${d[1].toFixed(2)} × ${d[2].toFixed(2)} m · ` +
|
| 236 |
+
`${item.mass_kg.toFixed(1)} kg · ${item.tris} tris · ` +
|
| 237 |
+
`${item.joints.length} joints`;
|
| 238 |
+
document.getElementById("oneliner").textContent =
|
| 239 |
+
`blender --python-expr "import bpy,urllib.request,tempfile,os; ` +
|
| 240 |
+
`p=os.path.join(tempfile.gettempdir(),'kaedim_asset.blend'); ` +
|
| 241 |
+
`urllib.request.urlretrieve('${HFDS}/${cls.dataset}/resolve/main/` +
|
| 242 |
+
`${cls.items_dir}/${item.id}/${item.id}.blend',p); ` +
|
| 243 |
+
`bpy.ops.wm.open_mainfile(filepath=p)"`;
|
| 244 |
+
const gltf = await loader.loadAsync(dsUrl(cls.dataset, item.glb));
|
| 245 |
+
model = new THREE.Group();
|
| 246 |
+
model.rotation.x = -Math.PI / 2; // assets are Z-up
|
| 247 |
+
const inner = new THREE.Group();
|
| 248 |
+
inner.add(gltf.scene);
|
| 249 |
+
model.add(inner);
|
| 250 |
+
scene.add(model);
|
| 251 |
+
const jdiv = document.getElementById("joints");
|
| 252 |
+
const groupByChild = {}; // link name -> pivot group (for joint chains)
|
| 253 |
+
for (const j of item.joints) {
|
| 254 |
+
const nodes = [];
|
| 255 |
+
gltf.scene.traverse(n => {
|
| 256 |
+
if (j.meshes.some(m => n.name === m || n.name === m.replaceAll(".", "_")))
|
| 257 |
+
nodes.push(n);
|
| 258 |
+
});
|
| 259 |
+
const parent = groupByChild[j.parent_link] || inner;
|
| 260 |
+
const group = new THREE.Group();
|
| 261 |
+
group.position.set(...j.pivot);
|
| 262 |
+
inner.add(group);
|
| 263 |
+
if (parent !== inner) parent.attach(group); // ride the parent joint
|
| 264 |
+
for (const n of nodes) group.attach(n);
|
| 265 |
+
groupByChild[j.child_link] = group;
|
| 266 |
+
const el = document.createElement("div");
|
| 267 |
+
el.className = "joint";
|
| 268 |
+
const step = j.unit === "deg" ? 1 : 0.005;
|
| 269 |
+
el.innerHTML = `<label><span>${j.name}</span><span class="out"></span></label>
|
| 270 |
+
<input type="range" min="${j.limits[0]}" max="${j.limits[1]}"
|
| 271 |
+
value="${j.limits[0]}" step="${step}">`;
|
| 272 |
+
jdiv.appendChild(el);
|
| 273 |
+
const r = { group, type: j.type, unit: j.unit,
|
| 274 |
+
axis: new THREE.Vector3(...j.axis).normalize(),
|
| 275 |
+
basePos: group.position.clone(), val: j.limits[0],
|
| 276 |
+
lo: j.limits[0], hi: j.limits[1], phase: j.phase || 0,
|
| 277 |
+
slider: el.querySelector("input"),
|
| 278 |
+
out: el.querySelector(".out") };
|
| 279 |
+
r.out.textContent = fmtVal(r, r.val);
|
| 280 |
+
r.slider.oninput = () => { animating = false;
|
| 281 |
+
setJoint(r, parseFloat(r.slider.value)); };
|
| 282 |
+
for (const n of nodes) {
|
| 283 |
+
n.traverse(c => {
|
| 284 |
+
if (c.isMesh) { c.userData.rigEntry = r; grabMeshes.push(c); }
|
| 285 |
+
});
|
| 286 |
+
}
|
| 287 |
+
rig.push(r);
|
| 288 |
+
}
|
| 289 |
+
const bb = new THREE.Box3().setFromObject(model);
|
| 290 |
+
const c = bb.getCenter(new THREE.Vector3());
|
| 291 |
+
const size = bb.getSize(new THREE.Vector3()).length();
|
| 292 |
+
camera.position.set(c.x + size * 0.9, c.y + size * 0.6, c.z + size * 1.1);
|
| 293 |
+
controls.target.copy(c);
|
| 294 |
+
camera.near = size / 100; camera.far = size * 50;
|
| 295 |
+
camera.updateProjectionMatrix();
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
document.getElementById("openall").onclick = () => {
|
| 299 |
+
animating = false;
|
| 300 |
+
rig.forEach(r => setJoint(r, r.lo + (r.hi - r.lo) * 0.85));
|
| 301 |
+
};
|
| 302 |
+
document.getElementById("closeall").onclick = () => {
|
| 303 |
+
animating = false;
|
| 304 |
+
rig.forEach(r => setJoint(r, r.lo));
|
| 305 |
+
};
|
| 306 |
+
document.getElementById("anim").onclick = () => { animating = !animating; };
|
| 307 |
+
document.getElementById("copy").onclick = async () => {
|
| 308 |
+
try { await navigator.clipboard.writeText(
|
| 309 |
+
document.getElementById("oneliner").textContent); } catch (e) {}
|
| 310 |
+
};
|
| 311 |
+
|
| 312 |
+
const smooth = t => t * t * (3 - 2 * t);
|
| 313 |
+
function tick() {
|
| 314 |
+
requestAnimationFrame(tick);
|
| 315 |
+
if (animating && rig.length) {
|
| 316 |
+
tAnim += 0.008;
|
| 317 |
+
const cyc = (Math.sin(tAnim) + 1) / 2;
|
| 318 |
+
const twoPhase = rig.some(r => r.phase === 1);
|
| 319 |
+
for (const r of rig) {
|
| 320 |
+
let local = cyc;
|
| 321 |
+
if (twoPhase) local = r.phase === 0 ? Math.min(1, cyc * 2)
|
| 322 |
+
: Math.max(0, cyc * 2 - 1);
|
| 323 |
+
setJoint(r, r.lo + (r.hi - r.lo) * 0.85 * smooth(local));
|
| 324 |
+
}
|
| 325 |
+
}
|
| 326 |
+
controls.update();
|
| 327 |
+
renderer.render(scene, camera);
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
// ---- catalog / tabs / item list ----------------------------------------------
|
| 331 |
+
fetch("classes.json")
|
| 332 |
+
.then(r => r.json())
|
| 333 |
+
.then(classes => Promise.all(classes.map(c =>
|
| 334 |
+
fetch(dsUrl(c.dataset, "viewer/catalog.json"))
|
| 335 |
+
.then(r => r.json())
|
| 336 |
+
.then(v => ({ ...c, items_dir: v.items_dir, items: v.items })))))
|
| 337 |
+
.then(catalog => {
|
| 338 |
+
const tabs = document.getElementById("tabs");
|
| 339 |
+
const list = document.getElementById("itemlist");
|
| 340 |
+
function selectClass(cls) {
|
| 341 |
+
document.querySelectorAll(".tab").forEach(t =>
|
| 342 |
+
t.classList.toggle("active", t.dataset.cls === cls.class));
|
| 343 |
+
document.getElementById("dslink").href = `${HFDS}/${cls.dataset}`;
|
| 344 |
+
list.innerHTML = "";
|
| 345 |
+
cls.items.forEach((item, i) => {
|
| 346 |
+
const btn = document.createElement("button");
|
| 347 |
+
btn.className = "itembtn";
|
| 348 |
+
const d = item.dims_m;
|
| 349 |
+
btn.innerHTML = `${item.id.replace(/^[a-z_]+_(?=\d)/, "#")}
|
| 350 |
+
<small>${d[0].toFixed(2)}×${d[1].toFixed(2)}×${d[2].toFixed(2)} m ·
|
| 351 |
+
${item.mass_kg.toFixed(1)} kg · ${item.joints.length} j</small>`;
|
| 352 |
+
btn.onclick = () => {
|
| 353 |
+
document.querySelectorAll(".itembtn").forEach(x =>
|
| 354 |
+
x.classList.remove("active"));
|
| 355 |
+
btn.classList.add("active");
|
| 356 |
+
loadItem(cls, item);
|
| 357 |
+
};
|
| 358 |
+
list.appendChild(btn);
|
| 359 |
+
if (i === 0) btn.click();
|
| 360 |
+
});
|
| 361 |
+
}
|
| 362 |
+
for (const cls of catalog) {
|
| 363 |
+
const t = document.createElement("button");
|
| 364 |
+
t.className = "tab";
|
| 365 |
+
t.dataset.cls = cls.class;
|
| 366 |
+
t.textContent = cls.label;
|
| 367 |
+
t.onclick = () => selectClass(cls);
|
| 368 |
+
tabs.appendChild(t);
|
| 369 |
+
}
|
| 370 |
+
selectClass(catalog[0]);
|
| 371 |
+
resize();
|
| 372 |
+
tick();
|
| 373 |
+
});
|
| 374 |
+
</script>
|
| 375 |
+
</body>
|
| 376 |
</html>
|