Files
mpm-documentation-kit/_STANDARDS/mpmdoc/figkit.py
T

70 lines
4.1 KiB
Python
Raw Normal View History

2026-11-09 11:51:56 -06:00
"""Shared SVG helpers for figures. Import what you need in a project's figs.py:
from mpmdoc.figkit import T, svg_open, markers, INK, GOLD, KICK, ...
Conventions (MPM_Document_Standard.md §7):
* labels Montserrat 700, uppercase, letter-spaced; values/dimensions JetBrains Mono 700
* strokes INK; brand accent GOLD/KICK; limits and warnings RED (dashed)
* draw at viewBox width 700 for full-width figures, ~240 for step side-figures
"""
from .brand import INK, GOLD, KICK, MUTE, CREAM, LINE, RED, REDBG, GRN, GRNBG, BLU, GRY # noqa: F401
def T(x, y, s, size=8, w=600, fill=INK, anchor="start", rot=None, fam="Montserrat", ls=0.4, halo=False):
"""One <text> element. halo=True adds a white outline so labels read over linework."""
r = f' transform="rotate({rot} {x} {y})"' if rot is not None else ""
h = ' stroke="#fff" stroke-width="3" paint-order="stroke"' if halo else ""
return (f'<text x="{x:.1f}" y="{y:.1f}" font-family="{fam}" font-size="{size}" font-weight="{w}" '
f'fill="{fill}" text-anchor="{anchor}" letter-spacing="{ls}"{r}{h}>{s}</text>')
def svg_open(w, h, grid=False, prefix="f"):
"""Open an <svg> at width 100% with shared markers; grid=True adds the faint drafting grid."""
s = f'<svg viewBox="0 0 {w} {h}" width="100%" xmlns="http://www.w3.org/2000/svg">' + markers(prefix)
if grid:
s += f'<rect width="{w}" height="{h}" fill="url(#{prefix}grid)"/>'
return s
def markers(p="f"):
"""Arrowheads (#{p}a ink, #{p}g gold), hatches (#{p}h grey, #{p}r red) and grid (#{p}grid)."""
return (f'<defs><marker id="{p}a" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">'
f'<path d="M0,1.5 L10,5 L0,8.5 z" fill="{INK}"/></marker>'
f'<marker id="{p}g" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">'
f'<path d="M0,1 L10,5 L0,9 z" fill="{KICK}"/></marker>'
f'<pattern id="{p}h" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">'
f'<line x1="0" y1="0" x2="0" y2="6" stroke="#CFC7BC" stroke-width="1.2"/></pattern>'
f'<pattern id="{p}r" width="5" height="5" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">'
f'<line x1="0" y1="0" x2="0" y2="5" stroke="{RED}" stroke-width="1.3"/></pattern>'
f'<pattern id="{p}grid" width="14" height="14" patternUnits="userSpaceOnUse">'
f'<path d="M14 0H0V14" fill="none" stroke="#EFEAE3" stroke-width="0.7"/></pattern></defs>')
def dim_h(x1, x2, y, label, ext_from=None, p="f", size=8.5):
"""Horizontal dimension line from x1 to x2 at height y with centred label.
ext_from: y the extension lines start from (the feature edge)."""
o = []
if ext_from is not None:
for x in (x1, x2):
o.append(f'<line x1="{x}" y1="{ext_from}" x2="{x}" y2="{y + (4 if y > ext_from else -4)}" stroke="{MUTE}" stroke-width=".6"/>')
o.append(f'<line x1="{x1}" y1="{y}" x2="{x2}" y2="{y}" stroke="{INK}" stroke-width=".8" marker-start="url(#{p}a)" marker-end="url(#{p}a)"/>')
o.append(T((x1 + x2) / 2, y - 4, label, size=size, w=700, anchor="middle", fam="JetBrains Mono", ls=0, halo=True))
return "".join(o)
def dim_v(x, y1, y2, label, ext_from=None, p="f", size=8.5):
"""Vertical dimension line from y1 to y2 at x, label rotated alongside."""
o = []
if ext_from is not None:
for y in (y1, y2):
o.append(f'<line x1="{ext_from}" y1="{y}" x2="{x + (4 if x > ext_from else -4)}" y2="{y}" stroke="{MUTE}" stroke-width=".6"/>')
o.append(f'<line x1="{x}" y1="{y1}" x2="{x}" y2="{y2}" stroke="{INK}" stroke-width=".8" marker-start="url(#{p}a)" marker-end="url(#{p}a)"/>')
cy = (y1 + y2) / 2
o.append(T(x - 5, cy, label, size=size, w=700, anchor="middle", fam="JetBrains Mono", ls=0, rot=-90, halo=True))
return "".join(o)
def tag(x, y, t, r=10):
"""Gold callout bubble matching the drawing schedule tags (B-1, F-1 …)."""
return (f'<circle cx="{x}" cy="{y}" r="{r}" fill="{GOLD}" stroke="{INK}" stroke-width=".8"/>'
+ T(x, y + 3, t, size=7, w=800, anchor="middle", ls=0))