Spaces:
Running
Running
Download index.html from mncai/G1_physical_ai_maps: direct link, hf CLI and curl.
- Browser
- Download file 14.4 kB
-
https://huggingface.co/spaces/mncai/G1_physical_ai_maps/resolve/main/index.html
- Command line
-
hf download hf://spaces/mncai/G1_physical_ai_maps/index.html
-
curl -L -o index.html https://huggingface.co/spaces/mncai/G1_physical_ai_maps/resolve/main/index.html
14.4 kB
| <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 | right-drag: pan | wheel: zoom | |
| | 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> | ' + | |
| pz.count.toLocaleString() + ' pts | 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> | |