"""ISO 7200 title blocks for every ISO 5457 sheet from A4 to A0. SOURCE ------ Width ISO 7200:2004 -- the title block is 180 mm wide and sits in the lower right-hand corner of the drawing space, against the frame. 180 mm is exactly the drawing-space width of a portrait A4, which is why one block fits every size. Data fields ISO 7200:2004 -- the mandatory fields are the legal owner (`company`), the identification number (`drawing_no`), the date of issue (`date`), the segment/sheet number (`sheet`) and the title (`title`). The rest carried here are optional fields: creator (`drawn_by`), approval person (`checked_by`), version (`revision`), and the supplementary item data this shop draws (`part_no`, `material`, `scale`, `units`). ISO 7200's full list of optional fields is NOT transcribed -- only the fields this block has a cell for. Projection angle ISO 128-30 / ISO 5456-2 -- the truncated-cone symbol; see `projection_symbol_prims` for the arrangement and where it comes from. The row heights (20 / 15 / 15 / 10 mm) and the cell splits are this repository's layout, not ISO 7200 values: the standard fixes the *data*, not the ruling. They are exactly the ones `titleblock_apply_iso_a3` has drawn since v1.0, so the A3 sheet this server makes has not moved. """ from __future__ import annotations from dataclasses import asdict, dataclass from typing import TYPE_CHECKING from engineering.mech.primitives import Circle, Line, Poly, Prim, Pt, Text from engineering.sheet.frames import ( draw_sheet_prims, enter_layout, frame_metrics, frame_prims, ) if TYPE_CHECKING: from backends.base import AutoCADBackend __all__ = [ "LABEL_HEIGHT", "LABEL_PAD_X", "LABEL_PAD_Y", "PROJECTION_ANGLES", "ROW1_H", "ROW2_H", "ROW3_H", "ROW4_H", "ROW4_VALUE_PAD_Y", "TB_HEIGHT", "TB_WIDTH", "TITLEBLOCK_FIELDS", "TITLEBLOCK_RULE_COUNT", "TITLE_HEIGHT", "TITLE_TEXT_INDEX", "VALUE_FIELDS", "VALUE_HEIGHT", "VALUE_PAD_X", "VALUE_PAD_Y", "TitleBlockMetadata", "apply_titleblock", "projection_symbol_prims", "titleblock_origin", "titleblock_prims", "value_text_indices", ] TB_WIDTH = 180.0 TB_HEIGHT = 60.0 ROW1_H = 20.0 # title ROW2_H = 15.0 # drawing_no | revision | sheet ROW3_H = 15.0 # part_no | material | scale ROW4_H = 10.0 # drawn_by | checked_by | date | company LABEL_HEIGHT = 2.0 VALUE_HEIGHT = 3.5 TITLE_HEIGHT = 5.0 LABEL_PAD_X = 1.5 LABEL_PAD_Y = 1.5 VALUE_PAD_X = 3.0 VALUE_PAD_Y = 5.0 ROW4_VALUE_PAD_Y = 1.5 PROJECTION_ANGLES = ("first", "third") PROJECTION_CONE_LENGTH = 8.0 PROJECTION_R = 4.0 PROJECTION_SMALL_R = 2.0 PROJECTION_GAP = 4.0 #: Field -> the label printed in its cell, in the order the cells are drawn. VALUE_FIELDS: tuple[tuple[str, str], ...] = ( ("drawing_no", "DWG NO"), ("revision", "REV"), ("sheet", "SHEET"), ("part_no", "PART NO"), ("material", "MATERIAL"), ("scale", "SCALE"), ("drawn_by", "DRAWN"), ("checked_by", "CHECKED"), ("date", "DATE"), ("company", "COMPANY"), ) #: Every ISO 7200 data field this block carries. `units` has no cell of its own #: (the sheet is millimetres and the drawing says so), but it round-trips in the #: payload so a reader is never told the drawing is unitless. TITLEBLOCK_FIELDS: tuple[str, ...] = ( "title", "drawing_no", "part_no", "material", "scale", "units", "drawn_by", "checked_by", "date", "sheet", "revision", "company", ) #: The block frame plus the ten rules, at the head of the primitive tuple. TITLEBLOCK_RULE_COUNT = 11 TITLE_TEXT_INDEX = 11 @dataclass class TitleBlockMetadata: """ISO 7200 data fields. Strings, written verbatim -- no transformation.""" title: str drawing_no: str part_no: str = "" material: str = "" scale: str = "1:1" units: str = "mm" drawn_by: str = "" checked_by: str = "" date: str = "" sheet: str = "1/1" revision: str = "A" company: str = "Anka-Makine" def titleblock_origin(size: str, *, orientation: str = "landscape") -> Pt: """Lower-left corner of the title block, sheet-local.""" metrics = frame_metrics(size, orientation=orientation) x0, y0, x1, _y1 = metrics["frame"] if x1 - x0 < TB_WIDTH - 1e-9: raise ValueError( f"{metrics['size']} {orientation}: the drawing area is {x1 - x0:.1f} mm wide, " f"narrower than the ISO 7200 title block ({TB_WIDTH:.0f} mm)" ) return (x1 - TB_WIDTH, y0) def projection_symbol_prims(at: Pt, angle: str = "first") -> tuple[Prim, ...]: """The ISO 128-30 projection-angle symbol, centred on `at`. Arrangement, and where it comes from. The symbol is a truncated cone with a horizontal axis and its small end facing left. The first- and third-angle symbols are mirror images about a vertical axis, and which is which follows from the projection rule itself: first-angle projection puts the view seen from the left ON THE RIGHT, and the view of this cone seen from the left is the small circle inside the large one. So FIRST angle draws the cone on the left and the concentric circles on the right; THIRD angle is its mirror. The proportions (large circle diameter 8, small 4, cone 8 mm long) are this module's; ISO 5456-2's proportion table is not transcribed. The arrangement is the part that can be *wrong* on a drawing, and it is derived above. """ if angle not in PROJECTION_ANGLES: raise ValueError(f"projection angle must be one of {PROJECTION_ANGLES}, got {angle!r}") cx, cy = float(at[0]), float(at[1]) half = (PROJECTION_CONE_LENGTH + PROJECTION_GAP + 2 * PROJECTION_R) / 2.0 sign = 1.0 if angle == "first" else -1.0 cone_far = -half * sign cone_near = cone_far + sign * PROJECTION_CONE_LENGTH circles_x = (half - PROJECTION_R) * sign cone = Poly( points=( (cx + cone_far, cy - PROJECTION_SMALL_R), (cx + cone_near, cy - PROJECTION_R), (cx + cone_near, cy + PROJECTION_R), (cx + cone_far, cy + PROJECTION_SMALL_R), ), closed=True, ) axis = Line(p1=(cx - half - 1.0, cy), p2=(cx + half + 1.0, cy), role="center") return ( cone, Circle(center=(cx + circles_x, cy), radius=PROJECTION_R), Circle(center=(cx + circles_x, cy), radius=PROJECTION_SMALL_R), axis, ) def value_text_indices() -> dict[str, int]: """Field -> its index in the tuple `titleblock_prims` returns. The cells are drawn label-then-value, starting right after the title, so the value of the i-th field is at TITLE_TEXT_INDEX + 2i + 2. """ return {field: TITLE_TEXT_INDEX + 2 * i + 2 for i, (field, _label) in enumerate(VALUE_FIELDS)} def titleblock_prims( size: str, fields, *, orientation: str = "landscape", projection: str | None = "first", ) -> tuple[Prim, ...]: """The title block as primitive descriptions, sheet-local. Order is pinned and read back by `value_text_indices`: the block frame, the ten rules, the title, then (label, value) per cell in `VALUE_FIELDS` order, then the four projection-symbol primitives when `projection` is not None. """ meta = fields if isinstance(fields, TitleBlockMetadata) else TitleBlockMetadata(**fields) if projection is not None and projection not in PROJECTION_ANGLES: raise ValueError( f"projection must be one of {PROJECTION_ANGLES} or None, got {projection!r}" ) tb_x0, tb_y0 = titleblock_origin(size, orientation=orientation) tb_x1, tb_y1 = tb_x0 + TB_WIDTH, tb_y0 + TB_HEIGHT row4_top = tb_y0 + ROW4_H row3_top = row4_top + ROW3_H row2_top = row3_top + ROW2_H r2_split1 = tb_x0 + TB_WIDTH * 0.5 r2_split2 = tb_x0 + TB_WIDTH * 0.75 r3_split1 = tb_x0 + TB_WIDTH / 3.0 r3_split2 = tb_x0 + TB_WIDTH * 2.0 / 3.0 r4_split1 = tb_x0 + TB_WIDTH * 0.25 r4_split2 = tb_x0 + TB_WIDTH * 0.50 r4_split3 = tb_x0 + TB_WIDTH * 0.75 prims: list[Prim] = [ Poly( points=((tb_x0, tb_y0), (tb_x1, tb_y0), (tb_x1, tb_y1), (tb_x0, tb_y1)), closed=True, ), Line(p1=(tb_x0, row4_top), p2=(tb_x1, row4_top)), Line(p1=(tb_x0, row3_top), p2=(tb_x1, row3_top)), Line(p1=(tb_x0, row2_top), p2=(tb_x1, row2_top)), Line(p1=(r2_split1, row3_top), p2=(r2_split1, row2_top)), Line(p1=(r2_split2, row3_top), p2=(r2_split2, row2_top)), Line(p1=(r3_split1, row4_top), p2=(r3_split1, row3_top)), Line(p1=(r3_split2, row4_top), p2=(r3_split2, row3_top)), Line(p1=(r4_split1, tb_y0), p2=(r4_split1, row4_top)), Line(p1=(r4_split2, tb_y0), p2=(r4_split2, row4_top)), Line(p1=(r4_split3, tb_y0), p2=(r4_split3, row4_top)), ] title_x = tb_x0 + TB_WIDTH * 0.5 - (len(meta.title) * TITLE_HEIGHT * 0.3) title_y = row2_top + (ROW1_H - TITLE_HEIGHT) / 2.0 prims.append(Text(at=(title_x, title_y), text=meta.title, height=TITLE_HEIGHT)) # field -> (cell left edge, the row's top rule, the value's baseline) cells: dict[str, tuple[float, float, float]] = { "drawing_no": (tb_x0, row2_top, row3_top + VALUE_PAD_Y), "revision": (r2_split1, row2_top, row3_top + VALUE_PAD_Y), "sheet": (r2_split2, row2_top, row3_top + VALUE_PAD_Y), "part_no": (tb_x0, row3_top, row4_top + VALUE_PAD_Y), "material": (r3_split1, row3_top, row4_top + VALUE_PAD_Y), "scale": (r3_split2, row3_top, row4_top + VALUE_PAD_Y), "drawn_by": (tb_x0, row4_top, tb_y0 + ROW4_VALUE_PAD_Y), "checked_by": (r4_split1, row4_top, tb_y0 + ROW4_VALUE_PAD_Y), "date": (r4_split2, row4_top, tb_y0 + ROW4_VALUE_PAD_Y), "company": (r4_split3, row4_top, tb_y0 + ROW4_VALUE_PAD_Y), } for field, label in VALUE_FIELDS: left, row_top, value_y = cells[field] prims.append( Text( at=(left + LABEL_PAD_X, row_top - LABEL_HEIGHT - LABEL_PAD_Y), text=label, height=LABEL_HEIGHT, ) ) prims.append( Text(at=(left + VALUE_PAD_X, value_y), text=getattr(meta, field), height=VALUE_HEIGHT) ) if projection is not None: prims.extend(projection_symbol_prims((tb_x0 + 15.0, row2_top + ROW1_H / 2.0), projection)) return tuple(prims) async def apply_titleblock( backend: AutoCADBackend, *, size: str = "A3", metadata: TitleBlockMetadata, origin: Pt = (0.0, 0.0), orientation: str = "landscape", projection: str | None = "first", frame: bool = True, zones: bool = False, marks: bool = False, layout: str | None = None, ) -> dict: """Draw the ISO 5457 frame (optionally) and the ISO 7200 title block. `projection=None`, `zones=False` and `marks=False` reproduce exactly what `titleblock_apply_iso_a3` has always drawn. Every refusal -- an unknown size, an unknown orientation, an unknown projection angle, a missing layout -- fires before the first entity reaches the drawing. """ metrics = frame_metrics(size, orientation=orientation) tb_prims = titleblock_prims(size, metadata, orientation=orientation, projection=projection) sheet_prims = ( frame_prims(size, orientation=orientation, zones=zones, marks=marks) if frame else () ) previous = await enter_layout(backend, layout, caller="titleblock_apply") try: frame_handles = await draw_sheet_prims(backend, sheet_prims, origin=origin) tb_handles = await draw_sheet_prims(backend, tb_prims, origin=origin) finally: if layout is not None: await backend.layout_set_current(previous) ox, oy = float(origin[0]), float(origin[1]) w, h = metrics["sheet"] symbol_start = TITLE_TEXT_INDEX + 1 + 2 * len(VALUE_FIELDS) return { "ok": True, "size": metrics["size"], "orientation": orientation, "layout": layout or "Model", "projection": projection, "outer_border": frame_handles[0] if frame else None, "inner_border": frame_handles[1] if frame else None, "frame_handles": frame_handles, "titleblock_lines": tb_handles[:TITLEBLOCK_RULE_COUNT], "title_text": tb_handles[TITLE_TEXT_INDEX], "value_texts": {f: tb_handles[i] for f, i in value_text_indices().items()}, "projection_symbol": tb_handles[symbol_start:], "metadata": asdict(metadata), "bbox": {"min": [ox, oy], "max": [ox + w, oy + h]}, }