Merge c87f0733b05c1f7ae7b0b1722f74c99056ff02f9 into a6efd281248975144f3396f233df84814d8fc911
This commit is contained in:
commit
901a0deae9
@ -1,11 +1,11 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from typing import Dict, List, Optional, Tuple, Union
|
from typing import Dict, List, Optional, Tuple, Union
|
||||||
from dataclasses import dataclass, field, InitVar
|
from dataclasses import asdict, dataclass, field, InitVar
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from wireviz.wv_helper import int2tuple, aspect_ratio
|
from wireviz.wv_helper import int2tuple, aspect_ratio
|
||||||
from wireviz.wv_colors import Color, Colors, ColorMode, ColorScheme, COLOR_CODES
|
from wireviz.wv_colors import Color, Colors, ColorMode, ColorScheme, COLOR_CODES, translate_color
|
||||||
|
|
||||||
|
|
||||||
# Each type alias have their legal values described in comments - validation might be implemented in the future
|
# Each type alias have their legal values described in comments - validation might be implemented in the future
|
||||||
@ -13,6 +13,7 @@ PlainText = str # Text not containing HTML tags nor newlines
|
|||||||
Hypertext = str # Text possibly including HTML hyperlinks that are removed in all outputs except HTML output
|
Hypertext = str # Text possibly including HTML hyperlinks that are removed in all outputs except HTML output
|
||||||
MultilineHypertext = str # Hypertext possibly also including newlines to break lines in diagram output
|
MultilineHypertext = str # Hypertext possibly also including newlines to break lines in diagram output
|
||||||
Designator = PlainText # Case insensitive unique name of connector or cable
|
Designator = PlainText # Case insensitive unique name of connector or cable
|
||||||
|
Points = float # Size in points = 1/72 inch
|
||||||
|
|
||||||
# Literal type aliases below are commented to avoid requiring python 3.8
|
# Literal type aliases below are commented to avoid requiring python 3.8
|
||||||
ConnectorMultiplier = PlainText # = Literal['pincount', 'populated']
|
ConnectorMultiplier = PlainText # = Literal['pincount', 'populated']
|
||||||
@ -33,25 +34,68 @@ class Metadata(dict):
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Options:
|
class Look:
|
||||||
fontname: PlainText = 'arial'
|
"""Colors and font that defines how an element should look like."""
|
||||||
bgcolor: Color = 'WH'
|
bordercolor: Optional[Color] = None
|
||||||
bgcolor_node: Optional[Color] = 'WH'
|
bgcolor: Optional[Color] = None
|
||||||
bgcolor_connector: Optional[Color] = None
|
fontcolor: Optional[Color] = None
|
||||||
bgcolor_cable: Optional[Color] = None
|
fontname: Optional[PlainText] = None
|
||||||
bgcolor_bundle: Optional[Color] = None
|
fontsize: Optional[Points] = None
|
||||||
|
|
||||||
|
def lookdict(self) -> dict:
|
||||||
|
"""Return Look attributes as dict."""
|
||||||
|
return {k:v for k,v in asdict(self).items() if k in asdict(DEFAULT_LOOK).keys()}
|
||||||
|
|
||||||
|
def _2dict(self) -> dict:
|
||||||
|
"""Return dict of non-None strings with color values translated to hex."""
|
||||||
|
return {
|
||||||
|
k:translate_color(v, "hex") if 'color' in k else str(v)
|
||||||
|
for k,v in self.lookdict().items() if v is not None
|
||||||
|
}
|
||||||
|
|
||||||
|
def graph_args(self) -> dict:
|
||||||
|
"""Return dict with arguments to a dot graph."""
|
||||||
|
return {k:v for k,v in self._2dict().items() if k != 'bordercolor'}
|
||||||
|
|
||||||
|
def node_args(self) -> dict:
|
||||||
|
"""Return dict with arguments to a dot node with filled style."""
|
||||||
|
return {k.replace('border', '').replace('bg', 'fill'):v for k,v in self._2dict().items()}
|
||||||
|
|
||||||
|
def html_style(self, color_prefix: Optional[str] = None, include_all: bool = True) -> str:
|
||||||
|
"""Return HTML style value containing all non-empty option values."""
|
||||||
|
translated = Look(**self._2dict())
|
||||||
|
return ' '.join(value for value in (
|
||||||
|
f'{color_prefix} {translated.bordercolor};' if self.bordercolor and color_prefix else None,
|
||||||
|
f'background-color: {translated.bgcolor};' if self.bgcolor and include_all else None,
|
||||||
|
f'color: {translated.fontcolor};' if self.fontcolor and include_all else None,
|
||||||
|
f'font-family: {self.fontname};' if self.fontname and include_all else None,
|
||||||
|
f'font-size: {self.fontsize}pt;' if self.fontsize and include_all else None,
|
||||||
|
) if value)
|
||||||
|
|
||||||
|
DEFAULT_LOOK = Look(
|
||||||
|
bordercolor = 'BK',
|
||||||
|
bgcolor = 'WH',
|
||||||
|
fontcolor = 'BK',
|
||||||
|
fontname = 'arial',
|
||||||
|
fontsize = 14,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Options(Look):
|
||||||
|
node: Look = field(default_factory=dict)
|
||||||
|
connector: Look = field(default_factory=dict)
|
||||||
|
cable: Look = field(default_factory=dict)
|
||||||
|
bundle: Look = field(default_factory=dict)
|
||||||
color_mode: ColorMode = 'SHORT'
|
color_mode: ColorMode = 'SHORT'
|
||||||
mini_bom_mode: bool = True
|
mini_bom_mode: bool = True
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
if not self.bgcolor_node:
|
# Build initialization dicts with default values followed by dict entries from YAML input.
|
||||||
self.bgcolor_node = self.bgcolor
|
self.node = Look(**{**self.lookdict(), **self.node})
|
||||||
if not self.bgcolor_connector:
|
self.connector = Look(**{**asdict(self.node), **self.connector})
|
||||||
self.bgcolor_connector = self.bgcolor_node
|
self.cable = Look(**{**asdict(self.node), **self.cable})
|
||||||
if not self.bgcolor_cable:
|
self.bundle = Look(**{**asdict(self.cable), **self.bundle})
|
||||||
self.bgcolor_cable = self.bgcolor_node
|
|
||||||
if not self.bgcolor_bundle:
|
|
||||||
self.bgcolor_bundle = self.bgcolor_cable
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -67,16 +111,19 @@ class Image:
|
|||||||
src: str
|
src: str
|
||||||
scale: Optional[ImageScale] = None
|
scale: Optional[ImageScale] = None
|
||||||
# Attributes of the image cell <td> containing the image:
|
# Attributes of the image cell <td> containing the image:
|
||||||
width: Optional[int] = None
|
width: Optional[Points] = None
|
||||||
height: Optional[int] = None
|
height: Optional[Points] = None
|
||||||
fixedsize: Optional[bool] = None
|
fixedsize: Optional[bool] = None
|
||||||
bgcolor: Optional[Color] = None
|
box: Optional[Look] = None
|
||||||
# Contents of the text cell <td> just below the image cell:
|
# Contents of the text cell <td> just below the image cell:
|
||||||
caption: Optional[MultilineHypertext] = None
|
caption: Optional[MultilineHypertext] = None
|
||||||
# See also HTML doc at https://graphviz.org/doc/info/shapes.html#html
|
# See also HTML doc at https://graphviz.org/doc/info/shapes.html#html
|
||||||
|
|
||||||
def __post_init__(self, gv_dir):
|
def __post_init__(self, gv_dir):
|
||||||
|
|
||||||
|
if isinstance(self.box, dict):
|
||||||
|
self.box = Look(**self.box)
|
||||||
|
|
||||||
if self.fixedsize is None:
|
if self.fixedsize is None:
|
||||||
# Default True if any dimension specified unless self.scale also is specified.
|
# Default True if any dimension specified unless self.scale also is specified.
|
||||||
self.fixedsize = (self.width or self.height) and self.scale is None
|
self.fixedsize = (self.width or self.height) and self.scale is None
|
||||||
@ -109,7 +156,11 @@ class AdditionalComponent:
|
|||||||
qty: float = 1
|
qty: float = 1
|
||||||
unit: Optional[str] = None
|
unit: Optional[str] = None
|
||||||
qty_multiplier: Union[ConnectorMultiplier, CableMultiplier, None] = None
|
qty_multiplier: Union[ConnectorMultiplier, CableMultiplier, None] = None
|
||||||
bgcolor: Optional[Color] = None
|
box: Optional[Look] = None
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if isinstance(self.box, dict):
|
||||||
|
self.box = Look(**self.box)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def description(self) -> str:
|
def description(self) -> str:
|
||||||
@ -119,8 +170,8 @@ class AdditionalComponent:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Connector:
|
class Connector:
|
||||||
name: Designator
|
name: Designator
|
||||||
bgcolor: Optional[Color] = None
|
box: Optional[Look] = None
|
||||||
bgcolor_title: Optional[Color] = None
|
title: Optional[Look] = None
|
||||||
manufacturer: Optional[MultilineHypertext] = None
|
manufacturer: Optional[MultilineHypertext] = None
|
||||||
mpn: Optional[MultilineHypertext] = None
|
mpn: Optional[MultilineHypertext] = None
|
||||||
supplier: Optional[MultilineHypertext] = None
|
supplier: Optional[MultilineHypertext] = None
|
||||||
@ -147,6 +198,10 @@ class Connector:
|
|||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
|
|
||||||
|
if isinstance(self.box, dict):
|
||||||
|
self.box = Look(**self.box)
|
||||||
|
if isinstance(self.title, dict):
|
||||||
|
self.title = Look(**self.title)
|
||||||
if isinstance(self.image, dict):
|
if isinstance(self.image, dict):
|
||||||
self.image = Image(**self.image)
|
self.image = Image(**self.image)
|
||||||
|
|
||||||
@ -205,8 +260,8 @@ class Connector:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Cable:
|
class Cable:
|
||||||
name: Designator
|
name: Designator
|
||||||
bgcolor: Optional[Color] = None
|
box: Optional[Look] = None
|
||||||
bgcolor_title: Optional[Color] = None
|
title: Optional[Look] = None
|
||||||
manufacturer: Union[MultilineHypertext, List[MultilineHypertext], None] = None
|
manufacturer: Union[MultilineHypertext, List[MultilineHypertext], None] = None
|
||||||
mpn: Union[MultilineHypertext, List[MultilineHypertext], None] = None
|
mpn: Union[MultilineHypertext, List[MultilineHypertext], None] = None
|
||||||
supplier: Union[MultilineHypertext, List[MultilineHypertext], None] = None
|
supplier: Union[MultilineHypertext, List[MultilineHypertext], None] = None
|
||||||
@ -235,6 +290,10 @@ class Cable:
|
|||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
|
|
||||||
|
if isinstance(self.box, dict):
|
||||||
|
self.box = Look(**self.box)
|
||||||
|
if isinstance(self.title, dict):
|
||||||
|
self.title = Look(**self.title)
|
||||||
if isinstance(self.image, dict):
|
if isinstance(self.image, dict):
|
||||||
self.image = Image(**self.image)
|
self.image = Image(**self.image)
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@ from wireviz import wv_colors, __version__, APP_NAME, APP_URL
|
|||||||
from wireviz.DataClasses import Metadata, Options, Tweak, Connector, Cable
|
from wireviz.DataClasses import Metadata, Options, Tweak, Connector, Cable
|
||||||
from wireviz.wv_colors import get_color_hex, translate_color
|
from wireviz.wv_colors import get_color_hex, translate_color
|
||||||
from wireviz.wv_gv_html import nested_html_table, \
|
from wireviz.wv_gv_html import nested_html_table, \
|
||||||
html_bgcolor_attr, html_bgcolor, html_colorbar, \
|
html_cell, html_colorbar, \
|
||||||
html_image, html_caption, remove_links, html_line_breaks
|
html_image, html_caption, remove_links, html_line_breaks
|
||||||
from wireviz.wv_bom import pn_info_string, component_table_entry, \
|
from wireviz.wv_bom import pn_info_string, component_table_entry, \
|
||||||
get_additional_component_table, bom_list, generate_bom, \
|
get_additional_component_table, bom_list, generate_bom, \
|
||||||
@ -97,17 +97,16 @@ class Harness:
|
|||||||
dot.body.append(f'// {APP_URL}')
|
dot.body.append(f'// {APP_URL}')
|
||||||
dot.attr('graph', rankdir='LR',
|
dot.attr('graph', rankdir='LR',
|
||||||
ranksep='2',
|
ranksep='2',
|
||||||
bgcolor=wv_colors.translate_color(self.options.bgcolor, "HEX"),
|
|
||||||
nodesep='0.33',
|
nodesep='0.33',
|
||||||
fontname=self.options.fontname)
|
**self.options.graph_args())
|
||||||
dot.attr('node',
|
dot.attr('node', shape='none',
|
||||||
shape='none',
|
|
||||||
width='0', height='0', margin='0', # Actual size of the node is entirely determined by the label.
|
|
||||||
style='filled',
|
style='filled',
|
||||||
fillcolor=wv_colors.translate_color(self.options.bgcolor_node, "HEX"),
|
width='0', height='0', margin='0', # Actual size of the node is entirely determined by the label.
|
||||||
fontname=self.options.fontname)
|
**self.options.node.node_args())
|
||||||
dot.attr('edge', style='bold',
|
dot.attr('edge', style='bold',
|
||||||
fontname=self.options.fontname)
|
**self.options.node_args())
|
||||||
|
|
||||||
|
wire_border_hex = wv_colors.get_color_hex(self.options.bordercolor)[0]
|
||||||
|
|
||||||
# prepare ports on connectors depending on which side they will connect
|
# prepare ports on connectors depending on which side they will connect
|
||||||
for _, cable in self.cables.items():
|
for _, cable in self.cables.items():
|
||||||
@ -125,7 +124,7 @@ class Harness:
|
|||||||
|
|
||||||
html = []
|
html = []
|
||||||
|
|
||||||
rows = [[f'{html_bgcolor(connector.bgcolor_title)}{remove_links(connector.name)}'
|
rows = [[html_cell(connector.title, remove_links(connector.name))
|
||||||
if connector.show_name else None],
|
if connector.show_name else None],
|
||||||
[pn_info_string(HEADER_PN, None, remove_links(connector.pn)),
|
[pn_info_string(HEADER_PN, None, remove_links(connector.pn)),
|
||||||
html_line_breaks(pn_info_string(HEADER_MPN, connector.manufacturer, connector.mpn)),
|
html_line_breaks(pn_info_string(HEADER_MPN, connector.manufacturer, connector.mpn)),
|
||||||
@ -140,7 +139,7 @@ class Harness:
|
|||||||
[html_caption(connector.image)]]
|
[html_caption(connector.image)]]
|
||||||
rows.extend(get_additional_component_table(self, connector))
|
rows.extend(get_additional_component_table(self, connector))
|
||||||
rows.append([html_line_breaks(connector.notes)])
|
rows.append([html_line_breaks(connector.notes)])
|
||||||
html.extend(nested_html_table(rows, html_bgcolor_attr(connector.bgcolor)))
|
html.extend(nested_html_table(rows, connector.box))
|
||||||
|
|
||||||
if connector.style != 'simple':
|
if connector.style != 'simple':
|
||||||
pinhtml = []
|
pinhtml = []
|
||||||
@ -175,10 +174,11 @@ class Harness:
|
|||||||
|
|
||||||
html = '\n'.join(html)
|
html = '\n'.join(html)
|
||||||
dot.node(connector.name, label=f'<\n{html}\n>', shape='box', style='filled',
|
dot.node(connector.name, label=f'<\n{html}\n>', shape='box', style='filled',
|
||||||
fillcolor=translate_color(self.options.bgcolor_connector, "HEX"))
|
**self.options.connector.node_args())
|
||||||
|
|
||||||
if len(connector.loops) > 0:
|
if len(connector.loops) > 0:
|
||||||
dot.attr('edge', color='#000000:#ffffff:#000000')
|
# TODO: Use self.options.wire.color and self.options.wire.bgcolor here?
|
||||||
|
dot.attr('edge', color=f'{wire_border_hex}:#ffffff:{wire_border_hex}')
|
||||||
if connector.ports_left:
|
if connector.ports_left:
|
||||||
loop_side = 'l'
|
loop_side = 'l'
|
||||||
loop_dir = 'w'
|
loop_dir = 'w'
|
||||||
@ -210,7 +210,7 @@ class Harness:
|
|||||||
elif cable.gauge_unit.upper() == 'AWG':
|
elif cable.gauge_unit.upper() == 'AWG':
|
||||||
awg_fmt = f' ({mm2_equiv(cable.gauge)} mm\u00B2)'
|
awg_fmt = f' ({mm2_equiv(cable.gauge)} mm\u00B2)'
|
||||||
|
|
||||||
rows = [[f'{html_bgcolor(cable.bgcolor_title)}{remove_links(cable.name)}'
|
rows = [[html_cell(cable.title, remove_links(cable.name))
|
||||||
if cable.show_name else None],
|
if cable.show_name else None],
|
||||||
[pn_info_string(HEADER_PN, None,
|
[pn_info_string(HEADER_PN, None,
|
||||||
remove_links(cable.pn)) if not isinstance(cable.pn, list) else None,
|
remove_links(cable.pn)) if not isinstance(cable.pn, list) else None,
|
||||||
@ -233,7 +233,7 @@ class Harness:
|
|||||||
|
|
||||||
rows.extend(get_additional_component_table(self, cable))
|
rows.extend(get_additional_component_table(self, cable))
|
||||||
rows.append([html_line_breaks(cable.notes)])
|
rows.append([html_line_breaks(cable.notes)])
|
||||||
html.extend(nested_html_table(rows, html_bgcolor_attr(cable.bgcolor)))
|
html.extend(nested_html_table(rows, cable.box))
|
||||||
|
|
||||||
wirehtml = []
|
wirehtml = []
|
||||||
wirehtml.append('<table border="0" cellspacing="0" cellborder="0">') # conductor table
|
wirehtml.append('<table border="0" cellspacing="0" cellborder="0">') # conductor table
|
||||||
@ -258,10 +258,11 @@ class Harness:
|
|||||||
wirehtml.append(f' <td><!-- {i}_out --></td>')
|
wirehtml.append(f' <td><!-- {i}_out --></td>')
|
||||||
wirehtml.append(' </tr>')
|
wirehtml.append(' </tr>')
|
||||||
|
|
||||||
bgcolors = ['#000000'] + get_color_hex(connection_color, pad=pad) + ['#000000']
|
bgcolors = [wire_border_hex] + get_color_hex(connection_color, pad=pad) + [wire_border_hex]
|
||||||
wirehtml.append(f' <tr>')
|
wirehtml.append(f' <tr>')
|
||||||
wirehtml.append(f' <td colspan="3" border="0" cellspacing="0" cellpadding="0" port="w{i}" height="{(2 * len(bgcolors))}">')
|
wirehtml.append(f' <td colspan="3" border="0" cellspacing="0" cellpadding="0" port="w{i}" height="{(2 * len(bgcolors))}">')
|
||||||
wirehtml.append(' <table cellspacing="0" cellborder="0" border="0">')
|
wirehtml.append(' <table cellspacing="0" cellborder="0" border="0">')
|
||||||
|
# TODO: Reverse curved wire colors instead? Test also with empty wire colors! wv_colors.default_color ??
|
||||||
for j, bgcolor in enumerate(bgcolors[::-1]): # Reverse to match the curved wires when more than 2 colors
|
for j, bgcolor in enumerate(bgcolors[::-1]): # Reverse to match the curved wires when more than 2 colors
|
||||||
wirehtml.append(f' <tr><td colspan="3" cellpadding="0" height="2" bgcolor="{bgcolor if bgcolor != "" else wv_colors.default_color}" border="0"></td></tr>')
|
wirehtml.append(f' <tr><td colspan="3" cellpadding="0" height="2" bgcolor="{bgcolor if bgcolor != "" else wv_colors.default_color}" border="0"></td></tr>')
|
||||||
wirehtml.append(' </table>')
|
wirehtml.append(' </table>')
|
||||||
@ -301,10 +302,10 @@ class Harness:
|
|||||||
if isinstance(cable.shield, str):
|
if isinstance(cable.shield, str):
|
||||||
# shield is shown with specified color and black borders
|
# shield is shown with specified color and black borders
|
||||||
shield_color_hex = wv_colors.get_color_hex(cable.shield)[0]
|
shield_color_hex = wv_colors.get_color_hex(cable.shield)[0]
|
||||||
attributes = f'height="6" bgcolor="{shield_color_hex}" border="2" sides="tb"'
|
attributes = f'height="6" bgcolor="{shield_color_hex}" color="{wire_border_hex}" border="2" sides="tb"'
|
||||||
else:
|
else:
|
||||||
# shield is shown as a thin black wire
|
# shield is shown as a thin black wire
|
||||||
attributes = f'height="2" bgcolor="#000000" border="0"'
|
attributes = f'height="2" bgcolor="{wire_border_hex}" border="0"'
|
||||||
wirehtml.append(f' <tr><td colspan="3" cellpadding="0" {attributes} port="ws"></td></tr>')
|
wirehtml.append(f' <tr><td colspan="3" cellpadding="0" {attributes} port="ws"></td></tr>')
|
||||||
|
|
||||||
wirehtml.append(' <tr><td> </td></tr>')
|
wirehtml.append(' <tr><td> </td></tr>')
|
||||||
@ -315,10 +316,10 @@ class Harness:
|
|||||||
# connections
|
# connections
|
||||||
for connection in cable.connections:
|
for connection in cable.connections:
|
||||||
if isinstance(connection.via_port, int): # check if it's an actual wire and not a shield
|
if isinstance(connection.via_port, int): # check if it's an actual wire and not a shield
|
||||||
dot.attr('edge', color=':'.join(['#000000'] + wv_colors.get_color_hex(cable.colors[connection.via_port - 1], pad=pad) + ['#000000']))
|
dot.attr('edge', color=':'.join([wire_border_hex] + wv_colors.get_color_hex(cable.colors[connection.via_port - 1], pad=pad) + [wire_border_hex]))
|
||||||
else: # it's a shield connection
|
else: # it's a shield connection
|
||||||
# shield is shown with specified color and black borders, or as a thin black wire otherwise
|
# shield is shown with specified color and black borders, or as a thin black wire otherwise
|
||||||
dot.attr('edge', color=':'.join(['#000000', shield_color_hex, '#000000']) if isinstance(cable.shield, str) else '#000000')
|
dot.attr('edge', color=':'.join([wire_border_hex, shield_color_hex, wire_border_hex]) if isinstance(cable.shield, str) else wire_border_hex)
|
||||||
if connection.from_port is not None: # connect to left
|
if connection.from_port is not None: # connect to left
|
||||||
from_connector = self.connectors[connection.from_name]
|
from_connector = self.connectors[connection.from_name]
|
||||||
from_port = f':p{connection.from_port+1}r' if from_connector.style != 'simple' else ''
|
from_port = f':p{connection.from_port+1}r' if from_connector.style != 'simple' else ''
|
||||||
@ -352,11 +353,10 @@ class Harness:
|
|||||||
to_string = ''
|
to_string = ''
|
||||||
html = [row.replace(f'<!-- {connection.via_port}_out -->', to_string) for row in html]
|
html = [row.replace(f'<!-- {connection.via_port}_out -->', to_string) for row in html]
|
||||||
|
|
||||||
style, bgcolor = ('filled,dashed', self.options.bgcolor_bundle) if cable.category == 'bundle' else \
|
style, options = ('filled,dashed', self.options.bundle) if cable.category == 'bundle' else \
|
||||||
('filled', self.options.bgcolor_cable)
|
('filled', self.options.cable)
|
||||||
html = '\n'.join(html)
|
html = '\n'.join(html)
|
||||||
dot.node(cable.name, label=f'<\n{html}\n>', shape='box',
|
dot.node(cable.name, label=f'<\n{html}\n>', shape='box', style=style, **options.node_args())
|
||||||
style=style, fillcolor=translate_color(bgcolor, "HEX"))
|
|
||||||
|
|
||||||
def typecheck(name: str, value: Any, expect: type) -> None:
|
def typecheck(name: str, value: Any, expect: type) -> None:
|
||||||
if not isinstance(value, expect):
|
if not isinstance(value, expect):
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
from dataclasses import asdict
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
@ -13,7 +14,7 @@ if __name__ == '__main__':
|
|||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
|
||||||
from wireviz import __version__
|
from wireviz import __version__
|
||||||
from wireviz.DataClasses import Metadata, Options, Tweak
|
from wireviz.DataClasses import DEFAULT_LOOK, Metadata, Options, Tweak
|
||||||
from wireviz.Harness import Harness
|
from wireviz.Harness import Harness
|
||||||
from wireviz.wv_helper import expand, open_file_read
|
from wireviz.wv_helper import expand, open_file_read
|
||||||
|
|
||||||
@ -37,7 +38,7 @@ def parse(yaml_input: str, file_out: (str, Path) = None, return_types: (None, st
|
|||||||
|
|
||||||
harness = Harness(
|
harness = Harness(
|
||||||
metadata = Metadata(**yaml_data.get('metadata', {})),
|
metadata = Metadata(**yaml_data.get('metadata', {})),
|
||||||
options = Options(**yaml_data.get('options', {})),
|
options = Options(**asdict(DEFAULT_LOOK), **yaml_data.get('options', {})),
|
||||||
tweak = Tweak(**yaml_data.get('tweak', {})),
|
tweak = Tweak(**yaml_data.get('tweak', {})),
|
||||||
)
|
)
|
||||||
if 'title' not in harness.metadata:
|
if 'title' not in harness.metadata:
|
||||||
|
|||||||
@ -4,9 +4,9 @@ from dataclasses import asdict
|
|||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
from wireviz.DataClasses import AdditionalComponent, Cable, Color, Connector
|
from wireviz.DataClasses import AdditionalComponent, Cable, Connector, Look
|
||||||
from wireviz.wv_colors import translate_color
|
from wireviz.wv_colors import Color, translate_color
|
||||||
from wireviz.wv_gv_html import html_bgcolor_attr, html_line_breaks
|
from wireviz.wv_gv_html import font_tag, html_line_breaks, table_attr
|
||||||
from wireviz.wv_helper import clean_whitespace
|
from wireviz.wv_helper import clean_whitespace
|
||||||
|
|
||||||
BOM_COLUMNS_ALWAYS = ('id', 'description', 'qty', 'unit', 'designators')
|
BOM_COLUMNS_ALWAYS = ('id', 'description', 'qty', 'unit', 'designators')
|
||||||
@ -35,7 +35,7 @@ def get_additional_component_table(harness: "Harness", component: Union[Connecto
|
|||||||
common_args = {
|
common_args = {
|
||||||
'qty': part.qty * component.get_qty_multiplier(part.qty_multiplier),
|
'qty': part.qty * component.get_qty_multiplier(part.qty_multiplier),
|
||||||
'unit': part.unit,
|
'unit': part.unit,
|
||||||
'bgcolor': part.bgcolor,
|
'box': part.box,
|
||||||
}
|
}
|
||||||
if harness.options.mini_bom_mode:
|
if harness.options.mini_bom_mode:
|
||||||
id = get_bom_index(harness.bom(), bom_entry_key({**asdict(part), 'description': part.description}))
|
id = get_bom_index(harness.bom(), bom_entry_key({**asdict(part), 'description': part.description}))
|
||||||
@ -158,7 +158,7 @@ def component_table_entry(
|
|||||||
type: str,
|
type: str,
|
||||||
qty: Union[int, float],
|
qty: Union[int, float],
|
||||||
unit: Optional[str] = None,
|
unit: Optional[str] = None,
|
||||||
bgcolor: Optional[Color] = None,
|
box: Optional[Look] = None,
|
||||||
pn: Optional[str] = None,
|
pn: Optional[str] = None,
|
||||||
manufacturer: Optional[str] = None,
|
manufacturer: Optional[str] = None,
|
||||||
mpn: Optional[str] = None,
|
mpn: Optional[str] = None,
|
||||||
@ -178,8 +178,8 @@ def component_table_entry(
|
|||||||
+ (', '.join([pn for pn in part_number_list if pn])))
|
+ (', '.join([pn for pn in part_number_list if pn])))
|
||||||
# format the above output as left aligned text in a single visible cell
|
# format the above output as left aligned text in a single visible cell
|
||||||
# indent is set to two to match the indent in the generated html table
|
# indent is set to two to match the indent in the generated html table
|
||||||
return f'''<table border="0" cellspacing="0" cellpadding="3" cellborder="1"{html_bgcolor_attr(bgcolor)}><tr>
|
return f'''<table border="0" cellspacing="0" cellpadding="3" cellborder="1"{table_attr(box)}><tr>
|
||||||
<td align="left" balign="left">{html_line_breaks(output)}</td>
|
<td align="left" balign="left">{font_tag(box, html_line_breaks(output))}</td>
|
||||||
</tr></table>'''
|
</tr></table>'''
|
||||||
|
|
||||||
def pn_info_string(header: str, name: Optional[str], number: Optional[str]) -> Optional[str]:
|
def pn_info_string(header: str, name: Optional[str], number: Optional[str]) -> Optional[str]:
|
||||||
|
|||||||
@ -1,19 +1,24 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from typing import List, Optional, Union
|
from typing import List, Optional, Union
|
||||||
import re
|
|
||||||
|
|
||||||
from wireviz.DataClasses import Color
|
from wireviz.DataClasses import Image, Look
|
||||||
from wireviz.wv_colors import translate_color
|
from wireviz.wv_colors import Color, translate_color
|
||||||
from wireviz.wv_helper import remove_links
|
from wireviz.wv_helper import remove_links
|
||||||
|
|
||||||
def nested_html_table(rows: List[Union[str, List[Optional[str]], None]], table_attrs: str = '') -> str:
|
GvHtml = str # Graphviz HTML-like label string
|
||||||
# input: list, each item may be scalar or list
|
GvHtmlX = str # Graphviz HTML-like label string possibly including a leading <tdX> tag
|
||||||
|
GvHtmlAttr = str # Attributes part of Graphviz HTML-like tag (including a leading space)
|
||||||
|
|
||||||
|
def nested_html_table(rows: List[Union[GvHtml, List[Optional[GvHtmlX]], None]], look: Optional[Look]) -> GvHtml:
|
||||||
|
# input: list, each item may be scalar or list, and look with optional table look attributes
|
||||||
# output: a parent table with one child table per parent item that is list, and one cell per parent item that is scalar
|
# output: a parent table with one child table per parent item that is list, and one cell per parent item that is scalar
|
||||||
# purpose: create the appearance of one table, where cell widths are independent between rows
|
# purpose: create the appearance of one table, where cell widths are independent between rows
|
||||||
# attributes in any leading <tdX> inside a list are injected into to the preceeding <td> tag
|
# attributes in any leading <tdX> inside a list are injected into to the preceeding <td> tag
|
||||||
html = []
|
html = []
|
||||||
html.append(f'<table border="0" cellspacing="0" cellpadding="0"{table_attrs or ""}>')
|
attr = font_attr(look)
|
||||||
|
font = f'<font{attr}>' if attr else ''
|
||||||
|
html.append(f'{font}<table border="0" cellspacing="0" cellpadding="0"{table_attr(look)}>')
|
||||||
for row in rows:
|
for row in rows:
|
||||||
if isinstance(row, List):
|
if isinstance(row, List):
|
||||||
if len(row) > 0 and any(row):
|
if len(row) > 0 and any(row):
|
||||||
@ -29,23 +34,36 @@ def nested_html_table(rows: List[Union[str, List[Optional[str]], None]], table_a
|
|||||||
html.append(' <tr><td>')
|
html.append(' <tr><td>')
|
||||||
html.append(f' {row}')
|
html.append(f' {row}')
|
||||||
html.append(' </td></tr>')
|
html.append(' </td></tr>')
|
||||||
html.append('</table>')
|
html.append(f'</table>{"</font>" if font else ""}')
|
||||||
return html
|
return html
|
||||||
|
|
||||||
def html_bgcolor_attr(color: Color) -> str:
|
def table_attr(look: Optional[Look]) -> GvHtmlAttr:
|
||||||
"""Return attributes for bgcolor or '' if no color."""
|
"""Return table tag attributes containing all non-empty table option values."""
|
||||||
return f' bgcolor="{translate_color(color, "HEX")}"' if color else ''
|
return '' if not look else ''.join({
|
||||||
|
f' {k.replace("border", "")}="{v}"' for k,v in look._2dict().items() if v and 'font' not in k})
|
||||||
|
|
||||||
def html_bgcolor(color: Color, _extra_attr: str = '') -> str:
|
def font_attr(look: Optional[Look]) -> GvHtmlAttr:
|
||||||
"""Return <td> attributes prefix for bgcolor or '' if no color."""
|
"""Return font tag attributes containing all non-empty font option values."""
|
||||||
return f'<tdX{html_bgcolor_attr(color)}{_extra_attr}>' if color else ''
|
attr = {k:v for k,v in look._2dict().items() if v and 'font' in k} if look else {}
|
||||||
|
return ((f' color="{attr["fontcolor"]}"' if attr.get('fontcolor') else '')
|
||||||
|
+ (f' face="{attr["fontname"]}"' if attr.get('fontname') else '')
|
||||||
|
+ (f' point-size="{attr["fontsize"]}"' if attr.get('fontsize') else ''))
|
||||||
|
|
||||||
def html_colorbar(color: Color) -> str:
|
def font_tag(look: Optional[Look], text: GvHtml) -> GvHtml:
|
||||||
"""Return <tdX> attributes prefix for bgcolor and minimum width or None if no color."""
|
"""Return text in Graphviz HTML font tag with all non-empty font option values."""
|
||||||
return html_bgcolor(color, ' width="4"') if color else None
|
attr = font_attr(look)
|
||||||
|
return f'<font{attr}>{text}</font>' if attr and text > '' else text
|
||||||
|
|
||||||
def html_image(image):
|
def html_cell(look: Optional[Look], text: GvHtml = '', attr: GvHtmlAttr = '') -> GvHtmlX:
|
||||||
from wireviz.DataClasses import Image
|
"""Return cell to be included in the rows list for nested_html_table()."""
|
||||||
|
return f'<tdX{attr}{table_attr(look)}>{font_tag(look, text)}'
|
||||||
|
|
||||||
|
def html_colorbar(color: Optional[Color]) -> Optional[GvHtmlX]:
|
||||||
|
"""Return colored cell to be included in the rows list for nested_html_table() or None if no color."""
|
||||||
|
return html_cell(Look(bgcolor=color), attr=' width="4"') if color else None
|
||||||
|
|
||||||
|
def html_image(image: Optional[Image]) -> Optional[GvHtmlX]:
|
||||||
|
"""Return image cell to be included in the rows list for nested_html_table() or None if no image."""
|
||||||
if not image:
|
if not image:
|
||||||
return None
|
return None
|
||||||
# The leading attributes belong to the preceeding tag. See where used below.
|
# The leading attributes belong to the preceeding tag. See where used below.
|
||||||
@ -58,16 +76,14 @@ def html_image(image):
|
|||||||
<td{html}</td>
|
<td{html}</td>
|
||||||
</tr></table>
|
</tr></table>
|
||||||
'''
|
'''
|
||||||
return f'''<tdX{' sides="TLR"' if image.caption else ''}{html_bgcolor_attr(image.bgcolor)}{html}'''
|
return f'''<tdX{' sides="TLR"' if image.caption else ''}{table_attr(image.box)}{html}'''
|
||||||
|
|
||||||
def html_caption(image):
|
def html_caption(image: Optional[Image]) -> Optional[GvHtmlX]:
|
||||||
from wireviz.DataClasses import Image
|
"""Return image caption cell to be included just after the image cell or None if no caption."""
|
||||||
return (f'<tdX sides="BLR"{html_bgcolor_attr(image.bgcolor)}>{html_line_breaks(image.caption)}'
|
return html_cell(image.box, html_line_breaks(image.caption), ' sides="BLR"') if image and image.caption else None
|
||||||
if image and image.caption else None)
|
|
||||||
|
|
||||||
def html_size_attr(image):
|
def html_size_attr(image: Optional[Image]) -> GvHtmlAttr:
|
||||||
from wireviz.DataClasses import Image
|
"""Return Graphviz HTML attributes to specify minimum or fixed size of a TABLE or TD object."""
|
||||||
# Return Graphviz HTML attributes to specify minimum or fixed size of a TABLE or TD object
|
|
||||||
return ((f' width="{image.width}"' if image.width else '')
|
return ((f' width="{image.width}"' if image.width else '')
|
||||||
+ (f' height="{image.height}"' if image.height else '')
|
+ (f' height="{image.height}"' if image.height else '')
|
||||||
+ ( ' fixedsize="true"' if image.fixedsize else '')) if image else ''
|
+ ( ' fixedsize="true"' if image.fixedsize else '')) if image else ''
|
||||||
|
|||||||
@ -15,9 +15,7 @@ def generate_html_output(filename: Union[str, Path], bom_list: List[List[str]],
|
|||||||
file.write(' <meta charset="UTF-8">\n')
|
file.write(' <meta charset="UTF-8">\n')
|
||||||
file.write(f' <meta name="generator" content="{APP_NAME} {__version__} - {APP_URL}">\n')
|
file.write(f' <meta name="generator" content="{APP_NAME} {__version__} - {APP_URL}">\n')
|
||||||
file.write(f' <title>{metadata["title"]}</title>\n')
|
file.write(f' <title>{metadata["title"]}</title>\n')
|
||||||
file.write(f'</head><body style="font-family:{options.fontname};background-color:'
|
file.write(f'</head><body style="{options.html_style()}">\n')
|
||||||
f'{wv_colors.translate_color(options.bgcolor, "HEX")}">\n')
|
|
||||||
|
|
||||||
file.write(f'<h1>{metadata["title"]}</h1>\n')
|
file.write(f'<h1>{metadata["title"]}</h1>\n')
|
||||||
description = metadata.get('description')
|
description = metadata.get('description')
|
||||||
if description:
|
if description:
|
||||||
@ -33,17 +31,18 @@ def generate_html_output(filename: Union[str, Path], bom_list: List[List[str]],
|
|||||||
|
|
||||||
file.write('<h2>Bill of Materials</h2>\n')
|
file.write('<h2>Bill of Materials</h2>\n')
|
||||||
listy = flatten2d(bom_list)
|
listy = flatten2d(bom_list)
|
||||||
file.write('<table style="border:1px solid #000000; font-size: 14pt; border-spacing: 0px">\n')
|
border = options.html_style(color_prefix="border: 1px solid", include_all=False)
|
||||||
|
file.write(f'<table style="{border} border-spacing: 0px;">\n')
|
||||||
file.write(' <tr>\n')
|
file.write(' <tr>\n')
|
||||||
for item in listy[0]:
|
for item in listy[0]:
|
||||||
file.write(f' <th style="text-align:left; border:1px solid #000000; padding: 8px">{item}</th>\n')
|
file.write(f' <th style="{border} padding: 8px; text-align:left;">{item}</th>\n')
|
||||||
file.write(' </tr>\n')
|
file.write(' </tr>\n')
|
||||||
for row in listy[1:]:
|
for row in listy[1:]:
|
||||||
file.write(' <tr>\n')
|
file.write(' <tr>\n')
|
||||||
for i, item in enumerate(row):
|
for i, item in enumerate(row):
|
||||||
item_str = item.replace('\u00b2', '²')
|
item_str = item.replace('\u00b2', '²')
|
||||||
align = '; text-align:right' if listy[0][i] == 'Qty' else ''
|
align = '; text-align:right' if listy[0][i] == 'Qty' else ''
|
||||||
file.write(f' <td style="border:1px solid #000000; padding: 4px{align}">{item_str}</td>\n')
|
file.write(f' <td style="{border} padding: 4px{align};">{item_str}</td>\n')
|
||||||
file.write(' </tr>\n')
|
file.write(' </tr>\n')
|
||||||
file.write('</table>\n')
|
file.write('</table>\n')
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user