draftwright 0.2.2__py3-none-any.whl → 0.2.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
draftwright/_core.py CHANGED
@@ -14,7 +14,7 @@ import functools
14
14
  import logging
15
15
  import re
16
16
  from collections.abc import Callable
17
- from dataclasses import dataclass, field
17
+ from dataclasses import dataclass
18
18
  from pathlib import Path
19
19
  from types import SimpleNamespace
20
20
  from typing import TYPE_CHECKING
@@ -166,16 +166,17 @@ def _dim(p1, p2, side, distance, draft, **kwargs):
166
166
  class Strip:
167
167
  """A one-dimensional annotation band adjacent to an orthographic view.
168
168
 
169
- Annotations are stacked outward from the view edge by calling
170
- :meth:`allocate`. The cursor starts at ``anchor + direction * gap`` and
171
- advances after each successful allocation.
169
+ A plain geometry record: the collect-then-solve placers (ADR 0009) read its
170
+ bounds (:func:`~draftwright.annotations._common.strip_free_span`) and carve
171
+ around the placed annotations. The mutable ``allocate``/``peek`` cursor was
172
+ retired once every placer moved to the carve (#150).
172
173
 
173
174
  Attributes:
174
175
  anchor: Page coordinate of the view edge this strip starts from.
175
176
  outer_limit: Page coordinate at which the strip ends (page margin,
176
177
  neighbouring view, or title-block boundary).
177
- direction: ``+1`` — cursor moves away from anchor (right/above);
178
- ``-1`` — cursor retreats from anchor (left/below).
178
+ direction: ``+1`` — stacks away from anchor (right/above);
179
+ ``-1`` — stacks back toward smaller coords (left/below).
179
180
  gap: Clearance between the view edge and the first annotation.
180
181
  spacing: Clearance between successive annotations.
181
182
  """
@@ -185,56 +186,12 @@ class Strip:
185
186
  direction: float = 1.0
186
187
  gap: float = 8.0
187
188
  spacing: float = 4.0
188
- _cursor: float = field(init=False, compare=False, repr=False)
189
-
190
- def __post_init__(self):
191
- self._cursor = self.anchor + self.direction * self.gap
192
-
193
- # ------------------------------------------------------------------
194
- # Public API
195
189
 
196
190
  @property
197
191
  def available(self) -> float:
198
192
  """Total space available in this strip (mm)."""
199
193
  return abs(self.outer_limit - self.anchor)
200
194
 
201
- @property
202
- def depth_used(self) -> float:
203
- """How far the cursor has advanced from the anchor (mm)."""
204
- return abs(self._cursor - self.anchor)
205
-
206
- def peek(self, size: float) -> float | None:
207
- """Return what ``allocate(size)`` would return without advancing the cursor."""
208
- if self.direction == 1:
209
- start = self._cursor
210
- return start if (start + size) <= self.outer_limit else None
211
- else:
212
- end = self._cursor
213
- return end if (end - size) >= self.outer_limit else None
214
-
215
- def allocate(self, size: float) -> float | None:
216
- """Reserve *size* mm; return the near-edge page coordinate, or ``None`` if full.
217
-
218
- The returned value is the page coordinate of the annotation's
219
- dimension line (or leader elbow). Convert to a relative offset with::
220
-
221
- distance = abs(page_coord - strip.anchor)
222
- """
223
- if self.direction == 1:
224
- start = self._cursor
225
- end = start + size
226
- if end > self.outer_limit:
227
- return None
228
- self._cursor = end + self.spacing
229
- return start
230
- else:
231
- end = self._cursor
232
- start = end - size
233
- if start < self.outer_limit:
234
- return None
235
- self._cursor = start - self.spacing
236
- return end
237
-
238
195
 
239
196
  @dataclass
240
197
  class ViewZones:
