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
+1 -1
View File
@@ -14,7 +14,7 @@ The three SVG files represent layers of the same design, intended to be combined
- **`E440-Scale_template.svg`** — Inkscape working template. Contains the case body path (with mounting ears and rim cutout), the pivot guide circle, tick marks, numeric labels `0``50`, the `uA` unit label, and `Text`/`Left` and `Text`/`Right` corner label placeholders.
- **`E440-Scale_paths.svg`** — Paths-only version: all tick marks and text as `<path>`/`<line>` elements, no live text. Used for cutting/printing workflows where fonts must not be embedded as live text.
- **`E440-Scale_text.svg`** — Text-overlay version: tick mark lines plus live `<text>` elements for numeric/unit/corner labels, font embedded as base64.
- **`E440-Scale_text.svg`** — Text-overlay version: tick mark lines plus live `<text>` elements for numeric/corner labels (the unit label is always emitted as outline paths since it supports markup), font embedded as base64.
## Key Design Parameters
+4 -6
View File
@@ -2,8 +2,8 @@
<svg width="93.824249mm" height="57.215687mm" viewBox="0 0 93.824249 57.215687" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-60.717741,-107.14126)">
<path style="fill:#f9f9f9;stroke:#1a1a1a;stroke-width:0.184;stroke-linecap:round;stroke-miterlimit:10" 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" />
<path style="fill:none;stroke:#000000;stroke-width:0.484;stroke-linecap:round;stroke-miterlimit:10" d="M 76.06805,131.98978 A 43.842751,43.842751 0 0 1 107.42845,118.78544 A 43.842751,43.842751 0 0 1 138.78885,131.98978" />
<circle style="fill:none;stroke:#b3b3b3;stroke-width:0.184;stroke-linecap:round;stroke-miterlimit:10" cx="107.42845" cy="162.62819" r="1.7287594" />
<path style="fill:none;stroke:#000000;stroke-width:0.484;stroke-linecap:round;stroke-miterlimit:10" d="M 76.06805,131.98978 A 43.84275,43.84275 0 0 1 107.42845,118.78544 A 43.84275,43.84275 0 0 1 138.78885,131.98978" />
<circle style="fill:none;stroke:#b3b3b3;stroke-width:0.184;stroke-linecap:round;stroke-miterlimit:10" cx="107.42845" cy="162.62819" r="1.72876" />
<line x1="107.42845" y1="118.78544" x2="107.42845" y2="114.00152" style="stroke:#1a1a1a;stroke-width:0.79658;stroke-linecap:butt" transform="rotate(-45.66719,107.42845,162.62819)" />
<line x1="107.42845" y1="118.78544" x2="107.42845" y2="114.00152" style="stroke:#1a1a1a;stroke-width:0.79658;stroke-linecap:butt" transform="rotate(-27.40032,107.42845,162.62819)" />
<line x1="107.42845" y1="118.78544" x2="107.42845" y2="114.00152" style="stroke:#1a1a1a;stroke-width:0.79658;stroke-linecap:butt" transform="rotate(-9.13344,107.42845,162.62819)" />
@@ -66,10 +66,8 @@
<path style="fill:#1a1a1a" transform="translate(107.42845,112.76619) scale(0.00249639,-0.00249639)" d="M698 418V1037Q698 1142 641.5 1201.0Q585 1260 493 1260Q400 1260 344.0 1201.0Q288 1142 288 1037V418Q288 312 344.0 253.5Q400 195 493 195Q585 195 641.5 253.5Q698 312 698 418ZM79 406V1049Q79 1250 205.0 1359.5Q331 1469 493 1469Q654 1469 780.5 1359.5Q907 1250 907 1049V406Q907 205 780.5 96.0Q654 -13 493 -13Q331 -13 205.0 96.0Q79 205 79 406Z"/></g>
<g transform="rotate(45.66719,107.42845,162.62819)"><path style="fill:#1a1a1a" transform="translate(104.97450,112.76619) scale(0.00249639,-0.00249639)" d="M104 668V1456H880V1247H301V918Q401 998 534 998Q617 998 685.5 966.0Q754 934 794 891Q854 826 881.5 749.5Q909 673 909 492Q909 345 887.5 273.5Q866 202 819 142Q774 85 693.0 36.0Q612 -13 503 -13Q362 -13 231.0 74.0Q100 161 77 365H286Q323 195 495 195Q648 195 674.0 300.0Q700 405 700 508Q700 629 659.0 709.0Q618 789 489 789Q368 789 301 668Z"/>
<path style="fill:#1a1a1a" transform="translate(107.42845,112.76619) scale(0.00249639,-0.00249639)" d="M698 418V1037Q698 1142 641.5 1201.0Q585 1260 493 1260Q400 1260 344.0 1201.0Q288 1142 288 1037V418Q288 312 344.0 253.5Q400 195 493 195Q585 195 641.5 253.5Q698 312 698 418ZM79 406V1049Q79 1250 205.0 1359.5Q331 1469 493 1469Q654 1469 780.5 1359.5Q907 1250 907 1049V406Q907 205 780.5 96.0Q654 -13 493 -13Q331 -13 205.0 96.0Q79 205 79 406Z"/></g>
<g transform="translate(107.03512,0) scale(1.0,1) translate(-107.03512,0)">
<path style="fill:#1a1a1a" transform="translate(101.54445,139.98658) scale(0.00462372,-0.00462372)" d="M137 347V1037H346V418Q346 312 404.5 253.5Q463 195 553 195Q640 195 698.5 253.5Q757 312 757 418V1037H966V-1H757V109H753Q656 -13 485 -13Q362 -13 249.5 78.5Q137 170 137 347Z"/>
<path style="fill:#1a1a1a" transform="translate(106.62592,139.98658) scale(0.00462372,-0.00462372)" d="M2 0 534 1458H716L1248 0H1016L904 329H351L235 0ZM416 524H838Q792 659 765.5 735.5Q739 812 719.5 871.0Q700 930 680.0 991.0Q660 1052 627 1156Q592 1048 571.0 983.5Q550 919 530.5 859.0Q511 799 485.5 725.0Q460 651 416 524Z"/>
</g>
<path style="fill:#1a1a1a" transform="translate(101.54445,136.64594) scale(0.00462372,-0.00462372)" d="M137 347V1037H346V418Q346 312 404.5 253.5Q463 195 553 195Q640 195 698.5 253.5Q757 312 757 418V1037H966V-1H757V109H753Q656 -13 485 -13Q362 -13 249.5 78.5Q137 170 137 347Z"/>
<path style="fill:#1a1a1a" transform="translate(106.62592,136.64594) scale(0.00462372,-0.00462372)" d="M2 0 534 1458H716L1248 0H1016L904 329H351L235 0ZM416 524H838Q792 659 765.5 735.5Q739 812 719.5 871.0Q700 930 680.0 991.0Q660 1052 627 1156Q592 1048 571.0 983.5Q550 919 530.5 859.0Q511 799 485.5 725.0Q460 651 416 524Z"/>
<path style="fill:#1a1a1a" transform="translate(73.43225,145.16051) scale(0.00174163,-0.00174163)" d="M403 -1V1260H4V1456H1023V1260H624V-1Z"/>
<path style="fill:#1a1a1a" transform="translate(75.21568,145.16051) scale(0.00174163,-0.00174163)" d="M936 439H290Q290 311 359.5 247.0Q429 183 537 183Q666 183 765 283L916 154Q839 64 742.5 25.5Q646 -13 541 -13Q381 -13 231.0 90.5Q81 194 81 514Q81 817 219.5 933.0Q358 1049 509 1049Q680 1049 808.0 930.5Q936 812 936 611ZM290 611H728Q723 728 662.0 790.0Q601 852 509 852Q416 852 354.5 790.0Q293 728 290 611Z"/>
<path style="fill:#1a1a1a" transform="translate(76.99039,145.16051) scale(0.00174163,-0.00174163)" d="M376 529 36 1037H286L493 701L700 1037H950L610 529L964 -1H714L493 344L272 -1H22Z"/>

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

