diff --git a/src/draftwright/_geometry.py b/src/draftwright/_geometry.py index e5829041..eacdb430 100644 --- a/src/draftwright/_geometry.py +++ b/src/draftwright/_geometry.py @@ -653,6 +653,11 @@ def _boxes_overlap(a, b) -> bool: return bool(a[0] < b[2] and a[2] > b[0] and a[1] < b[3] and a[3] > b[1]) +def detail_caption_text_gap(font_size: float) -> float: + """Minimum page-space gap between a detail caption and another label.""" + return font_size / 3.0 + + def _segment_crosses_box(p1, p2, box) -> bool: """True when line segment *p1*-*p2* intersects axis-aligned *box* ``(x0, y0, x1, y1)`` — the precise counterpart of ``_box_hits`` for a @@ -1039,6 +1044,62 @@ def material_span(p, q, field: MaterialField) -> float: ) +def label_on_narrow_material(box, field: MaterialField) -> bool: + """Whether text covers material too close to its projected boundary. + + A projected edge can miss a text box by a fraction of a millimetre while + the glyphs still fill a thin shaft. A broad face keeps room around the box. + Probe both centre lines and half the shorter label extent beyond each edge; the + material field is the same one used for leader routing. An edge of a broad + face alone is not a narrow shaft: the local material band across the label's + short axis must also be less than three short-axis extents. + """ + if not field or field.box is None: + return False + x0, y0, x1, y1 = box + width, height = x1 - x0, y1 - y0 + if width <= 0.0 or height <= 0.0: + return False + x = (x0 + x1) / 2.0 + y = (y0 + y1) / 2.0 + if ( + material_span((x0, y), (x1, y), field) < width - MATERIAL_VISIBLE_FLOOR + or material_span((x, y0), (x, y1), field) < height - MATERIAL_VISIBLE_FLOOR + ): + return False + short = min(width, height) + if width <= height: + p, q, centre = (field.box[0], y), (field.box[2], y), x + axis = 0 + else: + p, q, centre = (x, field.box[1]), (x, field.box[3]), y + axis = 1 + span = q[axis] - p[axis] + if span <= 0.0: + return False + centre_tick = round((centre - p[axis]) / span * _MATERIAL_SPAN_TICKS) + local_band = next( + ( + span * (hi - lo) / _MATERIAL_SPAN_TICKS + for lo, hi in material_intervals(p, q, field) + if lo <= centre_tick <= hi + ), + None, + ) + if local_band is None or local_band >= 3.0 * short: + return False + margin = short / 2.0 + return any( + material_span(p, q, field) < margin - MATERIAL_VISIBLE_FLOOR + for p, q in ( + ((x0 - margin, y), (x0, y)), + ((x1, y), (x1 + margin, y)), + ((x, y0 - margin), (x, y0)), + ((x, y1), (x, y1 + margin)), + ) + ) + + def material_intervals( p, q, field: MaterialField, *, bridge: float = 0.0 ) -> tuple[tuple[int, int], ...]: diff --git a/src/draftwright/annotations/_diameters.py b/src/draftwright/annotations/_diameters.py index 04a8154f..76944d8b 100644 --- a/src/draftwright/annotations/_diameters.py +++ b/src/draftwright/annotations/_diameters.py @@ -898,7 +898,9 @@ def _within_page(box): # axis. The far axial margin is deliberately NOT a candidate — a leader run # the whole length of the part to the opposite end reads worse than the # near-miss it replaces; restore-and-flag is the honest fallback instead. - near_a = lo_b - gap if tip[ax] - lo_b <= hi_b - tip[ax] else hi_b + gap + toward_lo = tip[ax] - lo_b <= hi_b - tip[ax] + near_a = lo_b - gap if toward_lo else hi_b + gap + edge_a = lo_b - draft.pad_around_text if toward_lo else hi_b + draft.pad_around_text def _pt(a_val, r_val, _ax=ax): p = [0.0, 0.0] @@ -907,6 +909,7 @@ def _pt(a_val, r_val, _ax=ax): candidates = ( _pt(near_a, tip[rad]), + _pt(edge_a, tip[rad]), _pt(tip[ax], fb[rad] - gap), _pt(tip[ax], fb[rad + 2] + gap), ) diff --git a/src/draftwright/annotations/_gdt.py b/src/draftwright/annotations/_gdt.py index f7835a17..fab1ac43 100644 --- a/src/draftwright/annotations/_gdt.py +++ b/src/draftwright/annotations/_gdt.py @@ -18,7 +18,7 @@ _text_line_spacing_em, _text_size, ) -from draftwright._geometry import _turned_profile_site +from draftwright._geometry import _turned_profile_site, material_span from draftwright.annotations._common import ( PRIORITY, CorridorCandidate, @@ -349,6 +349,25 @@ def _gdt_retry_sides(side: str, *, normal_side_only: bool) -> tuple[str, ...]: }[side] +def _datum_label_has_whitespace(label, strip, horizontal, field) -> bool: + """A local datum glyph must clear projected material or sit beyond the view.""" + x0, y0, x1, y1 = label + if horizontal: + outside = y1 <= strip.anchor if strip.direction < 0 else y0 >= strip.anchor + else: + outside = x1 <= strip.anchor if strip.direction < 0 else x0 >= strip.anchor + if outside: + return True + if not field: + return False + # The shared view-edge guard rejects partial boundary crossings. These + # interior probes reject a glyph sitting wholly on a blank projected face. + return all( + material_span((x0, y), (x1, y), field) <= 1e-6 + for y in (y0 + (y1 - y0) * 0.1, (y0 + y1) / 2.0, y1 - (y1 - y0) * 0.1) + ) + + def _gdt_retry_geometry(state, tier, alt): """Give the deferred carve its strip and glyph extent.""" alt_strip = getattr(state.zones, alt, None) @@ -518,8 +537,57 @@ def _retry(): return _drop +def _gdt_compact_candidates( + original, build, strip, size, horizontal, item, site, tier, material_field +): + """Nearest-first same-strip landings checked later against exact ink. + + Dimension extension lines make their conservative boxes intentionally + broad. A GD&T leader may pass through the empty part of such a box, so + the corridor result is an upper bound rather than necessarily the best + landing. Keep this search finite and inside the requested strip. + """ + if original is None: + return + original_pos = original.elbow[1 if horizontal else 0] + extent = size[1 if horizontal else 0] + if item.kind == "datum_ref" and item.reference_surface_kind: + # A whole-view strip can be far from a small datum face. First try + # bounded positions between the face and that strip, while keeping + # the shaft on the proven surface normal. The shared postsolve + # checks the complete glyph, view edges and settled annotation ink. + distance = (original_pos - site) * strip.direction + first = extent / 2.0 + 1.0 + step = max(tier / 2.0, 1.0) + for index in range(min(12, max(0, int((distance - first) / step) + 1))): + travel = first + index * step + if travel >= distance - 1e-6: + break + pos = site + strip.direction * travel + candidate = build(pos) + # A symbol inside the whole-view box must sit in projected + # whitespace, not on a blank face with no visible edge. The + # shared postsolve also checks visible edges and exact ink. + if _datum_label_has_whitespace( + candidate.label_bbox, strip, horizontal, material_field + ): + yield candidate + near = strip.anchor + strip.direction * (strip.gap + extent / 2.0) + distance = (original_pos - near) * strip.direction + if distance <= 1e-6: + return + step = max(tier + strip.spacing, 1.0) + count = min(64, int(math.ceil(distance / step)) + 1) + for index in range(count): + travel = min(distance, index * step) + pos = near + strip.direction * travel + if abs(pos - original_pos) <= 1e-6: + return + yield build(pos) + + def _gdt_candidate_builders( - item, draft, leader_ctor, fallback_glyph, px, py, horizontal, strip, size, tier + item, draft, leader_ctor, fallback_glyph, px, py, horizontal, strip, size, tier, material_field ): """Build one glyph's primary and fallback leaders plus bounded strip retries.""" @@ -608,36 +676,18 @@ def _build_routed(bends, elbow, _px=px, _py=py, _it=item, _g=fallback_glyph): _attach_gdt_text_evidence(leader, _g, _it, draft) return leader - def _compact_candidates( - original, - _build=_build, - _strip=strip, - _size=size, - _horizontal=horizontal, - ): - """Nearest-first same-strip landings checked later against exact ink. - - Dimension extension lines make their conservative boxes intentionally - broad. A GD&T leader may pass through the empty part of such a box, so - the corridor result is an upper bound rather than necessarily the best - landing. Keep this search finite and inside the requested strip. - """ - if original is None: - return - original_pos = original.elbow[1 if _horizontal else 0] - extent = _size[1 if _horizontal else 0] - near = _strip.anchor + _strip.direction * (_strip.gap + extent / 2.0) - distance = (original_pos - near) * _strip.direction - if distance <= 1e-6: - return - step = max(tier + _strip.spacing, 1.0) - count = min(64, int(math.ceil(distance / step)) + 1) - for index in range(count): - travel = min(distance, index * step) - pos = near + _strip.direction * travel - if abs(pos - original_pos) <= 1e-6: - return - yield _build(pos) + def _compact_candidates(original): + return _gdt_compact_candidates( + original, + _build, + strip, + size, + horizontal, + item, + py if horizontal else px, + tier, + material_field, + ) def _ink_repair_candidates( original, @@ -772,6 +822,12 @@ def render_gdt( ) continue size = (gb.X, gb.Y) + placed_view = dwg.views.get(item.view) + field = ( + dwg.material_fields().get(id(placed_view[0])) + if item.kind == "datum_ref" and item.reference_surface_kind and placed_view + else None + ) ( _build, @@ -780,7 +836,17 @@ def render_gdt( _compact_candidates, _ink_repair_candidates, ) = _gdt_candidate_builders( - item, draft, leader_ctor, fallback_glyph, px, py, horizontal, strip, size, tier + item, + draft, + leader_ctor, + fallback_glyph, + px, + py, + horizontal, + strip, + size, + tier, + field, ) _drop = _gdt_drop_callback( diff --git a/src/draftwright/annotations/_height_ladder.py b/src/draftwright/annotations/_height_ladder.py index 9a029297..54fb48c9 100644 --- a/src/draftwright/annotations/_height_ladder.py +++ b/src/draftwright/annotations/_height_ladder.py @@ -128,8 +128,19 @@ def register_height_ladder_candidates( _tolerances = {c[0]: c[8] for c in chain} names = [c[0] for c in chain] + side_right_space = ( + frame.zones("side").right.outer_limit - frame.zones("side").right.anchor + if "side" in dwg.views + else float("inf") + ) + overall_side = ( + "left" + if side_right_space < frame.zones(view).right.outer_limit - frame.zones(view).right.anchor + and frame.zones(view).left.anchor - frame.zones(view).left.outer_limit >= tier + else "right" + ) sides = { - name: (overall.rungs[0].side or "right") + name: (overall.rungs[0].side or overall_side) if name == "dim_height" and overall is not None else "right" for name in names diff --git a/src/draftwright/annotations/_leader_fixed_ink.py b/src/draftwright/annotations/_leader_fixed_ink.py index 1cc755a1..b294897b 100644 --- a/src/draftwright/annotations/_leader_fixed_ink.py +++ b/src/draftwright/annotations/_leader_fixed_ink.py @@ -357,9 +357,8 @@ def _fixed_ink_hull(polygon): return hull if len(hull) >= 3 else None -def _annotation_fixed_ink(dwg, name, annotation, *, max_components=None): - """Exact-width fixed ink components for one already-rendered annotation.""" - +def _reserved_fixed_ink(dwg, name, annotation, max_components): + """Return a complete block reservation when the annotation owns one.""" if isinstance(annotation, DerivedViewReservation): # A future required view owns its entire planned rectangle, not merely # the strokes it will eventually draw. Keep it hard even in the bounded @@ -371,6 +370,27 @@ def _annotation_fixed_ink(dwg, name, annotation, *, max_components=None): ), ) + if getattr(annotation, "table_rows", None) is not None: + # A table reserves its cells as a block. Individual glyph/line faces leave + # apparently free pockets where another annotation can cross a cell. + if max_components is not None and max_components < 1: + return _FIXED_INVENTORY_EXHAUSTED + box = _coerce_box(_geom_box(annotation, getattr(dwg, "box_cache", None))) + if box is None: + if max_components is not None: + return _FIXED_INVENTORY_EXHAUSTED + box = (0.0, 0.0, float(dwg.page_w), float(dwg.page_h)) + return (_FixedInkComponent(f"{name}:table", box=box, kind="Table"),) + return None + + +def _annotation_fixed_ink(dwg, name, annotation, *, max_components=None): + """Exact-width fixed ink components for one already-rendered annotation.""" + + reserved = _reserved_fixed_ink(dwg, name, annotation, max_components) + if reserved is not None: + return reserved + components: list[_FixedInkComponent] = [] owner = dwg.registry.feature_of(name) kind = type(annotation).__name__ diff --git a/src/draftwright/annotations/_machined_leaders.py b/src/draftwright/annotations/_machined_leaders.py index e7cf74dd..99ad92fb 100644 --- a/src/draftwright/annotations/_machined_leaders.py +++ b/src/draftwright/annotations/_machined_leaders.py @@ -14,6 +14,7 @@ from build123d_drafting.helpers import DEFAULT_FONT_PATH +from draftwright.annotations._placement_occupancy import label_clears_foreign_annotations from draftwright.annotations.leaders import ( FeatureLeaderCandidate, FeatureLeaderJob, @@ -39,7 +40,6 @@ class MachinedLeaderBindings: collect_feature_leader: Callable place_feature_leader_jobs: Callable[..., int] attribute_annotations: Callable - boxes_overlap: Callable @dataclass @@ -75,14 +75,8 @@ def foreign_label_clear( for _name, owner, box, has_label in self.bindings.attribute_annotations(self.dwg) if owner != view ) - pad = self.dwg.draft.pad_around_text - padded = (label[0] - pad, label[1] - pad, label[2] + pad, label[3] + pad) - return not any( - self.bindings.boxes_overlap( - padded, - (box[0] - pad, box[1] - pad, box[2] + pad, box[3] + pad) if has_label else box, - ) - for box, has_label in self.foreign_boxes_by_view[view] + return label_clears_foreign_annotations( + label, self.foreign_boxes_by_view[view], self.dwg.draft.pad_around_text ) def lower(self, row) -> FeatureLeaderJob: diff --git a/src/draftwright/annotations/_placement_occupancy.py b/src/draftwright/annotations/_placement_occupancy.py index 58339f79..51da529b 100644 --- a/src/draftwright/annotations/_placement_occupancy.py +++ b/src/draftwright/annotations/_placement_occupancy.py @@ -6,6 +6,7 @@ from __future__ import annotations import math +from collections.abc import Iterable from itertools import chain from build123d_drafting.helpers import Dimension, Leader, Note, SafeDimension @@ -713,3 +714,21 @@ def view_label_clearance(dwg, view): return None entries = _view_edge_entries(placed[0], {}) return lambda box: entries is not None and not _edges_intersect_rect(entries, box) + + +def label_clears_foreign_annotations( + box: tuple[float, float, float, float], + foreign_boxes: Iterable[tuple[tuple[float, float, float, float], bool]], + pad: float, +) -> bool: + """Keep a candidate label clear of another view's settled text and ink.""" + protected = (box[0] - pad, box[1] - pad, box[2] + pad, box[3] + pad) + return not any( + _boxes_overlap( + protected, + (other[0] - pad, other[1] - pad, other[2] + pad, other[3] + pad) + if has_label + else other, + ) + for other, has_label in foreign_boxes + ) diff --git a/src/draftwright/annotations/from_model.py b/src/draftwright/annotations/from_model.py index 5c859b0c..db644e15 100644 --- a/src/draftwright/annotations/from_model.py +++ b/src/draftwright/annotations/from_model.py @@ -49,7 +49,6 @@ layout_frame, ) from draftwright._geometry import ( - _boxes_overlap, _segment_clips_box, ) from draftwright.annotations._axial_render import ( @@ -708,7 +707,6 @@ def place_machined_leader_jobs( collect_feature_leader=collect_feature_leader, place_feature_leader_jobs=place_feature_leader_jobs, attribute_annotations=_attribute_annotations, - boxes_overlap=_boxes_overlap, ), ) diff --git a/src/draftwright/annotations/holes.py b/src/draftwright/annotations/holes.py index 1249bf34..f7d8d4e8 100644 --- a/src/draftwright/annotations/holes.py +++ b/src/draftwright/annotations/holes.py @@ -73,6 +73,7 @@ from draftwright.annotations._patterns import ( _pitch_text, ) +from draftwright.annotations._placement_occupancy import label_clears_foreign_annotations from draftwright.annotations.from_model import ( _diameter_column_left, _diameter_row_below, @@ -99,6 +100,7 @@ collect_feature_leader, feature_leader_candidates, ) +from draftwright.compose import _attribute_annotations from draftwright.layout import StripCandidate, plan_strip from draftwright.leader_policy import effective_leader_region_policy from draftwright.model import plan_dimensions @@ -1662,29 +1664,29 @@ def _collect_shared_queue( elbow_dx, ): """Register compatible hole leaders and staged furniture in the late solve.""" - edge = sctx.edge - min_gap = sctx.min_gap - y_min = sctx.y_min - y_max = sctx.y_max a = sctx.a to_page = sctx.to_page draft = sctx.draft projected_clear = view_label_clearance(dwg, view) + foreign_boxes = None + + def foreign_label_clear(box): + nonlocal foreign_boxes + if foreign_boxes is None: + foreign_boxes = tuple( + (other, has_label) + for _name, owner, other, has_label in _attribute_annotations(dwg) + if owner != view + ) + return label_clears_foreign_annotations(box, foreign_boxes, draft.pad_around_text) + i = start_i for s in queue: locations, dia, callout, feat, _natural_y, _rep = s owner = _callout_member_owner(callout, _rep, feat_of_callout.get(id(callout))) requested_side = side_of_callout.get(id(callout)) - # Hole callouts are one explicitly interior-capable semantic family. - # The shared adapter still proves each candidate clear and retains the - # established exterior inventory, so this is eligibility rather than a - # family-specific placement rule. In particular, recognised repeated - # holes commonly remain a HoleFeature with several members rather than a - # PatternFeature; class-testing here would silently exclude those patterns. - # - # An authored side is different from automatic family eligibility: it is - # a placement constraint. Keep that job in the exterior inventory so an - # interior candidate cannot silently defeat ``side="left"``/``"right"``. + # Include repeated HoleFeatures in the interior-capable family; the shared + # adapter still proves clearance. An authored side fixes an exterior edge. family_region_policy = ( LeaderRegionPolicy.AUTO if requested_side is None else LeaderRegionPolicy.EXTERIOR ) @@ -1693,7 +1695,7 @@ def _collect_shared_queue( getattr(a, "leader_region", "auto"), ) callout_box = _geom_box(callout, cache) - winner_y, rows = strip_plan.rows_for(s, y_min, y_max, obstacle_intervals) + winner_y, rows = strip_plan.rows_for(s, sctx.y_min, sctx.y_max, obstacle_intervals) adapter = HoleLeaderCandidateAdapter( entry=s, locations=tuple(locations or ()), @@ -1705,12 +1707,12 @@ def _collect_shared_queue( callout_box=callout_box, projected_clear=projected_clear, column_bands=leader_column_bands, - edge=edge, + edge=sctx.edge, side=side, view_bounds=vb, - y_min=y_min, - y_max=y_max, - min_gap=min_gap, + y_min=sctx.y_min, + y_max=sctx.y_max, + min_gap=sctx.min_gap, to_page=to_page, elbow_dx=elbow_dx, draft=draft, @@ -1722,15 +1724,10 @@ def _collect_shared_queue( expand_regions=feature_leader_candidates, build_leader=_profiled_callout_leader, ) - _raw_candidates = adapter.raw - name = _hc_name(only, view, i, hc_used) - # Pitch/BCD furniture is a separate non-leader requirement. Keep it - # in its established early stage so the corridor solve sees it and - # the late shared leader inventory routes around it. Coverage still - # waits for the callout winner below: visible furniture alone must - # not claim that the bore callout was placed. + # Stage pitch/BCD furniture before the corridor solve, but defer + # coverage until the callout wins; furniture alone proves no bore callout. staged_furniture = () staged_issues = () staged_furnished = False @@ -1771,7 +1768,7 @@ def _on_place( ) callbacks = _HoleLeaderCallbacks( - _raw_candidates, + adapter.raw, adapter.build, callout, callout_box, @@ -1794,7 +1791,7 @@ def _on_place( view=view, silhouette=vb, label=str(callout.label), - candidates=_raw_candidates(), + candidates=adapter.raw(), build=adapter.build, measurement=tuple(callout.measurements), noun="hole", @@ -1812,6 +1809,7 @@ def _on_place( interior_label_clear=( projected_clear if region_policy is not LeaderRegionPolicy.EXTERIOR else None ), + foreign_label_clear=foreign_label_clear if side == "left" else None, allow_policy_b_fixed=True, # A shaft-to-shaft crossing may remain a Policy-B fallback, # but no compatibility floor may put a pitch witness through @@ -2207,14 +2205,13 @@ def _place_planside_callouts( plan_left, side_right, ): - """Plan/side-view two-pass leader callout placement (#638): Pass 1 assigns each spec to the - nearer strip edge that fits the page; Pass 2 (``_place_queue``) solves Y placement + - over-capacity drop per edge, right then left with index continuity (``next_i``).""" + """Plan/side-view two-pass leader callout placement (#638): Pass 1 takes the nearer + fitting plan edge or the right side edge when it fits; Pass 2 (``_place_queue``) + solves Y placement and over-capacity drop per edge, right then left with index + continuity (``next_i``).""" edge_right = plan_right if view == "plan" else side_right edge_left = plan_left if view == "plan" else None - if view == "side" and any( - side_of_callout.get(id(callout)) == "left" for _, _, callout, _ in specs - ): + if view == "side": edge_left = a.proj.side_x(a.bb.min.Y) right_strip = a.pv_zones.right if view == "plan" else a.sv_zones.right @@ -2319,7 +2316,7 @@ def _place_planside_callouts( continue # Natural Y is the bore's own row; keep-out-band avoidance is `_place_queue`'s carve. - if can_right and (not can_left or d_right <= d_left): + if can_right and (view == "side" or not can_left or d_right <= d_left): right_queue.append((locs, dia, callout, feat, centre_r[1], rep_r)) else: left_queue.append((locs, dia, callout, feat, centre_l[1], rep_l)) diff --git a/src/draftwright/annotations/sections.py b/src/draftwright/annotations/sections.py index 4200f684..867a3b16 100644 --- a/src/draftwright/annotations/sections.py +++ b/src/draftwright/annotations/sections.py @@ -52,7 +52,9 @@ _boxes_overlap, _leader_ink_polygons, _scale_world, + _segment_clips_box, _stroke_polygon, + detail_caption_text_gap, ) from draftwright.annotations._common import ( _clear_derived_view_reservation, @@ -127,6 +129,57 @@ def _detail_ink_within_reservation(reserved_box, shapes, *, tolerance=0.05): ) +def _detail_caption_clear_of_text(dwg, text, at, region): + """Find the nearest clear caption position inside its planned reservation.""" + caption = Note(text, at, dwg.draft) + box = _anno_box(caption) + gap = detail_caption_text_gap(dwg.draft.font_size) + settled = [] + strokes: list[tuple[tuple[float, float], tuple[float, float]]] = [] + for _name, annotation in dwg.iter_annotations(): + strokes.extend(getattr(annotation, "segments", None) or ()) + if not ( + getattr(annotation, "label", None) or getattr(annotation, "_annotate_label", None) + ): + continue + label_box = getattr(annotation, "label_bbox", None) + if label_box is not None: + settled.append(label_box) + + def clear(candidate_box): + protected = ( + candidate_box[0] - gap, + candidate_box[1] - gap, + candidate_box[2] + gap, + candidate_box[3] + gap, + ) + return not any(_boxes_overlap(protected, other) for other in settled) and not any( + _segment_clips_box(start, end, protected) for start, end in strokes + ) + + if clear(box): + return caption + # Move down only as far as needed, retaining the caption near its detail. + # The original box translates without changing size; construct ink only + # for a candidate that clears the settled labels and strokes. + travel = box[1] - region[1] + step = max(gap, 0.5) + for index in range(1, min(64, math.ceil(travel / step)) + 1): + down = min(index * step, travel) + moved = (box[0], box[1] - down, box[2], box[3] - down) + if ( + moved[0] >= region[0] - 1e-6 + and moved[1] >= region[1] - 1e-6 + and moved[2] <= region[2] + 1e-6 + and moved[3] <= region[3] + 1e-6 + and clear(moved) + ): + lowered = Note(text, (at[0], at[1] - down), dwg.draft) + if clear(_anno_box(lowered)): + return lowered + return caption + + def feature_hole_keys(model, a: Analysis) -> set[HoleRef]: """The :class:`HoleRef` position keys of the part's feature holes — the membership set the callout/furniture passes and ``plan_sections`` gate on (#420). @@ -1096,10 +1149,11 @@ def _fits(s): _log.info("Detail %s skipped (no legible dims at the detail scale)", letter) return False dvb = placed.bounding_box() - caption = Note( + caption = _detail_caption_clear_of_text( + dwg, _caption_text(detail_scale), ((dvb.min.X + dvb.max.X) / 2, dvb.min.Y - cap_h), - dwg.draft, + reserved_box if reserved_box is not None else (rx0, ry0, rx1, ry1), ) if reserved_box is not None: assert annotation_snapshot is not None @@ -1116,7 +1170,6 @@ def _fits(s): dwg.views.pop(view_name, None) return False dwg._set_view_coordinates(view_name, coords) - _place_detail_marker(dwg, a, req, letter, ctx=ctx) # Caption below the placed view (anchored to its real footprint). diff --git a/src/draftwright/builder.py b/src/draftwright/builder.py index 347800b3..fea7cb8d 100644 --- a/src/draftwright/builder.py +++ b/src/draftwright/builder.py @@ -398,7 +398,11 @@ def _annotation_view_overlaps(dwg, a) -> int: only a text label landing on another view's geometry does. """ geom = _view_geom(a) - boxes = {v: (cx - hw, cy - hh, cx + hw, cy + hh) for v, (cx, cy, hw, hh) in geom.items()} + boxes = { + v: (cx - hw, cy - hh, cx + hw, cy + hh) + for v, (cx, cy, hw, hh) in geom.items() + if v in dwg.views + } clearance = _annotation_clearance(dwg) n = 0 for _name, v, bb, label in _attribute_annotations(dwg): @@ -1076,6 +1080,26 @@ def _needs_repack(dwg, a) -> bool: _cross_view_overlaps(dwg) != 0 or _annotation_view_overlaps(dwg, a) != 0 or _annotations_out_of_bounds(dwg, a) + or _source_placement_drop_with_table(dwg) + ) + + +def _source_placement_drop_with_table(dwg) -> bool: + """A table may take required annotation space after the seed layout. + + Repack the measured blocks when that happens. An unselected view's geometry + cannot act as the accidental trigger: it has no ink on the finished sheet. + """ + if not any( + getattr(issue, "outcome_stage", None) == "placement" + and getattr(issue, "source_ids", ()) + and _is_required_scale_drop(issue) + for issue in getattr(getattr(dwg, "registry", None), "issues", ()) + ): + return False + return any( + getattr(annotation, "table_rows", None) is not None + for _name, annotation in dwg.iter_annotations() ) diff --git a/src/draftwright/layout_safety.py b/src/draftwright/layout_safety.py index f5a40099..6c9a2446 100644 --- a/src/draftwright/layout_safety.py +++ b/src/draftwright/layout_safety.py @@ -15,9 +15,11 @@ "annotation_out_of_bounds", "annotation_overlap", "annotation_ink_overlap", + "detail_caption_clearance", "dim_inside_part", "feature_leader_crossing", "feature_leader_fixed_ink_unverified", + "interior_label_on_narrow_material", "label_centerline_overlap", "layout_repack_stalled", "legibility_floor_breached", diff --git a/src/draftwright/linting/orchestration.py b/src/draftwright/linting/orchestration.py index 80ba282d..c9873b64 100644 --- a/src/draftwright/linting/orchestration.py +++ b/src/draftwright/linting/orchestration.py @@ -152,6 +152,7 @@ def _lint_structure(ctx: LintContext, aggregation: Any, display_decimals: Any) - ctx.items, page_bbox=ctx.page_bbox, drawing_scale=ctx.scale, + caption_font_size=ctx.drawing.draft.font_size, view_shapes=view_shapes, view_names=view_names, view_edge_cache=ctx.view_edge_cache, @@ -165,6 +166,12 @@ def _lint_structure(ctx: LintContext, aggregation: Any, display_decimals: Any) - for name, obj in ctx.registry.iter_named() if (view := ctx.registry.view_of(name)) is not None }, + annotation_datums={ + id(obj) + for name, obj in ctx.registry.iter_named() + if getattr(ctx.registry.declaration_of(name), "kind", None) == "datum_ref" + or getattr(ctx.registry.feature_of(name), "pmi_kind", None) == "datum" + }, annotation_regions={ id(obj): ctx.registry.candidate_region_of(name) for name, obj in ctx.registry.iter_named() diff --git a/src/draftwright/linting/quality.py b/src/draftwright/linting/quality.py index a951ab4a..f368b9f5 100644 --- a/src/draftwright/linting/quality.py +++ b/src/draftwright/linting/quality.py @@ -62,11 +62,14 @@ "title_field_overflow", "annotation_overlap", "annotation_ink_overlap", + "datum_leader_remote", + "detail_caption_clearance", "derived_view_identifier_reused", "detail_unplaceable", "dim_inside_part", "feature_leader_crossing", "feature_leader_fixed_ink_unverified", + "interior_label_on_narrow_material", "label_centerline_overlap", "leader_crosses_silhouette", "leader_shaft_unverified", @@ -574,6 +577,7 @@ def _is_legibility_issue(issue) -> bool: "annotation_ink_overlap", "annotation_out_of_bounds", "annotation_overlap", + "detail_caption_clearance", "feature_leader_fixed_ink_unverified", "label_centerline_overlap", "leader_line_through_text", diff --git a/src/draftwright/linting/structural.py b/src/draftwright/linting/structural.py index a4039fdd..7d5ed08b 100644 --- a/src/draftwright/linting/structural.py +++ b/src/draftwright/linting/structural.py @@ -23,6 +23,8 @@ _boxes_overlap, _segment_clip_extent, _segment_clips_box, + detail_caption_text_gap, + label_on_narrow_material, material_reentry_span, ) from draftwright.linting.angular import is_angular_label as _is_angular_label @@ -340,6 +342,8 @@ def lint_drawing( annotation_specs: dict[int, DimensionPlacementSpec | RegisteredDimensionSpec | None] | None = None, annotation_views: dict[int, str] | None = None, + annotation_datums: set[int] | None = None, + caption_font_size: float = 3.0, ) -> list[LintIssue]: """Structural checks on a composed annotation list, duck-typed. @@ -475,8 +479,12 @@ def lint_drawing( warned_label_bbox, _aggregation, pair_tokens, + caption_font_size, ) _lint_annotation_bounds(items, page_bbox, issues, box_cache, names) + _lint_datum_leader_locality( + items, issues, annotation_datums or set(), names, annotation_views or {} + ) if view_shapes is not None: _lint_view_shapes( @@ -512,6 +520,35 @@ def lint_drawing( return issues +def _lint_datum_leader_locality(items, issues, datum_ids, names, views) -> None: + """Judge a placed datum's normal shaft from its own visible glyph size.""" + for item in items: + if id(item) not in datum_ids: + continue + tip, elbow = getattr(item, "tip", None), getattr(item, "elbow", None) + if tip is None or elbow is None: + continue + try: + x0, y0, x1, y1 = item.label_bbox + length = ((tip[0] - elbow[0]) ** 2 + (tip[1] - elbow[1]) ** 2) ** 0.5 + except (AttributeError, IndexError, TypeError, ValueError): + continue + # Page-space distance is independent of drawing scale. A shaft longer + # than several glyphs makes the surface relationship hard to read. + limit = max(24.0, 2.5 * max(x1 - x0, y1 - y0)) + if length > limit: + issues.append( + LintIssue( + "warning", + f"datum leader {names.get(id(item), '?')} is {length:.1f} mm from its surface", + location=(elbow[0], elbow[1]), + code="datum_leader_remote", + annotation_name=names.get(id(item)), + view=views.get(id(item)), + ) + ) + + def _lint_annotation_items( items, part_bbox, @@ -550,6 +587,60 @@ def _lint_annotation_items( ) +def _lint_detail_caption_pair( + item_a, + item_b, + i, + j, + la_box, + lb_box, + ox, + oy, + names, + label_boxes, + caption_font_size, + issues, + aggregation, + pair_tokens, +) -> None: + """Require readable clearance around a detail caption's settled text.""" + name_a, name_b = names.get(id(item_a)), names.get(id(item_b)) + caption, other, caption_box, other_box = ( + (name_a, name_b, la_box, lb_box) + if name_a is not None and name_a.startswith("detail_caption_") + else (name_b, name_a, lb_box, la_box) + ) + other_item = item_b if caption == name_a else item_a + other_index = j if caption == name_a else i + if ( + caption is not None + and caption.startswith("detail_caption_") + and _item_label(other_item) + and label_boxes[other_index] is not None + and not (ox > 0.5 and oy > 0.5) + ): + x_gap = max(caption_box[0] - other_box[2], other_box[0] - caption_box[2], 0.0) + y_gap = max(caption_box[1] - other_box[3], other_box[1] - caption_box[3], 0.0) + minimum = detail_caption_text_gap(caption_font_size) + if x_gap < minimum and y_gap < minimum: + issue = LintIssue( + severity="warning", + code="detail_caption_clearance", + message=( + f"detail caption '{caption}' and annotation '{other or '?'}' " + f"have only {max(x_gap, y_gap):.2f} mm text clearance; " + f"keep at least {minimum:.2f} mm" + ), + annotation_name=caption, + related_annotation_names=(other,) if other is not None else (), + ) + issues.append(issue) + if aggregation is not None: + caption_token = pair_tokens.get(id(item_a if caption == name_a else item_b)) + if caption_token is not None: + aggregation.record_pair(issue, caption_token) + + def _lint_annotation_pairs( items, issues, @@ -558,6 +649,7 @@ def _lint_annotation_pairs( warned_label_bbox, _aggregation, pair_tokens, + caption_font_size, ) -> None: """Measure label and line ink once, then inspect pairs in input order.""" @@ -670,6 +762,22 @@ def _label_box(item, index): continue ox = max(0.0, min(la_box[2], lb_box[2]) - max(la_box[0], lb_box[0])) oy = max(0.0, min(la_box[3], lb_box[3]) - max(la_box[1], lb_box[1])) + _lint_detail_caption_pair( + item_a, + item_b, + i, + j, + la_box, + lb_box, + ox, + oy, + names, + label_boxes, + caption_font_size, + issues, + _aggregation, + pair_tokens, + ) if ox > 0.5 and oy > 0.5: la = getattr(item_a, "label", "?") lb = getattr(item_b, "label", "?") @@ -718,27 +826,10 @@ def _label_box(item, index): related_annotation_names=named_pair[1:], ) issues.append(overlap_issue) - # `annotation_overlap` deliberately does NOT enter #1147's ledger, - # and this branch must not put it there. Two revisions tried and - # both changed the score of a code that never participated, - # because `_primary_issues` keys on `(code, token)`: recording - # every overlap collapsed pairs that merely shared a subject - # (2 raw -> 1 primary, and which survived depended on `items` - # order), and restricting it to pairs that ALSO cross collapsed - # pairs that shared a *crossed* label (3 raw -> 2 primary against - # main's 3), so adding ink crossings to a sheet RAISED its - # legibility score. - # - # Collapsing by the crossed label is right for - # `annotation_ink_overlap`, where one unreadable label is one - # defect. It is wrong for `annotation_overlap`, which is about two - # labels colliding with each other and has no single subject. - # - # The cost is real and stays: a pair both overlapped and crossed - # reports only the overlap, so the reader loses which label is - # obscured, by what, and how far. #1333 owns that, and the fix is - # to carry the detail in the surviving message rather than to key - # a second code into the ledger. + # Do not ledger annotation_overlap: the defect belongs to the + # pair, while a ledger token names one obscured label. Collapsing + # pairs that share a token changes the legibility score. Carry + # line-crossing detail in the surviving message instead (#1333). continue # The label boxes clear each other, which does not mean the @@ -1125,47 +1216,20 @@ def _lint_title_fields(item, issues) -> None: ) -def _lint_view_shapes( - view_shapes, +def _lint_view_annotation_overlap( + named_views, + view_shape_ids, ann_items, issues, *, - view_names=None, - page_bbox=None, - edge_cache=None, - box_cache=None, - warned=None, - material_fields=None, - annotation_names=None, - annotation_regions=None, - annotation_views=None, + annotation_names, + annotation_regions, + edge_cache, + box_cache, + warned, + material_fields, ) -> None: - """Check views against annotations (#159/#76), each other (#160), and the page (#75).""" - # Build the named bbox list. The name must be DETERMINISTIC: several messages - # below identify a view by it, and `id()` is a fresh memory address on every - # render, so the same sheet linted twice produced different text and any consumer - # diffing two renders saw a change that did not exist in the drawing (#1196). - # Prefer the caller's own name for the view; fall back to position, never identity. - named_views = [] - view_shape_ids = set() - for index, vs in enumerate(view_shapes): - bb = _ann_box(vs, box_cache if box_cache is not None else {}) - if bb is None: - continue - supplied = ( - view_names[index] if view_names is not None and index < len(view_names) else None - ) - # *supplied* first: the docstring argues the caller is the authority on what a - # view is called, so letting a shape attribute outrank it would contradict that. - # Latent today — every projected view carries `label == ""` — but DXF layer - # naming is an obvious future reason to label a view compound, and it would - # then silently override the drawing's own key. - name = ( - supplied or getattr(vs, "label", None) or getattr(vs, "name", None) or f"view[{index}]" - ) - named_views.append((name, bb, vs)) - view_shape_ids.add(id(vs)) - + """Check annotation text against each projected view's settled edges.""" # #159 — view shape vs annotation overlaps. Line-work (witness lines, # leader shafts, centrelines) legitimately enters the view, so test the # label-text bbox where the annotation exposes one and skip centrelines @@ -1175,8 +1239,6 @@ def _lint_view_shapes( # mostly blank face, where placing callouts is a legitimate convention — # so a label over a blank region is reported as an info-level notice. names = {} if annotation_names is None else annotation_names - owners = {} if annotation_views is None else annotation_views - unreadable_shafts: set[int] = set() regions = {} if annotation_regions is None else annotation_regions cache = {} if edge_cache is None else edge_cache ann_cache = box_cache if box_cache is not None else {} @@ -1204,7 +1266,29 @@ def _lint_view_shapes( albl = getattr(ann, "label", None) or getattr(ann, "name", None) or type(ann).__name__ what = "label of annotation" if label_box is not None else "annotation" edges = _view_edge_entries(vs, cache) - if edges is None or _edges_intersect_rect(edges, ab): + edge_overlap = edges is None or _edges_intersect_rect(edges, ab) + narrow_material = ( + not edge_overlap + and label_box is not None + and vx0 <= label_box[0] <= label_box[2] <= vx1 + and vy0 <= label_box[1] <= label_box[3] <= vy1 + and (field := (material_fields or {}).get(id(vs))) is not None + and label_on_narrow_material(label_box, field) + ) + if narrow_material: + issues.append( + LintIssue( + severity="warning", + code="interior_label_on_narrow_material", + message=( + f"interior label '{albl}' in view '{vname}' lies too close " + "to the projected material boundary" + ), + annotation_name=names.get(id(ann)), + view=vname, + ) + ) + if edge_overlap: issues.append( LintIssue( severity="warning", @@ -1221,7 +1305,7 @@ def _lint_view_shapes( # annotation ink. Do not turn that deliberate result into the generic # advisory emitted for unclassified annotations inside a view. The # warning path above remains active if projected edges do intersect it. - if regions.get(id(ann)) == "interior": + if regions.get(id(ann)) == "interior" or narrow_material: continue issues.append( LintIssue( @@ -1235,6 +1319,64 @@ def _lint_view_shapes( ) ) + +def _lint_view_shapes( + view_shapes, + ann_items, + issues, + *, + view_names=None, + page_bbox=None, + edge_cache=None, + box_cache=None, + warned=None, + material_fields=None, + annotation_names=None, + annotation_regions=None, + annotation_views=None, +) -> None: + """Check views against annotations (#159/#76), each other (#160), and the page (#75).""" + # Build the named bbox list. The name must be DETERMINISTIC: several messages + # below identify a view by it, and `id()` is a fresh memory address on every + # render, so the same sheet linted twice produced different text and any consumer + # diffing two renders saw a change that did not exist in the drawing (#1196). + # Prefer the caller's own name for the view; fall back to position, never identity. + named_views = [] + view_shape_ids = set() + for index, vs in enumerate(view_shapes): + bb = _ann_box(vs, box_cache if box_cache is not None else {}) + if bb is None: + continue + supplied = ( + view_names[index] if view_names is not None and index < len(view_names) else None + ) + # *supplied* first: the docstring argues the caller is the authority on what a + # view is called, so letting a shape attribute outrank it would contradict that. + # Latent today — every projected view carries `label == ""` — but DXF layer + # naming is an obvious future reason to label a view compound, and it would + # then silently override the drawing's own key. + name = ( + supplied or getattr(vs, "label", None) or getattr(vs, "name", None) or f"view[{index}]" + ) + named_views.append((name, bb, vs)) + view_shape_ids.add(id(vs)) + + _lint_view_annotation_overlap( + named_views, + view_shape_ids, + ann_items, + issues, + annotation_names=annotation_names, + annotation_regions=annotation_regions, + edge_cache=edge_cache, + box_cache=box_cache, + warned=warned, + material_fields=material_fields, + ) + + owners = {} if annotation_views is None else annotation_views + names = {} if annotation_names is None else annotation_names + unreadable_shafts: set[int] = set() # #796/#798 — leader shaft cuts back through the part body. Measured against the # build's FILLED projected material (`Drawing.material_fields`), the same lowering # ADR 2 (was 0014) leader routing solves against, so the notice and the router cannot reach diff --git a/src/draftwright/reporting.py b/src/draftwright/reporting.py index e66be21d..f2b37faa 100644 --- a/src/draftwright/reporting.py +++ b/src/draftwright/reporting.py @@ -44,6 +44,7 @@ "label_centerline_overlap": ("view", "side", "priority", "pin"), "leader_line_through_text": ("view", "side", "priority", "pin"), "annotation_out_of_bounds": ("page", "scale", "view", "side"), + "interior_label_on_narrow_material": ("page", "scale", "view", "side"), "view_annotation_overlap": ("page", "scale", "view", "side"), "view_overlap": ("page", "scale", "view"), "view_out_of_bounds": ("page", "scale", "view"), diff --git a/tests/_complexity_baseline.json b/tests/_complexity_baseline.json index 6fe804a1..d4faead2 100644 --- a/tests/_complexity_baseline.json +++ b/tests/_complexity_baseline.json @@ -80,7 +80,7 @@ "linting/oriented_slot_coverage.py:oriented_slot_requirement_outcomes": 16, "linting/profiled_bore_coverage.py:lint_profiled_bore_coverage": 23, "linting/structural.py:_lint_annotation_pairs": 16, - "linting/structural.py:_lint_view_shapes": 30, + "linting/structural.py:_lint_view_shapes": 19, "linting/suggest.py:_suggest_fix": 19, "linting/through_step_coverage.py:_index_evidence": 17, "linting/through_step_coverage.py:_legacy_owner_plans": 19, diff --git a/tests/_function_length_baseline.json b/tests/_function_length_baseline.json index 78a11f83..27a64227 100644 --- a/tests/_function_length_baseline.json +++ b/tests/_function_length_baseline.json @@ -35,7 +35,6 @@ "linting/coverage.py:lint_feature_coverage": 222, "linting/hole_coverage.py:hole_requirement_outcomes": 214, "linting/structural.py:_lint_annotation_pairs": 257, - "linting/structural.py:_lint_view_shapes": 234, "linting/turned_step_coverage.py:turned_step_requirement_outcomes": 232, "model/detect.py:_append_late_feature_families": 220, "pdf_text.py:pdf_text_runs": 229, diff --git a/tests/_tier_manifest.py b/tests/_tier_manifest.py index cf24bb72..6872cbb6 100644 --- a/tests/_tier_manifest.py +++ b/tests/_tier_manifest.py @@ -595,6 +595,13 @@ class ContractGroup: "test_envelope_view_route_issue_1813.py", "test_issue_1000_envelope_reuse.py", ), + "src/draftwright/annotations/_height_ladder.py": ("test_issue_1299_page_escalation.py",), + "src/draftwright/annotations/_leader_fixed_ink.py": ( + "test_issue_1338_scale_before_page_escalation.py", + ), + "src/draftwright/annotations/_placement_occupancy.py": ( + "test_issue_1338_scale_before_page_escalation.py", + ), "src/draftwright/annotations/_pocket_pad.py": ("test_issue_916_pocket_leader.py",), "src/draftwright/annotations/balloons.py": ("test_balloon_ring_standoff.py",), "src/draftwright/annotations/from_model.py": ( @@ -608,7 +615,10 @@ class ContractGroup: "test_suppression_marks.py", ), "src/draftwright/annotations/gears.py": ("test_issue_1086_declared_gears.py",), - "src/draftwright/annotations/holes.py": ("test_issue_367_leader_ink.py",), + "src/draftwright/annotations/holes.py": ( + "test_issue_367_leader_ink.py", + "test_issue_1338_scale_before_page_escalation.py", + ), "src/draftwright/annotations/leaders.py": ( "test_issue_1187_unroutable_leaders.py", "test_issue_1188_per_view_assignment.py", @@ -621,6 +631,7 @@ class ContractGroup: ), "src/draftwright/builder.py": ( "test_issue_1155_grm04_sheet_use.py", + "test_issue_1338_scale_before_page_escalation.py", "test_repack_geometry_seam.py", "test_scale_policy.py", ), @@ -669,6 +680,11 @@ class ContractGroup: "test_candidate_preview_sheet_holdout.py", ), "src/draftwright/linting/_registry.py": ("test_issue_1229_ledger_member_keying.py",), + "src/draftwright/linting/orchestration.py": ( + "test_gdt_placement.py", + "test_interior_label_placement.py", + "test_pmi_manufacturing_finish.py", + ), "src/draftwright/linting/angled_step_coverage.py": ( "test_issue_1247_angled_step_disposition.py", ), @@ -710,7 +726,6 @@ class ContractGroup: "test_issue_1432_adoption.py", "test_issue_1432_boundary_failures.py", ), - "src/draftwright/linting/orchestration.py": ("test_pmi_manufacturing_finish.py",), "src/draftwright/linting/paired_ramp_step_coverage.py": ("test_paired_ramp_semantics.py",), "src/draftwright/linting/pmi_coverage.py": ( "test_manufacturing_schedule.py", @@ -731,6 +746,8 @@ class ContractGroup: ), "src/draftwright/linting/schedule_evidence.py": ("test_issue_1543_schedule_requirements.py",), "src/draftwright/linting/structural.py": ( + "test_gdt_placement.py", + "test_interior_label_placement.py", "test_issue_1153_contradictory_dimensions.py", "test_issue_1196_deterministic_view_names.py", "test_issue_1204_multiscale_view_issues.py", diff --git a/tests/fixtures/grm03_specify_pmi_ap242.step b/tests/fixtures/grm03_specify_pmi_ap242.step new file mode 100644 index 00000000..0e1f121c --- /dev/null +++ b/tests/fixtures/grm03_specify_pmi_ap242.step @@ -0,0 +1,879 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('Open CASCADE Model'),'2;1'); +FILE_NAME('Open CASCADE Shape Model','2026-10-03T19:21:41',('Author'),( + 'Open CASCADE'),'Open CASCADE STEP processor 7.9','Open CASCADE 7.9' + ,'Unknown'); +FILE_SCHEMA(( +'AP242_MANAGED_MODEL_BASED_3D_ENGINEERING_MIM_LF {1 0 10303 442 1 1 4 }' + )); +ENDSEC; +DATA; +#1 = APPLICATION_PROTOCOL_DEFINITION('international standard', + 'ap242_managed_model_based_3d_engineering',2013,#2); +#2 = APPLICATION_CONTEXT('Managed model based 3d engineering'); +#3 = SHAPE_DEFINITION_REPRESENTATION(#4,#10); +#4 = PRODUCT_DEFINITION_SHAPE('','',#5); +#5 = PRODUCT_DEFINITION('design','',#6,#9); +#6 = PRODUCT_DEFINITION_FORMATION('','',#7); +#7 = PRODUCT('COMPOUND','COMPOUND','',(#8)); +#8 = PRODUCT_CONTEXT('',#2,'mechanical'); +#9 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#10 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#15),#632); +#11 = AXIS2_PLACEMENT_3D('',#12,#13,#14); +#12 = CARTESIAN_POINT('',(0.,0.,0.)); +#13 = DIRECTION('',(0.,0.,1.)); +#14 = DIRECTION('',(1.,0.,-0.)); +#15 = MANIFOLD_SOLID_BREP('',#16); +#16 = CLOSED_SHELL('',(#17,#85,#138,#193,#248,#283,#336,#391,#426,#479, + #534,#538,#573,#628)); +#17 = ADVANCED_FACE('',(#18,#54),#31,.T.); +#18 = FACE_BOUND('',#19,.T.); +#19 = EDGE_LOOP('',(#20)); +#20 = ORIENTED_EDGE('',*,*,#21,.T.); +#21 = EDGE_CURVE('',#22,#22,#24,.T.); +#22 = VERTEX_POINT('',#23); +#23 = CARTESIAN_POINT('',(2.5,0.,-4.7)); +#24 = SURFACE_CURVE('',#25,(#30,#42),.PCURVE_S1.); +#25 = CIRCLE('',#26,4.7); +#26 = AXIS2_PLACEMENT_3D('',#27,#28,#29); +#27 = CARTESIAN_POINT('',(2.5,0.,2.22044604925E-16)); +#28 = DIRECTION('',(1.,-0.,2.22044604925E-16)); +#29 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#30 = PCURVE('',#31,#36); +#31 = PLANE('',#32); +#32 = AXIS2_PLACEMENT_3D('',#33,#34,#35); +#33 = CARTESIAN_POINT('',(2.5,0.,2.22044604925E-16)); +#34 = DIRECTION('',(1.,0.,2.22044604925E-16)); +#35 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#36 = DEFINITIONAL_REPRESENTATION('',(#37),#41); +#37 = CIRCLE('',#38,4.7); +#38 = AXIS2_PLACEMENT_2D('',#39,#40); +#39 = CARTESIAN_POINT('',(0.,0.)); +#40 = DIRECTION('',(1.,0.)); +#41 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#42 = PCURVE('',#43,#48); +#43 = CONICAL_SURFACE('',#44,5.,0.785398163397); +#44 = AXIS2_PLACEMENT_3D('',#45,#46,#47); +#45 = CARTESIAN_POINT('',(2.2,0.,1.554312234475E-16)); +#46 = DIRECTION('',(-1.,0.,-2.22044604925E-16)); +#47 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#48 = DEFINITIONAL_REPRESENTATION('',(#49),#53); +#49 = LINE('',#50,#51); +#50 = CARTESIAN_POINT('',(6.28318530718,-0.3)); +#51 = VECTOR('',#52,1.); +#52 = DIRECTION('',(-1.,0.)); +#53 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#54 = FACE_BOUND('',#55,.T.); +#55 = EDGE_LOOP('',(#56)); +#56 = ORIENTED_EDGE('',*,*,#57,.F.); +#57 = EDGE_CURVE('',#58,#58,#60,.T.); +#58 = VERTEX_POINT('',#59); +#59 = CARTESIAN_POINT('',(2.5,-6.123233995737E-16,-2.5)); +#60 = SURFACE_CURVE('',#61,(#66,#73),.PCURVE_S1.); +#61 = CIRCLE('',#62,2.5); +#62 = AXIS2_PLACEMENT_3D('',#63,#64,#65); +#63 = CARTESIAN_POINT('',(2.5,0.,-3.330669073875E-16)); +#64 = DIRECTION('',(1.,-0.,2.22044604925E-16)); +#65 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#66 = PCURVE('',#31,#67); +#67 = DEFINITIONAL_REPRESENTATION('',(#68),#72); +#68 = CIRCLE('',#69,2.5); +#69 = AXIS2_PLACEMENT_2D('',#70,#71); +#70 = CARTESIAN_POINT('',(5.551115123125E-16,0.)); +#71 = DIRECTION('',(1.,0.)); +#72 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#73 = PCURVE('',#74,#79); +#74 = CYLINDRICAL_SURFACE('',#75,2.5); +#75 = AXIS2_PLACEMENT_3D('',#76,#77,#78); +#76 = CARTESIAN_POINT('',(2.5,0.,-3.330669073875E-16)); +#77 = DIRECTION('',(1.,0.,2.22044604925E-16)); +#78 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#79 = DEFINITIONAL_REPRESENTATION('',(#80),#84); +#80 = LINE('',#81,#82); +#81 = CARTESIAN_POINT('',(0.,0.)); +#82 = VECTOR('',#83,1.); +#83 = DIRECTION('',(1.,0.)); +#84 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#85 = ADVANCED_FACE('',(#86),#43,.T.); +#86 = FACE_BOUND('',#87,.T.); +#87 = EDGE_LOOP('',(#88,#117,#136,#137)); +#88 = ORIENTED_EDGE('',*,*,#89,.T.); +#89 = EDGE_CURVE('',#90,#90,#92,.T.); +#90 = VERTEX_POINT('',#91); +#91 = CARTESIAN_POINT('',(2.2,0.,-5.)); +#92 = SURFACE_CURVE('',#93,(#98,#105),.PCURVE_S1.); +#93 = CIRCLE('',#94,5.); +#94 = AXIS2_PLACEMENT_3D('',#95,#96,#97); +#95 = CARTESIAN_POINT('',(2.2,0.,1.554312234475E-16)); +#96 = DIRECTION('',(1.,-0.,2.22044604925E-16)); +#97 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#98 = PCURVE('',#43,#99); +#99 = DEFINITIONAL_REPRESENTATION('',(#100),#104); +#100 = LINE('',#101,#102); +#101 = CARTESIAN_POINT('',(6.28318530718,4.440892098501E-16)); +#102 = VECTOR('',#103,1.); +#103 = DIRECTION('',(-1.,0.)); +#104 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#105 = PCURVE('',#106,#111); +#106 = CYLINDRICAL_SURFACE('',#107,5.); +#107 = AXIS2_PLACEMENT_3D('',#108,#109,#110); +#108 = CARTESIAN_POINT('',(0.5,0.,-2.22044604925E-16)); +#109 = DIRECTION('',(1.,0.,2.22044604925E-16)); +#110 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#111 = DEFINITIONAL_REPRESENTATION('',(#112),#116); +#112 = LINE('',#113,#114); +#113 = CARTESIAN_POINT('',(0.,1.7)); +#114 = VECTOR('',#115,1.); +#115 = DIRECTION('',(1.,0.)); +#116 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#117 = ORIENTED_EDGE('',*,*,#118,.F.); +#118 = EDGE_CURVE('',#22,#90,#119,.T.); +#119 = SEAM_CURVE('',#120,(#124,#130),.PCURVE_S1.); +#120 = LINE('',#121,#122); +#121 = CARTESIAN_POINT('',(2.2,0.,-5.)); +#122 = VECTOR('',#123,1.); +#123 = DIRECTION('',(-0.707106781187,0.,-0.707106781187)); +#124 = PCURVE('',#43,#125); +#125 = DEFINITIONAL_REPRESENTATION('',(#126),#129); +#126 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#127,#128),.UNSPECIFIED.,.F.,.F., + (2,2),(-0.424264068712,0.),.PIECEWISE_BEZIER_KNOTS.); +#127 = CARTESIAN_POINT('',(0.,-0.3)); +#128 = CARTESIAN_POINT('',(0.,0.)); +#129 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#130 = PCURVE('',#43,#131); +#131 = DEFINITIONAL_REPRESENTATION('',(#132),#135); +#132 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#133,#134),.UNSPECIFIED.,.F.,.F., + (2,2),(-0.424264068712,0.),.PIECEWISE_BEZIER_KNOTS.); +#133 = CARTESIAN_POINT('',(6.283185307179,-0.3)); +#134 = CARTESIAN_POINT('',(6.283185307179,0.)); +#135 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#136 = ORIENTED_EDGE('',*,*,#21,.F.); +#137 = ORIENTED_EDGE('',*,*,#118,.T.); +#138 = ADVANCED_FACE('',(#139),#74,.T.); +#139 = FACE_BOUND('',#140,.T.); +#140 = EDGE_LOOP('',(#141,#170,#191,#192)); +#141 = ORIENTED_EDGE('',*,*,#142,.F.); +#142 = EDGE_CURVE('',#143,#143,#145,.T.); +#143 = VERTEX_POINT('',#144); +#144 = CARTESIAN_POINT('',(5.5,-6.123233995737E-16,-2.5)); +#145 = SURFACE_CURVE('',#146,(#151,#158),.PCURVE_S1.); +#146 = CIRCLE('',#147,2.5); +#147 = AXIS2_PLACEMENT_3D('',#148,#149,#150); +#148 = CARTESIAN_POINT('',(5.5,0.,3.330669073875E-16)); +#149 = DIRECTION('',(1.,-0.,2.22044604925E-16)); +#150 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#151 = PCURVE('',#74,#152); +#152 = DEFINITIONAL_REPRESENTATION('',(#153),#157); +#153 = LINE('',#154,#155); +#154 = CARTESIAN_POINT('',(0.,3.)); +#155 = VECTOR('',#156,1.); +#156 = DIRECTION('',(1.,0.)); +#157 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#158 = PCURVE('',#159,#164); +#159 = PLANE('',#160); +#160 = AXIS2_PLACEMENT_3D('',#161,#162,#163); +#161 = CARTESIAN_POINT('',(5.5,0.,3.330669073875E-16)); +#162 = DIRECTION('',(1.,0.,2.22044604925E-16)); +#163 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#164 = DEFINITIONAL_REPRESENTATION('',(#165),#169); +#165 = CIRCLE('',#166,2.5); +#166 = AXIS2_PLACEMENT_2D('',#167,#168); +#167 = CARTESIAN_POINT('',(0.,0.)); +#168 = DIRECTION('',(1.,0.)); +#169 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#170 = ORIENTED_EDGE('',*,*,#171,.F.); +#171 = EDGE_CURVE('',#58,#143,#172,.T.); +#172 = SEAM_CURVE('',#173,(#177,#184),.PCURVE_S1.); +#173 = LINE('',#174,#175); +#174 = CARTESIAN_POINT('',(2.5,-6.123233995737E-16,-2.5)); +#175 = VECTOR('',#176,1.); +#176 = DIRECTION('',(1.,0.,2.22044604925E-16)); +#177 = PCURVE('',#74,#178); +#178 = DEFINITIONAL_REPRESENTATION('',(#179),#183); +#179 = LINE('',#180,#181); +#180 = CARTESIAN_POINT('',(0.,-0.)); +#181 = VECTOR('',#182,1.); +#182 = DIRECTION('',(0.,1.)); +#183 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#184 = PCURVE('',#74,#185); +#185 = DEFINITIONAL_REPRESENTATION('',(#186),#190); +#186 = LINE('',#187,#188); +#187 = CARTESIAN_POINT('',(6.28318530718,-0.)); +#188 = VECTOR('',#189,1.); +#189 = DIRECTION('',(0.,1.)); +#190 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#191 = ORIENTED_EDGE('',*,*,#57,.T.); +#192 = ORIENTED_EDGE('',*,*,#171,.T.); +#193 = ADVANCED_FACE('',(#194),#106,.T.); +#194 = FACE_BOUND('',#195,.T.); +#195 = EDGE_LOOP('',(#196,#219,#220,#221)); +#196 = ORIENTED_EDGE('',*,*,#197,.T.); +#197 = EDGE_CURVE('',#198,#90,#200,.T.); +#198 = VERTEX_POINT('',#199); +#199 = CARTESIAN_POINT('',(0.8,0.,-5.)); +#200 = SEAM_CURVE('',#201,(#205,#212),.PCURVE_S1.); +#201 = LINE('',#202,#203); +#202 = CARTESIAN_POINT('',(0.5,-1.224646799147E-15,-5.)); +#203 = VECTOR('',#204,1.); +#204 = DIRECTION('',(1.,0.,2.22044604925E-16)); +#205 = PCURVE('',#106,#206); +#206 = DEFINITIONAL_REPRESENTATION('',(#207),#211); +#207 = LINE('',#208,#209); +#208 = CARTESIAN_POINT('',(6.28318530718,0.)); +#209 = VECTOR('',#210,1.); +#210 = DIRECTION('',(0.,1.)); +#211 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#212 = PCURVE('',#106,#213); +#213 = DEFINITIONAL_REPRESENTATION('',(#214),#218); +#214 = LINE('',#215,#216); +#215 = CARTESIAN_POINT('',(4.138911435803E-13,0.)); +#216 = VECTOR('',#217,1.); +#217 = DIRECTION('',(0.,1.)); +#218 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#219 = ORIENTED_EDGE('',*,*,#89,.F.); +#220 = ORIENTED_EDGE('',*,*,#197,.F.); +#221 = ORIENTED_EDGE('',*,*,#222,.T.); +#222 = EDGE_CURVE('',#198,#198,#223,.T.); +#223 = SURFACE_CURVE('',#224,(#229,#236),.PCURVE_S1.); +#224 = CIRCLE('',#225,5.); +#225 = AXIS2_PLACEMENT_3D('',#226,#227,#228); +#226 = CARTESIAN_POINT('',(0.8,0.,-1.554312234475E-16)); +#227 = DIRECTION('',(1.,-0.,2.22044604925E-16)); +#228 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#229 = PCURVE('',#106,#230); +#230 = DEFINITIONAL_REPRESENTATION('',(#231),#235); +#231 = LINE('',#232,#233); +#232 = CARTESIAN_POINT('',(0.,0.3)); +#233 = VECTOR('',#234,1.); +#234 = DIRECTION('',(1.,0.)); +#235 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#236 = PCURVE('',#237,#242); +#237 = CONICAL_SURFACE('',#238,5.,0.785398163397); +#238 = AXIS2_PLACEMENT_3D('',#239,#240,#241); +#239 = CARTESIAN_POINT('',(0.8,0.,-1.554312234475E-16)); +#240 = DIRECTION('',(1.,-0.,2.22044604925E-16)); +#241 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#242 = DEFINITIONAL_REPRESENTATION('',(#243),#247); +#243 = LINE('',#244,#245); +#244 = CARTESIAN_POINT('',(0.,0.)); +#245 = VECTOR('',#246,1.); +#246 = DIRECTION('',(1.,-0.)); +#247 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#248 = ADVANCED_FACE('',(#249,#252),#159,.T.); +#249 = FACE_BOUND('',#250,.T.); +#250 = EDGE_LOOP('',(#251)); +#251 = ORIENTED_EDGE('',*,*,#142,.T.); +#252 = FACE_BOUND('',#253,.T.); +#253 = EDGE_LOOP('',(#254)); +#254 = ORIENTED_EDGE('',*,*,#255,.F.); +#255 = EDGE_CURVE('',#256,#256,#258,.T.); +#256 = VERTEX_POINT('',#257); +#257 = CARTESIAN_POINT('',(5.5,-3.673940397442E-16,-1.5)); +#258 = SURFACE_CURVE('',#259,(#264,#271),.PCURVE_S1.); +#259 = CIRCLE('',#260,1.5); +#260 = AXIS2_PLACEMENT_3D('',#261,#262,#263); +#261 = CARTESIAN_POINT('',(5.5,0.,-1.998401444325E-15)); +#262 = DIRECTION('',(1.,-0.,2.22044604925E-16)); +#263 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#264 = PCURVE('',#159,#265); +#265 = DEFINITIONAL_REPRESENTATION('',(#266),#270); +#266 = CIRCLE('',#267,1.5); +#267 = AXIS2_PLACEMENT_2D('',#268,#269); +#268 = CARTESIAN_POINT('',(2.331468351713E-15,0.)); +#269 = DIRECTION('',(1.,0.)); +#270 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#271 = PCURVE('',#272,#277); +#272 = CYLINDRICAL_SURFACE('',#273,1.5); +#273 = AXIS2_PLACEMENT_3D('',#274,#275,#276); +#274 = CARTESIAN_POINT('',(5.5,0.,-1.998401444325E-15)); +#275 = DIRECTION('',(1.,0.,2.22044604925E-16)); +#276 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#277 = DEFINITIONAL_REPRESENTATION('',(#278),#282); +#278 = LINE('',#279,#280); +#279 = CARTESIAN_POINT('',(0.,0.)); +#280 = VECTOR('',#281,1.); +#281 = DIRECTION('',(1.,0.)); +#282 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#283 = ADVANCED_FACE('',(#284),#237,.T.); +#284 = FACE_BOUND('',#285,.T.); +#285 = EDGE_LOOP('',(#286,#315,#334,#335)); +#286 = ORIENTED_EDGE('',*,*,#287,.T.); +#287 = EDGE_CURVE('',#288,#288,#290,.T.); +#288 = VERTEX_POINT('',#289); +#289 = CARTESIAN_POINT('',(0.5,0.,-4.7)); +#290 = SURFACE_CURVE('',#291,(#296,#303),.PCURVE_S1.); +#291 = CIRCLE('',#292,4.7); +#292 = AXIS2_PLACEMENT_3D('',#293,#294,#295); +#293 = CARTESIAN_POINT('',(0.5,0.,-2.22044604925E-16)); +#294 = DIRECTION('',(1.,-0.,2.22044604925E-16)); +#295 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#296 = PCURVE('',#237,#297); +#297 = DEFINITIONAL_REPRESENTATION('',(#298),#302); +#298 = LINE('',#299,#300); +#299 = CARTESIAN_POINT('',(0.,-0.3)); +#300 = VECTOR('',#301,1.); +#301 = DIRECTION('',(1.,-0.)); +#302 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#303 = PCURVE('',#304,#309); +#304 = PLANE('',#305); +#305 = AXIS2_PLACEMENT_3D('',#306,#307,#308); +#306 = CARTESIAN_POINT('',(0.5,0.,-2.22044604925E-16)); +#307 = DIRECTION('',(1.,0.,2.22044604925E-16)); +#308 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#309 = DEFINITIONAL_REPRESENTATION('',(#310),#314); +#310 = CIRCLE('',#311,4.7); +#311 = AXIS2_PLACEMENT_2D('',#312,#313); +#312 = CARTESIAN_POINT('',(0.,0.)); +#313 = DIRECTION('',(1.,0.)); +#314 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#315 = ORIENTED_EDGE('',*,*,#316,.F.); +#316 = EDGE_CURVE('',#198,#288,#317,.T.); +#317 = SEAM_CURVE('',#318,(#322,#328),.PCURVE_S1.); +#318 = LINE('',#319,#320); +#319 = CARTESIAN_POINT('',(0.8,0.,-5.)); +#320 = VECTOR('',#321,1.); +#321 = DIRECTION('',(-0.707106781187,0.,0.707106781187)); +#322 = PCURVE('',#237,#323); +#323 = DEFINITIONAL_REPRESENTATION('',(#324),#327); +#324 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#325,#326),.UNSPECIFIED.,.F.,.F., + (2,2),(0.,0.424264068712),.PIECEWISE_BEZIER_KNOTS.); +#325 = CARTESIAN_POINT('',(6.28318530718,0.)); +#326 = CARTESIAN_POINT('',(6.28318530718,-0.3)); +#327 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#328 = PCURVE('',#237,#329); +#329 = DEFINITIONAL_REPRESENTATION('',(#330),#333); +#330 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#331,#332),.UNSPECIFIED.,.F.,.F., + (2,2),(0.,0.424264068712),.PIECEWISE_BEZIER_KNOTS.); +#331 = CARTESIAN_POINT('',(4.138911435803E-13,0.)); +#332 = CARTESIAN_POINT('',(4.138911435803E-13,-0.3)); +#333 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#334 = ORIENTED_EDGE('',*,*,#222,.F.); +#335 = ORIENTED_EDGE('',*,*,#316,.T.); +#336 = ADVANCED_FACE('',(#337),#272,.T.); +#337 = FACE_BOUND('',#338,.T.); +#338 = EDGE_LOOP('',(#339,#362,#389,#390)); +#339 = ORIENTED_EDGE('',*,*,#340,.T.); +#340 = EDGE_CURVE('',#256,#341,#343,.T.); +#341 = VERTEX_POINT('',#342); +#342 = CARTESIAN_POINT('',(23.,0.,-1.5)); +#343 = SEAM_CURVE('',#344,(#348,#355),.PCURVE_S1.); +#344 = LINE('',#345,#346); +#345 = CARTESIAN_POINT('',(5.5,-3.673940397442E-16,-1.5)); +#346 = VECTOR('',#347,1.); +#347 = DIRECTION('',(1.,0.,2.22044604925E-16)); +#348 = PCURVE('',#272,#349); +#349 = DEFINITIONAL_REPRESENTATION('',(#350),#354); +#350 = LINE('',#351,#352); +#351 = CARTESIAN_POINT('',(6.28318530718,0.)); +#352 = VECTOR('',#353,1.); +#353 = DIRECTION('',(0.,1.)); +#354 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#355 = PCURVE('',#272,#356); +#356 = DEFINITIONAL_REPRESENTATION('',(#357),#361); +#357 = LINE('',#358,#359); +#358 = CARTESIAN_POINT('',(4.138911435803E-13,0.)); +#359 = VECTOR('',#360,1.); +#360 = DIRECTION('',(0.,1.)); +#361 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#362 = ORIENTED_EDGE('',*,*,#363,.F.); +#363 = EDGE_CURVE('',#341,#341,#364,.T.); +#364 = SURFACE_CURVE('',#365,(#370,#377),.PCURVE_S1.); +#365 = CIRCLE('',#366,1.5); +#366 = AXIS2_PLACEMENT_3D('',#367,#368,#369); +#367 = CARTESIAN_POINT('',(23.,0.,1.887379141863E-15)); +#368 = DIRECTION('',(1.,-0.,2.22044604925E-16)); +#369 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#370 = PCURVE('',#272,#371); +#371 = DEFINITIONAL_REPRESENTATION('',(#372),#376); +#372 = LINE('',#373,#374); +#373 = CARTESIAN_POINT('',(0.,17.5)); +#374 = VECTOR('',#375,1.); +#375 = DIRECTION('',(1.,0.)); +#376 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#377 = PCURVE('',#378,#383); +#378 = CONICAL_SURFACE('',#379,1.5,0.785398163397); +#379 = AXIS2_PLACEMENT_3D('',#380,#381,#382); +#380 = CARTESIAN_POINT('',(23.,0.,1.887379141863E-15)); +#381 = DIRECTION('',(-1.,0.,-2.22044604925E-16)); +#382 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#383 = DEFINITIONAL_REPRESENTATION('',(#384),#388); +#384 = LINE('',#385,#386); +#385 = CARTESIAN_POINT('',(6.28318530718,0.)); +#386 = VECTOR('',#387,1.); +#387 = DIRECTION('',(-1.,0.)); +#388 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#389 = ORIENTED_EDGE('',*,*,#340,.F.); +#390 = ORIENTED_EDGE('',*,*,#255,.T.); +#391 = ADVANCED_FACE('',(#392,#395),#304,.F.); +#392 = FACE_BOUND('',#393,.F.); +#393 = EDGE_LOOP('',(#394)); +#394 = ORIENTED_EDGE('',*,*,#287,.T.); +#395 = FACE_BOUND('',#396,.F.); +#396 = EDGE_LOOP('',(#397)); +#397 = ORIENTED_EDGE('',*,*,#398,.F.); +#398 = EDGE_CURVE('',#399,#399,#401,.T.); +#399 = VERTEX_POINT('',#400); +#400 = CARTESIAN_POINT('',(0.5,-7.347880794884E-16,-3.)); +#401 = SURFACE_CURVE('',#402,(#407,#414),.PCURVE_S1.); +#402 = CIRCLE('',#403,3.); +#403 = AXIS2_PLACEMENT_3D('',#404,#405,#406); +#404 = CARTESIAN_POINT('',(0.5,0.,5.551115123126E-17)); +#405 = DIRECTION('',(1.,-0.,2.22044604925E-16)); +#406 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#407 = PCURVE('',#304,#408); +#408 = DEFINITIONAL_REPRESENTATION('',(#409),#413); +#409 = CIRCLE('',#410,3.); +#410 = AXIS2_PLACEMENT_2D('',#411,#412); +#411 = CARTESIAN_POINT('',(-2.775557561563E-16,0.)); +#412 = DIRECTION('',(1.,0.)); +#413 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#414 = PCURVE('',#415,#420); +#415 = CYLINDRICAL_SURFACE('',#416,3.); +#416 = AXIS2_PLACEMENT_3D('',#417,#418,#419); +#417 = CARTESIAN_POINT('',(0.,0.,-5.551115123126E-17)); +#418 = DIRECTION('',(1.,0.,2.22044604925E-16)); +#419 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#420 = DEFINITIONAL_REPRESENTATION('',(#421),#425); +#421 = LINE('',#422,#423); +#422 = CARTESIAN_POINT('',(0.,0.5)); +#423 = VECTOR('',#424,1.); +#424 = DIRECTION('',(1.,0.)); +#425 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#426 = ADVANCED_FACE('',(#427),#378,.T.); +#427 = FACE_BOUND('',#428,.T.); +#428 = EDGE_LOOP('',(#429,#450,#451,#452)); +#429 = ORIENTED_EDGE('',*,*,#430,.F.); +#430 = EDGE_CURVE('',#341,#431,#433,.T.); +#431 = VERTEX_POINT('',#432); +#432 = CARTESIAN_POINT('',(23.5,0.,-1.)); +#433 = SEAM_CURVE('',#434,(#438,#444),.PCURVE_S1.); +#434 = LINE('',#435,#436); +#435 = CARTESIAN_POINT('',(23.,0.,-1.5)); +#436 = VECTOR('',#437,1.); +#437 = DIRECTION('',(0.707106781187,-0.,0.707106781187)); +#438 = PCURVE('',#378,#439); +#439 = DEFINITIONAL_REPRESENTATION('',(#440),#443); +#440 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#441,#442),.UNSPECIFIED.,.F.,.F., + (2,2),(0.,0.707106781187),.PIECEWISE_BEZIER_KNOTS.); +#441 = CARTESIAN_POINT('',(6.28318530718,0.)); +#442 = CARTESIAN_POINT('',(6.28318530718,-0.5)); +#443 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#444 = PCURVE('',#378,#445); +#445 = DEFINITIONAL_REPRESENTATION('',(#446),#449); +#446 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#447,#448),.UNSPECIFIED.,.F.,.F., + (2,2),(0.,0.707106781187),.PIECEWISE_BEZIER_KNOTS.); +#447 = CARTESIAN_POINT('',(-4.138911435803E-13,0.)); +#448 = CARTESIAN_POINT('',(-4.138911435803E-13,-0.5)); +#449 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#450 = ORIENTED_EDGE('',*,*,#363,.T.); +#451 = ORIENTED_EDGE('',*,*,#430,.T.); +#452 = ORIENTED_EDGE('',*,*,#453,.F.); +#453 = EDGE_CURVE('',#431,#431,#454,.T.); +#454 = SURFACE_CURVE('',#455,(#460,#467),.PCURVE_S1.); +#455 = CIRCLE('',#456,1.); +#456 = AXIS2_PLACEMENT_3D('',#457,#458,#459); +#457 = CARTESIAN_POINT('',(23.5,0.,1.998401444325E-15)); +#458 = DIRECTION('',(1.,-0.,2.22044604925E-16)); +#459 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#460 = PCURVE('',#378,#461); +#461 = DEFINITIONAL_REPRESENTATION('',(#462),#466); +#462 = LINE('',#463,#464); +#463 = CARTESIAN_POINT('',(6.28318530718,-0.5)); +#464 = VECTOR('',#465,1.); +#465 = DIRECTION('',(-1.,0.)); +#466 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#467 = PCURVE('',#468,#473); +#468 = PLANE('',#469); +#469 = AXIS2_PLACEMENT_3D('',#470,#471,#472); +#470 = CARTESIAN_POINT('',(23.5,0.,1.998401444325E-15)); +#471 = DIRECTION('',(1.,0.,2.22044604925E-16)); +#472 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#473 = DEFINITIONAL_REPRESENTATION('',(#474),#478); +#474 = CIRCLE('',#475,1.); +#475 = AXIS2_PLACEMENT_2D('',#476,#477); +#476 = CARTESIAN_POINT('',(0.,0.)); +#477 = DIRECTION('',(1.,0.)); +#478 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#479 = ADVANCED_FACE('',(#480),#415,.T.); +#480 = FACE_BOUND('',#481,.T.); +#481 = EDGE_LOOP('',(#482,#483,#506,#533)); +#482 = ORIENTED_EDGE('',*,*,#398,.F.); +#483 = ORIENTED_EDGE('',*,*,#484,.F.); +#484 = EDGE_CURVE('',#485,#399,#487,.T.); +#485 = VERTEX_POINT('',#486); +#486 = CARTESIAN_POINT('',(6.661338147751E-16,-7.347880794884E-16,-3.)); +#487 = SEAM_CURVE('',#488,(#492,#499),.PCURVE_S1.); +#488 = LINE('',#489,#490); +#489 = CARTESIAN_POINT('',(6.661338147751E-16,-7.347880794884E-16,-3.)); +#490 = VECTOR('',#491,1.); +#491 = DIRECTION('',(1.,0.,2.22044604925E-16)); +#492 = PCURVE('',#415,#493); +#493 = DEFINITIONAL_REPRESENTATION('',(#494),#498); +#494 = LINE('',#495,#496); +#495 = CARTESIAN_POINT('',(0.,-0.)); +#496 = VECTOR('',#497,1.); +#497 = DIRECTION('',(0.,1.)); +#498 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#499 = PCURVE('',#415,#500); +#500 = DEFINITIONAL_REPRESENTATION('',(#501),#505); +#501 = LINE('',#502,#503); +#502 = CARTESIAN_POINT('',(6.28318530718,-0.)); +#503 = VECTOR('',#504,1.); +#504 = DIRECTION('',(0.,1.)); +#505 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#506 = ORIENTED_EDGE('',*,*,#507,.T.); +#507 = EDGE_CURVE('',#485,#485,#508,.T.); +#508 = SURFACE_CURVE('',#509,(#514,#521),.PCURVE_S1.); +#509 = CIRCLE('',#510,3.); +#510 = AXIS2_PLACEMENT_3D('',#511,#512,#513); +#511 = CARTESIAN_POINT('',(0.,0.,-5.551115123126E-17)); +#512 = DIRECTION('',(1.,-0.,2.22044604925E-16)); +#513 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#514 = PCURVE('',#415,#515); +#515 = DEFINITIONAL_REPRESENTATION('',(#516),#520); +#516 = LINE('',#517,#518); +#517 = CARTESIAN_POINT('',(0.,0.)); +#518 = VECTOR('',#519,1.); +#519 = DIRECTION('',(1.,0.)); +#520 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#521 = PCURVE('',#522,#527); +#522 = PLANE('',#523); +#523 = AXIS2_PLACEMENT_3D('',#524,#525,#526); +#524 = CARTESIAN_POINT('',(0.,0.,-5.551115123126E-17)); +#525 = DIRECTION('',(1.,0.,2.22044604925E-16)); +#526 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#527 = DEFINITIONAL_REPRESENTATION('',(#528),#532); +#528 = CIRCLE('',#529,3.); +#529 = AXIS2_PLACEMENT_2D('',#530,#531); +#530 = CARTESIAN_POINT('',(0.,0.)); +#531 = DIRECTION('',(1.,0.)); +#532 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#533 = ORIENTED_EDGE('',*,*,#484,.T.); +#534 = ADVANCED_FACE('',(#535),#468,.T.); +#535 = FACE_BOUND('',#536,.T.); +#536 = EDGE_LOOP('',(#537)); +#537 = ORIENTED_EDGE('',*,*,#453,.T.); +#538 = ADVANCED_FACE('',(#539,#542),#522,.F.); +#539 = FACE_BOUND('',#540,.F.); +#540 = EDGE_LOOP('',(#541)); +#541 = ORIENTED_EDGE('',*,*,#507,.T.); +#542 = FACE_BOUND('',#543,.F.); +#543 = EDGE_LOOP('',(#544)); +#544 = ORIENTED_EDGE('',*,*,#545,.F.); +#545 = EDGE_CURVE('',#546,#546,#548,.T.); +#546 = VERTEX_POINT('',#547); +#547 = CARTESIAN_POINT('',(1.7763568394E-16,-1.959434878636E-16,-0.8)); +#548 = SURFACE_CURVE('',#549,(#554,#561),.PCURVE_S1.); +#549 = CIRCLE('',#550,0.8); +#550 = AXIS2_PLACEMENT_3D('',#551,#552,#553); +#551 = CARTESIAN_POINT('',(0.,0.,-4.218847493576E-16)); +#552 = DIRECTION('',(1.,-0.,2.22044604925E-16)); +#553 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#554 = PCURVE('',#522,#555); +#555 = DEFINITIONAL_REPRESENTATION('',(#556),#560); +#556 = CIRCLE('',#557,0.8); +#557 = AXIS2_PLACEMENT_2D('',#558,#559); +#558 = CARTESIAN_POINT('',(3.663735981263E-16,0.)); +#559 = DIRECTION('',(1.,0.)); +#560 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#561 = PCURVE('',#562,#567); +#562 = CYLINDRICAL_SURFACE('',#563,0.8); +#563 = AXIS2_PLACEMENT_3D('',#564,#565,#566); +#564 = CARTESIAN_POINT('',(0.,0.,-4.218847493576E-16)); +#565 = DIRECTION('',(1.,0.,2.22044604925E-16)); +#566 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#567 = DEFINITIONAL_REPRESENTATION('',(#568),#572); +#568 = LINE('',#569,#570); +#569 = CARTESIAN_POINT('',(0.,0.)); +#570 = VECTOR('',#571,1.); +#571 = DIRECTION('',(1.,0.)); +#572 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#573 = ADVANCED_FACE('',(#574),#562,.F.); +#574 = FACE_BOUND('',#575,.F.); +#575 = EDGE_LOOP('',(#576,#605,#626,#627)); +#576 = ORIENTED_EDGE('',*,*,#577,.F.); +#577 = EDGE_CURVE('',#578,#578,#580,.T.); +#578 = VERTEX_POINT('',#579); +#579 = CARTESIAN_POINT('',(3.8,-1.959434878636E-16,-0.8)); +#580 = SURFACE_CURVE('',#581,(#586,#593),.PCURVE_S1.); +#581 = CIRCLE('',#582,0.8); +#582 = AXIS2_PLACEMENT_3D('',#583,#584,#585); +#583 = CARTESIAN_POINT('',(3.8,0.,4.218847493576E-16)); +#584 = DIRECTION('',(1.,-0.,2.22044604925E-16)); +#585 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#586 = PCURVE('',#562,#587); +#587 = DEFINITIONAL_REPRESENTATION('',(#588),#592); +#588 = LINE('',#589,#590); +#589 = CARTESIAN_POINT('',(0.,3.8)); +#590 = VECTOR('',#591,1.); +#591 = DIRECTION('',(1.,0.)); +#592 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#593 = PCURVE('',#594,#599); +#594 = PLANE('',#595); +#595 = AXIS2_PLACEMENT_3D('',#596,#597,#598); +#596 = CARTESIAN_POINT('',(3.8,0.,4.218847493576E-16)); +#597 = DIRECTION('',(1.,0.,2.22044604925E-16)); +#598 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#599 = DEFINITIONAL_REPRESENTATION('',(#600),#604); +#600 = CIRCLE('',#601,0.8); +#601 = AXIS2_PLACEMENT_2D('',#602,#603); +#602 = CARTESIAN_POINT('',(0.,0.)); +#603 = DIRECTION('',(1.,0.)); +#604 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#605 = ORIENTED_EDGE('',*,*,#606,.F.); +#606 = EDGE_CURVE('',#546,#578,#607,.T.); +#607 = SEAM_CURVE('',#608,(#612,#619),.PCURVE_S1.); +#608 = LINE('',#609,#610); +#609 = CARTESIAN_POINT('',(1.7763568394E-16,-1.959434878636E-16,-0.8)); +#610 = VECTOR('',#611,1.); +#611 = DIRECTION('',(1.,0.,2.22044604925E-16)); +#612 = PCURVE('',#562,#613); +#613 = DEFINITIONAL_REPRESENTATION('',(#614),#618); +#614 = LINE('',#615,#616); +#615 = CARTESIAN_POINT('',(0.,-0.)); +#616 = VECTOR('',#617,1.); +#617 = DIRECTION('',(0.,1.)); +#618 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#619 = PCURVE('',#562,#620); +#620 = DEFINITIONAL_REPRESENTATION('',(#621),#625); +#621 = LINE('',#622,#623); +#622 = CARTESIAN_POINT('',(6.28318530718,-0.)); +#623 = VECTOR('',#624,1.); +#624 = DIRECTION('',(0.,1.)); +#625 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#626 = ORIENTED_EDGE('',*,*,#545,.T.); +#627 = ORIENTED_EDGE('',*,*,#606,.T.); +#628 = ADVANCED_FACE('',(#629),#594,.F.); +#629 = FACE_BOUND('',#630,.F.); +#630 = EDGE_LOOP('',(#631)); +#631 = ORIENTED_EDGE('',*,*,#577,.T.); +#632 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#636)) GLOBAL_UNIT_ASSIGNED_CONTEXT +((#633,#634,#635)) REPRESENTATION_CONTEXT('Context #1', + '3D Context with UNIT and UNCERTAINTY') ); +#633 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#634 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#635 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#636 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#633, + 'distance_accuracy_value','confusion accuracy'); +#637 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7)); +#638 = DATUM_FEATURE('','',#4,.T.); +#639 = GEOMETRIC_ITEM_SPECIFIC_USAGE('','',#638,#10,#17); +#640 = DATUM('','',#4,.F.,'A'); +#641 = SHAPE_ASPECT_RELATIONSHIP('',$,#638,#640); +#642 = DATUM_FEATURE('','',#4,.T.); +#643 = GEOMETRIC_ITEM_SPECIFIC_USAGE('','',#642,#10,#336); +#644 = DATUM('','',#4,.F.,'B'); +#645 = SHAPE_ASPECT_RELATIONSHIP('',$,#642,#644); +#646 = DATUM_REFERENCE_COMPARTMENT('','',#4,.F.,#640,$); +#647 = DATUM_REFERENCE_COMPARTMENT('','',#4,.F.,#644,$); +#648 = DATUM_SYSTEM('','',#4,.F.,(#646,#647)); +#649 = LENGTH_MEASURE_WITH_UNIT(LENGTH_MEASURE(2.E-04),#650); +#650 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT($,.METRE.) ); +#651 = SHAPE_ASPECT('','',#4,.T.); +#652 = GEOMETRIC_ITEM_SPECIFIC_USAGE('','',#651,#10,#573); +#653 = ( GEOMETRIC_TOLERANCE('','',#649,#651) +GEOMETRIC_TOLERANCE_WITH_DATUM_REFERENCE((#648)) POSITION_TOLERANCE() ); +#654 = TOLERANCE_ZONE_FORM('cylindrical or circular'); +#655 = TOLERANCE_ZONE('','',#4,.F.,(#653),#654); +#660 = REPRESENTATION('material name',(#661),#632); +#661 = DESCRIPTIVE_REPRESENTATION_ITEM('Brass CZ121 (CW614N)',''); +#662 = PROPERTY_DEFINITION('material property','material name',#5); +#663 = PROPERTY_DEFINITION_REPRESENTATION(#662,#660); +#664=DRAUGHTING_PRE_DEFINED_CURVE_FONT('continuous'); +#665=DRAUGHTING_PRE_DEFINED_COLOUR('black'); +#666=CURVE_STYLE('',#664,POSITIVE_LENGTH_MEASURE(0.35),#665); +#667=PRESENTATION_STYLE_ASSIGNMENT((#666)); +#668=DESCRIPTIVE_REPRESENTATION_ITEM('internal thread','M2 x 0.4-6H RH, 2.5 mm minimum full thread; DIA 1.6 tapping drill x 3.8 mm full-diameter depth'); +#669=REPRESENTATION('internal thread requirement',(#668),#632); +#670=PROPERTY_DEFINITION('manufacturing requirement','internal thread',#5); +#671=PROPERTY_DEFINITION_REPRESENTATION(#670,#669); +#672=SHAPE_ASPECT('internal thread','',#4,.T.); +#673=GEOMETRIC_ITEM_SPECIFIC_USAGE('internal thread','',#672,#10,#573); +#674=COORDINATES_LIST('',2,((13.8000,-0.0000,9.2000),(3.8000,-0.0000,-0.8000))); +#675=TESSELLATED_CURVE_SET('',#674,((1,2))); +#676=TESSELLATED_GEOMETRIC_SET('note',(#675)); +#677=TESSELLATED_ANNOTATION_OCCURRENCE('Internal thread requirement',(#667),#676); +#678=DRAUGHTING_CALLOUT('Internal thread requirement',(#677)); +#679=SHAPE_ASPECT('internal thread','',#4,.T.); +#680=GEOMETRIC_ITEM_SPECIFIC_USAGE('internal thread','',#679,#10,#573); +#681=DESCRIPTIVE_REPRESENTATION_ITEM('thread side','internal'); +#682=DESCRIPTIVE_REPRESENTATION_ITEM('designation','M2x0.4'); +#683=DESCRIPTIVE_REPRESENTATION_ITEM('nominal size','M2'); +#684=MEASURE_REPRESENTATION_ITEM('pitch',LENGTH_MEASURE(0.4),#633); +#685=DESCRIPTIVE_REPRESENTATION_ITEM('fit class','6H'); +#686=DESCRIPTIVE_REPRESENTATION_ITEM('hand','right'); +#687=MEASURE_REPRESENTATION_ITEM('tapping drill diameter',LENGTH_MEASURE(1.6),#633); +#688=MEASURE_REPRESENTATION_ITEM('tapping drill depth',LENGTH_MEASURE(3.8),#633); +#689=MEASURE_REPRESENTATION_ITEM('minimum full thread',LENGTH_MEASURE(2.5),#633); +#690=PROPERTY_DEFINITION('internal thread','pmi-assist',#679); +#691=GENERAL_PROPERTY('','user defined attribute',$); +#692=GENERAL_PROPERTY_ASSOCIATION('',$,#691,#690); +#693=REPRESENTATION('internal thread',(#681,#682,#683,#684,#685,#686,#687,#688,#689),#632); +#694=PROPERTY_DEFINITION_REPRESENTATION(#690,#693); +#695=DESCRIPTIVE_REPRESENTATION_ITEM('knurl','Straight knurl, 1 mm pitch, DIA 10 mm maximum after knurling; cut or formed process permitted'); +#696=REPRESENTATION('knurl requirement',(#695),#632); +#697=PROPERTY_DEFINITION('manufacturing requirement','knurl',#5); +#698=PROPERTY_DEFINITION_REPRESENTATION(#697,#696); +#699=SHAPE_ASPECT('knurl','',#4,.T.); +#700=GEOMETRIC_ITEM_SPECIFIC_USAGE('knurl','',#699,#10,#193); +#701=COORDINATES_LIST('',2,((10.8000,0.0000,5.0000),(0.8000,0.0000,-5.0000))); +#702=TESSELLATED_CURVE_SET('',#701,((1,2))); +#703=TESSELLATED_GEOMETRIC_SET('note',(#702)); +#704=TESSELLATED_ANNOTATION_OCCURRENCE('Knurl requirement',(#667),#703); +#705=DRAUGHTING_CALLOUT('Knurl requirement',(#704)); +#706=SHAPE_ASPECT('knurl','',#4,.T.); +#707=GEOMETRIC_ITEM_SPECIFIC_USAGE('knurl','',#706,#10,#193); +#708=DESCRIPTIVE_REPRESENTATION_ITEM('pattern','straight'); +#709=MEASURE_REPRESENTATION_ITEM('diametral pitch',LENGTH_MEASURE(1.0),#633); +#710=MEASURE_REPRESENTATION_ITEM('major diameter',LENGTH_MEASURE(10.0),#633); +#711=PROPERTY_DEFINITION('knurl','pmi-assist',#706); +#712=GENERAL_PROPERTY('','user defined attribute',$); +#713=GENERAL_PROPERTY_ASSOCIATION('',$,#712,#711); +#714=REPRESENTATION('knurl',(#708,#709,#710),#632); +#715=PROPERTY_DEFINITION_REPRESENTATION(#711,#714); +#716=DESCRIPTIVE_REPRESENTATION_ITEM('external thread','M3 x 0.5-6g RH, full available length on nominal DIA 3 region'); +#717=REPRESENTATION('external thread requirement',(#716),#632); +#718=PROPERTY_DEFINITION('manufacturing requirement','external thread',#5); +#719=PROPERTY_DEFINITION_REPRESENTATION(#718,#717); +#720=SHAPE_ASPECT('external thread','',#4,.T.); +#721=GEOMETRIC_ITEM_SPECIFIC_USAGE('external thread','',#720,#10,#336); +#722=COORDINATES_LIST('',2,((15.5000,-0.0000,8.5000),(5.5000,-0.0000,-1.5000))); +#723=TESSELLATED_CURVE_SET('',#722,((1,2))); +#724=TESSELLATED_GEOMETRIC_SET('note',(#723)); +#725=TESSELLATED_ANNOTATION_OCCURRENCE('External thread requirement',(#667),#724); +#726=DRAUGHTING_CALLOUT('External thread requirement',(#725)); +#727=SHAPE_ASPECT('external thread','',#4,.T.); +#728=GEOMETRIC_ITEM_SPECIFIC_USAGE('external thread','',#727,#10,#336); +#729=DESCRIPTIVE_REPRESENTATION_ITEM('thread side','external'); +#730=DESCRIPTIVE_REPRESENTATION_ITEM('designation','M3x0.5'); +#731=DESCRIPTIVE_REPRESENTATION_ITEM('nominal size','M3'); +#732=MEASURE_REPRESENTATION_ITEM('pitch',LENGTH_MEASURE(0.5),#633); +#733=DESCRIPTIVE_REPRESENTATION_ITEM('fit class','6g'); +#734=DESCRIPTIVE_REPRESENTATION_ITEM('hand','right'); +#735=MEASURE_REPRESENTATION_ITEM('thread length',LENGTH_MEASURE(18.0),#633); +#736=PROPERTY_DEFINITION('external thread','pmi-assist',#727); +#737=GENERAL_PROPERTY('','user defined attribute',$); +#738=GENERAL_PROPERTY_ASSOCIATION('',$,#737,#736); +#739=REPRESENTATION('external thread',(#729,#730,#731,#732,#733,#734,#735),#632); +#740=PROPERTY_DEFINITION_REPRESENTATION(#736,#739); +#741=DESCRIPTIVE_REPRESENTATION_ITEM('general surface texture','Ra 3.2 \X2\00B5\X0\m unless otherwise stated'); +#742=REPRESENTATION('general surface texture requirement',(#741),#632); +#743=PROPERTY_DEFINITION('manufacturing requirement','general surface texture',#5); +#744=PROPERTY_DEFINITION_REPRESENTATION(#743,#742); +#745=DESCRIPTIVE_REPRESENTATION_ITEM('edge condition','Break sharp edges 0.2 max'); +#746=REPRESENTATION('edge condition requirement',(#745),#632); +#747=PROPERTY_DEFINITION('manufacturing requirement','edge condition',#5); +#748=PROPERTY_DEFINITION_REPRESENTATION(#747,#746); +#749=DESCRIPTIVE_REPRESENTATION_ITEM('general tolerances','ISO 2768-m'); +#750=REPRESENTATION('general tolerances requirement',(#749),#632); +#751=PROPERTY_DEFINITION('manufacturing requirement','general tolerances',#5); +#752=PROPERTY_DEFINITION_REPRESENTATION(#751,#750); +#753=REPRESENTATION_CONTEXT('','default setting'); +#754=DESCRIPTIVE_REPRESENTATION_ITEM('tolerance class','ISO 2768-m'); +#755=REPRESENTATION('default tolerances',(#754),#753); +#756=PROPERTY_DEFINITION('default tolerances','',#4); +#757=PROPERTY_DEFINITION_REPRESENTATION(#756,#755); +#758=COORDINATES_LIST('',17,((1.7686,6.4628,5.0000),(3.2314,6.4628,5.0000),(3.2314,7.9256,5.0000),(1.7686,7.9256,5.0000),(1.7686,6.4628,5.0000),(2.5000,7.6513,5.0000),(2.1517,6.7371,5.0000),(2.5000,7.6513,5.0000),(2.8483,6.7371,5.0000),(2.2823,7.0418,5.0000),(2.7177,7.0418,5.0000),(2.5000,6.4628,5.0000),(2.5000,0.4571,-4.7000),(2.0429,0.0000,-4.7000),(2.9571,0.0000,-4.7000),(2.5000,0.4571,-4.7000),(2.0429,0.0000,-4.7000))); +#759=TESSELLATED_CURVE_SET('',#758,((1,2,3,4,5),(6,7),(8,9),(10,11),(12,13),(14,15,16,17))); +#760=TESSELLATED_GEOMETRIC_SET('datum',(#759)); +#761=TESSELLATED_ANNOTATION_OCCURRENCE('Datum A',(#667),#760); +#762=DRAUGHTING_CALLOUT('Datum A',(#761)); +#763=COORDINATES_LIST('',32,((4.7686,6.4628,5.0000),(6.2314,6.4628,5.0000),(6.2314,7.9256,5.0000),(4.7686,7.9256,5.0000),(4.7686,6.4628,5.0000),(5.2170,7.6513,5.0000),(5.2170,6.7371,5.0000),(5.2170,7.6513,5.0000),(5.6088,7.6513,5.0000),(5.7394,7.6078,5.0000),(5.7830,7.5642,5.0000),(5.8265,7.4772,5.0000),(5.8265,7.3901,5.0000),(5.7830,7.3030,5.0000),(5.7394,7.2595,5.0000),(5.6088,7.2159,5.0000),(5.2170,7.2159,5.0000),(5.6088,7.2159,5.0000),(5.7394,7.1724,5.0000),(5.7830,7.1289,5.0000),(5.8265,7.0418,5.0000),(5.8265,6.9112,5.0000),(5.7830,6.8241,5.0000),(5.7394,6.7806,5.0000),(5.6088,6.7371,5.0000),(5.2170,6.7371,5.0000),(5.5000,6.4628,5.0000),(5.5000,0.4571,-1.5000),(5.0429,-0.0000,-1.5000),(5.9571,-0.0000,-1.5000),(5.5000,0.4571,-1.5000),(5.0429,-0.0000,-1.5000))); +#764=TESSELLATED_CURVE_SET('',#763,((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))); +#765=TESSELLATED_GEOMETRIC_SET('datum',(#764)); +#766=TESSELLATED_ANNOTATION_OCCURRENCE('Datum B',(#667),#765); +#767=DRAUGHTING_CALLOUT('Datum B',(#766)); +#768=DRAUGHTING_MODEL('',(#678,#705,#726,#762,#767),#632); +#769=DRAUGHTING_MODEL_ITEM_ASSOCIATION('PMI representation to presentation link','',#672,#768,#678); +#770=DRAUGHTING_MODEL_ITEM_ASSOCIATION('PMI representation to presentation link','',#699,#768,#705); +#771=DRAUGHTING_MODEL_ITEM_ASSOCIATION('PMI representation to presentation link','',#720,#768,#726); +#772=DRAUGHTING_MODEL_ITEM_ASSOCIATION('PMI representation to presentation link','',#638,#768,#762); +#773=DRAUGHTING_MODEL_ITEM_ASSOCIATION('PMI representation to presentation link','',#642,#768,#767); +ENDSEC; +END-ISO-10303-21; diff --git a/tests/test_build_drawing_entrypoints.py b/tests/test_build_drawing_entrypoints.py index 34cb1bee..c76f3ac0 100644 --- a/tests/test_build_drawing_entrypoints.py +++ b/tests/test_build_drawing_entrypoints.py @@ -67,33 +67,15 @@ def counted_lint(self, *, physical=True): drawing = build_drawing(source) placement = [(owner, state) for owner, physical, state, _ in calls if not physical] - # The settled drawing loses a seed fit advisory without changing its ink. - assert not any(issue.code == "page_fit_uncertain" for issue in drawing.registry.issues) + # Each built state is critiqued once, including the settled drawing. assert all(count == 1 for count in Counter(placement).values()) assert len(placement) <= 5 assert any(owner is drawing and physical for owner, physical, _, _ in calls) assert len(calls) <= 5 - # The repack advisory is the only change to this drawing between its cached - # critique and the final decision. A fresh public critique must agree in order. - current_geometry = ( - tuple( - (name, id(visible), id(hidden)) for name, (visible, hidden) in drawing.views.items() - ), - tuple(id(item) for item in drawing.items), - ) - earlier = [ - issues - for owner, physical, state, issues in calls - if owner is drawing - and not physical - and state[:2] == current_geometry - and any(code == "page_fit_uncertain" for code, _, _ in state[2]) - ] - assert earlier - assert tuple(drawing.lint(physical=False)) == tuple( - issue for issue in earlier[-1] if issue.code != "page_fit_uncertain" - ) + settled = [issues for owner, physical, _, issues in calls if owner is drawing and not physical] + assert settled + assert tuple(drawing.lint(physical=False)) == settled[-1] def test_build_local_placement_critique_keeps_subclass_lint_dispatch_issue_1945(): diff --git a/tests/test_compose_then_pack.py b/tests/test_compose_then_pack.py index 04dd9aff..45b150db 100644 --- a/tests/test_compose_then_pack.py +++ b/tests/test_compose_then_pack.py @@ -49,6 +49,7 @@ def _fake_dwg(self, named, views): return SimpleNamespace( _named=named, _anno_view=views, + views=dict.fromkeys(("front", "plan", "side")), box_cache={}, iter_annotations=lambda: named.items(), get_annotation=lambda n: named.get(n), diff --git a/tests/test_gdt_placement.py b/tests/test_gdt_placement.py index ef67084a..5a4be2d9 100644 --- a/tests/test_gdt_placement.py +++ b/tests/test_gdt_placement.py @@ -8,6 +8,7 @@ placement stays lint-clean. """ +import hashlib import json import math from collections import defaultdict @@ -17,10 +18,12 @@ import ezdxf import pytest from build123d import Box, Cylinder, Draft, Pos -from build123d_drafting import FeatureControlFrame +from build123d_drafting import DatumFeature, FeatureControlFrame, Leader from draftwright.builder import build_drawing, detect_part_model +from draftwright.linting.structural import lint_drawing from draftwright.model.ir import ControlFrame, DatumRef, Finish, Frame, Note, PmiFeature +from draftwright.pmi import extract_pmi_report def _part(): @@ -194,6 +197,85 @@ def test_imported_planar_datum_leader_stays_normal_to_end_face(): assert leader.tip[0] > leader.elbow[0] +def test_grm03_imported_datums_try_local_normal_symbols_issue_2177(): + fixture = Path(__file__).parent / "fixtures" / "grm03_thumbwheel_drive_screw_ap242_pmi.step" + assert hashlib.sha256(fixture.read_bytes()).hexdigest() == ( + "4b6462b9cc9f0d419250933bd77fb305f9cfebb7ec2b3f377008732876010a21" + ) + records = extract_pmi_report(fixture).records + assert {(r.label, r.source_id) for r in records if r.kind == "datum"} == { + ("A", "datum_definition:#777"), + ("B", "datum_definition:#810"), + } + + dwg = build_drawing(fixture, pmi="annotate", page="A4", scale=2.0, scale_policy="permissive") + assert (dwg.page_w, dwg.page_h, dwg.scale) == (297.0, 210.0, 2.0) + assert set(dwg.views) == {"front", "side", "iso", "detail_a"} + a, b = (dwg.get_annotation(name) for name in ("m_gdt0", "m_gdt1")) + assert a.tip[0] == pytest.approx(a.elbow[0]) + assert a.elbow[1] < a.tip[1] + assert b.tip[1] == pytest.approx(b.elbow[1]) + assert b.elbow[0] > b.tip[0] + # A's external symbol fits near the source face. B's normal path is still + # obstructed by the existing front-view dimension ink, so it stays placed + # and critique reports the long shaft rather than silently declaring it tidy. + assert _leader_path_length(a) < 20.0 + assert "m_gdt0" not in { + issue.annotation_name for issue in dwg.lint() if issue.code == "datum_leader_remote" + } + assert "m_gdt1" in { + issue.annotation_name for issue in dwg.lint() if issue.code == "datum_leader_remote" + } + + +def test_grm03_a4_five_to_one_datums_use_projected_whitespace_issue_2177(): + fixture = Path(__file__).parent / "fixtures" / "grm03_thumbwheel_drive_screw_ap242_pmi.step" + # This public explicit-scale build exercises the same geometry as the A4 5:1 + # automatic candidate; scale completeness is a separate #2177 fix. + dwg = build_drawing(fixture, pmi="annotate", page="A4", scale=5.0, scale_policy="permissive") + assert (dwg.page_w, dwg.page_h, dwg.scale) == (297.0, 210.0, 5.0) + a, b = (dwg.get_annotation(name) for name in ("m_gdt0", "m_gdt1")) + assert a.tip[0] == pytest.approx(a.elbow[0]) + assert a.elbow[1] < a.tip[1] + assert b.tip[1] == pytest.approx(b.elbow[1]) + assert b.elbow[0] > b.tip[0] + assert _leader_path_length(a) < 20.0 + assert _leader_path_length(b) < 20.0 + assert not [issue for issue in dwg.lint() if issue.code == "datum_leader_remote"] + + +def test_datum_locality_lint_reads_placed_ink_independently_issue_2177(): + draft = Draft(font_size=3.0) + glyph = DatumFeature("A", draft=draft) + remote = Leader(tip=(40.0, 60.0), elbow=(40.0, 10.0), label="", draft=draft, callout=glyph) + issues = lint_drawing( + [remote], + annotation_datums={id(remote)}, + annotation_names={id(remote): "datum_A"}, + annotation_views={id(remote): "front"}, + ) + assert [(issue.code, issue.annotation_name, issue.view) for issue in issues] == [ + ("datum_leader_remote", "datum_A", "front") + ] + assert not [issue for issue in lint_drawing([remote]) if issue.code == "datum_leader_remote"] + + +def test_grm03_remote_datum_producer_mutation_is_visible_to_lint_issue_2177(monkeypatch): + import draftwright.annotations._gdt as gdt + + original = gdt._gdt_candidate_builders + + def without_local_candidates(*args): + build, build_at, build_routed, _compact, repair = original(*args) + return build, build_at, build_routed, lambda _placed: (), repair + + monkeypatch.setattr(gdt, "_gdt_candidate_builders", without_local_candidates) + fixture = Path(__file__).parent / "fixtures" / "grm03_thumbwheel_drive_screw_ap242_pmi.step" + dwg = build_drawing(fixture, pmi="annotate", page="A4", scale=5.0, scale_policy="permissive") + remote = [issue for issue in dwg.lint() if issue.code == "datum_leader_remote"] + assert {issue.annotation_name for issue in remote} == {"m_gdt0", "m_gdt1"} + + def test_imported_datum_refuses_a_wrong_side_fallback(monkeypatch, tmp_path): import draftwright.annotations._gdt as gdt import draftwright.annotations.from_model as from_model diff --git a/tests/test_hole_annotations.py b/tests/test_hole_annotations.py index 609b6f5b..fc91f6cb 100644 --- a/tests/test_hole_annotations.py +++ b/tests/test_hole_annotations.py @@ -266,6 +266,52 @@ def test_sheet_is_lint_clean(self, plate_drawing): issues = [i for i in plate_drawing.lint() if i.severity != "info"] assert [i.code for i in issues] == [] + def test_broad_plate_edge_labels_are_not_narrow_material_issue_2177( + self, plate_drawing, monkeypatch + ): + import draftwright._geometry as geometry + + names = {"hc_plan2", "hc_side0"} + centre_by_field = {} + for name in names: + annotation = plate_drawing.get_annotation(name) + view = plate_drawing.view_of(name) + field = plate_drawing.material_fields()[id(plate_drawing.views[view][0])] + assert plate_drawing.registry.candidate_region_of(name).value == "interior" + assert annotation.label_bbox is not None + centre_by_field[id(field)] = (annotation.label_bbox[1] + annotation.label_bbox[3]) / 2 + assert not geometry.label_on_narrow_material(annotation.label_bbox, field) + assert not names & { + issue.annotation_name + for issue in plate_drawing.lint() + if issue.code == "interior_label_on_narrow_material" + } + + # Disable just the local-band probe, preserving the old edge and label + # probes. Both real callouts must then fail the independent lint check. + material_intervals = geometry.material_intervals + substituted = [] + + def no_local_band(p, q, field, *, bridge=0.0): + if p[0] == q[0] and p[1] == field.box[1] and q[1] == field.box[3]: + substituted.append((p, q)) + tick = round( + (centre_by_field[id(field)] - p[1]) + / (q[1] - p[1]) + * geometry._MATERIAL_SPAN_TICKS + ) + return ((tick - 1, tick + 1),) + return material_intervals(p, q, field, bridge=bridge) + + monkeypatch.setattr(geometry, "material_intervals", no_local_band) + flagged = { + issue.annotation_name + for issue in plate_drawing.lint() + if issue.code == "interior_label_on_narrow_material" + } + assert substituted + assert names <= flagged + @pytest.mark.timeout(60) def test_bore_callout_stays_clear_of_the_outline_without_section_line(self): # When no section line is placed (no cbore/spotface/blind holes) the diff --git a/tests/test_interior_label_placement.py b/tests/test_interior_label_placement.py index 1f6b8318..c950f041 100644 --- a/tests/test_interior_label_placement.py +++ b/tests/test_interior_label_placement.py @@ -10,12 +10,129 @@ import pytest from draftwright import build_drawing +from draftwright._geometry import label_on_narrow_material, material_field from draftwright.annotations import _common from draftwright.linting.quality import is_hard_layout_issue from draftwright.model.compiled import compile_dimensions from draftwright.registry import DimensionPlacementSpec, PlacedDimension _CTC01_AP203 = Path(__file__).parent / "fixtures" / "nist_ctc_01_asme1_ap203.stp" +_GRM03_PMI = Path(__file__).parent / "fixtures" / "grm03_thumbwheel_drive_screw_ap242_pmi.step" + + +def _rectangle_field(width, height): + return material_field( + ( + ((0, 0), (width, 0), (width, height)), + ((0, 0), (width, height), (0, height)), + ) + ) + + +def test_interior_text_near_a_thin_material_edge_has_no_clear_margin(): + narrow = _rectangle_field(30, 5.5) + upright = _rectangle_field(5, 30) + broad = _rectangle_field(30, 30) + label = (10, 3.5, 17, 5.5) + + assert label_on_narrow_material(label, narrow) + assert label_on_narrow_material((2.5, 10, 4.5, 17), upright) + assert not label_on_narrow_material((10, 10, 17, 12), broad) + assert not label_on_narrow_material((10, 15, 17, 17), broad) + assert not label_on_narrow_material((0.1, 10, 7.1, 12), broad) + assert not label_on_narrow_material((31, 3.5, 38, 5.5), narrow) + + +def test_disconnected_material_cannot_hide_a_narrow_label_band_issue_2177(monkeypatch): + import draftwright._geometry as geometry + + label = (10, 3.5, 17, 5.5) + first = _rectangle_field(30, 5.5) + islands = material_field( + first.triangles + + ( + ((0, 10), (30, 10), (30, 15.5)), + ((0, 10), (30, 15.5), (0, 15.5)), + ) + ) + p, q = (13.5, 0), (13.5, 15.5) + intervals = geometry.material_intervals(p, q, islands) + assert len(intervals) == 2 + assert geometry.material_span(p, q, islands) > 3 * (label[3] - label[1]) + assert geometry.label_on_narrow_material(label, first) + assert geometry.label_on_narrow_material(label, islands) + + # Merge the disconnected intervals as if the second strip were attached. + # That deliberate substitution must change the verdict for this same label. + original = geometry.material_intervals + substituted = [] + + def merged_intervals(start, end, field, *, bridge=0.0): + found = original(start, end, field, bridge=bridge) + if start == p and end == q and field is islands: + substituted.append(found) + return ((found[0][0], found[-1][1]),) + return found + + monkeypatch.setattr(geometry, "material_intervals", merged_intervals) + assert not geometry.label_on_narrow_material(label, islands) + assert substituted == [intervals] + + +def test_grm03_chamfer_label_clears_narrow_shaft_issue_2177(): + # The explicit former scale remains a public-path witness for the defect; + # lint must state it even when the caller deliberately keeps that scale. + before = build_drawing( + _GRM03_PMI, pmi="annotate", out=None, page="A4", scale=2, scale_policy="permissive" + ) + name = "m_chamfer_x1" + before_label = before.get_annotation(name) + before_field = before.material_fields()[id(before.views[before.view_of(name)][0])] + assert before.registry.candidate_region_of(name).value == "interior" + assert before_label.label == "C0.5" + assert label_on_narrow_material(before_label.label_bbox, before_field) + assert [ + (issue.code, issue.severity, issue.annotation_name) + for issue in before.lint() + if issue.severity != "info" + ] == [ + ("datum_leader_remote", "warning", "m_gdt1"), + ("interior_label_on_narrow_material", "warning", name), + ] + before.registry._anno_candidate_region.pop(name) + assert before.registry.candidate_region_of(name) is None + assert before.get_annotation(name) is before_label + assert [ + (issue.code, issue.severity, issue.annotation_name) + for issue in before.lint() + if issue.severity != "info" + ] == [ + ("datum_leader_remote", "warning", "m_gdt1"), + ("interior_label_on_narrow_material", "warning", name), + ] + before.registry._anno_view.pop(name) + assert [ + (issue.code, issue.severity, issue.annotation_name) + for issue in before.lint() + if issue.severity != "info" + ] == [ + ("datum_leader_remote", "warning", "m_gdt1"), + ("interior_label_on_narrow_material", "warning", name), + ] + before_requirements = before.report()["recognition"]["requirements"] + assert len(before_requirements) == 17 + assert {requirement["state"] for requirement in before_requirements} == {"placed"} + + after = build_drawing(_GRM03_PMI, pmi="annotate", out=None) + assert (after.page_w, after.page_h, after.scale) == (297.0, 210.0, 5.0) + after_label = after.get_annotation(name) + after_field = after.material_fields()[id(after.views[after.view_of(name)][0])] + assert after_label.label == "C0.5" + assert not label_on_narrow_material(after_label.label_bbox, after_field) + assert not [issue for issue in after.lint() if issue.severity != "info"] + after_requirements = after.report()["recognition"]["requirements"] + assert len(after_requirements) == 17 + assert {requirement["state"] for requirement in after_requirements} == {"placed"} @pytest.fixture(scope="module", params=("A2", "A3")) @@ -77,6 +194,7 @@ def test_ctc01_feature_families_share_one_joint_assignment(ctc01_without_pmi): issues = drawing.lint() assert not [issue for issue in issues if is_hard_layout_issue(issue)] assert not [issue for issue in issues if issue.code == "leader_crosses_silhouette"] + assert not [issue for issue in issues if issue.code == "interior_label_on_narrow_material"] def test_ctc01_a3_keeps_required_dimensions_when_exterior_space_is_available( diff --git a/tests/test_issue_1130_view_planning_evidence.py b/tests/test_issue_1130_view_planning_evidence.py index 85a67ceb..0480cf81 100644 --- a/tests/test_issue_1130_view_planning_evidence.py +++ b/tests/test_issue_1130_view_planning_evidence.py @@ -1,4 +1,4 @@ -"""ADR 2 (was 0018)'s motivating failure, reproduced from a synthetic part. +"""ADR 2 (was 0018)'s thin-plate view-planning case, reproduced from a synthetic part. The ADR was proposed from a user-supplied `worm_planetary_concept_Alimacznicy.step` that this repository does not have, and its first required-evidence item is: @@ -6,17 +6,9 @@ A synthetic thin rotational plate reproduces the A1/fixed-four-view failure without relying on a proprietary or externally supplied STEP file. -This is that fixture. It exists so every later slice of #1130 is measured against something the -repository owns, and so the claim "the fixed four-view topology forces the sheet" is a number -here rather than a recollection of someone else's file. - -**These tests pin the safety counterexample, deliberately.** Nothing here is a defect report -against the packer — given four views the engine's choice is correct, and its refusal to fit A2 -is honest. Automatic selection now tries the smaller profile + end-view set, but this fixture -loses required slot annotations under that layout, so the finished-drawing gate retains the -full topology. - -A slice that changes these numbers is doing ADR 2 (was 0018)'s work, and must update them and say so. +This fixture measures both plans: retaining three principal views uses A1 at 1:1, while automatic +selection omits the redundant plan view and fits A2 at 1:1 with required outcomes retained. +The drawing and its independent lint must agree on that result. """ from __future__ import annotations @@ -94,33 +86,32 @@ def automatic(): return build_drawing(thin_rotational_plate(), title="T", number="N") -class TestTheFixedTopologyForcesTheSheet: - def test_the_automatic_result_is_a1_at_full_scale_and_reports_no_problem(self, automatic): - """The ADR's headline: A1 landscape at 1:1 for a part 43 mm thick. - - The sheet is the ADR's subject and is unchanged because the automatic reduced candidate - loses required outcomes. #1250's completeness gate keeps that loss explicit. - """ +class TestTheReducedPlanFitsTheSheet: + def test_the_automatic_result_is_a2_at_full_scale_and_reports_no_problem(self, automatic): + """A redundant plan view can yield while required outcomes survive on A2.""" drawing = automatic - assert (drawing.page_w, drawing.page_h) == (841.0, 594.0), "not A1 landscape" + assert (drawing.page_w, drawing.page_h) == (594.0, 420.0), "not A2 landscape" assert drawing.scale == 1.0 - assert set(drawing.views) == {"front", "plan", "side", "iso"} - assert drawing.view_decision["status"] == "retained_after_rejection" - # Interior recovery now preserves the previously dropped outcomes on this same - # proposal, so the automatic path truthfully reports success. + assert set(drawing.views) == {"front", "side", "iso"} + assert drawing.view_decision["status"] == "reduced" + assert drawing.view_decision["chosen"] == ("front", "side") assert not [i for i in drawing.lint() if i.severity == "error"] assert drawing.lint_summary()["passed"] is True - def test_the_plan_view_repeats_the_front_and_carries_almost_nothing(self, automatic): - """WHY it is the wrong sheet, not just that it is a big one. + def test_the_omitted_plan_repeats_the_front_and_carries_almost_nothing(self): + """Retaining all views exposes the redundant projection the planner omits. On an X-axis rotational part the front and plan are both edge-on: same silhouette, same extent. One of them is a second look at the same thing, and the annotations show which — - the disc face (side) carries the hole patterns and diameters, the front carries the - axial dimensions, and the plan carries almost nothing while occupying 217 mm. + the disc face (side) carries the hole patterns and diameters, the front carries axial + dimensions, and the plan carries almost nothing. """ - drawing = automatic + drawing = build_drawing( + thin_rotational_plate(), title="T", number="N", _views=("front", "plan", "side") + ) + assert (drawing.page_w, drawing.page_h) == (841.0, 594.0) + assert drawing.scale == 1.0 front = drawing.view_bounds("front") plan = drawing.view_bounds("plan") side = drawing.view_bounds("side") @@ -147,32 +138,11 @@ def test_the_plan_view_repeats_the_front_and_carries_almost_nothing(self, automa ) def test_the_automatic_sheet_agrees_with_the_explicit_engine_verdict(self, automatic): - """The sharp end of the evidence, and the defect #1250 fixed. - - Before #1250 the automatic build chose A1 at 1:1 and reported `passed: True` with no - lint errors. Asking for that SAME page and scale explicitly made the engine refuse — - "requested scale 1 cannot preserve required annotations". Same part, same sheet, same - scale, two verdicts, decided by how the caller phrased the request: the explicit path - ran `_scale_blockers` and the automatic path did not. - - The blockers are real, not an artefact of the stricter path: the automatic drawing - still carries `slot_dim_dropped` and `hole_requirement_missing`, so it IS the - incomplete drawing the explicit gate exists to prevent. - - ADR 2 (was 0018)'s evidence list requires: "A forced small sheet/large scale that drops a - requirement is rejected, not accepted with a warning-only incomplete drawing." The - automatic path now runs the same gate and reports the settled drawing's loss at error - severity. Candidate search remains the joint planner's responsibility (#1262), because - partial registry provenance cannot prove that a rebuilt candidate preserves everything. - - The first version of this test asserted that A2 at 1:1 raises, and read that as the - four-view topology forcing the sheet. It does raise — but so does A1, so the assertion - demonstrated this inconsistency rather than the sheet cost it claimed. The mutation that - found it changed `page="A2"` to `page="A1"` and the test still passed. - """ + """Automatic and explicit A2 at 1:1 agree on the complete selected plan.""" assert automatic.lint_summary()["passed"] is True explicit = build_drawing( - thin_rotational_plate(), page="A1", scale=1.0, title="T", number="N" + thin_rotational_plate(), page="A2", scale=1.0, title="T", number="N" ) assert explicit.lint_summary()["passed"] is True + assert set(explicit.views) == set(automatic.views) assert not [issue for issue in explicit.lint() if issue.code.endswith("_dropped")] diff --git a/tests/test_issue_1155_grm04_sheet_use.py b/tests/test_issue_1155_grm04_sheet_use.py index 5c8ffb84..986efc36 100644 --- a/tests/test_issue_1155_grm04_sheet_use.py +++ b/tests/test_issue_1155_grm04_sheet_use.py @@ -15,6 +15,9 @@ def test_grm04_measured_replan_keeps_diameter_and_location_on_a_clean_sheet(): assert (drawing.page_w, drawing.page_h) == (297.0, 210.0) assert drawing.scale == 2.0 assert drawing.scale_decision["status"] == "automatic" + detail_bottom = drawing.view_bounds("detail_a")[1] + caption_top = drawing.get_annotation("detail_caption_A").label_bbox[3] + assert 0 <= detail_bottom - caption_top <= 13.0 assert [ (item["scale"], item["status"], item.get("rejection")) for item in drawing.scale_decision["attempts"] diff --git a/tests/test_issue_1262_automatic_turned_views.py b/tests/test_issue_1262_automatic_turned_views.py index 83b6ce20..7e900638 100644 --- a/tests/test_issue_1262_automatic_turned_views.py +++ b/tests/test_issue_1262_automatic_turned_views.py @@ -66,7 +66,7 @@ def test_a_radial_feature_vetoes_reduction_before_any_annotation_can_disappear() cross_hole = Pos(0, 0, -15) * Cylinder(3, 30) drawing = build_drawing(shaft - cross_hole, title="CROSS DRILLED SHAFT", number="2") - assert tuple(drawing.views) == ("front", "plan", "side", "iso") + assert {"front", "plan", "side", "iso"}.issubset(drawing.views) assert drawing.view_decision["status"] == "retained_for_requirements" attempt = drawing.view_decision["attempts"][0] assert attempt["status"] == "rejected" diff --git a/tests/test_issue_1299_page_escalation.py b/tests/test_issue_1299_page_escalation.py index d6caeced..79e33e34 100644 --- a/tests/test_issue_1299_page_escalation.py +++ b/tests/test_issue_1299_page_escalation.py @@ -40,19 +40,18 @@ def _five_step_grm_profile(): return Rotation(0, 90, 0) * shaft -def test_crowded_head_uses_a_detail_before_spending_the_sheet(): - # The current crop guard keeps the short head stations in an enlarged detail, - # allowing the complete drawing to remain on A4. The 2.5 main-view dimension - # locates the whole head; its separate 0.5 and 2 lengths are in detail_a. +def test_crowded_head_uses_larger_scale_before_spending_the_sheet(): + # The settled 5:1 candidate keeps all shoulder lengths in the main view + # on A4, without needing a cropped detail. drawing = build_estimated_strips(_five_step_grm_profile(), pmi="off") - assert (drawing.page_w, drawing.page_h, drawing.scale) == (297.0, 210.0, 2.0) - assert {"iso", "detail_a"} <= drawing.views.keys() - assert drawing.scale_decision["status"] == "automatic" + assert (drawing.page_w, drawing.page_h, drawing.scale) == (297.0, 210.0, 5.0) + assert "iso" in drawing.views and "detail_a" not in drawing.views + assert drawing.scale_decision["status"] == "automatic_replanned" assert all(attempt["page"] == (297.0, 210.0) for attempt in drawing.scale_decision["attempts"]) assert { drawing.get_annotation(name).label for name in drawing.annotations() if "steplen" in name - } == {"3.2", "2.5", "0.5", "2", "3", "20"} + } == {"3.2", "0.5", "2", "3", "20"} assert not [ issue for issue in drawing.lint() @@ -949,13 +948,13 @@ def test_explicit_a4_remains_fixed_instead_of_escalating(): assert (drawing.page_w, drawing.page_h) == (297.0, 210.0) assert all(attempt["page"] == (297.0, 210.0) for attempt in drawing.scale_decision["attempts"]) - assert drawing.scale_decision["status"] == "automatic" - assert drawing.scale == 2.0 - assert {"iso", "detail_a"} <= drawing.views.keys() + assert drawing.scale_decision["status"] == "automatic_replanned" + assert drawing.scale == 5.0 + assert "iso" in drawing.views and "detail_a" not in drawing.views assert not drawing.lint() assert { drawing.get_annotation(name).label for name in drawing.annotations() if "steplen" in name - } == {"3.2", "2.5", "0.5", "2", "3", "20"} + } == {"3.2", "0.5", "2", "3", "20"} def test_exact_grm03_recovers_all_axial_stations_on_a4_with_pmi_off(): diff --git a/tests/test_issue_1338_scale_before_page_escalation.py b/tests/test_issue_1338_scale_before_page_escalation.py index 7b6a27e2..083a9011 100644 --- a/tests/test_issue_1338_scale_before_page_escalation.py +++ b/tests/test_issue_1338_scale_before_page_escalation.py @@ -8,16 +8,25 @@ from __future__ import annotations +import hashlib import warnings from pathlib import Path +from types import SimpleNamespace import pytest import draftwright.builder as builder from draftwright import build_drawing, observe_build +from draftwright.annotations._common import _geom_box +from draftwright.annotations._leader_fixed_ink import _annotation_fixed_ink +from draftwright.annotations._placement_occupancy import label_clears_foreign_annotations +from draftwright.pmi import extract_pmi_report FIXTURE = Path(__file__).parent / "fixtures" / "grm03_thumbwheel_drive_screw_ap242_pmi.step" A4 = (297.0, 210.0) +FIXTURE_SHA256 = "4b6462b9cc9f0d419250933bd77fb305f9cfebb7ec2b3f377008732876010a21" +SPECIFY_PMI_FIXTURE = Path(__file__).parent / "fixtures/grm03_specify_pmi_ap242.step" +SPECIFY_PMI_SHA256 = "0e0d9c1f2f81b1181b6039a2254740290a724bf200cfa936f7bf28986b59cf41" def _requirement_failures(drawing): @@ -34,6 +43,150 @@ def _requirement_failures(drawing): ] +def _assert_grm03_pmi_hole_source(): + assert hashlib.sha256(FIXTURE.read_bytes()).hexdigest() == FIXTURE_SHA256 + report = extract_pmi_report(FIXTURE) + assert any(record.part21_id == "#2004" for record in report.records) + + +def test_ap242_pmi_side_hole_survives_selected_page_upscale_issue_2177(): + _assert_grm03_pmi_hole_source() + + drawing = build_drawing(FIXTURE, pmi="annotate", out=None) + + assert (drawing.page_w, drawing.page_h, drawing.scale) == (*A4, 5.0) + assert set(drawing.views) == {"front", "side", "iso"} + assert drawing.scale_decision["status"] == "automatic_replanned" + assert drawing.scale_decision["attempts"][-1]["status"] == "complete" + hole = drawing.get_annotation("hc_side0") + assert "⌀1.6" in hole.label and "MFG 2" in hole.label + assert not [issue for issue in drawing.lint() if issue.severity in {"warning", "error"}] + + table = drawing.get_annotation("manufacturing_requirements") + (reserved,) = _annotation_fixed_ink(drawing, "manufacturing_requirements", table) + table_box = _geom_box(table) + assert reserved.box == table_box + assert reserved.kind == "Table" + assert hole.label_bbox is not None and table_box is not None + assert label_clears_foreign_annotations( + hole.label_bbox, ((table_box, False),), drawing.draft.pad_around_text + ) + + +def test_explicit_a4_2_to_1_pmi_keeps_required_hole_after_measured_repack_issue_2177(): + _assert_grm03_pmi_hole_source() + + drawing = build_drawing(FIXTURE, pmi="annotate", page="A4", scale=2.0) + + assert (drawing.page_w, drawing.page_h, drawing.scale) == (*A4, 2.0) + assert drawing.scale_decision["status"] == "honored" + assert "⌀1.6" in drawing.get_annotation("hc_side0").label + assert { + (issue.code, issue.annotation_name) + for issue in drawing.lint() + if issue.severity in {"warning", "error"} + } == { + ("datum_leader_remote", "m_gdt1"), + ("interior_label_on_narrow_material", "m_chamfer_x1"), + } + + +def test_table_repack_trigger_ignores_unrelated_and_clean_outcomes_issue_2177(): + table = SimpleNamespace(table_rows=(("REF", "REQUIREMENT"),)) + drawing = SimpleNamespace( + registry=SimpleNamespace(issues=[]), + iter_annotations=lambda: iter((("schedule", table),)), + ) + assert not builder._source_placement_drop_with_table(drawing) + + issue = SimpleNamespace( + code="pmi_dropped", source_ids=("source:1",), outcome_stage="validation" + ) + drawing.registry.issues = [issue] + assert not builder._source_placement_drop_with_table(drawing) + + issue.outcome_stage = "placement" + issue.source_ids = () + assert not builder._source_placement_drop_with_table(drawing) + + issue.source_ids = ("source:1",) + drawing.iter_annotations = lambda: iter(()) + assert not builder._source_placement_drop_with_table(drawing) + drawing.iter_annotations = lambda: iter((("schedule", table),)) + assert builder._source_placement_drop_with_table(drawing) + + +def test_unselected_view_geometry_cannot_force_repack_issue_2177(monkeypatch): + # Analysis retains candidate plan geometry even when the settled drawing + # contains only front and side. A front label there is not a clash. + monkeypatch.setattr( + builder, + "_view_geom", + lambda _analysis: {"front": (0, 0, 5, 5), "plan": (20, 20, 5, 5)}, + ) + monkeypatch.setattr( + builder, + "_attribute_annotations", + lambda _drawing: iter((("front_label", "front", (19, 19, 21, 21), True),)), + ) + drawing = SimpleNamespace( + views={"front": object()}, draft=SimpleNamespace(pad_around_text=0.5) + ) + assert builder._annotation_view_overlaps(drawing, object()) == 0 + drawing.views["plan"] = object() + assert builder._annotation_view_overlaps(drawing, object()) == 1 + + +def test_side_hole_label_clears_neighbour_view_text_issue_2177(): + label = (10.0, 10.0, 20.0, 12.0) + assert not label_clears_foreign_annotations(label, (((20.8, 10.0, 25.0, 12.0), True),), 0.5) + assert label_clears_foreign_annotations(label, (((30.0, 10.0, 35.0, 12.0), True),), 0.5) + + +def test_specify_pmi_detail_caption_clears_required_hole_text_issue_2177(): + assert hashlib.sha256(SPECIFY_PMI_FIXTURE.read_bytes()).hexdigest() == SPECIFY_PMI_SHA256 + source = extract_pmi_report(SPECIFY_PMI_FIXTURE) + assert any( + record.part21_id == "#670" and record.kind == "internal_thread" + for record in source.records + ) + + drawing = build_drawing(SPECIFY_PMI_FIXTURE, pmi="annotate", page="A4", scale=2.0) + assert (drawing.page_w, drawing.page_h, drawing.scale) == (*A4, 2.0) + assert drawing.scale_decision["status"] == "honored" + hole = drawing.get_annotation("hc_side0") + caption = drawing.get_annotation("detail_caption_A") + assert "⌀1.6 ↧ 3.8 SEE MFG 1" == hole.label + assert caption.label.startswith("DETAIL A") + assert "detail_a" in drawing.views + hole_box, caption_box = hole.label_bbox, caption.label_bbox + assert hole_box is not None and caption_box is not None + x_gap = max(hole_box[0] - caption_box[2], caption_box[0] - hole_box[2], 0.0) + y_gap = max(hole_box[1] - caption_box[3], caption_box[1] - hole_box[3], 0.0) + assert max(x_gap, y_gap) >= drawing.draft.font_size / 3 + assert not [issue for issue in drawing.lint() if issue.code == "detail_caption_clearance"] + + # Deliberately move the caption next to the callout through the public edit verb. + target_left = hole_box[2] + 0.25 + target_bottom = hole_box[1] - 0.25 + drawing.note( + caption.label, + at=( + target_left + (caption_box[2] - caption_box[0]) / 2, + target_bottom + (caption_box[3] - caption_box[1]) / 2, + ), + name="detail_caption_A", + ) + mutated = drawing.get_annotation("detail_caption_A") + assert mutated is not caption and mutated.label_bbox != caption_box + assert mutated.label_bbox[0] - hole_box[2] < drawing.draft.font_size / 3 + assert { + (issue.code, issue.annotation_name, issue.related_annotation_names) + for issue in drawing.lint() + if issue.code == "detail_caption_clearance" + } == {("detail_caption_clearance", "detail_caption_A", ("hc_side0",))} + + def test_first_selected_scale_uses_a_detail_for_short_shoulders(): # The 2:1 A4 drawing now preserves the short shoulders in a defining detail. # The optional measured upscale below may replace that detail, not rescue a drop. diff --git a/tests/test_issue_1396_layout_advisories.py b/tests/test_issue_1396_layout_advisories.py index adacb617..0a1fa0f9 100644 --- a/tests/test_issue_1396_layout_advisories.py +++ b/tests/test_issue_1396_layout_advisories.py @@ -113,14 +113,17 @@ def test_computed_scale_is_reported_by_scale_selection(): def test_measured_fit_retracts_seed_uncertainty(): - from draftwright.builder import _repack + from draftwright.builder import _PlacementCritique, _repack a = _analyse(Box(20, 20, 20), title="", number="", tolerance="", drawn_by="", out="") a = replace(a, layout_advisories=(("page_fit_uncertain", "fixture seed fit failure"),)) drawing = _assemble(a, "", None, None, auto_dims=False) assert "page_fit_uncertain" in _codes(drawing) - assert _repack(a, drawing, "", None, None) is None + critique = _PlacementCritique() + assert any(issue.code == "page_fit_uncertain" for issue in critique.get(drawing)) + assert _repack(a, drawing, "", None, None, placement_critique=critique) is None assert "page_fit_uncertain" not in _codes(drawing) + assert critique.get(drawing) == tuple(drawing.lint(physical=False)) def test_computed_scale_reaches_public_declared_lint(): diff --git a/tests/test_layout_safety.py b/tests/test_layout_safety.py index c94f27dc..2d01ba10 100644 --- a/tests/test_layout_safety.py +++ b/tests/test_layout_safety.py @@ -124,6 +124,21 @@ def test_missing_requirement_and_overlap_are_independent_failures(): assert "lint_blockers" in verdict["failed_checks"] +def test_detail_caption_clearance_warning_blocks_candidate_issue_2177(): + report = _raw_report( + total=1, + requirements=({"state": "placed"},), + issues=({"code": "detail_caption_clearance", "severity": "warning"},), + ) + + verdict = candidate_safety_evidence(DrawingStub(report)) + + assert verdict["failed_checks"] == ["lint_blockers"] + assert next( + check["detail"] for check in verdict["checks"] if check["name"] == "lint_blockers" + ) == ["detail_caption_clearance"] + + @pytest.mark.parametrize( ("annotations", "reason"), [ diff --git a/tests/test_pmi.py b/tests/test_pmi.py index 10b8d2de..d9a0d113 100644 --- a/tests/test_pmi.py +++ b/tests/test_pmi.py @@ -1843,7 +1843,7 @@ def fail_one( @pytest.fixture(scope="module") def ctc01_annotated(tmp_path_factory): """One ``pmi='annotate'`` build of CTC-01, shared **read-only** across the - annotate assertions below — each used to rebuild the ~18 s CTC AP242 import + + annotate assertions below — each used to rebuild the expensive CTC AP242 import + annotate just to check a different read-only property (#153). Any test that MUTATES the drawing (add/remove/pin/repair/export-to-a-new-path) must build its own, not use this fixture.""" @@ -1866,6 +1866,9 @@ def _single_source_dimension_drawing(**opts): return sheet.build() +# The shared CTC-01 fixture already approached the global 300-second test timeout +# on unchanged main under Python 3.11 CI. Keep a bounded setup margin (#2166). +@pytest.mark.timeout(360) class TestBuildDrawingPmi: def test_ctc01_reconciled_pmi_keeps_automatic_a3_sheet(self, ctc01_annotated): drawing = ctc01_annotated diff --git a/tests/test_tier_manifest.py b/tests/test_tier_manifest.py index ed656ccb..896f0c2f 100644 --- a/tests/test_tier_manifest.py +++ b/tests/test_tier_manifest.py @@ -77,6 +77,44 @@ def test_finish_source_owner_routes_manufacturing_contract(source): assert "test_pmi_manufacturing_finish.py" in pr_modules(_TESTS, [source]) +@pytest.mark.parametrize( + ("source", "contracts"), + ( + ( + "src/draftwright/linting/structural.py", + {"test_interior_label_placement.py", "test_gdt_placement.py"}, + ), + ( + "src/draftwright/linting/orchestration.py", + {"test_interior_label_placement.py", "test_gdt_placement.py"}, + ), + ( + "src/draftwright/builder.py", + {"test_issue_1338_scale_before_page_escalation.py"}, + ), + ( + "src/draftwright/annotations/holes.py", + {"test_issue_1338_scale_before_page_escalation.py"}, + ), + ( + "src/draftwright/annotations/_placement_occupancy.py", + {"test_issue_1338_scale_before_page_escalation.py"}, + ), + ( + "src/draftwright/annotations/_leader_fixed_ink.py", + {"test_issue_1338_scale_before_page_escalation.py"}, + ), + ( + "src/draftwright/annotations/_height_ladder.py", + {"test_issue_1299_page_escalation.py"}, + ), + ), +) +def test_layout_issue_2177_source_owners_route_public_contracts(source, contracts): + assert (_TESTS.parent / source).is_file() + assert contracts <= set(pr_modules(_TESTS, [source])) + + def test_every_fast_module_has_a_pr_route(): available = {path.name for path in _TESTS.glob("test_*.py")} routed = set(PR_CORE_MODULES) | set(PR_POLICY_MODULES) | set(UNIT_MODULES) diff --git a/tests/test_view_plan.py b/tests/test_view_plan.py index 720a1ed7..7471934a 100644 --- a/tests/test_view_plan.py +++ b/tests/test_view_plan.py @@ -298,17 +298,18 @@ class TestPerViewRequirementCoverage: """ def test_a_rotational_plate_has_one_view_carrying_nothing_of_its_own(self): - """The redundant case, and the reason the thin plate needs an A1. + """The redundant case, measured with all three principal views retained. On an X-axis rotational part the front and plan are the same edge-on projection. The - engine draws both because the topology is fixed, and the plan ends up carrying no - measurement at all — 217 mm of sheet for a repeat of its neighbour. + plan carries no measurement of its own and is a candidate for automatic removal. """ from test_issue_1130_view_planning_evidence import thin_rotational_plate from draftwright.view_plan import view_coverage, views_carrying_nothing_exclusively - drawing = build_drawing(thin_rotational_plate(), title="T", number="N") + drawing = build_drawing( + thin_rotational_plate(), title="T", number="N", _views=("front", "plan", "side") + ) coverage = view_coverage(drawing) assert views_carrying_nothing_exclusively(drawing) == ("plan",) @@ -397,7 +398,9 @@ def test_a_turned_part_with_radial_features_keeps_its_end_view(self): from draftwright.view_plan import view_coverage, views_carrying_nothing_exclusively - drawing = build_drawing(thin_rotational_plate(), title="T", number="N") + drawing = build_drawing( + thin_rotational_plate(), title="T", number="N", _views=("front", "plan", "side") + ) coverage = view_coverage(drawing) assert views_carrying_nothing_exclusively(drawing) == ("plan",)