Files

190 lines
13 KiB
Python
Raw Permalink Normal View History

2026-11-09 11:51:56 -06:00
"""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