"""
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(
''
.format(cx_svg, cy_svg, r))
parts = []
if path_segments:
path_d = ' '.join(path_segments)
parts.append(''.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 (
'\n'
''
).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 (
''
# X axis line and ticks
''
''
''
'{xmin:.0f}'
'{xmax:.0f}'
# Y axis line and ticks
''
''
''
'{ymin:.0f}'
'{ymax:.0f}'
''
).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 (
'\n'
''
).format(w=width, h=height, x=width // 2 - 30, y=height // 2)