@@ -7,6 +7,50 @@ and an AABB overlap test (`_box_hits`). Bottom of the annotations DAG.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import logging
11
+ from dataclasses import dataclass, field
12
+
13
+ from build123d_drafting.helpers import Dimension, SafeDimension
14
+
15
+ from draftwright.layout import StripCandidate, plan_strip
16
+
17
+ _log = logging.getLogger(__name__)
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class Escalation:
22
+ """A first-class "could not place this here" signal (ADR 0009 Amendment 1, P5-strand-2).
23
+
24
+ Placers *collect* one of these into ``dwg._escalations`` at the point of failure —
25
+ instead of recording a stringly-typed ``*_dropped`` lint code and letting the escalators
26
+ grep for it — and one later resolver pass groups them by ``(view, feature-or-pattern)``,
27
+ picks a remedy per group (ISO pattern-grouped balloon / table / detail / drop), and emits
28
+ the ``*_dropped`` lint codes only for what stays unresolved (so coverage lint + the
29
+ cleanliness ratchet keep working). See the ADR / epic #351.
30
+
31
+ The hole callout/location placers emit these (#351 PR-2); the resolver in
32
+ ``annotations/orchestrator.py`` (``_maybe_tabulate_holes``) consumes them, including
33
+ the ISO pattern-grouped balloon fallback for a dropped pattern callout (#351 PR-3).
34
+
35
+ Attributes:
36
+ kind: what could not be placed — ``"callout" | "location" | "slot" | "step" | "pmi"``.
37
+ view: the owning orthographic view (``None`` for drawing-level).
38
+ feature: reference to the IR feature / ``HoleRef`` / key it belongs to — carries the
39
+ pattern membership the resolver groups on (a ``"callout"`` escalation's
40
+ feature is the dropped group's ``PatternFeature`` when it is a
41
+ fully-surviving recognised pattern, else ``None``). Left untyped to keep
42
+ this module a leaf (no dependency on ``model.ir``).
43
+ reason: why placement failed — ``"strip_full" | "illegible" | "corridor_blocked" | "no_room"``.
44
+ remedies: ranked candidate remedies the resolver may pick, e.g.
45
+ ``("group_balloon", "table", "detail", "drop")``. Empty = resolver's default ladder.
46
+ """
47
+
48
+ kind: str
49
+ view: str | None
50
+ feature: object
51
+ reason: str
52
+ remedies: tuple[str, ...] = field(default_factory=tuple)
53
+
10
54
 
11
55
  def _anno_box(o):
