"""mpmdoc.Drawing — 11×17 landscape drawing sheets (DWG). See MPM_Document_Standard.md §9.7. dwg = Drawing(number="DWG-NSH-B17-01", rev="0.1", date="2026-11-06", title="Bay 17 Sign Hanger", subtitle="Mounting arrangement", project="Nashville MCC Bay 17", client="Nashville MCC (WeGo)", location="Nashville, TN", discipline="Mechanical drawing · Mounting", drawn="JS", checked="—", approved="—", slug="Nashville_Bay17_Sign-Hanger", history=[("0.1", "2026-11-06", "Initial issue.")]) dwg.sheet(views=[dwg.view(svg, 1, "Elevation A", "1:10", area="1 / 1 / 3 / 4"), ...], schedule=[("B-1", "Bracket", "6 mm HDG steel", 1)], notes=["All dimensions in mm.", ...], scale="As shown") dwg.build() Each dwg.sheet(...) is one 11×17 page. The view area is a 6-column × 4-row grid; place each view with a CSS grid-area "row-start / col-start / row-end / col-end". Sheet numbers and the title block are automatic. """ import base64 from pathlib import Path from . import brand from .doc import Doc from .units import show_date from . import i18n W_IN, H_IN = 17, 11 CSS = """ @page{size:17in 11in;margin:0} *{box-sizing:border-box} body{margin:0;font-family:'Open Sans',Montserrat,sans-serif;color:var(--ink);-webkit-print-color-adjust:exact} .sheet{width:17in;height:11in;padding:.3in;position:relative;break-after:page;overflow:hidden} .sheet:last-child{break-after:auto} .frame{position:absolute;inset:.3in;border:1.6px solid var(--ink)} .zones{position:absolute;inset:.3in;pointer-events:none} .zh,.zv{position:absolute;font-family:Montserrat;font-weight:700;font-size:7pt;color:var(--mute);display:flex} .zh{left:0;right:0;height:.2in;justify-content:space-around;align-items:center} .zh.t{top:0;border-bottom:1px solid var(--ink)}.zh.b{bottom:0;border-top:1px solid var(--ink)} .zv{top:.2in;bottom:.2in;width:.2in;flex-direction:column;justify-content:space-around;align-items:center} .zv.l{left:0;border-right:1px solid var(--ink)}.zv.r{right:0;border-left:1px solid var(--ink)} .inner{position:absolute;inset:.62in .64in .6in .64in;display:grid;grid-template-columns:1fr 3.35in;grid-template-rows:.62in 1fr;gap:.12in} .hbar{grid-column:1 / 3;display:flex;align-items:center;gap:16px;border-bottom:2px solid var(--gold);padding-bottom:6px} .hbar img{height:.44in} .hbar .disc{font-family:Montserrat;font-weight:800;font-size:10pt;letter-spacing:.14em;text-transform:uppercase;border-left:1.5px solid var(--line);padding-left:16px} .hbar .dno{margin-left:auto;text-align:right;font-family:Montserrat;font-weight:700;font-size:7.5pt;letter-spacing:.12em;color:var(--kick);text-transform:uppercase} .hbar .dno b{display:block;font-family:'JetBrains Mono',Montserrat,monospace;font-size:11pt;color:var(--ink);letter-spacing:0} .views{display:grid;grid-template-columns:repeat(6,1fr);grid-template-rows:repeat(4,1fr);gap:.12in;min-height:0} .view{border:1px solid var(--line);border-radius:4px;display:flex;flex-direction:column;min-height:0;padding:6px 8px 6px} .view .art{flex:1;min-height:0;display:flex;align-items:center;justify-content:center} .view .art svg{max-width:100%;max-height:100%;width:100%;height:100%} .vlab{display:flex;align-items:center;gap:8px;border-top:1px solid var(--line);padding-top:5px;margin-top:4px} .vlab .bub{width:24px;height:24px;border-radius:50%;border:1.6px solid var(--ink);font-family:Montserrat;font-weight:800;font-size:9pt;display:flex;align-items:center;justify-content:center;flex:none} .vlab b{font-family:Montserrat;font-weight:800;font-size:8.5pt;letter-spacing:.08em;text-transform:uppercase;display:block} .vlab small{font-family:'JetBrains Mono',Montserrat,monospace;font-size:7pt;color:var(--mute)} .side{display:flex;flex-direction:column;gap:.1in;min-height:0} .blk{border:1px solid var(--line);border-radius:4px;padding:6px 9px} .blk .kicker{font-size:6.6pt;margin-bottom:4px} .side table{width:100%;border-collapse:collapse;font-size:7.2pt} .side th{font-family:Montserrat;font-weight:700;font-size:6pt;letter-spacing:.1em;text-transform:uppercase;text-align:left;padding:3px 4px;border-bottom:1.5px solid var(--gold)} .side td{padding:2.5px 4px;border-bottom:1px solid var(--line);vertical-align:top} .tag{display:inline-block;min-width:26px;text-align:center;font-family:Montserrat;font-weight:800;font-size:6.6pt;background:var(--gold);border-radius:8px;padding:1px 4px} .notes ol{margin:0;padding-left:15px;font-size:7.2pt}.notes li{margin:1.5px 0} .tb{margin-top:auto;border:1.6px solid var(--ink);border-radius:0;padding:0} .tb .brandrow{display:flex;justify-content:space-between;align-items:center;padding:6px 9px;border-bottom:1px solid var(--ink)} .tb .brandrow img{height:.3in} .tb .brandrow span{font-family:Montserrat;font-weight:600;font-size:6.6pt;color:var(--mute);text-align:right;line-height:1.3} .tb .proj{padding:6px 9px;border-bottom:1px solid var(--ink)} .tb .proj .kicker{font-size:6pt} .tb .proj h1{font-family:Montserrat;font-weight:800;font-size:15pt;line-height:1.05;margin:2px 0 1px} .tb .proj p{margin:0;font-size:7.4pt;color:var(--mute)} .tb .cells{display:grid;grid-template-columns:1fr 1fr 1fr} .tb .cells div{padding:4px 8px;border-bottom:1px solid var(--ink);border-right:1px solid var(--ink)} .tb .cells div:nth-child(3),.tb .cells div:nth-child(6),.tb .cells div:nth-child(8),.tb .cells div:nth-child(10){border-right:none} .tb .cells .w2{grid-column:span 2} .tb .cells b{display:block;font-family:Montserrat;font-weight:700;font-size:5.6pt;letter-spacing:.12em;text-transform:uppercase;color:var(--mute)} .tb .cells span{font-family:'JetBrains Mono',Montserrat,monospace;font-size:8pt;font-weight:700} .tb .revs2{padding:4px 8px;font-size:6.8pt;border-bottom:1px solid var(--ink)} .tb .revs2 .kicker{font-size:5.6pt;margin-bottom:1px} .tb .revs2 div{display:flex;gap:6px}.tb .revs2 code{font-family:'JetBrains Mono',Montserrat,monospace;font-weight:700} .tb .conf{background:var(--ink);color:#fff;display:flex;justify-content:space-between;align-items:center;padding:5px 9px;font-size:6.4pt} .tb .conf em{font-style:normal;font-family:Montserrat;font-weight:700;color:var(--gold-light,#F5CD15)} """ class Drawing(Doc): def __init__(self, number, rev, date, title, subtitle, project, client, slug, history, location="", discipline="Mechanical drawing", drawn="JS", checked="—", approved="—", classification=None, lang="en"): super().__init__(number=number, rev=rev, date=date, title=title, lede=subtitle, meta=[("Project", project), ("Client", client)], slug=slug, history=history, customer=client, classification=classification, lang=lang) if self.type != "DWG": raise ValueError("Drawing numbers must start with DWG-") import inspect self.src_dir = Path(inspect.stack()[1].filename).resolve().parent self.subtitle, self.project, self.client, self.location = subtitle, project, client, location self.discipline, self.drawn, self.checked, self.approved = discipline, drawn, checked, approved self.sheets = [] # ---- building blocks ---------------------------------------------------- def view(self, svg, n, title, scale="N.T.S.", area="1 / 1 / 5 / 7"): sep = ":" if self.lang == "zh" else ": " return (f'
{svg}
' f'
{n}
{title}{self.L("SCALE")}{sep}{self.L(scale)}
') def sheet(self, views, schedule=None, notes=None, scale="As shown", schedule_title="Schedule"): notes = list(notes or []) if self.lang != "en" and not any("以英文版为准" in n for n in notes): notes.append(i18n.governing_note(self.number, self.rev)) self.sheets.append(dict(views=views, schedule=schedule or [], notes=notes, scale=self.L(scale), schedule_title=self.L(schedule_title))) return self def _logo(self, path): return "data:image/png;base64," + base64.b64encode(Path(path).read_bytes()).decode() def _sheet_html(self, i, s, n_sheets): L, D = self.L, self.fmt_date zones_h = "".join(f"{k}" for k in range(8, 0, -1)) zones_v = "".join(f"{k}" for k in "DCBA") sched = "" if s["schedule"]: rows = "".join(f'{t}{d}{desc}{q}' for t, d, desc, q in s["schedule"]) sched = (f'
{s["schedule_title"]}
' f'{rows}
{L("Tag")}{L("Designation")}{L("Description")}{L("Qty")}
') notes = "" if s["notes"]: notes = (f'
{L("General notes")}
    ' + "".join(f"
  1. {x}
  2. " for x in s["notes"]) + "
