diff --git a/src/wireviz/DataClasses.py b/src/wireviz/DataClasses.py index 2f7cc14e..6b3b1e36 100644 --- a/src/wireviz/DataClasses.py +++ b/src/wireviz/DataClasses.py @@ -88,6 +88,7 @@ class Cable: gauge_unit: Optional[str] = None show_equiv: bool = False length: float = 0 + color: Optional[str] = None wirecount: Optional[int] = None shield: bool = False notes: Optional[str] = None @@ -149,8 +150,6 @@ def __post_init__(self): else: raise Exception('lists of part data are only supported for bundles') - # for BOM generation - self.wirecount_and_shield = (self.wirecount, self.shield) def connect(self, from_name, from_pin, via_pin, to_name, to_pin): from_pin = int2tuple(from_pin) diff --git a/src/wireviz/Harness.py b/src/wireviz/Harness.py index 9cc6b96d..af7946e3 100644 --- a/src/wireviz/Harness.py +++ b/src/wireviz/Harness.py @@ -86,44 +86,47 @@ def create_graph(self) -> Graph: if connection_color.to_port is not None: # connect to right self.connectors[connection_color.to_name].ports_left = True - for key, connector in self.connectors.items(): + for connector in self.connectors.values(): + + html = [] rows = [[connector.name if connector.show_name else None], [f'P/N: {connector.pn}' if connector.pn else None, - manufacturer_info_field(connector.manufacturer, connector.mpn)], + html_line_breaks(manufacturer_info_field(connector.manufacturer, connector.mpn))], [html_line_breaks(connector.type), html_line_breaks(connector.subtype), f'{connector.pincount}-pin' if connector.show_pincount else None, connector.color, '' if connector.color else None], '' if connector.style != 'simple' else None, [html_line_breaks(connector.notes)]] - html = nested_html_table(rows) + html.extend(nested_html_table(rows)) if connector.color: # add color bar next to color info, if present colorbar = f' bgcolor="{wv_colors.translate_color(connector.color, "HEX")}" width="4">' # leave out ' tag - html = html.replace('>', colorbar) + html = [row.replace('>', colorbar) for row in html] if connector.style != 'simple': - pinlist = [] + pinhtml = [] + pinhtml.append('') + for pin, pinlabel in zip(connector.pins, connector.pinlabels): if connector.hide_disconnected_pins and not connector.visible_pins.get(pin, False): continue - pinlist.append([f'' if connector.ports_left else None, - f'' if pinlabel else '', - f'' if connector.ports_right else None]) + pinhtml.append(' ') + if connector.ports_left: + pinhtml.append(f' ') + if pinlabel: + pinhtml.append(f' ') + if connector.ports_right: + pinhtml.append(f' ') + pinhtml.append(' ') - pinhtml = '
{pin}{pinlabel}{pin}
{pin}{pinlabel}{pin}
' - for i, pin in enumerate(pinlist): - pinhtml = f'{pinhtml}' - for column in pin: - if column is not None: - pinhtml = f'{pinhtml}{column}' - pinhtml = f'{pinhtml}' - pinhtml = f'{pinhtml}
' - html = html.replace('', pinhtml) + pinhtml.append(' ') + html = [row.replace('', '\n'.join(pinhtml)) for row in html] - dot.node(key, label=f'<{html}>', shape='none', margin='0', style='filled', fillcolor='white') + html = '\n'.join(html) + dot.node(connector.name, label=f'<\n{html}\n>', shape='none', margin='0', style='filled', fillcolor='white') if len(connector.loops) > 0: dot.attr('edge', color='#000000:#ffffff:#000000') @@ -139,11 +142,14 @@ def create_graph(self) -> Graph: dot.edge(f'{connector.name}:p{loop[0]}{loop_side}:{loop_dir}', f'{connector.name}:p{loop[1]}{loop_side}:{loop_dir}') + # determine if there are double- or triple-colored wires in the harness; # if so, pad single-color wires to make all wires of equal thickness pad = any(len(colorstr) > 2 for cable in self.cables.values() for colorstr in cable.colors) - for _, cable in self.cables.items(): + for cable in self.cables.values(): + + html = [] awg_fmt = '' if cable.show_equiv: @@ -155,80 +161,71 @@ def create_graph(self) -> Graph: elif cable.gauge_unit.upper() == 'AWG': awg_fmt = f' ({mm2_equiv(cable.gauge)} mm\u00B2)' - identification = [f'P/N: {cable.pn}' if (cable.pn and not isinstance(cable.pn, list)) else '', - manufacturer_info_field(cable.manufacturer if not isinstance(cable.manufacturer, list) else None, - cable.mpn if not isinstance(cable.mpn, list) else None)] - identification = list(filter(None, identification)) - - attributes = [html_line_breaks(cable.type) if cable.type else '', - f'{len(cable.colors)}x' if cable.show_wirecount else '', - f'{cable.gauge} {cable.gauge_unit}{awg_fmt}' if cable.gauge else '', - '+ S' if cable.shield else '', - f'{cable.length} m' if cable.length > 0 else ''] - attributes = list(filter(None, attributes)) - - html = '' # main table - - if cable.show_name or len(attributes) > 0: - html = f'{html}' # name+attributes table - - html = f'{html}' # spacer between attributes and wires - - html = f'{html}') - html = f'{html}' # main table - if cable.notes: - html = f'{html}' # notes table - html = f'{html}' # spacer at the end + wirehtml.append(' ') + wirehtml.append('
' # name+attributes table - if cable.show_name: - html = f'{html}' - if(len(identification) > 0): # print an identification row if values specified - html = f'{html}' # end identification row - if(len(attributes) > 0): - html = f'{html}' # attribute row - for attrib in attributes: - html = f'{html}' - html = f'{html}' # attribute row - html = f'{html}
{cable.name}
' - for attrib in identification[0:-1]: - html = f'{html}' # all columns except last have a border on the right (sides="R") - if len(identification) > 0: - html = f'{html}' # last column has no border on the right because the enclosing table borders it - html = f'{html}
{attrib}{identification[-1]}
{attrib}
 
