diff --git a/web/app.py b/web/app.py index 101a60c..d6d5419 100644 --- a/web/app.py +++ b/web/app.py @@ -52,18 +52,40 @@ def _load_fonts(): _FONTS = _load_fonts() _DEFAULT_FONT = 'alte-din-1451-mittelschrift.regular' -# SVG absolute coordinate constants (1 user unit = 1 mm) -ZERO_X = 60.844130 -MAX_X = 180.251230 -BAR_Y = 107.54670 -SCALE_LEN = MAX_X - ZERO_X # 119.4071 mm +# SVG absolute coordinate constants (1 user unit = 1 mm), pre-layer-translate +# local frame — the wrapper applies translate(-60.717741,-107.14126). +PIVOT_X, PIVOT_Y = 107.42845, 162.62819 +R0 = 43.842751 # rim / inner-tick radius +ANGLE_MIN, ANGLE_MAX = -45.667193, 45.667193 # degrees from vertical, clockwise-positive -BIG_H = 5.0895 -SMALL_H = 2.6683 +MAJOR_LEN, MAJOR_W = 4.7839203, 0.79658329 +MEDIUM_LEN, MEDIUM_W = 3.1215858, 0.37985462 +SMALL_LEN, SMALL_W = 1.8121802, 0.27478597 -_UNIT_AREA_WIDTH = 18.445 -_UNIT_SCALE_X = 0.96486402 -_UNIT_MAX_FS = 18.7981 +LABEL_RADIUS = 49.862 + +UNIT_X, UNIT_Y = 107.03512, 136.64594 + +LEFT_X, LEFT_Y0 = 73.432251, 145.16051 +RIGHT_X, RIGHT_Y0 = 142.4485, 145.32773 +CORNER_LINE_SPACING = 4.4586 +CORNER_FONT_SIZE = 3.56685 + +PIVOT_CIRCLE_R = 1.7287594 + +BODY_D = ( + "m 62.162097,107.14126 c -0.800087,0 -1.444356,0.64428 -1.444356,1.44436 v 45.44839 " + "c 0,0.80008 0.644269,1.44436 1.444356,1.44436 h 7.345288 c 0.902634,0 1.233517,1.12706 " + "1.233517,1.12706 l 0.09043,3.03909 21.903573,-0.0904 2.226221,-5.98775 c 8.352594,-2.8884 " + "16.624974,-2.8884 24.817094,0 l 2.22674,5.98775 21.90357,0.0904 0.0904,-3.03909 c 0,0 " + "0.33088,-1.12706 1.23352,-1.12706 h 7.86567 c 0.80009,0 1.44384,-0.64428 1.44384,-1.44436 " + "v -45.44839 c 0,-0.80008 -0.64375,-1.44436 -1.44384,-1.44436 z m 29.619898,45.85354 " + "a 1.3990685,1.3990685 0 0 1 1.398881,1.39939 1.3990685,1.3990685 0 0 1 -1.398881,1.39888 " + "1.3990685,1.3990685 0 0 1 -1.399398,-1.39888 1.3990685,1.3990685 0 0 1 1.399398,-1.39939 " + "z m 31.220835,0 a 1.3990685,1.3990685 0 0 1 1.39888,1.39939 1.3990685,1.3990685 0 0 1 " + "-1.39888,1.39888 1.3990685,1.3990685 0 0 1 -1.3994,-1.39888 1.3990685,1.3990685 0 0 1 " + "1.3994,-1.39939 z" +) # --- Inline markup for the unit label ------------------------------------- _SUP_SCALE = 0.62 @@ -74,9 +96,15 @@ _FRAC_AXIS = 0.32 _FRAC_GAP = 0.14 _FRAC_SIDE = 0.12 -_UNIT_BOX_X0, _UNIT_BOX_X1 = 39.6, 57.4 -_UNIT_BOX_Y0, _UNIT_BOX_Y1 = 92.8, 114.8 -_UNIT_BOX_HALF_Y0 = (_UNIT_BOX_Y0 + _UNIT_BOX_Y1) / 2 # 103.8 — bottom half starts here +_UNIT_BOX_X0, _UNIT_BOX_X1 = UNIT_X - 15.0, UNIT_X + 15.0 +_UNIT_BOX_Y0, _UNIT_BOX_Y1 = UNIT_Y - 6.0, UNIT_Y + 6.0 +_UNIT_MAX_FS = 9.46938021 +_UNIT_SCALE_X = 1.0 + + +def _point_at(radius: float, angle_deg: float): + th = math.radians(angle_deg) + return PIVOT_X + radius * math.sin(th), PIVOT_Y - radius * math.cos(th) def _glyph_svg(ch: str, x: float, y: float, fs: float, fill: str, fd: _FontData): @@ -247,11 +275,10 @@ def _has_markup(s: str) -> bool: return any(c in s for c in '^_') or ('{' in s and '}' in s) -def _markup_unit_svg(unit: str, fd: _FontData, fill='#1a1a1a', box_y0=None, box_y1=None) -> str: +def _markup_unit_svg(unit: str, fd: _FontData, fill='#1a1a1a') -> str: nodes = _parse_markup(unit) x0, x1 = _UNIT_BOX_X0, _UNIT_BOX_X1 - y0 = box_y0 if box_y0 is not None else _UNIT_BOX_Y0 - y1 = box_y1 if box_y1 is not None else _UNIT_BOX_Y1 + y0, y1 = _UNIT_BOX_Y0, _UNIT_BOX_Y1 xc, yc = (x0 + x1) / 2, (y0 + y1) / 2 w1 = _measure(nodes, 1.0, fd) @@ -270,13 +297,6 @@ def _markup_unit_svg(unit: str, fd: _FontData, fill='#1a1a1a', box_y0=None, box_ return f'\n{chr(10).join(parts)}\n' -def _unit_font_size(unit: str, fd: _FontData) -> float: - em_w = _measure(_parse_markup(unit), 1.0, fd) - if em_w <= 0: - return _UNIT_MAX_FS - return min(_UNIT_MAX_FS, _UNIT_AREA_WIDTH / (em_w * _UNIT_SCALE_X)) - - def _text_to_paths(text, x, y, font_size, fd: _FontData, anchor='start', fill='#1a1a1a', wrap_transform=None): s = font_size / fd.upm @@ -319,90 +339,10 @@ def _fmt(v: float) -> str: return s.rstrip('0').rstrip('.') if '.' in s else s -def generate_svg(unit, min_val, max_val, range_label, big_ticks, small_ticks, label_count, - use_paths=True, scale_type='linear', custom_labels=None, label_size=9, - font_key=None, half_height_unit=False): +def generate_svg(unit, min_val, max_val, left_label, right_label, big_ticks, small_ticks, + label_count, use_paths=True, scale_type='linear', custom_labels=None, + label_size=5.1126, font_key=None): fd = _FONTS.get(font_key) or _FONTS.get(_DEFAULT_FONT) or next(iter(_FONTS.values())) - unit_box_y0 = _UNIT_BOX_HALF_Y0 if half_height_unit else None - - if scale_type == 'log': - if min_val <= 0 or max_val <= 0: - raise ValueError("Logarithmic scale requires min and max values greater than 0.") - lmin, lmax = math.log(min_val), math.log(max_val) - lspan = lmax - lmin - if lspan == 0: - raise ValueError("Logarithmic scale requires min and max values to differ.") - - def value_to_x(v): - return ZERO_X + SCALE_LEN * (math.log(v) - lmin) / lspan - - def major_value(i): - return min_val * (max_val / min_val) ** (i / big_ticks) - - elif scale_type == 'sqrt': - if min_val < 0: - raise ValueError("Square-root scale requires min value ≥ 0.") - span = max_val - min_val - if span <= 0: - raise ValueError("Square-root scale requires max value greater than min.") - - def value_to_x(v): - return ZERO_X + SCALE_LEN * math.sqrt((v - min_val) / span) - - def major_value(i): - return min_val + span * (i / big_ticks) ** 2 - - elif scale_type == 'reciprocal': - if min_val <= 0: - raise ValueError("Reciprocal scale requires min and max values greater than 0.") - if not math.isinf(max_val) and max_val <= min_val: - raise ValueError("Reciprocal scale requires max value greater than min.") - if math.isinf(max_val): - def value_to_x(v): - if math.isinf(v): - return ZERO_X - return ZERO_X + SCALE_LEN * min_val / v - - def major_value(i): - if i == 0: - return math.inf - return min_val * big_ticks / i - else: - if max_val <= 0: - raise ValueError("Reciprocal scale requires min and max values greater than 0.") - k = 1.0 / min_val - 1.0 / max_val - - def value_to_x(v): - return ZERO_X + SCALE_LEN * (1.0 / v - 1.0 / max_val) / k - - def major_value(i): - return 1.0 / (1.0 / max_val + i / big_ticks * k) - - else: - span = max_val - min_val - - def value_to_x(v): - if span == 0: - return ZERO_X - return ZERO_X + SCALE_LEN * (v - min_val) / span - - def major_value(i): - return min_val + span * i / big_ticks - - big_interval = SCALE_LEN / big_ticks - big_xs = [ZERO_X + i * big_interval for i in range(big_ticks + 1)] - big_vals = [major_value(i) for i in range(big_ticks + 1)] - - if custom_labels: - label_count = max(1, min(len(custom_labels), big_ticks + 1)) - custom_labels = custom_labels[:label_count] - else: - label_count = max(2, min(label_count, big_ticks + 1)) - - if label_count == 1: - label_indices = [0] - else: - label_indices = [round(k * big_ticks / (label_count - 1)) for k in range(label_count)] font_face = ( '