Fix final-review findings: unit-label baseline, sqrt minor-tick math, dead code, edge cases

- Unit label was vertical-centered on a box derived from a baseline
  coordinate, shifting it 3.34mm too low and making the generated
  deliverables inconsistent with E440-Scale_template.svg. UNIT_Y is now
  used directly as the baseline; regenerated E440-Scale_paths.svg and
  E440-Scale_text.svg accordingly.
- sqrt scale's minor-tick angle formula double-applied the square-root
  mapping, badly misplacing minor ticks. Removed the special case so sqrt
  falls through to the already-correct generic value-interpolation branch.
  Corrected README's sqrt row (crowds toward the high end, not low).
- Removed dead code: unused _has_markup(), identity-transform wrapper and
  _UNIT_SCALE_X in _markup_unit_svg, unreachable max_val<=0 check,
  redundant math.isinf() in the minor-tick condition.
- Linear scale with min==max now raises ValueError like the other three
  mappings, instead of silently producing a degenerate dial.
- Form numeric parsing moved inside the try/except so bad input returns
  400 instead of crashing with 500.
- left_label/right_label now split on splitlines() to handle CRLF from
  browser textareas correctly.
- Formatting consistency: PIVOT_CIRCLE_R/R0 use :.5f like other constants;
  font_face indentation matches its sibling elements.
- LICENSE and CLAUDE.md: last M1730-Scale references and a doc inaccuracy
  about which labels are live text vs. outlined paths.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 00:48:54 +02:00