' # conductor table + rows = [[cable.name if cable.show_name else None], + [f'P/N: {cable.pn}' if (cable.pn and not isinstance(cable.pn, list)) else None, + html_line_breaks(manufacturer_info_field( + cable.manufacturer if not isinstance(cable.manufacturer, list) else None, + cable.mpn if not isinstance(cable.mpn, list) else None))], + [html_line_breaks(cable.type), + f'{cable.wirecount}x' if cable.show_wirecount else None, + f'{cable.gauge} {cable.gauge_unit}{awg_fmt}' if cable.gauge else None, + '+ S' if cable.shield else None, + f'{cable.length} m' if cable.length > 0 else None, + cable.color, '' if cable.color else None], + '', + [html_line_breaks(cable.notes)]] + html.extend(nested_html_table(rows)) + + if cable.color: # add color bar next to color info, if present + colorbar = f' bgcolor="{wv_colors.translate_color(cable.color, "HEX")}" width="4">' # leave out ' tag + html = [row.replace('>', colorbar) for row in html] + + wirehtml = [] + wirehtml.append('
') # conductor table + wirehtml.append(' ') for i, connection_color in enumerate(cable.colors, 1): - p = [] - p.append(f'') - p.append(wv_colors.translate_color(connection_color, self.color_mode)) - p.append(f'') - html = f'{html}' - for bla in p: - html = f'{html}' - html = f'{html}' + wirehtml.append(' ') + wirehtml.append(f' ') + wirehtml.append(f' ') + wirehtml.append(f' ') + wirehtml.append(' ') bgcolors = ['#000000'] + get_color_hex(connection_color, pad=pad) + ['#000000'] - html = f'{html}') + wirehtml.append(' ') if(cable.category == 'bundle'): # for bundles individual wires can have part information # create a list of wire parameters wireidentification = [] if isinstance(cable.pn, list): wireidentification.append(f'P/N: {cable.pn[i - 1]}') - manufacturer_info = manufacturer_info_field(cable.manufacturer[i - 1] if isinstance(cable.manufacturer, list) else None, - cable.mpn[i - 1] if isinstance(cable.mpn, list) else None) + manufacturer_info = manufacturer_info_field( + cable.manufacturer[i - 1] if isinstance(cable.manufacturer, list) else None, + cable.mpn[i - 1] if isinstance(cable.mpn, list) else None) if manufacturer_info: - wireidentification.append(manufacturer_info) + wireidentification.append(html_line_breaks(manufacturer_info)) # print parameters into a table row under the wire if(len(wireidentification) > 0): - html = f'{html}') if cable.shield: - p = ['', 'Shield', ''] - html = f'{html}' # spacer - html = f'{html}' - for bla in p: - html = html + f'' - html = f'{html}' + wirehtml.append(' ') # spacer + wirehtml.append(' ') + wirehtml.append(' ') + wirehtml.append(' ') + wirehtml.append(' ') + wirehtml.append(' ') if isinstance(cable.shield, str): # shield is shown with specified color and black borders shield_color_hex = wv_colors.get_color_hex(cable.shield)[0] @@ -236,18 +233,12 @@ def create_graph(self) -> Graph: else: # shield is shown as a thin black wire attributes = f'height="2" bgcolor="#000000" border="0"' - html = f'{html}' - - html = f'{html}' # spacer at the end - - html = f'{html}
 
{bla}
{wv_colors.translate_color(connection_color, self.color_mode)}
' + wirehtml.append(f' ') + wirehtml.append(f' ' + wirehtml.append(f' ') + wirehtml.append('
') + wirehtml.append(' ') for j, bgcolor in enumerate(bgcolors[::-1]): # Reverse to match the curved wires when more than 2 colors - html = f'{html}' - html = html + '
') + wirehtml.append('
' + wirehtml.append(' ' + wirehtml.append(f' ') + wirehtml.append('
') + wirehtml.append(' ') for attrib in wireidentification: - html = f'{html}' - html = f'{html}
{attrib}
{attrib}
') + wirehtml.append('
 
{bla}
 
Shield
 
' # conductor table + wirehtml.append(f'
{html_line_breaks(cable.notes)}
 
 
') - html = f'{html}' # main table + html = [row.replace('', '\n'.join(wirehtml)) for row in html] # connections for connection_color in cable.connections: @@ -262,16 +253,17 @@ def create_graph(self) -> Graph: code_left_2 = f'{cable.name}:w{connection_color.via_port}:w' dot.edge(code_left_1, code_left_2) from_string = f'{connection_color.from_name}:{connection_color.from_port}' if self.connectors[connection_color.from_name].show_name else '' - html = html.replace(f'', from_string) + html = [row.replace(f'', from_string) for row in html] if connection_color.to_port is not None: # connect to right code_right_1 = f'{cable.name}:w{connection_color.via_port}:e' to_port = f':p{connection_color.to_port}l' if self.connectors[connection_color.to_name].style != 'simple' else '' code_right_2 = f'{connection_color.to_name}{to_port}:w' dot.edge(code_right_1, code_right_2) to_string = f'{connection_color.to_name}:{connection_color.to_port}' if self.connectors[connection_color.to_name].show_name else '' - html = html.replace(f'', to_string) + html = [row.replace(f'', to_string) for row in html] - dot.node(cable.name, label=f'<{html}>', shape='box', + html = '\n'.join(html) + dot.node(cable.name, label=f'<\n{html}\n>', shape='box', style='filled,dashed' if cable.category == 'bundle' else '', margin='0', fillcolor='white') return dot @@ -355,7 +347,7 @@ def bom(self): conn_color = f', {shared.color}' if shared.color else '' name = f'Connector{conn_type}{conn_subtype}{conn_pincount}{conn_color}' item = {'item': name, 'qty': len(designators), 'unit': '', 'designators': designators if shared.show_name else '', - 'manufacturer': shared.manufacturer, 'mpn': shared.mpn, 'pn': shared.pn} + 'manufacturer': remove_line_breaks(shared.manufacturer), 'mpn': remove_line_breaks(shared.mpn), 'pn': shared.pn} bom_connectors.append(item) bom_connectors = sorted(bom_connectors, key=lambda k: k['item']) # https://stackoverflow.com/a/73050 bom.extend(bom_connectors) @@ -374,7 +366,7 @@ def bom(self): shield_name = ' shielded' if shared.shield else '' name = f'Cable{cable_type}, {shared.wirecount}{gauge_name}{shield_name}' item = {'item': name, 'qty': round(total_length, 3), 'unit': 'm', 'designators': designators, - 'manufacturer': shared.manufacturer, 'mpn': shared.mpn, 'pn': shared.pn} + 'manufacturer': remove_line_breaks(shared.manufacturer), 'mpn': remove_line_breaks(shared.mpn), 'pn': shared.pn} bom_cables.append(item) # bundles (ignores wirecount) wirelist = [] @@ -384,8 +376,8 @@ def bom(self): # add each wire from each bundle to the wirelist for index, color in enumerate(bundle.colors, 0): wirelist.append({'type': bundle.type, 'gauge': bundle.gauge, 'gauge_unit': bundle.gauge_unit, 'length': bundle.length, 'color': color, 'designator': bundle.name, - 'manufacturer': index_if_list(bundle.manufacturer, index), - 'mpn': index_if_list(bundle.mpn, index), + 'manufacturer': remove_line_breaks(index_if_list(bundle.manufacturer, index)), + 'mpn': remove_line_breaks(index_if_list(bundle.mpn, index)), 'pn': index_if_list(bundle.pn, index)}) # join similar wires from all the bundles to a single BOM item wire_group = lambda w: (w.get('type', None), w['gauge'], w['gauge_unit'], w['color'], w['manufacturer'], w['mpn'], w['pn']) diff --git a/src/wireviz/wv_helper.py b/src/wireviz/wv_helper.py index 42b03b8e..418060d2 100644 --- a/src/wireviz/wv_helper.py +++ b/src/wireviz/wv_helper.py @@ -34,18 +34,23 @@ def nested_html_table(rows): # input: list, each item may be scalar or list # 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 - html = '' + html = [] + html.append('
') for row in rows: if isinstance(row, List): if len(row) > 0 and any(row): - html = f'{html}') elif row is not None: - html = f'{html}' - html = f'{html}
' + html.append(' ' + html.append(f' ') + html.append('
') + html.append(' ') for cell in row: if cell is not None: - html = f'{html}' - html = f'{html}
{cell}
{cell}
') + html.append('
{row}
' + html.append(' ') + html.append(f' {row}') + html.append(' ') + html.append('') return html @@ -115,7 +120,7 @@ def graphviz_line_breaks(inp): return inp.replace('\n', '\\n') if isinstance(inp, str) else inp # \n generates centered new lines. http://www.graphviz.org/doc/info/attrs.html#k:escString def remove_line_breaks(inp): - return inp.replace('\n', ' ').rstrip() if isinstance(inp, str) else inp + return inp.replace('\n', ' ').strip() if isinstance(inp, str) else inp def open_file_read(filename): # TODO: Intelligently determine encoding