Fix HP/SAN track label bleed — BRP v4 and Stormbringer v3
Same root cause as LTCI/NAT: box top was rendering above label baseline. BRP v4 (build_brp_characters_v4.py): BOX=20 → BOX=14, GAP=4 → GAP=3 y_start: top-20 → top-29 (bleed was 13pt — worst of all scripts) height: 20+rows*(B+G) → 29+rows*(B+G) Number font: 9pt → 8pt, vertical centre adjusted Stormbringer v3 (build_character_pdf_v3.py): BOX kept at 15, y_start: top-18 → top-29 (bleed was 9pt) height: 18+rows*(B+G) → 29+rows*(B+G) Portrait paths updated: /mnt/user-data/uploads → repo art directory IMG_BASE cleared (paths now fully absolute) Ringworld (apps/ringworld-character-sheets/build.py): CLEAN — uses fixed mm-sized anatomy diagram boxes, not flowable tracks. Labels sit inside each box at fixed mm offsets. No bleed risk. All PDFs rebuild successfully at correct page counts: BRP Day One: 14 pages, Night Crawler: 13 pages, Stormbringer: 13 pages
This commit is contained in:
@@ -346,7 +346,7 @@ class SectionBanner(Flowable):
|
||||
|
||||
class HPTrack(Flowable):
|
||||
"""Clean HP tracker: 20pt boxes, every-5th-shaded, NO milestone text, numbers at 9pt."""
|
||||
BOX=20; GAP=4
|
||||
BOX=14; GAP=3
|
||||
def __init__(self, max_hp, width, t, label="HIT POINTS", fill=None, fill5=None, border=None, num=None):
|
||||
super().__init__()
|
||||
self.max_hp=max_hp; self.width=width; self._t=t
|
||||
@@ -361,7 +361,7 @@ class HPTrack(Flowable):
|
||||
bpr=int(width/(B+G)); self.bpr=max(bpr,1)
|
||||
rows=(max_hp+self.bpr-1)//self.bpr
|
||||
# Height: 20pt label area + rows*(box+gap) - last gap
|
||||
self.height=20+rows*(B+G)
|
||||
self.height=29+rows*(B+G)
|
||||
def wrap(self,a,b): return (self.width,self.height)
|
||||
def draw(self):
|
||||
c=self.canv; B,G=self.BOX,self.GAP
|
||||
@@ -374,7 +374,7 @@ class HPTrack(Flowable):
|
||||
c.setStrokeColor(self._border); c.setLineWidth(0.4)
|
||||
c.line(0,top-16,w,top-16)
|
||||
|
||||
y_start=top-20 # top of first row of boxes
|
||||
y_start=top-29 # clear 2pt above label baseline (top-13)
|
||||
|
||||
for i in range(max_hp):
|
||||
row=i//bpr; col=i%bpr
|
||||
@@ -395,7 +395,7 @@ class HPTrack(Flowable):
|
||||
c.rect(x,y,B,B,fill=1,stroke=1)
|
||||
# Number — 9pt, centered
|
||||
c.setFillColor(self._num); c.setFont('Helvetica-Bold',9)
|
||||
c.drawCentredString(x+B/2, y+(B-7)/2, str(hp_val))
|
||||
c.drawCentredString(x+B/2, y+(B-6)/2, str(hp_val))
|
||||
|
||||
class PortraitPlaceholder(Flowable):
|
||||
"""Reserved portrait space — to be replaced with Midjourney art."""
|
||||
|
||||
@@ -34,14 +34,14 @@ PAGE_W, PAGE_H = A4
|
||||
MARGIN = 17 * mm
|
||||
|
||||
# ── IMAGE PATHS ─────────────────────────────────────────────────────────────
|
||||
IMG_BASE = '/mnt/user-data/uploads/'
|
||||
IMG_BASE = ''
|
||||
PORTRAITS = {
|
||||
'Davin Kell': IMG_BASE + 'slaguru6666_Portrait_of_a_mercenary_warrior_Ilmioran_mid-thir_8f2350d5-3c94-427c-8a15-907aca7f9bef_0_Medium.jpeg',
|
||||
'Ysolde of Vilmir': IMG_BASE + 'slaguru6666_Portrait_of_a_young_Vilmirian_woman_scholar-sorce_9eafe360-7808-4b56-80ae-0b1acd8c1e21_1_Medium.jpeg',
|
||||
'Cray': IMG_BASE + 'slaguru6666_Portrait_of_a_young_woman_thief_from_Nadsokor_the_07aa47cd-ad56-4fe6-b277-9c81d5861791_0_Medium.jpeg',
|
||||
'Sarath the Twice-Turned': IMG_BASE + 'slaguru6666_Portrait_of_a_heavyset_man_in_his_forties_Pan_Tan_d937185c-9eed-4bbc-9b69-8cb28eb5882c_2_Medium.jpeg',
|
||||
'Captain Brenn': IMG_BASE + 'slaguru6666_Portrait_of_a_Tarkeshite_sea_captain_late_thirtie_a95be513-e159-4beb-ba5e-522c1024ad50_2_Medium.jpeg',
|
||||
'Lian': IMG_BASE + 'slaguru6666_Portrait_of_a_young_Ilmioran_devotee_of_Law_mid-t_098976e0-2bbc-47cd-8dd0-2552786a2bac_1_Medium.jpeg',
|
||||
'Davin Kell': IMG_BASE + '/home/claude/ChaosiumCon26/scenarios/art/stormbringer/sb-pc01-davin-kell.jpeg',
|
||||
'Ysolde of Vilmir': IMG_BASE + '/home/claude/ChaosiumCon26/scenarios/art/stormbringer/sb-pc02-ysolde-vilmir.jpeg',
|
||||
'Cray': IMG_BASE + '/home/claude/ChaosiumCon26/scenarios/art/stormbringer/sb-pc03-cray.jpeg',
|
||||
'Sarath the Twice-Turned': IMG_BASE + '/home/claude/ChaosiumCon26/scenarios/art/stormbringer/sb-pc04-sarath.jpeg',
|
||||
'Captain Brenn': IMG_BASE + '/home/claude/ChaosiumCon26/scenarios/art/stormbringer/sb-pc05-captain-brenn.jpeg',
|
||||
'Lian': IMG_BASE + '/home/claude/ChaosiumCon26/scenarios/art/stormbringer/sb-pc06-lian.jpeg',
|
||||
}
|
||||
IMG_RATIO = 640 / 427 # height/width
|
||||
|
||||
@@ -180,7 +180,7 @@ class HitPointTrack(Flowable):
|
||||
bpr = int(width / (self.BOX + self.GAP))
|
||||
self.bpr = bpr
|
||||
self.rows = (max_hp + bpr - 1) // bpr
|
||||
self.height = 18 + self.rows * (self.BOX + 10 + self.GAP)
|
||||
self.height = 29 + self.rows * (self.BOX + 10 + self.GAP)
|
||||
|
||||
def wrap(self, aW, aH): return (self.width, self.height)
|
||||
|
||||
@@ -197,7 +197,7 @@ class HitPointTrack(Flowable):
|
||||
c.setStrokeColor(RULE_COLOR); c.setLineWidth(0.4)
|
||||
c.line(0,top-15,w,top-15)
|
||||
|
||||
y_start=top-18
|
||||
y_start=top-29 # clear 2pt above label baseline (top-12)
|
||||
for i in range(max_hp):
|
||||
row=i//bpr; col=i%bpr
|
||||
items_in_row=min(bpr,max_hp-row*bpr)
|
||||
|
||||
Reference in New Issue
Block a user