""" 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' '\n' '\n{body}\n\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' '\n' 'No content\n' ).format(w=width, h=height, x=width // 2 - 30, y=height // 2)