attachToWall function
The one attachment rule (spec 09c D4): where a symbol whose local box is
box stands against one of runs for the anchor point p (the raw
pointer in placement, D6; the plain-moved back-centre in a move, D8), or
null when no run takes it.
neighbours is parallel to runs: neighbours[i] are the symbols
already against runs[i] (an ArgumentError when the lengths differ);
the one whose instance is exclude is passed over.
- The face. A run is a candidate when
−w/2 ≤ s ≤ captureWorldand−captureWorld ≤ u ≤ L + captureWorld,s = (p − a)·m,u = (p − a)·t:pin the room near the face, or in the wall's body on this face's half of the band. The winner: the smallest|s|; then the smallest distance fromuto[0, L]; then the lower wall handle; then the left face; then the lowera·t. The two distances are compared withinwallJoin.linear(two pieces of one face line give the samesonly within rounding). - The winner must hold the symbol:
W ≤ L + wallJoin.linear, else null. - The rotation takes local
+yto−m:(cos, sin) = (t.x, t.y)exactly,-0.0normalised (by placementTransform, which cleans every component it stores). - The centre at
u, then the edge snap withinedgeCaptureWorldthat shiftsuleast (a tie to the smalleru): the left sideu − W/2to0or to a neighbour'shi, the right sideu + W/2toLor to a neighbour'slo. Then the clamp to[W/2, L − W/2]whenW < L, elseu = L/2. placementTransform(at: q, basePoint: c, rotation: (cos, sin), mirrored),q = a + u·t,cthe box's back-centre: the back edge on the face line, the front in the room, a mirror pivoting about the box's centrex.
With keep (the host's rotate refused, Slice 4's F-7), the symbol
keeps keep's linear part: a run is a candidate in step 1 only when
its t is the rotation keep already has, (keep.d, −keep.c) within
Tolerance.standard.angular (a mirror negates a and b only), and
step 5's transform is keep's linear part, bit for bit, translated so
the box's back-centre lands on q; mirrored is then keep's own.
A keep within that tolerance of t but not on it leaves the back edge
off the face line by up to W/2 times it; the keeps the planner passes
(an earlier attachment's, a quarter turn's) are exact.
O(runs + neighbours of the winner), allocating only the result (no per-run allocation).
Implementation
WallAttachment? attachToWall(
List<FaceRun> runs,
SymbolBox box,
Vector2 p,
double captureWorld, {
required bool mirrored,
required List<List<FaceNeighbour>> neighbours,
required double edgeCaptureWorld,
Handle? exclude,
Transform2? keep,
}) {
if (neighbours.length != runs.length) {
throw ArgumentError.value(neighbours.length, 'neighbours',
'must be parallel to runs (${runs.length})');
}
final tol = wallJoin.linear;
final turnTol = Tolerance.standard.angular;
var best = -1;
var bestS = 0.0, bestGap = 0.0, bestU = 0.0;
for (var i = 0; i < runs.length; i++) {
final r = runs[i];
if (keep != null &&
((r.t.x - keep.d).abs() > turnTol ||
(r.t.y + keep.c).abs() > turnTol)) {
continue;
}
final dx = p.x - r.a.x, dy = p.y - r.a.y;
final s = dx * r.m.x + dy * r.m.y;
if (s < -r.thickness / 2 || s > captureWorld) continue;
final u = dx * r.t.x + dy * r.t.y;
if (u < -captureWorld || u > r.length + captureWorld) continue;
final absS = s.abs();
final gap = u < 0 ? -u : (u > r.length ? u - r.length : 0.0);
if (best >= 0 &&
!_ranksBefore(r, absS, gap, runs[best], bestS, bestGap, tol)) {
continue;
}
best = i;
bestS = absS;
bestGap = gap;
bestU = u;
}
if (best < 0) return null;
final run = runs[best];
final width = box.right - box.left;
final length = run.length;
if (width > length + tol) return null;
final half = width / 2;
var u = bestU;
var shift = 0.0;
var snapped = false;
// Each target is where the centre goes when that side meets it; `d` is
// the shift to it. Plain locals and a static test, no closure: a closure
// over `u` and `shift` would allocate its context and box each double it
// writes (D4: the query allocates O(1)).
var d = half - u;
if (_snapsBefore(d, shift, snapped, edgeCaptureWorld)) {
shift = d;
snapped = true;
}
d = (length - half) - u;
if (_snapsBefore(d, shift, snapped, edgeCaptureWorld)) {
shift = d;
snapped = true;
}
final ns = neighbours[best];
for (var k = 0; k < ns.length; k++) {
final n = ns[k];
if (exclude != null && n.instance == exclude) continue;
d = (n.hi + half) - u;
if (_snapsBefore(d, shift, snapped, edgeCaptureWorld)) {
shift = d;
snapped = true;
}
d = (n.lo - half) - u;
if (_snapsBefore(d, shift, snapped, edgeCaptureWorld)) {
shift = d;
snapped = true;
}
}
if (snapped) u += shift;
if (width < length) {
final lo = half, hi = length - half;
u = u < lo ? lo : (u > hi ? hi : u);
} else {
u = length / 2;
}
final q = Vector2(run.a.x + run.t.x * u, run.a.y + run.t.y * u);
if (keep != null) {
final cx = (box.left + box.right) / 2, cy = box.back;
// D5: no `-0.0` stored, as `placementTransform` cleans its own.
final e = q.x - (keep.a * cx + keep.c * cy);
final f = q.y - (keep.b * cx + keep.d * cy);
return (
transform: Transform2(
keep.a, keep.b, keep.c, keep.d, e == 0 ? 0.0 : e, f == 0 ? 0.0 : f),
q: q,
run: run,
);
}
return (
transform: placementTransform(
at: q,
basePoint: Vector2((box.left + box.right) / 2, box.back),
// Step 3: `(t.x, t.y)` exactly. `placementTransform` normalises
// every `-0.0` it stores (D5), so a `-0.0` in `t` never reaches the
// transform; normalising here as well would be dead code.
rotation: (run.t.x, run.t.y),
mirrored: mirrored,
),
q: q,
run: run,
);
}