G1_physical_ai_maps / index.html
kwminjii's picture
G1 map viewer: three.js static page
19e2c08 verified
Raw History Blame Contribute Delete
14.4 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>G1 Map Viewer</title>
<style>
html, body { margin: 0; height: 100%; overflow: hidden; background: #1a1a1a;
color: #ddd; font-family: monospace; font-size: 10px; }
#info { position: absolute; top: 8px; left: 8px; padding: 6px 10px;
background: rgba(0,0,0,0.55); border-radius: 4px; max-width: 60vw;
line-height: 1.5; }
#info b { color: #ffd86b; }
#err { color: #f66; }
#ctrl { position: absolute; top: 8px; right: 8px; padding: 8px 12px;
background: rgba(0,0,0,0.55); border-radius: 4px; min-width: 210px; }
#ctrl label { display: block; margin: 6px 0; }
#ctrl input[type=range], #ctrl select { width: 100%; box-sizing: border-box;
background:#2a2a2a; color:#ddd; border:1px solid #444; }
#pick { position: absolute; top: 66px; left: 8px; width: 230px;
padding: 8px 10px; background: rgba(0,0,0,0.6); border-radius: 4px; }
#pick .hdr { color:#ffb347; font-weight:bold; margin-bottom:6px; }
#pick label { display:block; margin:5px 0; color:#bbb; }
#pick select { width:100%; box-sizing:border-box; background:#0f0f14;
color:#ddd; border:1px solid #444; border-radius:3px; padding:4px; }
#tasklist { max-height: 34vh; overflow-y: auto; margin-top: 8px; }
.trow { display:flex; justify-content:space-between; gap:6px; padding:2px 0;
border-bottom:1px solid #2a2a2a; }
.trow .n { color:#ffa500; }
.muted { color:#888; }
#help { position: absolute; bottom: 8px; left: 8px; padding: 6px 10px;
background: rgba(0,0,0,0.45); border-radius: 4px; opacity: 0.7; }
button { background:#2a2a2a; color:#ddd; border:1px solid #555; padding:4px 8px;
cursor:pointer; border-radius:3px; }
button:hover { background:#3a3a3a; }
</style>
</head>
<body>
<div id="info">
<div id="map-info">Loadingโ€ฆ</div>
<div id="repo-info" class="muted"></div>
</div>
<div id="pick">
<div class="hdr">Map</div>
<label>map
<select id="mapSel"></select>
</label>
<label>version
<select id="revSel"></select>
</label>
<div class="hdr" style="margin-top:8px">Goals <span id="tcount" class="muted"></span></div>
<div id="tasklist"></div>
</div>
<div id="ctrl">
<label>Point size: <span id="psv">3</span>
<input type="range" id="psize" min="1" max="15" step="1" value="3">
</label>
<label>Color by:
<select id="cmode">
<option value="height" selected>Height (Z)</option>
<option value="depth">Distance from center</option>
<option value="white">White</option>
</select>
</label>
<label><input type="checkbox" id="pathOn" checked> Connect goals</label>
<label><input type="checkbox" id="arrowOn" checked> Yaw arrows</label>
<label><input type="checkbox" id="grid" checked> Grid + axes</label>
<label><input type="checkbox" id="topdown"> Top-down view</label>
<label><button id="fitMap" type="button">Fit whole map</button></label>
<label><button id="fitGoals" type="button">Fit goals</button></label>
</div>
<div id="help">drag: rotate &nbsp;|&nbsp; right-drag: pan &nbsp;|&nbsp; wheel: zoom
&nbsp;|&nbsp; orange = goal</div>
<script type="importmap">
{
"imports": {
"three": "https://cdnjs.cloudflare.com/ajax/libs/three.js/0.160.0/three.module.min.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { PCDLoader } from 'three/addons/loaders/PCDLoader.js';
const $ = id => document.getElementById(id);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a1a);
const camera = new THREE.PerspectiveCamera(
55, window.innerWidth / window.innerHeight, 0.01, 5000);
camera.up.set(0, 0, 1); // Z-up (map frame)
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.08;
controls.screenSpacePanning = true;
const grid = new THREE.GridHelper(40, 40, 0x666666, 0x303030);
grid.rotation.x = Math.PI / 2;
scene.add(grid);
const axes = new THREE.AxesHelper(1.0);
scene.add(axes);
const MARKER_Z = 0.05;
let pointsObj = null, tasks = [], center = new THREE.Vector3();
let bbSize = 10, minZ = 0, maxZ = 1;
// ---- colorize (์›๋ณธ๊ณผ ๋™์ผ) ----
function colorize(mode) {
if (!pointsObj) return;
const pos = pointsObj.geometry.attributes.position;
const n = pos.count;
let colors = pointsObj.geometry.attributes.color;
if (!colors || colors.count !== n) {
colors = new THREE.BufferAttribute(new Float32Array(n * 3), 3);
pointsObj.geometry.setAttribute('color', colors);
}
const arr = colors.array, tmp = new THREE.Color();
const span = Math.max(1e-6, maxZ - minZ);
for (let i = 0; i < n; i++) {
let t;
if (mode === 'height') {
t = (pos.getZ(i) - minZ) / span;
} else if (mode === 'depth') {
const dx = pos.getX(i) - center.x, dy = pos.getY(i) - center.y,
dz = pos.getZ(i) - center.z;
t = Math.min(1, Math.sqrt(dx*dx + dy*dy + dz*dz) / (bbSize * 0.5 + 1e-9));
} else {
arr[i*3] = arr[i*3+1] = arr[i*3+2] = 1.0; continue;
}
tmp.setHSL((1 - t) * 0.7, 1.0, 0.5);
arr[i*3] = tmp.r; arr[i*3+1] = tmp.g; arr[i*3+2] = tmp.b;
}
colors.needsUpdate = true;
pointsObj.material.vertexColors = true;
pointsObj.material.needsUpdate = true;
}
// ---- goal markers (์›๋ณธ ์žฌ์‚ฌ์šฉ) + ์—ฐ๊ฒฐ์„ /ํ™”์‚ดํ‘œ ----
const taskGroup = new THREE.Group();
scene.add(taskGroup);
function makeTextSprite(text) {
const cv = document.createElement('canvas');
const ctx = cv.getContext('2d');
const fs = 44;
ctx.font = 'bold ' + fs + 'px monospace';
const w = Math.ceil(ctx.measureText(text).width);
cv.width = w + 20; cv.height = fs + 16;
ctx.font = 'bold ' + fs + 'px monospace';
ctx.fillStyle = 'rgba(0,0,0,0.55)';
ctx.fillRect(0, 0, cv.width, cv.height);
ctx.fillStyle = '#ffcc66';
ctx.textBaseline = 'middle';
ctx.fillText(text, 10, cv.height / 2);
const tex = new THREE.CanvasTexture(cv);
tex.minFilter = THREE.LinearFilter;
const sp = new THREE.Sprite(new THREE.SpriteMaterial({
map: tex, depthTest: false, transparent: true }));
const s = 0.012;
sp.scale.set(cv.width * s, cv.height * s, 1);
return sp;
}
function clearGroup(g) {
while (g.children.length) {
const c = g.children.pop();
if (c.geometry) c.geometry.dispose();
if (c.material) {
if (c.material.map) c.material.map.dispose();
c.material.dispose();
}
}
}
function yawOf(t) {
return Math.atan2(2*(t.q_w*t.q_z + t.q_x*t.q_y),
1 - 2*(t.q_y*t.q_y + t.q_z*t.q_z));
}
function renderTasks() {
clearGroup(taskGroup);
const top = MARKER_Z + 0.30;
for (const t of tasks) {
const sph = new THREE.Mesh(new THREE.SphereGeometry(0.14, 16, 12),
new THREE.MeshBasicMaterial({ color: 0xffa500 }));
sph.position.set(t.x, t.y, top);
taskGroup.add(sph);
const stick = new THREE.BufferGeometry().setFromPoints(
[new THREE.Vector3(t.x, t.y, 0), new THREE.Vector3(t.x, t.y, top)]);
taskGroup.add(new THREE.Line(stick,
new THREE.LineBasicMaterial({ color: 0xffa500 })));
const lbl = makeTextSprite(t.name);
lbl.position.set(t.x, t.y, top + 0.28);
taskGroup.add(lbl);
// yaw arrow: ๊ฐ ์ง€์ ์—์„œ ๋กœ๋ด‡์ด ๋ฐ”๋ผ๋ณผ ๋ฐฉํ–ฅ
if ($('arrowOn').checked) {
const yaw = yawOf(t), L = 0.55;
const dir = new THREE.Vector3(Math.cos(yaw), Math.sin(yaw), 0);
const shaft = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(t.x, t.y, top),
new THREE.Vector3(t.x + dir.x * L, t.y + dir.y * L, top)]);
taskGroup.add(new THREE.Line(shaft,
new THREE.LineBasicMaterial({ color: 0x66ddff })));
const head = new THREE.Mesh(new THREE.ConeGeometry(0.07, 0.20, 12),
new THREE.MeshBasicMaterial({ color: 0x66ddff }));
head.position.set(t.x + dir.x * (L + 0.10), t.y + dir.y * (L + 0.10), top);
head.rotation.z = yaw - Math.PI / 2; // cone ์€ +Y ๋ฅผ ํ–ฅํ•œ๋‹ค
taskGroup.add(head);
}
}
// ์ง€์  ๊ฐ„ ์—ฐ๊ฒฐ์„ . ๋ฐฐ์—ด์€ ๋“ฑ๋ก ์ˆœ์„œ์ด๋ฉฐ ์ฃผํ–‰ ์ˆœ์„œ๊ฐ€ ์•„๋‹ˆ๋‹ค.
if ($('pathOn').checked && tasks.length > 1) {
const pts = tasks.map(t => new THREE.Vector3(t.x, t.y, top));
taskGroup.add(new THREE.Line(
new THREE.BufferGeometry().setFromPoints(pts),
new THREE.LineDashedMaterial({ color: 0x888888, dashSize: 0.18,
gapSize: 0.12 })).computeLineDistances());
}
const list = $('tasklist');
list.innerHTML = '';
$('tcount').textContent = '(' + tasks.length + ')';
tasks.forEach(t => {
const d = document.createElement('div');
d.className = 'trow';
d.innerHTML = '<span class="n">' + t.name + '</span>' +
'<span class="muted">' + t.x.toFixed(2) + ', ' + t.y.toFixed(2) +
' ' + (yawOf(t) * 180 / Math.PI).toFixed(0) + 'ยฐ</span>';
list.appendChild(d);
});
}
// ---- camera ----
function fitBox(box, pad) {
const c = new THREE.Vector3(), sz = new THREE.Vector3();
box.getCenter(c); box.getSize(sz);
const d = Math.max(sz.length(), 1.0) * (pad || 0.8);
controls.target.copy(c);
camera.position.set(c.x + d, c.y - d, c.z + d * 0.6);
controls.update();
}
function fitGoals() {
if (!tasks.length) return fitMap();
const box = new THREE.Box3();
tasks.forEach(t => box.expandByPoint(new THREE.Vector3(t.x, t.y, 0)));
box.expandByScalar(1.2); // ๋ชฉํ‘œ ์ง€์ ์ด ์ข๊ฒŒ ๋ชฐ๋ ค ์žˆ์–ด ์—ฌ์œ ๋ฅผ ์ค€๋‹ค
fitBox(box, 1.1);
}
function fitMap() {
if (!pointsObj) return;
pointsObj.geometry.computeBoundingBox();
fitBox(pointsObj.geometry.boundingBox.clone(), 0.8);
}
// ---- load ----
const mapInfo = $('map-info');
const loader = new PCDLoader();
// Static Space: ์„œ๋ฒ„ ์—†์ด ๋ธŒ๋ผ์šฐ์ €๊ฐ€ public dataset ์—์„œ ์ง์ ‘ ๋ฐ›๋Š”๋‹ค.
const REPO = 'mncai/G1_physical_ai_maps';
const HF = 'https://huggingface.co';
function hfUrl(rev, rel) {
return HF + '/datasets/' + REPO + '/resolve/' + encodeURIComponent(rev) + '/' + rel;
}
async function loadMap(map, rev) {
mapInfo.textContent = 'Loading ' + map + ' @ ' + rev + ' โ€ฆ';
if (pointsObj) {
scene.remove(pointsObj);
pointsObj.geometry.dispose();
pointsObj.material.dispose();
pointsObj = null;
}
clearGroup(taskGroup);
tasks = [];
// goals
try {
const r = await fetch(hfUrl(rev, map + '/tasklist.json'), {cache: 'no-store'});
const j = await r.json();
tasks = Array.isArray(j) ? j : [];
} catch (e) { tasks = []; }
// point cloud
loader.load(hfUrl(rev, map + '/map.pcd'), (points) => {
pointsObj = points;
points.geometry.computeBoundingBox();
const bb = points.geometry.boundingBox;
bb.getCenter(center);
bbSize = bb.getSize(new THREE.Vector3()).length();
const pz = points.geometry.attributes.position;
minZ = Infinity; maxZ = -Infinity;
for (let i = 0; i < pz.count; i++) {
const z = pz.getZ(i);
if (z < minZ) minZ = z;
if (z > maxZ) maxZ = z;
}
points.material.size = +$('psize').value;
points.material.sizeAttenuation = false;
colorize($('cmode').value);
scene.add(points);
renderTasks();
fitGoals(); // ๋ชฉํ‘œ ์ง€์  ์ฃผ๋ณ€์—์„œ ์‹œ์ž‘
mapInfo.innerHTML = 'map: <b>' + map + '</b> @ <b>' + rev + '</b> &nbsp;|&nbsp; ' +
pz.count.toLocaleString() + ' pts &nbsp;|&nbsp; z=[' +
minZ.toFixed(2) + ', ' + maxZ.toFixed(2) + '] m';
}, null, (err) => {
mapInfo.innerHTML = '<span id="err">map load failed: ' + err + '</span>';
});
}
// ---- init ----
async function init() {
let maps = [], revisions = ['main'];
try {
const idx = await fetch(hfUrl('main', 'index.json'), {cache: 'no-store'})
.then(r => r.json());
maps = idx.datasets || [];
} catch (e) {
mapInfo.innerHTML = '<span id="err">index.json failed: ' + e + '</span>';
return;
}
try {
const refs = await fetch(HF + '/api/datasets/' + REPO + '/refs').then(r => r.json());
const tags = (refs.tags || []).map(t => t.name).sort().reverse();
revisions = ['main'].concat(tags);
} catch (e) { /* ํƒœ๊ทธ ์กฐํšŒ ์‹คํŒจ ์‹œ main ๋งŒ */ }
$('repo-info').textContent = REPO;
const ms = $('mapSel'), rs = $('revSel');
maps.forEach(m => {
const o = document.createElement('option');
o.value = m.path;
o.textContent = m.map + ' (' + m.points.toLocaleString() + ' pts)';
ms.appendChild(o);
});
revisions.forEach(r => {
const o = document.createElement('option');
o.value = r; o.textContent = r;
rs.appendChild(o);
});
if (!ms.options.length) {
mapInfo.innerHTML = '<span id="err">no maps in index.json</span>';
return;
}
const go = () => loadMap(ms.value, rs.value);
ms.addEventListener('change', go);
rs.addEventListener('change', go);
go();
}
// ---- UI ----
$('psize').addEventListener('input', e => {
$('psv').textContent = e.target.value;
if (pointsObj) pointsObj.material.size = +e.target.value;
});
$('cmode').addEventListener('change', e => colorize(e.target.value));
$('grid').addEventListener('change', e => {
grid.visible = e.target.checked; axes.visible = e.target.checked;
});
$('pathOn').addEventListener('change', renderTasks);
$('arrowOn').addEventListener('change', renderTasks);
$('fitMap').addEventListener('click', fitMap);
$('fitGoals').addEventListener('click', fitGoals);
$('topdown').addEventListener('change', e => {
if (e.target.checked) {
const t = controls.target, rr = Math.max(8, bbSize * 0.5);
camera.position.set(t.x, t.y, t.z + rr); controls.update();
}
});
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
init();
(function loop() {
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(loop);
})();
</script>
</body>
</html>