190 lines
13 KiB
Python
190 lines
13 KiB
Python
"""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
|