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
This commit is contained in:
143 files changed
+4097
No files matched your search
@@ -0,0 +1,190 @@
|
||||
"""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
|
||||
Reference in new issue
Block a user