praveen287 commited on
Commit
fdeaf3f
Β·
1 Parent(s): 610ba6d

Update viewer

Browse files
Files changed (1) hide show
  1. viewer/index.html +783 -122
viewer/index.html CHANGED
@@ -5,7 +5,7 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Origami RL</title>
7
  <style>
8
- @import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;1,9..40,300&family=Instrument+Serif:ital@0;1&display=swap');
9
 
10
  * { margin: 0; padding: 0; box-sizing: border-box; }
11
 
@@ -23,6 +23,7 @@
23
  --accent: #c0392b;
24
  --accent-soft: #f5eeec;
25
  --success: #27ae60;
 
26
  --shadow-sm: 0 1px 3px rgba(26,24,22,0.04);
27
  --shadow-md: 0 4px 16px rgba(26,24,22,0.06);
28
  --shadow-lg: 0 8px 32px rgba(26,24,22,0.08);
@@ -38,30 +39,394 @@ body {
38
  -webkit-font-smoothing: antialiased;
39
  }
40
 
 
 
 
 
 
41
  /* --- HEADER --- */
42
  header {
43
- padding: 32px 48px 24px;
 
 
 
 
 
 
 
 
44
  display: flex;
45
  align-items: baseline;
46
- gap: 16px;
47
  }
48
 
49
  header h1 {
50
  font-family: 'Instrument Serif', serif;
51
- font-size: 28px;
52
  font-weight: 400;
53
  letter-spacing: -0.02em;
54
  }
55
 
56
- header span {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  font-size: 13px;
 
 
 
58
  color: var(--text-tertiary);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  font-weight: 300;
60
  }
61
 
62
- /* --- GRID VIEW --- */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  #grid-view {
64
- padding: 0 48px 64px;
 
 
 
 
65
  }
66
 
67
  .grid {
@@ -146,21 +511,15 @@ header span {
146
  text-align: right;
147
  }
148
 
149
- /* --- DETAIL VIEW --- */
 
 
 
150
  #detail-view {
151
- display: none;
152
  height: 100vh;
153
  flex-direction: column;
154
  }
155
 
