49 KiB
E440 Arc-Dial Scale Generator Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Replace the M1730 linear-strip scale generator with an E440-style round-dial (arc) scale generator, matching the hand-drawn reference e440.svg, across the web app, the three deliverable SVG files, and all docs/infra naming.
Architecture: web/app.py's generate_svg() is rewritten around polar geometry (a fixed pivot point, fixed rim radius, fixed angular sweep) instead of the old Cartesian geometry (fixed x-range, fixed y-baseline). Ticks and labels are drawn as <line>/<text>/path elements positioned "as if angle=0" (straight up from the pivot) and then given an SVG rotate(angle, PIVOT_X, PIVOT_Y) transform — this reuses the reference file's own rotation trick but around an explicit pivot, which avoids inverse-rotation math. All four scale mappings (linear/√/log/1÷x), both output modes (paths/text), custom labels, and unit markup are ported by substituting angle for x. range_label is replaced by independent left_label/right_label fields (each up to 2 lines). The three deliverable SVGs, README, CLAUDE.md, Dockerfile-adjacent docs, and k8s manifests are all renamed/rewritten from M1730 to E440.
Tech Stack: Python 3 / Flask, fontTools (TTFont, SVGPathPen) for glyph-to-path conversion — both already in web/requirements.txt, no new dependencies.
Global Constraints
- No build, lint, or test tooling exists in this repo (per
CLAUDE.md) — do not add pytest or any test framework. Verification steps in this plan use plainpython3 -c/ heredoc scripts withassert, run directly via Bash. - All canvas/geometry constants below are measured directly from
e440.svg(or computed from measurements) — use these exact values, don't re-derive or approximate differently. - Coordinate convention: all constants are expressed in the pre-layer-translate local frame (the same frame
e440.svg's own elements use), with the SVG output wrapped in<g transform="translate(-60.717741,-107.14126)">. This mirrors the existing M1730 code's convention (ZERO_X,MAX_X, etc. were also pre-translate). - Canvas size is fixed:
width="93.824249mm" height="57.215687mm" viewBox="0 0 93.824249 57.215687". - Colors/strokes: ink
#1a1a1afor ticks/labels/unit/corner text (matches reference), body fill#f9f9f9with#1a1a1astroke at width0.184, pivot guide circle stroke#b3b3b3at width0.184, rim arc stroke#000000at width0.484. - Geometry constants (local frame, mm):
PIVOT_X = 107.42845,PIVOT_Y = 162.62819R0 = 43.842751(rim / inner-tick radius)ANGLE_MIN = -45.667193,ANGLE_MAX = 45.667193(degrees from vertical, clockwise-positive; low value on the left/negative side)MAJOR_LEN = 4.7839203,MAJOR_W = 0.79658329MEDIUM_LEN = 3.1215858,MEDIUM_W = 0.37985462SMALL_LEN = 1.8121802,SMALL_W = 0.27478597LABEL_RADIUS = 49.862UNIT_X = 107.03512,UNIT_Y = 136.64594LEFT_X = 73.432251,LEFT_Y0 = 145.16051RIGHT_X = 142.4485,RIGHT_Y0 = 145.32773CORNER_LINE_SPACING = 4.4586,CORNER_FONT_SIZE = 3.56685PIVOT_CIRCLE_R = 1.7287594
- The
half_height_unitfeature (and its UI toggle) is dropped — it existed only to solve a crowding problem specific to M1730's narrow side panel, which doesn't exist in the E440 layout. - Point-on-circle formula (verified against the reference to within 0.003mm):
x = PIVOT_X + radius * sin(radians(angle_deg)),y = PIVOT_Y - radius * cos(radians(angle_deg)).
Task 1: Clean up fonts directory and register the DIN font labels/default
Files:
- Modify:
fonts/(remove stray Windows metadata files) - Modify:
web/app.py:19-26(_FONT_LABELSdict),web/app.py:51(_DEFAULT_FONT)
Interfaces:
-
Consumes: nothing new —
_load_fonts()(web/app.py:29-47) already globs*.ttfinfonts/and builds_FontDataper file; unchanged. -
Produces:
_DEFAULT_FONTnow resolves to the DIN regular font's key, used by every later task's defaultfont_keyargument. -
Step 1: Remove stray Zone.Identifier files
rm -f "fonts/alte-din-1451-mittelschrift.regular.ttf:Zone.Identifier" \
"fonts/alte-din-1451-mittelschrift.gepraegt.ttf:Zone.Identifier"
ls fonts/
Expected: only .ttf files remain (6 GOST-family + 2 DIN files).
- Step 2: Install
defusedxmlinto the venv for later verification steps
This plan's well-formedness checks (Tasks 2 and 9) parse generated SVGs with defusedxml instead of the stdlib xml.etree.ElementTree, per this repo's security guidance (stdlib XML parsers are vulnerable to XXE/billion-laughs by default). It's not an app.py runtime dependency, just a verification-script tool, so it's installed into the venv but not added to web/requirements.txt.
cd /home/adebaumann/development/44-Scale
source .venv/bin/activate
pip install defusedxml
python3 -c "import defusedxml; print('OK')"
Expected: OK.
- Step 3: Add DIN font display labels and change the default font
In web/app.py, in the _FONT_LABELS dict, add two entries (key = filename stem, i.e. Path('alte-din-1451-mittelschrift.regular.ttf').stem):
_FONT_LABELS = {
'GOST_2.30481_type_A': 'GOST Type A',
'GOST_2.30481_type_A_Bold': 'GOST Type A Bold',
'Gost2.30481TypeB': 'GOST Type B',
'ISOCPEUR': 'ISOCPEUR',
'SIEMENS_GOST_Type_A': 'Siemens GOST Type A',
'TGL_31034-1': 'TGL 31 034',
'alte-din-1451-mittelschrift.regular': 'Alte DIN 1451 Mittelschrift',
'alte-din-1451-mittelschrift.gepraegt': 'Alte DIN 1451 Mittelschrift (Gepraegt)',
}
Change the default font constant:
_DEFAULT_FONT = 'alte-din-1451-mittelschrift.regular'
- Step 4: Verify the font loads and is the default
cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import _FONTS, _DEFAULT_FONT
assert _DEFAULT_FONT in _FONTS, _FONTS.keys()
assert _FONTS[_DEFAULT_FONT].label == 'Alte DIN 1451 Mittelschrift'
print('OK', len(_FONTS), 'fonts loaded')
"
Expected: OK 8 fonts loaded.
- Step 5: Commit
git add fonts/ web/app.py
git commit -m "Add DIN 1451 fonts as default; drop stray Zone.Identifier files"
Task 2: Rewrite generate_svg() skeleton around arc geometry (body + pivot + rim, no ticks/labels yet)
Files:
- Modify:
web/app.py(constants blockweb/app.py:53-77,generate_svg()web/app.py:320-488, route handlerweb/app.py:498-523)
Interfaces:
-
Consumes:
_FONTS,_DEFAULT_FONT,_markup_unit_svg,_text_to_paths,_fmt(all unchanged, from earlier in the file). -
Produces: new
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)signature that later tasks extend. Produces_point_at(radius, angle_deg)helper used by Tasks 3–4. -
Step 1: Replace the linear geometry constants with arc constants
Replace web/app.py:53-77 (from # SVG absolute coordinate constants... through the _UNIT_BOX_* block) with:
# SVG absolute coordinate constants (1 user unit = 1 mm), pre-layer-translate
# local frame — the <g> 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
MAJOR_LEN, MAJOR_W = 4.7839203, 0.79658329
MEDIUM_LEN, MEDIUM_W = 3.1215858, 0.37985462
SMALL_LEN, SMALL_W = 1.8121802, 0.27478597
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"
)
_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)
- Step 2: Simplify
_markup_unit_svgto drop the half-height box-override params
Replace the _markup_unit_svg function (originally web/app.py:248-268) with:
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
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))
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>'
(This drops the box_y0/box_y1 parameters and the module-level _UNIT_BOX_HALF_Y0 constant — search web/app.py for _UNIT_BOX_HALF_Y0 and half_height_unit and delete every reference; they no longer exist anywhere after this task.)
- Step 3: Delete the now-unused
_unit_font_sizefunction
Delete the _unit_font_size function entirely (originally web/app.py:271-275) — after Task 5 the unit is always rendered via _markup_unit_svg, so this fast-path helper becomes dead code.
- Step 4: Replace
generate_svg()with the new skeleton
Replace the whole generate_svg function (originally web/app.py:320-488) with:
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()))
font_face = (
'<defs><style>'
f'@font-face {{font-family:"{fd.label}";'
f'src:url("data:font/truetype;base64,{fd.b64}") format("truetype");}}'
'</style></defs>'
)
parts = [
'<?xml version="1.0" encoding="UTF-8" standalone="no"?>',
'<svg width="93.824249mm" height="57.215687mm"'
' viewBox="0 0 93.824249 57.215687" version="1.1"'
' xmlns="http://www.w3.org/2000/svg">',
]
if use_paths:
parts.append(' <g transform="translate(-60.717741,-107.14126)">')
else:
parts += [font_face, ' <g transform="translate(-60.717741,-107.14126)">']
parts.append(
f' <path style="fill:#f9f9f9;stroke:#1a1a1a;stroke-width:0.184;'
f'stroke-linecap:round;stroke-miterlimit:10" d="{BODY_D}" />'
)
p0 = _point_at(R0, ANGLE_MIN)
pmid = _point_at(R0, 0.0)
p1 = _point_at(R0, ANGLE_MAX)
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}" />'
)
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}" />'
)
parts += [' </g>', '</svg>']
return '\n'.join(parts)
(Ticks, labels, unit, and corner text are added by Tasks 3–6; this step must produce a valid, renderable SVG containing just the case body, rim arc, and pivot guide circle.)
- Step 5: Update the
/generateFlask route to the new parameter set
Replace the /generate route body (originally web/app.py:498-523) with:
@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:
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)
except ValueError as err:
return Response(str(err), status=400, mimetype='text/plain')
return Response(svg, mimetype='image/svg+xml')
- Step 6: Verify the skeleton renders a valid SVG
cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import generate_svg
svg = generate_svg('uA', 0, 50, 'Text\nLeft', 'Text\nRight', 5, 9, 6)
assert svg.startswith('<?xml')
assert 'viewBox=\"0 0 93.824249 57.215687\"' in svg
assert 'translate(-60.717741,-107.14126)' in svg
assert 'cx=\"107.42845\"' in svg
from defusedxml import ElementTree as ET
ET.fromstring(svg)
print('OK, valid XML,', len(svg), 'bytes')
"
Expected: OK, valid XML, ... bytes with no exception. (Requires defusedxml — pip install defusedxml if not already available.)
- Step 7: Commit
git add web/app.py
git commit -m "Rewrite generate_svg skeleton around arc geometry (body/rim/pivot only)"
Task 3: Implement tick generation (3-tier, all 4 scale mappings)
Files:
- Modify:
web/app.py(insidegenerate_svg, after the pivot-circleparts.appendfrom Task 2 Step 4, beforeparts += [' </g>', '</svg>'])
Interfaces:
-
Consumes:
_point_at,R0,ANGLE_MIN,ANGLE_MAX,MAJOR_LEN/W,MEDIUM_LEN/W,SMALL_LEN/Wfrom Task 2. -
Produces:
big_angles(list[float], lengthbig_ticks + 1) andbig_vals(list[float], same length) — consumed by Task 4's label loop. -
Step 1: Add the mapping functions (
value_to_angle/major_value) and tick-line helper
Insert immediately after the fd = _FONTS.get(...) line inside generate_svg:
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_angle(v):
return ANGLE_MIN + (ANGLE_MAX - ANGLE_MIN) * (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_angle(v):
return ANGLE_MIN + (ANGLE_MAX - ANGLE_MIN) * 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_angle(v):
if math.isinf(v):
return ANGLE_MIN
return ANGLE_MIN + (ANGLE_MAX - ANGLE_MIN) * 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_angle(v):
return ANGLE_MIN + (ANGLE_MAX - ANGLE_MIN) * (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_angle(v):
if span == 0:
return ANGLE_MIN
return ANGLE_MIN + (ANGLE_MAX - ANGLE_MIN) * (v - min_val) / span
def major_value(i):
return min_val + span * i / big_ticks
big_interval_angle = (ANGLE_MAX - ANGLE_MIN) / big_ticks
big_angles = [ANGLE_MIN + i * big_interval_angle 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)]
def _tick_line(angle_deg, length, width):
x = PIVOT_X
y1 = PIVOT_Y - R0
y2 = PIVOT_Y - R0 - length
return (
f' <line x1="{x:.5f}" y1="{y1:.5f}" x2="{x:.5f}" y2="{y2:.5f}"'
f' style="stroke:#1a1a1a;stroke-width:{width:.5f};stroke-linecap:butt"'
f' transform="rotate({angle_deg:.5f},{PIVOT_X:.5f},{PIVOT_Y:.5f})" />'
)
- Step 2: Draw the ticks
Insert immediately after the pivot-circle parts.append(...) block (still before parts += [' </g>', '</svg>']):
for angle in big_angles:
parts.append(_tick_line(angle, MAJOR_LEN, MAJOR_W))
if small_ticks > 0:
sub = small_ticks + 1
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]):
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
angle = value_to_angle(v)
length, width = (MEDIUM_LEN, MEDIUM_W) if j == medium_j else (SMALL_LEN, SMALL_W)
parts.append(_tick_line(angle, length, width))
- Step 3: Verify tick angles for the reference case (0–50, 5 major, 9 minor)
cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import generate_svg
svg = generate_svg('uA', 0, 50, '', '', 5, 9, 6)
# major ticks at -45.66719, -27.40032, -9.13344, 9.13344, 27.40032, 45.66719
for a in ['-45.66719', '-27.40032', '-9.13344', '9.13344', '27.40032', '45.66719']:
assert f'rotate({a},' in svg, a
# medium ticks (values 5,15,25,35,45) at the interval midpoints
for a in ['-36.53375', '-18.26688', '0.00000', '18.26688', '36.53375']:
assert f'rotate({a},' in svg, a
assert svg.count('<line') == 6 + 5 + 40 # 6 major + 5 medium + 40 small (8 per interval * 5)
print('OK')
"
Expected: OK with no assertion error.
- Step 4: Commit
git add web/app.py
git commit -m "Implement arc tick generation with 3 tiers across all scale mappings"
Task 4: Implement numeric label generation
Files:
- Modify:
web/app.py(insidegenerate_svg, after the tick-drawing block from Task 3, beforeparts += [' </g>', '</svg>'])
Interfaces:
-
Consumes:
big_angles,big_vals,label_indices,custom_labels,label_size,_point_at,LABEL_RADIUS,_text_to_paths,_fmt— all already available insidegenerate_svg. -
Produces: nothing new consumed by later tasks (labels are a leaf feature).
-
Step 1: Draw the labels
Insert immediately after the tick small-ticks loop from Task 3 Step 2:
lx, ly = PIVOT_X, PIVOT_Y - LABEL_RADIUS
for k, idx in enumerate(label_indices):
angle = big_angles[idx]
label = custom_labels[k] if custom_labels else _fmt(big_vals[idx])
wrap = f'rotate({angle:.5f},{PIVOT_X:.5f},{PIVOT_Y:.5f})'
if use_paths:
parts.append(' ' + _text_to_paths(label, lx, ly, label_size, fd,
anchor='middle', wrap_transform=wrap))
else:
parts.append(
f' <text style="font-size:{label_size:.5f}px;font-family:\'{fd.label}\',monospace;fill:#1a1a1a"'
f' text-anchor="middle" x="{lx:.5f}" y="{ly:.5f}" transform="{wrap}">{escape(label)}</text>'
)
- Step 2: Verify labels for the reference case
cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import generate_svg
svg = generate_svg('uA', 0, 50, '', '', 5, 9, 6, use_paths=False)
for val, angle in [('0','-45.66719'), ('10','-27.40032'), ('20','-9.13344'),
('30','9.13344'), ('40','27.40032'), ('50','45.66719')]:
assert f'rotate({angle},' in svg and f'>{val}</text>' in svg, (val, angle)
print('OK')
svg2 = generate_svg('uA', 0, 50, '', '', 5, 9, 3, custom_labels=['Lo','Mid','Hi'], use_paths=False)
for val in ('Lo', 'Mid', 'Hi'):
assert f'>{val}</text>' in svg2, val
print('OK custom labels')
"
Expected: OK then OK custom labels.
- Step 3: Commit
git add web/app.py
git commit -m "Implement rotated numeric label generation for the arc scale"
Task 5: Implement the unit label
Files:
- Modify:
web/app.py(insidegenerate_svg, after the label loop from Task 4)
Interfaces:
-
Consumes:
_markup_unit_svg(Task 2 Step 2),unit,fd. -
Produces: nothing new consumed later.
-
Step 1: Draw the unit label
Insert immediately after the label loop from Task 4 Step 1:
parts.append(' ' + _markup_unit_svg(unit, fd))
- Step 2: Verify plain and markup units render
cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import generate_svg
svg = generate_svg('uA', 0, 50, '', '', 5, 9, 6)
assert svg.count('<path') >= 2 # body path + rim path + at least glyph paths
svg2 = generate_svg('cm^2', 0, 50, '', '', 5, 9, 6)
assert svg2 != svg
print('OK')
"
Expected: OK.
- Step 3: Commit
git add web/app.py
git commit -m "Wire up unit label rendering via markup-aware helper"
Task 6: Implement left/right corner labels; fix _text_to_paths end-anchor support
Files:
- Modify:
web/app.py(_text_to_pathsfunction, originallyweb/app.py:278-305; and insidegenerate_svg, after the unit label from Task 5)
Interfaces:
-
Consumes:
left_label,right_label(strings, already threaded through the function signature and route since Task 2),LEFT_X/Y0,RIGHT_X/Y0,CORNER_LINE_SPACING,CORNER_FONT_SIZE. -
Produces: nothing new — this is the last content block before
</g></svg>. -
Step 1: Add
endanchor support to_text_to_paths
_text_to_paths currently only handles anchor == 'middle' for width-based offset (start is the implicit default with no offset). Right-anchored corner text needs a third case. Replace the anchor-handling block at the top of _text_to_paths:
if anchor == 'middle':
total_w = sum(
fd.hmtx.get(fd.cmap.get(ord(ch)), (0, 0))[0] * s
for ch in text if fd.cmap.get(ord(ch))
)
x -= total_w / 2
with:
if anchor in ('middle', 'end'):
total_w = sum(
fd.hmtx.get(fd.cmap.get(ord(ch)), (0, 0))[0] * s
for ch in text if fd.cmap.get(ord(ch))
)
x -= total_w / 2 if anchor == 'middle' else total_w
- Step 2: Draw the corner labels
Insert immediately after the unit-label line from Task 5 Step 1:
left_lines = left_label.split('\n')[:2]
right_lines = right_label.split('\n')[:2]
for i, line in enumerate(left_lines):
if not line:
continue
y = LEFT_Y0 + i * CORNER_LINE_SPACING
if use_paths:
parts.append(' ' + _text_to_paths(line, LEFT_X, y, CORNER_FONT_SIZE, fd, anchor='start'))
else:
parts.append(
f' <text style="font-size:{CORNER_FONT_SIZE:.5f}px;font-family:\'{fd.label}\',monospace;fill:#1a1a1a"'
f' x="{LEFT_X:.5f}" y="{y:.5f}">{escape(line)}</text>'
)
for i, line in enumerate(right_lines):
if not line:
continue
y = RIGHT_Y0 + i * CORNER_LINE_SPACING
if use_paths:
parts.append(' ' + _text_to_paths(line, RIGHT_X, y, CORNER_FONT_SIZE, fd, anchor='end'))
else:
parts.append(
f' <text style="font-size:{CORNER_FONT_SIZE:.5f}px;font-family:\'{fd.label}\',monospace;fill:#1a1a1a"'
f' text-anchor="end" x="{RIGHT_X:.5f}" y="{y:.5f}">{escape(line)}</text>'
)
- Step 3: Verify corner labels (2-line, start/end anchored) render correctly
cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import generate_svg
svg = generate_svg('uA', 0, 50, 'Text\nLeft', 'Text\nRight', 5, 9, 6, use_paths=False)
assert '>Text</text>' in svg and '>Left</text>' in svg and '>Right</text>' in svg
assert 'text-anchor=\"end\"' in svg
svg_one_line = generate_svg('uA', 0, 50, 'OnlyLine', '', 5, 9, 6, use_paths=False)
assert '>OnlyLine</text>' in svg_one_line
print('OK')
"
Expected: OK.
- Step 4: Commit
git add web/app.py
git commit -m "Implement left/right corner labels; add end-anchor support to text-to-paths"
Task 7: Update the web form (web/templates/index.html)
Files:
- Modify:
web/templates/index.html
Interfaces:
-
Consumes: the
/generateroute's new form fields (left_label,right_label) from Task 2 Step 5. -
Produces: nothing consumed by later tasks — this is the UI leaf.
-
Step 1: Update page title and header wordmark
Replace:
<title>M1730 Scale Generator — Baumann Works</title>
with:
<title>E440 Scale Generator — Baumann Works</title>
Replace:
<span class="wordmark-sub">M1730 Scale Generator</span>
with:
<span class="wordmark-sub">E440 Scale Generator</span>
- Step 2: Replace the Unit/Range-label row with Unit/Left-label/Right-label, dropping the half-height toggle
Replace the entire Labels section block:
<div class="section">
<div class="section-eye">Labels</div>
<div class="row row-2">
<div class="field">
<label>Unit</label>
<input name="unit" type="text" value="%" required>
<span style="font-family:var(--f-mono);font-size:0.65rem;color:var(--muted)">Markup: cm^2 · H_2O · {1/2} fraction · nests, e.g. {m/s^2}</span>
<div style="display:flex;align-items:center;gap:0.5rem;margin-top:0.2rem">
<div class="mode-toggle" role="group" aria-label="Unit height">
<button type="button" class="mode-btn active" data-unit-height="normal">Full height</button>
<button type="button" class="mode-btn" data-unit-height="half"
title="Restrict unit label to the upper half of the panel — use when numeric labels visually crowd the unit">Half height</button>
</div>
<input type="hidden" name="half_height_unit" id="half-height-unit" value="0">
</div>
</div>
<div class="field">
<label>Range label</label>
<input name="range_label" type="text" value="5mA" required>
</div>
</div>
</div>
with:
<div class="section">
<div class="section-eye">Labels</div>
<div class="row row-2">
<div class="field">
<label>Unit</label>
<input name="unit" type="text" value="uA" required>
<span style="font-family:var(--f-mono);font-size:0.65rem;color:var(--muted)">Markup: cm^2 · H_2O · {1/2} fraction · nests, e.g. {m/s^2}</span>
</div>
</div>
<div class="row row-2" style="margin-top:1.25rem">
<div class="field">
<label>Left label (up to 2 lines)</label>
<textarea name="left_label" rows="2" style="background:var(--white);border:1px solid var(--rule);border-radius:1px;padding:0.5rem 0.7rem;font-family:var(--f-body);font-size:0.925rem;color:var(--ink);width:100%;outline:none;resize:vertical">Text
Left</textarea>
</div>
<div class="field">
<label>Right label (up to 2 lines)</label>
<textarea name="right_label" rows="2" style="background:var(--white);border:1px solid var(--rule);border-radius:1px;padding:0.5rem 0.7rem;font-family:var(--f-body);font-size:0.925rem;color:var(--ink);width:100%;outline:none;resize:vertical">Text
Right</textarea>
</div>
</div>
</div>
- Step 3: Update the default Label size value to match the new reference default
Replace:
<input name="label_size" type="number" step="any" min="0.5" max="25" value="9" required>
with:
<input name="label_size" type="number" step="any" min="0.5" max="25" value="5.1126" required>
- Step 4: Remove the now-dead unit-height JS handler and download filename
Delete this block from the <script>:
document.querySelectorAll('[data-unit-height]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('[data-unit-height]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.getElementById('half-height-unit').value = btn.dataset.unitHeight === 'half' ? '1' : '0';
doGenerate();
});
});
Replace:
download: 'M1730-Scale.svg',
with:
download: 'E440-Scale.svg',
- Step 5: Update the source link and version tag
Replace:
<span class="version-tag">v{{ version }} — <a href="https://git.baumann.gr/adebaumann/M1730-Scale" target="_blank" rel="noopener">Source</a></span>
with:
<span class="version-tag">v{{ version }} — <a href="https://git.baumann.gr/adebaumann/E440-Scale" target="_blank" rel="noopener">Source</a></span>
- Step 6: Run the app locally and verify the form generates a scale
cd /home/adebaumann/development/44-Scale
source .venv/bin/activate 2>/dev/null || true
(cd web && python app.py &)
sleep 2
curl -s -X POST http://localhost:5000/generate \
-F unit=uA -F min=0 -F max=50 -F left_label=$'Text\nLeft' -F right_label=$'Text\nRight' \
-F big_ticks=5 -F small_ticks=9 -F label_count=6 -F output_mode=paths -F scale_type=linear \
-F label_size=5.1126 -F font=alte-din-1451-mittelschrift.regular \
| head -c 300
echo
pkill -f "python app.py" || true
Expected: response starts with <?xml version="1.0"... and contains no error text. Then also open http://localhost:5000/ in a browser (or note that a headless check isn't sufficient) to visually confirm the form renders and the light-table preview shows an arc dial, not a strip.
- Step 7: Commit
git add web/templates/index.html
git commit -m "Update web form for arc-dial fields: unit, left/right labels"
Task 8: Manual visual verification against the reference
Files: none modified — verification only.
Interfaces:
-
Consumes: the fully working generator from Tasks 1–7.
-
Step 1: Generate the reference-matching scale and save it
cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import generate_svg
svg = generate_svg('uA', 0, 50, 'Text\nLeft', 'Text\nRight', 5, 9, 6,
use_paths=False, font_key='alte-din-1451-mittelschrift.regular')
open('/tmp/e440-generated-check.svg', 'w').write(svg)
print('written')
"
- Step 2: Open both files in a browser or Inkscape and compare side-by-side
xdg-open /home/adebaumann/development/44-Scale/e440.svg 2>/dev/null || true
xdg-open /tmp/e440-generated-check.svg 2>/dev/null || true
Check specifically:
- Tick marks form a symmetric fan matching the reference's angular spread (not compressed/stretched/mirrored).
- The rim arc (
<path>with stroke#000000) follows the same curve as the ticks' base, with correct orientation (not flipped inside-out — if the arc bulges the wrong way, its sweep-flag in Task 2 Step 4 needs flipping from0 0 1to0 0 0). - Numbers 0–50 sit just outside the tick tips, tilted to match their tick's angle, same reading direction as the reference (not upside-down).
uAsits centered above the arc;Text/LeftandText/Rightsit in the bottom corners matching the reference's placeholder positions.
If the rim arc orientation is wrong, fix it now in web/app.py (Task 2 Step 4's sweep-flag) and re-run Step 1.
- Step 3: No commit for this task (verification only; any fixes found get their own commit against the task that introduced the bug).
Task 9: Produce the three deliverable SVG files; remove the old M1730 files
Files:
- Create:
E440-Scale_template.svg(renamed/tidied frome440.svg) - Create:
E440-Scale_paths.svg - Create:
E440-Scale_text.svg - Delete:
e440.svg,M1730-Scale_template.svg,M1730-Scale_paths.svg,M1730-Scale_text.svg
Interfaces:
-
Consumes:
generate_svg()from Tasks 1–6, run with the reference parameters (unituA, 0–50, 5 major/9 minor/6 labels, leftText\nLeft, rightText\nRight, fontalte-din-1451-mittelschrift.regular). -
Step 1: Create the template file
cd /home/adebaumann/development/44-Scale
git mv e440.svg E440-Scale_template.svg
Edit E440-Scale_template.svg: change sodipodi:docname="e440.svg" to sodipodi:docname="E440-Scale.svg".
- Step 2: Generate the paths (outline) variant
cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import generate_svg
svg = generate_svg('uA', 0, 50, 'Text\nLeft', 'Text\nRight', 5, 9, 6,
use_paths=True, font_key='alte-din-1451-mittelschrift.regular')
open('E440-Scale_paths.svg', 'w').write(svg + '\n')
"
- Step 3: Generate the text (embedded-font) variant
cd /home/adebaumann/development/44-Scale
python3 -c "
import sys; sys.path.insert(0, 'web')
from app import generate_svg
svg = generate_svg('uA', 0, 50, 'Text\nLeft', 'Text\nRight', 5, 9, 6,
use_paths=False, font_key='alte-din-1451-mittelschrift.regular')
open('E440-Scale_text.svg', 'w').write(svg + '\n')
"
- Step 4: Delete the old M1730 files
cd /home/adebaumann/development/44-Scale
git rm M1730-Scale_template.svg M1730-Scale_paths.svg M1730-Scale_text.svg
- Step 5: Verify all three new files are well-formed XML
cd /home/adebaumann/development/44-Scale
python3 -c "
from defusedxml.ElementTree import parse
for f in ('E440-Scale_template.svg', 'E440-Scale_paths.svg', 'E440-Scale_text.svg'):
parse(f)
print(f, 'OK')
"
Expected: OK printed for all three files. (Requires defusedxml — pip install defusedxml if it's not already available; stdlib XML parsers are avoided here per repo security guidance since they're vulnerable to XXE/billion-laughs by default, even though these particular files are locally generated and trusted.)
- Step 6: Commit
git add E440-Scale_template.svg E440-Scale_paths.svg E440-Scale_text.svg
git commit -m "Replace M1730 deliverable SVGs with generated E440 template/paths/text files"
Task 10: Rebrand infra and docs (Dockerfile check, k8s, README.md, CLAUDE.md, VERSION bump)
Files:
- Modify:
k8s/deployment.yaml,k8s/ingress.yaml,k8s/service.yaml - Modify:
README.md,CLAUDE.md - Modify:
web/app.py:12(VERSION)
Interfaces: none — this is the final documentation/infra pass.
- Step 1: Bump the app version
In web/app.py, change:
VERSION = "0.87"
to:
VERSION = "1.0"
- Step 2: Rename k8s resources from m1730 to e440
In k8s/deployment.yaml, replace every occurrence of m1730-generator with e440-generator, and the image tag:
image: git.baumann.gr/adebaumann/m1730-generator:0.87
with:
image: git.baumann.gr/adebaumann/e440-generator:1.0
In k8s/ingress.yaml, replace every occurrence of m1730-strip-prefix with e440-strip-prefix, m1730-generator with e440-generator, and /M1730-generator with /E440-generator.
In k8s/service.yaml, replace every occurrence of m1730-generator with e440-generator.
- Step 3: Verify no
m1730/M1730strings remain in k8s manifests
cd /home/adebaumann/development/44-Scale
grep -ri m1730 k8s/ && echo "FOUND STALE REFS" || echo "clean"
Expected: clean.
- Step 4: Rewrite
README.md
Replace the full contents of README.md with:
# E440-Scale
SVG scale artwork and web generator for E440-style round-dial analog panel
meters. Scales are typeset in **Alte DIN 1451 Mittelschrift**.
## Repository layout
E440-Scale_template.svg Inkscape working file (case body, tick marks, labels) E440-Scale_paths.svg Paths-only export — text converted to outlines E440-Scale_text.svg Live-text export — embedded font, editable labels fonts/ TTF fonts (required for Docker build) web/ Flask web generator k8s/ Kubernetes manifests Dockerfile Container build
## SVG files
The canvas is **93.824249 × 57.215687 mm**. The coordinate origin is
offset: layer 1 carries `transform="translate(-60.717741,-107.14126)"`, so
absolute SVG coordinates begin around x=61, y=107.
The dial's case geometry — body outline, mounting ears, pivot point
(local `107.42845, 162.62819`), rim radius (`43.842751` mm), and angular
sweep (`±45.667°` from vertical) — is fixed across every generated scale.
Only the tick marks, numeric labels, unit label, and corner text vary.
| File | Purpose |
|---|---|
| `E440-Scale_template.svg` | Master Inkscape file. Edit this one. |
| `E440-Scale_paths.svg` | All text converted to outlines. Use for cutting plotters, laser engravers, or any workflow where fonts must not be embedded as live text. |
| `E440-Scale_text.svg` | Live `<text>` nodes with the font embedded as base64. Smaller file; requires an SVG viewer that supports embedded fonts. |
Open files in **Inkscape** (developed with v1.4.3). There is no build step
— edit the SVG directly or use the web generator.
## Web generator
A Flask app that generates arc-dial scale SVGs from a form. Supports two
output modes switchable in the UI:
- **Paths** — glyphs rendered as outlines via fontTools; opens cleanly in
Inkscape, Illustrator, and cutting software
- **Text** — live `<text>` elements with the selected font embedded as
base64
### Run locally
```bash
source .venv/bin/activate
cd web && python app.py # http://localhost:5000
Fonts are loaded at startup from the fonts/ directory in the repo root.
Form parameters
| Field | Description |
|---|---|
| Unit | Symbol shown centered above the arc (e.g. uA, V, %). Supports inline markup — see below |
| Left label / Right label | Up to 2 lines each, placed in the bottom-left/bottom-right corners of the dial |
| Mapping | Linear, √ (square root), Log, or 1/x (reciprocal). Major ticks are always evenly spaced angularly; the mapping controls their values and how minor ticks are placed within each interval. |
| Min / Max | Numeric range of the scale |
| Major intervals | Number of major tick intervals |
| Minor per interval | Minor ticks between each pair of major ticks. When odd, the exact midpoint subdivision is drawn as a taller "medium" tick, matching the reference dial's 3-tier tick scheme |
| Labels | Number of numeric labels; Labels − 1 should divide evenly into Major intervals |
| Custom labels | Optional comma-separated labels that override the calculated numbers |
| Label size (mm) | Font size of the numeric labels, in millimetres (default 5.1126) |
Scale mappings
| 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 |
| 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 ∞ |
Unit markup
The Unit field accepts a lightweight shorthand (no LaTeX engine — everything is drawn as glyph paths and auto-sized to fit above the arc):
| Markup | Result |
|---|---|
cm^2 |
superscript (^{...} for multiple characters) |
H_2O |
subscript (_{...} for multiple characters) |
{1/2} |
stacked fraction with a horizontal rule; a bare / stays inline |
Constructs nest: {m/s^2} is a fraction with a superscript in the
denominator, x^{1/2} a superscripted fraction.
Container
docker build -t e440-scale:latest .
docker run -p 5000:5000 e440-scale:latest
Fonts are loaded from fonts/ in the repo root; they are bundled into the
image at build time.
The production image is at git.baumann.gr/adebaumann/e440-generator.
Kubernetes
Manifests in k8s/ deploy to the works namespace with a Traefik v2
IngressRoute at works.baumann.gr/E440-generator.
# Managed by ArgoCD — push to git to deploy
git push
Resources: namespace, deployment (1 replica, gunicorn), ClusterIP service, Traefik IngressRoute + stripPrefix middleware.
License
GPL-3.0 — see LICENSE.
- [ ] **Step 5: Rewrite `CLAUDE.md`**
Replace the full contents of `CLAUDE.md` with:
```markdown
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
SVG scale artwork for E440-style round-dial analog meters. The scales are designed in Inkscape (v1.4.3) using **Alte DIN 1451 Mittelschrift**, a German technical drafting standard typeface.
The canvas is **93.824249 mm × 57.215687 mm**. The coordinate origin is offset: `transform="translate(-60.717741,-107.14126)"` is applied to `layer1` in all files, so absolute SVG coordinates start around x=61, y=107 rather than 0,0.
## File Roles
The three SVG files represent layers of the same design, intended to be combined or used separately:
- **`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.
## Key Design Parameters
- Stroke colors: `#1a1a1a` (dark near-black) for tick marks, numeric/unit/corner text; fill `#f9f9f9` for the case body; `#000000` for the rim arc stroke; `#b3b3b3` for the pivot guide circle.
- Fixed case geometry (identical across every generated scale — only tick/label/text content varies):
- Pivot point (local, pre-translate frame): `(107.42845, 162.62819)`
- Rim / inner-tick radius: `43.842751` mm
- Angular sweep: `±45.667193°` from vertical, clockwise-positive (low value on the left)
- Tick radial lengths: major `4.7839203` mm, medium `3.1215858` mm, small `1.8121802` mm
- Tick stroke widths: major `0.79658329`, medium `0.37985462`, small `0.27478597`
- Label radius: `49.862` mm
- Tick tiers: major ticks at each labeled interval boundary; a medium tick appears only when the minor-tick count per interval is odd, at the exact midpoint subdivision; all other subdivisions render as small ticks.
- Numeric labels rotate to follow the arc (radial orientation), matching their tick's angle.
## Web Generator (`web/`)
A Flask app that generates scale SVGs from a form. The venv lives at the repo root.
source .venv/bin/activate cd web && python app.py # runs on http://localhost:5000
`app.py` has one route (`GET /`) serving the form and one (`POST /generate`) that returns SVG. All SVG geometry is computed in `generate_svg()` using the same coordinate constants as the hand-drawn files: ticks and labels are positioned "as if angle=0" (straight up from the pivot) and given an SVG `rotate(angle, PIVOT_X, PIVOT_Y)` transform. The form exposes: unit label, min/max values, scale mapping, major tick intervals, minor ticks per interval, number of numeric labels, custom labels, label size, font, output mode, and left/right corner labels (up to 2 lines each).
Fonts are loaded at startup from every `.ttf` in `fonts/` and embedded as base64 in every generated SVG (text output mode) so output files are self-contained. The default font is `alte-din-1451-mittelschrift.regular`.
## Container & Kubernetes
docker build -t e440-scale:latest .
`fonts/` must contain the required TTF files before building.
kubectl apply -f k8s/
`k8s/ingress.yaml` adds a Traefik `IngressRoute` for `works.baumann.gr/E440-generator` and a `stripPrefix` middleware. Adjust `certResolver` in the file if your Traefik instance uses a different name.
## Editing SVG files directly
Open files in **Inkscape**. There is no build, lint, or test tooling — changes are made directly to the SVG XML or via Inkscape's GUI. When modifying tick positions or labels, keep all three files consistent.
- Step 6: Verify the Docker build still works with the renamed image tag
cd /home/adebaumann/development/44-Scale
docker build -t e440-scale:latest . 2>&1 | tail -20
Expected: build completes successfully (Successfully tagged e440-scale:latest or equivalent final layer message). If Docker isn't available in this environment, skip this step and note it in the final report instead of claiming it was verified.
- Step 7: Commit
git add web/app.py k8s/ README.md CLAUDE.md
git commit -m "Rebrand infra and docs from M1730 to E440; bump version to 1.0"
Final check
- Run
grep -ril m1730 --include='*.py' --include='*.html' --include='*.yaml' --include='*.md' .from the repo root and confirm no output (aside from this plan/spec doc's historical references, which are expected to mention M1730 as the prior state). - Confirm
git statusshows a clean tree with all changes committed. - Confirm the three deliverable SVGs (
E440-Scale_template.svg,E440-Scale_paths.svg,E440-Scale_text.svg) exist and the three oldM1730-Scale_*.svgfiles are gone.