discontinuedPackage jet_cad_floor_plan has been discontinued and replaced by jet_cad_2d_plan.

attachToWall function

WallAttachment? attachToWall(
  1. List<FaceRun> runs,
  2. SymbolBox box,
  3. Vector2 p,
  4. double captureWorld, {
  5. required bool mirrored,
  6. required List<List<FaceNeighbour>> neighbours,
  7. required double edgeCaptureWorld,
  8. Handle? exclude,
  9. Transform2? keep,
})

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.

  1. The face. A run is a candidate when −w/2 ≤ s ≤ captureWorld and −captureWorld ≤ u ≤ L + captureWorld, s = (p − a)·m, u = (p − a)·t: p in 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 from u to [0, L]; then the lower wall handle; then the left face; then the lower a·t. The two distances are compared within wallJoin.linear (two pieces of one face line give the same s only within rounding).
  2. The winner must hold the symbol: W ≤ L + wallJoin.linear, else null.
  3. The rotation takes local +y to −m: (cos, sin) = (t.x, t.y) exactly, -0.0 normalised (by placementTransform, which cleans every component it stores).
  4. The centre at u, then the edge snap within edgeCaptureWorld that shifts u least (a tie to the smaller u): the left side u − W/2 to 0 or to a neighbour's hi, the right side u + W/2 to L or to a neighbour's lo. Then the clamp to [W/2, L − W/2] when W < L, else u = L/2.
  5. placementTransform(at: q, basePoint: c, rotation: (cos, sin), mirrored), q = a + u·t, c the box's back-centre: the back edge on the face line, the front in the room, a mirror pivoting about the box's centre x.

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,
  );
}