Files
mimaki/hpgl_svg.py
Lukas Cremer cd846577a4 add webui
2026-02-03 22:41:29 +01:00

128 lines
4.9 KiB
Python

"""
Convert a Program to SVG for browser display.
No tkinter dependency; used by the web UI.
"""
from __future__ import division
# Plotter units to mm (HP-GL typical: 1 unit = 0.025 mm)
UNITS_PER_MM = 40.0
# Space for X and Y rulers (labels in plotter units)
RULER_LEFT = 52
RULER_BOTTOM = 36
MARGIN_TOP = 16
MARGIN_RIGHT = 16
def program_to_svg(program, width=800, height=600, margin=20, show_scale_bar=True):
"""
Render Program to SVG string. Fits content into width x height.
Always draws X and Y rulers showing bounds (xmin, xmax, ymin, ymax) in plotter units.
HPGL Y is flipped so it displays top-down in SVG.
"""
if not program.commands:
return _empty_svg(width, height)
w, h = program.winsize
if w == 0 or h == 0:
return _empty_svg(width, height)
xmin, ymin = program.xmin, program.ymin
xmax, ymax = program.xmax, program.ymax
vw = width - RULER_LEFT - MARGIN_RIGHT
vh = height - MARGIN_TOP - RULER_BOTTOM
scale = min(vw / w, vh / h)
cx, cy = program.center
ox = RULER_LEFT + vw / 2 - cx * scale
oy = MARGIN_TOP + vh / 2 + cy * scale # flip Y: HPGL y up -> SVG y down
def tx(x):
return x * scale + ox
def ty(y):
return -y * scale + oy
path_segments = []
circles = []
x, y = None, None
for cmd in program.commands:
if cmd.name == 'PU':
x, y = cmd.x, cmd.y
elif cmd.name == 'PD':
if x is not None and y is not None:
path_segments.append('M {:.2f} {:.2f} L {:.2f} {:.2f}'.format(
tx(x), ty(y), tx(cmd.x), ty(cmd.y)))
x, y = cmd.x, cmd.y
elif cmd.name == 'CI' and cmd.args:
r = abs(cmd.args[0]) * scale
cx_svg = tx(x) if x is not None else (RULER_LEFT + vw / 2)
cy_svg = ty(y) if y is not None else (MARGIN_TOP + vh / 2)
circles.append(
'<circle cx="{:.2f}" cy="{:.2f}" r="{:.2f}" fill="none" stroke="currentColor" stroke-width="1"/>'
.format(cx_svg, cy_svg, r))
parts = []
if path_segments:
path_d = ' '.join(path_segments)
parts.append('<path d="{}" fill="none" stroke="currentColor" stroke-width="1"/>'.format(path_d))
parts.extend(circles)
# X and Y rulers showing bounds (always)
parts.append(_rulers_svg(width, height, xmin, ymin, xmax, ymax, tx, ty))
return (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<svg xmlns="http://www.w3.org/2000/svg" '
'width="{w}" height="{h}" viewBox="0 0 {w} {h}">\n'
'<g>\n{body}\n</g>\n</svg>'
).format(w=width, h=height, body='\n'.join(parts))
def _rulers_svg(width, height, xmin, ymin, xmax, ymax, tx, ty):
"""Draw X and Y rulers with bounds labels (plotter units)."""
# X ruler: bottom, from xmin to xmax
y_xruler = height - RULER_BOTTOM + 8
x0_x = tx(xmin)
x1_x = tx(xmax)
tick_y0 = y_xruler
tick_y1 = y_xruler + 6
# Y ruler: left, from ymin (bottom in SVG) to ymax (top in SVG)
x_yruler = RULER_LEFT - 24
y0_y = ty(ymin) # bottom of drawing in SVG
y1_y = ty(ymax) # top of drawing in SVG
tick_x0 = x_yruler + 18
tick_x1 = x_yruler + 24
font = 'font-size="11" font-family="system-ui,sans-serif" fill="#374151"'
return (
'<g stroke="#6b7280" fill="#374151" stroke-width="1">'
# X axis line and ticks
'<line x1="{x0_x:.1f}" y1="{y_xruler:.1f}" x2="{x1_x:.1f}" y2="{y_xruler:.1f}"/>'
'<line x1="{x0_x:.1f}" y1="{tick_y0:.1f}" x2="{x0_x:.1f}" y2="{tick_y1:.1f}"/>'
'<line x1="{x1_x:.1f}" y1="{tick_y0:.1f}" x2="{x1_x:.1f}" y2="{tick_y1:.1f}"/>'
'<text x="{x0_x:.1f}" y="{label_y:.1f}" text-anchor="middle" {font}>{xmin:.0f}</text>'
'<text x="{x1_x:.1f}" y="{label_y:.1f}" text-anchor="middle" {font}>{xmax:.0f}</text>'
# Y axis line and ticks
'<line x1="{x_yruler:.1f}" y1="{y0_y:.1f}" x2="{x_yruler:.1f}" y2="{y1_y:.1f}"/>'
'<line x1="{tick_x0:.1f}" y1="{y0_y:.1f}" x2="{tick_x1:.1f}" y2="{y0_y:.1f}"/>'
'<line x1="{tick_x0:.1f}" y1="{y1_y:.1f}" x2="{tick_x1:.1f}" y2="{y1_y:.1f}"/>'
'<text x="{y_label_x:.1f}" y="{y0_y:.1f}" text-anchor="end" dominant-baseline="middle" {font}>{ymin:.0f}</text>'
'<text x="{y_label_x:.1f}" y="{y1_y:.1f}" text-anchor="end" dominant-baseline="middle" {font}>{ymax:.0f}</text>'
'</g>'
).format(
x0_x=x0_x, x1_x=x1_x, y_xruler=y_xruler, tick_y0=tick_y0, tick_y1=tick_y1,
label_y=y_xruler + 20, xmin=xmin, xmax=xmax,
x_yruler=x_yruler, y0_y=y0_y, y1_y=y1_y, tick_x0=tick_x0, tick_x1=tick_x1,
y_label_x=x_yruler + 14, ymin=ymin, ymax=ymax,
font=font
)
def _empty_svg(width, height):
return (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}">\n'
'<text x="{x}" y="{y}" fill="#999">No content</text>\n</svg>'
).format(w=width, h=height, x=width // 2 - 30, y=height // 2)