anvilsign in

collin/mahjong · 81bd92f0

Stand the wall square on its corner, and a page for placing it

Collin Richards · 2026-08-21 20:35 UTC · 81bd92f06dacbd92072c009227f96852fee72b47 · parent 792fe2fd · browse files

modifiedREADME.md+12 −0
⋯ 554 unchanged lines
555555 nothing to clip it, so a tile lifted out of a hand crosses the table in one
556556 piece.
557557
558+The square is set at an angle — `--wall-turn` in `styles.css`, 45° — so its four
559+arms run out towards the corners of the table rather than squaring up with its
560+edges. That one property is the only place it is decided: `geometry.ts` reads it
561+back off the element, so the colliders turn with it, and `WallRing` reads it too
562+so the square is built smaller by however much the turn costs it (stood on its
563+corner a square needs its diagonal where it used to need its side). Set it to
564+zero and everything squares up again. `--wall-room`, `--wall-x` and `--wall-y`
565+put it where you want it, and `npm run dev` serves **`/wall.html`** for turning
566+all four by hand — the real `WallRing` in a real `.center`, with every collider
567+the physics would be handed drawn over the top, because "it looks right" and "a
568+tile would bounce off it there" are two different claims.
569+
558570 Nothing about the square is calculated twice. Its size lives entirely in CSS
559571 (`--ws`, `--wd`), so `geometry.ts` **measures** it instead of restating that
560572 arithmetic: `WallRing` already renders every stack as a real element tagged with
⋯ 71 unchanged lines
modifiedsrc/styles.css+25 −2
⋯ 661 unchanged lines
662662 `.wall-ring` is the opening, not the outside: the sides hang off its edges,
663663 so the square encloses the pool the way it does on a table. */
664664 .wall-ring {
665- --ws: var(--tile-w);
665+ /* How big a tile in the wall is, against one in a hand.
666+
667+ A side is only as long as the stacks that fit on it, so this is really the
668+ knob for *how many* tiles the square holds: shrink it and more of them fit,
669+ the sides reach their corners, and the pinwheel closes up. At 1 they are the
670+ same tile and a side runs short, which is where the gaps at the corners come
671+ from. Turned by hand in `/wall.html` like the rest of the square. */
672+ --wall-tile: 1;
673+ --ws: calc(var(--tile-w) * var(--wall-tile));
666674 --wd: calc(var(--ws) * 1.375);
675+ /* How far round the whole square is set. Four walls built the same way round
676+ are already a pinwheel; standing it on its corner is what sends the arms
677+ out at the corners of the table instead of squaring up with its edges.
678+
679+ The one place this is decided: `table/geometry.ts` reads it back off the
680+ element, so the colliders turn with it and nothing has to be kept in step
681+ by hand. Zero squares it up again. */
682+ --wall-turn: 45deg;
683+ /* How much of the room it has the square actually takes, and where its middle
684+ sits relative to the middle of the table. All three are turned by hand in
685+ `/wall.html` and pasted back here. */
686+ --wall-room: 0.95;
687+ --wall-x: 0px;
688+ --wall-y: 0px;
667689 position: absolute;
668690 top: 50%;
669691 left: 50%;
670692 width: 0;
671693 height: 0;
672- transform: translate(-50%, -50%);
694+ transform: translate(-50%, -50%) translate(var(--wall-x), var(--wall-y))
695+ rotate(var(--wall-turn));
673696 pointer-events: none;
674697 z-index: 0;
675698 }
⋯ 1622 unchanged lines
modifiedsrc/table/geometry.ts+59 −14
⋯ 40 unchanged lines
4141 * The throw gate's business — what the physics collides with is `barriers`,
4242 * which includes these.
4343 */
44- walls: Rect[];
44+ walls: Barrier[];
4545 /**
4646 * Everything solid, which is to say every tile on the table: the standing
4747 * stacks, the sets people have laid down, and the sixteen each of them is
⋯ 33 unchanged lines
8181 });
8282
8383 const stacks = wallStacks(state);
84- const walls: Rect[] = [];
84+ const walls: Barrier[] = [];
8585 const barriers: Barrier[] = [];
8686 // A stack element is --ws by --wd (or the other way round on the left and
8787 // right sides), so its narrower side is the tile's width whichever wall it is
⋯ 1 unchanged line
8989 // The probe is one cell that is always there, spent wall or not.
9090 const probe = ring.querySelector<HTMLElement>('.wall-probe')?.getBoundingClientRect();
9191 let short = probe ? Math.min(probe.width, probe.height) : 0;
92+ // The whole square is set at an angle — see `--wall-turn` in styles.css — so
93+ // a stack's box is not what `getBoundingClientRect` reports, which is only the
94+ // upright box *around* the turned one. What is true either way is the centre
95+ // it reports and the size the layout gave it, and those two plus the angle are
96+ // the stack itself.
97+ const turn = wallTurn(ring);
9298 for (const el of ring.querySelectorAll<HTMLElement>('[data-stack]')) {
9399 const i = Number(el.dataset.stack);
94- const r = local(el.getBoundingClientRect());
100+ const around = local(el.getBoundingClientRect());
101+ const r: Rect = {
102+ x: around.x + around.w / 2 - el.offsetWidth / 2,
103+ y: around.y + around.h / 2 - el.offsetHeight / 2,
104+ w: el.offsetWidth,
105+ h: el.offsetHeight,
106+ };
95107 short = Math.max(short, Math.min(r.w, r.h));
96108 const stack = stacks[i];
97109 if (stack && isBarrier(stack)) {
98- walls.push(r);
99- barriers.push({ rect: r });
110+ walls.push({ rect: r, angle: turn });
111+ barriers.push({ rect: r, angle: turn });
100112 }
101113 }
102114 if (short === 0) return null;
⋯ 1 unchanged line
104116 // `.wall-ring` is the opening itself — the walls hang off its edges — so the
105117 // pool is simply its box. (It used to be the wall's outside, which is why this
106118 // had to subtract the wall's own thickness.)
107- const ringRect = local(ring.getBoundingClientRect());
108- const pool: Rect = {
109- x: ringRect.x,
110- y: ringRect.y,
111- w: Math.max(short, ringRect.w),
112- h: Math.max(short, ringRect.h),
119+ //
120+ // Taken at the size the layout gave it rather than the size it covers once
121+ // turned: the opening is the same opening whichever way round the square is
122+ // standing, and this is only ever where a *tapped* discard is aimed. A flick
123+ // is not aimed at anything — it goes where it was thrown.
124+ const around = local(ring.getBoundingClientRect());
125+ const openW = Math.max(short, ring.offsetWidth);
126+ const openH = Math.max(short, ring.offsetHeight);
127+ const ringRect: Rect = {
128+ x: around.x + around.w / 2 - openW / 2,
129+ y: around.y + around.h / 2 - openH / 2,
130+ w: openW,
131+ h: openH,
113132 };
133+ const pool: Rect = ringRect;
114134
115135 const launch: Partial<Record<SeatId, Point>> = {};
116136 const throwFrom: Partial<Record<SeatId, Point>> = {};
⋯ 64 unchanged lines
181201 return { w, h: w * 1.375 };
182202 }
183203
204+/**
205+ * How far round the wall square is set, in radians.
206+ *
207+ * Read off the element rather than written down here, the same way everything
208+ * else about the wall is: `--wall-turn` in styles.css is the one place it is
209+ * decided, and turning it there turns the colliders with it.
210+ */
211+function wallTurn(ring: HTMLElement): number {
212+ const deg = parseFloat(getComputedStyle(ring).getPropertyValue('--wall-turn'));
213+ return Number.isFinite(deg) ? (deg * Math.PI) / 180 : 0;
214+}
215+
184216 export const centreOf = (r: Rect): Point => ({ x: r.x + r.w / 2, y: r.y + r.h / 2 });
185217
186218 /**
⋯ 76 unchanged lines
263295 }
264296
265297 /** Segment against every standing stack, by the slab method. */
266-function clearLine(walls: Rect[], from: Point, to: Point): boolean {
267- return !walls.some((w) => segmentHitsRect(from, to, w));
298+function clearLine(walls: Barrier[], from: Point, to: Point): boolean {
299+ return !walls.some((w) => segmentHitsRect(from, to, w.rect, w.angle));
268300 }
269301
270-export function segmentHitsRect(a: Point, b: Point, r: Rect): boolean {
302+export function segmentHitsRect(a: Point, b: Point, r: Rect, angle = 0): boolean {
303+ // A turned box is the same box asked from where it is standing: put the
304+ // segment into its frame and the slabs are axis-aligned again.
305+ if (angle) {
306+ const cx = r.x + r.w / 2;
307+ const cy = r.y + r.h / 2;
308+ const c = Math.cos(-angle);
309+ const s = Math.sin(-angle);
310+ const into = (p: Point): Point => ({
311+ x: cx + (p.x - cx) * c - (p.y - cy) * s,
312+ y: cy + (p.x - cx) * s + (p.y - cy) * c,
313+ });
314+ return segmentHitsRect(into(a), into(b), r);
315+ }
271316 const dx = b.x - a.x;
272317 const dy = b.y - a.y;
273318 let t0 = 0;
⋯ 16 unchanged lines
modifiedsrc/table/physics.test.ts+1 −1
⋯ 455 unchanged lines
456456 return {
457457 size: { w: 620, h: 620 },
458458 pool: POOL,
459- walls,
459+ walls: walls.map((rect) => ({ rect })),
460460 barriers: walls.map((rect) => ({ rect })),
461461 tile: TILE,
462462 felt: { x: 120, y: 120, w: 380, h: 380 },
⋯ 210 unchanged lines
modifiedsrc/table/physics.ts+7 −4
⋯ 228 unchanged lines
229229 */
230230 export interface Barrier {
231231 rect: Rect;
232+ /** Turned, where it is turned: the wall is built as a pinwheel and the whole
233+ * square is set at an angle, so its stacks are not axis-aligned boxes. */
234+ angle?: number;
232235 seat?: SeatId;
233236 }
234237
⋯ 146 unchanged lines
381384 }
382385
383386 /** A standing stack, as something the tests above can chew on. */
384-const boxOf = (r: Rect): Box => ({
387+const boxOf = (r: Rect, angle = 0): Box => ({
385388 x: r.x + r.w / 2,
386389 y: r.y + r.h / 2,
387390 w: r.w,
388391 h: r.h,
389- angle: 0,
392+ angle,
390393 });
391394
392395 // ---- stepping -------------------------------------------------------------
⋯ 173 unchanged lines
566569 for (const wall of world.walls) {
567570 if (wall.seat === b.seat && !b.leftSeat) {
568571 // Still inside the row it was thrown from: note it, but pass through.
569- const over = overlapOf(boxOf(wall.rect), b);
572+ const over = overlapOf(boxOf(wall.rect, wall.angle), b);
570573 if (over && over.depth > CONTACT_SLOP) inOwnHand = true;
571574 continue;
572575 }
⋯ 76 unchanged lines
649652 /** Bounce off whatever is standing there, corner to corner if that is how it
650653 * hits — a stack of the wall, a laid-down set, somebody's hand. */
651654 function hitWall(b: Body, wall: Barrier, impacts: Impact[]) {
652- const hit = overlapOf(boxOf(wall.rect), b);
655+ const hit = overlapOf(boxOf(wall.rect, wall.angle), b);
653656 if (!hit || hit.depth <= CONTACT_SLOP) return;
654657
655658 const speed = Math.hypot(b.vx, b.vy);
⋯ 339 unchanged lines
modifiedsrc/tune.css+34 −0
⋯ 142 unchanged lines
143143 white-space: pre-wrap;
144144 opacity: 0.85;
145145 }
146+
147+/* ---------- the wall square's page ---------- */
148+/* A patch of table to stand the square in, and a canvas over it for the
149+ colliders. The felt is the game's own, since that is what it will sit on. */
150+.wall-stage {
151+ display: flex;
152+ align-items: center;
153+ justify-content: center;
154+ padding: 24px;
155+}
156+.wall-stage .table {
157+ position: relative;
158+ width: min(76vw, 900px);
159+ height: min(76vh, 720px);
160+ border-radius: 10px;
161+ overflow: hidden;
162+}
163+/* The middle is the whole of it here: there are no seats to leave room for, and
164+ the square is the only thing being looked at. */
165+.wall-stage .center {
166+ inset: 0;
167+}
168+.wall-overlay {
169+ position: absolute;
170+ inset: 0;
171+ pointer-events: none;
172+}
173+/* What the square came out as, under the sliders that decided it. */
174+.panel .built {
175+ margin: 12px 0 0;
176+ font-size: 12px;
177+ opacity: 0.75;
178+ color: var(--gold);
179+}
addedsrc/tuneWall.tsx+237 −0
1+import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
2+import { createRoot } from 'react-dom/client';
3+import { measure } from './table/geometry';
4+import type { WallProgress } from './game/wall';
5+import { WallRing } from './ui/WallRing';
6+import './styles.css';
7+import './tune.css';
8+
9+/**
10+ * A page for standing the wall square where you want it.
11+ *
12+ * It is the real `<WallRing>` in a real `.center`, so what is on screen is what
13+ * the table builds — and over the top of it, drawn from `measure()`, every
14+ * collider the physics would actually be given. That second part is the point
15+ * of the page rather than a nicety: the square is laid out in CSS and read back
16+ * off the elements, so "it looks right" and "a tile would bounce off it there"
17+ * are two different claims, and this is where you get to check both at once.
18+ *
19+ * Dev only — `wall.html` is not one of the build's entry points.
20+ */
21+
22+interface Knob {
23+ key: keyof Placing;
24+ label: string;
25+ min: number;
26+ max: number;
27+ step: number;
28+ note: string;
29+ unit: string;
30+}
31+
32+interface Placing {
33+ turn: number;
34+ room: number;
35+ tile: number;
36+ x: number;
37+ y: number;
38+}
39+
40+const START: Placing = { turn: 45, room: 0.95, tile: 1, x: 0, y: 0 };
41+
42+const KNOBS: Knob[] = [
43+ { key: 'turn', label: 'Turn', min: 0, max: 90, step: 1, note: '° clockwise', unit: 'deg' },
44+ { key: 'room', label: 'Room it takes', min: 0.3, max: 1.3, step: 0.01, note: 'of the middle', unit: '' },
45+ { key: 'tile', label: 'Tile size', min: 0.3, max: 1.4, step: 0.02, note: 'against a hand tile', unit: '' },
46+ { key: 'x', label: 'Across', min: -200, max: 200, step: 1, note: 'from the middle', unit: 'px' },
47+ { key: 'y', label: 'Down', min: -200, max: 200, step: 1, note: 'from the middle', unit: 'px' },
48+];
49+
50+/** Where the hand has got to, so the square can be seen whole and eaten away. */
51+const DRAWN = [0, 16, 32, 48, 64, 80, 100, 120];
52+
53+function WallTuner() {
54+ const [place, setPlace] = useState<Placing>({ ...START });
55+ const [drawn, setDrawn] = useState(0);
56+ const [showColliders, setShowColliders] = useState(true);
57+ const table = useRef<HTMLDivElement>(null);
58+ const canvas = useRef<HTMLCanvasElement>(null);
59+ // What the square actually came out as, so the number of tiles is a thing you
60+ // can read rather than count off the screen.
61+ const [built, setBuilt] = useState({ stacks: 0, gap: 0 });
62+
63+ // Stable across renders: the measuring effect keys off it, and a fresh object
64+ // every render would have it re-measuring for ever.
65+ const state = useMemo<WallProgress>(
66+ () => ({ drawnFront: drawn, drawnBack: 0, rules: { wallReserve: 16 } }),
67+ [drawn],
68+ );
69+
70+ // On the ring itself rather than a stylesheet, so this page and the table are
71+ // reading the very same custom properties.
72+ useLayoutEffect(() => {
73+ const ring = table.current?.querySelector<HTMLElement>('.wall-ring');
74+ if (!ring) return;
75+ ring.style.setProperty('--wall-turn', `${place.turn}deg`);
76+ ring.style.setProperty('--wall-room', `${place.room}`);
77+ ring.style.setProperty('--wall-tile', `${place.tile}`);
78+ ring.style.setProperty('--wall-x', `${place.x}px`);
79+ ring.style.setProperty('--wall-y', `${place.y}px`);
80+ }, [place]);
81+
82+ // Then measure what came out and draw it, which is the whole point.
83+ useEffect(() => {
84+ const el = table.current;
85+ const cv = canvas.current;
86+ const ctx = cv?.getContext('2d');
87+ if (!el || !cv || !ctx) return;
88+
89+ const paint = () => {
90+ const dpr = window.devicePixelRatio || 1;
91+ cv.width = Math.round(el.clientWidth * dpr);
92+ cv.height = Math.round(el.clientHeight * dpr);
93+ cv.style.width = `${el.clientWidth}px`;
94+ cv.style.height = `${el.clientHeight}px`;
95+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
96+ ctx.clearRect(0, 0, el.clientWidth, el.clientHeight);
97+ if (!showColliders) return;
98+
99+ const geo = measure(el, state);
100+ if (!geo) return;
101+
102+ // How many stacks are standing, and the most room any one side has left
103+ // over — which is the gap you can see at the corners, since a side packs
104+ // its stacks to the end it was built from.
105+ const all = [...el.querySelectorAll<HTMLElement>('[data-stack]')];
106+ const sides = [...el.querySelectorAll<HTMLElement>('.wall-side')];
107+ let gap = 0;
108+ for (const side of sides) {
109+ const kids = [...side.querySelectorAll<HTMLElement>('[data-stack]')];
110+ if (!kids.length) continue;
111+ const along = side.classList.contains('left') || side.classList.contains('right');
112+ const room = along ? side.clientHeight : side.clientWidth;
113+ const used = kids.reduce((n, k) => n + (along ? k.offsetHeight : k.offsetWidth), 0);
114+ gap = Math.max(gap, room - used);
115+ }
116+ setBuilt((was) =>
117+ was.stacks === all.length && Math.round(was.gap) === Math.round(gap)
118+ ? was
119+ : { stacks: all.length, gap },
120+ );
121+
122+ ctx.lineWidth = 1;
123+ ctx.strokeStyle = '#ff2d20';
124+ for (const w of geo.walls) {
125+ ctx.save();
126+ ctx.translate(w.rect.x + w.rect.w / 2, w.rect.y + w.rect.h / 2);
127+ ctx.rotate(w.angle ?? 0);
128+ ctx.strokeRect(-w.rect.w / 2, -w.rect.h / 2, w.rect.w, w.rect.h);
129+ ctx.restore();
130+ }
131+
132+ // Where a *tapped* discard is aimed. Not a fence, and not turned with the
133+ // square — which is the one thing on this page that is an approximation.
134+ ctx.setLineDash([5, 5]);
135+ ctx.strokeStyle = 'rgba(255, 45, 32, 0.45)';
136+ ctx.strokeRect(geo.pool.x + 0.5, geo.pool.y + 0.5, geo.pool.w - 1, geo.pool.h - 1);
137+ ctx.setLineDash([]);
138+ };
139+
140+ // After the layout has caught up with the properties just set.
141+ const id = requestAnimationFrame(paint);
142+ const observer = new ResizeObserver(paint);
143+ observer.observe(el);
144+ return () => {
145+ cancelAnimationFrame(id);
146+ observer.disconnect();
147+ };
148+ }, [place, drawn, showColliders, state]);
149+
150+ const css = `.wall-ring {\n --wall-turn: ${place.turn}deg;\n --wall-room: ${place.room};\n --wall-tile: ${place.tile};\n --wall-x: ${place.x}px;\n --wall-y: ${place.y}px;\n}`;
151+
152+ return (
153+ <div className="tune">
154+ <div className="stage wall-stage">
155+ <div className="table" ref={table}>
156+ <div className="center">
157+ <WallRing state={state} />
158+ </div>
159+ <canvas className="wall-overlay" ref={canvas} />
160+ </div>
161+ </div>
162+
163+ <div className="panel">
164+ <h1>The wall square</h1>
165+ <p className="hint">
166+ The real <code>WallRing</code> in a real <code>.center</code>, with every collider the
167+ physics would be handed drawn over it in red. Paste the block at the bottom into{' '}
168+ <code>styles.css</code>.
169+ </p>
170+
171+ {KNOBS.map((k) => (
172+ <label className="knob" key={k.key}>
173+ <span>
174+ {k.label}
175+ <em>{k.note}</em>
176+ </span>
177+ <input
178+ type="range"
179+ min={k.min}
180+ max={k.max}
181+ step={k.step}
182+ value={place[k.key]}
183+ onChange={(e) => setPlace((p) => ({ ...p, [k.key]: +e.target.value }))}
184+ />
185+ <output>{place[k.key]}</output>
186+ </label>
187+ ))}
188+
189+ <label className="knob">
190+ <span>
191+ Drawn from it<em>how far into the hand</em>
192+ </span>
193+ <input
194+ type="range"
195+ min={0}
196+ max={DRAWN.length - 1}
197+ step={1}
198+ value={DRAWN.indexOf(drawn) < 0 ? 0 : DRAWN.indexOf(drawn)}
199+ onChange={(e) => setDrawn(DRAWN[+e.target.value])}
200+ />
201+ <output>{drawn}</output>
202+ </label>
203+
204+ <label className="knob">
205+ <span>
206+ Colliders<em>what the physics gets</em>
207+ </span>
208+ <input
209+ type="checkbox"
210+ checked={showColliders}
211+ onChange={(e) => setShowColliders(e.target.checked)}
212+ />
213+ <output />
214+ </label>
215+
216+ <p className="hint built">
217+ {built.stacks} stacks standing · {built.stacks * 2} tiles ·{' '}
218+ {built.gap > 1
219+ ? `${Math.round(built.gap)}px of empty side`
220+ : 'the sides are full'}
221+ </p>
222+
223+ <div className="row">
224+ <button className="btn" onClick={() => setPlace({ ...START })}>
225+ Back to the table's
226+ </button>
227+ <button className="btn" onClick={() => void navigator.clipboard?.writeText(css)}>
228+ Copy
229+ </button>
230+ </div>
231+ <pre>{css}</pre>
232+ </div>
233+ </div>
234+ );
235+}
236+
237+createRoot(document.getElementById('root')!).render(<WallTuner />);
modifiedsrc/ui/Pool.tsx+12 −1
⋯ 389 unchanged lines
390390 // The table's own edge, and every tile standing on it.
391391 ctx.strokeStyle = '#ff2d20';
392392 line(world.bounds);
393- for (const w of world.walls) line(w.rect);
393+ for (const w of world.walls) {
394+ if (!w.angle) {
395+ line(w.rect);
396+ continue;
397+ }
398+ // Turned, so it is drawn turned — the wall square stands on its corner.
399+ ctx.save();
400+ ctx.translate(w.rect.x + w.rect.w / 2, w.rect.y + w.rect.h / 2);
401+ ctx.rotate(w.angle);
402+ ctx.strokeRect(-w.rect.w / 2, -w.rect.h / 2, w.rect.w, w.rect.h);
403+ ctx.restore();
404+ }
394405
395406 // Where a throw is aimed. Not a fence — see TableGeometry.pool.
396407 const aim = pool.poolRect;
⋯ 85 unchanged lines
modifiedsrc/ui/WallRing.tsx+22 −3
⋯ 5 unchanged lines
66 WALL_SIDES,
77 wallStacks,
88 type RingCapacity,
9+ type WallProgress,
910 } from '../game/wall';
10-import type { GameState } from '../game/engine';
11+
1112 import { useT } from './lang';
1213
1314 /**
⋯ 11 unchanged lines
2526 * measures these elements to build the colliders rather than restating the CSS
2627 * that sizes them.
2728 */
28-export function WallRing({ state }: { state: GameState }) {
29+export function WallRing({ state }: { state: WallProgress }) {
2930 const t = useT();
3031 const ring = useRef<HTMLDivElement>(null);
3132 const { cap, opening } = useSquare(ring);
⋯ 54 unchanged lines
8687 const box = probe.getBoundingClientRect();
8788 const cell = { s: box.width, d: box.height };
8889 if (cell.s <= 0) return;
89- const cap = ringFor({ w: middle.clientWidth, h: middle.clientHeight }, cell);
90+ // Stood on its corner, a square needs its *diagonal* where it used to
91+ // need its side — so the room it has to be built in shrinks by however
92+ // much the turn costs. Square on that is 1 and nothing changes; at 45° it
93+ // is √2 and the square comes out about seven tenths the size, which is
94+ // what keeps its arms on the table rather than off the edge of it.
95+ const style = getComputedStyle(el);
96+ const turn = (parseFloat(style.getPropertyValue('--wall-turn')) || 0) * (Math.PI / 180);
97+ const spread = Math.abs(Math.cos(turn)) + Math.abs(Math.sin(turn));
98+ const room = parseFloat(style.getPropertyValue('--wall-room')) || 1;
99+ // The walls hang *outside* the opening, one depth on each side, and
100+ // `ringFor` only expects to be told about one of them. Turned, the other
101+ // one is what runs off the table, so it comes off here.
102+ const cap = ringFor(
103+ {
104+ w: (middle.clientWidth * room) / spread - cell.d,
105+ h: (middle.clientHeight * room) / spread - cell.d,
106+ },
107+ cell,
108+ );
90109 const opening = ringOpening(cap, cell);
91110 setSquare((was) =>
92111 was.cap.h === cap.h &&
⋯ 19 unchanged lines
addedwall.html+13 −0
1+<!doctype html>
2+<html lang="en" translate="no">
3+ <head>
4+ <meta charset="UTF-8" />
5+ <meta name="google" content="notranslate" />
6+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+ <title>The wall square</title>
8+ </head>
9+ <body>
10+ <div id="root"></div>
11+ <script type="module" src="/src/tuneWall.tsx"></script>
12+ </body>
13+</html>