clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

refactor/root-tui-model
puzzletea / export / pdf / render_tokens.go
render_tokens.go 4.9K
1package pdfexport23import (4 "math"56 "codeberg.org/go-pdf/fpdf"7)89const (10 halfLetterWidthMM = 139.711 halfLetterHeightMM = 215.912 letterWidthMM = 279.413 letterHeightMM = 215.91415 footerTextGray = 7816 secondaryTextGray = 6017 ruleTextGray = 5418 dimTextGray = 9219 primaryTextGray = 202021 pageMarginXMM = 10.022 bindingGutterExtraMM = 2.623 puzzleTopMM = 28.024 puzzleBottomInsetMM = 16.025 instructionGapMM = 4.226 instructionLineHMM = 4.627 wordSearchGridListGap = 4.02829 thinGridLineMM = 0.2430 majorGridLineMM = 0.3431 outerBorderLineMM = 0.6232)3334var logicalPageNumberOverride int3536const (37 puzzleTitleFontSize = 13.0 + pdfFontSizeDelta38 puzzleSubtitleFontSize = 9.0 + pdfFontSizeDelta39 puzzleInstructionFontSize = 7.0 + pdfFontSizeDelta40 puzzleWordBankFontSize = 8.8 + pdfFontSizeDelta41 puzzleWordBankHeadSize = 9.2 + pdfFontSizeDelta42 difficultyStarSizeMM = 3.543 difficultyStarGapMM = 0.944 difficultyTextToStarsGapMM = 2.145 difficultyStarOutlineMM = 0.2646)4748type (49 boardFamily int50 rectMM struct {51 x float6452 y float6453 w float6454 h float6455 }56)5758const (59 boardFamilyCompact boardFamily = iota60 boardFamilySudoku61 boardFamilyHashi62 boardFamilyNonogram63 boardFamilyWordSearch64 boardFamilyTable65)6667type boardSizing struct {68 minCell float6469 maxCell float6470 targetFill float6471}7273func puzzleBodyRect(pageW, pageH float64, pageNo int) rectMM {74 leftMargin, rightMargin := puzzleHorizontalMargins(pageNo)75 return rectMM{76 x: leftMargin,77 y: puzzleTopMM,78 w: pageW - leftMargin - rightMargin,79 h: pageH - puzzleTopMM - puzzleBottomInsetMM,80 }81}8283func puzzleBoardRect(pageW, pageH float64, pageNo, ruleLines int) rectMM {84 rect := puzzleBodyRect(pageW, pageH, pageNo)85 if ruleLines > 0 {86 rect.h -= instructionGapMM + float64(ruleLines)*instructionLineHMM87 if rect.h < 0 {88 rect.h = 089 }90 }91 return rect92}9394func puzzleHorizontalMargins(pageNo int) (left, right float64) {95 pageNo = effectiveLogicalPageNumber(pageNo)96 left = pageMarginXMM97 right = pageMarginXMM98 if pageNo <= 1 {99 return left, right100 }101 if pageNo%2 == 0 {102 // Even pages sit on the left side in a spread, so inside edge is right.103 right += bindingGutterExtraMM104 return left, right105 }106 // Odd pages sit on the right side in a spread, so inside edge is left.107 left += bindingGutterExtraMM108 return left, right109}110111func effectiveLogicalPageNumber(pageNo int) int {112 if logicalPageNumberOverride > 0 {113 return logicalPageNumberOverride114 }115 return pageNo116}117118func withLogicalPageNumber(pageNo int, fn func() error) error {119 previous := logicalPageNumberOverride120 logicalPageNumberOverride = pageNo121 defer func() {122 logicalPageNumberOverride = previous123 }()124 return fn()125}126127func centeredOrigin(area rectMM, cols, rows int, cellSize float64) (float64, float64) {128 boardW := float64(cols) * cellSize129 boardH := float64(rows) * cellSize130 return area.x + (area.w-boardW)/2, area.y + (area.h-boardH)/2131}132133func instructionY(boardBottom, pageH float64, lineCount int) float64 {134 y := boardBottom + instructionGapMM135 maxY := pageH - puzzleBottomInsetMM - float64(max(lineCount-1, 0))*instructionLineHMM136 if y > maxY {137 return maxY138 }139 return y140}141142func fitBoardCellSize(cols, rows int, area rectMM, family boardFamily) float64 {143 if cols <= 0 || rows <= 0 || area.w <= 0 || area.h <= 0 {144 return 0145 }146147 sizeCfg := boardSizingFor(family)148 maxFit := math.Min(area.w/float64(cols), area.h/float64(rows))149 size := clampFloat(maxFit, sizeCfg.minCell, sizeCfg.maxCell)150 target := (area.h * sizeCfg.targetFill) / float64(rows)151 if target > size {152 size = math.Min(target, maxFit)153 }154155 size = clampFloat(size, sizeCfg.minCell, sizeCfg.maxCell)156 if size > maxFit {157 return maxFit158 }159 return size160}161162func boardSizingFor(family boardFamily) boardSizing {163 switch family {164 case boardFamilySudoku:165 return boardSizing{minCell: 10.8, maxCell: 12.8, targetFill: 0.66}166 case boardFamilyHashi:167 return boardSizing{minCell: 6.0, maxCell: 16.2, targetFill: 0.58}168 case boardFamilyNonogram:169 return boardSizing{minCell: 3.6, maxCell: 8.8, targetFill: 0.64}170 case boardFamilyWordSearch:171 return boardSizing{minCell: 4.8, maxCell: 10.2, targetFill: 0.66}172 case boardFamilyTable:173 return boardSizing{minCell: 3.6, maxCell: 11.0, targetFill: 0.60}174 default:175 return boardSizing{minCell: 5.4, maxCell: 13.4, targetFill: 0.58}176 }177}178179func clampFloat(v, minV, maxV float64) float64 {180 if v < minV {181 return minV182 }183 if v > maxV {184 return maxV185 }186 return v187}188189func setPuzzleTitleStyle(pdf *fpdf.Fpdf) {190 pdf.SetTextColor(primaryTextGray, primaryTextGray, primaryTextGray)191 pdf.SetFont(sansFontFamily, "B", puzzleTitleFontSize)192}193194func setPuzzleSubtitleStyle(pdf *fpdf.Fpdf) {195 pdf.SetTextColor(secondaryTextGray, secondaryTextGray, secondaryTextGray)196 pdf.SetFont(sansFontFamily, "", puzzleSubtitleFontSize)197}198199func setInstructionStyle(pdf *fpdf.Fpdf) {200 pdf.SetTextColor(ruleTextGray, ruleTextGray, ruleTextGray)201 pdf.SetFont(sansFontFamily, "", puzzleInstructionFontSize)202}