aboutsummaryrefslogtreecommitdiffstats
path: root/prototypes/workbench/web/output.mjs
blob: 694fce639820e67305046026222369a6e5b50122 (plain)
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
// Bind authored declarations to concrete DOM destinations once per apply.
// The DOM holds the published values; no parallel scene/value model is kept.
const NS = 'http://www.w3.org/2000/svg';
const number = value => Number.isInteger(value) ? String(value) : Number(value.toPrecision(7)).toString();
const attributes = { box: { x: 'x', y: 'y', width: 'width', height: 'height' },
  circle: { x: 'cx', y: 'cy', radius: 'r' }, text: { x: 'x', y: 'y' } };

function text(node, value) {
  if (node.textContent !== value) node.textContent = value;
}
function attribute(node, name, value) {
  const next = String(value);
  if (node.getAttribute(name) !== next) node.setAttribute(name, next);
}

export function createOutput(document) {
  const get = id => document.getElementById(id);
  const scene = get('scene-content'), states = get('state-values');
  const empty = get('empty-scene'), description = get('scene-description');
  let active = undefined, nodes = [], amounts = [];
  const costNames = ['Wasm 선형 메모리', '코어 예약 공간', '사용 중인 명령·연결',
    '현재 상태 값', '새 코드 준비 공간 (예약에 포함)', '평가 스택 용량',
    '마지막 이벤트 실행 명령', '적용된 소스 크기'];
  const costs = costNames.map(label => {
    const dt = document.createElement('dt'), dd = document.createElement('dd');
    dt.textContent = label;
    return { dt, dd };
  });
  get('cost-values').replaceChildren(...costs.flatMap(({dt, dd}) => [dt, dd]));

  return {
    begin(program) {
      if (program === active) return;
      active = program;
      const targets = new Map();
      for (const input of program?.inputs ?? []) {
        if (input.target) targets.set(input.target, {name:input.name});
        for (const route of input.routes ?? []) targets.set(route.target, {name:input.name,target:route.target});
      }
      scene.ownerSVGElement.classList.toggle('direct-input', targets.size > 0);
      nodes = (program?.views ?? []).map(view => {
        const node = document.createElementNS(NS, {box:'rect', circle:'circle', text:'text'}[view.type]);
        node.setAttribute('data-view-id', view.id);
        if (targets.has(view.id)) {
          const input = targets.get(view.id);
          node.setAttribute('data-point-input', input.name);
          if (input.target) node.setAttribute('data-point-target', input.target);
        }
        node.setAttribute('fill', view.color ?? '#e7ece6');
        if (view.type === 'text') {
          node.setAttribute('font-family', 'ui-monospace, monospace');
          node.setAttribute('font-size', '20');
        }
        return node;
      });
      scene.replaceChildren(...nodes);
      const holders = [];
      amounts = (program?.states ?? []).map(state => {
        const dl = document.createElement('dl'), dt = document.createElement('dt'), dd = document.createElement('dd');
        dl.className = 'state-value'; dt.textContent = state.name; dd.dataset.state = state.name;
        dl.append(dt, dd); holders.push(dl);
        return dd;
      });
      if (!holders.length) {
        const note = document.createElement('p');
        note.className = 'empty-note'; note.textContent = '선언한 상태의 값이 표시됩니다.';
        holders.push(note);
      }
      states.replaceChildren(...holders);
    },
    state(index, value) { text(amounts[index], number(value)); },
    view(index, valid) {
      const node = nodes[index];
      if (valid) { if (node.hasAttribute('display')) node.removeAttribute('display'); }
      else attribute(node, 'display', 'none');
    },
    value(index, name, value) {
      const view = active.views[index], node = nodes[index];
      if (name === 'value') text(node, `${view.prefix || ''}${number(value)}`);
      else attribute(node, attributes[view.type][name], value);
    },
    end(visible) {
      const hidden = visible !== 0;
      if (empty.hidden !== hidden) empty.hidden = hidden;
      const failed = !visible && nodes.length > 0;
      text(empty.querySelector('strong'), failed ? '장면을 표시할 수 없습니다' : '아직 아무것도 없습니다');
      text(empty.querySelector('span:last-child'), failed ? '현재 상태 아래의 표시 오류를 확인하세요.' : '도형이나 글자를 선언하고 코드를 적용하세요.');
      text(description, visible ? `사용자가 만든 장면. 도형과 글자 ${visible}개.` : failed ? '표시 오류로 장면을 표시할 수 없습니다.' : '사용자가 코드로 만든 장면. 현재 비어 있습니다.');
    },
    costs(status, metrics, droppedTicks) {
      const m = status.memory;
      const values = [m.linearBytes, m.reservedEngineBytes, m.activeIRBytes, m.activeStateBytes,
        m.stagingReservedBytes, m.evalStackBytes, status.lastOps, metrics?.sourceBytes];
      for (let i = 0; i < values.length; i++) {
        const value = values[i], {dt, dd} = costs[i], hidden = !Number.isFinite(value);
        if (dt.hidden !== hidden) dt.hidden = hidden;
        if (dd.hidden !== hidden) dd.hidden = hidden;
        if (!hidden) text(dd, i === 6 ? `${value}개` : `${value.toLocaleString('en-US')} B`);
      }
      text(get('dropped-ticks'), `시간 지연으로 건너뛴 프레임: ${droppedTicks}`);
    },
  };
}