parent fce7040b21
commit 3b1e244d55
6 changed files with 35 additions and 51 deletions
+22 -34
View File
@@ -99,7 +99,6 @@ _FRAC_SIDE = 0.12
_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):
@@ -271,30 +270,23 @@ def _draw_nodes(nodes, x: float, y: float, fs: float, fill: str, fd: _FontData):
return parts, cx - x
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') -> str:
nodes = _parse_markup(unit)
x0, x1 = _UNIT_BOX_X0, _UNIT_BOX_X1
y0, y1 = _UNIT_BOX_Y0, _UNIT_BOX_Y1
xc, yc = (x0 + x1) / 2, (y0 + y1) / 2
xc = (x0 + x1) / 2
w1 = _measure(nodes, 1.0, fd)
top1, bot1 = _vbounds(nodes, 1.0, fd)
fs = _UNIT_MAX_FS
if w1 > 0:
fs = min(fs, (x1 - x0) / (w1 * _UNIT_SCALE_X))
fs = min(fs, (x1 - x0) / w1)
if bot1 - top1 > 0:
fs = min(fs, (y1 - y0) / (bot1 - top1))
w = _measure(nodes, fs, fd)
top, bot = _vbounds(nodes, fs, fd)
y_base = yc - (top + bot) / 2
parts, _ = _draw_nodes(nodes, xc - w / 2, y_base, fs, fill, fd)
cond = f'translate({xc:.5f},0) scale({_UNIT_SCALE_X},1) translate({-xc:.5f},0)'
return f'<g transform="{cond}">\n{chr(10).join(parts)}\n</g>'
parts, _ = _draw_nodes(nodes, xc - w / 2, UNIT_Y, fs, fill, fd)
return '\n'.join(parts)
def _text_to_paths(text, x, y, font_size, fd: _FontData, anchor='start', fill='#1a1a1a', wrap_transform=None):
@@ -387,8 +379,6 @@ def generate_svg(unit, min_val, max_val, left_label, right_label, big_ticks, sma
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_angle(v):
@@ -399,10 +389,10 @@ def generate_svg(unit, min_val, max_val, left_label, right_label, big_ticks, sma
else:
span = max_val - min_val
if span == 0:
raise ValueError("Linear scale requires min and max values to differ.")
def value_to_angle(v):
if span == 0:
return ANGLE_MIN
return ANGLE_MIN + (ANGLE_MAX - ANGLE_MIN) * (v - min_val) / span
def major_value(i):
@@ -434,7 +424,7 @@ def generate_svg(unit, min_val, max_val, left_label, right_label, big_ticks, sma
)
font_face = (
'<defs><style>'
' <defs><style>'
f'@font-face {{font-family:"{fd.label}";'
f'src:url("data:font/truetype;base64,{fd.b64}") format("truetype");}}'
'</style></defs>'
@@ -463,14 +453,14 @@ def generate_svg(unit, min_val, max_val, left_label, right_label, big_ticks, sma
parts.append(
f' <path style="fill:none;stroke:#000000;stroke-width:0.484;'
f'stroke-linecap:round;stroke-miterlimit:10"'
f' d="M {p0[0]:.5f},{p0[1]:.5f} A {R0},{R0} 0 0 1 {pmid[0]:.5f},{pmid[1]:.5f}'
f' A {R0},{R0} 0 0 1 {p1[0]:.5f},{p1[1]:.5f}" />'
f' d="M {p0[0]:.5f},{p0[1]:.5f} A {R0:.5f},{R0:.5f} 0 0 1 {pmid[0]:.5f},{pmid[1]:.5f}'
f' A {R0:.5f},{R0:.5f} 0 0 1 {p1[0]:.5f},{p1[1]:.5f}" />'
)
parts.append(
f' <circle style="fill:none;stroke:#b3b3b3;stroke-width:0.184;'
f'stroke-linecap:round;stroke-miterlimit:10"'
f' cx="{PIVOT_X:.5f}" cy="{PIVOT_Y:.5f}" r="{PIVOT_CIRCLE_R}" />'
f' cx="{PIVOT_X:.5f}" cy="{PIVOT_Y:.5f}" r="{PIVOT_CIRCLE_R:.5f}" />'
)
for angle in big_angles:
@@ -481,9 +471,7 @@ def generate_svg(unit, min_val, max_val, left_label, right_label, big_ticks, sma
medium_j = sub // 2 if small_ticks % 2 == 1 else None
for i in range(big_ticks):
for j in range(1, small_ticks + 1):
if scale_type == 'sqrt':
angle = big_angles[i] + (j / sub) ** 2 * (big_angles[i + 1] - big_angles[i])
elif scale_type == 'reciprocal' or math.isinf(big_vals[i]):
if scale_type == 'reciprocal':
angle = big_angles[i] + (j / sub) * (big_angles[i + 1] - big_angles[i])
else:
v = big_vals[i] + j * (big_vals[i + 1] - big_vals[i]) / sub
@@ -507,8 +495,8 @@ def generate_svg(unit, min_val, max_val, left_label, right_label, big_ticks, sma
parts.append(' ' + _markup_unit_svg(unit, fd))
left_lines = left_label.split('\n')[:2]
right_lines = right_label.split('\n')[:2]
left_lines = left_label.splitlines()[:2]
right_lines = right_label.splitlines()[:2]
for i, line in enumerate(left_lines):
if not line:
@@ -548,23 +536,23 @@ def index():
@app.route('/generate', methods=['POST'])
def generate():
unit = request.form.get('unit', '%')
min_val = float(request.form.get('min', 0))
max_val = float(request.form.get('max', 100))
left_label = request.form.get('left_label', '')
right_label = request.form.get('right_label', '')
big_ticks = max(1, min(50, int(request.form.get('big_ticks', 10))))
small_ticks = max(0, min(20, int(request.form.get('small_ticks', 4))))
label_count = max(2, int(request.form.get('label_count', 6)))
use_paths = request.form.get('output_mode', 'paths') == 'paths'
scale_type = request.form.get('scale_type', 'linear')
font_key = request.form.get('font', _DEFAULT_FONT)
if request.form.get('max_is_inf') == '1':
max_val = math.inf
custom_labels = [s.strip() for s in request.form.get('labels', '').split(',') if s.strip()] or None
label_size = max(0.5, min(25.0, float(request.form.get('label_size', 5.1126))))
try:
min_val = float(request.form.get('min', 0))
max_val = float(request.form.get('max', 100))
big_ticks = max(1, min(50, int(request.form.get('big_ticks', 10))))
small_ticks = max(0, min(20, int(request.form.get('small_ticks', 4))))
label_count = max(2, int(request.form.get('label_count', 6)))
label_size = max(0.5, min(25.0, float(request.form.get('label_size', 5.1126))))
if request.form.get('max_is_inf') == '1':
max_val = math.inf
svg = generate_svg(unit, min_val, max_val, left_label, right_label, big_ticks,
small_ticks, label_count, use_paths, scale_type, custom_labels,
label_size, font_key)