1function [vx, vy] = make_region(convex)
2%MAKE_REGION Random 2D sampling region, returned as CCW vertices (VX, VY).
3% MAKE_REGION(true) — a convex polygon (vertices on a random ellipse).
4% MAKE_REGION(false) — a non-convex "dumbbell": two convex disks joined by a
5% narrow tube. It is not star-shaped and need not contain
6% the origin — the sampler finds its own interior start.
7if nargin < 1 || isempty(convex)
8 convex = true;
9end
11if convex
12 [vx, vy] = convex_region();
13else
14 [vx, vy] = nonconvex_region();
15end
17% CCW so the interior is to the left of each edge.
18if signed_area(vx, vy) < 0
19 vx = vx(end:-1:1);
20 vy = vy(end:-1:1);
21end
22end
24function [vx, vy] = convex_region()
25% Vertices at increasing angles on an anisotropic, randomly rotated ellipse.
26% Points taken in angular order on the ellipse are always in convex position,
27% so the polygon is convex by construction — no convhull backend needed (the
28% browser worker may not have finished loading it when the figure view
29% auto-runs the script). Even angular slots + bounded jitter keep the angles
30% ordered and the edges non-degenerate while still random.
31m = 6 + randi(4); % 7..10 vertices
32slot = 2 * pi / m;
33ang = (0:m - 1).' * slot + (rand(m, 1) - 0.5) * slot * 0.8;
34ex = 1.4 * cos(ang); % on an ellipse (anisotropic)
35ey = 1.0 * sin(ang);
36phi = 2 * pi * rand; % random orientation
37vx = cos(phi) * ex - sin(phi) * ey;
38vy = sin(phi) * ex + cos(phi) * ey;
39end
41function [vx, vy] = nonconvex_region()
42% A "dumbbell": two convex disks (radius r, centers at x = +/-cx) joined by a
43% narrow tube of half-width w < r. This is non-convex and non-star-shaped — the
44% tube occludes each bulb from the other, so no single point sees the whole
45% region — which is what separates the two sampling modes (the local walk mostly
46% stays in one bulb, escaping only along a line down the tube; the union walk
47% also hops between bulbs along a line that clips both without the tube). Built
48% as one CCW loop: the outer (major) arc of each disk, with the straight tube
49% edges closing the gaps between the arc ends.
50r = 0.52 + 0.12 * rand; % bulb radius
51cx = 0.82 + 0.24 * rand; % half-distance between bulb centers (> r: disjoint)
52w = 0.07 + 0.06 * rand; % tube half-width (a narrow neck)
53w = min(w, 0.5 * r); % keep the tube clearly narrower than a bulb
54gam = asin(w / r); % half-angle each bulb's tube opening subtends
55K = 16; % points per bulb arc
57% Right bulb: major arc from the bottom opening CCW round to the top opening.
58tR = linspace(pi + gam, 3 * pi - gam, K);
59rxx = cx + r * cos(tR);
60ryy = r * sin(tR);
61% Left bulb: major arc from the top opening CCW round to the bottom opening.
62tL = linspace(gam, 2 * pi - gam, K);
63lxx = -cx + r * cos(tL);
64lyy = r * sin(tL);
66% Concatenate; the jumps arc-end -> next-arc-start are the straight tube edges.
67px = [rxx, lxx].';
68py = [ryy, lyy].';
70phi = 2 * pi * rand; % random overall orientation
71vx = cos(phi) * px - sin(phi) * py;
72vy = sin(phi) * px + cos(phi) * py;
73end
75function A = signed_area(vx, vy)
76%SIGNED_AREA Shoelace area; positive when the vertices run counterclockwise.
77n = numel(vx);
78A = 0;
79for i = 1:n
80 j = mod(i, n) + 1;
81 A = A + (vx(i) * vy(j) - vx(j) * vy(i));
82end
83A = A / 2;
84end