") revs = "".join(f'
{r}{D(d)}{t}
' for r, d, t in self.history) mark = brand.CLASSIFICATIONS[self.classification] or L("Proprietary to Message Point Media") if self.lang == "zh" and self.classification in i18n.MARK_ZH: mark = f"{i18n.MARK_ZH[self.classification]} {mark}" conf = f"{mark},{L('Do not distribute without authorization.')}" if self.lang == "zh" else f"{mark}. Do not distribute without authorization." tb = f"""
Message Point Media
{L("Irondale, AL")} · messagepoint.tv
{L("Project")} · {self.project}

{self.title}

{self.subtitle}

{L("Client")}{self.client}
{L("Location")}{self.location or "—"}
{L("Scale")}{s["scale"]}
{L("Drawn")}{self.drawn}
{L("Checked")}{self.checked}
{L("Approved")}{self.approved}
{L("Dwg no.")}{self.number}
{L("Sheet")}{i} {"/" if self.lang == "zh" else "of"} {n_sheets}
{L("Rev")}{self.rev} · {L(self.status)}
{L("Date ")}{D(self.date)}
{L("Revision history")}
{revs}
{conf}{brand.TAGLINE}
""" return f"""
{zones_h}
{zones_h}
{zones_v}
{zones_v}
{self.discipline}
{L("Dwg no. · Sheet")}{self.number} · {i}/{n_sheets} · Rev {self.rev}
{"".join(s["views"])}
{sched}{notes}{tb}
""" def html(self): base = (Path(__file__).parent / "mpm.css").read_text() fonts = brand.FONTS_CSS.read_text() n = len(self.sheets) body = "".join(self._sheet_html(i + 1, s, n) for i, s in enumerate(self.sheets)) css = base + CSS hl = "" if self.lang != "en": css, body = i18n.add_fallback(css), i18n.add_fallback(body) fonts += i18n.font_css(self.title + body) hl = ' lang="zh-CN"' return (f'{self.number} · {self.title}' f'{body}') def build(self, out_dir=None, html_dir=None): import inspect from playwright.sync_api import sync_playwright caller = Path(inspect.stack()[1].filename).resolve().parent out_dir = Path(out_dir) if out_dir else caller.parent html_dir = Path(html_dir) if html_dir else caller / "_build" html_dir.mkdir(parents=True, exist_ok=True) hp = html_dir / f"{self.number}.html" hp.write_text(self.html()) self._check_english(out_dir) out = out_dir / self.filename() with sync_playwright() as p: b = p.chromium.launch() pg = b.new_page() pg.goto(hp.resolve().as_uri(), wait_until="load") pg.evaluate("() => document.fonts.ready") pg.wait_for_timeout(150) pg.pdf(path=str(out), width=f"{W_IN}in", height=f"{H_IN}in", print_background=True, margin={"top": "0", "bottom": "0", "left": "0", "right": "0"}) b.close() print(out) return out