Files
mpm-documentation-kit/_STANDARDS/mpmdoc/sheet.py
T
Jason StedwellandClaude Opus 5.5 5b52636003 MPM Documentation Kit v1.3 (CW-034)
MPM Document Standard 1.3, mpmdoc build engine, templates for TN, INS, SOP,
BOM, SVY, TAP, DWG, TSB, PRO, SOW and WO (Chinese editions for TN and DWG),
glossary, MPM fonts and logos, and the mpm-technical-documents plugin 1.3.0.
Worked examples ship in the kit zip on Drive only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWatTMmqmGatgHjDZvWs5b
2026-11-09 11:51:56 -06:00

191 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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'<div class="view" style="grid-area:{area}"><div class="art">{svg}</div>'
f'<div class="vlab"><div class="bub">{n}</div><div><b>{title}</b><small>{self.L("SCALE")}{sep}{self.L(scale)}</small></div></div></div>')
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"<span>{k}</span>" for k in range(8, 0, -1))
zones_v = "".join(f"<span>{k}</span>" for k in "DCBA")
sched = ""
if s["schedule"]:
rows = "".join(f'<tr><td><span class="tag">{t}</span></td><td><b>{d}</b></td><td>{desc}</td><td class="r">{q}</td></tr>'
for t, d, desc, q in s["schedule"])
sched = (f'<div class="blk"><div class="kicker">{s["schedule_title"]}</div><table>'
f'<tr><th>{L("Tag")}</th><th>{L("Designation")}</th><th>{L("Description")}</th><th>{L("Qty")}</th></tr>{rows}</table></div>')
notes = ""
if s["notes"]:
notes = (f'<div class="blk notes"><div class="kicker">{L("General notes")}</div><ol>'
+ "".join(f"<li>{x}</li>" for x in s["notes"]) + "</ol></div>")
revs = "".join(f'<div><code>{r}</code><span>{D(d)}</span><span>{t}</span></div>' 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"""<div class="tb">
<div class="brandrow"><img src="{self._logo(brand.LOGO_HEADER)}"><span>Message Point Media<br>{L("Irondale, AL")} · messagepoint.tv</span></div>
<div class="proj"><div class="kicker">{L("Project")} · {self.project}</div><h1>{self.title}</h1><p>{self.subtitle}</p></div>
<div class="cells">
<div><b>{L("Client")}</b><span>{self.client}</span></div><div><b>{L("Location")}</b><span>{self.location or "—"}</span></div><div><b>{L("Scale")}</b><span>{s["scale"]}</span></div>
<div><b>{L("Drawn")}</b><span>{self.drawn}</span></div><div><b>{L("Checked")}</b><span>{self.checked}</span></div><div><b>{L("Approved")}</b><span>{self.approved}</span></div>
<div class="w2"><b>{L("Dwg no.")}</b><span>{self.number}</span></div><div><b>{L("Sheet")}</b><span>{i} {"/" if self.lang == "zh" else "of"} {n_sheets}</span></div>
<div><b>{L("Rev")}</b><span>{self.rev} · {L(self.status)}</span></div><div class="w2"><b>{L("Date ")}</b><span>{D(self.date)}</span></div>
</div>
<div class="revs2"><div class="kicker">{L("Revision history")}</div>{revs}</div>
<div class="conf"><span>{conf}</span><em>{brand.TAGLINE}</em></div>
</div>"""
return f"""<div class="sheet"><div class="frame"></div>
<div class="zones"><div class="zh t">{zones_h}</div><div class="zh b">{zones_h}</div><div class="zv l">{zones_v}</div><div class="zv r">{zones_v}</div></div>
<div class="inner">
<div class="hbar"><img src="{self._logo(brand.LOGO_HEADER)}"><div class="disc">{self.discipline}</div>
<div class="dno">{L("Dwg no. · Sheet")}<b>{self.number} · {i}/{n_sheets} · Rev {self.rev}</b></div></div>
<div class="views">{"".join(s["views"])}</div>
<div class="side">{sched}{notes}{tb}</div>
</div></div>"""
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'<!doctype html><html{hl}><head><meta charset="utf-8"><title>{self.number} · {self.title}</title>'
f'<style>{fonts}{css}</style></head><body>{body}</body></html>')
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