1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
//! Boundary-glue operation: given two angle sequences and a known-valid
//! match interval between them, produce the angle sequence of the
//! combined boundary.
//!
//! This is the *operation* side of gluing. The data shapes describing
//! "where to glue" (positional spec, tile ids, etc.) live in
//! [`crate::geom::matches`]; pure-sequence angle utilities (revcomp, abs/rel
//! conversion, normalization) live in [`super::angles`].
//!
//! ## Match-interval convention
//!
//! Every glue operation here uses the asymmetric `(ns, mlen, ne)`
//! convention: the match consumes A's edges `[ns, ns + mlen)` cyclically,
//! and on B the match **ends** at index `ne` — i.e. `ne` is the first
//! *surviving* edge on B. Equivalently, the consumed B-edges run
//! `[ne - mlen, ne)` cyclically (walked in reverse since B is glued
//! anti-parallel to A). This matches what `Rat::get_match` returns and
//! what [`crate::geom::matches::TileMatch`] / [`crate::geom::matches::PatchMatch`]
//! store.
//!
//! ## Junction angles
//!
//! After a glue, the new boundary has (up to) two junction vertices
//! where the surviving A-edges meet the surviving B-edges:
//!
//! - **CW junction** at position `ns` (where surviving B meets
//! surviving A on the right side of the match).
//! - **CCW junction** at position `(ns + mlen) mod n` (where
//! surviving A meets surviving B on the left side).
//!
//! Each junction's "raw angle sum" is the sum of the two boundary
//! angles meeting there; the actual junction angle after gluing is
//! `sum - hturn` (normalized to `[-hturn, hturn]`). [`junction_sums`]
//! returns both sums; [`junctions_glueable`]
//! are cheap pre-filters.
use crateIsRing;
use normalize_angle;
// ============================================================
// Junction-validity primitives (cheap pre-filters).
// ============================================================
/// Raw angle sums at the two junction vertices of a candidate match.
///
/// Returns `(cw, ccw)` where:
///
/// `cw = a[ns] + b[ne] `
/// `ccw = a[(ns + mlen) % n] + b[(ne + m - mlen) % m] `
///
/// The actual signed junction angle after gluing is `sum - hturn`,
/// normalized to `[-hturn, hturn]`. Each sum has three regimes with
/// distinct geometric meanings:
///
/// - `sum > 0`: the junction is a **valid, non-degenerate corner** of
/// a glue that is **maximal** at this endpoint (cannot be extended).
/// - `sum == 0`: the revcomp identity `a[idx] == -b[idx']` holds at
/// this endpoint — the match could be **extended past this endpoint**
/// into a longer match. The current `mlen` is *not maximal* on this side.
/// - `sum < 0`: the would-be junction angle is `< -hturn`. After
/// normalization the boundary would **fold back / self-intersect** at
/// this junction; no valid simple-polygon glue is possible.
/// Pre-filter: the candidate match's two new junctions are
/// **glueable** — valid corners of a **maximal** glue at this endpoint.
///
/// Specifically, both `cw` and `ccw` from [`junction_sums`] are strictly
/// positive. The strict `> 0` simultaneously rejects two distinct bad
/// cases at each junction:
///
/// 1. `sum < 0`: the glue would self-intersect (junction angle below
/// fold-back). The full glue cannot exist.
/// 2. `sum == 0`: this candidate is **not maximal** at this endpoint —
/// a longer match exists with the same edges. Whoever is enumerating
/// matches should use that longer one instead of this candidate.
///
/// Necessary but not sufficient: the full glue still has to pass
/// [`glue_raw_angles`]'s `±hturn` check at each junction (which catches
/// the upper-end degeneracy `sum == 2*hturn`) and the downstream
/// self-intersection check.
///
/// Use this filter:
///
/// - **Before** `get_match`, when brute-force-enumerating single-edge
/// matches by sweeping `(ia, ib)` pairs (e.g.
/// `matchtypes::MatchFinder::candidates_for_pair`): rejects pairs that
/// are non-maximal as length-1 matches.
/// - **After** `get_match`, for multi-edge matches: maximality is
/// already guaranteed by `get_match`'s construction, so the `sum == 0`
/// case won't fire in practice; the check effectively reduces to
/// "no self-intersection at the junctions".
// ============================================================
// Glue operation.
// ============================================================
/// Result of gluing two boundary angle sequences.
/// One junction angle: `(angle_a + angle_b - hturn)` normalised to the
/// canonical `[-hturn, hturn]` range. The two arguments are the two
/// boundary angles meeting at the new junction (one from each side).
/// Compute the angle sequence resulting from gluing two boundary
/// segments along a **known-valid** match interval.
///
/// This is a pure algebraic transform on angle sequences: it does not
/// check or know about planar geometry, simplicity, or self-intersection
/// of the resulting boundary. The caller is responsible for ensuring
/// the input is geometrically sensible and for validating the output
/// (see "Non-local validity" below).
///
/// # Output shape per case
///
/// Three branches, distinguished by which side(s) retain surviving edges:
///
/// ## Normal glue (`surv_a > 0 && surv_b > 0`)
///
/// Both sides contribute. The new boundary is `[A-survivors,
/// B-survivors]`. Two distinct junction vertices are created -- `ccw`
/// (`a_yx`) at position `0` and `cw` (`a_xy`) at position `surv_a`.
/// This is the configuration of every "normal" tile-onto-patch glue.
///
/// ## Keystone glue (`surv_b == 0`, `mlen == m`)
///
/// All of B's edges are matched; B contributes zero surviving edges. **
/// This is a hole-filling operation, not a normal glue.** The
/// pre-glue patch A is *weakly simple*: its boundary has a tile-shaped
/// notch enclosing an empty cavity, with the notch's CW and CCW
/// endpoints coinciding at a single pinch-point vertex. B is the tile
/// that fits exactly into the cavity; placing it heals the pinch into
/// a strictly simple polygon.
///
/// Geometrically the CW and CCW junctions of the normal case collapse
/// into one **seam vertex** at position `0` whose merged angle absorbs
/// both junction contributions plus the B-side angle that would have
/// lived between them:
///
/// ```text
/// merged = a_yx + a_xy - y_first
/// = (x_first + y_first - hturn) + (y_first + x_last - hturn) - y_first
/// = x_first + x_last + y_first - turn
/// ```
///
/// Returns `None` if `merged.abs() == hturn` (degenerate pinched
/// junction).
///
/// The only production caller is [`crate::geom::patch::flower_petal_glue`]
/// during Penrose-style flower constructions where a central tile +
/// petals leave a tile-shaped cavity that the keystone tile closes.
/// `Snake` would reject the weakly-simple intermediate state; this is
/// why the keystone path operates at the `RawBoundary` level rather
/// than going through Snake validation at every step.
///
/// ## Degenerate / unreachable cases
///
/// - `surv_a == 0 && surv_b > 0` (**A-keystone**): mirror image of the
/// keystone case with A and B swapped -- A is the tile filling a
/// tile-shaped cavity in B's boundary. By convention every caller
/// has A = patch (large) and B = single tile, so this never fires;
/// `glue_raw_angles` panics via `unreachable!`. (Symmetric merged
/// formula could be added if ever needed.)
/// - `surv_a == 0 && surv_b == 0` (**both consumed**, `mlen == n == m`):
/// would require A and B to be the same tile shape glued along
/// their entire perimeters, producing a topological sphere with no
/// planar polygon realisation. Panics.
///
/// # Caller contract (preconditions)
///
/// 1. **Valid match interval.** The `mlen - 1` interior angle pairs
/// along the match must satisfy the revcomp relation
/// `a[(ns + i) % n] == -b[(ne + m - i) % m]` for every
/// `i ∈ 1..mlen`. The canonical way to obtain such a tuple is
/// [`crate::geom::rat::Rat::get_match`] or
/// [`crate::stringmatch::forward_match_length`] /
/// [`crate::geom::patch::GrowingPatch::get_all_matches`].
/// A `debug_assert!` enforces this in test builds; in release,
/// `glue_raw_angles` is unchecked and may produce a geometrically
/// meaningless boundary if fed a bogus interval.
///
/// 2. **No `±hturn` junctions.** Callers must reject results whose
/// `a_yx` or `a_xy` equals `±hturn` (degenerate pinched junction).
/// `glue_raw_angles` only rejects this internally in the
/// keystone-glue branch (where the two junctions collapse to one
/// merged angle, and that single angle is checked).
///
/// # Non-local validity (post-glue self-intersection)
///
/// `glue_raw_angles` only builds the new angle sequence. Whether the
/// resulting boundary is a valid simple polygon (no self-intersection,
/// hole-free) is the caller's responsibility, enforced by one of:
///
/// - [`crate::geom::snake::Snake::try_from`] on the returned `angles`
/// (used by `Rat::try_glue` and `GrowingPatch::init_from_first_add`);
/// - incremental grid-segment checks (`check_edge_clear`) used by
/// `GrowingPatch::add_tile_growing`;
/// - upstream canonical enumeration (`forward_match_length` chains in
/// `flower_petal_glue`) which guarantees the result is a valid simple
/// polygon when fed back into `GrowingPatch::from_parts`.
///
/// # Returns
///
/// `Some(GlueResult)` on success; `None` only in the keystone-glue case
/// if the merged junction angle would be the degenerate `±hturn`.
/// Other unreachable configurations panic rather than return `None`,
/// so a `None` here unambiguously means "valid keystone match, but the
/// merged angle pinches".