9fbdc3eInteractive hit-and-run sampling of a 2D convex regionJeremy Magland 1import { useEffect, useState, type CSSProperties } from "react";
2import {
3 RegionView,
4 type Points,
5 type Segment,
6 type Pt,
7} from "./render/RegionView.js";
8import { onData, onHostEvent, sendToMATLAB } from "./bridge.js";
10/** Payload from the numbl script: a convex region (CCW polygon) plus the
11 * hit-and-run samples drawn from it. Mirrors what hitandrun_sampler.m sends,
12 * both as the initial `Data` and as the `data` event after "New region". */
13interface HitAndRunData {
14 type: "hitandrun";
15 region: Points;
16 samples: Points;
17 n: number;
d9d003fAdd non-convex (star) regions with a general hit-and-run samplerJeremy Magland 18 convex?: boolean; // false → non-convex (star) region
21function isHitAndRunData(d: unknown): d is HitAndRunData {
22 return (
23 !!d &&
24 typeof d === "object" &&
25 (d as HitAndRunData).type === "hitandrun" &&
26 !!(d as HitAndRunData).region &&
27 !!(d as HitAndRunData).samples
28 );
29}
31/** `samples` event payload: fresh points for the *same* region (after Resample
32 * or moving the samples slider). */
33interface SamplesEvent {
34 x: number[];
35 y: number[];
36 n: number;
37}
d9d003fAdd non-convex (star) regions with a general hit-and-run samplerJeremy Magland 39// Discrete sample-count choices; the slider indexes into the active array.
9fbdc3eInteractive hit-and-run sampling of a 2D convex regionJeremy Magland 40const SAMPLE_CHOICES = [10, 100, 1000, 10000, 100000, 1000000];
d9d003fAdd non-convex (star) regions with a general hit-and-run samplerJeremy Magland 41// Non-convex sampling runs in the interpreter (no JIT), so cap it lower.
42const SAMPLE_CHOICES_NONCONVEX = [10, 100, 1000, 10000];
9fbdc3eInteractive hit-and-run sampling of a 2D convex regionJeremy Magland 43const DEFAULT_N = 10000;
45// Sampling movie: reveal points one at a time. Each frame shows the chord the
46// step samples along AND the point that landed on it, together.
47const MOVIE_STEP_MS = 750;
48const MOVIE_LAST_INDEX = 41; // animate sampling of points up to this index
50interface MovieState {
51 step: number; // index of the point currently being sampled
52}
d9d003fAdd non-convex (star) regions with a general hit-and-run samplerJeremy Magland 54/** The in-region segment(s) hit-and-run samples along: the line through
55 * (px,py) with direction (dx,dy), intersected with the polygon. One segment
a50c671Non-convex: add local-segment vs. union sampling modeJeremy Magland 56 * for a convex region, possibly several for a non-convex one. With `localOnly`
57 * it keeps just the segment containing the current point (t = 0) — matching the
58 * sampler's local-segment mode. Mirrors the sampler's geometry, so it
59 * reproduces each step exactly. */
d9d003fAdd non-convex (star) regions with a general hit-and-run samplerJeremy Magland 60function regionSegments(
62 px: number,
63 py: number,
64 dx: number,
66 localOnly: boolean
d9d003fAdd non-convex (star) regions with a general hit-and-run samplerJeremy Magland 67): Segment[] {
68 if (Math.hypot(dx, dy) < 1e-12) return [];
9fbdc3eInteractive hit-and-run sampling of a 2D convex regionJeremy Magland 69 const m = region.x.length;
d9d003fAdd non-convex (star) regions with a general hit-and-run samplerJeremy Magland 70 const ts: number[] = [];
9fbdc3eInteractive hit-and-run sampling of a 2D convex regionJeremy Magland 71 for (let i = 0; i < m; i++) {
72 const j = (i + 1) % m;
73 const ex = region.x[j] - region.x[i];
74 const ey = region.y[j] - region.y[i];
d9d003fAdd non-convex (star) regions with a general hit-and-run samplerJeremy Magland 75 const denom = dy * ex - dx * ey;
76 if (Math.abs(denom) < 1e-12) continue;
77 const wx = region.x[i] - px;
78 const wy = region.y[i] - py;
79 const s = (dx * wy - dy * wx) / denom; // position along the edge
80 if (s >= 0 && s < 1) ts.push((wy * ex - wx * ey) / denom);
d9d003fAdd non-convex (star) regions with a general hit-and-run samplerJeremy Magland 82 ts.sort((a, b) => a - b);
83 const segs: Segment[] = [];
84 for (let k = 0; k < ts.length - 1; k++) {
85 const tm = (ts[k] + ts[k + 1]) / 2;
a50c671Non-convex: add local-segment vs. union sampling modeJeremy Magland 86 if (!pointInPolygon(region, px + tm * dx, py + tm * dy)) continue;
87 // Local mode: keep only the in-region interval straddling t = 0.
88 if (localOnly && !(ts[k] <= 0 && ts[k + 1] >= 0)) continue;
89 segs.push({
90 x0: px + ts[k] * dx,
91 y0: py + ts[k] * dy,
92 x1: px + ts[k + 1] * dx,
93 y1: py + ts[k + 1] * dy,
94 });
96 return segs;
97}
99function pointInPolygon(region: Points, x: number, y: number): boolean {
100 const n = region.x.length;
101 let inside = false;
102 for (let i = 0, j = n - 1; i < n; j = i++) {
103 const xi = region.x[i];
104 const yi = region.y[i];
105 const xj = region.x[j];
106 const yj = region.y[j];
107 if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) {
108 inside = !inside;
109 }
110 }
111 return inside;
114const prefixPoints = (p: Points, k: number): Points => ({
115 x: p.x.slice(0, k),
116 y: p.y.slice(0, k),
117});
119export function App() {
120 const [data, setData] = useState<HitAndRunData | null>(null);
121 const [n, setN] = useState(DEFAULT_N);
122 const [busy, setBusy] = useState(false);
123 const [movie, setMovie] = useState<MovieState | null>(null);
a50c671Non-convex: add local-segment vs. union sampling modeJeremy Magland 124 // Non-convex sampling mode: false = sample the union of all in-region
125 // segments the line makes; true = only the segment through the current point.
126 const [local, setLocal] = useState(false);
128 useEffect(() => {
129 // Initial region + samples (script → page via `Data`).
130 const offData = onData(d => {
131 if (isHitAndRunData(d)) {
132 setData(d);
133 setN(d.n);
134 setMovie(null);
135 }
136 });
137 // "New region": the script sends a full fresh payload.
138 const offFull = onHostEvent("data", d => {
139 if (isHitAndRunData(d)) {
140 setData(d);
141 setN(d.n);
142 setBusy(false);
143 setMovie(null);
144 }
145 });
146 // Resample (same region): only the points change.
147 const offSamples = onHostEvent("samples", ev => {
148 const s = ev as SamplesEvent;
149 if (!s || !Array.isArray(s.x)) return;
150 setData(prev =>
151 prev ? { ...prev, samples: { x: s.x, y: s.y }, n: s.n } : prev
152 );
153 setBusy(false);
154 setMovie(null);
155 });
156 return () => {
157 offData();
158 offFull();
159 offSamples();
160 };
161 }, []);
163 // Movie clock: advance phase/step on a timer (a setTimeout chain — the effect
164 // reruns whenever `movie` changes). Ends (→ full cloud) past the last index.
165 useEffect(() => {
166 if (!movie || !data) return;
167 const lastIndex = Math.min(data.samples.x.length - 1, MOVIE_LAST_INDEX);
168 const id = setTimeout(() => {
169 setMovie(m => {
170 if (!m) return m;
171 const next = m.step + 1;
172 return next > lastIndex ? null : { step: next };
173 });
174 }, MOVIE_STEP_MS);
175 return () => clearTimeout(id);
176 }, [movie, data]);
d9d003fAdd non-convex (star) regions with a general hit-and-run samplerJeremy Magland 178 const nonConvex = !!data && data.convex === false;
179 const choices = nonConvex ? SAMPLE_CHOICES_NONCONVEX : SAMPLE_CHOICES;
a50c671Non-convex: add local-segment vs. union sampling modeJeremy Magland 180 // `local` only applies to non-convex regions.
181 const useLocal = nonConvex && local;
a50c671Non-convex: add local-segment vs. union sampling modeJeremy Magland 183 // Re-draw `count` samples in the current region (script round-trip). The
184 // sampling mode (`localMode`) can be overridden — the local checkbox passes
185 // its new value directly, since the `local` state hasn't committed yet.
186 const resample = (count: number, localMode: boolean = local) => {
188 setBusy(true);
d9d003fAdd non-convex (star) regions with a general hit-and-run samplerJeremy Magland 189 sendToMATLAB("resample", {
190 n: count,
191 x: data.region.x,
192 y: data.region.y,
193 convex: data.convex !== false,
d9d003fAdd non-convex (star) regions with a general hit-and-run samplerJeremy Magland 198 // Generate a brand new region (convex or not) and sample it.
199 const newRegion = (count: number, convex: boolean) => {
a50c671Non-convex: add local-segment vs. union sampling modeJeremy Magland 201 sendToMATLAB("newRegion", { n: count, convex, local });
204 // Checkbox: switch region type. Clamp N to the active set's max first.
205 const setNonConvex = (makeNonConvex: boolean) => {
206 const c = makeNonConvex ? SAMPLE_CHOICES_NONCONVEX : SAMPLE_CHOICES;
207 const clamped = Math.min(n, c[c.length - 1]);
208 setN(clamped);
209 newRegion(clamped, !makeNonConvex);
a50c671Non-convex: add local-segment vs. union sampling modeJeremy Magland 212 // Checkbox: switch the non-convex sampling mode; resample the same region.
213 const setLocalMode = (value: boolean) => {
214 setLocal(value);
215 resample(n, value);
216 };
9fbdc3eInteractive hit-and-run sampling of a 2D convex regionJeremy Magland 218 const toggleMovie = () => {
219 if (movie) {
220 setMovie(null);
221 } else if (data && data.samples.x.length >= 3) {
222 setMovie({ step: 2 });
223 }
224 };
d9d003fAdd non-convex (star) regions with a general hit-and-run samplerJeremy Magland 226 // Overlay for the current movie frame (settled points + segments + marks).
9fbdc3eInteractive hit-and-run sampling of a 2D convex regionJeremy Magland 227 let cloud: Points = data ? data.samples : { x: [], y: [] };
d9d003fAdd non-convex (star) regions with a general hit-and-run samplerJeremy Magland 228 let segments: Segment[] | null = null;
9fbdc3eInteractive hit-and-run sampling of a 2D convex regionJeremy Magland 229 let from: Pt | null = null;
230 let newPoint: Pt | null = null;
231 if (movie && data) {
232 const i = movie.step; // point being sampled
233 const f = i - 1; // point the step starts from
234 const { x, y } = data.samples;
235 cloud = prefixPoints(data.samples, i); // settled points 0..i-1
236 from = { x: x[f], y: y[f] };
a50c671Non-convex: add local-segment vs. union sampling modeJeremy Magland 237 segments = regionSegments(
238 data.region,
239 x[f],
240 y[f],
241 x[i] - x[f],
242 y[i] - y[f],
243 useLocal
244 );
9fbdc3eInteractive hit-and-run sampling of a 2D convex regionJeremy Magland 245 newPoint = { x: x[i], y: y[i] };
246 }
248 const canPlay = !!data && data.samples.x.length >= 3;
250 return (
251 <div style={rootStyle}>
252 {data ? (
253 <RegionView
254 region={data.region}
255 samples={cloud}
d9d003fAdd non-convex (star) regions with a general hit-and-run samplerJeremy Magland 256 segments={segments}
258 newPoint={newPoint}
259 />
260 ) : (
261 <div style={waitingStyle}>Waiting for the region from the script…</div>
262 )}
264 <div style={panelStyle}>
265 <label style={labelStyle}>
266 Samples: <b>{n.toLocaleString()}</b>
267 <input
268 type="range"
269 min={0}
d9d003fAdd non-convex (star) regions with a general hit-and-run samplerJeremy Magland 270 max={choices.length - 1}
d9d003fAdd non-convex (star) regions with a general hit-and-run samplerJeremy Magland 272 value={Math.max(0, choices.indexOf(n))}
9fbdc3eInteractive hit-and-run sampling of a 2D convex regionJeremy Magland 273 disabled={!data || busy || !!movie}
274 // Drag updates the label live; the script round-trip fires on
275 // release (and on arrow-key release) to avoid flooding it.
d9d003fAdd non-convex (star) regions with a general hit-and-run samplerJeremy Magland 276 onChange={e => setN(choices[Number(e.target.value)])}
277 onPointerUp={e => resample(choices[Number(e.currentTarget.value)])}
279 if (e.key.startsWith("Arrow")) {
d9d003fAdd non-convex (star) regions with a general hit-and-run samplerJeremy Magland 280 resample(choices[Number(e.currentTarget.value)]);
282 }}
283 style={sliderStyle}
284 />
285 </label>
287 <div style={{ display: "flex", gap: 6, marginTop: 6 }}>
288 <button
289 style={btnStyle}
290 disabled={!data || busy || !!movie}
291 onClick={() => resample(n)}
292 title="Draw a fresh set of samples in the same region"
293 >
294 Resample
295 </button>
296 <button
297 style={btnStyle}
298 disabled={busy || !!movie}
d9d003fAdd non-convex (star) regions with a general hit-and-run samplerJeremy Magland 299 onClick={() => newRegion(n, !nonConvex)}
300 title="Generate a new region and sample it"
302 New region
303 </button>
304 </div>
d9d003fAdd non-convex (star) regions with a general hit-and-run samplerJeremy Magland 306 <label style={checkLabelStyle}>
307 <input
308 type="checkbox"
309 checked={nonConvex}
310 disabled={!data || busy || !!movie}
311 onChange={e => setNonConvex(e.target.checked)}
312 />
313 non-convex region
314 </label>
317 <label
318 style={subCheckLabelStyle}
319 title="Sample only the segment through the current point instead of every segment the line crosses"
320 >
321 <input
322 type="checkbox"
323 checked={local}
324 disabled={!data || busy || !!movie}
325 onChange={e => setLocalMode(e.target.checked)}
326 />
327 local segment only
328 </label>
329 )}
332 style={playBtnStyle}
333 disabled={!canPlay || busy}
334 onClick={toggleMovie}
335 title="Animate the hit-and-run steps: chord, then the sampled point"
336 >
337 {movie ? "■ Stop movie" : "▶ Play movie"}
338 </button>
340 <div style={{ fontSize: 10, color: "#64748b", marginTop: 6 }}>
341 {busy
342 ? "sampling…"
343 : movie
344 ? `movie · point ${movie.step + 1}`
345 : `${data ? data.n.toLocaleString() : "—"} points`}
346 </div>
347 </div>
348 </div>
349 );
350}
352const rootStyle: CSSProperties = {
353 position: "absolute",
354 inset: 0,
355 overflow: "hidden",
356 background: "#ffffff",
357 fontFamily: "system-ui, -apple-system, Arial, sans-serif",
358};
360const waitingStyle: CSSProperties = {
361 position: "absolute",
362 inset: 0,
363 display: "flex",
364 alignItems: "center",
365 justifyContent: "center",
366 color: "#94a3b8",
367};
369const panelStyle: CSSProperties = {
370 position: "absolute",
371 top: 8,
372 left: 8,
373 width: 150,
374 padding: "7px 9px",
375 background: "rgba(255,255,255,0.9)",
376 border: "1px solid #e2e8f0",
377 borderRadius: 6,
378 boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
379 color: "#0f172a",
380};
382const labelStyle: CSSProperties = {
383 display: "block",
384 fontSize: 11,
385};
d9d003fAdd non-convex (star) regions with a general hit-and-run samplerJeremy Magland 387const checkLabelStyle: CSSProperties = {
388 display: "flex",
389 alignItems: "center",
390 gap: 5,
391 fontSize: 11,
392 marginTop: 8,
393 cursor: "pointer",
394};
a50c671Non-convex: add local-segment vs. union sampling modeJeremy Magland 396const subCheckLabelStyle: CSSProperties = {
397 display: "flex",
398 alignItems: "center",
399 gap: 5,
400 fontSize: 10,
401 marginTop: 4,
402 marginLeft: 14,
403 color: "#475569",
404 cursor: "pointer",
405};
9fbdc3eInteractive hit-and-run sampling of a 2D convex regionJeremy Magland 407const sliderStyle: CSSProperties = {
408 width: "100%",
409 marginTop: 2,
410};
412const btnStyle: CSSProperties = {
413 flex: 1,
414 padding: "3px 4px",
415 fontSize: 10,
416 whiteSpace: "nowrap",
417 cursor: "pointer",
418 background: "#f8fafc",
419 border: "1px solid #cbd5e1",
420 borderRadius: 5,
421 color: "#0f172a",
422};
424const playBtnStyle: CSSProperties = {
425 width: "100%",
426 marginTop: 6,
427 padding: "4px 6px",
428 fontSize: 10,
429 cursor: "pointer",
430 background: "#eff6ff",
431 border: "1px solid #bfdbfe",
432 borderRadius: 5,
433 color: "#1e3a8a",
434};