""" 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' \n' f' \n' f' ' ) 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 += '
' + _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 ( '\n' f' \n' ' \n' ' \n' ' \n' ' \n' f'{body}\n' ' \n' ' \n' ' \n' '\n' )