+5 -7
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 81 KiB

+2 -2
View File
@@ -208,7 +208,7 @@ If you develop a new program, and you want it to be of the greatest possible use
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
M1730-Scale
E440-Scale
Copyright (C) 2026 adebaumann
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
@@ -221,7 +221,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
M1730-Scale Copyright (C) 2026 adebaumann
E440-Scale Copyright (C) 2026 adebaumann
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
+1 -1
View File
@@ -73,7 +73,7 @@ Fonts are loaded at startup from the `fonts/` directory in the repo root.
| Mapping | Major tick values | Minor tick placement | Constraints |
|---|---|---|---|
| Linear | Evenly spaced | Uniform | None |
| √ | Quadratic (0, 1, 4, 9 … × max) — compresses low values | Crowded toward the low end of each interval | Min ≥ 0 |
| √ | Quadratic (0, 1, 4, 9 … × max) — compresses low values | Crowded toward the high end of each interval | Min ≥ 0 |
| Log | Geometric (min × r⁰, r¹, r² …) — each interval spans the same ratio | Crowded toward the high end of each interval | Min and Max > 0 |
| 1/x | Harmonic (values decrease left → right, e.g. ∞, 10, 5, 2, 1) — compresses high values | Uniform (in angle space, i.e. proportional to 1/v) | Min and Max > 0; Max may be ∞ |
+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)