File size: 6,888 Bytes
6fff552
97ba36c
 
6fff552
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97ba36c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6fff552
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97ba36c
6fff552
 
 
 
 
 
97ba36c
 
 
6fff552
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97ba36c
6fff552
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
// A set's validation report: each probe's MuJoCo trace replayed in 3D, as delivered and after the fix, beside the
// hidden checks in MuJoCo and Genesis, the static tipping margin, the per-knife numbers, how far anything went into
// anything else, and the MJCF + OBJ export.
import { SimViewer } from "./viewer.js";

const $ = (id) => document.getElementById(id);
const el = (tag, cls, text) => { const e = document.createElement(tag); if (cls) e.className = cls; if (text !== undefined) e.textContent = text; return e; };
const PROBES = { rest: "Rest", seat: "Insert", extract: "Pull", tip: "Lean" };
const ABOUT = {
  rest: "Every body starts at its designed pose and settles for 1.5 s: how far each knife ends from its seat.",
  seat: "Each knife is dropped into its slot from 2 cm above: does it go all the way in?",
  extract: "Each knife in turn is pulled out along its slot, the force rising to 3 times its weight: when does it start to come out, and does the block lift?",
  tip: "The table tilts toward the block's back, the way it and the handles lean, in 5° steps: the first angle where something gives.",
};

