index.jsx

8.2 kB · jsx · 157 lines

1import { useMemo, useRef, useState } from 'react';2import { ready, ink } from '../../lib/mrly.js';3import { cubes } from '../../lib/stage.js';4import { mount, Page, Row, Pick, Slider, Check, Btn, Stats, Stat, Note, Group } from '../../lib/app.jsx';5import { Grid, Markup } from '../../lib/draw.jsx';6import { Stage } from '../../lib/stage.jsx';7import { useQuery } from '../../lib/query.js';8import { useSeeds, seeded, Picker } from '../../lib/select.jsx';910const m = await ready();11const DIMS = [[2, 'the plane'], [3, 'the cube'], [6, 'the hexagon']];12const PROJECTIONS = [['cut', 'the middle slice'], ['pro', 'three facing sides'], ['iso', 'the isometric skin']];13const PRESETS = [[5, 5], [3, 9]];14const PLANE_CELLS = 262144;15const SOLID_CELLS = 1000000;16const HEX_DRAWN = 24000;17const SOLID_FILLS = 150000;18const SEED = { 2: ['495', 3], 3: ['23', 2], 6: ['23', 2] };1920function capOf(q, reps) {21  const copies = reps.reduce((a, b) => a * b, 1);22  try {23    if (q.dim === 2) return m.level_cap(q.number, 2, Math.floor(PLANE_CELLS / copies));24    if (q.dim === 3) {25      const room = m.level_cap(q.number, 3, Math.floor(SOLID_CELLS / copies));26      return Math.min(room, m.fill_cap(q.code.trim(), q.number, 3, q.base, Math.floor(SOLID_FILLS / copies)));27    }28    return Math.min(m.level_cap(q.number, 1, 81), m.level_cap(q.number, 2, Math.floor(HEX_DRAWN / (8 * copies))));29  } catch {30    return 1;31  }32}3334function App() {35  const s = useSeeds();36  const [q, set] = useQuery({ dim: 2, code: seeded(s, 2, 3, '495'), base: 3, number: 3, level: 2, x: 5, y: 5, z: 5, proj: 'cut', overhang: true });37  const [spin, setSpin] = useState(true);38  const live = useRef(null);3940  const reps = q.dim === 3 ? [q.x, q.y, q.z] : [q.x, q.y];41  const top = capOf(q, reps);42  const level = Math.min(q.level, top);43  const code = q.code.trim();44  const solid = q.dim !== 2;45  const crop = !q.overhang && q.x >= 2 && q.y >= 2;4647  const data = useMemo(() => {48    const out = {};49    try {50      out.name = m.name_of(code, solid ? 3 : 2, q.base);51      out.census = JSON.parse(m.tile_census(code, q.number, level, q.base, q.dim, q.proj, reps, crop));52      if (q.dim === 2) out.grid = m.tile_grid(code, q.number, level, q.base, q.x, q.y);53      else if (q.dim === 3) out.cells = m.tile_cells(code, q.number, level, q.base, q.x, q.y, q.z);54      else {55        if (out.census.triangles > HEX_DRAWN) throw new Error(`${out.census.triangles} triangles is more than this page draws; lower the level or the repeats.`);56        const scale = Math.max(1, Math.round(900 / out.census.sheet[0]));57        const art = m.tile_svg(code, q.number, level, q.base, q.proj, q.x, q.y, crop, scale);58        if (art.length > 4000000) throw new Error('that drawing is larger than this page serves; lower the level or the repeats.');59        out.art = art;60      }61    } catch (error) {62      out.error = error;63    }64    return out;65  }, [code, q.base, q.number, level, q.dim, q.proj, q.x, q.y, q.z, crop]);6667  const census = data.census ?? {};68  const sheet = `${data.name ?? ''} repeated into a sheet`;6970  const shift = (value) => {71    const dim = +value;72    const [code, base] = SEED[dim];73    s.drop();74    set({ dim, code, base, number: 3, level: dim === 2 ? 2 : 1 });75  };7677  const preset = ([a, b]) => set({ x: a, y: b, z: a });7879  const turn = (on) => {80    setSpin(on);81    if (live.current) live.current.spin = on ? 0.004 : 0;82  };8384  const onStage = (stage) => {85    live.current = stage;86    stage.spin = spin ? 0.004 : 0;87    if (!data.cells) {88      stage.clear();89      return;90    }91    const span = [census.sheet[1], census.sheet[0], census.sheet[2]];92    const side = Math.max(...span);93    const mesh = cubes(data.cells, side, ink.orange);94    mesh.position.set(...span.map((n) => 1 - n / side));95    stage.show(mesh);96  };9798  const controls = (99    <>100      <Group name="Design">101        <Pick label="dimension" value={q.dim} options={DIMS} onChange={shift} />102        <Picker dimension={solid ? 3 : 2} bases={q.dim === 2 ? [3, 2] : [2, 3]} code={q.code} base={q.base} seeds={s} onChange={set} />103        <Pick label="side" value={q.number} options={[[3, 3], [5, 5], [7, 7]]} onChange={(v) => set({ number: +v })} />104        <Slider label="level" value={level} min={1} max={top} onChange={(v) => set({ level: v })} />105      </Group>106      <Group name="Repeats">107        <Slider label="across" value={q.x} min={1} max={12} onChange={(v) => set({ x: v })} />108        <Slider label="down" value={q.y} min={1} max={12} onChange={(v) => set({ y: v })} />109        {q.dim === 3 && <Slider label="deep" value={q.z} min={1} max={12} onChange={(v) => set({ z: v })} />}110        <span className="tabs" role="group" aria-label="Repeat presets">111          {PRESETS.map(([a, b]) => <Btn key={`${a}.${b}`} on={q.x === a && q.y === b} onClick={() => preset([a, b])}>tile({a},{b})</Btn>)}112        </span>113      </Group>114      {q.dim === 6 && (115        <Group name="View">116          <Pick label="projection" value={q.proj} options={PROJECTIONS} onChange={(v) => set({ proj: v })} />117          <Check label="keep the overhang" checked={q.overhang} onChange={(v) => set({ overhang: v })} />118        </Group>119      )}120      {q.dim === 3 && (121        <Group name="View">122          <Check label="spin" checked={spin} onChange={turn} />123        </Group>124      )}125    </>126  );127128  return (129    <Page crumb="tile" title="One design repeated tiles the plane, the cube and the hexagon" controls={controls}130      sub="One design, repeated. The same rule that fills the corners of a cube lays its tile side by side on the square lattice in the plane and in the cube, and interlocks it as a hexagon on the triangular one. On an uncropped sheet the fills multiply by the copy count exactly; the exposed faces do not, because every copy buries the faces it shares with a neighbour, and a cropped hexagon sheet trims fills away as well."131      foot={<>The tile is the design at its level and the sheet is that tile repeated, both built in Rust before a pixel is drawn. The hexagon interlocks rather than butts: the crop trims the jagged overhang to a clean rectangle and slides the whole triangle grid by one interlocking step, so the crate flips the start parity to keep every triangle pointing the way the tile's do, and backs a tall sheet of wide hexagons onto a wider frame so the renderer still reads the hexagon the right way up. Exposed means perimeter in the plane, surface in the cube and the boundary edges of the filled sub-mesh on the hexagon; buried is the difference between the copies' own exposure and the sheet's, and under the crop that difference also counts what the trim removed. Why a design's symmetries are symmetries of the infinite tiling and not of a truncation to a finite side is in <a href="/research/core/">the core</a>. Nearby: <a href="../words">the words</a> nests one design inside another instead of repeating it, <a href="../slices">the slices</a> counts the single hexagon, <a href="../sponge">the sponge</a> grows the cube.</>}>132      {q.dim === 2 && data.grid && <Grid grid={data.grid} on={ink.yellow} role="img" aria-label={sheet} />}133      <Stage hidden={q.dim !== 3} role="img" aria-label={sheet} deps={[data]} onStage={onStage} />134      {q.dim === 6 && <Markup svg={data.art ?? ''} role="img" aria-label={sheet} />}135      <Stats>136        <Stat label="name">{data.name}</Stat>137        <Stat label="tile side">{census.side}</Stat>138        <Stat label="tile">{census.tile?.join(' x ')}</Stat>139        <Stat label="copies">{census.copies}</Stat>140        <Stat label="sheet">{census.sheet?.join(' x ')}</Stat>141        <Stat label={q.dim === 6 ? 'triangles' : 'cells'}>{census.cells}</Stat>142      </Stats>143      <Stats>144        <Stat label="filled">{census.fills}</Stat>145        <Stat label="empty">{census.voids}</Stat>146        <Stat label="density">{census.ratio?.toFixed(6)}</Stat>147        <Stat label="exposed">{census.exposed}</Stat>148        <Stat label="one copy exposed">{census.tile_exposed}</Stat>149        <Stat label={crop ? "buried or trimmed" : "buried by the tiling"}>{census.buried}</Stat>150        {census.walked && <span>corners <b>{census.vertices}</b> edges <b>{census.edges}</b>{census.faces ? <> faces <b>{census.faces}</b></> : null} euler <b>{census.euler}</b></span>}151      </Stats>152      <Note error={data.error} />153    </Page>154  );155}156157mount(<App />);