#!/usr/bin/env python3
"""
FIG. R8 — EXPLODED ASSEMBLY · OPTION 3 — SINGLE-PIVOT TILTING TABLE (owner-favored)
PRSM Phase-1 ground-mount · INHERITED racking adaptation
(SOLAR_RACKING_HANDOFF §5; Vandewetering/Hayibo/Pearce, Designs 2022 6(3):41;
tilting-pivot-mount.md + tilting-bom-reconciliation.md consensus BOM)
VISUALIZATION ONLY of an inherited engineering ESTIMATE — not a stamped calc.
ENERGY figures are script-computed (15_tilt_sweep.py) and used verbatim.
The only electrical items are the cross-pivot BOND jumper (>=10 AWG, cited) and
the PV SERVICE LOOPS — no new electrical numbers invented.
Adapted from the Fixed-55 "tiered-cards" exploded template (exploded_final.py).
Same iso() projection, box/cyl helpers, shading, palette, layout — only the
CONTENT changes for the SINGLE-PIVOT TILT option:
STAGE PLAN
1 FOOTINGS — ~6-8 holes (~HALF the base) on dedicated post-pairs
2 PIVOT POSTS — ~6-8 dedicated >=6x6 pairs (NO 4x4 fronts)
3 PIVOT MECH — through-bolt + steel bushings + positive wind-lock + bond
braid + PV service loop (the HEADLINE stage, exploded)
4 RAILS/JOISTS— 2x10 rotating frame (largely no continuous 2x8 beam line)
5 BLOCKS+PANELS — attach blocks + 14x Waaree on the rotating plane, tilt arc,
"~390 ft2 PLANE = A SAIL · POSITIVE LOCK MANDATORY" note
"""
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon, FancyBboxPatch, Circle, Ellipse, FancyArrow, Arc
from matplotlib.lines import Line2D
from matplotlib import font_manager
import numpy as np
# ----------------------------------------------------------------------------
# FONTS — mono field-guide feel.
# ----------------------------------------------------------------------------
MONO = "DejaVu Sans Mono"
for cand in ("IBM Plex Mono",):
try:
font_manager.findfont(cand, fallback_to_default=False)
MONO = cand
break
except Exception:
pass
plt.rcParams["font.family"] = "monospace"
plt.rcParams["font.monospace"] = [MONO, "DejaVu Sans Mono"]
plt.rcParams["svg.fonttype"] = "none"
# ----------------------------------------------------------------------------
# PALETTE (field-guide; same as template)
# ----------------------------------------------------------------------------
PAPER = "#F4F0E7"
SURFACE = "#FCFAF4"
SURFACE2 = "#F9F5EE"
INK = "#271F18"
INK2 = "#50473F"
INK3 = "#797269"
RULE = "#D3CFC6"
RULE2 = "#B6B1A9"
AMBER = "#AE692D"
AMBER_SOFT= "#EBDECF"
PINE = "#316843"
PINE_SOFT = "#DDE0D3"
PINE_STK = "#091E10"
PINE_LITE = "#7FA587"
SLATE = "#456783"
SLATE_SOFT= "#D7E0E6"
CLAY = "#B0512F" # safety / positive-lock accent (per racking.css --clay role)
def hx(c):
c = c.lstrip("#")
return tuple(int(c[i:i+2], 16) / 255 for i in (0, 2, 4))
def darken(c, k):
r, g, b = hx(c)
return (r * k, g * k, b * k)
# ----------------------------------------------------------------------------
# ISOMETRIC PROJECTION (true 2:1 iso)
# ----------------------------------------------------------------------------
COS30 = 0.86602540378
SIN30 = 0.5
def iso(X, Y, Z, S=1.0, ox=0.0, oy=0.0):
return (ox + S * (X - Y) * COS30,
oy + S * (-(X + Y) * SIN30 + Z))
K_TOP, K_LEFT, K_RIGHT = 1.00, 0.82, 0.66
def box(ax, X, Y, Z, dx, dy, dz, fill, stroke, lw=1.5, S=1.0, ox=0.0, oy=0.0,
z=2, alpha=1.0, top_k=None, left_k=None, right_k=None):
kt = K_TOP if top_k is None else top_k
kl = K_LEFT if left_k is None else left_k
kr = K_RIGHT if right_k is None else right_k
def P(x, y, zz):
return iso(x, y, zz, S, ox, oy)
c = [P(X, Y, Z), P(X+dx, Y, Z), P(X+dx, Y+dy, Z), P(X, Y+dy, Z),
P(X, Y, Z+dz), P(X+dx, Y, Z+dz), P(X+dx, Y+dy, Z+dz), P(X, Y+dy, Z+dz)]
base = hx(fill)
right = [c[1], c[2], c[6], c[5]]
ax.add_patch(Polygon(right, closed=True, facecolor=tuple(v*kr for v in base),
edgecolor=stroke, linewidth=lw, zorder=z, joinstyle="miter",
alpha=alpha))
left = [c[0], c[1], c[5], c[4]]
ax.add_patch(Polygon(left, closed=True, facecolor=tuple(v*kl for v in base),
edgecolor=stroke, linewidth=lw, zorder=z+0.1, joinstyle="miter",
alpha=alpha))
top = [c[4], c[5], c[6], c[7]]
ax.add_patch(Polygon(top, closed=True, facecolor=tuple(v*kt for v in base),
edgecolor=stroke, linewidth=lw, zorder=z+0.2, joinstyle="miter",
alpha=alpha))
return c
def cyl(ax, X, Y, Z, dia, depth, fill, stroke, lw=1.4, S=1.0, ox=0.0, oy=0.0, z=2,
hatch=None, side_k=K_LEFT):
r = dia / 2.0
cxt, cyt = iso(X, Y, Z, S, ox, oy)
cxb, cyb = iso(X, Y, Z - depth, S, ox, oy)
ew = dia * S * 1.0
eh = dia * S * SIN30
base = hx(fill)
side = [(cxt - ew/2, cyt), (cxt + ew/2, cyt),
(cxb + ew/2, cyb), (cxb - ew/2, cyb)]
ax.add_patch(Polygon(side, closed=True, facecolor=tuple(v*side_k for v in base),
edgecolor=stroke, linewidth=lw, zorder=z, hatch=hatch))
ax.add_patch(Ellipse((cxb, cyb), ew, eh, facecolor=tuple(v*K_RIGHT for v in base),
edgecolor=stroke, linewidth=lw*0.8, zorder=z+0.05))
ax.add_patch(Ellipse((cxt, cyt), ew, eh, facecolor=tuple(v*K_TOP for v in base),
edgecolor=stroke, linewidth=lw, zorder=z+0.3))
rng = np.random.default_rng(int(abs(X*97 + Y*131)))
for _ in range(7):
fx = cxt + (rng.random()-0.5)*ew*0.7
fy = cyt + (rng.random()-0.5)*eh*0.7
ax.add_patch(Circle((fx, fy), ew*0.018, facecolor=INK3, alpha=0.4,
edgecolor="none", zorder=z+0.4))
return (cxt, cyt)
# ----------------------------------------------------------------------------
# DRESSED-LUMBER CROSS SECTIONS (meters)
# ----------------------------------------------------------------------------
IN = 0.0254
S66 = 5.5 * IN # 6x6
S44 = 3.5 * IN # 4x4 (NOT used as a structural front here — reference only)
T2 = 1.5 * IN # nominal 2x thickness
D10 = 9.25 * IN # 2x10
D8 = 7.25 * IN # 2x8
D6 = 5.5 * IN # 2x6
D4 = 3.5 * IN # 2x4
BAY = 16.1 / 7.0 # 2.30 m
DEPTH = 1.30 # front->back run of the rotating frame footprint
BACK_TOP = 2.23
FRONT_TOP = 0.50
# Single-pivot option: pivot-post PAIRS, one pair per sub-section.
# Representative 2-of-7-bay section -> show 2 post-pairs framing one tilt bay
# plus the start of the next -> 3 post rows (X positions) along width.
N_PAIRS = 3
SPAN_X = 2 * BAY # 4.60 m
# Pivot axis height (the plane bears on the axle, mid-height of the >=6x6 posts)
PIVOT_Z = 1.35
# ----------------------------------------------------------------------------
# CALL-OUT BADGE + LEADER helper
# ----------------------------------------------------------------------------
def callout(ax, anchor_xy, badge_xy, num, z=30, color=INK, ring=SURFACE):
ax_pt, ay = anchor_xy
bx, by = badge_xy
ax.add_line(Line2D([ax_pt, bx], [ay, by], color=INK3, lw=1.0,
linestyle=(0, (3, 3)), zorder=z))
ax.add_patch(Circle((ax_pt, ay), 2.6, facecolor=INK, edgecolor="none", zorder=z+0.5))
ax.add_patch(Circle((bx, by), 10.5, facecolor=ring, edgecolor=color,
linewidth=1.5, zorder=z+1))
ax.text(bx, by, num, family="monospace",
fontsize=8.5 if len(num) == 1 else 7.2,
color=color, ha="center", va="center", zorder=z+2, weight="bold")
# ============================================================================
# FIGURE
# ============================================================================
fig = plt.figure(figsize=(13, 16.4), dpi=200)
fig.patch.set_facecolor(PAPER)
ax = fig.add_axes([0, 0, 1, 1])
ax.set_facecolor(PAPER)
ax.set_xlim(0, 1300)
ax.set_ylim(0, 1640)
ax.set_aspect("equal")
ax.axis("off")
YSHIFT = 40
LEFT_W = 1010
RIGHT_X = 1028
# ---- TOP STRIP: fig label + title --------------------------------------------
ax.text(34, 1560 + YSHIFT, "FIG. R8 · OPTION 3 — SINGLE-PIVOT TILT · EXPLODED ASSEMBLY",
family="monospace", fontsize=11.5, color=INK3, weight="bold")
ax.add_line(Line2D([34, 990], [1548 + YSHIFT, 1548 + YSHIFT], color=RULE2, lw=1.0))
ax.text(34, 1524 + YSHIFT, "OPTION 3 — SINGLE-PIVOT TILTING TABLE (OWNER-FAVORED)",
family="monospace", fontsize=15.5, color=INK, weight="bold")
ax.text(34, 1500 + YSHIFT,
"REPRESENTATIVE 2 OF 7 BAYS · 14 × WAAREE 575 W · ROTATING PLANE · DEDICATED PIVOT-POST PAIRS",
family="monospace", fontsize=9.8, color=INK2)
# energy strip (script-computed, verbatim from 15_tilt_sweep.py)
ax.text(34, 1481 + YSHIFT,
"ENERGY (15_tilt_sweep.py): 11,023 kWh/yr · +577 (+5.5%) gross vs fixed 55° · "
"monthly ≈ 4-position (4→monthly adds ≈ 0)",
family="monospace", fontsize=8.6, color=PINE, weight="bold")
# ---- STAMP (top-right, estimate/amber style) ---------------------------------
stamp_w, stamp_h = 268, 56
sx0, sy0 = LEFT_W - stamp_w, 1500 + YSHIFT
stamp = FancyBboxPatch((sx0, sy0), stamp_w, stamp_h,
boxstyle="round,pad=2,rounding_size=6",
facecolor=AMBER_SOFT, edgecolor=AMBER, linewidth=1.4,
zorder=20)
ax.add_patch(stamp)
ax.add_patch(Circle((sx0 + 16, sy0 + stamp_h/2), 4.5, facecolor=AMBER,
edgecolor="none", zorder=21))
ax.text(sx0 + 30, sy0 + stamp_h/2 + 14, "INHERITED · NOT STAMPED",
family="monospace", fontsize=8.8, color=AMBER, weight="bold",
va="center", zorder=21)
ax.text(sx0 + 30, sy0 + stamp_h/2, "ENGINEER REVIEW BEFORE POUR",
family="monospace", fontsize=8.8, color=AMBER, weight="bold",
va="center", zorder=21)
ax.text(sx0 + 30, sy0 + stamp_h/2 - 14, "PIVOT/LOCK/BUSHING = ENGINEER-SET (A7)",
family="monospace", fontsize=7.6, color=AMBER, weight="bold",
va="center", zorder=21)
# ============================================================================
# STAGE VIGNETTES (instruction-sheet tiers)
# ============================================================================
S = 48.0
TIER_X0 = 34
TIER_X1 = LEFT_W - 6
TIER_TOP = 1456 + YSHIFT
TIER_BOT = 70 + YSHIFT
N_TIERS = 5
GAP = 18
tier_h = (TIER_TOP - TIER_BOT - GAP * (N_TIERS - 1)) / N_TIERS
def tier_rect(i):
y1 = TIER_TOP - i * (tier_h + GAP)
y0 = y1 - tier_h
return (TIER_X0, y0, TIER_X1 - TIER_X0, tier_h)
def draw_card(i, num, name, sub):
x0, y0, w, h = tier_rect(i)
card = FancyBboxPatch((x0, y0), w, h,
boxstyle="round,pad=0,rounding_size=8",
facecolor=SURFACE, edgecolor=RULE2, linewidth=1.2,
zorder=1)
ax.add_patch(card)
bx, by = x0 + 34, y0 + h - 32
ax.add_patch(Circle((bx, by), 19, facecolor=SURFACE2, edgecolor=INK2,
linewidth=1.6, zorder=5))
ax.text(bx, by, str(num), family="monospace", fontsize=17, color=INK,
weight="bold", ha="center", va="center", zorder=6)
ax.text(x0 + 64, y0 + h - 22, name, family="monospace", fontsize=12.5,
color=INK, weight="bold", va="center")
ax.text(x0 + 64, y0 + h - 40, sub, family="monospace", fontsize=9.0,
color=INK2, va="center")
return (x0, y0, w, h)
def stage_label_chip(x0, y0, h, callouts):
cx = x0 + 64
cyy = y0 + h - 66
for c in callouts:
ax.add_patch(Circle((cx, cyy), 10.5, facecolor=SURFACE, edgecolor=INK2,
linewidth=1.3, zorder=6))
ax.text(cx, cyy, c, family="monospace", fontsize=8.5, color=INK,
ha="center", va="center", zorder=7)
cx += 28
def down_arrow(i):
x0, y0, w, h = tier_rect(i)
ax_pt = x0 + 150
ya = y0 - 3
yb = y0 - GAP + 3
ax.add_patch(FancyArrow(ax_pt, ya, 0, (yb - ya), width=2.4,
head_width=12, head_length=8, length_includes_head=True,
facecolor=INK3, edgecolor="none", zorder=8, alpha=0.9))
# ----------------------------------------------------------------------------
# VIGNETTE BUILDERS
# ----------------------------------------------------------------------------
def ground_plane(ox, oy, x_lo=-0.55, x_hi=None, alpha=0.5):
if x_hi is None:
x_hi = SPAN_X + 0.55
pts = [iso(x_lo, 0.0, 0.0, S, ox, oy),
iso(x_hi, 0.0, 0.0, S, ox, oy),
iso(x_hi, DEPTH, 0.0, S, ox, oy),
iso(x_lo, DEPTH, 0.0, S, ox, oy)]
ax.add_patch(Polygon(pts, closed=True, facecolor=SURFACE2, edgecolor=RULE2,
linewidth=1.0, zorder=1.5, alpha=alpha))
ax.add_line(Line2D([pts[0][0], pts[1][0]], [pts[0][1], pts[1][1]],
color=INK, lw=1.6, zorder=1.7))
return pts
def vignette_origin(x0, y0, w, h):
# taller pivot geometry -> shift left + sit lower so the structure rises
# into the card and the wide 2-bay array stays inside the right edge
ox = x0 + w * 0.42
oy = y0 + h * 0.30
return ox, oy
def centered_origin(rect, zlo, zhi, x_lo=-0.55, x_hi=None,
header=46, margin=10, fx=0.50, scale=None):
"""Center a stage's *projected* iso bounding box inside the card's drawing
area (below the header). iso() is linear → screen bbox = min/max over the
world-AABB corners; center THAT so each stage sits centered regardless of how
its geometry extends. `scale` lets a stage with a local scale (footings SF)
center correctly."""
sc = S if scale is None else scale
x0, y0, w, h = rect
if x_hi is None:
x_hi = SPAN_X + 0.55
corners = [(X, Y, Z) for X in (x_lo, x_hi) for Y in (0.0, DEPTH)
for Z in (zlo, zhi)]
sxs = [sc * (X - Y) * COS30 for (X, Y, Z) in corners]
sys = [sc * (-(X + Y) * SIN30 + Z) for (X, Y, Z) in corners]
cx0 = (min(sxs) + max(sxs)) / 2.0
cy0 = (min(sys) + max(sys)) / 2.0
tcx = x0 + w * fx
tcy = y0 + margin + (h - header - margin) / 2.0
return tcx - cx0, tcy - cy0
# per-stage vertical world extent (Z) for the iso stages that use the loop origin
STAGE_Z3 = {
"draw_pivot_posts": (0.0, PIVOT_Z + 0.30),
"draw_rails": (0.0, PIVOT_Z + 0.70),
"draw_panels": (0.0, PIVOT_Z + 0.85),
}
# Pivot-post pair Y positions (front and back of each pair, straddling the axis)
PAIR_DY = 0.34 # spacing front<->back within a post pair
PY_FRONT = DEPTH/2 - PAIR_DY/2
PY_BACK = DEPTH/2 + PAIR_DY/2
def draw_footings(ox, oy):
"""STAGE 1 — ~6-8 holes, ~HALF the base. Post-PAIR footings (two close
holes per pivot-post pair) instead of separate back+front rows."""
# Footings extend ~1.4 m BELOW grade. The 2-bay-wide array + the downward
# piers are TALLER than the card at the full scale, so this stage uses a
# local reduced scale + raised origin to keep all buried geometry + the
# BURIED depth dimension INSIDE the card (no spill into stage 2).
x0c, y0c, wc, hc = tier_rect(0)
SF = 44.0
ox, oy = centered_origin((x0c, y0c, wc, hc), -1.10, 0.30, scale=SF, fx=0.50)
DEP = 1.10 # drawn pier depth (BURIED dim says 4–5 ft)
for p in range(N_PAIRS):
X = p * BAY
for Y in (PY_BACK, PY_FRONT):
cyl(ax, X, Y, 0.0, 0.32, DEP, "#9C8A6E", INK2, lw=1.3, S=SF,
ox=ox, oy=oy, z=3, hatch="////", side_k=0.62)
# local-scale grade plane (ground_plane() uses the global S, so build here)
pts = [iso(-0.55, 0.0, 0.0, SF, ox, oy), iso(SPAN_X+0.55, 0.0, 0.0, SF, ox, oy),
iso(SPAN_X+0.55, DEPTH, 0.0, SF, ox, oy), iso(-0.55, DEPTH, 0.0, SF, ox, oy)]
ax.add_patch(Polygon(pts, closed=True, facecolor=SURFACE2, edgecolor=RULE2,
linewidth=1.0, zorder=4.5, alpha=0.5))
ax.add_line(Line2D([pts[0][0], pts[1][0]], [pts[0][1], pts[1][1]],
color=INK, lw=1.6, zorder=4.7))
gx0, gy0 = pts[0]; gx1, gy1 = pts[1]
band_h = 14
soil = [(gx0, gy0), (gx1, gy1), (gx1, gy1 - band_h), (gx0, gy0 - band_h)]
ax.add_patch(Polygon(soil, closed=True, facecolor="#E4D8C2", edgecolor=INK2,
linewidth=0.8, hatch="....", zorder=4.6, alpha=0.9))
for p in range(N_PAIRS):
X = p * BAY
for Y in (PY_BACK, PY_FRONT):
cyl(ax, X, Y, 0.10, 0.32, 0.10, SURFACE, INK2, lw=1.3, S=SF,
ox=ox, oy=oy, z=5)
# frost line
fx0, fy0 = iso(-0.55, 0.0, -0.95, SF, ox, oy)
fx1, fy1 = iso(SPAN_X + 0.55, 0.0, -0.95, SF, ox, oy)
ax.add_line(Line2D([fx0, fx1], [fy0, fy1], color=SLATE, lw=1.4,
linestyle=(0, (6, 4)), zorder=4))
ax.text(fx1 + 6, fy1, "frost ~1.2 m", family="monospace", fontsize=8,
color=SLATE, va="center", zorder=6)
# depth dimension on near-right pier
Xd = (N_PAIRS - 1) * BAY
dim_x = iso(Xd, PY_FRONT, 0.0, SF, ox, oy)[0] + 0.36 * SF * COS30 + 14
yg = iso(Xd, PY_FRONT, 0.0, SF, ox, oy)[1]
yb = iso(Xd, PY_FRONT, -DEP, SF, ox, oy)[1]
ax.add_line(Line2D([dim_x, dim_x], [yg, yb], color=INK2, lw=1.2, zorder=6))
for yy, dy in ((yg, -7), (yb, 7)):
ax.add_patch(Polygon([(dim_x, yy), (dim_x - 3.2, yy + dy),
(dim_x + 3.2, yy + dy)], closed=True,
facecolor=INK2, edgecolor="none", zorder=6))
ax.add_line(Line2D([iso(Xd,PY_FRONT,0,SF,ox,oy)[0], dim_x], [yg, yg],
color=INK2, lw=0.7, linestyle=(0,(2,2)), zorder=6))
ax.add_line(Line2D([iso(Xd,PY_FRONT,-DEP,SF,ox,oy)[0], dim_x], [yb, yb],
color=INK2, lw=0.7, linestyle=(0,(2,2)), zorder=6))
ax.text(dim_x + 7, (yg + yb)/2, "BURIED\n4–5 ft", family="monospace",
fontsize=8, color=INK2, va="center", zorder=6, weight="bold")
# spec + KEY NOTE: stacked in the OPEN left column, clear of the sub-title
# band above and the drawing to the right (fixes the label/title collision).
x0c, y0c, wc, hc = tier_rect(0)
ax.text(x0c + 60, y0c + hc*0.64, "30 MPa pier · Ø320 mm",
family="monospace", fontsize=8, color=INK2, ha="left",
va="center", zorder=8)
ax.text(x0c + 60, y0c + hc*0.46,
"≈ HALF THE BASE — POST PAIRS\n~6–8 holes (vs base 16)",
family="monospace", fontsize=8.4, color=AMBER, weight="bold",
ha="left", va="center", zorder=8)
ah = iso((N_PAIRS-1)*BAY, PY_BACK, 0.18, SF, ox, oy)
callout(ax, ah, (ah[0] - 70, ah[1] + 34), "F")
def draw_pivot_posts(ox, oy):
"""STAGE 2 — dedicated >=6x6 pivot-post PAIRS. NO 4x4 fronts.
Both members of each pair are 6x6, taller (to PIVOT_Z + headroom) so the
plane can rotate on the axle."""
ground_plane(ox, oy, alpha=0.5)
for p in range(N_PAIRS):
X = p * BAY
for Y in (PY_FRONT, PY_BACK):
cx, cy = iso(X, Y, 0.0, S, ox, oy)
ax.add_patch(Ellipse((cx, cy), 0.32*S, 0.32*S*SIN30,
facecolor=SURFACE2, edgecolor=RULE2,
linewidth=1.0, zorder=2))
post_top = PIVOT_Z + 0.20
for p in range(N_PAIRS):
X = p * BAY
for Y in (PY_BACK, PY_FRONT):
zb = 4 + (0 if Y == PY_BACK else 4) # near pair member draws over far
box(ax, X - S66/2, Y - S66/2, 0.0, S66, S66, post_top,
AMBER_SOFT, AMBER, lw=2.0, S=S, ox=ox, oy=oy, z=zb)
# pivot-hole mark near the top of each post (where the axle passes)
hx_, hy_ = iso(X, Y, PIVOT_Z, S, ox, oy)
ax.add_patch(Circle((hx_, hy_), 3.0, facecolor=SURFACE,
edgecolor=INK, linewidth=1.3, zorder=zb+1))
# note: ALL >=6x6, no 4x4 — placed in the OPEN upper-left of the card.
x0c, y0c, wc, hc = tier_rect(1)
ax.text(x0c + 60, y0c + hc*0.62,
"ALL ≥6×6 PIVOT-POST PAIRS\nNO 4×4 FRONTS",
family="monospace", fontsize=8.4, color=AMBER, weight="bold",
ha="left", va="center", zorder=10)
# pivot-axis reference line through the post-top holes
a0 = iso(-0.4, DEPTH/2, PIVOT_Z, S, ox, oy)
a1 = iso(SPAN_X + 0.4, DEPTH/2, PIVOT_Z, S, ox, oy)
ax.add_line(Line2D([a0[0], a1[0]], [a0[1], a1[1]], color=SLATE, lw=1.2,
linestyle=(0,(5,3)), zorder=9))
ax.text(a1[0] + 6, a1[1], "pivot axis", family="monospace", fontsize=7.6,
color=SLATE, va="center", zorder=9)
callout(ax, iso((N_PAIRS-1)*BAY, PY_FRONT, PIVOT_Z*0.6, S, ox, oy),
(iso((N_PAIRS-1)*BAY, PY_FRONT, PIVOT_Z*0.6, S, ox, oy)[0]+62,
iso((N_PAIRS-1)*BAY, PY_FRONT, PIVOT_Z*0.6, S, ox, oy)[1]-22), "1")
def draw_pivot_mech(ox, oy):
"""STAGE 3 — THE HEADLINE. One pivot crossing shown IN CONTEXT on the left,
with a large EXPLODED axle detail spread horizontally across the right so
each engineered-steel part is separated + clearly labeled:
through-bolt + steel bushings (sleeve BOTH post & frame holes) + positive
wind-lock pin/strut (frame->post) + flexible braided-Cu bond jumper + UV
PV service loop."""
x0c, y0c, wc, hc = tier_rect(2)
# ---- LEFT: one post pair IN CONTEXT, with the crossing assembled ----
# local origin for this context vignette (left third of the card); sit it
# LOWER so the structure + labels clear the header + chip row.
cox = x0c + wc*0.34
coy = y0c + hc*0.22
Xc = 0.0
post_top = PIVOT_Z + 0.20
ground_plane(cox, coy, x_lo=Xc-0.7, x_hi=Xc+0.7, alpha=0.40)
for Y in (PY_BACK, PY_FRONT):
zb = 4 + (0 if Y == PY_BACK else 4)
box(ax, Xc - S66/2, Y - S66/2, 0.0, S66, S66, post_top,
AMBER_SOFT, AMBER, lw=1.6, S=S, ox=cox, oy=coy, z=zb)
pcx, pcy = iso(Xc, PY_FRONT, PIVOT_Z, S, cox, coy) # front-post pivot point
pbx, pby = iso(Xc, PY_BACK, PIVOT_Z, S, cox, coy) # back-post pivot point
# --- rotating 2x10 frame rail stub bearing on the axle (tilted up-back) ---
ang = np.deg2rad(48)
rail_len = 0.85
r_lo = iso(Xc, PY_FRONT - 0.10, PIVOT_Z - rail_len*0.30*np.sin(ang), S, cox, coy)
r_hi = iso(Xc, PY_FRONT - 0.10, PIVOT_Z + rail_len*np.sin(ang), S, cox, coy)
ax.add_line(Line2D([r_lo[0], r_hi[0]], [r_lo[1], r_hi[1]], color=PINE_STK,
lw=11, zorder=24, solid_capstyle="butt"))
ax.add_line(Line2D([r_lo[0], r_hi[0]], [r_lo[1], r_hi[1]], color=AMBER,
lw=7, zorder=24.1, solid_capstyle="butt"))
ax.text(r_hi[0] + 14, r_hi[1] + 2, "rotating\n2×10 rail",
family="monospace", fontsize=6.8, color=AMBER, va="center",
ha="left", zorder=28, weight="bold")
# --- POSITIVE WIND-LOCK pin/strut: frame -> back post (clay, mandatory) ---
lock_top = iso(Xc, PY_FRONT - 0.10, PIVOT_Z + rail_len*0.62*np.sin(ang), S, cox, coy)
lock_bot = iso(Xc, PY_BACK, 0.62, S, cox, coy)
ax.add_line(Line2D([lock_top[0], lock_bot[0]], [lock_top[1], lock_bot[1]],
color=CLAY, lw=3.6, zorder=25, solid_capstyle="round"))
for fr in (0.0, 0.5, 1.0):
ph = iso(Xc, PY_BACK, 0.55 + fr*0.55, S, cox, coy)
ax.add_patch(Circle((ph[0], ph[1]), 2.4, facecolor=SURFACE,
edgecolor=CLAY, linewidth=1.3, zorder=25.5))
# --- flexible braided-Cu BOND jumper across the pivot (pine) ---
bj_x = np.linspace(pcx - 8, pbx + 8, 40)
bj_y = np.linspace(pcy, pby, 40) + 8*np.sin(np.linspace(0, np.pi, 40))
ax.add_line(Line2D(bj_x, bj_y, color=PINE, lw=2.4, zorder=26))
for lx0, ly0 in ((bj_x[0], bj_y[0]), (bj_x[-1], bj_y[-1])):
ax.add_patch(Circle((lx0, ly0), 3.0, facecolor=PINE, edgecolor=PINE_STK,
linewidth=1.0, zorder=26.1))
# --- UV PV SERVICE LOOP at the crossing (slate coil), below the axle ---
loop_cx, loop_cy = pcx - 6, pcy - 30
th = np.linspace(0, 2.4*np.pi, 80)
rr = 2 + th*1.2
slx = loop_cx + rr*np.cos(th)
sly = loop_cy + rr*np.sin(th)*SIN30
ax.add_line(Line2D(slx, sly, color=SLATE, lw=2.0, zorder=23.5))
# pivot point marker
ax.add_patch(Circle((pcx, pcy), 4.0, facecolor=SLATE_SOFT, edgecolor=SLATE,
linewidth=2.0, zorder=27))
# context callouts (kept short, leadered down-left so they don't cross the
# exploded detail on the right)
callout(ax, ((lock_top[0]+lock_bot[0])/2, (lock_top[1]+lock_bot[1])/2),
(lock_top[0] + 24, lock_top[1] - 18), "3c", color=CLAY)
ax.text(lock_top[0] + 38, lock_top[1] - 18, "positive wind-lock\n(NOT friction/winch)",
family="monospace", fontsize=6.8, color=CLAY, va="center",
ha="left", zorder=31, weight="bold")
callout(ax, (bj_x[20], bj_y[20]), (bj_x[20] - 70, bj_y[20] + 26), "B", color=PINE)
ax.text(bj_x[20] - 84, bj_y[20] + 26, "≥10 AWG\nbraided-Cu bond",
family="monospace", fontsize=6.8, color=PINE, va="center",
ha="right", zorder=31)
callout(ax, (slx[40], sly[40]), (loop_cx - 64, loop_cy - 6), "L", color=SLATE)
ax.text(loop_cx - 78, loop_cy - 6, "PV service\nloop", family="monospace",
fontsize=6.8, color=SLATE, va="center", ha="right", zorder=31)
# ---- RIGHT: large EXPLODED axle detail, parts spread horizontally ----
ex_y = y0c + hc*0.46
ex_x0 = x0c + wc*0.50
ex_dx = (wc*0.46) / 4.0 # spacing between the 5 exploded stations
stations = [ex_x0 + k*ex_dx for k in range(5)]
# the exploded assembly axis (dashed) tying the stations together
ax.add_line(Line2D([stations[0]-18, stations[-1]+34], [ex_y, ex_y],
color=INK3, lw=1.0, linestyle=(0,(4,3)), zorder=21))
def part_circle(cx, r, fc, ec, lw=1.8, z=30):
ax.add_patch(Circle((cx, ex_y), r, facecolor=fc, edgecolor=ec,
linewidth=lw, zorder=z))
# station 0: POST hole (in wood)
part_circle(stations[0], 13, SURFACE2, INK2, lw=1.6, z=30)
ax.text(stations[0], ex_y - 26, "POST\nHOLE", family="monospace", fontsize=6.6,
color=INK2, ha="center", va="center", zorder=32, weight="bold")
# station 1: steel bushing (post side)
part_circle(stations[1], 12, SLATE_SOFT, SLATE, lw=2.6, z=30)
part_circle(stations[1], 6.5, SURFACE, SLATE, lw=1.4, z=30.1)
ax.text(stations[1], ex_y - 26, "STEEL\nBUSHING", family="monospace", fontsize=6.6,
color=SLATE, ha="center", va="center", zorder=32, weight="bold")
# station 2: frame hole + its bushing
part_circle(stations[2], 13, AMBER_SOFT, AMBER, lw=1.8, z=30)
part_circle(stations[2], 12, SLATE_SOFT, SLATE, lw=2.6, z=30.1)
part_circle(stations[2], 6.5, SURFACE, SLATE, lw=1.4, z=30.2)
ax.text(stations[2], ex_y - 26, "FRAME +\nBUSHING", family="monospace", fontsize=6.6,
color=SLATE, ha="center", va="center", zorder=32, weight="bold")
# station 3: washer
ax.add_patch(Ellipse((stations[3], ex_y), 9, 22, facecolor=SLATE_SOFT,
edgecolor=SLATE, linewidth=1.6, zorder=30))
ax.text(stations[3], ex_y - 26, "WASHER", family="monospace", fontsize=6.6,
color=SLATE, ha="center", va="center", zorder=32, weight="bold")
# station 4: hex nut/bolt head
hexpts = [(stations[4] + 11*np.cos(a), ex_y + 11*np.sin(a))
for a in np.linspace(0, 2*np.pi, 7)]
ax.add_patch(Polygon(hexpts, closed=True, facecolor=INK, edgecolor=INK,
linewidth=1.0, zorder=31))
ax.text(stations[4], ex_y - 26, "HEX\nNUT", family="monospace", fontsize=6.6,
color=INK, ha="center", va="center", zorder=32, weight="bold")
# the THROUGH-BOLT rod running through all stations
ax.add_line(Line2D([stations[0]-20, stations[4]], [ex_y, ex_y], color=INK,
lw=6.0, zorder=33, solid_capstyle="round"))
ax.add_line(Line2D([stations[0]-20, stations[4]], [ex_y, ex_y], color=SLATE,
lw=2.4, zorder=33.1))
# exploded callouts (leadered UP, into clear paper above the detail row)
callout(ax, (stations[0], ex_y), (stations[0], ex_y + 58), "3a", color=SLATE)
ax.text(stations[0], ex_y + 70, "through-bolt pivot", family="monospace",
fontsize=7.0, color=INK2, ha="center", va="bottom", zorder=31)
callout(ax, (stations[1], ex_y), (stations[2], ex_y + 60), "3b", color=SLATE)
ax.text(stations[2]+18, ex_y + 60, "steel bushings sleeve BOTH\nholes (anti-wallow)",
family="monospace", fontsize=7.0, color=INK2, ha="left",
va="center", zorder=31)
# headline label — top-right corner of the card, clear of the detail
ax.text(x0c + wc - 16, y0c + hc - 14,
"ENGINEERED-STEEL PIVOT MECHANISM\nanti-wallow · positive lock · bonded",
family="monospace", fontsize=8.0, color=INK, weight="bold",
ha="right", va="top", zorder=12)
# ----------------------------------------------------------------------------
# SHARED TILTED-PLANE GEOMETRY (stages 4 & 5)
# The rotating frame pivots about the axle at PIVOT_Z. Drawn as a CONTAINED
# tilted table: a representative plane length + a MODERATED draw angle so the
# plane reads in iso instead of collapsing to an edge-on sliver. The true
# 55°/4-position tilt is conveyed by the tilt-arc + labels in stage 5.
# t in [0,1]: 0 = front-LOW edge, 1 = back-HIGH edge. off lifts along normal.
# ----------------------------------------------------------------------------
PLANE_L = 1.05 # representative plane length (drawn, m)
DRAW_ANG = np.deg2rad(24) # opened draw angle so plane reads, not edge-on
def plane_pt(X, t, ox, oy, lift=0.0):
s = (t - 0.5) * PLANE_L
Yc, Zc = DEPTH/2, PIVOT_Z
Y = Yc + s*np.cos(DRAW_ANG)
Z = Zc + s*np.sin(DRAW_ANG) + lift
return iso(X, Y, Z, S, ox, oy)
def context_posts(ox, oy, lw=1.4, z0=3):
"""Pivot-post pairs as built context (amber), with the pivot axis line."""
post_top = PIVOT_Z + 0.20
for p in range(N_PAIRS):
X = p * BAY
# draw far member first, then near, so overlap reads
for Y in (PY_BACK, PY_FRONT):
zb = z0 + (0 if Y == PY_BACK else 0.6)
box(ax, X - S66/2, Y - S66/2, 0.0, S66, S66, post_top,
AMBER_SOFT, AMBER, lw=lw, S=S, ox=ox, oy=oy, z=zb)
# pivot-hole mark at the axle
hx_, hy_ = iso(X, Y, PIVOT_Z, S, ox, oy)
ax.add_patch(Circle((hx_, hy_), 2.6, facecolor=SURFACE,
edgecolor=INK2, linewidth=1.0, zorder=zb+1))
a0 = iso(-0.45, DEPTH/2, PIVOT_Z, S, ox, oy)
a1 = iso(SPAN_X + 0.45, DEPTH/2, PIVOT_Z, S, ox, oy)
ax.add_line(Line2D([a0[0], a1[0]], [a0[1], a1[1]], color=SLATE, lw=1.1,
linestyle=(0,(5,3)), zorder=2.5))
def draw_rails(ox, oy):
"""STAGE 4 — the ROTATING 2x10 frame (rails/joists). The continuous 2x8
beam line is largely ELIMINATED (plane bears on the axle); show only ~short
bearing headers at the pivot posts. Rails drawn TILTED about the pivot."""
ground_plane(ox, oy, alpha=0.5)
context_posts(ox, oy, lw=1.5, z0=4)
# SHORT bearing headers (~0-8) at the pivot crossings — small, not a beam line
for p in range(N_PAIRS):
X = p * BAY
hp = iso(X, DEPTH/2, PIVOT_Z, S, ox, oy)
ax.add_patch(FancyBboxPatch((hp[0]-9, hp[1]-5), 18, 10,
boxstyle="round,pad=0,rounding_size=2",
facecolor=SURFACE, edgecolor=INK, linewidth=1.4,
zorder=12))
# the rotating 2x10 frame as iso rail BOXES tilted about the axle.
# Rails run X-wise (across the width, the long members the panels bolt to);
# joists run front->back. We draw 2 long rails (top & bottom edge of plane)
# + intermediate purlins, all as thin tilted slabs so it reads as a frame.
edge_t = (0.0, 1.0)
# two long rails spanning full width at the low + high edges
for t in edge_t:
a = plane_pt(-0.20, t, ox, oy); b = plane_pt(SPAN_X + 0.20, t, ox, oy)
# thickness offset (down the normal) to give the rail a visible depth
a2 = plane_pt(-0.20, t, ox, oy, lift=-0.10)
b2 = plane_pt(SPAN_X + 0.20, t, ox, oy, lift=-0.10)
face = [a, b, b2, a2]
ax.add_patch(Polygon(face, closed=True,
facecolor=tuple(v*K_RIGHT for v in hx(AMBER_SOFT)),
edgecolor=AMBER, linewidth=1.6, zorder=9.6))
ax.add_line(Line2D([a[0], b[0]], [a[1], b[1]], color=PINE_STK, lw=2.2,
zorder=10, solid_capstyle="butt"))
# mid purlin (lighter)
a = plane_pt(-0.20, 0.5, ox, oy); b = plane_pt(SPAN_X + 0.20, 0.5, ox, oy)
ax.add_line(Line2D([a[0], b[0]], [a[1], b[1]], color=AMBER, lw=2.0,
zorder=9.8, solid_capstyle="butt"))
# front->back joists at each column (the 2x10 the panels sit on)
for X in [p*BAY for p in range(N_PAIRS)]:
lo = plane_pt(X, 0.0, ox, oy); hi = plane_pt(X, 1.0, ox, oy)
lo2 = plane_pt(X, 0.0, ox, oy, lift=-0.09); hi2 = plane_pt(X, 1.0, ox, oy, lift=-0.09)
side = [lo, hi, hi2, lo2]
ax.add_patch(Polygon(side, closed=True,
facecolor=tuple(v*K_LEFT for v in hx(AMBER_SOFT)),
edgecolor=AMBER, linewidth=1.5, zorder=10.3))
ax.add_line(Line2D([lo[0], hi[0]], [lo[1], hi[1]], color=PINE_STK, lw=2.0,
zorder=10.5, solid_capstyle="butt"))
# labels — placed in the OPEN LEFT of the card, clear of the geometry
x0c, y0c, wc, hc = tier_rect(3)
ax.text(x0c + 60, y0c + hc*0.50,
"ROTATING 2×10 FRAME\n(one tilt sub-bay)",
family="monospace", fontsize=8.4, color=AMBER, va="center",
ha="left", weight="bold", zorder=14)
ax.text(x0c + 60, y0c + hc*0.27,
"long 2×8 beam line\nELIMINATED — the\nplane bears on the\naxle (~0–8 short\nbearing headers)",
family="monospace", fontsize=7.8, color=INK2, va="center",
ha="left", zorder=14)
# callouts
callout(ax, plane_pt((N_PAIRS-1)*BAY, 0.62, ox, oy),
(plane_pt((N_PAIRS-1)*BAY, 0.62, ox, oy)[0]+64,
plane_pt((N_PAIRS-1)*BAY, 0.62, ox, oy)[1]+22), "2")
ax.text(plane_pt((N_PAIRS-1)*BAY, 0.62, ox, oy)[0]+78,
plane_pt((N_PAIRS-1)*BAY, 0.62, ox, oy)[1]+22, "2×10 rail",
family="monospace", fontsize=7.2, color=INK2, va="center", zorder=31)
hp = iso((N_PAIRS-1)*BAY, DEPTH/2, PIVOT_Z, S, ox, oy)
callout(ax, (hp[0], hp[1]), (hp[0]+58, hp[1]-30), "H", color=INK)
ax.text(hp[0]+72, hp[1]-30, "bearing header",
family="monospace", fontsize=7.2, color=INK2, va="center", zorder=31)
def draw_panels(ox, oy):
"""STAGE 5 — attach blocks + 14x Waaree on the ROTATING plane. Tilt arc +
'A SAIL · POSITIVE LOCK MANDATORY' safety note. Module-bolt+WEEB callout."""
ground_plane(ox, oy, alpha=0.45)
context_posts(ox, oy, lw=1.2, z0=3)
# context rails (light) on the tilted plane
for X in [p*BAY for p in range(N_PAIRS)]:
lo = plane_pt(X, 0.0, ox, oy); hi = plane_pt(X, 1.0, ox, oy)
ax.add_line(Line2D([lo[0], hi[0]], [lo[1], hi[1]], color=RULE2, lw=2.0,
zorder=6))
# PANELS on the tilted plane: 2 columns (X) x 2 rows (lower/upper along slope)
rows = [(0.0, 0.5), (0.5, 1.0)]
for col in range(2):
x_a = col * BAY + 0.02
x_b = (col+1) * BAY - 0.02
for ri, (ta, tb) in enumerate(rows):
quad = [plane_pt(x_a, ta, ox, oy, 0.04), plane_pt(x_b, ta, ox, oy, 0.04),
plane_pt(x_b, tb, ox, oy, 0.04), plane_pt(x_a, tb, ox, oy, 0.04)]
ax.add_patch(Polygon(quad, closed=True, facecolor=PINE,
edgecolor=PINE_STK, linewidth=1.6, zorder=14))
for tt in (0.25, 0.5, 0.75):
ga = plane_pt(x_a + (x_b-x_a)*tt, ta, ox, oy, 0.05)
gb = plane_pt(x_a + (x_b-x_a)*tt, tb, ox, oy, 0.05)
ax.add_line(Line2D([ga[0], gb[0]], [ga[1], gb[1]],
color=PINE_LITE, lw=1.0, alpha=0.85, zorder=15))
gm = 0.5
ga = plane_pt(x_a, ta + (tb-ta)*gm, ox, oy, 0.05)
gb = plane_pt(x_b, ta + (tb-ta)*gm, ox, oy, 0.05)
ax.add_line(Line2D([ga[0], gb[0]], [ga[1], gb[1]],
color=PINE_LITE, lw=1.0, alpha=0.85, zorder=15))
# inter-panel seam
sa = plane_pt(x_a, 0.5, ox, oy, 0.06); sb = plane_pt(x_b, 0.5, ox, oy, 0.06)
ax.add_line(Line2D([sa[0], sb[0]], [sa[1], sb[1]], color=SURFACE, lw=3.0, zorder=16))
ax.add_line(Line2D([sa[0], sb[0]], [sa[1], sb[1]], color=PINE_STK, lw=1.4, zorder=16.1))
# module-attach blocks at the lower edge
for p in range(N_PAIRS):
X = p * BAY
bp = plane_pt(X, 0.0, ox, oy, 0.0)
ax.add_patch(Circle((bp[0], bp[1]), 3.2, facecolor=AMBER_SOFT,
edgecolor=AMBER, linewidth=1.0, zorder=17))
# TILT ARC about the pivot — placed in the OPEN LEFT of the card, clear of
# the plane, the bottom strip + scale bar.
pivot_scr = iso(-1.05, DEPTH/2, PIVOT_Z - 0.10, S, ox, oy)
R = 0.74 * S
for adeg, lab in ((22, "30°"), (44, "45°"), (66, "55°")):
d = np.array([np.cos(np.deg2rad(adeg)), np.sin(np.deg2rad(adeg))])
tip = (pivot_scr[0] + R*d[0], pivot_scr[1] + R*d[1])
ax.add_line(Line2D([pivot_scr[0], tip[0]], [pivot_scr[1], tip[1]],
color=INK3, lw=1.0, linestyle=(0,(3,2)), zorder=18))
ax.text(tip[0]+4, tip[1]+3, lab, family="monospace", fontsize=6.8,
color=INK3, va="center", zorder=18)
arc = Arc((pivot_scr[0], pivot_scr[1]), 2*R, 2*R, angle=0,
theta1=22, theta2=66, color=INK2, lw=1.4, zorder=18)
ax.add_patch(arc)
ax.add_patch(Circle((pivot_scr[0], pivot_scr[1]), 3.5, facecolor=INK,
edgecolor=SURFACE, linewidth=1.2, zorder=19))
ax.text(pivot_scr[0]+R*0.5, pivot_scr[1]-16, "tilt range\n(4-position)",
family="monospace", fontsize=6.8, color=INK2, ha="center",
va="top", zorder=19)
# SAFETY note banner — pill at the TOP of the card, above the plane, full
# clear of geometry.
x0c, y0c, wc, hc = tier_rect(4)
bx_c, by_c = x0c + wc*0.50, y0c + hc - 60
btxt = "⚠ ≈390 ft² PLANE = A SAIL · POSITIVE LOCK MANDATORY"
ax.add_patch(FancyBboxPatch((bx_c - 198, by_c - 13), 396, 26,
boxstyle="round,pad=0,rounding_size=6",
facecolor="#F1DDD5",
edgecolor=CLAY, linewidth=1.3, zorder=19.5))
ax.text(bx_c, by_c, btxt, family="monospace", fontsize=8.6, color=CLAY,
weight="bold", ha="center", va="center", zorder=20)
# module bolt + WEEB callout (lower-right edge of the plane)
am = plane_pt((N_PAIRS-1)*BAY, 0.0, ox, oy, 0.04)
callout(ax, am, (am[0] + 58, am[1] - 26), "M")
ax.text(am[0] + 72, am[1] - 26, "¼\" bolt + WEEB", family="monospace",
fontsize=7.2, color=INK2, va="center", zorder=31)
# attach-block callout (lower-left edge of the plane)
ab = plane_pt(0.0, 0.0, ox, oy, 0.0)
callout(ax, ab, (ab[0] - 60, ab[1] - 24), "A", color=AMBER)
ax.text(ab[0] - 74, ab[1] - 24, "attach block", family="monospace",
fontsize=7.2, color=AMBER, ha="right", va="center", zorder=31)
# ---- face-on INSET: one 2-high pair (countable) ----
iw, ih = 84, 146
ix0 = x0c + wc - iw - 26
iy0 = oy - 0.10 * S
ax.add_patch(FancyBboxPatch((ix0, iy0), iw, ih,
boxstyle="round,pad=0,rounding_size=4",
facecolor=SURFACE2, edgecolor=RULE2,
linewidth=1.0, zorder=25))
ax.text(ix0 + iw/2, iy0 + ih + 9, "1 PAIR (2-HIGH)", family="monospace",
fontsize=6.8, color=INK3, ha="center", va="center", zorder=27)
pad = 8; pw = iw - 2*pad; ph = (ih - 3*pad) / 2.0
for r in range(2):
py0 = iy0 + pad + r*(ph + pad)
ax.add_patch(FancyBboxPatch((ix0+pad, py0), pw, ph,
boxstyle="round,pad=0,rounding_size=2",
facecolor=PINE, edgecolor=PINE_STK,
linewidth=1.2, zorder=26))
for cc in range(1, 6):
gx = ix0 + pad + pw*cc/6.0
ax.add_line(Line2D([gx, gx], [py0+2, py0+ph-2],
color=PINE_LITE, lw=0.8, alpha=0.8, zorder=27))
for rr in range(1, 4):
gy = py0 + ph*rr/4.0
ax.add_line(Line2D([ix0+pad+2, ix0+pad+pw-2], [gy, gy],
color=PINE_LITE, lw=0.8, alpha=0.8, zorder=27))
sy = iy0 + pad + ph + pad/2.0
ax.add_line(Line2D([ix0+pad, ix0+pad+pw], [sy, sy], color=INK2, lw=1.6, zorder=28))
# ----------------------------------------------------------------------------
# RENDER THE 5 STAGES
# ----------------------------------------------------------------------------
stages = [
(1, "FOOTINGS", "~6–8 holes · ≈ HALF the base · post pairs", ["F"], draw_footings),
(2, "PIVOT POSTS", "~6–8 dedicated ≥6×6 pairs · NO 4×4 fronts", ["1"], draw_pivot_posts),
(3, "PIVOT MECHANISM", "through-bolt · steel bushings · positive lock · bond · loop",
["3a", "3b", "3c", "B", "L"], draw_pivot_mech),
(4, "RAILS / JOISTS (ROTATING)", "2×10 frame · 2×8 beam line eliminated",
["2", "H"], draw_rails),
(5, "BLOCKS + PANELS (ON PLANE)", "attach blocks · 14× Waaree 575 W · tilt arc",
["A", "M"], draw_panels),
]
for idx, (num, name, sub, calls, fn) in enumerate(stages):
x0, y0, w, h = draw_card(idx, num, name, sub)
stage_label_chip(x0, y0, h, calls)
if fn.__name__ in STAGE_Z3: # iso stages center via the loop origin
zlo, zhi = STAGE_Z3[fn.__name__]
ox, oy = centered_origin((x0, y0, w, h), zlo, zhi, fx=0.48)
else: # footings & pivot-mech self-place
ox, oy = vignette_origin(x0, y0, w, h)
fn(ox, oy)
if idx < len(stages) - 1:
down_arrow(idx)
# ============================================================================
# RIGHT COLUMN — BOM LEGEND BOX (reconciliation consensus)
# ============================================================================
BOX_X = RIGHT_X
BOX_W = 1300 - RIGHT_X - 18
BOX_Y = 96 + YSHIFT
BOX_H = 1394
ax.add_patch(FancyBboxPatch((BOX_X, BOX_Y), BOX_W, BOX_H,
boxstyle="round,pad=0,rounding_size=6",
facecolor=SURFACE, edgecolor=RULE2, linewidth=1.2,
zorder=10))
hb_h = 30
ax.add_patch(FancyBboxPatch((BOX_X, BOX_Y + BOX_H - hb_h), BOX_W, hb_h,
boxstyle="round,pad=0,rounding_size=6",
facecolor=SURFACE2, edgecolor=RULE2, linewidth=1.0,
zorder=11))
ax.text(BOX_X + BOX_W/2, BOX_Y + BOX_H - hb_h/2, "BILL OF MATERIALS · OPTION 3",
family="monospace", fontsize=10.5, color=INK3, weight="bold",
ha="center", va="center", zorder=12)
# MOST MATERIAL-INTENSIVE banner just under the header
ban_h = 40
ax.add_patch(FancyBboxPatch((BOX_X + 6, BOX_Y + BOX_H - hb_h - ban_h - 4), BOX_W - 12, ban_h,
boxstyle="round,pad=0,rounding_size=4",
facecolor=AMBER_SOFT, edgecolor=AMBER, linewidth=1.2,
zorder=11.5))
ax.text(BOX_X + BOX_W/2, BOX_Y + BOX_H - hb_h - ban_h/2 - 4,
"MOST MATERIAL-INTENSIVE OPTION\n$360 video = disclaimed light-panel build",
family="monospace", fontsize=6.8, color=AMBER, weight="bold",
ha="center", va="center", zorder=12)
AMB = ("amber", AMBER, AMBER_SOFT)
BM = ("beam", INK, SURFACE)
PN = ("pine", PINE, PINE_SOFT)
HW = ("slate", SLATE, SLATE)
FT = ("foot", INK2, SURFACE2)
# badge, name, spec, qty, family, e-set flag
bom = [
("C1", "Pivot posts", "≥6×6 MCA pairs", "~6–8", AMB, True),
("C2", "Rails / joists", "2×10 MCA · rotates", "~14", AMB, False),
("C3", "Pivot through-bolts", "Ø/grade · SS/HDG", "~6–8", HW, True),
("C4", "Steel bushings", "sleeve BOTH holes", "~12–16", HW, True),
("C5", "Positive-lock assy", "pins/struts → post", "~6–8", HW, True),
("C6", "Cross-pivot bond", "braided-Cu ≥10 AWG", "~6–8", PN, False),
("C7", "PV service loops", "UV PV 8 + 10AWG EGC", "/cross", HW, False),
("F", "Footings", "30 MPa Ø320 · ≈half base", "~18–24", FT, True),
("B", "Beams (headers)", "2×8 mostly ELIMINATED", "~0–8", BM, False),
("A", "Module blocks", "offcut MCA", "~56", AMB, False),
("M", "Module bolts+WEEB", "¼\"×2½\" SS + WEEB", "~56", HW, False),
]
row_top = BOX_Y + BOX_H - hb_h - ban_h - 18
note_zone = 210
row_area = (row_top - (BOX_Y + note_zone))
rh = row_area / len(bom)
for i, (badge, name, spec, qty, fam, eset) in enumerate(bom):
ry = row_top - i * rh - rh/2
if i > 0:
ax.add_line(Line2D([BOX_X + 10, BOX_X + BOX_W - 10],
[ry + rh/2, ry + rh/2], color=RULE,
lw=0.8, zorder=11))
ax.add_patch(Circle((BOX_X + 26, ry + 8), 11, facecolor=SURFACE,
edgecolor=INK2, linewidth=1.3, zorder=13))
ax.text(BOX_X + 26, ry + 8, badge, family="monospace",
fontsize=9.5 if len(badge) == 1 else 7.6,
color=INK, ha="center", va="center", zorder=14)
ax.add_patch(Circle((BOX_X + 26, ry - 12), 4.5, facecolor=fam[1],
edgecolor="none", zorder=14))
ax.text(BOX_X + 46, ry + 11, name, family="monospace", fontsize=10.2,
color=INK, weight="bold", va="center", zorder=13)
ax.text(BOX_X + 46, ry - 5, spec, family="monospace", fontsize=7.8,
color=INK2, va="center", zorder=13)
# qty (right) — and e-set tag if applicable
ax.text(BOX_X + BOX_W - 12, ry + 6, qty, family="monospace", fontsize=9.0,
color=INK, weight="bold", ha="right", va="center", zorder=13)
if eset:
ax.text(BOX_X + BOX_W - 12, ry - 9, "⚙ ENG-SET", family="monospace",
fontsize=6.6, color=CLAY, ha="right", va="center", zorder=13,
weight="bold")
ny = BOX_Y + note_zone - 8
ax.add_line(Line2D([BOX_X + 10, BOX_X + BOX_W - 10], [ny, ny],
color=RULE2, lw=1.0, zorder=12))
note = ("structure INHERITED (Vandewetering/\n"
"Hayibo/Pearce 2022 6(3):41) + video,\n"
"via tilting-bom-reconciliation.md —\n"
"estimate, NOT stamped. Pivot/lock/\n"
"bushing = ENGINEER-SET (A7).\n"
"Energy = 15_tilt_sweep.py (computed).")
ax.text(BOX_X + 14, ny - 12, note, family="monospace", fontsize=7.6,
color=INK3, va="top", zorder=12)
# swatch key
keys = [("amber=timber", AMBER), ("beam=2×8", INK), ("pine=panel/bond", PINE),
("slate=steel/elec", SLATE), ("clay=positive lock", CLAY)]
kx = BOX_X + 14
ky = BOX_Y + 14
for lbl, col in keys:
ax.add_patch(Polygon([(kx, ky), (kx+12, ky), (kx+12, ky+10), (kx, ky+10)],
closed=True, facecolor=col, edgecolor=INK2,
linewidth=0.6, zorder=13))
ax.text(kx + 16, ky + 5, lbl, family="monospace", fontsize=7.4,
color=INK2, va="center", zorder=13)
ky += 17
# ============================================================================
# ASSEMBLY-ORDER RIBBON — far left margin
# ============================================================================
rib_x = 16
ax.add_line(Line2D([rib_x, rib_x], [TIER_BOT, TIER_TOP], color=RULE2,
lw=1.4, zorder=2))
ribbon = ["FOOTINGS", "PIVOT POSTS", "PIVOT MECH", "ROT. FRAME", "PANELS"]
for idx, lbl in enumerate(ribbon):
x0, y0, w, h = tier_rect(idx)
yc = y0 + h/2
ax.add_patch(Circle((rib_x, yc), 7, facecolor=SURFACE, edgecolor=INK2,
linewidth=1.2, zorder=3))
ax.text(rib_x, yc, str(idx+1), family="monospace", fontsize=8, color=INK,
ha="center", va="center", zorder=4)
ax.text(rib_x + 12, yc, lbl, family="monospace", fontsize=7.6, color=INK3,
rotation=90, ha="center", va="center", zorder=4)
# ============================================================================
# BOTTOM STRIP — provenance + scale bar
# ============================================================================
prov = ("PROVENANCE: SOLAR_RACKING_HANDOFF §5 + tilting-pivot-mount.md / "
"tilting-bom-reconciliation.md (inherited estimate) · energy 15_tilt_sweep.py · "
"matplotlib/Agg · 2026-06-06")
ax.text(34, 60, prov, family="monospace", fontsize=8.0, color=INK3, va="center")
sb_x = 34
sb_y = 30
seg = 0.5 * S
n_seg = 4
ax.text(sb_x, sb_y + 4, "0", family="monospace", fontsize=7.5, color=INK3,
ha="right", va="center")
for k in range(n_seg):
col = INK2 if k % 2 == 0 else SURFACE
ax.add_patch(Polygon([(sb_x + k*seg, sb_y), (sb_x + (k+1)*seg, sb_y),
(sb_x + (k+1)*seg, sb_y + 8), (sb_x + k*seg, sb_y + 8)],
closed=True, facecolor=col, edgecolor=INK,
linewidth=1.0, zorder=5))
ax.text(sb_x + n_seg*seg + 8, sb_y + 4,
f"{n_seg*0.5:.1f} m / ~{n_seg*0.5*3.281:.1f} ft · VIGNETTE SCALE",
family="monospace", fontsize=7.5, color=INK3, ha="left", va="center")
# ----------------------------------------------------------------------------
# EXPORT
# ----------------------------------------------------------------------------
import os
OUT = "/Users/jasonkrueger/Library/CloudStorage/GoogleDrive-jasonkrue@gmail.com/My Drive/prsm/electrical-review/artifacts/racking/exploded"
png = os.path.join(OUT, "exploded_opt3_final.png")
svg = os.path.join(OUT, "exploded_opt3_final.svg")
fig.savefig(png, dpi=200, facecolor=PAPER, bbox_inches="tight", pad_inches=0.22)
fig.savefig(svg, facecolor=PAPER, bbox_inches="tight", pad_inches=0.22)
print("wrote", png)
print("wrote", svg)