export async function mount(name) {
  const r = await (await fetch(`validation/${name}.json`)).json();
  const meta = await (await fetch(`scenes/${name}.json`)).json();
  const v = new SimViewer($("vr-view"), { onFrame: (st) => { $("vr-label").hidden = !(st && st.label); $("vr-label").textContent = (st && st.label) || ""; } });
  await v.load(`scenes/${name}.glb`, meta);
  $("vr-grid").textContent = v.gridLabel;
  let state = 0, probe = "seat";

  r.states.forEach((s, i) => {
    const b = el("button", "btn", s.label);
    b.onclick = () => { state = i; draw(); };
    $("vr-states").append(b);
  });
  for (const [k, lab] of Object.entries(PROBES)) {
    const b = el("button", "btn", lab);
    b.dataset.k = k;
    b.onclick = () => { probe = k; draw(); };
    $("vr-probes").append(b);
  }
  $("vr-replay").onclick = () => v.play(r.states[state].probes[probe].trace);
  $("vr-export").href = r.export;
  const names = Object.fromEntries(r.bodies.map((b) => [b.name, b.label || b.name]));
  names.floor = "the floor";
  const say = (o) => `${names[o.body] || o.body} ${o.mm.toFixed(1)} mm into ${names[o.into] || o.into}`;

  // how far anything went into anything else in the run on screen (for the lean, the angle replayed), with a button
  // that stops the replay at that moment and lights up the body
  function overlap(p) {
    const box = $("vr-overlap");
    box.textContent = "";
    const ov = p.trace.kind === "tip" ? (p.result.rows.find((x) => x.deg === p.trace.lean_deg) || {}).overlap : p.result.overlap;
    if (!ov) return;
    const deep = ov.pairs[0], end = ov.end.pairs[0];
    box.append(el("b", "", "Overlap. "));
    if (!deep) { box.append("Nothing goes into anything else in this run."); return; }
    box.append(`Deepest: ${say(deep)}, at ${deep.t.toFixed(2)} s. `);
    const b = el("button", "btn", "Show it");
    b.onclick = () => {
      v.seek(deep.t, true);
      v.playing = false;
      v.highlight(v.meta.geoms.filter((g) => g.link === deep.body).map((g) => g.part));
    };
    box.append(b, ` Once it settles: ${end ? say(end) : "nothing is inside anything else"}.`);
  }

  function draw() {
    const s = r.states[state], p = s.probes[probe];
    [...$("vr-states").children].forEach((b, i) => b.classList.toggle("pri", i === state));
    [...$("vr-probes").children].forEach((b) => b.classList.toggle("pri", b.dataset.k === probe));
    $("vr-about").textContent = ABOUT[probe];
    v.play(p.trace);
    // checks: MuJoCo grades; Genesis gives a second opinion on the checks it runs
    const g = Object.fromEntries(s.second_opinion.checks.map((c) => [c.id, c.result]));
    const t = $("vr-checks");
    t.textContent = "";
    const head = el("tr");
    for (const h of ["Check", "MuJoCo", "Genesis", "What it measured"]) head.append(el("th", "", h));
    t.append(head);
    for (const c of s.checks) {
      const tr = el("tr");
      tr.append(el("td", "", c.id), el("td", c.result ? "ok" : "bad", c.result ? "pass" : "fail"),
        el("td", c.id in g ? (g[c.id] ? "ok" : "bad") : "na", c.id in g ? (g[c.id] ? "pass" : "fail") : "not run"), el("td", "", c.description));
      t.append(tr);
    }
    $("vr-verdict").textContent = `${s.label}: ${s.passed ? "passes every check" : `fails ${s.checks.filter((c) => !c.result).length} of ${s.checks.length}`}` +
      (s.second_opinion.disagree.length ? `; Genesis disagrees on ${s.second_opinion.disagree.join(", ")}` : "; Genesis agrees wherever it runs");
    $("vr-verdict").className = "vr-verdict " + (s.passed ? "ok" : "bad");
    // per knife, for this probe
    const k = $("vr-knives");
    k.textContent = "";
    const res = p.result;
    const cols = probe === "extract" ? ["Knife", "Starts to come out at", "Block lift"] : probe === "tip" ? ["Tilt", "What happens", "Block tilt", "Left inside, at rest"] : ["Knife", "Out of its slot", "Turned"];
    const hr = el("tr");
    cols.forEach((c) => hr.append(el("th", "", c)));
    k.append(hr);
    if (probe === "tip") {
      for (const row of res.rows.slice(0, 6)) {
        const tr = el("tr");
        const e = row.overlap && row.overlap.end.pairs[0];
        tr.append(el("td", "", `${row.deg}°`), el("td", row.mode === "holds" ? "ok" : "bad", row.mode.replace(/_/g, " ")), el("td", "", `${row.block_tilt_deg}°`),
          el("td", e && e.mm > 0.5 ? "bad" : "", e ? `${e.mm.toFixed(1)} mm` : "none"));
        k.append(tr);
      }
    } else {
      for (const [n, x] of Object.entries(res.knives)) {
        const tr = el("tr");
        const lab = (res.labels && res.labels[n]) || n;
        if (probe === "extract") {
          const f = x.freed_at_weight_ratio;
          tr.append(el("td", "", lab), el("td", f !== null && f <= 2 ? "ok" : "bad", f === null ? "stuck" : `${f.toFixed(2)}× its weight`), el("td", "", `${x.block_lift_mm} mm`));
        } else {
          const o = x.out_of_slot_mm;
          tr.append(el("td", "", lab), el("td", Math.abs(o) <= 2 ? "ok" : "bad", `${o.toFixed(1)} mm`), el("td", "", `${x.rotation_deg}°`));
        }
        k.append(tr);
      }
    }
    overlap(p);
    // stability: how far it can tilt each way before its centre of mass passes the edge of its footprint
    const st = s.stability.tip_angle_deg;
    $("vr-stab").textContent = "";
    for (const [side, a] of Object.entries(st)) {
      const d = el("div", a < 12 ? "bad" : "");
      d.append(el("span", "", side), el("b", "", `${a}°`));
      $("vr-stab").append(d);
    }
    $("vr-load").textContent = s.load_check
      ? `Loads in plain MuJoCo ${s.load_check.mujoco}: ${Object.keys(s.load_check.objects).length} object files and a scene that settles within ${Math.max(...Object.values(s.load_check.scene.drift_mm_after_1s)).toFixed(1)} mm in 1 s.`
      : "";
  }
  draw();
}