import {useEffect, useState, type CSSProperties} from 'react' import {RegionView, type Segment, type Pt} from './render/RegionView' import {useNetwork} from './useNetwork' import type {Points} from './types' // Discrete sample-count choices; the slider indexes into the active array. // Capped at 100k: every resample fans the samples out from the central peer to // every viewer over WebRTC (~800 KB as Float32 at 100k). const SAMPLE_CHOICES = [10, 100, 1000, 10000, 100000] // Non-convex sampling runs in the interpreter (no JIT), so cap it lower. const SAMPLE_CHOICES_NONCONVEX = [10, 100, 1000, 10000] /** The in-region segment(s) hit-and-run samples along: the line through * (px,py) with direction (dx,dy), intersected with the polygon. One segment * for a convex region, possibly several for a non-convex one. With `localOnly` * it keeps just the segment containing the current point (t = 0) — matching the * sampler's local-segment mode. Mirrors the sampler's geometry, so it * reproduces each step exactly. */ function regionSegments( region: Points, px: number, py: number, dx: number, dy: number, localOnly: boolean ): Segment[] { if (Math.hypot(dx, dy) < 1e-12) return [] const m = region.x.length const ts: number[] = [] for (let i = 0; i < m; i++) { const j = (i + 1) % m const ex = region.x[j] - region.x[i] const ey = region.y[j] - region.y[i] const denom = dy * ex - dx * ey if (Math.abs(denom) < 1e-12) continue const wx = region.x[i] - px const wy = region.y[i] - py const s = (dx * wy - dy * wx) / denom // position along the edge if (s >= 0 && s < 1) ts.push((wy * ex - wx * ey) / denom) } ts.sort((a, b) => a - b) const segs: Segment[] = [] for (let k = 0; k < ts.length - 1; k++) { const tm = (ts[k] + ts[k + 1]) / 2 if (!pointInPolygon(region, px + tm * dx, py + tm * dy)) continue // Local mode: keep only the in-region interval straddling t = 0. if (localOnly && !(ts[k] <= 0 && ts[k + 1] >= 0)) continue segs.push({ x0: px + ts[k] * dx, y0: py + ts[k] * dy, x1: px + ts[k + 1] * dx, y1: py + ts[k + 1] * dy }) } return segs } function pointInPolygon(region: Points, x: number, y: number): boolean { const n = region.x.length let inside = false for (let i = 0, j = n - 1; i < n; j = i++) { const xi = region.x[i] const yi = region.y[i] const xj = region.x[j] const yj = region.y[j] if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) { inside = !inside } } return inside } const prefixPoints = (p: Points, k: number): Points => ({ x: p.x.slice(0, k), y: p.y.slice(0, k) }) const short = (id: string) => id.slice(0, 8) + '…' export default function App() { const {snapshot, dispatch} = useNetwork() const {view, samples, samplesSynced, roster, amCentral, centralId, selfId} = snapshot const {params, region, busy, engine, engineError, movieStep} = view // Slider position: local while dragging, following the shared value // otherwise (a remote viewer may move it). const [n, setN] = useState(params.n) useEffect(() => setN(params.n), [params.n]) const nonConvex = !params.convex const choices = nonConvex ? SAMPLE_CHOICES_NONCONVEX : SAMPLE_CHOICES const useLocal = nonConvex && params.local const moviePlaying = movieStep !== null const controlsDisabled = !region || busy || moviePlaying || engine !== 'ready' const resample = (count: number, localMode: boolean = params.local) => { dispatch({op: 'resample', n: count, local: localMode}) } const newRegion = (count: number, convex: boolean) => { dispatch({op: 'newRegion', n: count, convex, local: params.local}) } // Checkbox: switch region type. Clamp N to the active set's max first. const setNonConvex = (makeNonConvex: boolean) => { const c = makeNonConvex ? SAMPLE_CHOICES_NONCONVEX : SAMPLE_CHOICES const clamped = Math.min(n, c[c.length - 1]) setN(clamped) newRegion(clamped, !makeNonConvex) } // Overlay for the current movie frame (settled points + segments + marks). // movieStep is shared state driven by the central peer's clock, so every // viewer sees the same frame; the geometry is recomputed locally from the // same samples, so it is identical everywhere. let cloud: Points = samples ?? {x: [], y: []} let segments: Segment[] | null = null let from: Pt | null = null let newPoint: Pt | null = null if (moviePlaying && samples && region && movieStep >= 2) { const i = Math.min(movieStep, samples.x.length - 1) // point being sampled const f = i - 1 // point the step starts from const {x, y} = samples cloud = prefixPoints(samples, i) // settled points 0..i-1 from = {x: x[f], y: y[f]} segments = regionSegments(region, x[f], y[f], x[i] - x[f], y[i] - y[f], useLocal) newPoint = {x: x[i], y: y[i]} } const canPlay = !!samples && samples.x.length >= 3 && engine === 'ready' const status = busy ? 'sampling…' : moviePlaying ? `movie · point ${movieStep + 1}` : !samplesSynced && samples ? 'syncing samples…' : `${params.n.toLocaleString()} points` const engineLabel = engine === 'ready' ? amCentral ? 'engine running here' : 'engine on central peer' : engine === 'starting' ? 'starting engine…' : engine === 'error' ? 'engine failed' : 'waiting for a central peer…' const waitingMessage = engine === 'starting' ? 'The central peer is starting the sampling engine…' : engine === 'error' ? `Engine failed: ${engineError ?? 'unknown error'}` : 'Connecting to the room…' return (
{short(selfId)}
{amCentral ? ' (central)' : ''}
{centralId ? short(centralId) : '(none)'}