156
- #detail-view.active {
157
- display: flex;
158
- }
159
-
160
- #grid-view.hidden {
161
- display: none;
162
- }
163
-
164
  .detail-header {
165
  padding: 20px 32px;
166
  display: flex;
@@ -210,7 +569,6 @@ header span {
210
  min-height: 0;
211
  }
212
 
213
- /* 2D crease pattern panel */
214
  .panel-2d {
215
  border-right: 1px solid var(--border);
216
  background: var(--surface);
@@ -260,7 +618,6 @@ header span {
260
  border-radius: 1px;
261
  }
262
 
263
- /* 3D viewer panel */
264
  .panel-3d {
265
  background: var(--bg);
266
  position: relative;
@@ -359,7 +716,10 @@ input[type="range"]::-webkit-slider-thumb:hover {
359
  .metric-value.mid { color: #e67e22; }
360
  .metric-value.low { color: var(--accent); }
361
 
362
- /* --- ANIMATIONS --- */
 
 
 
363
  @keyframes fadeIn {
364
  from { opacity: 0; transform: translateY(8px); }
365
  to { opacity: 1; transform: translateY(0); }
@@ -373,24 +733,303 @@ input[type="range"]::-webkit-slider-thumb:hover {
373
  .card:nth-child(2) { animation-delay: 0.1s; }
374
  .card:nth-child(3) { animation-delay: 0.15s; }
375
  .card:nth-child(4) { animation-delay: 0.2s; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  </style>
377
  </head>
378
  <body>
379
 
380
- <header>
381
- <h1>Origami RL</h1>
382
- <span>fold pattern explorer</span>
 
 
 
 
 
 
383
  </header>
384
 
385
- <!-- GRID VIEW -->
386
- <div id="grid-view">
387
- <div class="grid" id="grid"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  </div>
389
 
390
- <!-- DETAIL VIEW -->
391
- <div id="detail-view">
 
 
392
  <div class="detail-header">
393
- <button class="back-btn" id="back-btn">← Back</button>
394
  <div class="detail-title" id="detail-title"></div>
395
  <div class="detail-subtitle" id="detail-subtitle"></div>
396
  </div>
@@ -423,27 +1062,42 @@ input[type="range"]::-webkit-slider-thumb:hover {
423
  <div class="metrics-bar">
424
  <div class="metric">
425
  <div class="metric-label">Similarity</div>
426
- <div class="metric-value high" id="m-sim">β€”</div>
427
  </div>
428
  <div class="metric">
429
  <div class="metric-label">Folds</div>
430
- <div class="metric-value" id="m-folds">β€”</div>
431
  </div>
432
  <div class="metric">
433
  <div class="metric-label">Strain</div>
434
- <div class="metric-value" id="m-strain">β€”</div>
435
  </div>
436
  <div class="metric">
437
  <div class="metric-label">Vertices</div>
438
- <div class="metric-value" id="m-verts">β€”</div>
439
  </div>
440
  <div class="metric">
441
  <div class="metric-label">Status</div>
442
- <div class="metric-value" id="m-status">β€”</div>
443
  </div>
444
  </div>
445
  </div>
446
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
447
  <script type="importmap">
448
  {
449
  "imports": {
@@ -457,7 +1111,7 @@ input[type="range"]::-webkit-slider-thumb:hover {
457
  import * as THREE from 'three';
458
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
459
 
460
- // ─── FOLD DATA (hardcoded demo) ───────────────────────────────
461
  const TASKS = {
462
  triangle: {
463
  name: 'Triangle',
@@ -562,10 +1216,8 @@ function analyticalFold(fold, creasePercent) {
562
  }
563
 
564
  // Per-face cumulative transform: folded = R * flat + t
565
- // R is a 3x3 matrix stored as [r00,r01,r02, r10,r11,r12, r20,r21,r22]
566
  const faceR = new Array(faces.length).fill(null);
567
  const faceT = new Array(faces.length).fill(null);
568
- // Face 0: identity
569
  faceR[0] = [1,0,0, 0,1,0, 0,0,1];
570
  faceT[0] = [0,0,0];
571
 
@@ -592,7 +1244,6 @@ function analyticalFold(fold, creasePercent) {
592
  let Rfj, tfj;
593
 
594
  if (Math.abs(angle) > 1e-10) {
595
- // Fold rotation around edge (v1->v2) in folded coords
596
  const p1 = [pos[v1*3], pos[v1*3+1], pos[v1*3+2]];
597
  const ax = [pos[v2*3]-p1[0], pos[v2*3+1]-p1[1], pos[v2*3+2]-p1[2]];
598
  const axLen = Math.sqrt(ax[0]*ax[0]+ax[1]*ax[1]+ax[2]*ax[2]);
@@ -603,7 +1254,6 @@ function analyticalFold(fold, creasePercent) {
603
  const u = [ax[0]/axLen, ax[1]/axLen, ax[2]/axLen];
604
  const foldR = rodriguesMat(u, angle);
605
  Rfj = matMul(foldR, faceR[fi]);
606
- // t_fj = foldR * (t_fi - p1) + p1
607
  const dt = [faceT[fi][0]-p1[0], faceT[fi][1]-p1[1], faceT[fi][2]-p1[2]];
608
  const rdt = matVec(foldR, dt);
609
  tfj = [rdt[0]+p1[0], rdt[1]+p1[1], rdt[2]+p1[2]];
@@ -616,7 +1266,6 @@ function analyticalFold(fold, creasePercent) {
616
  faceR[fj] = Rfj;
617
  faceT[fj] = tfj;
618
 
619
- // Place unplaced vertices
620
  for (const vi of faces[fj]) {
621
  if (!placed.has(vi)) {
622
  const fv = [flat[vi*3], flat[vi*3+1], flat[vi*3+2]];
@@ -634,7 +1283,6 @@ function analyticalFold(fold, creasePercent) {
634
  return pos;
635
  }
636
 
637
- // 3x3 rotation matrix from axis-angle (Rodrigues)
638
  function rodriguesMat(u, angle) {
639
  const c = Math.cos(angle), s = Math.sin(angle), t = 1 - c;
640
  return [
@@ -644,7 +1292,6 @@ function rodriguesMat(u, angle) {
644
  ];
645
  }
646
 
647
- // 3x3 matrix multiply (row-major flat arrays)
648
  function matMul(a, b) {
649
  return [
650
  a[0]*b[0]+a[1]*b[3]+a[2]*b[6], a[0]*b[1]+a[1]*b[4]+a[2]*b[7], a[0]*b[2]+a[1]*b[5]+a[2]*b[8],
@@ -653,7 +1300,6 @@ function matMul(a, b) {
653
  ];
654
  }
655
 
656
- // 3x3 matrix * vec3
657
  function matVec(m, v) {
658
  return [
659
  m[0]*v[0]+m[1]*v[1]+m[2]*v[2],
@@ -668,13 +1314,9 @@ const EDGE_COLORS = { M: 0xc0392b, V: 0x2471a3, B: 0x1a1816, F: 0xddd8d0, U: 0xa
668
 
669
  function buildMesh(task, creasePercent = 1) {
670
  const fold = task.fold;
671
- const n = fold.vertices_coords.length;
672
  const group = new THREE.Group();
673
-
674
- // Compute folded positions analytically
675
  const positions = analyticalFold(fold, creasePercent);
676
 
677
- // Faces
678
  const faces = fold.faces_vertices;
679
  const indices = [];
680
  for (const face of faces) {
@@ -698,7 +1340,6 @@ function buildMesh(task, creasePercent = 1) {
698
  const mesh = new THREE.Mesh(geo, mat);
699
  group.add(mesh);
700
 
701
- // Edges
702
  for (let i = 0; i < fold.edges_vertices.length; i++) {
703
  const [v1, v2] = fold.edges_vertices[i];
704
  const assignment = fold.edges_assignment[i];
@@ -716,7 +1357,6 @@ function buildMesh(task, creasePercent = 1) {
716
  group.add(new THREE.LineSegments(lineGeo, lineMat));
717
  }
718
 
719
- // Center
720
  const box = new THREE.Box3().setFromObject(group);
721
  const center = box.getCenter(new THREE.Vector3());
722
  group.position.sub(center);
@@ -739,7 +1379,6 @@ function setupScene(container, opts = {}) {
739
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
740
  container.appendChild(renderer.domElement);
741
 
742
- // Lights
743
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
744
  scene.add(ambientLight);
745
  const dirLight1 = new THREE.DirectionalLight(0xffffff, 0.7);
@@ -764,48 +1403,90 @@ function setupScene(container, opts = {}) {
764
  return { scene, camera, renderer, controls };
765
  }
766
 
767
- // ─── GRID VIEW ────────────────────────────────────────────────
768
 
769
- const gridEl = document.getElementById('grid');
770
  const cardScenes = [];
771
 
772
- for (const [key, task] of Object.entries(TASKS)) {
773
- const card = document.createElement('div');
774
- card.className = 'card';
775
- card.innerHTML = `
776
- <div class="card-canvas" id="card-${key}"></div>
777
- <div class="card-info">
778
- <div class="card-name">${task.name}</div>
779
- <div class="card-desc">${task.description} Β· difficulty ${task.difficulty}</div>
780
- <div class="card-score">
781
- <div class="score-bar-bg"><div class="score-bar-fill" style="width:${task.similarity * 100}%"></div></div>
782
- <div class="score-label">${Math.round(task.similarity * 100)}%</div>
783
- </div>
784
- </div>
785
- `;
786
- card.addEventListener('click', () => showDetail(key));
787
- gridEl.appendChild(card);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
788
  }
789
 
790
- // Init mini 3D scenes after DOM paint
791
- requestAnimationFrame(() => {
 
 
 
 
 
 
 
792
  for (const [key, task] of Object.entries(TASKS)) {
793
- const container = document.getElementById(`card-${key}`);
794
- if (!container) continue;
795
- const { scene, camera, renderer } = setupScene(container, { autoRotate: true });
796
- const mesh = buildMesh(task, 1.0);
797
- scene.add(mesh);
798
-
799
- // Slow auto-rotate
800
- const animate = () => {
801
- requestAnimationFrame(animate);
802
- mesh.rotation.y += 0.005;
803
- renderer.render(scene, camera);
804
- };
805
- animate();
806
- cardScenes.push({ key, scene, camera, renderer, mesh });
 
807
  }
808
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
809
 
810
  // ─── DETAIL VIEW ──────────────────────────────────────────────
811
 
@@ -821,8 +1502,7 @@ function showDetail(key) {
821
  currentTaskKey = key;
822
  const task = TASKS[key];
823
 
824
- document.getElementById('grid-view').classList.add('hidden');
825
- document.getElementById('detail-view').classList.add('active');
826
 
827
  document.getElementById('detail-title').textContent = task.name;
828
  document.getElementById('detail-subtitle').textContent = task.description;
@@ -843,7 +1523,6 @@ function showDetail(key) {
843
 
844
  // 3D viewer
845
  const panel = document.getElementById('panel-3d');
846
- // Clean up old
847
  if (detailRenderer) {
848
  panel.removeChild(detailRenderer.domElement);
849
  detailRenderer.dispose();
@@ -856,18 +1535,13 @@ function showDetail(key) {
856
  detailRenderer = setup.renderer;
857
  detailControls = setup.controls;
858
 
859
- // Reset slider
860
  const slider = document.getElementById('crease-slider');
861
  slider.value = 100;
862
  document.getElementById('slider-val').textContent = '100%';
863
 
864
- // Build mesh
865
  updateDetailMesh(1.0);
866
-
867
- // Target ghost overlay
868
  addTargetGhost(task);
869
 
870
- // Animate
871
  const animateDetail = () => {
872
  detailAnimId = requestAnimationFrame(animateDetail);
873
  detailControls.update();
@@ -875,7 +1549,6 @@ function showDetail(key) {
875
  };
876
  animateDetail();
877
 
878
- // Resize
879
  const resizeObserver = new ResizeObserver(() => {
880
  const w = panel.clientWidth;
881
  const h = panel.clientHeight;
@@ -890,7 +1563,6 @@ function updateDetailMesh(cp) {
890
  if (!currentTaskKey) return;
891
  const task = TASKS[currentTaskKey];
892
 
893
- // Remove old mesh group (keep ghost)
894
  if (detailMeshGroup) {
895
  detailScene.remove(detailMeshGroup);
896
  }
@@ -901,9 +1573,6 @@ function updateDetailMesh(cp) {
901
 
902
  function addTargetGhost(task) {
903
  const fold = task.fold;
904
- const n = fold.vertices_coords.length;
905
-
906
- // Wireframe ghost of fully folded state
907
  const positions = analyticalFold(fold, 1.0);
908
 
909
  const faces = fold.faces_vertices;
@@ -926,8 +1595,6 @@ function addTargetGhost(task) {
926
  });
927
 
928
  const ghost = new THREE.Mesh(geo, ghostMat);
929
-
930
- // Center same as main mesh
931
  const box = new THREE.Box3().setFromBufferAttribute(geo.getAttribute('position'));
932
  const center = box.getCenter(new THREE.Vector3());
933
  ghost.position.sub(center);
@@ -940,7 +1607,6 @@ function draw2DCrease(task) {
940
  const fold = task.fold;
941
  const verts = fold.vertices_coords;
942
 
943
- // Find bounds
944
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
945
  for (const v of verts) {
946
  minX = Math.min(minX, v[0]); minY = Math.min(minY, v[1]);
@@ -954,30 +1620,17 @@ function draw2DCrease(task) {
954
  const scale = size / Math.max(vw, vh);
955
 
956
  const toX = (x) => (x - minX + pad) * scale;
957
- const toY = (y) => size - (y - minY + pad) * scale; // flip Y
958
 
959
  const STROKE = { M: '#c0392b', V: '#2471a3', B: '#1a1816', F: '#ddd8d0', U: '#a9a49d' };
960
 
961
  let svg = `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">`;
962
 
963
- // Background paper
964
- const paperPts = [];
965
- // Find boundary edges and form polygon
966
- const bVerts = new Set();
967
- for (let i = 0; i < fold.edges_vertices.length; i++) {
968
- if (fold.edges_assignment[i] === 'B') {
969
- bVerts.add(fold.edges_vertices[i][0]);
970
- bVerts.add(fold.edges_vertices[i][1]);
971
- }
972
- }
973
-
974
- // Draw faces as background
975
  for (const face of fold.faces_vertices) {
976
  const pts = face.map(vi => `${toX(verts[vi][0])},${toY(verts[vi][1])}`).join(' ');
977
  svg += `<polygon points="${pts}" fill="#ffffff" stroke="none"/>`;
978
  }
979
 
980
- // Draw edges
981
  for (let i = 0; i < fold.edges_vertices.length; i++) {
982
  const [v1, v2] = fold.edges_vertices[i];
983
  const a = fold.edges_assignment[i];
@@ -988,7 +1641,6 @@ function draw2DCrease(task) {
988
  svg += `<line x1="${toX(verts[v1][0])}" y1="${toY(verts[v1][1])}" x2="${toX(verts[v2][0])}" y2="${toY(verts[v2][1])}" stroke="${color}" stroke-width="${width}" stroke-linecap="round" ${dash}/>`;
989
  }
990
 
991
- // Draw vertices
992
  for (let i = 0; i < verts.length; i++) {
993
  svg += `<circle cx="${toX(verts[i][0])}" cy="${toY(verts[i][1])}" r="3" fill="var(--text)" opacity="0.3"/>`;
994
  }
@@ -998,10 +1650,7 @@ function draw2DCrease(task) {
998
  }
999
 
1000
  function showGrid() {
1001
- document.getElementById('grid-view').classList.remove('hidden');
1002
- document.getElementById('detail-view').classList.remove('active');
1003
- if (detailAnimId) cancelAnimationFrame(detailAnimId);
1004
- currentTaskKey = null;
1005
  }
1006
 
1007
  // ─── SLIDER ───────────────────────────────────────────────────
@@ -1014,10 +1663,18 @@ document.getElementById('crease-slider').addEventListener('input', (e) => {
1014
 
1015
  // ─── BACK BUTTON & KEYBOARD ───────────────────────────────────
1016
 
1017
- document.getElementById('back-btn').addEventListener('click', () => showGrid());
1018
 
1019
  document.addEventListener('keydown', (e) => {
1020
- if (e.key === 'Escape' && currentTaskKey) showGrid();
 
 
 
 
 
 
 
 
1021
  });
1022
 
1023
  // ─── WEBSOCKET (optional live connection) ─────────────────────
@@ -1041,6 +1698,10 @@ function connectWS() {
1041
  }
1042
 
1043
  connectWS();
 
 
 
 
1044
  </script>
1045
 
1046
  </body>
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Origami RL</title>
7
  <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,300&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&display=swap');
9
 
10
  * { margin: 0; padding: 0; box-sizing: border-box; }
11
 
 
23
  --accent: #c0392b;
24
  --accent-soft: #f5eeec;
25
  --success: #27ae60;
26
+ --code-bg: #f3f1ee;
27
  --shadow-sm: 0 1px 3px rgba(26,24,22,0.04);
28
  --shadow-md: 0 4px 16px rgba(26,24,22,0.06);
29
  --shadow-lg: 0 8px 32px rgba(26,24,22,0.08);
 
39
  -webkit-font-smoothing: antialiased;
40
  }
41
 
42
+ /* --- VIEW MANAGEMENT --- */
43
+ .view { display: none; }
44
+ .view.active { display: block; }
45
+ #detail-view.active { display: flex; }
46
+
47
  /* --- HEADER --- */
48
  header {
49
+ padding: 28px 48px 24px;
50
+ display: flex;
51
+ align-items: center;
52
+ justify-content: space-between;
53
+ border-bottom: 1px solid var(--border);
54
+ background: var(--surface);
55
+ }
56
+
57
+ .header-left {
58
  display: flex;
59
  align-items: baseline;
60
+ gap: 14px;
61
  }
62
 
63
  header h1 {
64
  font-family: 'Instrument Serif', serif;
65
+ font-size: 26px;
66
  font-weight: 400;
67
  letter-spacing: -0.02em;
68
  }
69
 
70
+ .header-tag {
71
+ font-size: 11px;
72
+ color: var(--text-tertiary);
73
+ font-weight: 400;
74
+ border: 1px solid var(--border);
75
+ padding: 2px 8px;
76
+ border-radius: 4px;
77
+ }
78
+
79
+ .nav-btn {
80
+ display: flex;
81
+ align-items: center;
82
+ gap: 6px;
83
+ font-size: 13px;
84
+ color: var(--text-secondary);
85
+ cursor: pointer;
86
+ background: none;
87
+ border: 1px solid var(--border);
88
+ border-radius: var(--radius-sm);
89
+ padding: 7px 16px;
90
+ font-family: inherit;
91
+ font-weight: 400;
92
+ transition: all 0.2s;
93
+ letter-spacing: 0.01em;
94
+ }
95
+
96
+ .nav-btn:hover {
97
+ border-color: var(--border-hover);
98
+ color: var(--text);
99
+ background: var(--bg);
100
+ }
101
+
102
+ /* ═══════════════════════════════════════════════════════════
103
+ VIEW 1 β€” LANDING PAGE
104
+ ═══════════════════════════════════════════════════════════ */
105
+
106
+ #landing-view {
107
+ min-height: 100vh;
108
+ }
109
+
110
+ .landing-content {
111
+ max-width: 820px;
112
+ margin: 0 auto;
113
+ padding: 48px 48px 80px;
114
+ }
115
+
116
+ /* Hero */
117
+ .hero {
118
+ margin-bottom: 56px;
119
+ }
120
+
121
+ .hero p {
122
+ font-size: 18px;
123
+ line-height: 1.6;
124
+ color: var(--text-secondary);
125
+ font-weight: 300;
126
+ max-width: 640px;
127
+ }
128
+
129
+ /* Section headings */
130
+ .section {
131
+ margin-bottom: 48px;
132
+ }
133
+
134
+ .section h2 {
135
+ font-family: 'Instrument Serif', serif;
136
+ font-size: 22px;
137
+ font-weight: 400;
138
+ margin-bottom: 20px;
139
+ letter-spacing: -0.01em;
140
+ }
141
+
142
+ .section h3 {
143
  font-size: 13px;
144
+ font-weight: 500;
145
+ text-transform: uppercase;
146
+ letter-spacing: 0.06em;
147
  color: var(--text-tertiary);
148
+ margin-bottom: 16px;
149
+ }
150
+
151
+ /* How it works β€” 3-step flow */
152
+ .steps {
153
+ display: grid;
154
+ grid-template-columns: repeat(3, 1fr);
155
+ gap: 16px;
156
+ }
157
+
158
+ .step {
159
+ background: var(--surface);
160
+ border: 1px solid var(--border);
161
+ border-radius: var(--radius);
162
+ padding: 20px 22px;
163
+ }
164
+
165
+ .step-num {
166
+ font-family: 'Instrument Serif', serif;
167
+ font-size: 28px;
168
+ color: var(--accent);
169
+ line-height: 1;
170
+ margin-bottom: 8px;
171
+ }
172
+
173
+ .step-title {
174
+ font-size: 14px;
175
+ font-weight: 500;
176
+ margin-bottom: 6px;
177
+ }
178
+
179
+ .step-desc {
180
+ font-size: 13px;
181
+ color: var(--text-secondary);
182
+ font-weight: 300;
183
+ line-height: 1.5;
184
+ }
185
+
186
+ /* API Reference */
187
+ .api-group {
188
+ background: var(--surface);
189
+ border: 1px solid var(--border);
190
+ border-radius: var(--radius);
191
+ overflow: hidden;
192
+ margin-bottom: 12px;
193
+ }
194
+
195
+ .api-row {
196
+ padding: 14px 20px;
197
+ display: flex;
198
+ align-items: baseline;
199
+ gap: 16px;
200
+ border-bottom: 1px solid var(--border);
201
+ font-size: 13px;
202
+ }
203
+
204
+ .api-row:last-child {
205
+ border-bottom: none;
206
+ }
207
+
208
+ .api-method {
209
+ font-family: 'JetBrains Mono', monospace;
210
+ font-size: 11px;
211
+ font-weight: 500;
212
+ color: var(--accent);
213
+ background: var(--accent-soft);
214
+ padding: 2px 7px;
215
+ border-radius: 4px;
216
+ white-space: nowrap;
217
+ min-width: 48px;
218
+ text-align: center;
219
+ }
220
+
221
+ .api-method.ws {
222
+ color: var(--valley);
223
+ background: #eaf2f8;
224
+ }
225
+
226
+ .api-path {
227
+ font-family: 'JetBrains Mono', monospace;
228
+ font-size: 12px;
229
+ color: var(--text);
230
+ }
231
+
232
+ .api-desc {
233
+ color: var(--text-secondary);
234
+ font-weight: 300;
235
+ margin-left: auto;
236
+ white-space: nowrap;
237
+ }
238
+
239
+ /* Schema / FOLD format */
240
+ .schema-card {
241
+ background: var(--surface);
242
+ border: 1px solid var(--border);
243
+ border-radius: var(--radius);
244
+ overflow: hidden;
245
+ }
246
+
247
+ .schema-row {
248
+ padding: 12px 20px;
249
+ display: flex;
250
+ gap: 12px;
251
+ border-bottom: 1px solid var(--border);
252
+ font-size: 13px;
253
+ line-height: 1.5;
254
+ }
255
+
256
+ .schema-row:last-child {
257
+ border-bottom: none;
258
+ }
259
+
260
+ .schema-field {
261
+ font-family: 'JetBrains Mono', monospace;
262
+ font-size: 12px;
263
+ color: var(--text);
264
+ font-weight: 500;
265
+ min-width: 160px;
266
+ flex-shrink: 0;
267
+ }
268
+
269
+ .schema-type {
270
+ font-family: 'JetBrains Mono', monospace;
271
+ font-size: 11px;
272
+ color: var(--text-tertiary);
273
+ min-width: 130px;
274
+ flex-shrink: 0;
275
+ }
276
+
277
+ .schema-desc {
278
+ color: var(--text-secondary);
279
+ font-weight: 300;
280
+ }
281
+
282
+ .schema-tag {
283
+ font-size: 10px;
284
+ font-weight: 500;
285
+ padding: 1px 6px;
286
+ border-radius: 3px;
287
+ margin-left: 6px;
288
+ }
289
+
290
+ .schema-tag.required {
291
+ color: var(--accent);
292
+ background: var(--accent-soft);
293
+ }
294
+
295
+ .schema-tag.optional {
296
+ color: var(--text-tertiary);
297
+ background: var(--code-bg);
298
+ }
299
+
300
+ /* Tasks table β€” removed */
301
+
302
+ .tasks-table .task-name {
303
+ font-family: 'JetBrains Mono', monospace;
304
+ font-size: 12px;
305
+ font-weight: 500;
306
+ }
307
+
308
+ .tasks-table .task-diff {
309
+ display: inline-flex;
310
+ gap: 3px;
311
+ }
312
+
313
+ .tasks-table .task-diff .dot {
314
+ width: 7px;
315
+ height: 7px;
316
+ border-radius: 50%;
317
+ background: var(--accent);
318
+ }
319
+
320
+ .tasks-table .task-diff .dot.empty {
321
+ background: var(--border);
322
+ }
323
+
324
+ .tasks-table .task-desc {
325
+ color: var(--text-secondary);
326
  font-weight: 300;
327
  }
328
 
329
+ /* Code block */
330
+ .code-block {
331
+ background: #2b2926;
332
+ border-radius: var(--radius);
333
+ overflow: hidden;
334
+ }
335
+
336
+ .code-header {
337
+ padding: 10px 20px;
338
+ font-size: 11px;
339
+ color: #a09a92;
340
+ border-bottom: 1px solid #3d3a36;
341
+ font-family: 'JetBrains Mono', monospace;
342
+ display: flex;
343
+ align-items: center;
344
+ justify-content: space-between;
345
+ }
346
+
347
+ .code-block pre {
348
+ padding: 20px;
349
+ overflow-x: auto;
350
+ font-family: 'JetBrains Mono', monospace;
351
+ font-size: 12px;
352
+ line-height: 1.7;
353
+ color: #e8e4df;
354
+ }
355
+
356
+ .copy-btn {
357
+ background: none;
358
+ border: 1px solid #3d3a36;
359
+ border-radius: 4px;
360
+ color: #a09a92;
361
+ cursor: pointer;
362
+ padding: 4px 8px;
363
+ display: flex;
364
+ align-items: center;
365
+ gap: 5px;
366
+ font-family: 'JetBrains Mono', monospace;
367
+ font-size: 11px;
368
+ transition: all 0.2s;
369
+ }
370
+
371
+ .copy-btn:hover {
372
+ border-color: #666;
373
+ color: #e8e4df;
374
+ }
375
+
376
+ .copy-btn.copied {
377
+ border-color: var(--success);
378
+ color: var(--success);
379
+ }
380
+
381
+ .code-block .kw { color: #e67e22; }
382
+ .code-block .str { color: #27ae60; }
383
+ .code-block .cmt { color: #7a756e; }
384
+ .code-block .fn { color: #d4a96a; }
385
+ .code-block .num { color: #c0392b; }
386
+
387
+ /* Inline code */
388
+ .section code {
389
+ font-family: 'JetBrains Mono', monospace;
390
+ font-size: 12px;
391
+ background: var(--code-bg);
392
+ padding: 2px 6px;
393
+ border-radius: 4px;
394
+ color: var(--text);
395
+ }
396
+
397
+ /* WebSocket message blocks */
398
+ .ws-block {
399
+ background: var(--code-bg);
400
+ border-radius: var(--radius-sm);
401
+ padding: 12px 16px;
402
+ margin: 8px 0;
403
+ font-family: 'JetBrains Mono', monospace;
404
+ font-size: 12px;
405
+ line-height: 1.6;
406
+ color: var(--text);
407
+ overflow-x: auto;
408
+ }
409
+
410
+ .ws-label {
411
+ font-size: 11px;
412
+ font-weight: 500;
413
+ color: var(--text-tertiary);
414
+ text-transform: uppercase;
415
+ letter-spacing: 0.04em;
416
+ margin-bottom: 4px;
417
+ font-family: 'DM Sans', sans-serif;
418
+ }
419
+
420
+ /* ═══════════════════════════════════════════════════════════
421
+ VIEW 2 β€” GRID
422
+ ═══════════════════════════════════════════════════════════ */
423
+
424
  #grid-view {
425
+ min-height: 100vh;
426
+ }
427
+
428
+ .grid-content {
429
+ padding: 32px 48px 64px;
430
  }
431
 
432
  .grid {
 
511
  text-align: right;
512
  }
513
 
514
+ /* ═══════════════════════════════════════════════════════════
515
+ VIEW 3 β€” DETAIL
516
+ ═══════════════════════════════════════════════════════════ */
517
+
518
  #detail-view {
 
519
  height: 100vh;
520
  flex-direction: column;
521
  }
522
 
 
 
 
 
 
 
 
 
523
  .detail-header {
524
  padding: 20px 32px;
525
  display: flex;
 
569
  min-height: 0;
570
  }
571
 
 
572
  .panel-2d {
573
  border-right: 1px solid var(--border);
574
  background: var(--surface);
 
618
  border-radius: 1px;
619
  }
620
 
 
621
  .panel-3d {
622
  background: var(--bg);
623
  position: relative;
 
716
  .metric-value.mid { color: #e67e22; }
717
  .metric-value.low { color: var(--accent); }
718
 
719
+ /* ═══════════════════════════════════════════════════════════
720
+ ANIMATIONS
721
+ ═══════════════════════════════════════════════════════════ */
722
+
723
  @keyframes fadeIn {
724
  from { opacity: 0; transform: translateY(8px); }
725
  to { opacity: 1; transform: translateY(0); }
 
733
  .card:nth-child(2) { animation-delay: 0.1s; }
734
  .card:nth-child(3) { animation-delay: 0.15s; }
735
  .card:nth-child(4) { animation-delay: 0.2s; }
736
+
737
+ .section {
738
+ animation: fadeIn 0.5s cubic-bezier(0.4, 0, 0.2, 1) both;
739
+ }
740
+ .section:nth-child(1) { animation-delay: 0.05s; }
741
+ .section:nth-child(2) { animation-delay: 0.1s; }
742
+ .section:nth-child(3) { animation-delay: 0.15s; }
743
+ .section:nth-child(4) { animation-delay: 0.2s; }
744
+ .section:nth-child(5) { animation-delay: 0.25s; }
745
+ .section:nth-child(6) { animation-delay: 0.3s; }
746
+ .section:nth-child(7) { animation-delay: 0.35s; }
747
+
748
+ /* Responsive */
749
+ @media (max-width: 680px) {
750
+ header { padding: 20px 24px; }
751
+ .landing-content { padding: 32px 24px 64px; }
752
+ .grid-content { padding: 24px; }
753
+ .steps { grid-template-columns: 1fr; }
754
+ .schema-row { flex-direction: column; gap: 4px; }
755
+ .schema-type { min-width: 0; }
756
+ .schema-field { min-width: 0; }
757
+ .api-row { flex-direction: column; gap: 6px; }
758
+ .api-desc { margin-left: 0; }
759
+ }
760
  </style>
761
  </head>
762
  <body>
763
 
764
+ <!-- ═══════════════════════════════════════════════════════════
765
+ HEADER (shared, content swapped by JS)
766
+ ═══════════════════════════════════════════════════════════ -->
767
+ <header id="main-header">
768
+ <div class="header-left">
769
+ <h1 id="header-title">Origami RL</h1>
770
+ <span class="header-tag" id="header-tag">environment</span>
771
+ </div>
772
+ <button class="nav-btn" id="header-nav" onclick="showView('grid')">Origamis &rarr;</button>
773
  </header>
774
 
775
+ <!-- ═══════════════════════════════════════════════════════════
776
+ VIEW 1 β€” LANDING PAGE
777
+ ═══════════════════════════════════════════════════════════ -->
778
+ <div id="landing-view" class="view active">
779
+ <div class="landing-content">
780
+
781
+ <!-- Hero -->
782
+ <div class="hero section">
783
+ <p>OpenEnv RL environment for origami folding. Submit FOLD crease patterns, get physics simulation + shape similarity reward.</p>
784
+ <p style="margin-top:12px;font-size:14px;color:var(--text-tertiary);font-weight:300">Rewards inspired by AlphaFold &mdash; chamfer distance shape matching with rotational alignment across 24 orientations.</p>
785
+ </div>
786
+
787
+ <!-- How It Works -->
788
+ <div class="section">
789
+ <h2>How It Works</h2>
790
+ <div class="steps">
791
+ <div class="step">
792
+ <div class="step-num">1</div>
793
+ <div class="step-title">Reset</div>
794
+ <div class="step-desc">Pick a task &rarr; get target shape and flat paper starting state</div>
795
+ </div>
796
+ <div class="step">
797
+ <div class="step-num">2</div>
798
+ <div class="step-title">Step</div>
799
+ <div class="step-desc">Submit a FOLD JSON crease pattern describing your fold</div>
800
+ </div>
801
+ <div class="step">
802
+ <div class="step-num">3</div>
803
+ <div class="step-title">Reward</div>
804
+ <div class="step-desc">shape_similarity &times; 20.0 &mdash; score from 0 to 20</div>
805
+ </div>
806
+ </div>
807
+ </div>
808
+
809
+ <!-- FOLD Format -->
810
+ <div class="section">
811
+ <h2>FOLD Format</h2>
812
+ <div class="schema-card">
813
+ <div class="schema-row">
814
+ <span class="schema-field">vertices_coords</span>
815
+ <span class="schema-type">[[x,y], ...]</span>
816
+ <span class="schema-desc">Vertex positions<span class="schema-tag required">required</span></span>
817
+ </div>
818
+ <div class="schema-row">
819
+ <span class="schema-field">edges_vertices</span>
820
+ <span class="schema-type">[[v1,v2], ...]</span>
821
+ <span class="schema-desc">Edge connectivity<span class="schema-tag required">required</span></span>
822
+ </div>
823
+ <div class="schema-row">
824
+ <span class="schema-field">edges_assignment</span>
825
+ <span class="schema-type">["B"|"M"|"V"|"F"|"U"]</span>
826
+ <span class="schema-desc">Edge types. Need &ge;1 M or V, &ge;1 B<span class="schema-tag required">required</span></span>
827
+ </div>
828
+ <div class="schema-row">
829
+ <span class="schema-field">edges_foldAngle</span>
830
+ <span class="schema-type">[degrees, ...]</span>
831
+ <span class="schema-desc">Fold angles. Defaults: V&rarr;180&deg;, M&rarr;&minus;180&deg;<span class="schema-tag optional">optional</span></span>
832
+ </div>
833
+ <div class="schema-row">
834
+ <span class="schema-field">faces_vertices</span>
835
+ <span class="schema-type">[[v0,v1,...], ...]</span>
836
+ <span class="schema-desc">Face polygons. Auto-computed if missing<span class="schema-tag optional">optional</span></span>
837
+ </div>
838
+ </div>
839
+ </div>
840
+
841
+ <!-- Observation -->
842
+ <div class="section">
843
+ <h2>Observation</h2>
844
+ <div class="schema-card">
845
+ <div class="schema-row">
846
+ <span class="schema-field">shape_similarity</span>
847
+ <span class="schema-type">float 0.0&ndash;1.0</span>
848
+ <span class="schema-desc">Procrustes match to target shape</span>
849
+ </div>
850
+ <div class="schema-row">
851
+ <span class="schema-field">final_positions</span>
852
+ <span class="schema-type">[[x,y,z], ...]</span>
853
+ <span class="schema-desc">Folded vertex positions</span>
854
+ </div>
855
+ <div class="schema-row">
856
+ <span class="schema-field">target_positions</span>
857
+ <span class="schema-type">[[x,y,z], ...]</span>
858
+ <span class="schema-desc">Expected target positions</span>
859
+ </div>
860
+ <div class="schema-row">
861
+ <span class="schema-field">max_strain</span>
862
+ <span class="schema-type">float</span>
863
+ <span class="schema-desc">Edge deformation metric</span>
864
+ </div>
865
+ <div class="schema-row">
866
+ <span class="schema-field">is_stable</span>
867
+ <span class="schema-type">bool</span>
868
+ <span class="schema-desc">Convergence flag</span>
869
+ </div>
870
+ <div class="schema-row">
871
+ <span class="schema-field">reward</span>
872
+ <span class="schema-type">float</span>
873
+ <span class="schema-desc">similarity &times; 20.0, or &minus;2.0 on error</span>
874
+ </div>
875
+ </div>
876
+ </div>
877
+
878
+
879
+ <!-- Rewards -->
880
+ <div class="section">
881
+ <h2>Rewards</h2>
882
+ <p style="font-size:14px;color:var(--text-secondary);font-weight:300;line-height:1.6;margin-bottom:20px">
883
+ The environment computes reward from the physics simulation result. Two reward functions are available for training:
884
+ </p>
885
+ <div class="schema-card">
886
+ <div class="schema-row">
887
+ <span class="schema-field">valid_fold</span>
888
+ <span class="schema-type">format reward</span>
889
+ <span class="schema-desc">+1.0 valid FOLD JSON, &minus;0.5 parseable but invalid structure, &minus;2.0 not parseable</span>
890
+ </div>
891
+ <div class="schema-row">
892
+ <span class="schema-field">shape_match</span>
893
+ <span class="schema-type">main reward</span>
894
+ <span class="schema-desc">similarity &times; 20.0 (0&ndash;20). &minus;1.0 if simulation fails, &minus;2.0 if invalid</span>
895
+ </div>
896
+ </div>
897
+ <h3 style="margin-top:24px">How shape_similarity is computed</h3>
898
+ <div class="schema-card">
899
+ <div class="schema-row">
900
+ <span class="schema-field">1. Simulate</span>
901
+ <span class="schema-desc">Run physics engine on submitted crease pattern &rarr; get final 3D vertex positions</span>
902
+ </div>
903
+ <div class="schema-row">
904
+ <span class="schema-field">2. Center</span>
905
+ <span class="schema-desc">Center both predicted and target point clouds at origin</span>
906
+ </div>
907
+ <div class="schema-row">
908
+ <span class="schema-field">3. Align</span>
909
+ <span class="schema-desc">Try 24 rotation alignments (90&deg; rotations + mirrors) to handle equivalent orientations</span>
910
+ </div>
911
+ <div class="schema-row">
912
+ <span class="schema-field">4. Chamfer</span>
913
+ <span class="schema-desc">Bidirectional nearest-neighbor distance, normalized by bounding box diagonal</span>
914
+ </div>
915
+ <div class="schema-row">
916
+ <span class="schema-field">5. Score</span>
917
+ <span class="schema-desc">similarity = 1 &minus; (chamfer / diagonal), clamped to [0, 1]. Reward = similarity &times; 20</span>
918
+ </div>
919
+ </div>
920
+ <p style="font-size:13px;color:var(--text-tertiary);font-weight:300;line-height:1.5;margin-top:12px">
921
+ <code>max_strain</code> measures edge length deviation after folding (0 = no deformation). <code>is_stable</code> indicates whether the simulation converged.
922
+ </p>
923
+ </div>
924
+
925
+ <!-- API Reference -->
926
+ <div class="section">
927
+ <h2>API Reference</h2>
928
+ <h3>WebSocket</h3>
929
+ <div class="api-group">
930
+ <div class="api-row">
931
+ <span class="api-method ws">WS</span>
932
+ <span class="api-path">/ws</span>
933
+ <span class="api-desc">Persistent connection</span>
934
+ </div>
935
+ </div>
936
+ <div class="ws-label">Send: Reset</div>
937
+ <div class="ws-block">{"type": "reset", "data": {"task_name": "triangle"}}</div>
938
+ <div class="ws-label">Send: Step</div>
939
+ <div class="ws-block">{"type": "step", "data": {"fold_data": {...}}}</div>
940
+ <div class="ws-label">Receive: Observation</div>
941
+ <div class="ws-block">{"type": "observation", "data": {"reward": 20.0, "done": true, ...}}</div>
942
+
943
+ <h3 style="margin-top:24px">REST</h3>
944
+ <div class="api-group">
945
+ <div class="api-row">
946
+ <span class="api-method">POST</span>
947
+ <span class="api-path">/sessions</span>
948
+ <span class="api-desc">Create session</span>
949
+ </div>
950
+ <div class="api-row">
951
+ <span class="api-method">POST</span>
952
+ <span class="api-path">/sessions/{id}/reset</span>
953
+ <span class="api-desc">Reset with task_name</span>
954
+ </div>
955
+ <div class="api-row">
956
+ <span class="api-method">POST</span>
957
+ <span class="api-path">/sessions/{id}/step</span>
958
+ <span class="api-desc">Submit fold action</span>
959
+ </div>
960
+ <div class="api-row">
961
+ <span class="api-method" style="background:#eaf2f8;color:var(--valley)">GET</span>
962
+ <span class="api-path">/tasks</span>
963
+ <span class="api-desc">List all tasks</span>
964
+ </div>
965
+ <div class="api-row">
966
+ <span class="api-method" style="background:#eaf2f8;color:var(--valley)">GET</span>
967
+ <span class="api-path">/tasks/{name}</span>
968
+ <span class="api-desc">Task detail + target fold</span>
969
+ </div>
970
+ </div>
971
+ </div>
972
+
973
+ <!-- Quick Start -->
974
+ <div class="section">
975
+ <h2>Quick Start</h2>
976
+ <div class="code-block">
977
+ <div class="code-header"><span>python</span><button class="copy-btn" onclick="copyCode(this)" title="Copy to clipboard"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button></div>
978
+ <pre><span class="kw">from</span> origami_env.client <span class="kw">import</span> OrigamiEnv
979
+ <span class="kw">from</span> origami_env.models <span class="kw">import</span> OrigamiAction
980
+
981
+ <span class="kw">with</span> <span class="fn">OrigamiEnv</span>(base_url=<span class="str">"http://localhost:8000"</span>) <span class="kw">as</span> env:
982
+ env.<span class="fn">reset</span>(task_name=<span class="str">"triangle"</span>)
983
+ result = env.<span class="fn">step</span>(<span class="fn">OrigamiAction</span>(fold_data={
984
+ <span class="str">"vertices_coords"</span>: [[<span class="num">0</span>,<span class="num">0</span>],[<span class="num">1</span>,<span class="num">0</span>],[<span class="num">1</span>,<span class="num">1</span>],[<span class="num">0</span>,<span class="num">1</span>]],
985
+ <span class="str">"edges_vertices"</span>: [[<span class="num">0</span>,<span class="num">1</span>],[<span class="num">1</span>,<span class="num">2</span>],[<span class="num">2</span>,<span class="num">3</span>],[<span class="num">3</span>,<span class="num">0</span>],[<span class="num">0</span>,<span class="num">2</span>]],
986
+ <span class="str">"edges_assignment"</span>: [<span class="str">"B"</span>,<span class="str">"B"</span>,<span class="str">"B"</span>,<span class="str">"B"</span>,<span class="str">"V"</span>],
987
+ <span class="str">"edges_foldAngle"</span>: [<span class="num">0</span>,<span class="num">0</span>,<span class="num">0</span>,<span class="num">0</span>,<span class="num">180</span>]
988
+ }))
989
+ <span class="fn">print</span>(result.observation.shape_similarity) <span class="cmt"># 1.0</span></pre>
990
+ </div>
991
+
992
+ <div class="code-block" style="margin-top:16px">
993
+ <div class="code-header"><span>python &mdash; all tasks</span><button class="copy-btn" onclick="copyCode(this)" title="Copy to clipboard"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button></div>
994
+ <pre><span class="kw">import</span> requests
995
+
996
+ <span class="cmt"># Fetch available tasks</span>
997
+ tasks = requests.<span class="fn">get</span>(<span class="str">"http://localhost:8000/tasks"</span>).<span class="fn">json</span>()
998
+
999
+ <span class="kw">for</span> name, info <span class="kw">in</span> tasks.<span class="fn">items</span>():
1000
+ <span class="fn">print</span>(<span class="str">f"</span>{name}<span class="str">: difficulty </span>{info[<span class="str">'difficulty'</span>]}<span class="str">"</span>)
1001
+
1002
+ <span class="cmt"># Get target crease pattern for this task</span>
1003
+ detail = requests.<span class="fn">get</span>(<span class="str">f"http://localhost:8000/tasks/</span>{name}<span class="str">"</span>).<span class="fn">json</span>()
1004
+ target = detail[<span class="str">"target_fold"</span>]
1005
+
1006
+ <span class="cmt"># Create session, reset, step</span>
1007
+ s = requests.<span class="fn">post</span>(<span class="str">"http://localhost:8000/sessions"</span>).<span class="fn">json</span>()
1008
+ sid = s[<span class="str">"session_id"</span>]
1009
+ requests.<span class="fn">post</span>(<span class="str">f"http://localhost:8000/sessions/</span>{sid}<span class="str">/reset"</span>, json={<span class="str">"task_name"</span>: name})
1010
+ obs = requests.<span class="fn">post</span>(<span class="str">f"http://localhost:8000/sessions/</span>{sid}<span class="str">/step"</span>, json={<span class="str">"fold_data"</span>: target}).<span class="fn">json</span>()
1011
+ <span class="fn">print</span>(<span class="str">f" reward: </span>{obs[<span class="str">'reward'</span>]}<span class="str">, similarity: </span>{obs[<span class="str">'shape_similarity'</span>]}<span class="str">"</span>)</pre>
1012
+ </div>
1013
+ </div>
1014
+
1015
+ </div>
1016
+ </div>
1017
+
1018
+ <!-- ═══════════════════════════════════════════════════════════
1019
+ VIEW 2 β€” GRID
1020
+ ═══════════════════════════════════════════════════════════ -->
1021
+ <div id="grid-view" class="view">
1022
+ <div class="grid-content">
1023
+ <div class="grid" id="grid"></div>
1024
+ </div>
1025
  </div>
1026
 
1027
+ <!-- ═══════════════════════════════════════════════════════════
1028
+ VIEW 3 β€” DETAIL
1029
+ ═══════════════════════════════════════════════════════════ -->
1030
+ <div id="detail-view" class="view">
1031
  <div class="detail-header">
1032
+ <button class="back-btn" id="back-btn">&larr; Back</button>
1033
  <div class="detail-title" id="detail-title"></div>
1034
  <div class="detail-subtitle" id="detail-subtitle"></div>
1035
  </div>
 
1062
  <div class="metrics-bar">
1063
  <div class="metric">
1064
  <div class="metric-label">Similarity</div>
1065
+ <div class="metric-value high" id="m-sim">&mdash;</div>
1066
  </div>
1067
  <div class="metric">
1068
  <div class="metric-label">Folds</div>
1069
+ <div class="metric-value" id="m-folds">&mdash;</div>
1070
  </div>
1071
  <div class="metric">
1072
  <div class="metric-label">Strain</div>
1073
+ <div class="metric-value" id="m-strain">&mdash;</div>
1074
  </div>
1075
  <div class="metric">
1076
  <div class="metric-label">Vertices</div>
1077
+ <div class="metric-value" id="m-verts">&mdash;</div>
1078
  </div>
1079
  <div class="metric">
1080
  <div class="metric-label">Status</div>
1081
+ <div class="metric-value" id="m-status">&mdash;</div>
1082
  </div>
1083
  </div>
1084
  </div>
1085
 
1086
+ <script>
1087
+ function copyCode(btn) {
1088
+ const pre = btn.closest('.code-block').querySelector('pre');
1089
+ const text = pre.textContent;
1090
+ navigator.clipboard.writeText(text).then(() => {
1091
+ btn.classList.add('copied');
1092
+ btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
1093
+ setTimeout(() => {
1094
+ btn.classList.remove('copied');
1095
+ btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
1096
+ }, 2000);
1097
+ });
1098
+ }
1099
+ </script>
1100
+
1101
  <script type="importmap">
1102
  {
1103
  "imports": {
 
1111
  import * as THREE from 'three';
1112
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
1113
 
1114
+ // ─── FOLD DATA ─────────────────────────────────────────────
1115
  const TASKS = {
1116
  triangle: {
1117
  name: 'Triangle',
 
1216
  }
1217
 
1218
  // Per-face cumulative transform: folded = R * flat + t
 
1219
  const faceR = new Array(faces.length).fill(null);
1220
  const faceT = new Array(faces.length).fill(null);
 
1221
  faceR[0] = [1,0,0, 0,1,0, 0,0,1];
1222
  faceT[0] = [0,0,0];
1223
 
 
1244
  let Rfj, tfj;
1245
 
1246
  if (Math.abs(angle) > 1e-10) {
 
1247
  const p1 = [pos[v1*3], pos[v1*3+1], pos[v1*3+2]];
1248
  const ax = [pos[v2*3]-p1[0], pos[v2*3+1]-p1[1], pos[v2*3+2]-p1[2]];
1249
  const axLen = Math.sqrt(ax[0]*ax[0]+ax[1]*ax[1]+ax[2]*ax[2]);
 
1254
  const u = [ax[0]/axLen, ax[1]/axLen, ax[2]/axLen];
1255
  const foldR = rodriguesMat(u, angle);
1256
  Rfj = matMul(foldR, faceR[fi]);
 
1257
  const dt = [faceT[fi][0]-p1[0], faceT[fi][1]-p1[1], faceT[fi][2]-p1[2]];
1258
  const rdt = matVec(foldR, dt);
1259
  tfj = [rdt[0]+p1[0], rdt[1]+p1[1], rdt[2]+p1[2]];
 
1266
  faceR[fj] = Rfj;
1267
  faceT[fj] = tfj;
1268
 
 
1269
  for (const vi of faces[fj]) {
1270
  if (!placed.has(vi)) {
1271
  const fv = [flat[vi*3], flat[vi*3+1], flat[vi*3+2]];
 
1283
  return pos;
1284
  }
1285
 
 
1286
  function rodriguesMat(u, angle) {
1287
  const c = Math.cos(angle), s = Math.sin(angle), t = 1 - c;
1288
  return [
 
1292
  ];
1293
  }
1294
 
 
1295
  function matMul(a, b) {
1296
  return [
1297
  a[0]*b[0]+a[1]*b[3]+a[2]*b[6], a[0]*b[1]+a[1]*b[4]+a[2]*b[7], a[0]*b[2]+a[1]*b[5]+a[2]*b[8],
 
1300
  ];
1301
  }
1302
 
 
1303
  function matVec(m, v) {
1304
  return [
1305
  m[0]*v[0]+m[1]*v[1]+m[2]*v[2],
 
1314
 
1315
  function buildMesh(task, creasePercent = 1) {
1316
  const fold = task.fold;
 
1317
  const group = new THREE.Group();
 
 
1318
  const positions = analyticalFold(fold, creasePercent);
1319
 
 
1320
  const faces = fold.faces_vertices;
1321
  const indices = [];
1322
  for (const face of faces) {
 
1340
  const mesh = new THREE.Mesh(geo, mat);
1341
  group.add(mesh);
1342
 
 
1343
  for (let i = 0; i < fold.edges_vertices.length; i++) {
1344
  const [v1, v2] = fold.edges_vertices[i];
1345
  const assignment = fold.edges_assignment[i];
 
1357
  group.add(new THREE.LineSegments(lineGeo, lineMat));
1358
  }
1359
 
 
1360
  const box = new THREE.Box3().setFromObject(group);
1361
  const center = box.getCenter(new THREE.Vector3());
1362
  group.position.sub(center);
 
1379
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
1380
  container.appendChild(renderer.domElement);
1381
 
 
1382
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
1383
  scene.add(ambientLight);
1384
  const dirLight1 = new THREE.DirectionalLight(0xffffff, 0.7);
 
1403
  return { scene, camera, renderer, controls };
1404
  }
1405
 
1406
+ // ─── VIEW NAVIGATION ──────────────────────────────────────────
1407
 
1408
+ let gridInitialized = false;
1409
  const cardScenes = [];
1410
 
1411
+ function showView(name) {
1412
+ // Hide all views
1413
+ document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
1414
+
1415
+ // Update header
1416
+ const headerTitle = document.getElementById('header-title');
1417
+ const headerTag = document.getElementById('header-tag');
1418
+ const headerNav = document.getElementById('header-nav');
1419
+
1420
+ if (name === 'landing') {
1421
+ document.getElementById('landing-view').classList.add('active');
1422
+ headerTitle.textContent = 'Origami RL';
1423
+ headerTag.textContent = 'environment';
1424
+ headerTag.style.display = '';
1425
+ headerNav.textContent = 'Origamis \u2192';
1426
+ headerNav.onclick = () => showView('grid');
1427
+ headerNav.style.display = '';
1428
+ document.getElementById('main-header').style.display = '';
1429
+ } else if (name === 'grid') {
1430
+ document.getElementById('grid-view').classList.add('active');
1431
+ headerTitle.textContent = 'Origamis';
1432
+ headerTag.style.display = 'none';
1433
+ headerNav.textContent = '\u2190 Back';
1434
+ headerNav.onclick = () => showView('landing');
1435
+ headerNav.style.display = '';
1436
+ document.getElementById('main-header').style.display = '';
1437
+ if (!gridInitialized) initGrid();
1438
+ } else if (name === 'detail') {
1439
+ document.getElementById('detail-view').classList.add('active');
1440
+ document.getElementById('main-header').style.display = 'none';
1441
+ }
1442
  }
1443
 
1444
+ // Expose globally for inline onclick
1445
+ window.showView = showView;
1446
+
1447
+ // ─── GRID VIEW ────────────────────────────────────────────────
1448
+
1449
+ function initGrid() {
1450
+ gridInitialized = true;
1451
+ const gridEl = document.getElementById('grid');
1452
+
1453
  for (const [key, task] of Object.entries(TASKS)) {
1454
+ const card = document.createElement('div');
1455
+ card.className = 'card';
1456
+ card.innerHTML = `
1457
+ <div class="card-canvas" id="card-${key}"></div>
1458
+ <div class="card-info">
1459
+ <div class="card-name">${task.name}</div>
1460
+ <div class="card-desc">${task.description} \u00b7 difficulty ${task.difficulty}</div>
1461
+ <div class="card-score">
1462
+ <div class="score-bar-bg"><div class="score-bar-fill" style="width:${task.similarity * 100}%"></div></div>
1463
+ <div class="score-label">${Math.round(task.similarity * 100)}%</div>
1464
+ </div>
1465
+ </div>
1466
+ `;
1467
+ card.addEventListener('click', () => showDetail(key));
1468
+ gridEl.appendChild(card);
1469
  }
1470
+
1471
+ // Init mini 3D scenes after DOM paint
1472
+ requestAnimationFrame(() => {
1473
+ for (const [key, task] of Object.entries(TASKS)) {
1474
+ const container = document.getElementById(`card-${key}`);
1475
+ if (!container) continue;
1476
+ const { scene, camera, renderer } = setupScene(container, { autoRotate: true });
1477
+ const mesh = buildMesh(task, 1.0);
1478
+ scene.add(mesh);
1479
+
1480
+ const animate = () => {
1481
+ requestAnimationFrame(animate);
1482
+ mesh.rotation.y += 0.005;
1483
+ renderer.render(scene, camera);
1484
+ };
1485
+ animate();
1486
+ cardScenes.push({ key, scene, camera, renderer, mesh });
1487
+ }
1488
+ });
1489
+ }
1490
 
1491
  // ─── DETAIL VIEW ──────────────────────────────────────────────
1492
 
 
1502
  currentTaskKey = key;
1503
  const task = TASKS[key];
1504
 
1505
+ showView('detail');
 
1506
 
1507
  document.getElementById('detail-title').textContent = task.name;
1508
  document.getElementById('detail-subtitle').textContent = task.description;
 
1523
 
1524
  // 3D viewer
1525
  const panel = document.getElementById('panel-3d');
 
1526
  if (detailRenderer) {
1527
  panel.removeChild(detailRenderer.domElement);
1528
  detailRenderer.dispose();
 
1535
  detailRenderer = setup.renderer;
1536
  detailControls = setup.controls;
1537
 
 
1538
  const slider = document.getElementById('crease-slider');
1539
  slider.value = 100;
1540
  document.getElementById('slider-val').textContent = '100%';
1541
 
 
1542
  updateDetailMesh(1.0);
 
 
1543
  addTargetGhost(task);
1544
 
 
1545
  const animateDetail = () => {
1546
  detailAnimId = requestAnimationFrame(animateDetail);
1547
  detailControls.update();
 
1549
  };
1550
  animateDetail();
1551
 
 
1552
  const resizeObserver = new ResizeObserver(() => {
1553
  const w = panel.clientWidth;
1554
  const h = panel.clientHeight;
 
1563
  if (!currentTaskKey) return;
1564
  const task = TASKS[currentTaskKey];
1565
 
 
1566
  if (detailMeshGroup) {
1567
  detailScene.remove(detailMeshGroup);
1568
  }
 
1573
 
1574
  function addTargetGhost(task) {
1575
  const fold = task.fold;
 
 
 
1576
  const positions = analyticalFold(fold, 1.0);
1577
 
1578
  const faces = fold.faces_vertices;
 
1595
  });
1596
 
1597
  const ghost = new THREE.Mesh(geo, ghostMat);
 
 
1598
  const box = new THREE.Box3().setFromBufferAttribute(geo.getAttribute('position'));
1599
  const center = box.getCenter(new THREE.Vector3());
1600
  ghost.position.sub(center);
 
1607
  const fold = task.fold;
1608
  const verts = fold.vertices_coords;
1609
 
 
1610
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1611
  for (const v of verts) {
1612
  minX = Math.min(minX, v[0]); minY = Math.min(minY, v[1]);
 
1620
  const scale = size / Math.max(vw, vh);
1621
 
1622
  const toX = (x) => (x - minX + pad) * scale;
1623
+ const toY = (y) => size - (y - minY + pad) * scale;
1624
 
1625
  const STROKE = { M: '#c0392b', V: '#2471a3', B: '#1a1816', F: '#ddd8d0', U: '#a9a49d' };
1626
 
1627
  let svg = `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">`;
1628
 
 
 
 
 
 
 
 
 
 
 
 
 
1629
  for (const face of fold.faces_vertices) {
1630
  const pts = face.map(vi => `${toX(verts[vi][0])},${toY(verts[vi][1])}`).join(' ');
1631
  svg += `<polygon points="${pts}" fill="#ffffff" stroke="none"/>`;
1632
  }
1633
 
 
1634
  for (let i = 0; i < fold.edges_vertices.length; i++) {
1635
  const [v1, v2] = fold.edges_vertices[i];
1636
  const a = fold.edges_assignment[i];
 
1641
  svg += `<line x1="${toX(verts[v1][0])}" y1="${toY(verts[v1][1])}" x2="${toX(verts[v2][0])}" y2="${toY(verts[v2][1])}" stroke="${color}" stroke-width="${width}" stroke-linecap="round" ${dash}/>`;
1642
  }
1643
 
 
1644
  for (let i = 0; i < verts.length; i++) {
1645
  svg += `<circle cx="${toX(verts[i][0])}" cy="${toY(verts[i][1])}" r="3" fill="var(--text)" opacity="0.3"/>`;
1646
  }
 
1650
  }
1651
 
1652
  function showGrid() {
1653
+ showView('grid');
 
 
 
1654
  }
1655
 
1656
  // ─── SLIDER ───────────────────────────────────────────────────
 
1663
 
1664
  // ─── BACK BUTTON & KEYBOARD ───────────────────────────────────
1665
 
1666
+ document.getElementById('back-btn').addEventListener('click', () => showView('grid'));
1667
 
1668
  document.addEventListener('keydown', (e) => {
1669
+ if (e.key === 'Escape') {
1670
+ if (currentTaskKey) {
1671
+ showView('grid');
1672
+ currentTaskKey = null;
1673
+ if (detailAnimId) cancelAnimationFrame(detailAnimId);
1674
+ } else if (document.getElementById('grid-view').classList.contains('active')) {
1675
+ showView('landing');
1676
+ }
1677
+ }
1678
  });
1679
 
1680
  // ─── WEBSOCKET (optional live connection) ─────────────────────
 
1698
  }
1699
 
1700
  connectWS();
1701
+
1702
+ // ─── START ────────────────────────────────────────────────────
1703
+
1704
+ showView('landing');
1705
  </script>
1706
 
1707
  </body>