From ad41f338f8f438d242d7a2f631eefcdfa9e9d77f Mon Sep 17 00:00:00 2001 From: Daneel Date: Fri, 20 Feb 2026 19:11:05 +0100 Subject: [PATCH] fix: v4.5.0 - cross-day drag (ghost element, no overflow issue); adaptive block labels; PDF fit_text truncation --- app/config.py | 2 +- app/core/pdf_generator.py | 50 +++++++--- app/static/css/app.css | 13 +-- app/static/index.html | 2 +- app/static/js/canvas.js | 201 ++++++++++++++++++++------------------ 5 files changed, 155 insertions(+), 113 deletions(-) diff --git a/app/config.py b/app/config.py index a8413ab..271dc15 100644 --- a/app/config.py +++ b/app/config.py @@ -1,5 +1,5 @@ """Application configuration.""" -VERSION = "4.4.0" +VERSION = "4.5.0" MAX_FILE_SIZE_MB = 10 DEFAULT_COLOR = "#ffffff" diff --git a/app/core/pdf_generator.py b/app/core/pdf_generator.py index 2ea2067..3daafc7 100644 --- a/app/core/pdf_generator.py +++ b/app/core/pdf_generator.py @@ -153,6 +153,25 @@ def draw_clipped_text(c, text, x, y, w, h, font, size, rgb, align='center'): c.restoreState() +def fit_text(c, text: str, font: str, size: float, max_w: float) -> str: + """Truncate text with ellipsis so it fits within max_w points.""" + if not text: + return text + if c.stringWidth(text, font, size) <= max_w: + return text + # Binary-search trim + ellipsis = '…' + ellipsis_w = c.stringWidth(ellipsis, font, size) + lo, hi = 0, len(text) + while lo < hi: + mid = (lo + hi + 1) // 2 + if c.stringWidth(text[:mid], font, size) + ellipsis_w <= max_w: + lo = mid + else: + hi = mid - 1 + return (text[:lo] + ellipsis) if lo < len(text) else text + + def generate_pdf(doc) -> bytes: if not doc.blocks: raise ScenarsError("No blocks provided") @@ -373,19 +392,29 @@ def generate_pdf(doc) -> bytes: dim_rgb = ((text_rgb[0] * 0.78, text_rgb[1] * 0.78, text_rgb[2] * 0.78) if is_light(pt.color) else (0.82, 0.82, 0.82)) - # Determine vertical layout: how many lines fit? - has_responsible = bool(block.responsible) - sup_size = max(4.0, block_title_font * 0.65) + # Available width for text (inset + 2pt padding each side) + text_w_avail = max(1.0, bw - 2 * inset - 4) + + sup_size = max(4.0, block_title_font * 0.65) resp_size = max(4.0, block_time_font) + # Truncate title to fit (leave room for superscript number) + sup_reserve = (c.stringWidth(str(fn_num), _FONT_BOLD, sup_size) + 1.5 + if fn_num else 0) + fitted_title = fit_text(c, title_text, _FONT_BOLD, block_title_font, + text_w_avail - sup_reserve) + + # Determine vertical layout: how many lines fit? + has_responsible = bool(block.responsible) if has_responsible and row_h >= block_title_font + resp_size + 3: - # Two-line: title (with superscript) + responsible + # Two-line: title + responsible title_y = row_y + row_h * 0.55 resp_y = row_y + row_h * 0.55 - block_title_font - 1 - # responsible + fitted_resp = fit_text(c, block.responsible, _FONT_ITALIC, resp_size, + text_w_avail) c.setFont(_FONT_ITALIC, resp_size) set_fill(c, dim_rgb) - c.drawCentredString(bx + bw / 2, resp_y, block.responsible) + c.drawCentredString(bx + bw / 2, resp_y, fitted_resp) else: # Single line: title centred title_y = row_y + (row_h - block_title_font) / 2 @@ -395,16 +424,15 @@ def generate_pdf(doc) -> bytes: set_fill(c, text_rgb) if fn_num is not None: # Draw title then superscript footnote number - title_w = c.stringWidth(title_text, _FONT_BOLD, block_title_font) - tx = bx + bw / 2 - title_w / 2 - c.drawString(tx, title_y, title_text) - # Superscript: small number raised by ~font_size * 0.5 + title_w = c.stringWidth(fitted_title, _FONT_BOLD, block_title_font) + tx = bx + bw / 2 - (title_w + sup_reserve) / 2 + c.drawString(tx, title_y, fitted_title) c.setFont(_FONT_BOLD, sup_size) set_fill(c, dim_rgb) c.drawString(tx + title_w + 0.5, title_y + block_title_font * 0.45, str(fn_num)) else: - c.drawCentredString(bx + bw / 2, title_y, title_text) + c.drawCentredString(bx + bw / 2, title_y, fitted_title) c.restoreState() diff --git a/app/static/css/app.css b/app/static/css/app.css index 19af716..a43cee6 100644 --- a/app/static/css/app.css +++ b/app/static/css/app.css @@ -768,12 +768,6 @@ body { position: relative; } -/* Allow blocks to visually leave their row during cross-day drag */ -.day-rows.dragging .day-row, -.day-rows.dragging .day-timeline { - overflow: visible; -} - /* Highlight the row being dragged over */ .day-timeline.drag-target { background: #eff6ff; @@ -781,6 +775,13 @@ body { outline-offset: -2px; } +/* Floating ghost shown during cross-day drag */ +.drag-ghost { + border-radius: 4px; + pointer-events: none; + user-select: none; +} + .day-label { display: flex; align-items: center; diff --git a/app/static/index.html b/app/static/index.html index 74b2676..46ceb76 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -13,7 +13,7 @@

Scénář Creator

- v4.4 + v4.5