diff --git a/src/draftwright/_geometry.py b/src/draftwright/_geometry.py index 3e83157b..e5829041 100644 --- a/src/draftwright/_geometry.py +++ b/src/draftwright/_geometry.py @@ -261,6 +261,18 @@ def _radial_axis_in_view(axis: str, view: str) -> str: raise ValueError(f"no radial axis for shaft axis {axis!r} in view {view!r}") from e +def _cylindrical_finish_site(reference): + """Preferred visible site for one external orthographic cylinder finish.""" + axis = reference.principal_axis.lower() + if reference.sense != "external" or axis not in _EDGE_ON: + return None + view = _EDGE_ON[axis] + radial = _radial_axis_in_view(axis, view) + site = list(reference.midpoint) + site["xyz".index(radial)] += reference.radius + return (tuple(site), view, "above" if radial == "z" else "right") + + def _canonical_profile_site(site, centre, axis: str, view: str) -> tuple[float, float, float]: """Rotate a turned surface *site* about its shaft onto the selected profile plane. diff --git a/src/draftwright/annotations/_gdt.py b/src/draftwright/annotations/_gdt.py index c97874c5..f7835a17 100644 --- a/src/draftwright/annotations/_gdt.py +++ b/src/draftwright/annotations/_gdt.py @@ -287,9 +287,48 @@ def _gdt_visual_tolerance(glyph, draft, zone: str, *, modifier: bool = False) -> return "" +def _gdt_visual_finish(glyph, draft, value: str) -> str: + """Trust a finish value only when its glyph faces spell that value.""" + try: + expected = Text( + value, + font_size=draft.font_size, + font=getattr(draft, "font", "Arial"), + font_path=getattr(draft, "font_path", DEFAULT_FONT_PATH), + align=(Align.MIN, Align.MIN), + mode=Mode.PRIVATE, + ) + x0, y0, x1, y1 = glyph.label_bbox + box = expected.bounding_box() + expected = expected.moved(Location(Vector(x0 - box.min.X, y0 - box.min.Y, 0))) + tolerance = max(1e-3, draft.font_size * 1e-3) + faces = [ + face + for face in glyph.faces() + if (face_box := face.bounding_box()).min.X >= x0 - tolerance + and face_box.max.X <= x1 + tolerance + and face_box.min.Y >= y0 - tolerance + and face_box.max.Y <= y1 + tolerance + ] + if not faces: + return "" + actual = Sketch(children=faces) + area_tolerance = max(1e-8, draft.font_size**2 * 1e-8) + if ( + _cut_area(expected.cut(actual)) <= area_tolerance + and _cut_area(actual.cut(expected)) <= area_tolerance + ): + return value + except Exception: + return "" + return "" + + def _attach_gdt_text_evidence(leader, glyph, item, draft) -> None: """Keep the placed glyph's value beside PDF text for independent PMI lint.""" leader.pdf_text_relative_specs = _gdt_pdf_text_specs(glyph, item, draft) + if item.kind == "finish": + leader.gdt_visual_finish = _gdt_visual_finish(glyph, draft, item.ra) if item.kind == "control_frame": zone = _gdt_visual_zone(glyph, draft) leader.gdt_visual_tolerance = _gdt_visual_tolerance( diff --git a/src/draftwright/linting/orchestration.py b/src/draftwright/linting/orchestration.py index e7a791f0..80ba282d 100644 --- a/src/draftwright/linting/orchestration.py +++ b/src/draftwright/linting/orchestration.py @@ -629,6 +629,7 @@ def _lint_pmi(ctx: LintContext) -> list: ctx.analysis.pmi_mode, decorations=getattr(ctx.model, "decorations", {}), report=ctx.analysis.pmi_report, + drawing=ctx.drawing, overridden_general_tolerance=( ctx.analysis.tolerance is not None and ctx.build.general_tolerance_source is None ), diff --git a/src/draftwright/linting/pmi_coverage.py b/src/draftwright/linting/pmi_coverage.py index dee2439a..78d146dc 100644 --- a/src/draftwright/linting/pmi_coverage.py +++ b/src/draftwright/linting/pmi_coverage.py @@ -5,15 +5,22 @@ import re from collections import Counter from decimal import Decimal, InvalidOperation +from math import dist +from types import SimpleNamespace from typing import Literal from build123d import Align, Location, Mode, Sketch, Text -from draftwright._geometry import _fmt_pmi_magnitude +from draftwright._core import _font_safe_text, _table_metrics +from draftwright._geometry import _cylindrical_finish_site, _fmt_pmi_magnitude +from draftwright.auxiliary_layout import document_note_rows +from draftwright.fonts import PLEX_MONO from draftwright.linting.issues import LintIssue from draftwright.pmi import PmiExtractionReport -_SUPPORTED_MANUFACTURING_REQUIREMENTS = frozenset(("external_thread", "internal_thread", "knurl")) +_SUPPORTED_MANUFACTURING_REQUIREMENTS = frozenset( + ("external_thread", "internal_thread", "knurl", "edge_condition", "surface_finish") +) _MANUFACTURING_REF = re.compile(r"\bSEE (MFG [1-9][0-9]*)\b") _DIAMETER_TOKEN = re.compile(r"ø(\d+(?:\.\d+)?)") _KNURL_MAX_TOKEN = re.compile(r"ø(\d+(?:\.\d+)?)\s+MAX AFTER KNURL\b") @@ -452,7 +459,8 @@ def lint_pmi_unreconciled(report, features, *, decorations=None) -> list[LintIss This runs on exactly that gap: content claiming an AP242 origin with no census behind it. It reports the claim as unverified, never as satisfied or as false — the honest outcome - when the evidence is absent rather than negative. Supply the document (``Sheet(..., + when the evidence is absent rather than negative. A source-linked finish or document note + is an error because its source claim cannot be checked. Supply the document (``Sheet(..., source=…)`` or a STEP path) and the real reconciliation replaces this. """ if report is not None: @@ -461,11 +469,18 @@ def lint_pmi_unreconciled(report, features, *, decorations=None) -> list[LintIss source_id for feature in features for source_id in _source_ids(feature) - if getattr(feature, "source", "") == "ap242_pmi" or getattr(feature, "kind", None) == "pmi" + if getattr(feature, "source", "") == "ap242_pmi" + or getattr(feature, "kind", None) == "pmi" + or getattr(feature, "kind", None) == "finish" + or getattr(feature, "kind", None) == "document_note" } for _key, source_ids in _decorated_source_features(decorations, features=features): unchecked.update(source_ids) raw = sum(1 for feature in features if getattr(feature, "kind", None) == "pmi") + source_scoped = any( + getattr(feature, "kind", None) in {"finish", "document_note"} and _source_ids(feature) + for feature in features + ) if not unchecked and not raw: return [] detail = f"{len(unchecked)} AP242 source reference(s)" @@ -473,7 +488,7 @@ def lint_pmi_unreconciled(report, features, *, decorations=None) -> list[LintIss detail += f", including {raw} raw PMI record(s) not lowered to a drafting concept" return [ LintIssue( - severity="warning", + severity="error" if source_scoped else "warning", code="pmi_unreconciled", message=( f"this drawing declares {detail}, and no AP242 census was available to check " @@ -489,12 +504,26 @@ def lint_pmi_source_unknown(report, features, *, decorations=None) -> list[LintI """Report declared AP242 provenance the census does not contain (#1563). `lint_pmi_lowering` walks the census and asks what became of each record. Nothing walked - the other way, so a declaration could claim ``source='ap242_pmi'`` with a `source_id` no + the other way, so a declaration or source-linked finish could claim a `source_id` no record carries and be accepted in silence — a fabricated provenance, which is a worse failure than a missing one because it reads as evidence. """ + missing_source = [ + LintIssue( + severity="error", + code="pmi_source_site_mismatch", + message=( + f"{feature.kind} claims STEP entity {feature.part21_id} without a source_id; " + "the Part21 identity cannot be reconciled" + ), + ) + for feature in features + if getattr(feature, "kind", None) in {"finish", "document_note"} + and feature.part21_id + and not feature.source_id + ] if report is None: - return [] + return missing_source known = {source_id for record in report.records for source_id in _source_ids(record)} known.update(entity.source_id for entity in report.sources if getattr(entity, "source_id", "")) claimed: dict[str, object] = {} @@ -502,6 +531,8 @@ def lint_pmi_source_unknown(report, features, *, decorations=None) -> list[LintI if ( getattr(feature, "source", "") != "ap242_pmi" and getattr(feature, "kind", None) != "pmi" + and getattr(feature, "kind", None) != "finish" + and getattr(feature, "kind", None) != "document_note" ): continue for source_id in _source_ids(feature): @@ -509,7 +540,7 @@ def lint_pmi_source_unknown(report, features, *, decorations=None) -> list[LintI for _key, source_ids in _decorated_source_features(decorations, features=features): for source_id in source_ids: claimed.setdefault(source_id, None) - return [ + return missing_source + [ LintIssue( severity="error", code="pmi_source_unknown", @@ -616,6 +647,227 @@ def _lint_pmi_frame_values(report: PmiExtractionReport, registry) -> list[LintIs return issues +def _printed_document_notes(table) -> tuple[tuple[str, tuple[int, ...]], ...]: + """Read numbered note text and its table rows from the placed annotation.""" + notes: list[str] = [] + row_groups: list[list[int]] = [] + for row_index, row in enumerate(getattr(table, "table_rows", ())): + if len(row) != 1: + return () + line = row[0] + if match := re.fullmatch(r"([1-9][0-9]*) (.*)", line): + if int(match.group(1)) != len(notes) + 1: + return () + notes.append(match.group(2)) + row_groups.append([row_index]) + elif line.startswith(" ") and notes: + notes[-1] += " " + line.strip() + row_groups[-1].append(row_index) + elif line != "GENERAL NOTES": + return () + return tuple((note, tuple(indices)) for note, indices in zip(notes, row_groups, strict=True)) + + +def _note_rows_have_finished_ink(table, drawing, row_indices: tuple[int, ...]) -> bool: + """Check placed note glyphs rather than trusting the table's text metadata.""" + if drawing is None: + return True # A registry-only check has no page geometry to inspect. + try: + rows = table.table_rows + font_size = drawing.draft.font_size + pad = drawing.draft.pad_around_text + lefts, _rights, _width, height, row_height, _block_cols = _table_metrics( + rows, font_size, pad + ) + box = table.bounding_box() + faces = table.faces() + for index in row_indices: + lower = box.min.Y + height - (index + 1) * row_height + upper = lower + row_height + actual_faces = [ + face + for face in faces + if face.bounding_box().min.Y >= lower - 1e-6 + and face.bounding_box().max.Y <= upper + 1e-6 + ] + if not actual_faces: + return False + expected = Text( + txt=_font_safe_text(rows[index][0]), + font_size=font_size, + font_path=PLEX_MONO, + align=(Align.MIN, Align.CENTER), + mode=Mode.PRIVATE, + ).moved(Location((box.min.X + lefts[0] + pad, lower + row_height / 2, 0))) + actual = Sketch(children=actual_faces) + missing = sum(face.area for face in expected.cut(actual).faces()) + extra = sum(face.area for face in actual.cut(expected).faces()) + if missing + extra > 1e-5: + return False + return True + except Exception: + return False + + +def _finish_tip_at_source(annotation, drawing, site) -> bool: + """Check settled page ink against the independently reconstructed STEP site.""" + try: + expected = drawing.at(site[1], *site[0]) + return dist(annotation.tip[:2], expected[:2]) <= 0.05 # page millimetres + except Exception: + return False + + +def _lint_pmi_manufacturing_ink( + report: PmiExtractionReport, registry, drawing=None +) -> list[LintIssue]: + """Check source-scoped manufacturing text and face sites against surviving content.""" + source = { + record.source_id: record + for record in report.records + if record.source_category == "manufacturing_requirement" + and record.kind in {"edge_condition", "surface_finish"} + } + document_sources = { + record.source_id: record + for record in report.records + if record.source_category == "manufacturing_requirement" + and record.kind in {"datum_scheme", "model_representation", "edge_condition"} + } + known_sources = {record.source_id for record in report.records} + known_sources.update(entity.source_id for entity in report.sources if entity.source_id) + issues = [] + table = registry.named("general_notes") + note_owners = ( + tuple( + owner + for owner in registry.features_of("general_notes") + if getattr(owner, "kind", None) == "document_note" + ) + if table is not None + else () + ) + printed_notes = _printed_document_notes(table) + for index, owner in enumerate(note_owners): + source_id = getattr(owner, "source_id", "") + if not source_id: + continue # caller-authored note without an AP242 claim + record = document_sources.get(source_id) + if record is None: + if source_id in known_sources: + issues.append( + LintIssue( + severity="error", + code="pmi_source_site_mismatch", + message=f"general_notes claims non-edge STEP source {source_id}", + source_ids=(source_id,), + annotation_name="general_notes", + ) + ) + continue + if ( + record.kind != getattr(owner, "note_kind", None) + or getattr(owner, "part21_id", None) != record.part21_id + ): + issues.append( + LintIssue( + severity="error", + code="pmi_source_site_mismatch", + message=f"general_notes does not retain AP242 edge identity {source_id}", + source_ids=(source_id,), + annotation_name="general_notes", + ) + ) + continue + if record.kind != "edge_condition": + continue + # The shared wrapper may change spacing, so independently check words + # before comparing its rows and the finished glyphs. + source_rows = document_note_rows((SimpleNamespace(text=record.label),))[1:] + source_rows = ((f"{index + 1} {source_rows[0][0][3:]}",), *source_rows[1:]) + if ( + index < len(printed_notes) + and printed_notes[index][0].split() == _font_safe_text(record.label).split() + and tuple(table.table_rows[i] for i in printed_notes[index][1]) == source_rows + and _note_rows_have_finished_ink(table, drawing, printed_notes[index][1]) + ): + continue + issues.append( + LintIssue( + severity="error", + code="pmi_source_text_mismatch", + message=f"general_notes does not state AP242 edge condition {source_id}", + source_ids=(source_id,), + annotation_name="general_notes", + ) + ) + + for name, annotation in registry.iter_named(): + declaration = registry.declaration_of(name) + if getattr(declaration, "kind", None) != "finish": + continue + source_id = getattr(declaration, "source_id", "") + record = source.get(source_id) + if record is None or record.kind != "surface_finish": + if source_id in known_sources: + issues.append( + LintIssue( + severity="error", + code="pmi_source_site_mismatch", + message=f"{name} claims a STEP source without an exact face-finish record", + source_ids=(source_id,), + annotation_name=name, + ) + ) + continue + match = re.fullmatch(r"\s*Ra\s+(\d+(?:\.\d+)?)\s*(?:um|µm|μm)\s*", record.label, re.I) + expected = Decimal(match.group(1)) if match else None + pdf_specs = tuple(getattr(annotation, "pdf_text_relative_specs", ())) + glyph_label = getattr(annotation, "gdt_visual_finish", None) + try: + pdf_value = Decimal(str(pdf_specs[0][0])) if len(pdf_specs) == 1 else None + glyph_value = Decimal(str(glyph_label)) + except (InvalidOperation, IndexError, TypeError): + pdf_value = glyph_value = None + if expected is None or pdf_value != expected or glyph_value != expected: + issues.append( + LintIssue( + severity="error", + code="pmi_source_text_mismatch", + message=f"{name} does not state AP242 face finish {source_id} as {record.label}", + source_ids=(source_id,), + annotation_name=name, + ) + ) + site = ( + _cylindrical_finish_site(record.cylindrical_refs[0]) + if len(record.cylindrical_refs) == 1 and not record.lowering_blockers + else None + ) + origin = getattr(declaration, "origin", None) + if ( + site is not None + and dist(declaration.frame.origin, site[0]) <= 0.001 + and declaration.view == site[1] + and declaration.side == site[2] + and declaration.part21_id == record.part21_id + and tuple(getattr(origin, "reference_item_ids", ())) == record.reference_item_ids + and tuple(getattr(origin, "cylindrical_refs", ())) == record.cylindrical_refs + and (drawing is None or _finish_tip_at_source(annotation, drawing, site)) + ): + continue + issues.append( + LintIssue( + severity="error", + code="pmi_source_site_mismatch", + message=f"{name} is not anchored to the referenced face finish {source_id}", + source_ids=(source_id,), + annotation_name=name, + ) + ) + return issues + + def lint_pmi_rendering( features, registry, @@ -623,6 +875,7 @@ def lint_pmi_rendering( *, decorations=None, report: PmiExtractionReport | None = None, + drawing=None, overridden_general_tolerance=False, ) -> list[LintIssue]: """Reconcile source-bearing typed PMI with surviving annotation ink. @@ -634,7 +887,7 @@ def lint_pmi_rendering( frames are also checked against the independent extracted magnitude. """ if mode != "annotate": - return [] + return _lint_pmi_manufacturing_ink(report, registry, drawing) if report is not None else [] features = tuple(features) placed_datums = { @@ -692,6 +945,7 @@ def lint_pmi_rendering( ] if report is not None: issues.extend(_lint_pmi_frame_values(report, registry)) + issues.extend(_lint_pmi_manufacturing_ink(report, registry, drawing)) return issues diff --git a/src/draftwright/linting/quality.py b/src/draftwright/linting/quality.py index 4a119aa8..a951ab4a 100644 --- a/src/draftwright/linting/quality.py +++ b/src/draftwright/linting/quality.py @@ -286,7 +286,13 @@ # provenance asserts a machinist can trace it to the model. A fabricated provenance # is worse than a missing one, because it reads as evidence. "pmi_source_unknown", + "pmi_source_site_mismatch", + "pmi_source_text_mismatch", "pmi_value_mismatch", + # Source-selected title values and their finished ink contradict the + # reconciled STEP when these checks fire. + "step_general_tolerance_mismatch", + "step_material_mismatch", # A printed SEE MFG reference asserts a matching source-owned table row. # A missing or altered row makes that printed reference false. "manufacturing_reference_unresolved", @@ -374,6 +380,12 @@ # here rather than in fidelity, whose basis is a claim the source refutes — this one # reports that no source was consulted at all. "pmi_unreconciled", + # An unavailable/ambiguous source has no value to compare. A caller's + # explicit override may disagree with STEP without claiming to copy it. + "step_general_tolerance_disagreement", + "step_material_ambiguous", + "step_material_disagreement", + "step_material_unavailable", # The profile describes less of the body than the body has. An omission, # so not fidelity: nothing false is printed and nothing is misplaced. It scores # nowhere for the same reason the other *_missing codes do. diff --git a/src/draftwright/model/__init__.py b/src/draftwright/model/__init__.py index 6c9469ee..2290e013 100644 --- a/src/draftwright/model/__init__.py +++ b/src/draftwright/model/__init__.py @@ -91,6 +91,7 @@ Feature, FeatureSchedule, FilletFeature, + Finish, FlatFeature, Frame, GeneralTolerance, @@ -163,6 +164,7 @@ "ControlFrame", "CylindricalReference", "FilletFeature", + "Finish", "FlatFeature", "GrooveFeature", "GussetRibFeature", diff --git a/src/draftwright/model/ir.py b/src/draftwright/model/ir.py index 52c3aac0..af6069c7 100644 --- a/src/draftwright/model/ir.py +++ b/src/draftwright/model/ir.py @@ -2402,7 +2402,7 @@ def __post_init__(self) -> None: raise ValueError("document note needs non-empty text") if self.text != self.text.strip(): raise ValueError("document note text cannot contain surrounding whitespace") - if self.note_kind not in ("datum_scheme", "model_representation"): + if self.note_kind not in ("datum_scheme", "model_representation", "edge_condition"): raise ValueError(f"unsupported document-note kind {self.note_kind!r}") if not isinstance(self.on_drawing, bool): raise ValueError("document-note on_drawing must be a bool") @@ -2501,6 +2501,8 @@ class Finish: view: str side: str origin: object | None = None + source_id: str = "" + part21_id: str = "" kind: ClassVar[str] = "finish" def parameters(self) -> list[DimParameter]: diff --git a/src/draftwright/model/pmi_lowering.py b/src/draftwright/model/pmi_lowering.py index 7d8920c7..ad071352 100644 --- a/src/draftwright/model/pmi_lowering.py +++ b/src/draftwright/model/pmi_lowering.py @@ -16,6 +16,7 @@ from decimal import Decimal from typing import Literal, cast +from draftwright._geometry import _cylindrical_finish_site from draftwright.model.ir import ( AuthoredDimension, BossFeature, @@ -25,6 +26,8 @@ DefaultSurfaceFinish, DocumentNote, Feature, + Finish, + Frame, GeneralTolerance, HoleFeature, KnurlRequirement, @@ -1705,6 +1708,48 @@ def lower_ap242_manufacturing_requirements( return replace(lowered, features=rebuilt) +_FACE_FINISH = re.compile(r"\s*Ra\s+(?P\d+(?:\.\d+)?)\s*(?:um|µm|μm)\s*", re.IGNORECASE) + + +def lower_ap242_face_finishes(model: PartModel) -> PartModel: + """Attach a source-authored finish only to its one proved external cylinder.""" + features = list(model.features) + for index, feature in enumerate(features): + if not ( + isinstance(feature, PmiFeature) + and feature.source_category == "manufacturing_requirement" + and feature.pmi_kind == "surface_finish" + and not feature.lowering_blockers + ): + continue + match = _FACE_FINISH.fullmatch(feature.label) + if match is None: + features[index] = _block_requirement(feature, "unsupported face-specific Ra syntax") + continue + if len(feature.reference_item_ids) != 1 or len(feature.cylindrical_refs) != 1: + features[index] = _block_requirement( + feature, "face-specific finish needs one exact cylindrical face reference" + ) + continue + site = _cylindrical_finish_site(feature.cylindrical_refs[0]) + if site is None: + features[index] = _block_requirement( + feature, "face-specific finish needs an orthographic external cylinder" + ) + continue + origin, view, side = site + features[index] = Finish( + frame=Frame(origin, feature.cylindrical_refs[0].principal_axis.lower()), + ra=match.group("ra"), + view=view, + side=side, + origin=feature, + source_id=feature.source_id, + part21_id=feature.part21_id, + ) + return replace(model, features=features) + + def _turned_chamfer_matches_box(feature: ChamferFeature, box) -> bool: if not feature.turned or feature.axis not in "xyz": return False @@ -2088,7 +2133,7 @@ def lower_ap242_document_requirements(model: PartModel) -> PartModel: ], ) - document_kinds = {"datum_scheme", "model_representation"} + document_kinds = {"datum_scheme", "model_representation", "edge_condition"} features = list(model.features) for index, item in enumerate(features): if not ( @@ -2101,6 +2146,13 @@ def lower_ap242_document_requirements(model: PartModel) -> PartModel: if not item.label.strip(): features[index] = _block_requirement(item, "document requirement text is empty") continue + if item.pmi_kind == "edge_condition" and ( + item.reference_item_ids or item.shape_aspect_ids + ): + features[index] = _block_requirement( + item, "face-specific edge condition cannot be a document-wide note" + ) + continue represented_by = ( _datum_scheme_represented_by_symbols(model, item.label) if item.pmi_kind == "datum_scheme" @@ -2132,4 +2184,4 @@ def lower_ap242_dimensions( ) manufacturing = lower_ap242_manufacturing_requirements(dimensions, feature_remap=feature_remap) chamfers = lower_ap242_chamfer_requirements(manufacturing, feature_remap=feature_remap) - return lower_ap242_document_requirements(chamfers) + return lower_ap242_document_requirements(lower_ap242_face_finishes(chamfers)) diff --git a/src/draftwright/pmi.py b/src/draftwright/pmi.py index f8100add..f38e9244 100644 --- a/src/draftwright/pmi.py +++ b/src/draftwright/pmi.py @@ -28,6 +28,7 @@ from quiddity import PartFrame +from draftwright._geometry import _cylindrical_finish_site from draftwright._pmi_datum_geometry import ( _bbox_centroid, _datum_face_site, @@ -528,10 +529,7 @@ def _reference_geometry_with_groups(label, shape_tool, frame: PartFrame | None = def _reference_geometry(label, shape_tool, frame: PartFrame | None = None): """Compatible flattened geometry projection shared by non-dimensional PMI.""" - if frame is None: - geometry = _reference_geometry_with_groups(label, shape_tool) - else: - geometry = _reference_geometry_with_groups(label, shape_tool, frame) + geometry = _reference_geometry_with_groups(label, shape_tool, frame) points, ref_bbox, dominant_axis, reasons, _groups = geometry return points, ref_bbox, dominant_axis, reasons @@ -1040,8 +1038,6 @@ def _datum_reference_shapes(label, shape_tool): def _datum_reference_geometry(label, shape_tool, frame: PartFrame | None = None): """Measure datum faces reached through the direct XCAF relationship.""" shapes = _datum_reference_shapes(label, shape_tool) - if frame is None: - return _datum_geometry_from_shapes(shapes) return _datum_geometry_from_shapes(shapes, frame) @@ -1706,7 +1702,30 @@ def _surface_label_projection( return tuple(sources), tuple(records) -_CYLINDRICAL_REQUIREMENT_KINDS = frozenset(("external_thread", "internal_thread", "knurl")) +_CYLINDRICAL_REQUIREMENT_KINDS = frozenset( + ("external_thread", "internal_thread", "knurl", "surface_finish") +) + + +def _face_finish_site_blocker(shape, reference, frame: PartFrame | None) -> str | None: + """Refuse a finish whose preferred tip is outside its exact trimmed source face.""" + site = _cylindrical_finish_site(reference) + if site is None: + return None # Lowering reports the unsupported cylinder orientation or sense. + from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeVertex + from OCP.BRepExtrema import BRepExtrema_DistShapeShape + from OCP.gp import gp_Pnt + + world_site = site[0] if frame is None else frame.to_world(site[0]) + try: + vertex = BRepBuilderAPI_MakeVertex(gp_Pnt(*world_site)).Vertex() + distance = BRepExtrema_DistShapeShape(vertex, shape) + distance.Perform() + if distance.IsDone() and distance.Value() <= 1e-6: + return None + except Exception: + pass + return "face-specific finish leader site is not proved on referenced trimmed face" def _chamfer_reference_bboxes(shapes, frame: PartFrame | None = None): @@ -1737,6 +1756,9 @@ def _manufacturing_requirement_topology( imported_faces = TopTools_IndexedMapOfShape() TopExp.MapShapes_s(step_reader.OneShape(), TopAbs_FACE, imported_faces) resolver = _DatumTopologyResolver(step_reader, imported_faces) + # A face finish can qualify a cylinder already carrying a thread or knurl; + # duplicate finish claims still fail within their own resolver. + finish_resolver = _DatumTopologyResolver(step_reader, imported_faces) projected = [] for record in records: if ( @@ -1745,7 +1767,8 @@ def _manufacturing_requirement_topology( ): projected.append(record) continue - shapes, topology_reasons = resolver.resolve( + source_resolver = finish_resolver if record.kind == "surface_finish" else resolver + shapes, topology_reasons = source_resolver.resolve( record.part21_id, record.reference_item_ids, noun="manufacturing requirement", @@ -1770,6 +1793,13 @@ def _manufacturing_requirement_topology( blockers = tuple( dict.fromkeys((*record.lowering_blockers, *topology_reasons, *geometry_reasons)) ) + if ( + record.kind == "surface_finish" + and not blockers + and len(shapes) == len(references) == 1 + and (site_blocker := _face_finish_site_blocker(shapes[0], references[0], frame)) + ): + blockers = (site_blocker,) projected.append(replace(record, cylindrical_refs=references, lowering_blockers=blockers)) return tuple(projected) @@ -2965,6 +2995,4 @@ def extract_pmi(step_file: str | Path, *, frame: PartFrame | None = None) -> lis This compatibility surface deliberately remains a list. Callers that need to know what the source contained or why a record is absent must use :func:`extract_pmi_report`. """ - if frame is None: - return list(extract_pmi_report(step_file).records) return list(extract_pmi_report(step_file, frame=frame).records) diff --git a/src/draftwright/registry.py b/src/draftwright/registry.py index 2c99cbd6..b09694e8 100644 --- a/src/draftwright/registry.py +++ b/src/draftwright/registry.py @@ -329,8 +329,8 @@ def mark_section(self, name, mark: SectionMark) -> None: self._anno_section[name] = mark def names_for_feature(self, feature) -> list: - """Every annotation name owned by *feature* (matched by value equality, so a - feature from ``dwg.model()`` finds the annotations rendered for it) (#398). + """Every annotation name owned by or declared from *feature* (matched by value + equality, so a feature from ``dwg.model()`` finds its rendered mark) (#398). Value equality is safe because IR features are value-distinct: every ``Feature`` is a frozen dataclass in which *all* fields participate in ``==``, including the @@ -343,6 +343,7 @@ def names_for_feature(self, feature) -> list: name for name in self._named if any(owner == feature for owner in self.features_of(name)) + or self.declaration_of(name) == feature ] def features_of(self, name) -> tuple: diff --git a/src/draftwright/reporting.py b/src/draftwright/reporting.py index 44b3ddac..e66be21d 100644 --- a/src/draftwright/reporting.py +++ b/src/draftwright/reporting.py @@ -86,6 +86,9 @@ "nominal_rounded", "pmi_unreconciled", "section_recess_recognition_refused", + "step_general_tolerance_disagreement", + "step_material_ambiguous", + "step_material_disagreement", "step_position_coincident_with_datum", } ) @@ -97,6 +100,7 @@ "diameter_row_obstacle_unverified", "gear_correspondence_unverifiable", "pad_footprint_not_defined", + "step_material_unavailable", } ) _MISSING_CARRIER_CODES = frozenset( diff --git a/src/draftwright/sheet.py b/src/draftwright/sheet.py index cb1af2a3..92824e03 100644 --- a/src/draftwright/sheet.py +++ b/src/draftwright/sheet.py @@ -68,6 +68,7 @@ DimensionParameterId, DocumentNote, Feature, + Finish, Frame, GeneralTolerance, ) @@ -1214,7 +1215,7 @@ def add(self, feature) -> _Params: — ADR 4 (was 0016)'s own worked example — could not be written against a generated script. Naming would have been uniform across the verbs and silently absent for one feature in the middle of the file, which is worse than being absent everywhere. A raw - ``ControlFrame`` or ``DatumRef`` may name a handle as its ``origin``; ``add`` resolves + ``ControlFrame``, ``DatumRef``, or ``Finish`` may name a handle as its ``origin``; ``add`` resolves and token-binds that provenance exactly like the public GD&T verbs.""" if not isinstance(feature, Feature): raise TypeError("add() requires an IR Feature") @@ -1226,7 +1227,10 @@ def add(self, feature) -> _Params: if token is not None: return _Params(self, self._index_of_token(token)) src_token = None - if isinstance(feature, (ControlFrame, DatumRef, Note)) and feature.origin is not None: + if ( + isinstance(feature, (ControlFrame, DatumRef, Finish, Note)) + and feature.origin is not None + ): src_token = self._declared_token(feature.origin, verb=f"add() {feature.kind} origin") if src_token is not None: feature = replace( diff --git a/src/draftwright/sheet_emit.py b/src/draftwright/sheet_emit.py index dde4da19..70cbaf54 100644 --- a/src/draftwright/sheet_emit.py +++ b/src/draftwright/sheet_emit.py @@ -16,9 +16,8 @@ Kinds with no declarative verb are flagged inline — never silently dropped — and left to the auto-pass that runs over the declared model on re-run. Every *geometric* kind now has one (``rotational`` was the last, #945, keyword-only — see :func:`draftwright.model.rotational`); -what remains on the comment floor is the aspect kinds a detector never emits (``finish``, -``note``), so the branch is a live guard against a NEW kind -arriving unemitted rather than a standing gap. Imported authored +an unsupported kind stays an explicit comment, so the branch guards against a new kind +arriving unemitted. Imported authored dimensions, including AP242 dimensional PMI, emit as Sheet ``measured_dimension(...)`` declarations (#873 — never the transitional ``dimension`` overload, so a regenerated script is not born deprecated). @@ -105,6 +104,9 @@ from draftwright.sheet_feature_lines import ( _direction as _direction, ) +from draftwright.sheet_feature_lines import ( + _finish_line as _finish_line, +) from draftwright.sheet_feature_lines import ( _hole_group_args as _hole_group_args, ) @@ -229,6 +231,9 @@ def _general_tolerance_line(feature) -> str: return f"sheet.general_tolerance({feature.designation!r}{suffix})" +_SOURCE_NOTE_LINES = {"finish": _finish_line, "note": _note_line} + + def _feature_line( f, part_envelope=None, @@ -276,8 +281,8 @@ def _feature_line( return _control_frame_line(f, origin_ref) if k == "datum_ref": return _datum_ref_line(f, origin_ref) - if k == "note": - return _note_line(f, origin_ref) + if k in _SOURCE_NOTE_LINES: + return _SOURCE_NOTE_LINES[k](f, origin_ref) if k in { "envelope", "step_level", @@ -1049,7 +1054,7 @@ def _feature_block( else None ) exact_step_length = isinstance(step_length_nominal, NominalRequirement) - gdt_with_origin = f.kind in ("control_frame", "datum_ref", "note") + gdt_with_origin = f.kind in ("control_frame", "datum_ref", "finish", "note") origin_ref = names.get(id(f.origin)) if gdt_with_origin else None if ( gdt_with_origin @@ -1497,6 +1502,11 @@ def _declaration_metadata(model, source_feature_ids, source_detected, declaratio "document_note", }: provenance = "pmi" + elif feature.kind == "finish" and ( + feature.source_id + or getattr(getattr(feature, "origin", None), "kind", None) == "pmi" + ): + provenance = "pmi" elif feature.kind == "note": provenance = "structured-note" elif source_detected: @@ -1523,6 +1533,17 @@ def _pattern_requirement_imports(model) -> set[str]: } +def _finish_constructor_imports(model) -> set[str]: + imports: set[str] = set() + for feature in model.features: + if feature.kind != "finish": + continue + imports.update(("Finish", "Frame")) + if getattr(getattr(feature, "origin", None), "cylindrical_refs", ()): + imports.add("CylindricalReference") + return imports + + def _model_constructor_imports(model): """Find constructor names potentially used by the emitted feature declarations.""" # Every constructor a member template can name has to be listed here. The pattern verbs @@ -1555,6 +1576,7 @@ def _model_constructor_imports(model): model_imports.update(["ControlFrame", "Frame"]) if any(f.kind == "datum_ref" for f in model.features): model_imports.update(["DatumRef", "Frame"]) + model_imports.update(_finish_constructor_imports(model)) if any( f.kind == "note" and ( @@ -1580,7 +1602,7 @@ def _model_constructor_imports(model): if any(isinstance(aspect, KnurlRequirement) for aspect in typed_aspects): model_imports.update(["CylindricalReference", "KnurlRequirement"]) if any( - f.kind in ("control_frame", "datum_ref", "note") + f.kind in ("control_frame", "datum_ref", "finish", "note") and getattr(getattr(f, "origin", None), "kind", None) == "pmi" for f in model.features ): diff --git a/src/draftwright/sheet_feature_lines.py b/src/draftwright/sheet_feature_lines.py index 913aa938..824be573 100644 --- a/src/draftwright/sheet_feature_lines.py +++ b/src/draftwright/sheet_feature_lines.py @@ -517,6 +517,24 @@ def _datum_ref_line(f, origin_ref: str | None = None) -> str: ) +def _finish_line(f, origin_ref: str | None = None) -> str: + kw = [ + f"frame=Frame({_pt(f.frame.origin)}, {f.frame.axis!r})", + f"ra={f.ra!r}", + f"view={f.view!r}", + f"side={f.side!r}", + ] + if f.source_id: + kw.append(f"source_id={f.source_id!r}") + if f.part21_id: + kw.append(f"part21_id={f.part21_id!r}") + if origin_ref is not None: + kw.append(f"origin={origin_ref}") + elif getattr(f.origin, "kind", None) == "pmi": + kw.append(f"origin={_raw_pmi_expr(f.origin)}") + return "sheet.add(Finish(" + ", ".join(kw) + "))" + + def _note_line(f, origin_ref: str | None = None) -> str: """Emit fluent authored notes and provenance-rich imported labels without loss.""" raw_origin = getattr(f.origin, "kind", None) == "pmi" diff --git a/tests/_tier_manifest.py b/tests/_tier_manifest.py index 2c0400c8..cf24bb72 100644 --- a/tests/_tier_manifest.py +++ b/tests/_tier_manifest.py @@ -573,6 +573,7 @@ class ContractGroup: ADDITIONAL_SOURCE_CONTRACTS = { "src/draftwright/_build_profile.py": ("test_build_profile_harness.py",), "src/draftwright/_core.py": ("test_formatting.py",), + "src/draftwright/_geometry.py": ("test_pmi_manufacturing_finish.py",), "src/draftwright/_warnings.py": ("test_scale_policy.py",), "src/draftwright/analysis.py": ( "test_analysis_geometry.py", @@ -589,6 +590,7 @@ class ContractGroup: "test_view_blocks.py", ), "src/draftwright/annotations/_axial_render.py": ("test_issue_443_grm03_axial_coverage.py",), + "src/draftwright/annotations/_gdt.py": ("test_pmi_manufacturing_finish.py",), "src/draftwright/annotations/_envelope.py": ( "test_envelope_view_route_issue_1813.py", "test_issue_1000_envelope_reuse.py", @@ -708,8 +710,12 @@ 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",), + "src/draftwright/linting/pmi_coverage.py": ( + "test_manufacturing_schedule.py", + "test_pmi_manufacturing_finish.py", + ), "src/draftwright/linting/pocket_coverage.py": ( "test_issue_1485_curved_pockets.py", "test_issue_1485_rounded_pockets.py", @@ -771,6 +777,7 @@ class ContractGroup: ), "src/draftwright/model/detect_inventory.py": ("test_recogniser_step_inventory.py",), "src/draftwright/model/dimension_intent.py": ("test_dimension_intent_contract.py",), + "src/draftwright/model/__init__.py": ("test_pmi_manufacturing_finish.py",), "src/draftwright/model/ir.py": ( "test_dimension_role_vocabulary.py", "test_issue_1006_measurement_comparison.py", @@ -779,6 +786,7 @@ class ContractGroup: "test_issue_1357_pmi_frame.py", "test_issue_676_polygonal_boss.py", "test_parameter_id.py", + "test_pmi_manufacturing_finish.py", "test_turned_diameters.py", ), "src/draftwright/model/manufacturing_schedule.py": ("test_manufacturing_schedule.py",), @@ -786,11 +794,13 @@ class ContractGroup: "test_issue_1296_cylindrical_diameter_pmi.py", "test_manufacturing_pmi_lowering.py", "test_nominal_step_pmi.py", + "test_pmi_manufacturing_finish.py", ), "src/draftwright/pmi.py": ( "test_issue_1336_grm03_ap242_pmi_fixture.py", "test_issue_1563_sheet_pmi_reconciliation.py", "test_pmi_datum_lowering.py", + "test_pmi_manufacturing_finish.py", ), "src/draftwright/_pmi_datum_geometry.py": ( "test_pmi_datum_lowering.py", @@ -816,6 +826,7 @@ class ContractGroup: "test_plate_ownership.py", "test_through_step_ownership.py", ), + "src/draftwright/registry.py": ("test_pmi_manufacturing_finish.py",), "src/draftwright/repair.py": ( "test_issue_1153_contradictory_dimensions.py", "test_repair.py", @@ -835,7 +846,9 @@ class ContractGroup: "test_issue_958_cross_solid_recognition.py", "test_section_recess_contract.py", ), + "src/draftwright/sheet_feature_lines.py": ("test_pmi_manufacturing_finish.py",), "src/draftwright/sheet.py": ( + "test_pmi_manufacturing_finish.py", "test_sheet_of.py", "test_soft_deprecation.py", ), @@ -844,6 +857,7 @@ class ContractGroup: "test_issue_1536_title_overflow.py", "test_issue_1585_title_block_date.py", "test_projection_symbol_default.py", + "test_pmi_manufacturing_finish.py", "test_sheet_furniture.py", "test_sheet_generation_snapshot.py", ), diff --git a/tests/fixtures/grm03_edge_face_finish_ap242.step b/tests/fixtures/grm03_edge_face_finish_ap242.step new file mode 100644 index 00000000..e8175183 --- /dev/null +++ b/tests/fixtures/grm03_edge_face_finish_ap242.step @@ -0,0 +1,982 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('Open CASCADE Model'),'2;1'); +FILE_NAME('Open CASCADE Shape Model','2026-10-03T08:27:01',('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),#722); +#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,#663,#718)); +#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.,-2.22044604925E-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.553512956637E-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.,-2.22044604925E-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('',(25.,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('',(25.,0.,2.109423746788E-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.,19.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('',(25.,0.,2.109423746788E-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('',(25.5,0.,-1.)); +#433 = SEAM_CURVE('',#434,(#438,#444),.PCURVE_S1.); +#434 = LINE('',#435,#436); +#435 = CARTESIAN_POINT('',(25.,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('',(25.5,0.,2.22044604925E-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('',(25.5,0.,2.22044604925E-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('',(4.440892098501E-16,-4.898587196589E-16,-2.)); +#548 = SURFACE_CURVE('',#549,(#554,#561),.PCURVE_S1.); +#549 = CIRCLE('',#550,2.); +#550 = AXIS2_PLACEMENT_3D('',#551,#552,#553); +#551 = CARTESIAN_POINT('',(0.,0.,3.552713678801E-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,2.); +#557 = AXIS2_PLACEMENT_2D('',#558,#559); +#558 = CARTESIAN_POINT('',(-4.107825191114E-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,2.); +#563 = AXIS2_PLACEMENT_3D('',#564,#565,#566); +#564 = CARTESIAN_POINT('',(-3.2,0.,-3.552713678801E-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.,3.2)); +#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,.T.); +#574 = FACE_BOUND('',#575,.T.); +#575 = EDGE_LOOP('',(#576,#577,#600,#627)); +#576 = ORIENTED_EDGE('',*,*,#545,.F.); +#577 = ORIENTED_EDGE('',*,*,#578,.F.); +#578 = EDGE_CURVE('',#579,#546,#581,.T.); +#579 = VERTEX_POINT('',#580); +#580 = CARTESIAN_POINT('',(-3.2,-4.898587196589E-16,-2.)); +#581 = SEAM_CURVE('',#582,(#586,#593),.PCURVE_S1.); +#582 = LINE('',#583,#584); +#583 = CARTESIAN_POINT('',(-3.2,-4.898587196589E-16,-2.)); +#584 = VECTOR('',#585,1.); +#585 = DIRECTION('',(1.,0.,2.22044604925E-16)); +#586 = PCURVE('',#562,#587); +#587 = DEFINITIONAL_REPRESENTATION('',(#588),#592); +#588 = LINE('',#589,#590); +#589 = CARTESIAN_POINT('',(0.,-0.)); +#590 = VECTOR('',#591,1.); +#591 = DIRECTION('',(0.,1.)); +#592 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#593 = PCURVE('',#562,#594); +#594 = DEFINITIONAL_REPRESENTATION('',(#595),#599); +#595 = LINE('',#596,#597); +#596 = CARTESIAN_POINT('',(6.28318530718,-0.)); +#597 = VECTOR('',#598,1.); +#598 = DIRECTION('',(0.,1.)); +#599 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#600 = ORIENTED_EDGE('',*,*,#601,.T.); +#601 = EDGE_CURVE('',#579,#579,#602,.T.); +#602 = SURFACE_CURVE('',#603,(#608,#615),.PCURVE_S1.); +#603 = CIRCLE('',#604,2.); +#604 = AXIS2_PLACEMENT_3D('',#605,#606,#607); +#605 = CARTESIAN_POINT('',(-3.2,0.,-3.552713678801E-16)); +#606 = DIRECTION('',(1.,-0.,2.22044604925E-16)); +#607 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#608 = PCURVE('',#562,#609); +#609 = DEFINITIONAL_REPRESENTATION('',(#610),#614); +#610 = LINE('',#611,#612); +#611 = CARTESIAN_POINT('',(0.,0.)); +#612 = VECTOR('',#613,1.); +#613 = DIRECTION('',(1.,0.)); +#614 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#615 = PCURVE('',#616,#621); +#616 = PLANE('',#617); +#617 = AXIS2_PLACEMENT_3D('',#618,#619,#620); +#618 = CARTESIAN_POINT('',(-3.2,0.,-3.552713678801E-16)); +#619 = DIRECTION('',(1.,0.,2.22044604925E-16)); +#620 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#621 = DEFINITIONAL_REPRESENTATION('',(#622),#626); +#622 = CIRCLE('',#623,2.); +#623 = AXIS2_PLACEMENT_2D('',#624,#625); +#624 = CARTESIAN_POINT('',(0.,0.)); +#625 = DIRECTION('',(1.,0.)); +#626 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#627 = ORIENTED_EDGE('',*,*,#578,.T.); +#628 = ADVANCED_FACE('',(#629,#632),#616,.F.); +#629 = FACE_BOUND('',#630,.F.); +#630 = EDGE_LOOP('',(#631)); +#631 = ORIENTED_EDGE('',*,*,#601,.T.); +#632 = FACE_BOUND('',#633,.F.); +#633 = EDGE_LOOP('',(#634)); +#634 = ORIENTED_EDGE('',*,*,#635,.F.); +#635 = EDGE_CURVE('',#636,#636,#638,.T.); +#636 = VERTEX_POINT('',#637); +#637 = CARTESIAN_POINT('',(-3.2,-1.959434878636E-16,-0.8)); +#638 = SURFACE_CURVE('',#639,(#644,#651),.PCURVE_S1.); +#639 = CIRCLE('',#640,0.8); +#640 = AXIS2_PLACEMENT_3D('',#641,#642,#643); +#641 = CARTESIAN_POINT('',(-3.2,0.,-8.881784197001E-16)); +#642 = DIRECTION('',(1.,-0.,2.22044604925E-16)); +#643 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#644 = PCURVE('',#616,#645); +#645 = DEFINITIONAL_REPRESENTATION('',(#646),#650); +#646 = CIRCLE('',#647,0.8); +#647 = AXIS2_PLACEMENT_2D('',#648,#649); +#648 = CARTESIAN_POINT('',(5.3290705182E-16,0.)); +#649 = DIRECTION('',(1.,0.)); +#650 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#651 = PCURVE('',#652,#657); +#652 = CYLINDRICAL_SURFACE('',#653,0.8); +#653 = AXIS2_PLACEMENT_3D('',#654,#655,#656); +#654 = CARTESIAN_POINT('',(-3.2,0.,-8.881784197001E-16)); +#655 = DIRECTION('',(1.,0.,2.22044604925E-16)); +#656 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#657 = DEFINITIONAL_REPRESENTATION('',(#658),#662); +#658 = LINE('',#659,#660); +#659 = CARTESIAN_POINT('',(0.,0.)); +#660 = VECTOR('',#661,1.); +#661 = DIRECTION('',(1.,0.)); +#662 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#663 = ADVANCED_FACE('',(#664),#652,.F.); +#664 = FACE_BOUND('',#665,.F.); +#665 = EDGE_LOOP('',(#666,#695,#716,#717)); +#666 = ORIENTED_EDGE('',*,*,#667,.F.); +#667 = EDGE_CURVE('',#668,#668,#670,.T.); +#668 = VERTEX_POINT('',#669); +#669 = CARTESIAN_POINT('',(4.8,-1.959434878636E-16,-0.8)); +#670 = SURFACE_CURVE('',#671,(#676,#683),.PCURVE_S1.); +#671 = CIRCLE('',#672,0.8); +#672 = AXIS2_PLACEMENT_3D('',#673,#674,#675); +#673 = CARTESIAN_POINT('',(4.8,0.,8.881784197001E-16)); +#674 = DIRECTION('',(1.,-0.,2.22044604925E-16)); +#675 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#676 = PCURVE('',#652,#677); +#677 = DEFINITIONAL_REPRESENTATION('',(#678),#682); +#678 = LINE('',#679,#680); +#679 = CARTESIAN_POINT('',(0.,8.)); +#680 = VECTOR('',#681,1.); +#681 = DIRECTION('',(1.,0.)); +#682 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#683 = PCURVE('',#684,#689); +#684 = PLANE('',#685); +#685 = AXIS2_PLACEMENT_3D('',#686,#687,#688); +#686 = CARTESIAN_POINT('',(4.8,0.,8.881784197001E-16)); +#687 = DIRECTION('',(1.,0.,2.22044604925E-16)); +#688 = DIRECTION('',(2.22044604925E-16,0.,-1.)); +#689 = DEFINITIONAL_REPRESENTATION('',(#690),#694); +#690 = CIRCLE('',#691,0.8); +#691 = AXIS2_PLACEMENT_2D('',#692,#693); +#692 = CARTESIAN_POINT('',(0.,0.)); +#693 = DIRECTION('',(1.,0.)); +#694 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#695 = ORIENTED_EDGE('',*,*,#696,.F.); +#696 = EDGE_CURVE('',#636,#668,#697,.T.); +#697 = SEAM_CURVE('',#698,(#702,#709),.PCURVE_S1.); +#698 = LINE('',#699,#700); +#699 = CARTESIAN_POINT('',(-3.2,-1.959434878636E-16,-0.8)); +#700 = VECTOR('',#701,1.); +#701 = DIRECTION('',(1.,0.,2.22044604925E-16)); +#702 = PCURVE('',#652,#703); +#703 = DEFINITIONAL_REPRESENTATION('',(#704),#708); +#704 = LINE('',#705,#706); +#705 = CARTESIAN_POINT('',(0.,-0.)); +#706 = VECTOR('',#707,1.); +#707 = DIRECTION('',(0.,1.)); +#708 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#709 = PCURVE('',#652,#710); +#710 = DEFINITIONAL_REPRESENTATION('',(#711),#715); +#711 = LINE('',#712,#713); +#712 = CARTESIAN_POINT('',(6.28318530718,-0.)); +#713 = VECTOR('',#714,1.); +#714 = DIRECTION('',(0.,1.)); +#715 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#716 = ORIENTED_EDGE('',*,*,#635,.T.); +#717 = ORIENTED_EDGE('',*,*,#696,.T.); +#718 = ADVANCED_FACE('',(#719),#684,.F.); +#719 = FACE_BOUND('',#720,.F.); +#720 = EDGE_LOOP('',(#721)); +#721 = ORIENTED_EDGE('',*,*,#667,.T.); +#722 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#726)) GLOBAL_UNIT_ASSIGNED_CONTEXT +((#723,#724,#725)) REPRESENTATION_CONTEXT('Context #1', + '3D Context with UNIT and UNCERTAINTY') ); +#723 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#724 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#725 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#726 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#723, + 'distance_accuracy_value','confusion accuracy'); +#727 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7)); +#728 = DATUM_FEATURE('','',#4,.T.); +#729 = GEOMETRIC_ITEM_SPECIFIC_USAGE('','',#728,#10,#17); +#730 = DATUM('','',#4,.F.,'A'); +#731 = SHAPE_ASPECT_RELATIONSHIP('',$,#728,#730); +#732 = DATUM_FEATURE('','',#4,.T.); +#733 = GEOMETRIC_ITEM_SPECIFIC_USAGE('','',#732,#10,#336); +#734 = DATUM('','',#4,.F.,'B'); +#735 = SHAPE_ASPECT_RELATIONSHIP('',$,#732,#734); +#736 = DATUM_REFERENCE_COMPARTMENT('','',#4,.F.,#730,$); +#737 = DATUM_REFERENCE_COMPARTMENT('','',#4,.F.,#734,$); +#738 = DATUM_SYSTEM('','',#4,.F.,(#736,#737)); +#739 = LENGTH_MEASURE_WITH_UNIT(LENGTH_MEASURE(2.E-04),#740); +#740 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT($,.METRE.) ); +#741 = SHAPE_ASPECT('','',#4,.T.); +#742 = GEOMETRIC_ITEM_SPECIFIC_USAGE('','',#741,#10,#663); +#743 = ( GEOMETRIC_TOLERANCE('','',#739,#741) +GEOMETRIC_TOLERANCE_WITH_DATUM_REFERENCE((#738)) POSITION_TOLERANCE() ); +#744 = TOLERANCE_ZONE_FORM('cylindrical or circular'); +#745 = TOLERANCE_ZONE('','',#4,.F.,(#743),#744); +#746 = RUNOUT_ZONE_DEFINITION(#745,()); +#747 = RUNOUT_ZONE_ORIENTATION(#748); +#748 = PLANE_ANGLE_MEASURE_WITH_UNIT(PLANE_ANGLE_MEASURE(0.),#749); +#749 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.METRE.) ); +#750 = REPRESENTATION('material name',(#751),#722); +#751 = DESCRIPTIVE_REPRESENTATION_ITEM('Brass CZ121 (CW614N)',''); +#752 = PROPERTY_DEFINITION('material property','material name',#5); +#753 = PROPERTY_DEFINITION_REPRESENTATION(#752,#750); +#754=DRAUGHTING_PRE_DEFINED_CURVE_FONT('continuous'); +#755=DRAUGHTING_PRE_DEFINED_COLOUR('black'); +#756=CURVE_STYLE('',#754,POSITIVE_LENGTH_MEASURE(0.35),#755); +#757=PRESENTATION_STYLE_ASSIGNMENT((#756)); +#758=DESCRIPTIVE_REPRESENTATION_ITEM('internal thread','M2 x 0.4-6H RH, 6.5 mm minimum full thread; DIA 1.6 tapping drill x 8 mm full-diameter depth'); +#759=REPRESENTATION('internal thread requirement',(#758),#722); +#760=PROPERTY_DEFINITION('manufacturing requirement','internal thread',#5); +#761=PROPERTY_DEFINITION_REPRESENTATION(#760,#759); +#762=SHAPE_ASPECT('internal thread','',#4,.T.); +#763=GEOMETRIC_ITEM_SPECIFIC_USAGE('internal thread','',#762,#10,#663); +#764=COORDINATES_LIST('',2,((14.8000,-0.0000,9.2000),(4.8000,-0.0000,-0.8000))); +#765=TESSELLATED_CURVE_SET('',#764,((1,2))); +#766=TESSELLATED_GEOMETRIC_SET('',(#765)); +#767=TESSELLATED_ANNOTATION_OCCURRENCE('',(#757),#766); +#768=DRAUGHTING_CALLOUT('Internal thread requirement',(#767)); +#769=SHAPE_ASPECT('internal thread','',#4,.T.); +#770=GEOMETRIC_ITEM_SPECIFIC_USAGE('internal thread','',#769,#10,#663); +#771=DESCRIPTIVE_REPRESENTATION_ITEM('thread side','internal'); +#772=DESCRIPTIVE_REPRESENTATION_ITEM('designation','M2x0.4'); +#773=DESCRIPTIVE_REPRESENTATION_ITEM('nominal size','M2'); +#774=MEASURE_REPRESENTATION_ITEM('pitch',LENGTH_MEASURE(0.4),#723); +#775=DESCRIPTIVE_REPRESENTATION_ITEM('fit class','6H'); +#776=DESCRIPTIVE_REPRESENTATION_ITEM('hand','right'); +#777=MEASURE_REPRESENTATION_ITEM('tapping drill diameter',LENGTH_MEASURE(1.6),#723); +#778=MEASURE_REPRESENTATION_ITEM('tapping drill depth',LENGTH_MEASURE(8.0),#723); +#779=MEASURE_REPRESENTATION_ITEM('minimum full thread',LENGTH_MEASURE(6.5),#723); +#780=PROPERTY_DEFINITION('internal thread','pmi-assist',#769); +#781=GENERAL_PROPERTY('','user defined attribute',$); +#782=GENERAL_PROPERTY_ASSOCIATION('',$,#781,#780); +#783=REPRESENTATION('internal thread',(#771,#772,#773,#774,#775,#776,#777,#778,#779),#722); +#784=PROPERTY_DEFINITION_REPRESENTATION(#780,#783); +#785=DESCRIPTIVE_REPRESENTATION_ITEM('knurl','Straight knurl, 1 mm pitch, DIA 10 mm maximum after knurling; cut or formed process permitted'); +#786=REPRESENTATION('knurl requirement',(#785),#722); +#787=PROPERTY_DEFINITION('manufacturing requirement','knurl',#5); +#788=PROPERTY_DEFINITION_REPRESENTATION(#787,#786); +#789=SHAPE_ASPECT('knurl','',#4,.T.); +#790=GEOMETRIC_ITEM_SPECIFIC_USAGE('knurl','',#789,#10,#193); +#791=COORDINATES_LIST('',2,((10.8000,0.0000,5.0000),(0.8000,0.0000,-5.0000))); +#792=TESSELLATED_CURVE_SET('',#791,((1,2))); +#793=TESSELLATED_GEOMETRIC_SET('',(#792)); +#794=TESSELLATED_ANNOTATION_OCCURRENCE('',(#757),#793); +#795=DRAUGHTING_CALLOUT('Knurl requirement',(#794)); +#796=SHAPE_ASPECT('knurl','',#4,.T.); +#797=GEOMETRIC_ITEM_SPECIFIC_USAGE('knurl','',#796,#10,#193); +#798=DESCRIPTIVE_REPRESENTATION_ITEM('pattern','straight'); +#799=MEASURE_REPRESENTATION_ITEM('diametral pitch',LENGTH_MEASURE(1.0),#723); +#800=MEASURE_REPRESENTATION_ITEM('major diameter',LENGTH_MEASURE(10.0),#723); +#801=PROPERTY_DEFINITION('knurl','pmi-assist',#796); +#802=GENERAL_PROPERTY('','user defined attribute',$); +#803=GENERAL_PROPERTY_ASSOCIATION('',$,#802,#801); +#804=REPRESENTATION('knurl',(#798,#799,#800),#722); +#805=PROPERTY_DEFINITION_REPRESENTATION(#801,#804); +#806=DESCRIPTIVE_REPRESENTATION_ITEM('external thread','M3 x 0.5-6g RH, full available length on nominal DIA 3 region'); +#807=REPRESENTATION('external thread requirement',(#806),#722); +#808=PROPERTY_DEFINITION('manufacturing requirement','external thread',#5); +#809=PROPERTY_DEFINITION_REPRESENTATION(#808,#807); +#810=SHAPE_ASPECT('external thread','',#4,.T.); +#811=GEOMETRIC_ITEM_SPECIFIC_USAGE('external thread','',#810,#10,#336); +#812=COORDINATES_LIST('',2,((15.5000,-0.0000,8.5000),(5.5000,-0.0000,-1.5000))); +#813=TESSELLATED_CURVE_SET('',#812,((1,2))); +#814=TESSELLATED_GEOMETRIC_SET('',(#813)); +#815=TESSELLATED_ANNOTATION_OCCURRENCE('',(#757),#814); +#816=DRAUGHTING_CALLOUT('External thread requirement',(#815)); +#817=SHAPE_ASPECT('external thread','',#4,.T.); +#818=GEOMETRIC_ITEM_SPECIFIC_USAGE('external thread','',#817,#10,#336); +#819=DESCRIPTIVE_REPRESENTATION_ITEM('thread side','external'); +#820=DESCRIPTIVE_REPRESENTATION_ITEM('designation','M3x0.5'); +#821=DESCRIPTIVE_REPRESENTATION_ITEM('nominal size','M3'); +#822=MEASURE_REPRESENTATION_ITEM('pitch',LENGTH_MEASURE(0.5),#723); +#823=DESCRIPTIVE_REPRESENTATION_ITEM('fit class','6g'); +#824=DESCRIPTIVE_REPRESENTATION_ITEM('hand','right'); +#825=MEASURE_REPRESENTATION_ITEM('thread length',LENGTH_MEASURE(20.0),#723); +#826=PROPERTY_DEFINITION('external thread','pmi-assist',#817); +#827=GENERAL_PROPERTY('','user defined attribute',$); +#828=GENERAL_PROPERTY_ASSOCIATION('',$,#827,#826); +#829=REPRESENTATION('external thread',(#819,#820,#821,#822,#823,#824,#825),#722); +#830=PROPERTY_DEFINITION_REPRESENTATION(#826,#829); +#831=DESCRIPTIVE_REPRESENTATION_ITEM('surface texture','Ra 3.2 \X2\00B5\X0\m unless otherwise specified'); +#832=REPRESENTATION('surface texture requirement',(#831),#722); +#833=PROPERTY_DEFINITION('manufacturing requirement','surface texture',#5); +#834=PROPERTY_DEFINITION_REPRESENTATION(#833,#832); +#835=DESCRIPTIVE_REPRESENTATION_ITEM('edge condition','Break sharp edges 0.2 max'); +#836=REPRESENTATION('edge condition requirement',(#835),#722); +#837=PROPERTY_DEFINITION('manufacturing requirement','edge condition',#5); +#838=PROPERTY_DEFINITION_REPRESENTATION(#837,#836); +#839=DESCRIPTIVE_REPRESENTATION_ITEM('general tolerances','ISO 2768-m'); +#840=REPRESENTATION('general tolerances requirement',(#839),#722); +#841=PROPERTY_DEFINITION('manufacturing requirement','general tolerances',#5); +#842=PROPERTY_DEFINITION_REPRESENTATION(#841,#840); +#843=REPRESENTATION_CONTEXT('','default setting'); +#844=DESCRIPTIVE_REPRESENTATION_ITEM('tolerance class','ISO 2768-m'); +#845=REPRESENTATION('default tolerances',(#844),#843); +#846=PROPERTY_DEFINITION('default tolerances','',#4); +#847=PROPERTY_DEFINITION_REPRESENTATION(#846,#845); +#848=DRAUGHTING_MODEL('',(#768,#795,#816,#859),#722); +#849=DRAUGHTING_MODEL_ITEM_ASSOCIATION('PMI representation to presentation link','',#762,#848,#768); +#850=DRAUGHTING_MODEL_ITEM_ASSOCIATION('PMI representation to presentation link','',#789,#848,#795); +#851=DRAUGHTING_MODEL_ITEM_ASSOCIATION('PMI representation to presentation link','',#810,#848,#816); +#852=DESCRIPTIVE_REPRESENTATION_ITEM('surface finish','Ra 0.8 \X2\00B5\X0\m'); +#853=REPRESENTATION('surface finish requirement',(#852),#722); +#854=PROPERTY_DEFINITION('manufacturing requirement','surface finish',#5); +#855=PROPERTY_DEFINITION_REPRESENTATION(#854,#853); +#856=SHAPE_ASPECT('surface finish','',#4,.T.); +#857=GEOMETRIC_ITEM_SPECIFIC_USAGE('surface finish','',#856,#10,#138); +#858=TESSELLATED_ANNOTATION_OCCURRENCE('',(#757),#793); +#859=DRAUGHTING_CALLOUT('Surface finish requirement',(#858)); +#860=DRAUGHTING_MODEL_ITEM_ASSOCIATION('PMI representation to presentation link','',#856,#848,#859); +ENDSEC; +END-ISO-10303-21; diff --git a/tests/test_issue_1460_step_inspection.py b/tests/test_issue_1460_step_inspection.py index 33450e9b..462df9b5 100644 --- a/tests/test_issue_1460_step_inspection.py +++ b/tests/test_issue_1460_step_inspection.py @@ -66,6 +66,7 @@ "draftwright._core", "draftwright._geometry", "draftwright._pmi_part21", + "draftwright._pmi_topology", # resolver setup during the read-only PMI census "draftwright.annotation_layout_profile", # default profile lookup during detect "draftwright.analysis", "draftwright.angular_geometry", # pure sizing footprints, no compiled plan diff --git a/tests/test_pmi_manufacturing_finish.py b/tests/test_pmi_manufacturing_finish.py new file mode 100644 index 00000000..39e3db4b --- /dev/null +++ b/tests/test_pmi_manufacturing_finish.py @@ -0,0 +1,853 @@ +"""Source-scoped AP242 edge and surface-finish requirements on a compact GRM-03 STEP.""" + +from dataclasses import replace +from pathlib import Path +from types import SimpleNamespace + +import pytest +from build123d import Align, Box, Cylinder, GeomType, Rot, Vertex +from OCP.BRepAdaptor import BRepAdaptor_Surface +from OCP.TopoDS import TopoDS +from quiddity import FrameGauge, PartFrame + +import draftwright.drawing_tables as drawing_tables +import draftwright.pmi as pmi +from draftwright import Sheet, build_drawing +from draftwright._geometry import _cylindrical_finish_site +from draftwright.analysis import _import_step +from draftwright.linting.pmi_coverage import ( + _printed_document_notes, + lint_pmi_lowering, + lint_pmi_source_unknown, + lint_pmi_unreconciled, +) +from draftwright.model.ir import ( + CylindricalReference, + DefaultSurfaceFinish, + DocumentNote, + Finish, + Frame, + PmiFeature, +) +from draftwright.model.pmi_lowering import lower_ap242_document_requirements +from draftwright.pmi import _face_finish_site_blocker, extract_pmi_report +from draftwright.sheet_emit import emit_sheet_script + +_STEP = Path(__file__).parent / "fixtures/grm03_edge_face_finish_ap242.step" + + +@pytest.fixture(scope="module") +def _case(): + report = extract_pmi_report(_STEP) + source = { + record.kind: record + for record in report.records + if record.source_category == "manufacturing_requirement" + } + assert source["surface_texture"].label == "Ra 3.2 µm unless otherwise specified" + assert source["edge_condition"].label == "Break sharp edges 0.2 max" + assert source["edge_condition"].reference_item_ids == () + assert source["surface_finish"].label == "Ra 0.8 µm" + assert source["surface_finish"].reference_item_ids == ("#138",) + assert source["surface_finish"].lowering_blockers == () + return source, build_drawing(_STEP, pmi="annotate", out=None) + + +def test_document_edge_condition_is_printed_verbatim_and_covered(_case): + source, drawing = _case + (default_finish,) = [ + feature + for feature in drawing.model().features + if isinstance(feature, DefaultSurfaceFinish) + and feature.source_id == source["surface_texture"].source_id + ] + assert default_finish.ra == "3.2" + symbol = drawing.get_annotation("default_surface_finish") + assert symbol.pdf_text_relative_specs[0][0] == "Ra 3.2" + assert drawing.registry.feature_of("default_surface_finish") == default_finish + assert not [ + issue + for issue in drawing.lint() + if source["surface_texture"].source_id in issue.source_ids + and issue.code in {"pmi_not_lowered", "pmi_not_rendered", "pmi_dropped"} + ] + notes = [ + feature + for feature in drawing.model().features + if isinstance(feature, DocumentNote) + and feature.source_id == source["edge_condition"].source_id + ] + assert len(notes) == 1 + assert notes[0].text == source["edge_condition"].label + assert notes[0].note_kind == "edge_condition" + table = drawing.get_annotation("general_notes") + assert "Break sharp edges 0.2 max" in " ".join( + cell for row in table.table_rows for cell in row + ) + assert "general_notes" in drawing.registry.names_for_feature(notes[0]) + assert not [ + issue + for issue in drawing.lint() + if source["edge_condition"].source_id in issue.source_ids + and issue.code in {"pmi_not_lowered", "pmi_not_rendered"} + ] + + +def test_wrapped_source_edge_note_keeps_its_printed_meaning_issue_2176(tmp_path): + original = "Break sharp edges 0.2 max" + wrapped = "Break sharp edges and remove all burrs around external corners 0.2 max" + step_text = _STEP.read_text() + assert step_text.count(original) == 1 + step = tmp_path / "wrapped-edge-note.step" + step.write_text(step_text.replace(original, wrapped)) + report = extract_pmi_report(step) + (edge,) = [record for record in report.records if record.kind == "edge_condition"] + assert edge.label == wrapped + + drawing = build_drawing(step, pmi="annotate", out=None) + rows = drawing.get_annotation("general_notes").table_rows + assert ("1 Break sharp edges and remove all burrs around",) in rows + assert (" external corners 0.2 max",) in rows + assert not [ + issue + for issue in drawing.lint() + if issue.code == "pmi_source_text_mismatch" and edge.source_id in issue.source_ids + ] + + +def test_face_finish_uses_its_referenced_cylinder_and_survives_on_sheet(_case): + source, drawing = _case + finishes = [ + feature + for feature in drawing.model().features + if isinstance(feature, Finish) and feature.source_id == source["surface_finish"].source_id + ] + assert len(finishes) == 1 + finish = finishes[0] + assert finish.ra == "0.8" + assert finish.origin.reference_item_ids == ("#138",) + assert len(finish.origin.cylindrical_refs) == 1 + cylinder = finish.origin.cylindrical_refs[0] + assert cylinder.diameter == pytest.approx(5.0) + assert finish.view == "front" + assert finish.frame.origin[2] == pytest.approx(cylinder.axis_origin[2] + cylinder.radius) + names = drawing.registry.names_for_feature(finish.origin) + assert len(names) == 1 + annotation = drawing.get_annotation(names[0]) + assert annotation.tip[:2] == pytest.approx(drawing.at("front", *finish.frame.origin)[:2]) + assert annotation.pdf_text_relative_specs[0][0] == "0.8" + assert annotation.gdt_visual_finish == "0.8" + assert not [ + issue + for issue in drawing.lint() + if source["surface_finish"].source_id in issue.source_ids + and issue.code + in { + "pmi_not_lowered", + "pmi_not_rendered", + "pmi_dropped", + "pmi_source_text_mismatch", + "pmi_source_site_mismatch", + } + ] + + +def test_sourced_edge_and_face_finish_replay_as_declarations(_case): + source, drawing = _case + script = emit_sheet_script( + drawing.model(), + "part", + "grm03-edge-finish", + title="GRM-03", + number="GRM-03", + pmi="annotate", + pmi_source=str(_STEP.resolve()), + ) + assert "sheet.add(Finish(" in script + finish_line = next(line for line in script.splitlines() if "sheet.add(Finish(" in line) + assert "provenance='pmi'" in finish_line + assert "sheet.document_note('Break sharp edges 0.2 max', kind='edge_condition'" in script + namespace = {"part": _import_step(str(_STEP))} + build_end = script.index("drawing = sheet.build()") + len("drawing = sheet.build()") + exec( # noqa: S102 — run the generated declarations and their public build + compile(script[:build_end], "", "exec"), + namespace, + ) + replayed = namespace["sheet"].model() + (finish,) = [feature for feature in replayed.features if isinstance(feature, Finish)] + assert finish.source_id == source["surface_finish"].source_id + assert finish.origin.reference_item_ids == ("#138",) + assert finish.origin.cylindrical_refs[0].diameter == pytest.approx(5.0) + (edge,) = [ + feature + for feature in replayed.features + if isinstance(feature, DocumentNote) and feature.note_kind == "edge_condition" + ] + assert edge.source_id == source["edge_condition"].source_id + replayed_drawing = namespace["drawing"] + assert "Break sharp edges 0.2 max" in str( + replayed_drawing.get_annotation("general_notes").table_rows + ) + names = replayed_drawing.registry.names_for_feature(finish.origin) + assert len(names) == 1 + assert replayed_drawing.get_annotation(names[0]).pdf_text_relative_specs[0][0] == "0.8" + assert not [ + issue + for issue in replayed_drawing.lint() + if source["edge_condition"].source_id in issue.source_ids + or source["surface_finish"].source_id in issue.source_ids + ] + + +def test_declared_finish_replay_keeps_its_public_feature_origin(): + part = Cylinder(5, 10, align=(Align.CENTER, Align.CENTER, Align.MIN)) + sheet = Sheet(part, title="SHAFT", number="F1") + step = sheet.step(diameter=10, length=10, at=(0, 0, 0), axis="z") + sheet.add( + Finish( + frame=Frame((5, 0, 5), "z"), + ra="1.6", + view="front", + side="right", + origin=step, + ) + ) + step.thread("M10") # A later public replacement must rebind the finish origin. + sheet.authored_dimensions() + model = sheet.model() + original_step = next(feature for feature in model.features if feature.kind == "step") + original_finish = next(feature for feature in model.features if isinstance(feature, Finish)) + assert original_finish.origin is original_step + assert original_step.thread == "M10" + + script = emit_sheet_script(model, "part", "shaft-finish", title="SHAFT", number="F1") + finish_line = next(line for line in script.splitlines() if "sheet.add(Finish(" in line) + assert "origin=" in finish_line + assert "provenance='authored'" in finish_line + namespace = {"part": part} + build_end = script.index("drawing = sheet.build()") + len("drawing = sheet.build()") + exec(compile(script[:build_end], "", "exec"), namespace) # noqa: S102 + replayed = namespace["sheet"].model() + replayed_step = next(feature for feature in replayed.features if feature.kind == "step") + replayed_finish = next(feature for feature in replayed.features if isinstance(feature, Finish)) + assert replayed_finish.origin is replayed_step + drawing = namespace["drawing"] + placed = drawing.annotations_of(replayed_finish) + assert len(placed) == 1 + assert placed.keys() <= drawing.annotations_of(replayed_step).keys() + assert drawing.drop(replayed_finish) == list(placed) + assert drawing.annotations_of(replayed_finish) == {} + assert not [issue for issue in drawing.lint() if issue.code == "pmi_unreconciled"] + + +def test_replayed_source_finish_refuses_a_different_face_site(_case): + source, drawing = _case + script = emit_sheet_script( + drawing.model(), + "part", + "grm03-edge-finish", + title="GRM-03", + number="GRM-03", + pmi="annotate", + pmi_source=str(_STEP.resolve()), + ) + old = "Finish(frame=Frame((4, 0, 2.5), 'x')" + new = "Finish(frame=Frame((4, 0, 5), 'x')" + assert script.count(old) == 1 # The public declaration targets source face #138. + namespace = {"part": _import_step(str(_STEP))} + altered = script.replace(old, new) + build_end = altered.index("drawing = sheet.build()") + len("drawing = sheet.build()") + exec(compile(altered[:build_end], "", "exec"), namespace) # noqa: S102 + moved = namespace["drawing"] + finish = next( + feature + for feature in moved.model().features + if isinstance(feature, Finish) and feature.source_id == source["surface_finish"].source_id + ) + assert finish.frame.origin == (4, 0, 5) + assert finish.origin.reference_item_ids == ("#138",) + assert moved.annotations_of(finish) # The false claim visibly reached the sheet. + assert any( + issue.code == "pmi_source_site_mismatch" + and source["surface_finish"].source_id in issue.source_ids + for issue in moved.lint() + ) + + +@pytest.mark.parametrize( + ("new", "claimed_source", "claimed_part21", "expected_code"), + ( + ( + "source_id='manufacturing_requirement:#808', part21_id='#854', origin=", + "manufacturing_requirement:#808", + "#854", + "pmi_source_site_mismatch", + ), + ( + "source_id='manufacturing_requirement:#854', part21_id='#999999', origin=", + "manufacturing_requirement:#854", + "#999999", + "pmi_source_site_mismatch", + ), + ( + "source_id='manufacturing_requirement:#780', part21_id='#780', origin=", + "manufacturing_requirement:#780", + "#780", + "pmi_source_site_mismatch", + ), + ( + "source_id='manufacturing_requirement:#999998', part21_id='#999998', origin=", + "manufacturing_requirement:#999998", + "#999998", + "pmi_source_unknown", + ), + ), +) +def test_replayed_finish_refuses_false_source_identity( + _case, new, claimed_source, claimed_part21, expected_code +): + _source, drawing = _case + if claimed_source == "manufacturing_requirement:#780": + report = extract_pmi_report(_STEP) + assert claimed_source in {entity.source_id for entity in report.sources} + assert claimed_source not in {record.source_id for record in report.records} + if expected_code == "pmi_source_unknown": + report = extract_pmi_report(_STEP) + assert claimed_source not in {entity.source_id for entity in report.sources} + assert claimed_source not in {record.source_id for record in report.records} + script = emit_sheet_script( + drawing.model(), + "part", + "grm03-edge-finish", + title="GRM-03", + number="GRM-03", + pmi="annotate", + pmi_source=str(_STEP.resolve()), + ) + old = "source_id='manufacturing_requirement:#854', part21_id='#854', origin=" + assert script.count(old) == 1 + altered = script.replace(old, new) + namespace = {"part": _import_step(str(_STEP))} + build_end = altered.index("drawing = sheet.build()") + len("drawing = sheet.build()") + exec(compile(altered[:build_end], "", "exec"), namespace) # noqa: S102 + replayed = namespace["drawing"] + finish = next(feature for feature in replayed.model().features if isinstance(feature, Finish)) + assert finish.source_id == claimed_source + assert finish.part21_id == claimed_part21 + assert finish.origin.reference_item_ids == ("#138",) + assert replayed.annotations_of(finish) + issues = replayed.lint() + if claimed_source == "manufacturing_requirement:#780": + assert not [issue for issue in issues if issue.code == "pmi_source_unknown"] + assert any( + issue.code == expected_code and finish.source_id in issue.source_ids for issue in issues + ) + + +def test_source_claimed_finish_without_a_step_census_is_an_error(): + part = Cylinder(5, 10, align=(Align.CENTER, Align.CENTER, Align.MIN)) + sheet = Sheet(part, title="SHAFT", number="F2", pmi="annotate") + sheet.add( + Finish( + frame=Frame((5, 0, 5), "z"), + ra="1.6", + view="front", + side="right", + source_id="manufacturing_requirement:#missing", + part21_id="#missing", + ) + ) + sheet.authored_dimensions() + drawing = sheet.build() + finish = next(feature for feature in drawing.model().features if isinstance(feature, Finish)) + assert drawing.annotations_of(finish) + assert any( + issue.code == "pmi_unreconciled" + and issue.severity == "error" + and finish.source_id in issue.source_ids + for issue in drawing.lint() + ) + + +def test_independent_lint_rejects_changed_finished_manufacturing_text(_case): + source, drawing = _case + table = drawing.get_annotation("general_notes") + original_rows = table.table_rows + assert ("1 Break sharp edges 0.2 max",) in original_rows + try: + table.table_rows = tuple( + ("1 Deburr all edges",) if row == ("1 Break sharp edges 0.2 max",) else row + for row in original_rows + ) + assert any( + issue.code == "pmi_source_text_mismatch" + and source["edge_condition"].source_id in issue.source_ids + for issue in drawing.lint() + ) + assert ( + "pmi_source_text_mismatch" in drawing.lint_summary()["quality"]["fidelity"]["by_code"] + ) + finally: + table.table_rows = original_rows + + finish = next( + feature + for feature in drawing.model().features + if isinstance(feature, Finish) and feature.source_id == source["surface_finish"].source_id + ) + (name,) = drawing.registry.names_for_feature(finish.origin) + annotation = drawing.get_annotation(name) + original_specs = annotation.pdf_text_relative_specs + assert original_specs[0][0] == "0.8" + try: + annotation.pdf_text_relative_specs = (("9.9", *original_specs[0][1:]),) + assert any( + issue.code == "pmi_source_text_mismatch" + and source["surface_finish"].source_id in issue.source_ids + for issue in drawing.lint() + ) + finally: + annotation.pdf_text_relative_specs = original_specs + + +def test_edge_note_lint_catches_a_shared_formatter_dropping_source_words(monkeypatch): + import draftwright.annotations.from_model as from_model + import draftwright.linting.pmi_coverage as coverage + + (edge,) = [ + record for record in extract_pmi_report(_STEP).records if record.kind == "edge_condition" + ] + original = from_model.document_note_rows + changed = [] + + def omit_edge_value(notes): + altered = tuple(SimpleNamespace(text=note.text.replace(" 0.2 max", "")) for note in notes) + if any( + note.text != replacement.text for note, replacement in zip(notes, altered, strict=True) + ): + changed.append(True) + return original(altered) + + monkeypatch.setattr(from_model, "document_note_rows", omit_edge_value) + monkeypatch.setattr(coverage, "document_note_rows", omit_edge_value) + sheet = Sheet(Box(12, 12, 12), source=str(_STEP), pmi="annotate") + sheet.authored_dimensions() + sheet.document_note( + edge.label, kind="edge_condition", source_id=edge.source_id, part21_id=edge.part21_id + ) + drawing = sheet.build() + assert changed + rows = drawing.get_annotation("general_notes").table_rows + assert ("1 Break sharp edges",) in rows + assert not any("0.2 max" in row[0] for row in rows) + assert any( + issue.code == "pmi_source_text_mismatch" and edge.source_id in issue.source_ids + for issue in drawing.lint() + ) + + +@pytest.mark.parametrize( + "false_claim", ["wrong_part21", "wrong_kind", "wrong_note_kind", "unknown_source"] +) +def test_replayed_edge_note_rejects_false_source_identity(_case, false_claim): + source, drawing = _case + report = extract_pmi_report(_STEP) + edge = source["edge_condition"] + knurl = source["knurl"] + assert knurl.source_id != edge.source_id + assert knurl.kind == "knurl" + if false_claim == "wrong_part21": + source_id, part21_id, expected_code = edge.source_id, "#wrong", "pmi_source_site_mismatch" + elif false_claim == "wrong_kind": + source_id, part21_id, expected_code = ( + knurl.source_id, + knurl.part21_id, + "pmi_source_site_mismatch", + ) + elif false_claim == "wrong_note_kind": + source_id, part21_id, expected_code = ( + edge.source_id, + edge.part21_id, + "pmi_source_site_mismatch", + ) + else: + source_id, part21_id, expected_code = ( + "manufacturing_requirement:#9999", + "#9999", + "pmi_source_unknown", + ) + assert source_id not in {record.source_id for record in report.records} + + script = emit_sheet_script( + drawing.model(), + "part", + "grm03-edge-finish", + title="GRM-03", + number="GRM-03", + pmi="annotate", + pmi_source=str(_STEP.resolve()), + ) + marker = "drawing = sheet.build()" + assert script.count(marker) == 1 + note_kind = "datum_scheme" if false_claim == "wrong_note_kind" else "edge_condition" + added = ( + f"sheet.document_note('Break sharp edges 0.2 max', kind={note_kind!r}, " + f"source_id={source_id!r}, part21_id={part21_id!r})" + ) + altered = script.replace(marker, f"{added}\n{marker}") + namespace = {"part": _import_step(str(_STEP))} + build_end = altered.index(marker) + len(marker) + exec( # noqa: S102 — exercise a generated public Sheet script with one false claim + compile(altered[:build_end], "", "exec"), namespace + ) + replayed = namespace["drawing"] + rows = replayed.get_annotation("general_notes").table_rows + assert sum("Break sharp edges 0.2 max" in row[0] for row in rows) == 2 + assert any( + issue.code == expected_code and source_id in issue.source_ids for issue in replayed.lint() + ) + + +def test_valid_source_datum_note_retains_its_provenance_without_edge_diagnostic(): + step = Path(__file__).parent / "fixtures/grm03_thumbwheel_drive_screw_ap242_pmi.step" + report = extract_pmi_report(step) + (datum_scheme,) = [record for record in report.records if record.kind == "datum_scheme"] + sheet = Sheet(Box(12, 12, 12), source=str(step), pmi="annotate") + sheet.authored_dimensions() + sheet.document_note( + datum_scheme.label, + kind="datum_scheme", + source_id=datum_scheme.source_id, + part21_id=datum_scheme.part21_id, + ) + drawing = sheet.build() + assert ( + _printed_document_notes(drawing.get_annotation("general_notes"))[0][0] + == datum_scheme.label + ) + assert not [ + issue + for issue in drawing.lint() + if datum_scheme.source_id in issue.source_ids + and issue.code in {"pmi_source_unknown", "pmi_source_site_mismatch"} + ] + + +def test_edge_note_without_a_source_is_authored_but_a_source_claim_needs_a_census(_case): + source, drawing = _case + edge = next( + feature + for feature in drawing.model().features + if isinstance(feature, DocumentNote) + and feature.source_id == source["edge_condition"].source_id + ) + authored = replace(edge, source_id="", part21_id="") + assert lint_pmi_unreconciled(None, (authored,)) == [] + assert lint_pmi_source_unknown(extract_pmi_report(_STEP), (authored,)) == [] + assert { + issue.source_ids + for issue in lint_pmi_unreconciled(None, (edge,)) + if issue.code == "pmi_unreconciled" and issue.severity == "error" + } == {(edge.source_id,)} + + +@pytest.mark.parametrize("kind", ("finish", "document_note")) +@pytest.mark.parametrize("with_census", (False, True)) +def test_printed_part21_claim_requires_a_source_id_issue_2176(_case, kind, with_census): + if with_census: + source, control = _case + script = emit_sheet_script( + control.model(), + "part", + "grm03-edge-finish", + title="GRM-03", + number="GRM-03", + pmi="annotate", + pmi_source=str(_STEP.resolve()), + ) + record = source["surface_finish" if kind == "finish" else "edge_condition"] + tail = ", origin=" if kind == "finish" else ")" + old = f"source_id={record.source_id!r}, part21_id={record.part21_id!r}{tail}" + assert script.count(old) == 1 + altered = script.replace(old, f"source_id='', part21_id={record.part21_id!r}{tail}") + namespace = {"part": _import_step(str(_STEP))} + build_end = altered.index("drawing = sheet.build()") + len("drawing = sheet.build()") + exec(compile(altered[:build_end], "", "exec"), namespace) # noqa: S102 + drawing = namespace["drawing"] + else: + record = None + part = Cylinder(5, 10, align=(Align.CENTER, Align.CENTER, Align.MIN)) + sheet = Sheet(part, title="SHAFT", number="P21", pmi="annotate") + if kind == "finish": + sheet.add( + Finish( + frame=Frame((5, 0, 5), "z"), + ra="1.6", + view="front", + side="right", + part21_id="#854", + ) + ) + else: + sheet.document_note( + "Break sharp edges 0.2 max", kind="edge_condition", part21_id="#854" + ) + sheet.document_note("Authored note", kind="edge_condition") + sheet.authored_dimensions() + drawing = sheet.build() + part21_id = record.part21_id if record is not None else "#854" + claim = next( + feature + for feature in drawing.model().features + if feature.kind == kind and feature.part21_id == part21_id + ) + assert claim.source_id == "" + if kind == "finish": + assert drawing.annotations_of(claim) + else: + rows = drawing.get_annotation("general_notes").table_rows + assert any("Break sharp edges 0.2 max" in row[0] for row in rows) + if not with_census: + assert any("Authored note" in row[0] for row in rows) + assert any( + issue.code == "pmi_source_site_mismatch" + and part21_id in issue.message + and "source_id" in issue.message + for issue in drawing.lint() + ) + + +def test_independent_lint_rejects_wrong_visible_edge_note_ink(_case, monkeypatch): + source, control = _case + original = drawing_tables._build_table + changed = [] + + def wrong_note_ink(rows, draft, **kwargs): + if rows[0] != ("GENERAL NOTES",): + return original(rows, draft, **kwargs) + assert ("1 Break sharp edges 0.2 max",) in rows + changed.append(True) + visible_rows = tuple( + ("1 Break sharp edges 9.9 max",) if row == ("1 Break sharp edges 0.2 max",) else row + for row in rows + ) + return original(visible_rows, draft, **kwargs) + + monkeypatch.setattr(drawing_tables, "_build_table", wrong_note_ink) + drawing = build_drawing(_STEP, pmi="annotate", out=None) + assert changed + table = drawing.get_annotation("general_notes") + assert ("1 Break sharp edges 0.2 max",) in table.table_rows + assert table.table_size == control.get_annotation("general_notes").table_size + assert any( + issue.code == "pmi_source_text_mismatch" + and source["edge_condition"].source_id in issue.source_ids + for issue in drawing.lint() + ) + + +def test_source_finish_lint_rejects_wrong_same_width_visible_digits(_case, monkeypatch): + import draftwright.annotations._gdt as gdt + + source, _ = _case + original = gdt._gdt_glyph + changed = [] + + def wrong_ink(item, draft): + if item.kind != "finish" or item.source_id != source["surface_finish"].source_id: + return original(item, draft) + glyph = original(replace(item, ra="9.9"), draft) + assert glyph.label == "9.9" + glyph.label = item.ra # The helper claims 0.8 while its vector faces spell 9.9. + changed.append(glyph) + return glyph + + monkeypatch.setattr(gdt, "_gdt_glyph", wrong_ink) + drawing = build_drawing(_STEP, pmi="annotate", out=None) + assert changed # Confirm the altered producer was used by the public build. + finish = next( + feature + for feature in drawing.model().features + if isinstance(feature, Finish) and feature.source_id == source["surface_finish"].source_id + ) + (name,) = drawing.registry.names_for_feature(finish) + annotation = drawing.get_annotation(name) + assert annotation.pdf_text_relative_specs[0][0] == "0.8" + assert annotation.gdt_visual_finish == "" + assert any( + issue.code == "pmi_source_text_mismatch" + and source["surface_finish"].source_id in issue.source_ids + for issue in drawing.lint() + ) + + +def test_source_finish_lint_rejects_a_displaced_visible_leader_tip(_case, monkeypatch): + import draftwright.annotations._gdt as gdt + + source, _ = _case + original_builders = gdt._gdt_candidate_builders + shifted = [] + + def displaced_builders(item, draft, leader_ctor, *args): + if item.kind != "finish" or item.source_id != source["surface_finish"].source_id: + return original_builders(item, draft, leader_ctor, *args) + + def displaced_leader(*leader_args, **kwargs): + tip = kwargs["tip"] + kwargs["tip"] = (tip[0] - 0.5, tip[1] + 0.5) + shifted.append(kwargs["tip"]) + return leader_ctor(*leader_args, **kwargs) + + return original_builders(item, draft, displaced_leader, *args) + + monkeypatch.setattr(gdt, "_gdt_candidate_builders", displaced_builders) + drawing = build_drawing(_STEP, pmi="annotate", out=None) + assert shifted # The public build used the deliberately displaced leader producer. + finish = next( + feature + for feature in drawing.model().features + if isinstance(feature, Finish) and feature.source_id == source["surface_finish"].source_id + ) + (name,) = drawing.registry.names_for_feature(finish) + annotation = drawing.get_annotation(name) + source_site = _cylindrical_finish_site(source["surface_finish"].cylindrical_refs[0]) + assert source_site is not None + expected = drawing.at(source_site[1], *source_site[0]) + assert annotation.tip[:2] == pytest.approx((expected[0] - 0.5, expected[1] + 0.5)) + assert any( + issue.code == "pmi_source_site_mismatch" and finish.source_id in issue.source_ids + for issue in drawing.lint() + ) + + +def test_face_finish_can_share_an_exact_face_with_a_distinct_knurl_requirement(tmp_path): + source = _STEP.read_text() + original = "#857=GEOMETRIC_ITEM_SPECIFIC_USAGE('surface finish','',#856,#10,#138);" + shared = "#857=GEOMETRIC_ITEM_SPECIFIC_USAGE('surface finish','',#856,#10,#193);" + assert source.count(original) == 1 + assert "#790=GEOMETRIC_ITEM_SPECIFIC_USAGE('knurl','',#789,#10,#193);" in source + path = tmp_path / "shared-face.step" + path.write_text(source.replace(original, shared)) + + report = extract_pmi_report(path) + finish = next(record for record in report.records if record.kind == "surface_finish") + assert finish.reference_item_ids == ("#193",) + assert finish.lowering_blockers == () + assert len(finish.cylindrical_refs) == 1 + assert finish.cylindrical_refs[0].diameter == pytest.approx(10.0) + + +def test_face_finish_site_check_uses_source_coordinates_under_a_part_frame(): + frame = PartFrame( + origin=(11.0, -7.0, 3.0), + x=(0.0, 1.0, 0.0), + y=(0.0, 0.0, 1.0), + z=(1.0, 0.0, 0.0), + gauge=FrameGauge.FULL, + ) + report = extract_pmi_report(_STEP, frame=frame) + finish = next(record for record in report.records if record.kind == "surface_finish") + assert finish.cylindrical_refs[0].principal_axis == "Z" + assert finish.lowering_blockers == () + + +@pytest.mark.parametrize("trim", ("half_cylinder", "cross_hole")) +def test_face_finish_refuses_a_tip_outside_the_exact_trimmed_face(trim, monkeypatch): + part = ( + Rot(0, 0, 90) * Cylinder(5, 10, arc_size=180) + if trim == "half_cylinder" + else Cylinder(5, 10) - Rot(0, 90, 0) * Cylinder(1, 20) + ) + (face,) = [ + face + for face in part.faces() + if face.geom_type == GeomType.CYLINDER + and BRepAdaptor_Surface(TopoDS.Face_s(face.wrapped)).Cylinder().Radius() + == pytest.approx(5) + ] + surface = BRepAdaptor_Surface(TopoDS.Face_s(face.wrapped)) + axis = surface.Cylinder().Axis() + point, direction = axis.Location(), axis.Direction() + reference = CylindricalReference.canonical( + axis_point=(point.X(), point.Y(), point.Z()), + axis_direction=(direction.X(), direction.Y(), direction.Z()), + radius=surface.Cylinder().Radius(), + local_interval=(surface.FirstVParameter(), surface.LastVParameter()), + sense="external", + ) + site = _cylindrical_finish_site(reference) + assert site is not None + assert Vertex(*site[0]).distance_to(face) > 0.1 # defect exists before the refusal + assert _face_finish_site_blocker(face.wrapped, reference, None) == ( + "face-specific finish leader site is not proved on referenced trimmed face" + ) + monkeypatch.setattr( + pmi, + "_DatumTopologyResolver", + lambda *_args: SimpleNamespace(resolve=lambda *_args, **_kwargs: ((face.wrapped,), ())), + ) + source = pmi.PmiRecord( + kind="surface_finish", + type_code=None, + value=0.0, + label="Ra 0.8 µm", + source_id="manufacturing_requirement:partial", + part21_id="#finish", + source_category="manufacturing_requirement", + reference_item_ids=("#face",), + ) + (projected,) = pmi._manufacturing_requirement_topology( + (source,), SimpleNamespace(OneShape=lambda: part.wrapped) + ) + assert projected.cylindrical_refs == (reference,) + assert projected.lowering_blockers == ( + "face-specific finish leader site is not proved on referenced trimmed face", + ) + + +@pytest.mark.parametrize("kind", ["edge_condition", "surface_finish"]) +def test_unlowered_required_manufacturing_meaning_is_an_error(kind): + report = extract_pmi_report(_STEP) + source = next(record for record in report.records if record.kind == kind) + raw = PmiFeature( + frame=Frame((0.0, 0.0, 0.0), "z"), + pmi_kind=kind, + value=0.0, + label=source.label, + dominant_axis="?", + source_id=source.source_id, + source_category="manufacturing_requirement", + ) + issues = [ + issue + for issue in lint_pmi_lowering(report, [raw], "annotate") + if source.source_id in issue.source_ids + ] + assert [(issue.code, issue.severity) for issue in issues] == [("pmi_not_lowered", "error")] + + +def test_edge_condition_with_a_face_link_is_not_recast_as_a_document_note(_case): + source, drawing = _case + edge = next( + feature + for feature in drawing.model().features + if isinstance(feature, DocumentNote) + and feature.source_id == source["edge_condition"].source_id + ) + raw = PmiFeature( + frame=edge.frame, + pmi_kind="edge_condition", + value=0.0, + label=edge.text, + dominant_axis="?", + source_id=edge.source_id, + source_category="manufacturing_requirement", + reference_item_ids=("#138",), + ) + model = replace(drawing.model(), features=[raw]) + lowered = lower_ap242_document_requirements(model) + assert len(lowered.features) == 1 + assert isinstance(lowered.features[0], PmiFeature) + assert lowered.features[0].lowering_blockers == ( + "face-specific edge condition cannot be a document-wide note", + ) diff --git a/tests/test_tier_manifest.py b/tests/test_tier_manifest.py index a0cf3552..ed656ccb 100644 --- a/tests/test_tier_manifest.py +++ b/tests/test_tier_manifest.py @@ -63,6 +63,20 @@ def test_source_owner_routes_its_behavior_contract(source, module): assert module in pr_modules(_TESTS, [source]) +@pytest.mark.parametrize( + "source", + ( + "src/draftwright/_geometry.py", + "src/draftwright/annotations/_gdt.py", + "src/draftwright/linting/orchestration.py", + "src/draftwright/registry.py", + "src/draftwright/sheet.py", + ), +) +def test_finish_source_owner_routes_manufacturing_contract(source): + assert "test_pmi_manufacturing_finish.py" in 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_typed_remediation_projection_issue_1759.py b/tests/test_typed_remediation_projection_issue_1759.py index 36d7e897..53069cf9 100644 --- a/tests/test_typed_remediation_projection_issue_1759.py +++ b/tests/test_typed_remediation_projection_issue_1759.py @@ -212,3 +212,14 @@ def test_every_registered_non_axis_finding_has_a_bounded_domain() -> None: ) == "importer-lowering" ) + + +def test_step_title_default_diagnostics_keep_source_responsibility() -> None: + expected = { + "step_general_tolerance_disagreement": "source-ambiguity", + "step_material_ambiguous": "source-ambiguity", + "step_material_disagreement": "source-ambiguity", + "step_material_unavailable": "evidence-unavailable", + } + for code, domain in expected.items(): + assert _lint_remediation_domain({"code": code}, unclassified_codes=frozenset()) == domain