index.jsx

21.0 kB · jsx · 382 lines

1import { useMemo, useRef, useState } from 'react';2import * as THREE from 'three';3import { ready, ink } from '../../lib/mrly.js';4import { faces } from '../../lib/stage.js';5import { board, bars, line, axis, rules, tag } from '../../lib/chart.js';6import { mount, Page, Row, Pick, Slider, Check, Stats, Stat, Note, Group } from '../../lib/app.jsx';7import { Grid, Markup, Sketch } from '../../lib/draw.jsx';8import { Stage } from '../../lib/stage.jsx';9import { useSeeds, seeded, Picker } from '../../lib/select.jsx';10import { useQuery } from '../../lib/query.js';1112const m = await ready();13const RDEN = 120;14const RMAX = 108;15const CELLS = 600000;16const FOLDS = 64;17const PAD = 14;18const SIGNS = [];19for (let bits = 0; bits < 8; bits++) SIGNS.push([1 - 2 * (bits & 1), 1 - 2 * ((bits >> 1) & 1), 1 - 2 * ((bits >> 2) & 1)]);20const WALLS = {21  box: [[1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1]],22  diamond: SIGNS,23  octahedron: SIGNS,24  tetrahedron: [[1, 1, 1], [-1, -1, 1], [-1, 1, -1], [1, -1, -1]],25  pyramid: [[1, 0, 0], [-1, -2, 0], [-1, 2, 0], [-1, 0, -2], [-1, 0, 2]],26};27const POLICIES = { 2: ['inside', 'touching', 'refined1', 'refined2'], 3: ['inside', 'touching'] };28const ART = { background: 'var(--fg)', borderRadius: '8px', padding: '8px', lineHeight: 0 };29const DIM = +(new URLSearchParams(location.search).get('dim') ?? 2);3031function planes(shape, r) {32  return WALLS[shape].map((n) => {33    const size = Math.hypot(...n);34    return new THREE.Plane(new THREE.Vector3(-n[0], -n[1], -n[2]).divideScalar(size), (2 * r) / size);35  });36}3738function Num({ label, value, min, max, onChange }) {39  return <label>{label} <input type="number" value={value} min={min} max={max} onChange={(e) => onChange(+e.target.value)} /></label>;40}4142function Toggle({ label, checked, disabled, hidden, onChange }) {43  return <label hidden={hidden}><input type="checkbox" checked={checked} disabled={disabled} onChange={(e) => onChange(e.target.checked)} /> {label}</label>;44}4546function App() {47  const s = useSeeds();48  const [q, set] = useQuery({49    dim: DIM, code: seeded(s, DIM, 2, DIM === 2 ? '7' : '23'), base: 2, number: 3, level: 3,50    shape: 'ball', radius: 60, mode: 'crop', policy: 'touching',51    centre: 'corner', count: DIM === 2 ? 6 : 4, r: 27, low: -2, high: -1,52  });53  const [crisp, setCrisp] = useState(false);54  const [spin, setSpin] = useState(false);55  const live = useRef(null);56  const shapes = useMemo(() => JSON.parse(m.crop_shapes(q.dim)), [q.dim]);57  const top = m.level_cap(q.number, 1, q.dim === 2 ? 243 : 81);58  const level = Math.min(q.level, top);59  const countTop = m.level_cap(q.number, q.dim, CELLS);60  const count = Math.min(q.count, countTop);6162  const circle = useMemo(() => {63    const out = {};64    try {65      const code = q.code.trim();66      const flat = m.crop_circle(code, q.number, count, q.base, q.dim, q.centre);67      const width = flat.length / 3;68      out.seen = flat.subarray(0, width);69      out.inside = flat.subarray(width, 2 * width);70      out.cut = flat.subarray(2 * width, 3 * width);71      out.top = width - 1;72      out.mass = Number(m.fills(code, q.number, q.dim, 1, q.base));73      out.d = m.dimension(code, q.number, q.dim, q.base);74      out.side = m.grid_total(q.number, 1, count);75      out.marks = [];76      for (let r = q.number; r <= out.top; r *= q.number) out.marks.push(r);77    } catch (error) {78      out.error = error;79    }80    return out;81  }, [q.code, q.number, q.base, q.dim, q.centre, count]);8283  const at = Math.max(1, Math.min(q.r, circle.top ?? q.r));84  const defect = circle.seen && q.number * at <= circle.top ? circle.seen[q.number * at] - circle.mass * circle.seen[at] : null;8586  const fold = useMemo(() => {87    try {88      return JSON.parse(m.crop_collapse(q.code.trim(), q.number, count, q.base, q.dim, q.centre, FOLDS));89    } catch (error) {90      return { error, scales: [], pairs: [] };91    }92  }, [q.code, q.number, q.base, q.dim, q.centre, count]);9394  const scales = fold.scales ?? [];95  const held = (want) => (scales.length ? Math.max(0, Math.min(scales.length - 1, want < 0 ? scales.length + want : want)) : 0);96  const [low, high] = [held(q.low), held(q.high)];97  const gap = fold.pairs?.find((row) => row.low === Math.min(low, high) && row.high === Math.max(low, high));9899  const sweep = useMemo(() => {100    const out = {};101    try {102      const steps = q.dim === 2 ? 36 : 24;103      out.rows = JSON.parse(m.crop_series(q.code.trim(), q.number, level, q.base, q.dim, q.shape, q.radius, RDEN, q.mode === 'anti', 'radius', steps));104    } catch (error) {105      out.error = error;106    }107    return out;108  }, [q.code, q.base, q.number, level, q.dim, q.shape, q.mode]);109110  const data = useMemo(() => {111    const out = {};112    try {113      const code = q.code.trim(), d = q.dim, anti = q.mode === 'anti';114      out.name = m.name_of(code, d, q.base);115      out.side = m.grid_total(q.number, 1, level);116      const census = JSON.parse(m.crop_census(code, q.number, level, q.base, d, q.shape, q.radius, RDEN, anti));117      out.census = census;118      out.solid = d === 3;119      out.cut = !!(out.solid && crisp && !anti && WALLS[q.shape]);120      if (!out.solid) {121        if (crisp) {122          const side = Number(m.grid_total(q.number, 1, level));123          const art = m.crop_svg(code, q.number, level, q.base, q.shape, q.radius, RDEN, anti, Math.max(2, Math.round(512 / side)));124          if (art.length > 4000000) throw new Error('that drawing is larger than this page serves; lower the level.');125          out.art = art;126          out.note = 'touching cells under the exact outline';127        } else {128          out.grid = m.crop_grid(code, q.number, level, q.base, q.shape, q.radius, RDEN, anti, q.policy);129          out.note = q.policy;130        }131      } else {132        const load = Number(out.cut ? census.exposed_before : census.exposed_after);133        if (load > 400000) throw new Error(`${load} faces is more than this page draws; lower the level.`);134        out.mesh = out.cut135          ? m.three_faces(code, q.number, level, q.base)136          : m.crop_faces(code, q.number, level, q.base, q.shape, q.radius, RDEN, anti, q.policy);137        out.note = out.cut ? 'the exact walls clip the full mesh' : q.policy;138      }139      if (sweep.error) throw sweep.error;140      const total = Number(census.filled_in) + Number(census.filled_cut) + Number(census.filled_out);141      out.rows = [{ x: 0, filled_in: anti ? total : 0, filled_cut: 0 }, ...sweep.rows];142      out.frac = q.radius / RDEN;143      out.levels = JSON.parse(m.crop_series(code, q.number, level, q.base, d, q.shape, q.radius, RDEN, anti, 'level', top));144      out.level = level;145    } catch (error) {146      out.error = error;147      out.art = null;148      out.grid = null;149      out.mesh = null;150    }151    return out;152  }, [q.code, q.base, q.number, level, q.dim, q.shape, q.radius, q.mode, q.policy, crisp, sweep]);153154  const census = data.census ?? {};155156  const turn = (on) => {157    setSpin(on);158    if (live.current) live.current.spin = on ? 0.004 : 0;159  };160161  const shift = (v) => {162    const d = +v;163    const list = JSON.parse(m.crop_shapes(d));164    s.drop();165    set({166      dim: d, code: d === 2 ? '7' : '23',167      shape: list.includes(q.shape) ? q.shape : 'ball',168      policy: POLICIES[d].includes(q.policy) ? q.policy : 'touching',169    });170  };171172  const alongRadius = (canvas) => {173    if (!data.rows) return;174    const b = board(canvas, 170, { pad: PAD, top: 16, bottom: 20 });175    const peak = Math.max(...data.rows.map((r) => Math.max(r.filled_in, r.filled_cut)), 1);176    line(b, data.rows.map((r) => [r.x, r.filled_in / peak]), ink.yellow);177    line(b, data.rows.map((r) => [r.x, r.filled_cut / peak]), ink.blue);178    axis(b, [[0, '0'], [1, 'radius 1']]);179    rules(b, [data.frac], { color: ink.pink });180    const edge = tag(b, 'in', ink.yellow);181    tag(b, 'cut', ink.blue, 'left', edge + 12);182  };183184  const alongLevel = (canvas) => {185    if (!data.levels) return;186    const b = board(canvas, 170, { pad: PAD, top: 16, bottom: 20 });187    const logs = data.levels.map((r) => Math.log10(1 + r.filled_in));188    bars(b, logs, { color: (i) => (i === data.level ? ink.pink : ink.yellow), inset: 2 });189    axis(b, [[0, 'level 0'], [1, String(data.levels.length - 1)]]);190  };191192  const alongCircle = (canvas) => {193    const { seen, cut, top, d, mass, marks } = circle;194    if (!seen || !seen[top] || top < 2) return;195    const b = board(canvas, 230, { pad: PAD, top: 20, bottom: 22 });196    const span = Math.log(top);197    let ceiling = Math.max(seen[top], 2);198    for (let r = 1; r <= top; r++) {199      ceiling = Math.max(ceiling, cut[r]);200      if (q.number * r <= top) ceiling = Math.max(ceiling, Math.abs(seen[q.number * r] - mass * seen[r]));201    }202    const roof = Math.log(ceiling);203    const lx = (r) => Math.log(r) / span;204    const ly = (v) => Math.log(v) / roof;205    const trail = (pick) => {206      const points = [];207      for (let r = 1; r <= top; r++) {208        const v = pick(r);209        if (v >= 1) points.push([lx(r), ly(v)]);210      }211      return points;212    };213    const ramp = (slope, v0) => {214      if (!(slope > 0) || !(v0 > 1)) return;215      const start = Math.max(1, top * Math.pow(v0, -1 / slope));216      line(b, [[lx(start), ly(v0 * Math.pow(start / top, slope))], [1, ly(v0)]], ink.dim, { width: 1, dash: [4, 4] });217    };218    rules(b, marks.map(lx), { dash: [2, 4] });219    rules(b, [lx(at)], { color: ink.pink });220    ramp(d, seen[top]);221    ramp(d - 1, cut[top]);222    line(b, trail((r) => seen[r]), ink.yellow);223    line(b, trail((r) => cut[r]), ink.blue);224    line(b, trail((r) => (q.number * r <= top ? Math.abs(seen[q.number * r] - mass * seen[r]) : 0)), ink.orange, { width: 1 });225    for (const r of marks) if (seen[r] >= 1) line(b, [[lx(r), ly(seen[r])]], ink.yellow, { dots: 3 });226    axis(b, [[0, 'r 1'], [1, `${top}`]], { wall: true });227    let edge = tag(b, `N slope ${d.toFixed(4)}`, ink.yellow);228    edge = tag(b, `C slope ${(d - 1).toFixed(4)}`, ink.blue, 'left', edge + 12);229    tag(b, 'defect', ink.orange, 'left', edge + 12);230  };231232  const collapse = (canvas) => {233    if (scales.length < 1) return;234    const b = board(canvas, 230, { pad: PAD, top: 20, bottom: 22 });235    const [a, z] = [scales[low], scales[high]];236    const seen = a.main.concat(z.main);237    const floor = Math.min(...seen), roof = Math.max(...seen);238    const room = (roof - floor) * 0.1 || 0.05;239    const fx = (j) => j / FOLDS;240    const fy = (v) => (v - floor + room) / (roof - floor + 2 * room);241    const trail = (scale, color) => line(b, scale.main.map((v, j) => [fx(j), fy(v)]), color, { width: 1.6 });242    const step = Math.log(q.number);243    const turn = Math.log(at) / step;244    rules(b, [turn - Math.floor(turn)], { color: ink.pink });245    trail(a, ink.yellow);246    if (high !== low) trail(z, ink.green);247    axis(b, [[0, '0'], [0.5, `log_${q.number} r mod 1`], [1, '1']], { wall: true });248    let edge = tag(b, `R ${a.start} to ${a.stop}`, ink.yellow);249    if (high !== low) edge = tag(b, `R ${z.start} to ${z.stop}`, ink.green, 'left', edge + 12);250    tag(b, 'N / r^d', ink.dim, 'left', edge + 12);251    if (gap) tag(b, `gap ${gap.sup.toFixed(4)}`, ink.dim, 'right');252  };253254  const ridge = (canvas) => {255    if (scales.length < 1) return;256    const b = board(canvas, 230, { pad: PAD, top: 20, bottom: 22 });257    const [a, z] = [scales[low], scales[high]];258    const seen = a.drift.concat(high === low ? [] : z.drift);259    if (!seen.length) {260      tag(b, 'the fold runs past the counted radii', ink.dim);261      axis(b, [[0, '0'], [0.5, `log_${q.number} r mod 1`], [1, '1']], { wall: true });262      return;263    }264    const floor = Math.min(...seen), roof = Math.max(...seen);265    const room = (roof - floor) * 0.1 || 0.05;266    const fx = (j) => j / FOLDS;267    const fy = (v) => (v - floor + room) / (roof - floor + 2 * room);268    const trail = (scale, color) => scale.drift.length && line(b, scale.drift.map((v, j) => [fx(j), fy(v)]), color, { width: 1.6 });269    const turn = Math.log(at) / Math.log(q.number);270    rules(b, [turn - Math.floor(turn)], { color: ink.pink });271    trail(a, ink.orange);272    if (high !== low) trail(z, ink.green);273    axis(b, [[0, '0'], [0.5, `log_${q.number} r mod 1`], [1, '1']], { wall: true });274    let edge = tag(b, `R ${a.start} to ${a.stop}`, a.drift.length ? ink.orange : ink.dim);275    if (high !== low) edge = tag(b, `R ${z.start} to ${z.stop}`, z.drift.length ? ink.green : ink.dim, 'left', edge + 12);276    tag(b, 'delta / r^(d - 1)', ink.dim, 'left', edge + 12);277    if (gap?.rsup != null) tag(b, `gap ${gap.rsup.toFixed(4)}`, ink.dim, 'right');278  };279280  const folds = scales.map((scale, k) => [k, `R ${scale.start}`]);281282  const controls = (283    <>284      <Group name="Design">285        <Picker dimension={q.dim} bases={[2, 3]} code={q.code} base={q.base} seeds={s} onChange={set} />286        <Num label="number" value={q.number} min={2} max={5} onChange={(v) => set({ number: v })} />287        <Slider label="level" value={level} min={1} max={top} onChange={(v) => set({ level: v })} />288      </Group>289      <Group name="Shape">290        <Pick label="dimension" value={q.dim} options={[2, 3]} onChange={shift} />291        <Pick label="shape" value={q.shape} options={shapes} onChange={(v) => set({ shape: v })} />292        <Slider label="radius" value={q.radius} min={0} max={RMAX} show={`${q.radius}/${RDEN}`} onChange={(v) => set({ radius: v })} />293        <Pick label="mode" value={q.mode} options={['crop', 'anti']} onChange={(v) => set({ mode: v })} />294        <Pick label="policy" value={q.policy} options={POLICIES[q.dim]} onChange={(v) => set({ policy: v })} />295      </Group>296      <Group name="Circle">297        <Pick label="centre" value={q.centre} options={['corner', 'centre']} onChange={(v) => set({ centre: v })} />298        <Slider label="count level" value={count} min={1} max={countTop} onChange={(v) => set({ count: v })} />299        <Slider label="radius r" value={at} min={1} max={Math.max(1, circle.top ?? 1)} show={`${at}/${circle.top ?? 1}`} onChange={(v) => set({ r: v })} />300        <Pick label="scale A" value={low} options={folds} onChange={(v) => set({ low: +v })} />301        <Pick label="scale B" value={high} options={folds} onChange={(v) => set({ high: +v })} />302      </Group>303      <Group name="View">304        <Check label="exact edge" checked={crisp} onChange={setCrisp} />305        <Toggle label="spin" checked={spin} disabled={data.cut} hidden={q.dim === 2} onChange={turn} />306      </Group>307    </>308  );309310  return (311    <Page crumb="crop" title="A shape keeps only the cells of a design it reaches" controls={controls}312      sub="A named shape of rational radius sits on the unit square or cube and keeps only the cells of a design it reaches: strictly inside, touching, or rebuilt on a finer lattice at the rim. The census splits every cell into in, cut and out before anything is drawn, and the sweeps show how the kept mass grows with the radius and the level. Drag the radius chart to move the shape. Below it the radius stops being a fraction of the box and runs in whole cells: the count of filled cells the ball holds, the count its sphere crosses, and what the count leaves over when the radius is multiplied by the design's own side. The last panel takes two of the windows between consecutive powers of that side, divides the count by the radius to the dimension and reads both at the same offsets of the log radius, so a stranger can pick any two scales and watch them land on one curve. The panel beside it folds the left-over of that multiplication the same way, one power of the radius lower, so the defect has a ridge of its own to read."313      foot={<>The shape is exact rational geometry in Rust: a ball tested on squared fractions or a polytope of half-plane walls, never a float. A cell is in, cut or out by where its corners land, the census tallies the three regions and the perimeter or surface before and after the touching crop, and the sweeps re-run that census at every radius and level. The exact edge in the plane is the same touching crop clipped by the true circle or polygon in SVG; in the cube it clips the uncropped mesh with the shape's own walls as camera-space planes, so the ball and the anti crop stay on the raster mesh, which is always the source of truth for every count. The circle count is the same geometry in whole cells and one pass over the grid: a cell is seen when its own centre lands in the ball, inside when its far corner does and cut when the sphere separates its near corner from its far one, all on doubled integer coordinates with squared distances compared as integers, and the three columns are prefix sums by radius. Its main term is not a constant times r to the d: it is r to the d times a periodic multiplier of log r, which is why two windows between consecutive powers of the side, read at the same offsets of the log radius, land on one curve; the gap beside them is the largest distance between the two profiles and the share is that gap over the higher of their two mean levels, and both fall as the scales deepen, which is what makes the collapse a law rather than one lucky window. The defect is an exact integer, it rides one power below the count, and the last panel folds it at the same offsets over the radius to that lower power, so the two windows carry two ridges with a gap of their own; that ridge gap need not fall with the scale the way the count's does, and the panel shows it rather than hides it. A window whose multiplied radii run past the counted grid has no ridge and says so. At the grid centre the middle block is empty, so the count stays at zero out to the block's inradius. The exact classification, the census of the three regions, the circle theorem and the one open lane the cut column points at are in <a href="/research/crop/">the crop note</a>.</>}>314      <div className="arena" style={{ gridTemplateColumns: '3fr 2fr' }}>315        <div className="panel">316          <h2>The crop <span>{data.note}</span></h2>317          {data.grid && <Grid grid={data.grid} on={ink.yellow} role="img" aria-label="The crop" />}318          <Markup style={ART} hidden={!data.art} svg={data.art ?? ''} role="img" aria-label="The crop" />319          <Stage hidden={!data.solid} role="img" aria-label="The crop" deps={[data]} onStage={(st) => {320            live.current = st;321            st.renderer.localClippingEnabled = true;322            st.spin = data.cut || !spin ? 0 : 0.004;323            if (data.cut) setSpin(false);324            if (!data.mesh) {325              st.clear();326              return;327            }328            const mesh = faces(data.mesh, ink.blue);329            if (data.cut) mesh.material.clippingPlanes = planes(q.shape, q.radius / RDEN);330            st.show(mesh);331          }} />332        </div>333        <div className="panel">334          <h2>Along the radius <span>filled cells kept and cut</span></h2>335          <Sketch className="bars" role="img" aria-label="Along the radius" draw={alongRadius} deps={[data]} onSeek={(frac) => set({ radius: Math.min(RMAX, Math.max(0, Math.round(frac * RDEN))) })} />336          <h2>Along the level <span>kept fills, log scale</span></h2>337          <Sketch className="bars" role="img" aria-label="Along the level" draw={alongLevel} deps={[data]} />338        </div>339      </div>340      <Stats>341        <Stat label="name">{data.name}</Stat>342        <Stat label="side">{data.side}</Stat>343        <Stat label="filled in">{census.filled_in}</Stat>344        <Stat label="filled cut">{census.filled_cut}</Stat>345        <Stat label="filled out">{census.filled_out}</Stat>346        <Stat label="exposed before">{census.exposed_before}</Stat>347        <Stat label="exposed after">{census.exposed_after}</Stat>348      </Stats>349      <div className="arena">350        <div className="panel">351          <h2>The circle count <span>N, C and the defect on log axes</span></h2>352          <Sketch className="bars" role="img" aria-label="The circle count" draw={alongCircle} deps={[circle, at]} onSeek={(frac) => set({ r: Math.round(Math.exp(Math.max(0, Math.min(1, frac)) * Math.log(circle.top ?? 1))) })} />353        </div>354        <div className="panel">355          <h2>The collapse <span>two scales laid on each other</span></h2>356          <Sketch className="bars" role="img" aria-label="The collapse" draw={collapse} deps={[fold, low, high, at]} />357        </div>358        <div className="panel">359          <h2>The defect ridge <span>the same two scales, one power down</span></h2>360          <Sketch className="bars" role="img" aria-label="The defect ridge" draw={ridge} deps={[fold, low, high, at]} />361        </div>362      </div>363      <Stats>364        <Stat label="count side">{circle.side}</Stat>365        <Stat label="r">{at}</Stat>366        <Stat label="N(r)">{circle.seen?.[at]}</Stat>367        <Stat label="inside(r)">{circle.inside?.[at]}</Stat>368        <Stat label="C(r)">{circle.cut?.[at]}</Stat>369        <Stat label="defect">{defect === null ? 'past the grid' : defect}</Stat>370        <Stat label="d">{circle.d?.toFixed(7)}</Stat>371        <Stat label="fill of one tile">{circle.mass}</Stat>372        <Stat label="gap">{gap ? gap.sup.toFixed(5) : 'one scale'}</Stat>373        <Stat label="gap / level">{gap ? gap.share.toFixed(5) : 'one scale'}</Stat>374        <Stat label="ridge gap">{gap?.rsup != null ? gap.rsup.toFixed(5) : 'past the grid'}</Stat>375        <Stat label="ridge gap / ridge">{gap?.rshare != null ? gap.rshare.toFixed(5) : 'past the grid'}</Stat>376      </Stats>377      <Note error={data.error ?? circle.error ?? fold.error} />378    </Page>379  );380}381382mount(<App />);