a789495a98
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
296 lines
12 KiB
Python
Executable File
296 lines
12 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Probe Jcode pinned Mermaid fit planning for screenshot repros.
|
|
|
|
This intentionally mirrors the geometry math in src/tui/ui_diagram_pane.rs so a
|
|
bad crop can be reproduced without compiling the whole Rust test binary.
|
|
Defaults are the 2026-05-07 Beetle/Harbor clipped Mermaid screenshot:
|
|
inner pane 73x46 cells, font 8x16 px, PNG 1180x1470 px.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import math
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
TARGET_UTILIZATION_PERCENT = 85.0
|
|
MIN_READABLE_ZOOM_PERCENT = 70
|
|
MAX_AUTO_FILL_ZOOM_PERCENT = 1000
|
|
|
|
|
|
def clamp(value: int, lo: int, hi: int) -> int:
|
|
return max(lo, min(hi, value))
|
|
|
|
|
|
def utilization_percent(used: int, total: int) -> float:
|
|
return 0.0 if total == 0 else (used * 100.0) / total
|
|
|
|
|
|
def div_ceil(value: int, divisor: int) -> int:
|
|
return 0 if divisor == 0 else (value + divisor - 1) // divisor
|
|
|
|
|
|
def axis_fill_zoom_percent(available_cells: int, image_px: int, cell_px: int) -> int:
|
|
if available_cells == 0 or image_px == 0 or cell_px == 0:
|
|
return 100
|
|
return clamp((available_cells * cell_px * 100) // max(image_px, 1), 1, MAX_AUTO_FILL_ZOOM_PERCENT)
|
|
|
|
|
|
def fit_zoom_percent_for_area(width_cells: int, height_cells: int, img_w_px: int, img_h_px: int, font_w: int, font_h: int) -> int:
|
|
if width_cells == 0 or height_cells == 0 or img_w_px == 0 or img_h_px == 0:
|
|
return 100
|
|
zoom_w = (width_cells * max(font_w, 1) * 100) // max(img_w_px, 1)
|
|
zoom_h = (height_cells * max(font_h, 1) * 100) // max(img_h_px, 1)
|
|
return clamp(min(zoom_w, zoom_h), 1, MAX_AUTO_FILL_ZOOM_PERCENT)
|
|
|
|
|
|
def vcenter_fitted(width_cells: int, height_cells: int, img_w_px: int, img_h_px: int, font_w: int, font_h: int) -> dict[str, int]:
|
|
if width_cells == 0 or height_cells == 0 or img_w_px == 0 or img_h_px == 0:
|
|
return {"x": 0, "y": 0, "width": width_cells, "height": height_cells}
|
|
area_w_px = width_cells * max(font_w, 1)
|
|
area_h_px = height_cells * max(font_h, 1)
|
|
scale = min(area_w_px / img_w_px, area_h_px / img_h_px)
|
|
fitted_w = min(math.ceil((img_w_px * scale) / max(font_w, 1)), width_cells)
|
|
fitted_h = min(math.ceil((img_h_px * scale) / max(font_h, 1)), height_cells)
|
|
return {
|
|
"x": (width_cells - fitted_w) // 2,
|
|
"y": (height_cells - fitted_h) // 2,
|
|
"width": fitted_w,
|
|
"height": fitted_h,
|
|
}
|
|
|
|
|
|
def centered_viewport_scroll_cells(image_px: int, area_cells: int, zoom_percent: int, cell_px: int) -> int:
|
|
if image_px == 0 or area_cells == 0 or zoom_percent == 0 or cell_px == 0:
|
|
return 0
|
|
view_px = area_cells * cell_px * 100 // zoom_percent
|
|
max_scroll_px = max(0, image_px - view_px)
|
|
if max_scroll_px == 0:
|
|
return 0
|
|
cell_px_at_zoom = max(div_ceil(cell_px * 100, zoom_percent), 1)
|
|
return (max_scroll_px // 2) // cell_px_at_zoom
|
|
|
|
|
|
def plan(width_cells: int, height_cells: int, img_w_px: int, img_h_px: int, font_w: int, font_h: int) -> dict[str, Any]:
|
|
contain = vcenter_fitted(width_cells, height_cells, img_w_px, img_h_px, font_w, font_h)
|
|
fit_zoom = fit_zoom_percent_for_area(width_cells, height_cells, img_w_px, img_h_px, font_w, font_h)
|
|
width_fill_zoom = axis_fill_zoom_percent(width_cells, img_w_px, font_w)
|
|
height_fill_zoom = axis_fill_zoom_percent(height_cells, img_h_px, font_h)
|
|
preferred_fill_zoom = clamp(max(width_fill_zoom, height_fill_zoom), MIN_READABLE_ZOOM_PERCENT, MAX_AUTO_FILL_ZOOM_PERCENT)
|
|
|
|
width_utilization = utilization_percent(contain["width"], width_cells)
|
|
height_utilization = utilization_percent(contain["height"], height_cells)
|
|
area_utilization = utilization_percent(contain["width"] * contain["height"], width_cells * height_cells)
|
|
underutilized = (
|
|
width_utilization < TARGET_UTILIZATION_PERCENT
|
|
or height_utilization < TARGET_UTILIZATION_PERCENT
|
|
or area_utilization < TARGET_UTILIZATION_PERCENT
|
|
)
|
|
meaningfully_larger = preferred_fill_zoom > fit_zoom + 5
|
|
|
|
old_would_fill = (fit_zoom < MIN_READABLE_ZOOM_PERCENT or underutilized) and meaningfully_larger
|
|
fixed_would_fill = underutilized and meaningfully_larger
|
|
|
|
fill_plan = {
|
|
"mode": f"fit-fill@{preferred_fill_zoom}%",
|
|
"zoom_percent": preferred_fill_zoom,
|
|
"scroll_x": centered_viewport_scroll_cells(img_w_px, width_cells, preferred_fill_zoom, font_w),
|
|
"scroll_y": centered_viewport_scroll_cells(img_h_px, height_cells, preferred_fill_zoom, font_h),
|
|
}
|
|
|
|
return {
|
|
"input": {
|
|
"inner_width_cells": width_cells,
|
|
"inner_height_cells": height_cells,
|
|
"image_width_px": img_w_px,
|
|
"image_height_px": img_h_px,
|
|
"font_width_px": font_w,
|
|
"font_height_px": font_h,
|
|
},
|
|
"contain_rect_cells": contain,
|
|
"utilization_percent": {
|
|
"width": width_utilization,
|
|
"height": height_utilization,
|
|
"area": area_utilization,
|
|
},
|
|
"fit_zoom_percent": fit_zoom,
|
|
"axis_fill_zoom_percent": {"width": width_fill_zoom, "height": height_fill_zoom},
|
|
"preferred_fill_zoom_percent": preferred_fill_zoom,
|
|
"underutilized": underutilized,
|
|
"meaningfully_larger": meaningfully_larger,
|
|
"old_buggy_plan": fill_plan if old_would_fill else {"mode": "fit", "rect": contain},
|
|
"fixed_plan": fill_plan if fixed_would_fill else {"mode": "fit", "rect": contain},
|
|
"repro_was_clipping_bug": old_would_fill and not fixed_would_fill,
|
|
}
|
|
|
|
|
|
def maybe_png_info(path: str | None) -> dict[str, Any] | None:
|
|
if not path:
|
|
return None
|
|
png = Path(path).expanduser()
|
|
info: dict[str, Any] = {"path": str(png), "exists": png.exists()}
|
|
if not png.exists():
|
|
return info
|
|
try:
|
|
from PIL import Image # type: ignore
|
|
except Exception as exc: # pragma: no cover - diagnostic fallback
|
|
info["pil_error"] = str(exc)
|
|
return info
|
|
im = Image.open(png).convert("RGBA")
|
|
bbox = im.getchannel("A").getbbox()
|
|
info["size"] = list(im.size)
|
|
info["alpha_bbox"] = list(bbox) if bbox else None
|
|
if bbox:
|
|
left, top, right, bottom = bbox
|
|
info["content_size"] = [right - left, bottom - top]
|
|
info["transparent_margins"] = {
|
|
"left": left,
|
|
"top": top,
|
|
"right": im.size[0] - right,
|
|
"bottom": im.size[1] - bottom,
|
|
}
|
|
return info
|
|
|
|
|
|
def visual_fit_metrics(
|
|
*,
|
|
width_cells: int,
|
|
height_cells: int,
|
|
img_w_px: int,
|
|
img_h_px: int,
|
|
font_w: int,
|
|
font_h: int,
|
|
contain_rect: dict[str, int],
|
|
alpha_bbox: list[int] | tuple[int, int, int, int] | None,
|
|
) -> dict[str, Any] | None:
|
|
"""Project the visible PNG alpha bbox into the TUI pane.
|
|
|
|
The layout planner works in cells and the PNG alpha bbox works in pixels.
|
|
Projecting the bbox into cells gives a camera-like metric that matches the
|
|
visual complaint: even if the full image rect is centered, is the actual
|
|
non-transparent diagram content centered in the pane?
|
|
"""
|
|
if not alpha_bbox or img_w_px <= 0 or img_h_px <= 0 or font_w <= 0 or font_h <= 0:
|
|
return None
|
|
|
|
left_px, top_px, right_px, bottom_px = [float(value) for value in alpha_bbox]
|
|
render_w_px = contain_rect["width"] * font_w
|
|
render_h_px = contain_rect["height"] * font_h
|
|
scale_x = render_w_px / img_w_px
|
|
scale_y = render_h_px / img_h_px
|
|
|
|
content_left = contain_rect["x"] + (left_px * scale_x / font_w)
|
|
content_top = contain_rect["y"] + (top_px * scale_y / font_h)
|
|
content_right = contain_rect["x"] + (right_px * scale_x / font_w)
|
|
content_bottom = contain_rect["y"] + (bottom_px * scale_y / font_h)
|
|
content_width = max(0.0, content_right - content_left)
|
|
content_height = max(0.0, content_bottom - content_top)
|
|
|
|
left_blank = content_left
|
|
right_blank = width_cells - content_right
|
|
top_blank = content_top
|
|
bottom_blank = height_cells - content_bottom
|
|
center_x = (content_left + content_right) / 2.0
|
|
center_y = (content_top + content_bottom) / 2.0
|
|
pane_center_x = width_cells / 2.0
|
|
pane_center_y = height_cells / 2.0
|
|
offset_x = center_x - pane_center_x
|
|
offset_y = center_y - pane_center_y
|
|
|
|
return {
|
|
"content_bbox_cells": {
|
|
"left": content_left,
|
|
"top": content_top,
|
|
"right": content_right,
|
|
"bottom": content_bottom,
|
|
"width": content_width,
|
|
"height": content_height,
|
|
},
|
|
"blank_cells": {
|
|
"left": left_blank,
|
|
"right": right_blank,
|
|
"top": top_blank,
|
|
"bottom": bottom_blank,
|
|
},
|
|
"blank_imbalance_cells": {
|
|
"horizontal_right_minus_left": right_blank - left_blank,
|
|
"vertical_bottom_minus_top": bottom_blank - top_blank,
|
|
},
|
|
"content_center_offset_cells": {
|
|
"x": offset_x,
|
|
"y": offset_y,
|
|
},
|
|
"content_area_utilization_percent": utilization_percent(
|
|
int(round(content_width * content_height * 1000)),
|
|
int(width_cells * height_cells * 1000),
|
|
),
|
|
"camera_centered": abs(offset_x) <= 1.0 and abs(offset_y) <= 1.0,
|
|
"note": (
|
|
"Low content_area_utilization can be normal for a wide/short or tall/narrow diagram. "
|
|
"A large content_center_offset or top/bottom blank imbalance means the camera/placement is wrong."
|
|
),
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--inner", default="73x46", help="inner pane size in cells, WIDTHxHEIGHT")
|
|
parser.add_argument("--image", default="1180x1470", help="rendered PNG size in px, WIDTHxHEIGHT")
|
|
parser.add_argument("--font", default="8x16", help="terminal cell size in px, WIDTHxHEIGHT")
|
|
parser.add_argument("--png", help="optional rendered PNG path to inspect alpha bounds")
|
|
parser.add_argument(
|
|
"--alpha-bbox",
|
|
help="optional alpha/content bbox LEFT,TOP,RIGHT,BOTTOM in PNG pixels when --png is unavailable",
|
|
)
|
|
parser.add_argument(
|
|
"--assert-camera-centered",
|
|
action="store_true",
|
|
help="fail if projected visible content is more than one cell away from pane center",
|
|
)
|
|
parser.add_argument(
|
|
"--assert-old-clipping-repro",
|
|
action="store_true",
|
|
help="fail unless the old readability-floor rule would have produced the known clipping bug",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
def parse_pair(raw: str) -> tuple[int, int]:
|
|
left, right = raw.lower().split("x", 1)
|
|
return int(left), int(right)
|
|
|
|
width_cells, height_cells = parse_pair(args.inner)
|
|
img_w_px, img_h_px = parse_pair(args.image)
|
|
font_w, font_h = parse_pair(args.font)
|
|
result = plan(width_cells, height_cells, img_w_px, img_h_px, font_w, font_h)
|
|
png_info = maybe_png_info(args.png)
|
|
if png_info is not None:
|
|
result["png"] = png_info
|
|
alpha_bbox = None
|
|
if args.alpha_bbox:
|
|
alpha_bbox = [int(part) for part in args.alpha_bbox.split(",")]
|
|
elif png_info is not None:
|
|
alpha_bbox = png_info.get("alpha_bbox")
|
|
metrics = visual_fit_metrics(
|
|
width_cells=width_cells,
|
|
height_cells=height_cells,
|
|
img_w_px=img_w_px,
|
|
img_h_px=img_h_px,
|
|
font_w=font_w,
|
|
font_h=font_h,
|
|
contain_rect=result["contain_rect_cells"],
|
|
alpha_bbox=alpha_bbox,
|
|
)
|
|
if metrics is not None:
|
|
result["visual_fit_metrics"] = metrics
|
|
print(json.dumps(result, indent=2, sort_keys=True))
|
|
if args.assert_camera_centered and metrics is not None:
|
|
return 0 if metrics["camera_centered"] else 2
|
|
if args.assert_old_clipping_repro:
|
|
return 0 if result["repro_was_clipping_bug"] else 3
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|