index.jsx

5.8 kB · jsx · 109 lines

1import { useRef, useState } from 'react';2import { ready, ink } from '../../lib/mrly.js';3import { mount, Page, Row, Pick, Stats, Stat, Note, Group } from '../../lib/app.jsx';4import { Markup, Sketch } from '../../lib/draw.jsx';5import { useQuery } from '../../lib/query.js';6import { useSeeds, seeded, Picker } from '../../lib/select.jsx';7import { board, bars, line, axis, tag } from '../../lib/chart.js';89const m = await ready();10const TOP = 16;11const TALLY = [12  ['boundary', 'boundary'], ['edges', 'edges'], ['interior', 'interior'], ['vertices', 'vertices'], ['euler', 'euler'],13  ['fills', 'filled'], ['voids', 'empty'], ['components', 'pieces'], ['holes', 'holes'], ['giant', 'largest piece'],14];1516const capOf = (fractal) => (fractal ? m.level_cap(fractal, 1, 100) : m.level_cap(1, 1, 1));1718function App() {19  const s = useSeeds();20  const [pick, set] = useQuery({ code: seeded(s, 3, 2, '23'), k: 2, level: 1 });21  const [fractal, setFractal] = useState(0);22  const shown = useRef(null);2324  const lid = capOf(fractal);25  const level = Math.min(pick.level, lid);26  const k = Math.min(TOP, Math.max(1, pick.k));27  const code = pick.code.trim();2829  let error = null, art = null, split = null;30  try {31    const rows = JSON.parse(m.slice_series(code, TOP));32    const number = fractal || rows[k - 1].n;33    const tally = JSON.parse(m.slice_census(code, number, level, 2));34    shown.current = {35      rows, tally, here: fractal ? 0 : k,36      name: m.name_of(code, 3, 2),37      reading: fractal ? `tile ${number}` : `k ${k}, n ${number}`,38    };39    if (level === 1) split = JSON.parse(m.slice_partition(number));40    const svg = m.hex_svg(code, number, level, 2, 'cut', Math.max(1, Math.round(360 / tally.side)));41    if (svg.length > 4000000) throw new Error('that drawing is larger than this page serves; lower the level.');42    art = svg;43  } catch (fault) {44    error = fault;45  }4647  const chart = (canvas) => {48    const view = shown.current;49    if (!view) return;50    const b = board(canvas, 220);51    const rows = view.rows, n = rows.length;52    const peak = rows.reduce((a, r) => Math.max(a, r.fills), 1);53    const crest = rows.reduce((a, r) => Math.max(a, r.components, r.holes), 1);54    bars(b, rows.map((r) => r.fills), { peak, color: (i) => (rows[i].k === view.here ? ink.yellow : ink.blue) });55    for (const [key, color] of [['components', ink.green], ['holes', ink.pink]]) {56      line(b, rows.map((r, i) => [(i + 0.5) / n, r[key] / crest]), color, { dots: 2.5 });57    }58    axis(b, rows.map((r, i) => [(i + 0.5) / n, r.k]));59    const next = tag(b, `filled triangles, peak ${peak}`, ink.blue);60    tag(b, `holes, peak ${crest}`, ink.pink, 'left', tag(b, 'pieces', ink.green, 'left', next + 16) + 16);61  };6263  const toFractal = (value) => {64    setFractal(value);65    if (pick.level > capOf(value)) set({ level: capOf(value) });66  };6768  const onSeek = (frac) => {69    toFractal(0);70    set({ k: Math.min(TOP, Math.max(1, Math.floor(frac * TOP) + 1)) });71  };7273  const view = shown.current;7475  const controls = (76    <>77      <Group name="Design">78        <Picker dimension={3} code={pick.code} seeds={s} onChange={set} />79      </Group>80      <Group name="Section">81        <label>side <input type="range" min={1} max={TOP} value={k} disabled={fractal !== 0} onChange={(e) => set({ k: +e.target.value })} /><span className="num">{view?.reading}</span></label>82        <Pick label="fractal" value={fractal} options={[[0, 'off, odd side'], [3, 'base 3 tile'], [5, 'base 5 tile']]} onChange={(value) => toFractal(+value)} />83        <label>level <input type="range" min={1} max={lid} value={level} disabled={fractal === 0} onChange={(e) => set({ level: +e.target.value })} /><span className="num">{level}</span></label>84      </Group>85    </>86  );8788  return (89    <Page crumb="slices" title="The middle plane of a cube is a hexagon of triangles" controls={controls}90      sub={<>Cut the cube of odd side <code>side = 2k-1</code> through its centre, square to the main diagonal, and the section is a regular hexagon tiled by <code>6 side^2</code> unit triangles. A design's parity rule fills some of them: carpet and net split the hexagon between them with nothing left over, and the carpet's own section alternates as <code>k</code> grows, falling into many separate pieces at odd <code>k</code> and closing into one piece pierced by holes at even <code>k</code>. Drag the chart to move the side.</>}91      foot={<>The mesh is the crate's triangular section of the cube it builds from the code, and every count is read off that mesh; the closed form beside the triangle count is the polynomial in <code>k</code>, evaluated in Rust and never fitted here. Pieces come from the adjacency network of filled triangles and holes from the Euler number of the filled sub-mesh, so the alternation in the chart is counted, not drawn. The mesh census, the closed forms beside it and which designs pierce the hexagon with holes are in <a href="/research/slices/">the slices note</a>.</>}>92      <Sketch draw={chart} deps={[view]} onSeek={onSeek} className="bars" role="img" aria-label="Filled triangles, pieces and holes by side, drag to move the side" />93      <Stats>94        <Stat label="name">{view?.name}</Stat>95        <Stat label="side">{view?.tally.side}</Stat>96        <Stat label="triangles">{view?.tally.triangles}</Stat>97        <Stat label="closed form">{view?.tally.closed.triangles}</Stat>98        {TALLY.map(([key, label]) => <Stat key={key} label={label}>{view?.tally[key]}</Stat>)}99      </Stats>100      <Stats>101        {split && <><Stat label="carpet">{split.carpet}</Stat><Stat label="net">{split.net}</Stat><Stat label="together">{split.together}</Stat><Stat label="hexagon">{split.hexagon}</Stat><Stat label="partition">{split.exact ? 'exact' : 'broken'}</Stat></>}102      </Stats>103      <Markup svg={art ?? ''} role="img" aria-label="The middle slice" />104      <Note error={error} />105    </Page>106  );107}108109mount(<App />);