519 lines
19 KiB
Python
519 lines
19 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Scribus script: Generate the B-Prize 2026 "Living Pipeline" A3 poster.
|
|
Creates a landscape A3 (420x297mm) document with improved design.
|
|
|
|
Run via wrapper that sets sys.argv before exec.
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
|
|
def parse_args():
|
|
args = {}
|
|
argv = sys.argv[1:]
|
|
i = 0
|
|
while i < len(argv):
|
|
if argv[i] == "--output" and i + 1 < len(argv):
|
|
args["output"] = argv[i + 1]
|
|
i += 2
|
|
elif argv[i] == "--dpi" and i + 1 < len(argv):
|
|
args["dpi"] = int(argv[i + 1])
|
|
i += 2
|
|
else:
|
|
i += 1
|
|
return args
|
|
|
|
|
|
def define_colors(scribus):
|
|
scribus.defineColorRGB("Navy", 11, 29, 51)
|
|
scribus.defineColorRGB("DeepTeal", 20, 83, 105)
|
|
scribus.defineColorRGB("Forest", 27, 107, 74)
|
|
scribus.defineColorRGB("Green", 34, 168, 97)
|
|
scribus.defineColorRGB("LightGreen", 232, 245, 233)
|
|
scribus.defineColorRGB("Alert", 198, 40, 40)
|
|
scribus.defineColorRGB("AlertLight", 255, 235, 238)
|
|
scribus.defineColorRGB("Blue", 46, 134, 193)
|
|
scribus.defineColorRGB("LightBlue", 227, 242, 253)
|
|
scribus.defineColorRGB("DarkText", 26, 35, 50)
|
|
scribus.defineColorRGB("MidText", 84, 110, 122)
|
|
scribus.defineColorRGB("LightText", 144, 164, 174)
|
|
scribus.defineColorRGB("Border", 224, 224, 224)
|
|
scribus.defineColorRGB("Surface", 255, 255, 255)
|
|
scribus.defineColorRGB("BG", 248, 249, 250)
|
|
scribus.defineColorRGB("Purple", 123, 31, 162)
|
|
scribus.defineColorRGB("Earth", 121, 85, 72)
|
|
scribus.defineColorRGB("MapBG1", 214, 234, 248)
|
|
scribus.defineColorRGB("MapBG2", 212, 239, 223)
|
|
scribus.defineColorRGB("BarRed", 239, 83, 80)
|
|
scribus.defineColorRGB("BarGreen", 102, 187, 106)
|
|
|
|
|
|
_name_counter = [0]
|
|
|
|
def uname(prefix="obj"):
|
|
_name_counter[0] += 1
|
|
return f"{prefix}_{_name_counter[0]}"
|
|
|
|
|
|
def rect(s, x, y, w, h, fill, line_color="None", line_w=0, radius=0):
|
|
n = s.createRect(x, y, w, h, uname("r"))
|
|
s.setFillColor(fill, n)
|
|
if line_color != "None":
|
|
s.setLineColor(line_color, n)
|
|
s.setLineWidth(line_w, n)
|
|
else:
|
|
s.setLineColor("None", n)
|
|
s.setLineWidth(0, n)
|
|
if radius > 0:
|
|
s.setCornerRadius(int(radius), n)
|
|
return n
|
|
|
|
|
|
def txt(s, x, y, w, h, text, font="DejaVu Sans", size=10, color="DarkText", align=0):
|
|
n = s.createText(x, y, w, h, uname("t"))
|
|
s.setText(text, n)
|
|
try:
|
|
s.setFont(font, n)
|
|
except:
|
|
pass
|
|
s.setFontSize(size, n)
|
|
s.setTextColor(color, n)
|
|
s.setTextAlignment(align, n)
|
|
return n
|
|
|
|
|
|
def hline(s, x, y, length, color="Border", width=0.5):
|
|
n = s.createLine(x, y, x + length, y, uname("l"))
|
|
s.setLineColor(color, n)
|
|
s.setLineWidth(width, n)
|
|
return n
|
|
|
|
|
|
def vline(s, x1, y1, x2, y2, color="Green", width=0.75):
|
|
n = s.createLine(x1, y1, x2, y2, uname("vl"))
|
|
s.setLineColor(color, n)
|
|
s.setLineWidth(width, n)
|
|
return n
|
|
|
|
|
|
def main():
|
|
try:
|
|
import scribus
|
|
except ImportError:
|
|
print("ERROR: Must run inside Scribus")
|
|
sys.exit(1)
|
|
|
|
args = parse_args()
|
|
output_path = args.get("output", "/app/output/bprize-poster.pdf")
|
|
dpi = args.get("dpi", 300)
|
|
|
|
s = scribus
|
|
|
|
# ═══ PAGE SETUP ═══
|
|
PW = 420.0
|
|
PH = 297.0
|
|
|
|
s.newDocument(
|
|
(PW, PH),
|
|
(6, 6, 6, 6),
|
|
s.PORTRAIT,
|
|
1, s.UNIT_MILLIMETERS, s.PAGE_1, 0, 1,
|
|
)
|
|
|
|
define_colors(s)
|
|
|
|
# ═══ LAYOUT GRID ═══
|
|
HEADER_H = 22.0
|
|
FOOTER_H = 6.0
|
|
MARGIN = 0.0
|
|
COL_TOP = HEADER_H
|
|
COL_H = PH - HEADER_H - FOOTER_H
|
|
|
|
COL1_W = 110.0
|
|
COL2_W = 180.0
|
|
COL3_W = 130.0
|
|
|
|
COL1_X = 0
|
|
COL2_X = COL1_W
|
|
COL3_X = COL1_W + COL2_W
|
|
|
|
B = "DejaVu Sans Bold"
|
|
R = "DejaVu Sans"
|
|
I = "DejaVu Sans Oblique"
|
|
|
|
# ═══════════════════════════════════════
|
|
# HEADER
|
|
# ═══════════════════════════════════════
|
|
rect(s, 0, 0, PW, HEADER_H, "Navy")
|
|
# Gradient accent line
|
|
rect(s, 0, HEADER_H - 0.7, PW * 0.5, 0.7, "Blue")
|
|
rect(s, PW * 0.5, HEADER_H - 0.7, PW * 0.5, 0.7, "Green")
|
|
|
|
txt(s, 14, 2.5, 250, 8, "The Living Pipeline", B, 22, "White")
|
|
txt(s, 14, 10, 270, 4.5,
|
|
"A biomimicry-inspired distributed water system for the Collingwood\u2013Alliston corridor",
|
|
R, 7.5, "White")
|
|
txt(s, 14, 15, 270, 4,
|
|
"Instead of one $270M pipe, what if the landscape itself became the water system?",
|
|
I, 7, "White")
|
|
|
|
# Badge
|
|
rect(s, PW - 62, 3, 50, 16, "Navy", "White", 0.3)
|
|
txt(s, PW - 62, 4, 50, 3, "B-PRIZE 2026", B, 5.5, "White", 1)
|
|
txt(s, PW - 62, 8, 50, 4, "Biomimicry Commons", R, 6.5, "White", 1)
|
|
txt(s, PW - 62, 13, 50, 3, "Simcoe County, Ontario", R, 4.5, "White", 1)
|
|
|
|
# ═══════════════════════════════════════
|
|
# COLUMN BACKGROUNDS
|
|
# ═══════════════════════════════════════
|
|
rect(s, COL1_X, COL_TOP, COL1_W, COL_H, "Surface")
|
|
rect(s, COL2_X, COL_TOP, COL2_W, COL_H, "BG")
|
|
rect(s, COL3_X, COL_TOP, COL3_W, COL_H, "Surface")
|
|
# Column separators
|
|
vline(s, COL2_X, COL_TOP, COL2_X, PH - FOOTER_H, "Border", 0.25)
|
|
vline(s, COL3_X, COL_TOP, COL3_X, PH - FOOTER_H, "Border", 0.25)
|
|
|
|
# ═══════════════════════════════════════
|
|
# COLUMN 1 — THE CHALLENGE
|
|
# ═══════════════════════════════════════
|
|
cx = COL1_X + 8
|
|
cw = COL1_W - 16
|
|
cy = COL_TOP + 8
|
|
|
|
# Section title
|
|
txt(s, cx, cy, cw, 4, "THE CHALLENGE", B, 7.5, "Alert")
|
|
hline(s, cx, cy + 5, cw, "Alert", 0.5)
|
|
cy += 8
|
|
|
|
# Intro text
|
|
txt(s, cx, cy, cw, 18,
|
|
"Collingwood\u2019s water treatment plant expansion doubled from $121M to $270M to serve five municipalities along a single 53 km pipeline.\n\nOne pipe. Five towns. Zero redundancy.",
|
|
R, 7, "MidText")
|
|
cy += 20
|
|
|
|
# Hero stats
|
|
sw = (cw - 6) / 3
|
|
stats = [("$270M", "Expansion cost"), ("53 km", "Single pipeline"), ("5", "Towns at risk")]
|
|
for i, (val, lab) in enumerate(stats):
|
|
sx = cx + i * (sw + 3)
|
|
rect(s, sx, cy, sw, 14, "AlertLight", "None", 0, 1.5)
|
|
txt(s, sx, cy + 1.5, sw, 7, val, B, 14, "Alert", 1)
|
|
txt(s, sx, cy + 9, sw, 4, lab, R, 4.5, "LightText", 1)
|
|
cy += 17
|
|
|
|
# Community table
|
|
rect(s, cx, cy, cw, 3.5, "Navy")
|
|
txt(s, cx + 1.5, cy + 0.5, 26, 2.5, "Community", B, 4.5, "White")
|
|
txt(s, cx + 28, cy + 0.5, 32, 2.5, "Source", B, 4.5, "White")
|
|
txt(s, cx + 62, cy + 0.5, cw - 63, 2.5, "Status", B, 4.5, "White")
|
|
cy += 4
|
|
|
|
rows = [
|
|
("Collingwood", "Georgian Bay WTP", "$270M expansion", "MidText"),
|
|
("Stayner", "4 groundwater wells", "At capacity", "Alert"),
|
|
("Angus", "6 wells + pipeline", "Dev frozen", "Alert"),
|
|
("Alliston", "Pipeline + wells", "6,400 homes rejected", "Alert"),
|
|
("Blue Mountains", "Pipeline", "1,250 m\u00b3/day", "MidText"),
|
|
]
|
|
for i, (c, src, st, sc) in enumerate(rows):
|
|
bg = "Surface" if i % 2 == 0 else "BG"
|
|
rect(s, cx, cy, cw, 3.5, bg)
|
|
txt(s, cx + 1.5, cy + 0.5, 26, 2.5, c, B, 5, "DarkText")
|
|
txt(s, cx + 28, cy + 0.5, 32, 2.5, src, R, 4.5, "MidText")
|
|
txt(s, cx + 62, cy + 0.5, cw - 63, 2.5, st, B, 4.5, sc)
|
|
cy += 3.7
|
|
cy += 5
|
|
|
|
# Nature's Model header
|
|
txt(s, cx, cy, cw, 3, "NATURE\u2019S MODEL", B, 6, "LightText")
|
|
cy += 5
|
|
|
|
# Comparison boxes
|
|
bw = (cw - 4) / 2
|
|
|
|
# Problem box
|
|
rect(s, cx, cy, bw, 32, "AlertLight", "None", 0, 2)
|
|
txt(s, cx + 2, cy + 2, bw - 4, 3.5, "CURRENT SYSTEM", B, 5.5, "Alert", 1)
|
|
txt(s, cx + 2, cy + 8, bw - 4, 22,
|
|
"One trunk. One root.\nCut it \u2014 everything dies.\n\nSingle point of failure.\n53 km of pumping uphill.",
|
|
R, 6, "MidText", 1)
|
|
|
|
# Solution box
|
|
sx = cx + bw + 4
|
|
rect(s, sx, cy, bw, 32, "LightGreen", "None", 0, 2)
|
|
txt(s, sx + 2, cy + 2, bw - 4, 3.5, "MYCORRHIZAL FOREST", B, 5.5, "Forest", 1)
|
|
txt(s, sx + 2, cy + 8, bw - 4, 22,
|
|
"Many roots, connected\nunderground. Resources\nflow where needed.\n\nNo single point of failure.",
|
|
R, 6, "MidText", 1)
|
|
cy += 35
|
|
|
|
# Principle callout
|
|
rect(s, cx, cy, cw, 22, "LightGreen", "None", 0, 1.5)
|
|
rect(s, cx, cy, 0.8, 22, "Green")
|
|
txt(s, cx + 3, cy + 2, cw - 5, 16,
|
|
"In Ontario forests, 90% of rainfall events produce zero runoff. Every point along water\u2019s journey is a collection point, a storage node, and a treatment system.\n\nDecentralized networks improve resilience by 3\u00d7 minimum (Springer, 2024).",
|
|
I, 6, "DarkText")
|
|
|
|
# ═══════════════════════════════════════
|
|
# COLUMN 2 — THE SOLUTION
|
|
# ═══════════════════════════════════════
|
|
cx = COL2_X + 10
|
|
cw = COL2_W - 20
|
|
cy = COL_TOP + 8
|
|
|
|
txt(s, cx, cy, cw, 4, "THE LIVING PIPELINE", B, 7.5, "Forest")
|
|
hline(s, cx, cy + 5, cw, "Green", 0.5)
|
|
cy += 8
|
|
|
|
txt(s, cx, cy, cw, 7,
|
|
"Distribute capacity across the corridor \u2014 the landscape becomes a living water system where each community both gives and receives.",
|
|
R, 7.5, "DarkText")
|
|
cy += 10
|
|
|
|
# Solution Map
|
|
map_h = 68
|
|
rect(s, cx, cy, cw, map_h, "LightBlue", "Blue", 0.2, 2)
|
|
|
|
# Bay label
|
|
txt(s, cx + cw/2 - 15, cy + 1.5, 30, 3, "Georgian Bay", B, 5.5, "DeepTeal", 1)
|
|
|
|
# Backbone line
|
|
mx = cx + cw * 0.45
|
|
vline(s, mx, cy + 8, mx, cy + map_h - 8, "Green", 0.6)
|
|
|
|
# Nodes
|
|
nodes = [
|
|
(cy + 9, "Collingwood WTP", "(reduced expansion)", True),
|
|
(cy + 23, "Stayner Node", "3,000 m\u00b3/day", False),
|
|
(cy + 39, "Angus Node", "5,000 m\u00b3/day", True),
|
|
(cy + 55, "Alliston Node", "3,000 m\u00b3/day", False),
|
|
]
|
|
for ny, label, sub, right_side in nodes:
|
|
rect(s, mx - 2.5, ny - 2.5, 5, 5, "Green", "Surface", 0.3, 2.5)
|
|
lx = mx + 6 if right_side else cx + 3
|
|
txt(s, lx, ny - 2, 55, 6, f"{label}\n{sub}", R, 5, "DarkText")
|
|
|
|
# Flow arrows
|
|
for ay in [cy + 17, cy + 32, cy + 48]:
|
|
txt(s, mx + 3.5, ay, 6, 4, "\u2195", B, 6.5, "Green")
|
|
|
|
# MAR zone
|
|
rect(s, cx + cw - 42, cy + map_h - 22, 38, 17, "LightBlue", "Blue", 0.2, 3)
|
|
txt(s, cx + cw - 41, cy + map_h - 20, 36, 12, "MAR Zone\nAlliston Sand Plain", B, 5, "DeepTeal", 1)
|
|
|
|
# Wetland patches
|
|
for wy in [cy + 20, cy + 38]:
|
|
rect(s, cx + 2, wy, 24, 8, "LightGreen", "Green", 0.15, 2)
|
|
txt(s, cx + 3, wy + 2, 22, 4, "Wetland", R, 4.5, "Forest", 1)
|
|
|
|
# Legend
|
|
txt(s, cx + 2, cy + map_h - 4, cw - 4, 3,
|
|
"\u25cf Node \u25a2 MAR Zone \u25a2 Wetland \u2195 Bidirectional",
|
|
R, 4, "LightText")
|
|
cy += map_h + 4
|
|
|
|
# Strategy Cards
|
|
strategies = [
|
|
("1", "Satellite Treatment Nodes", "Earth",
|
|
"Each tree\u2019s own root system",
|
|
"3\u20134 modular membrane + UV units at existing wells. $2\u20138M each, online in 12\u201324 months. Reduces pipeline demand 30\u201350%."),
|
|
("2", "Managed Aquifer Recharge", "Blue",
|
|
"Forest floor + beaver dam storage",
|
|
"Alliston Sand Plain \u2014 Ontario\u2019s best MAR candidate. 1 ha basin = water for 15\u201320K people. Precedent: Turku, Finland serves 300K on identical geology."),
|
|
("3", "Constructed Treatment Wetlands", "Green",
|
|
"Riparian buffer zones",
|
|
"Subsurface-flow wetlands proven in Ontario winters (Fleming College CAWT). O&M 75% cheaper. Greywater reuse cuts demand 30\u201340%."),
|
|
("4", "Mycorrhizal Backbone", "Purple",
|
|
"Common mycorrhizal network",
|
|
"Existing pipeline becomes smart bidirectional grid \u2014 SCADA/IoT, adaptive routing. Precedent: SEQ Water Grid (Australia) \u2014 12 dams + 5 plants as one system."),
|
|
]
|
|
|
|
card_h = 24
|
|
for num, title, color, bio, desc in strategies:
|
|
rect(s, cx, cy, cw, card_h, "Surface", "Border", 0.15, 2)
|
|
rect(s, cx, cy, 1, card_h, color)
|
|
# Number badge
|
|
rect(s, cx + 3, cy + 2, 6.5, 6.5, color, "None", 0, 1.5)
|
|
txt(s, cx + 3, cy + 2.3, 6.5, 5, num, B, 8, "White", 1)
|
|
# Title
|
|
txt(s, cx + 12, cy + 2, cw - 16, 4, title, B, 7, "DarkText")
|
|
# Bio
|
|
txt(s, cx + 12, cy + 6, cw - 16, 3, f"Biomimicry: {bio}", I, 5, "LightText")
|
|
# Desc
|
|
txt(s, cx + 4, cy + 10, cw - 8, 12, desc, R, 5.5, "MidText")
|
|
cy += card_h + 2
|
|
|
|
# ═══════════════════════════════════════
|
|
# COLUMN 3 — FEASIBILITY
|
|
# ═══════════════════════════════════════
|
|
cx = COL3_X + 8
|
|
cw = COL3_W - 16
|
|
cy = COL_TOP + 8
|
|
|
|
txt(s, cx, cy, cw, 4, "FEASIBILITY & IMPACT", B, 7.5, "DeepTeal")
|
|
hline(s, cx, cy + 5, cw, "Blue", 0.5)
|
|
cy += 8
|
|
|
|
# ── Cost section ──
|
|
txt(s, cx, cy, cw, 3, "CAPITAL COST", B, 5.5, "LightText")
|
|
cy += 4
|
|
|
|
bar_w = (cw - 20) / 2
|
|
bar_base_y = cy + 28
|
|
|
|
# Red bar
|
|
bh_red = 24
|
|
rect(s, cx + 4, bar_base_y - bh_red, bar_w, bh_red, "BarRed", "None", 0, 1.5)
|
|
txt(s, cx + 4, bar_base_y - bh_red + 5, bar_w, 8, "$270M", B, 13, "White", 1)
|
|
txt(s, cx + 4, bar_base_y + 1.5, bar_w, 4, "Centralized", B, 5, "LightText", 1)
|
|
|
|
# Green bar
|
|
bh_green = 14
|
|
gx = cx + bar_w + 16
|
|
rect(s, gx, bar_base_y - bh_green, bar_w, bh_green, "BarGreen", "None", 0, 1.5)
|
|
txt(s, gx, bar_base_y - bh_green + 2, bar_w, 6, "$118\u2013170M", B, 9, "White", 1)
|
|
txt(s, gx, bar_base_y + 1.5, bar_w, 4, "Living Pipeline", B, 5, "LightText", 1)
|
|
cy = bar_base_y + 7
|
|
|
|
# Savings badge
|
|
rect(s, cx + cw/2 - 24, cy, 48, 5.5, "LightGreen", "Green", 0.2, 1)
|
|
txt(s, cx + cw/2 - 24, cy + 0.8, 48, 4, "Save $100\u2013150M (37\u201356%)", B, 6, "Forest", 1)
|
|
cy += 8
|
|
|
|
# Cost breakdown table
|
|
cost_items = [
|
|
("WTP expansion (Phase 1)", "$80\u2013100M"),
|
|
("Satellite nodes (3\u20134)", "$15\u201330M"),
|
|
("MAR infrastructure", "$8\u201315M"),
|
|
("Constructed wetlands (4)", "$12\u201320M"),
|
|
("Smart network integration", "$3\u20135M"),
|
|
]
|
|
rect(s, cx, cy, cw, 3, "Navy")
|
|
txt(s, cx + 1, cy + 0.3, cw * 0.6, 2.5, "Component", B, 4.5, "White")
|
|
txt(s, cx + cw * 0.6, cy + 0.3, cw * 0.4 - 1, 2.5, "Cost (CAD)", B, 4.5, "White", 2)
|
|
cy += 3.5
|
|
|
|
for i, (comp, cost) in enumerate(cost_items):
|
|
bg = "Surface" if i % 2 == 0 else "BG"
|
|
rect(s, cx, cy, cw, 3, bg)
|
|
txt(s, cx + 1, cy + 0.3, cw * 0.6, 2.5, comp, R, 4.5, "MidText")
|
|
txt(s, cx + cw * 0.6, cy + 0.3, cw * 0.4 - 1, 2.5, cost, R, 4.5, "DarkText", 2)
|
|
cy += 3.2
|
|
cy += 4
|
|
|
|
# ── Timeline ──
|
|
txt(s, cx, cy, cw, 3, "TIMELINE", B, 5.5, "LightText")
|
|
cy += 4
|
|
|
|
txt(s, cx, cy, 20, 3, "Centralized", B, 4.5, "LightText")
|
|
tbar_x = cx + 22
|
|
tbar_w = cw - 22
|
|
rect(s, tbar_x, cy, tbar_w * 0.55, 3.5, "Border")
|
|
rect(s, tbar_x + tbar_w * 0.55, cy, tbar_w * 0.45, 3.5, "BarRed", "None", 0, 1)
|
|
txt(s, tbar_x, cy + 0.5, tbar_w, 2.5, "First water: 2029", B, 4.5, "White", 2)
|
|
cy += 5
|
|
|
|
txt(s, cx, cy, 20, 3, "Living Pipeline", B, 4.5, "LightText")
|
|
rect(s, tbar_x, cy, tbar_w * 0.18, 3.5, "Border")
|
|
rect(s, tbar_x + tbar_w * 0.18, cy, tbar_w * 0.42, 3.5, "BarGreen", "None", 0, 1)
|
|
rect(s, tbar_x + tbar_w * 0.6, cy, tbar_w * 0.4, 3.5, "Border")
|
|
txt(s, tbar_x, cy + 0.5, tbar_w * 0.6, 2.5, "First water: 2027", B, 4.5, "White", 1)
|
|
cy += 4.5
|
|
|
|
txt(s, cx, cy, cw, 3, "2 years faster \u2014 unblocks ~3\u20135K homes sooner", B, 5, "Green", 1)
|
|
cy += 5.5
|
|
|
|
# ── Resilience ──
|
|
txt(s, cx, cy, cw, 3, "RESILIENCE", B, 5.5, "LightText")
|
|
cy += 4
|
|
|
|
rc = [cw * 0.25, cw * 0.375, cw * 0.375]
|
|
|
|
rect(s, cx, cy, cw, 3, "Navy")
|
|
txt(s, cx + 1, cy + 0.3, rc[0], 2.5, "Risk", B, 4, "White")
|
|
txt(s, cx + rc[0], cy + 0.3, rc[1], 2.5, "Centralized", B, 4, "White")
|
|
txt(s, cx + rc[0] + rc[1], cy + 0.3, rc[2], 2.5, "Living Pipeline", B, 4, "White")
|
|
cy += 3.5
|
|
|
|
res = [
|
|
("WTP failure", "All towns lose supply", "Others compensate"),
|
|
("Pipeline break", "Downstream cut off", "Nodes self-sufficient"),
|
|
("Drought", "System-wide stress", "Aquifers buffer"),
|
|
("Cost escalation", "$121M\u2192$270M", "Phased, no mega-risk"),
|
|
]
|
|
for i, (risk, cent, liv) in enumerate(res):
|
|
bg = "Surface" if i % 2 == 0 else "BG"
|
|
rect(s, cx, cy, cw, 3.2, bg)
|
|
txt(s, cx + 1, cy + 0.3, rc[0] - 1, 2.5, risk, R, 4, "DarkText")
|
|
txt(s, cx + rc[0], cy + 0.3, rc[1] - 1, 2.5, cent, R, 4, "Alert")
|
|
txt(s, cx + rc[0] + rc[1], cy + 0.3, rc[2] - 1, 2.5, liv, R, 4, "Green")
|
|
cy += 3.4
|
|
cy += 4
|
|
|
|
# ── Co-Benefits ──
|
|
txt(s, cx, cy, cw, 3, "CO-BENEFITS", B, 5.5, "LightText")
|
|
cy += 4
|
|
|
|
coben = [
|
|
("Ecological:", "10\u201320 ha new habitat along rail corridor"),
|
|
("Economic:", "3,000+ homes unlocked = $1.2B construction"),
|
|
("Indigenous:", "Working with the watershed, not against it"),
|
|
("Energy:", "40\u201355% less than pumping 53 km"),
|
|
]
|
|
for label, text in coben:
|
|
txt(s, cx, cy, cw, 4, f"{label} {text}", R, 4.5, "MidText")
|
|
cy += 4.5
|
|
cy += 3
|
|
|
|
# ── Design Spiral ──
|
|
txt(s, cx, cy, cw, 3, "DESIGN METHODOLOGY", B, 5.5, "LightText")
|
|
cy += 4
|
|
|
|
steps = [
|
|
("Define", "Supply 5 towns\ncost-effectively"),
|
|
("Biologize", "How does nature\ndistribute?"),
|
|
("Discover", "Mycorrhizal nets,\nwetlands"),
|
|
("Abstract", "Nodes + landscape\nas infra"),
|
|
("Emulate", "MAR, satellites,\nsmart grid"),
|
|
("Evaluate", "37\u201356% savings,\n3\u00d7 resilience"),
|
|
]
|
|
stw = (cw - 5) / 6
|
|
for i, (title, desc) in enumerate(steps):
|
|
sx = cx + i * (stw + 1)
|
|
rect(s, sx, cy, stw, 10, "Surface", "Border", 0.1, 1)
|
|
txt(s, sx, cy + 0.5, stw, 2.5, title, B, 4, "Forest", 1)
|
|
txt(s, sx, cy + 3.5, stw, 6, desc, R, 3.5, "LightText", 1)
|
|
cy += 13
|
|
|
|
# ── Sources ──
|
|
hline(s, cx, cy, cw, "Border", 0.25)
|
|
cy += 1.5
|
|
txt(s, cx, cy, cw, 12,
|
|
"Sources: Collingwood WTP Class EA (2022) \u2022 NVCA IWMP (2019) \u2022 New Tecumseth Master Plan (2016) \u2022 CFB Borden (U of Waterloo) \u2022 Region of Waterloo ASR \u2022 Fleming College CAWT \u2022 SEQ Water Grid (Australia) \u2022 Turku Finland MAR \u2022 Egerton-Warburton et al., J. Exp. Botany (2007) \u2022 Biomimicry Institute Design Spiral",
|
|
R, 4, "LightText")
|
|
|
|
# ═══ FOOTER ═══
|
|
rect(s, 0, PH - FOOTER_H, PW, FOOTER_H, "Navy")
|
|
txt(s, 14, PH - FOOTER_H + 1.5, 200, 3, "Jeff Emmett \u2022 The Living Pipeline \u2022 B-Prize 2026", R, 5, "White")
|
|
txt(s, PW - 200, PH - FOOTER_H + 1.5, 186, 3,
|
|
"Biomimicry Commons \u2022 Collingwood\u2013Alliston Corridor, Simcoe County, Ontario",
|
|
R, 5, "White", 2)
|
|
|
|
# ═══ EXPORT ═══
|
|
pdf = s.PDFfile()
|
|
pdf.file = output_path
|
|
pdf.quality = 0
|
|
pdf.resolution = dpi
|
|
pdf.version = 14
|
|
pdf.compress = True
|
|
pdf.compressmtd = 0
|
|
pdf.save()
|
|
|
|
s.closeDoc()
|
|
print(f"Exported: {output_path}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|