- Extract the elevation's <svg> body into inc/elevation_svg_content.html, shared by the on-page elevation and a new standalone export template so they never drift apart. - Add "Export" dropdown above the elevation: Front/Rear as standalone SVG (self-contained inline styles, no dependency on the page's CSS, absolute device-type image URLs so the file still works once saved elsewhere), Front/Rear as PNG (rasterized entirely client-side via <canvas> from that same SVG - no new server-side image dependency), and the whole concept as a draw.io (.drawio / mxGraph XML) diagram with both faces side by side, reusing the identical block geometry. - Center the "+ Add device" hint on empty units instead of right-aligning it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
113 lines
3.9 KiB
Python
113 lines
3.9 KiB
Python
"""
|
|
Export a rack concept as a draw.io (diagrams.net) diagram.
|
|
|
|
Reuses the same Elevation/ElevationBlock geometry the on-page SVG and the SVG export
|
|
are built from, so all three always agree on where everything sits. The output is a
|
|
plain mxGraph XML file - drawio can open it directly (File > Open) or import it
|
|
(Extras > Edit Diagram accepts the same XML pasted in).
|
|
"""
|
|
import html as html_lib
|
|
|
|
from .elevation import get_elevations
|
|
|
|
__all__ = ('build_drawio',)
|
|
|
|
LABEL_HEIGHT = 30
|
|
FACE_GAP = 60
|
|
|
|
|
|
def _escape(text):
|
|
"""HTML-escape for use inside an html=1 cell value (which is itself XML-escaped
|
|
again by the caller) - two layers, same as any HTML embedded in an XML attribute."""
|
|
return html_lib.escape(text or '', quote=False)
|
|
|
|
|
|
def _quote_attr(value):
|
|
"""XML-attribute-escape a string, matching xml.sax.saxutils.quoteattr but always
|
|
using double quotes so it's safe to splice straight into our hand-built XML."""
|
|
return '"' + (
|
|
value.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
|
) + '"'
|
|
|
|
|
|
class _CellBuilder:
|
|
"""Accumulates mxCell XML fragments and hands out fresh ids."""
|
|
|
|
def __init__(self):
|
|
self._next_id = 1
|
|
self.cells = []
|
|
|
|
def _id(self):
|
|
self._next_id += 1
|
|
return f'c{self._next_id}'
|
|
|
|
def add(self, value, style, x, y, width, height):
|
|
cell_id = self._id()
|
|
self.cells.append(
|
|
f' <mxCell id="{cell_id}" value={_quote_attr(value)} style={_quote_attr(style)} '
|
|
f'vertex="1" parent="1">\n'
|
|
f' <mxGeometry x="{x:.2f}" y="{y:.2f}" width="{width:.2f}" height="{height:.2f}" '
|
|
f'as="geometry"/>\n'
|
|
f' </mxCell>'
|
|
)
|
|
return cell_id
|
|
|
|
|
|
def _device_style(block):
|
|
style = (
|
|
f'rounded=1;whiteSpace=wrap;html=1;arcSize=8;'
|
|
f'fillColor=#{block.color};fontColor=#{block.text_color};fontSize=11;'
|
|
)
|
|
if block.opposite:
|
|
style += 'dashed=1;opacity=45;'
|
|
return style
|
|
|
|
|
|
def build_drawio(concept):
|
|
front, rear = get_elevations(concept)
|
|
builder = _CellBuilder()
|
|
|
|
offset_x = 0.0
|
|
for elevation in (front, rear):
|
|
builder.add(
|
|
_escape(elevation.face_label.title()),
|
|
'text;html=1;align=center;verticalAlign=middle;fontStyle=1;fontSize=14;',
|
|
offset_x, 0, elevation.width, LABEL_HEIGHT - 4
|
|
)
|
|
builder.add(
|
|
'', 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#35404A;strokeWidth=2;',
|
|
offset_x + elevation.rack_x, LABEL_HEIGHT + 6, elevation.rack_width, elevation.height - 12
|
|
)
|
|
for unit in elevation.units:
|
|
builder.add(
|
|
str(unit.number),
|
|
'text;html=1;align=right;verticalAlign=middle;fontSize=10;fontColor=#808F9C;spacingRight=4;',
|
|
offset_x, LABEL_HEIGHT + unit.y, elevation.rack_x - 4, unit.height
|
|
)
|
|
for block in elevation.blocks:
|
|
label = _escape(block.label)
|
|
if block.sublabel:
|
|
label += '<br>' + _escape(block.sublabel)
|
|
builder.add(
|
|
label, _device_style(block),
|
|
offset_x + block.x, LABEL_HEIGHT + block.y, block.width, block.height
|
|
)
|
|
offset_x += elevation.width + FACE_GAP
|
|
|
|
body = '\n'.join(builder.cells)
|
|
|
|
return (
|
|
'<mxfile host="netbox-rack-concept">\n'
|
|
f' <diagram id="concept-{concept.pk}" name={_quote_attr(concept.name)}>\n'
|
|
' <mxGraphModel dx="800" dy="600" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" '
|
|
'arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">\n'
|
|
' <root>\n'
|
|
' <mxCell id="0"/>\n'
|
|
' <mxCell id="1" parent="0"/>\n'
|
|
f'{body}\n'
|
|
' </root>\n'
|
|
' </mxGraphModel>\n'
|
|
' </diagram>\n'
|
|
'</mxfile>\n'
|
|
)
|