diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3e4ccc2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.venv/ +.git/ +k8s/ +web/__pycache__/ +__pycache__/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..77ac754 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.venv/ +__pycache__/ +*.pyc diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..be8d739 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,55 @@ +# 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 M1730 analog meters. The scales are designed in Inkscape (v1.4.3) using the **ГОСТ тип А** (GOST type A) font, a Russian technical drafting standard typeface. + +All canvas dimensions are **157.18373 mm × 26.683672 mm**. The coordinate origin is offset: `transform="translate(-37.352226,-90.454928)"` is applied to `layer1` in all files, so absolute SVG coordinates start around x=37, y=90 rather than 0,0. + +## File Roles + +The three SVG files represent layers of the same design, intended to be combined or used separately: + +- **`M1730-Scale_template.svg`** — Inkscape working template. Contains the background rect+cutout path, the zero tick mark, an end tick mark, the "U" axis label, numeric labels `0` and `X` (placeholder for the full-scale value), and a `5mA` range label. Also contains a `` inside `` used as a text flow shape (`shape-inside:url(#rect5)`) for the title text block (`id="text5"`). +- **`M1730-Scale_paths.svg`** — Paths-only version: all tick marks as `` elements, no text nodes (or text converted to paths). Used for cutting/printing workflows where fonts must not be embedded as live text. +- **`M1730-Scale_text.svg`** — Text-overlay version: tick mark paths plus live `` elements for numeric labels. The `sodipodi:docname` is `M1730-Scale.svg`, suggesting this is the "final" named file. + +## Key Design Parameters + +- Stroke colors: `#1a1a1a` (dark near-black) for all scale lines and text; fill `#f9f9f9` for the background panel, `#ffffff` for tick mark strokes. +- Major tick stroke-width: `0.555`; border stroke-width: `0.184`. +- Scale bar (the colored strip below tick marks): `y` range 107.547–113.746 mm absolute, `x` range 58.444–183.644. +- Zero tick at absolute x=60.844; full-scale tick at x=180.251. + +## 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. The form exposes: unit label, min/max values, major tick intervals, minor ticks per interval, number of numeric labels, and range label. + +The ГОСТ тип А font is loaded at startup from `/usr/local/share/fonts/г/ГОСТ_тип_А.ttf` and embedded as base64 in every generated SVG so output files are self-contained. + +## Container & Kubernetes + +``` +docker build -t m1730-scale:latest . +``` + +`fonts/ГОСТ_тип_А.ttf` must exist in the repo root before building (copy from `/usr/local/share/fonts/г/`). The image runs gunicorn on port 5000. + +``` +kubectl apply -f k8s/ +``` + +NPM should proxy `https://works.adebaumann.com/M1730-generator/` → `http://m1730-generator/` **with the prefix stripped** (trailing slash on `proxy_pass`). The frontend uses a relative `fetch('generate', ...)` URL so it works correctly under any subpath. + +## 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. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ac2be13 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY web/requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt gunicorn + +COPY fonts/ГОСТ_тип_А.ttf /usr/local/share/fonts/г/ГОСТ_тип_А.ttf + +COPY web/ . + +EXPOSE 5000 + +CMD ["gunicorn", "-w", "2", "-b", "0.0.0.0:5000", "app:app"] diff --git a/M1730-Scale_paths.svg b/M1730-Scale_paths.svg new file mode 100644 index 0000000..af46df6 --- /dev/null +++ b/M1730-Scale_paths.svg @@ -0,0 +1,292 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/M1730-Scale_template.svg b/M1730-Scale_template.svg new file mode 100644 index 0000000..789fb18 --- /dev/null +++ b/M1730-Scale_template.svg @@ -0,0 +1,112 @@ + + + + + + + + + + + + U + 0 + X + + 5mA + + + diff --git a/M1730-Scale_text.svg b/M1730-Scale_text.svg new file mode 100644 index 0000000..8f6863d --- /dev/null +++ b/M1730-Scale_text.svg @@ -0,0 +1,340 @@ + + + + + + + + + + % + 0 + 20 + 40 + 60 + 80 + 100 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 5mA + + diff --git a/fonts/ГОСТ_тип_А.ttf b/fonts/ГОСТ_тип_А.ttf new file mode 100644 index 0000000..aaa7819 Binary files /dev/null and b/fonts/ГОСТ_тип_А.ttf differ diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml new file mode 100644 index 0000000..e14341b --- /dev/null +++ b/k8s/deployment.yaml @@ -0,0 +1,28 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: m1730-generator + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: m1730-generator + template: + metadata: + labels: + app: m1730-generator + spec: + containers: + - name: m1730-generator + image: git.baumann.gr/adebaumann/m1730-generator:0.1 + imagePullPolicy: Always + ports: + - containerPort: 5000 + resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "256Mi" + cpu: "200m" diff --git a/k8s/service.yaml b/k8s/service.yaml new file mode 100644 index 0000000..7ae3623 --- /dev/null +++ b/k8s/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: m1730-generator + namespace: default +spec: + selector: + app: m1730-generator + ports: + - port: 80 + targetPort: 5000 + type: ClusterIP diff --git a/web/app.py b/web/app.py new file mode 100644 index 0000000..6b719b8 --- /dev/null +++ b/web/app.py @@ -0,0 +1,117 @@ +import base64 +import pathlib +from flask import Flask, render_template, request, Response +from html import escape + +app = Flask(__name__) + +_FONT_PATH = pathlib.Path('/usr/local/share/fonts/г/ГОСТ_тип_А.ttf') +_FONT_B64 = base64.b64encode(_FONT_PATH.read_bytes()).decode() +_FONT_FACE = ( + '' +) + +# SVG absolute coordinate constants (1 user unit = 1 mm) +ZERO_X = 60.844130 # x of the zero/left tick +MAX_X = 180.251230 # x of the full-scale/right tick +BAR_Y = 107.54670 # y at the top of the scale bar (tick baseline) +SCALE_LEN = MAX_X - ZERO_X # 119.4071 mm + +BIG_H = 5.0895 # major tick height +SMALL_H = 2.6683 # minor tick height + + +def _fmt(v: float) -> str: + if v == int(v): + return str(int(v)) + return f"{v:.4g}" + + +def generate_svg(unit, min_val, max_val, range_label, big_ticks, small_ticks, label_count): + big_interval = SCALE_LEN / big_ticks + big_xs = [ZERO_X + i * big_interval for i in range(big_ticks + 1)] + + label_count = max(2, min(label_count, big_ticks + 1)) + label_indices = [round(k * big_ticks / (label_count - 1)) for k in range(label_count)] + + parts = [ + '', + '', + _FONT_FACE, + ' ', + + # Background rect with scale-bar cutout + ' ', + + # Unit label (large letter in left panel) + f' {escape(unit)}', + + # Range label inside the strip on the right + f' {escape(range_label)}', + ] + + # Major ticks + for x in big_xs: + parts.append( + f' ' + ) + + # Minor ticks + if small_ticks > 0: + sub = small_ticks + 1 + for i in range(big_ticks): + for j in range(1, small_ticks + 1): + x = big_xs[i] + j * big_interval / sub + parts.append( + f' ' + ) + + # Numeric labels (centered on their tick, above the tick area) + for idx in label_indices: + x = big_xs[idx] + val = min_val + (max_val - min_val) * idx / big_ticks + parts.append( + f' {escape(_fmt(val))}' + ) + + parts += [' ', ''] + return '\n'.join(parts) + + +@app.route('/') +def index(): + return render_template('index.html') + + +@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)) + range_label = request.form.get('range_label', '5mA') + 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))) + + svg = generate_svg(unit, min_val, max_val, range_label, big_ticks, small_ticks, label_count) + return Response(svg, mimetype='image/svg+xml') + + +if __name__ == '__main__': + app.run(debug=True) diff --git a/web/requirements.txt b/web/requirements.txt new file mode 100644 index 0000000..e4a286c --- /dev/null +++ b/web/requirements.txt @@ -0,0 +1,2 @@ +flask +gunicorn diff --git a/web/templates/index.html b/web/templates/index.html new file mode 100644 index 0000000..99ddbbf --- /dev/null +++ b/web/templates/index.html @@ -0,0 +1,213 @@ + + + + + + M1730 Scale Generator + + + +
+

M1730 Scale Generator

+ +
+
+ + + + + + + + + + + + + + + +
+ + + +
+ +
+
+ +
+
Preview
+
+

Fill in the form and click Generate.

+
+
+
+ + + +