12
56
  """Page-space bbox ``(x0, y0, x1, y1)`` of an annotation — its text
@@ -38,6 +82,139 @@ def _occupied_boxes(dwg):
38
82
  return boxes
39
83
 
40
84
 
85
+ def _geom_box(o):
86
+ """Full rendered-geometry bbox ``(x0, y0, x1, y1)`` of an annotation — leader
87
+ shafts and arrow tips, dimension witness/extension lines, centrelines, hatch —
88
+ *not* just its label box. ``None`` if it does not bbox cleanly (logged at
89
+ debug: a silently dropped occupant is the wrong failure mode for an occupancy
90
+ model, so the omission is at least observable)."""
91
+ try:
92
+ b = o.bounding_box()
93
+ return (b.min.X, b.min.Y, b.max.X, b.max.Y)
94
+ except Exception as exc: # noqa: BLE001 — not every annotation bbox-es cleanly
95
+ _log.debug("strip occupancy: %s did not bbox (%s); omitted", type(o).__name__, exc)
96
+ return None
97
+
98
+
99
+ CROSSABLE_TYPES = frozenset({"Centerline", "CenterlineCircle", "CenterMark"})
100
+ """Annotation types a *dimension* may legitimately cross (ISO 128): centre lines
101
+ and centre marks. A **leader**, by contrast, must avoid them (#305) — so this is a
102
+ per-consumer choice, passed as ``crossable`` to :func:`strip_obstacles`."""
103
+
104
+
105
+ def strip_obstacles(dwg, view=None, *, crossable=()):
106
+ """The COMPLETE occupancy for strip placement (ADR 0009): every placed
107
+ annotation's full rendered footprint, optionally restricted to *view*, minus
108
+ any annotation whose type name is in *crossable* (things this particular
109
+ consumer may legitimately overlap — e.g. a location dim crosses a centre line
110
+ but a leader does not; see :data:`CROSSABLE_TYPES`).
111
+
112
+ Unlike :func:`_occupied_boxes` (label boxes only, with bare centrelines
113
+ excluded), this captures the geometry a label box hides — leader shafts and
114
+ arrow tips, dimension witness/extension lines, centrelines, and the section
115
+ hatch. That hidden geometry is the 'invisible occupant' class behind the
116
+ recurring strip overlaps (#133/#225/#305): a placer that consults only label
117
+ boxes commits a callout into space a leader or extension line already crosses.
118
+
119
+ *view* scoping keeps this view's own annotations **and** drawing-level obstacles
120
+ that no orthographic view owns (the section hatch, title block, …) — those a
121
+ strip placer must still avoid — and drops only the *other* ortho views' blocks
122
+ (which compose-then-pack keeps disjoint, ADR 0004). The section hatch
123
+ (``view_of`` ``None``) is therefore present in every per-view query, the way
124
+ :func:`_occupied_boxes` special-cased it; restricting it to ``view=None`` would
125
+ re-open the very blind spot this closes.
126
+
127
+ Boxes are AABBs ``(x0, y0, x1, y1)`` (use with :func:`_box_hits`) — intentionally
128
+ conservative: a diagonal leader's box over-claims its empty triangle (ADR 0009
129
+ notes angled leaders weaken the bound), which only ever over-avoids, never
130
+ under-avoids.
131
+
132
+ The occupancy source for the collect-then-solve carve — every migrated renderer's
133
+ ``place_strip_candidates`` call wires this in (#321/#150/P3)."""
134
+ boxes = []
135
+ for name, o in dwg.iter_annotations():
136
+ if view is not None:
137
+ owner = dwg.view_of(name)
138
+ if owner is not None and owner != view:
139
+ continue # owned by a different ortho view → its own (disjoint) block
140
+ if type(o).__name__ in crossable:
141
+ continue # this consumer may cross it (centre lines/marks for a dim)
142
+ bb = _geom_box(o)
143
+ if bb is not None:
144
+ boxes.append(bb)
145
+ return boxes
146
+
147
+
148
+ def strip_free_span(strip):
149
+ """``(lo, hi, inner)`` page coords of *strip* along its stacking axis, where
150
+ *inner* is the end nearest the view edge (the first tier a dim fills). Reads the
151
+ live ``outer_limit`` so an orchestrator reservation (#133) stays honoured. The
152
+ cursor-free counterpart of :meth:`Strip.allocate` — a collect-then-solve pass
153
+ (ADR 0009) reads these bounds and carves, rather than advancing a mutable cursor."""
154
+ near = strip.anchor + strip.direction * strip.gap
155
+ if strip.direction == 1:
156
+ return near, strip.outer_limit, near # lo, hi, inner (=lo)
157
+ return strip.outer_limit, near, near # lo, hi, inner (=hi)
158
+
159
+
160
+ def carve_free_segments(lo, hi, intervals, pad):
161
+ """``[lo, hi]`` minus every obstacle interval inflated by *pad*, merged and
162
+ complemented — the option-(c) occupancy carve (ADR 0009 / #321). A dim is then
163
+ spaced only WITHIN a clear segment, so it can never overprint a placed occupant
164
+ (a leader shaft, the section hatch, a location-dim tier): the old per-tier
165
+ ``allocate`` + post-hoc ``_box_hits`` retry becomes structural. *intervals* are
166
+ ``(a, b)`` pairs along the strip's stacking axis (e.g. ``(box_y0, box_y1)`` for a
167
+ below strip). Returns a list of ``(seg_lo, seg_hi)`` free segments, lo→hi."""
168
+ blocked = []
169
+ for a0, b0 in intervals:
170
+ a1, b1 = max(lo, a0 - pad), min(hi, b0 + pad)
171
+ if b1 > a1:
172
+ blocked.append((a1, b1))
173
+ blocked.sort()
174
+ merged: list[list[float]] = []
175
+ for a0, b0 in blocked:
176
+ if merged and a0 <= merged[-1][1]:
177
+ merged[-1][1] = max(merged[-1][1], b0)
178
+ else:
179
+ merged.append([a0, b0])
180
+ free, cur = [], lo
181
+ for a0, b0 in merged:
182
+ if a0 > cur:
183
+ free.append((cur, a0))
184
+ cur = b0
185
+ if cur < hi:
186
+ free.append((cur, hi))
187
+ return free
188
+
189
+
190
+ def corridor_blockers(dwg, view):
191
+ """Boxes of annotations a dimension's *witness corridor* (the span from the view
192
+ edge out to its dim line) must not cross — leaders/callouts, the section hatch, the
193
+ title block: everything that is neither a datum-chained ``Dimension`` nor a
194
+ crossable centre line/mark (:data:`CROSSABLE_TYPES`).
195
+
196
+ :func:`strip_obstacles` carves the 1-D strip so a dim *line* clears every occupant,
197
+ but a right/below dim also occupies the 2-D corridor back to the view — and a bore
198
+ callout's leader sitting in that corridor is crossed however far out the line is
199
+ placed (the #133/#225/#305 leader class, in its witness-corridor form). A dim whose
200
+ full footprint hits one of these must route to another view, not overprint it (ISO
201
+ 128). Sibling location/envelope dims are excluded: they chain off the shared datum
202
+ and legitimately share the corridor. View scoping mirrors :func:`strip_obstacles`
203
+ (this view's own annotations + drawing-level occupants that no ortho view owns)."""
204
+ boxes = []
205
+ for name, o in dwg.iter_annotations():
206
+ if view is not None:
207
+ owner = dwg.view_of(name)
208
+ if owner is not None and owner != view:
209
+ continue
210
+ if isinstance(o, (Dimension, SafeDimension)) or type(o).__name__ in CROSSABLE_TYPES:
211
+ continue # datum-chained dims share the corridor; centre lines are crossable
212
+ bb = _geom_box(o)
213
+ if bb is not None:
214
+ boxes.append(bb)
215
+ return boxes
216
+
217
+
41
218
  def _box_hits(bb, boxes):
42
219
  """True when ``bb`` overlaps any box in ``boxes`` (strict AABB test). Slightly
43
220
  more conservative than the within-view label lint (which tolerates a 0.5 mm
@@ -49,3 +226,176 @@ def _box_hits(bb, boxes):
49
226
  if min(bb[2], c[2]) > max(bb[0], c[0]) and min(bb[3], c[3]) > max(bb[1], c[1]):
50
227
  return True
51
228
  return False
229
+
230
+
231
+ def _segment_hits_box(p1, p2, box) -> bool:
232
+ """True when line segment *p1*-*p2* intersects axis-aligned *box*
233
+ ``(x0, y0, x1, y1)`` — the precise counterpart of :func:`_box_hits` for a
234
+ genuinely diagonal shaft (ADR 0009 P4/#318, #305: "a diagonal leader's box
235
+ over-claims its empty triangle"). Boxing an angled segment for a coarse
236
+ reject is correct and cheap; boxing it for the final accept/reject decision
237
+ over-avoids free space a real diagonal never crosses. Endpoint-in-box and
238
+ the 4 edge-crossing cases (a standard segment/AABB test).
239
+
240
+ The crossing test uses strict inequality deliberately, not an inclusive
241
+ ``<= 0`` — an inclusive test also treats a segment merely COLLINEAR with
242
+ one of the box's (infinite) edge lines as a hit, regardless of whether it
243
+ is anywhere near the box along that line (verified: a vertical segment at
244
+ ``x == box.x0`` but far outside ``[y0, y1]`` false-hits under `<=`). That
245
+ false-positive class is common (any axis-aligned shaft sharing an X or Y
246
+ coordinate with an edge), unlike the strict form's own known gap — a
247
+ segment passing exactly through two opposite corners is a measure-zero
248
+ event for the continuous, non-integer leader positions this computes over
249
+ (review finding, #351 P5 strand 3: tried the inclusive form, reverted)."""
250
+ x0, y0, x1, y1 = box
251
+
252
+ def _inside(p):
253
+ return x0 <= p[0] <= x1 and y0 <= p[1] <= y1
254
+
255
+ if _inside(p1) or _inside(p2):
256
+ return True
257
+
258
+ def _cross(o, a, b):
259
+ return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
260
+
261
+ def _seg_seg(a1, a2, b1, b2):
262
+ d1, d2 = _cross(b1, b2, a1), _cross(b1, b2, a2)
263
+ d3, d4 = _cross(a1, a2, b1), _cross(a1, a2, b2)
264
+ return ((d1 > 0 and d2 < 0) or (d1 < 0 and d2 > 0)) and (
265
+ (d3 > 0 and d4 < 0) or (d3 < 0 and d4 > 0)
266
+ )
267
+
268
+ corners = ((x0, y0), (x1, y0), (x1, y1), (x0, y1))
269
+ return any(_seg_seg(p1, p2, corners[i], corners[(i + 1) % 4]) for i in range(4))
270
+
271
+
272
+ def place_strip_candidates(dwg, strip, view, axis, cands, tier, *, force=False):
273
+ """Collect-then-solve placement of location/feature dims on one strip (ADR 0009).
274
+ The single shared strip placer that retires the ``Strip.allocate`` cursor (#150,
275
+ P3): each candidate in *cands* — an ``(name, build(pos)->dim)`` pair — is spaced by
276
+ one :func:`plan_strip` solve per free segment of the CARVED strip (`strip` carved
277
+ around :func:`strip_obstacles`), replacing the per-dim ``allocate`` + ``_box_hits``
278
+ tier-retry. *tier* is the label height (sets the inter-dim gap ``tier + spacing``).
279
+
280
+ Occupancy is THIS view's own placed annotations plus the drawing-level obstacles no
281
+ ortho view owns (the section hatch), recomputed per call so a dim placed earlier in
282
+ the pass is avoided; other ortho views are disjoint (ADR 0004) and excluded so their
283
+ rows never over-carve this strip. This makes the old post-hoc collision retry
284
+ structural: a dim can never land on a bore-callout leader shaft the label-only
285
+ occupancy missed (#133/#225/#305).
286
+
287
+ A right/below dim also occupies the 2-D corridor back to the view edge, which the
288
+ 1-D strip carve cannot represent: a leader in that corridor is crossed no matter how
289
+ far out the dim line lands. By default such a placement is rejected so the caller can
290
+ route the dim to the other view (its disjoint block cannot cross this leader).
291
+ ``force=True`` skips that corridor check — the caller's last resort when no view took
292
+ the dim cleanly: keep it on its natural view and accept the (same-feature) leader
293
+ crossing rather than drop a real dimension (policy B). Candidates that find no strip
294
+ tier AT ALL are still returned (a physically full strip — the caller records the
295
+ genuine drop)."""
296
+ if strip is None or not cands:
297
+ return list(cands)
298
+ lo, hi, inner = strip_free_span(strip)
299
+ # Reserve the outermost label's height at the strip boundary. plan_strip bounds the
300
+ # dim-LINE position, but the label extends `tier` OUTWARD from it — so without this
301
+ # the last tier's label overshoots outer_limit (into the iso view / page margin),
302
+ # unlike the old Strip.allocate which checked `start + tier <= outer_limit` (#338
303
+ # review). The strip edge is not an obstacle (obstacles carry their own footprint +
304
+ # pad), so only the boundary needs it; obstacle-bounded segments are unaffected.
305
+ if inner == lo:
306
+ hi -= tier
307
+ else:
308
+ lo += tier
309
+ idx = 1 if axis == "y" else 0
310
+ perp = 0 if axis == "y" else 1 # the axis the dims do NOT stack along
311
+ pad = tier + strip.spacing # min separation between stacked dim lines
312
+ # Perpendicular band of these candidates. The 1-D carve projects obstacles onto the
313
+ # stacking axis only, so an obstacle on ANOTHER strip of this view — disjoint in the
314
+ # perpendicular axis, never actually touching — would falsely block (e.g. the overall
315
+ # width dim below the view blocking a slot-width dim on the right strip). Filter such
316
+ # obstacles out first. The perpendicular extent is independent of the tier position,
317
+ # so a single probe build per candidate suffices; the corridor check below already
318
+ # uses the full 2-D box, so it needs no such filter.
319
+ pbands = [
320
+ (b[perp], b[perp + 2]) for _n, build in cands if (b := _geom_box(build(lo))) is not None
321
+ ]
322
+ occupied = strip_obstacles(dwg, view=view, crossable=CROSSABLE_TYPES)
323
+ if pbands:
324
+ band_lo, band_hi = min(p[0] for p in pbands), max(p[1] for p in pbands)
325
+ occupied = [b for b in occupied if b[perp] < band_hi and b[perp + 2] > band_lo]
326
+ blockers = () if force else corridor_blockers(dwg, view)
327
+ segs = carve_free_segments(lo, hi, [(b[idx], b[idx + 2]) for b in occupied], pad)
328
+ # Fill innermost-first (nearest the view), matching the old cursor's stack order.
329
+ segs.sort(key=lambda s: abs((s[0] if inner == lo else s[1]) - inner))
330
+ todo = list(cands)
331
+ for seg_lo, seg_hi in segs:
332
+ if not todo:
333
+ break
334
+ cap = int((seg_hi - seg_lo) / pad) + 1
335
+ take, todo = todo[:cap], todo[cap:]
336
+ nat = seg_lo if inner == lo else seg_hi
337
+ anch = (0.0, nat) if axis == "y" else (nat, 0.0)
338
+ # Keys order the tiers so the FIRST candidate lands on the inner tier: for an
339
+ # inner=lo strip that is the lowest position (ascending keys); for a below strip
340
+ # (inner=hi) it is the highest, so the keys reverse.
341
+ triples = [
342
+ (
343
+ StripCandidate(
344
+ f"{(k if inner == lo else len(take) - 1 - k):04d}", anch, (tier, tier)
345
+ ),
346
+ nb,
347
+ )
348
+ for k, nb in enumerate(take)
349
+ ]
350
+ res = plan_strip([sc for sc, _ in triples], seg_lo, seg_hi, pad, axis=axis)
351
+ for sc, (name, build) in triples:
352
+ pos = res.placed.get(sc.key)
353
+ if pos is None: # segment over its estimated capacity (shouldn't occur)
354
+ todo.append((name, build))
355
+ continue
356
+ dim = build(pos)
357
+ if not force and _box_hits(_geom_box(dim), blockers): # corridor crosses a leader
358
+ todo.append((name, build))
359
+ continue
360
+ dwg.add(dim, name, view=view)
361
+ return todo
362
+
363
+
364
+ def carve_free_position(dwg, strip, view, axis, tier, perp_span, *, outermost=False):
365
+ """The single free tier POSITION on *strip* at which a dim of height *tier* spanning
366
+ *perp_span* ``(lo, hi)`` on the perpendicular axis clears every placed obstacle in
367
+ *view* — the innermost (nearest the view) tier by default, or the outermost fitting
368
+ one when *outermost*. Returns the dim-line page coord, or None if the strip is full.
369
+
370
+ The position-returning counterpart of :func:`place_strip_candidates` (which batches,
371
+ builds and adds): a caller that needs a dim's assigned position BEFORE building the
372
+ next — the height-ladder leapfrog chain, where each step dim's witness base is the
373
+ previous dim's line — uses this. Same carve: outer-label tier reservation, the
374
+ perpendicular-band filter (*perp_span* drops obstacles disjoint from this dim's own
375
+ perpendicular extent), and innermost-first fill. No corridor check (a single-strip
376
+ ladder has no alternate view to route to; obstacle tiers are still avoided)."""
377
+ if strip is None:
378
+ return None
379
+ lo, hi, inner = strip_free_span(strip)
380
+ idx = 1 if axis == "y" else 0
381
+ perp = 0 if axis == "y" else 1
382
+ pad = tier + strip.spacing
383
+ band_lo, band_hi = perp_span
384
+ occ = [
385
+ b
386
+ for b in strip_obstacles(dwg, view=view, crossable=CROSSABLE_TYPES)
387
+ if b[perp] < band_hi and b[perp + 2] > band_lo
388
+ ]
389
+ segs = carve_free_segments(lo, hi, [(b[idx], b[idx + 2]) for b in occ], pad)
390
+ # A segment holds the dim iff it is at least `tier` wide (the label height). This IS
391
+ # the outer-label reservation — inclusive at the boundary (a strip exactly `gap+tier`
392
+ # wide fits one dim, as the old `allocate` did) — so it must NOT be combined with a
393
+ # separate `hi -= tier` pull-in, which would double-reserve and drop that dim.
394
+ fitting = [s for s in segs if s[1] - s[0] >= tier - 1e-9]
395
+ if not fitting:
396
+ return None
397
+ if inner == lo: # inner edge = seg lo; outermost = the segment reaching furthest out
398
+ seg = max(fitting, key=lambda s: s[1]) if outermost else min(fitting, key=lambda s: s[0])
399
+ return seg[0]
400
+ seg = min(fitting, key=lambda s: s[0]) if outermost else max(fitting, key=lambda s: s[1])
401
+ return seg[1]