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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 81 KiB |
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user