web: the composer, served behind the existing proxy chain
A browser page with controls on one side and the cross section on the other. Three separately classed layers: printed material, the cavities the stock passes through, and the stock drawn inside its cavity so the fit gap is visible. Under it the report and the design record, so the identity of a part is visible while tuning rather than discovered afterwards. Standard library only, no framework, no build step, no new dependency. Binding comes from /etc/mechcomp/mechcomp.env, which already declared 10.20.0.10:8770. An earlier draft invented a port on 0.0.0.0, which would have placed a second unproxied plaintext copy beside the proxied one. The fallback with no env file is loopback, never every interface: behind a proxy, binding too narrowly fails loudly as a 502 and too widely fails silently as an open service. Controls are filtered to the selected profile, with no catch all group. A knob that moves nothing is worse than an absent one. Renderer tests assert what an eye cannot: the vertical flip happens exactly once, holes render as holes, and the cavity is larger than the stock inside it. Seven mutations on the renderer, all caught.
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
Render a cross-section as SVG.
|
||||
|
||||
WHY SEPARATELY CLASSED PARTS
|
||||
ROADMAP section 4 gives this as the reason the port exists at all: the
|
||||
outline, each stock cavity and the stock itself are emitted as distinct
|
||||
classed groups, so a viewer can highlight or hide them independently. A
|
||||
single flattened silhouette would have been far easier and would not have
|
||||
justified leaving OpenSCAD.
|
||||
|
||||
COORDINATES
|
||||
Geometry has +Y up; SVG has +Y down. The whole drawing is flipped once in a
|
||||
wrapping transform rather than by negating coordinates, so the paths in the
|
||||
file remain the geometry's own numbers and can be read against the report.
|
||||
|
||||
HOLES
|
||||
A region is a list of rings, some of which are holes, and their winding
|
||||
direction is not something this module wants to depend on. Every ring of a
|
||||
region goes into one path with ``fill-rule="evenodd"``, which renders holes
|
||||
correctly whichever way they wind.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, List, Optional, Sequence, Tuple
|
||||
|
||||
Point = Tuple[float, float]
|
||||
Path = List[Point]
|
||||
Region = Sequence[Path]
|
||||
|
||||
|
||||
def _fmt(v: float) -> str:
|
||||
"""Trim to 4 decimal places -- well below anything visible at any zoom."""
|
||||
return ("%.4f" % v).rstrip("0").rstrip(".") or "0"
|
||||
|
||||
|
||||
def path_d(region: Region) -> str:
|
||||
"""One SVG path definition covering every ring of a region."""
|
||||
parts: List[str] = []
|
||||
for ring in region:
|
||||
if len(ring) < 3:
|
||||
continue
|
||||
parts.append("M " + " L ".join("%s %s" % (_fmt(x), _fmt(y))
|
||||
for x, y in ring) + " Z")
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def bounds(regions: Iterable[Region]) -> Tuple[float, float, float, float]:
|
||||
xs: List[float] = []
|
||||
ys: List[float] = []
|
||||
for region in regions:
|
||||
for ring in region:
|
||||
for x, y in ring:
|
||||
xs.append(x)
|
||||
ys.append(y)
|
||||
if not xs:
|
||||
return (-1.0, -1.0, 1.0, 1.0)
|
||||
return (min(xs), min(ys), max(xs), max(ys))
|
||||
|
||||
|
||||
def render(section: Region,
|
||||
cavities: Optional[Region] = None,
|
||||
stock: Optional[Sequence[Path]] = None,
|
||||
margin: float = 3.0,
|
||||
width_px: int = 520) -> str:
|
||||
"""
|
||||
The cross-section as a standalone SVG document.
|
||||
|
||||
``section`` is the printed material. ``cavities`` are the voids the stock
|
||||
passes through. ``stock`` is the stock itself, drawn inside its cavity so
|
||||
the fit gap is visible -- which is the whole point of looking at it.
|
||||
"""
|
||||
regions: List[Region] = [section]
|
||||
if cavities:
|
||||
regions.append(cavities)
|
||||
if stock:
|
||||
regions.append(list(stock))
|
||||
|
||||
x0, y0, x1, y1 = bounds(regions)
|
||||
x0 -= margin
|
||||
y0 -= margin
|
||||
x1 += margin
|
||||
y1 += margin
|
||||
w = x1 - x0
|
||||
h = y1 - y0
|
||||
height_px = int(round(width_px * (h / w))) if w > 0 else width_px
|
||||
|
||||
out: List[str] = [
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" '
|
||||
'viewBox="%s %s %s %s" width="%d" height="%d" '
|
||||
'class="mechcomp-section">'
|
||||
% (_fmt(x0), _fmt(y0), _fmt(w), _fmt(h), width_px, height_px),
|
||||
'<style>',
|
||||
'.mc-material { fill: #b9c6d4; stroke: #33414f; stroke-width: 0.25; }',
|
||||
'.mc-cavity { fill: #f4b183; stroke: #a8642a; stroke-width: 0.15; }',
|
||||
'.mc-stock { fill: #55606c; stroke: none; }',
|
||||
'</style>',
|
||||
# One flip for the whole drawing. Everything below is in geometry
|
||||
# coordinates, so a path here can be read against the report directly.
|
||||
'<g transform="translate(0,%s) scale(1,-1)">' % _fmt(y0 + y1),
|
||||
]
|
||||
|
||||
out.append('<g class="mc-layer-material">'
|
||||
'<path class="mc-material" fill-rule="evenodd" d="%s"/></g>'
|
||||
% path_d(section))
|
||||
|
||||
if cavities:
|
||||
out.append('<g class="mc-layer-cavity">'
|
||||
'<path class="mc-cavity" fill-rule="evenodd" d="%s"/></g>'
|
||||
% path_d(cavities))
|
||||
|
||||
if stock:
|
||||
out.append('<g class="mc-layer-stock">'
|
||||
'<path class="mc-stock" fill-rule="evenodd" d="%s"/></g>'
|
||||
% path_d(list(stock)))
|
||||
|
||||
out.append('</g>')
|
||||
out.append('</svg>')
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def render_result(result, geo=None) -> str:
|
||||
"""
|
||||
Render whatever ``build()`` returned.
|
||||
|
||||
Cavities and stock are drawn when the geometry layer can supply them, and
|
||||
quietly omitted when it cannot -- a picture of the material alone is still
|
||||
useful, and a viewer should not fail because a helper moved.
|
||||
"""
|
||||
section = result.section
|
||||
cavities = None
|
||||
stock = None
|
||||
|
||||
g = geo if geo is not None else getattr(result, "geo", None)
|
||||
members = getattr(result, "members", None)
|
||||
|
||||
if g is not None and members:
|
||||
try:
|
||||
from .geom.join import cavity_region
|
||||
cavities = cavity_region(members, g)
|
||||
except Exception: # noqa: BLE001
|
||||
cavities = None
|
||||
try:
|
||||
from .geom.records import strap_path
|
||||
stock = [strap_path(m, g) for m in members]
|
||||
except Exception: # noqa: BLE001
|
||||
stock = None
|
||||
|
||||
return render(section, cavities, stock)
|
||||
Reference in New Issue
Block a user