add webui
This commit is contained in:
127
hpgl_svg.py
Normal file
127
hpgl_svg.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user