From 3e4353e62a20eff3582a73306819bee861e20581 Mon Sep 17 00:00:00 2001 From: Tobias Falk Date: Wed, 12 Mar 2025 22:38:52 +0100 Subject: [PATCH] Add Jumper code --- src/wireviz/wv_dataclasses.py | 51 +++++++++++++++----- src/wireviz/wv_graphviz.py | 88 ++++++++++++++++++++++++++++++++--- src/wireviz/wv_gvpr.gvpr | 64 +++++++++++++++++++++++++ src/wireviz/wv_harness.py | 53 ++++++++++++++++++--- src/wireviz/wv_utils.py | 7 +++ 5 files changed, 237 insertions(+), 26 deletions(-) create mode 100644 src/wireviz/wv_gvpr.gvpr diff --git a/src/wireviz/wv_dataclasses.py b/src/wireviz/wv_dataclasses.py index ff7fa4b..d5b0453 100644 --- a/src/wireviz/wv_dataclasses.py +++ b/src/wireviz/wv_dataclasses.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from collections import namedtuple -from dataclasses import dataclass, field +from dataclasses import dataclass, field, asdict from enum import Enum from itertools import zip_longest from typing import Any, Dict, List, Optional, Tuple, Union @@ -263,10 +263,14 @@ class AdditionalComponent(GraphicalComponent): explicit_qty: bool = True amount_computed: Optional[NumberAndUnit] = None note: str = None + color: Optional[MultiColor] = None + references: Optional[List[str]] = field(default_factory=list) def __post_init__(self): super().__post_init__() + self.color = MultiColor(self.color) + if isinstance(self.qty_multiplier, float) or isinstance( self.qty_multiplier, int ): @@ -307,7 +311,9 @@ class TopLevelGraphicalComponent(GraphicalComponent): # abstract class class Connector(TopLevelGraphicalComponent): # connector-specific properties style: Optional[str] = None - loops: List[List[Pin]] = field(default_factory=list) + # TODO: Move shorts and loops to PinClass + loops: Dict[str, List[int]] = field(default_factory=dict) + shorts: Dict[str, List[int]] = field(default_factory=dict) # pin information in particular pincount: Optional[int] = None pins: List[Pin] = field(default_factory=list) # legacy @@ -412,20 +418,41 @@ class Connector(TopLevelGraphicalComponent): # hide pincount for simple (1 pin) connectors by default self.show_pincount = self.style != "simple" - for loop in self.loops: - # TODO: allow using pin labels in addition to pin numbers, - # just like when defining regular connections - # TODO: include properties of wire used to create the loop - if len(loop) != 2: - raise Exception("Loops must be between exactly two pins!") - for pin in loop: + # TODO: allow using pin labels in addition to pin numbers, + # just like when defining regular connections + # TODO: include properties of wire used to create the loop + for loopName in self.loops: + for pin in self.loops[loopName]: if pin not in self.pins: raise Exception( - f'Unknown loop pin "{pin}" for connector "{self.name}"!' + f'Unknown loop pin "{pin}" for connector "{self.designator}"!' ) # Make sure loop connected pins are not hidden. - # side=None, determine side to show loops during rendering - self.activate_pin(pin, side=None, is_connection=True) + self.activate_pin(pin, None) + for short in self.shorts: + for pin in self.shorts[short]: + if pin not in self.pins: + raise Exception( + f'Unknown loop pin "{pin}" for connector "{self.designator}"!' + ) + # Make sure loop connected pins are not hidden. + self.activate_pin(pin, None) + + # TODO: Remove the outcommented code here if it is no longer needed as reference + # for loop in self.loops: + # # TODO: allow using pin labels in addition to pin numbers, + # # just like when defining regular connections + # # TODO: include properties of wire used to create the loop + # if len(loop) != 2: + # raise Exception("Loops must be between exactly two pins!") + # for pin in loop: + # if pin not in self.pins: + # raise Exception( + # f'Unknown loop pin "{pin}" for connector "{self.name}"!' + # ) + # # Make sure loop connected pins are not hidden. + # # side=None, determine side to show loops during rendering + # self.activate_pin(pin, side=None, is_connection=True) for i, item in enumerate(self.additional_components): if isinstance(item, dict): diff --git a/src/wireviz/wv_graphviz.py b/src/wireviz/wv_graphviz.py index 60d89ed..8c2238e 100644 --- a/src/wireviz/wv_graphviz.py +++ b/src/wireviz/wv_graphviz.py @@ -22,7 +22,7 @@ from wireviz.wv_dataclasses import ( WireClass, ) from wireviz.wv_html import Img, Table, Td, Tr -from wireviz.wv_utils import html_line_breaks, remove_links +from wireviz.wv_utils import html_line_breaks, remove_links, getAddCompFromRef def gv_node_component(component: Component) -> Table: @@ -259,8 +259,27 @@ def nested_table_dict(d: dict) -> Table: return Table(rows, border=0, cellborder=1, cellpadding=3, cellspacing=0) + +def gv_shorts_info_row(component) -> Tr: + shorts_info = [] + if component.ports_left: + shorts_info.append(Td(f'')) + if component.pinlabels: + shorts_info.append(Td(f'')) + + for short in component.shorts: + shorts_info.append(Td(f'{short}')) + + if component.ports_right: + shorts_info.append(Td(f'')) + return Tr(shorts_info) + def gv_pin_table(component) -> Table: pin_rows = [] + + if len(component.shorts) > 0: + pin_rows.append(gv_shorts_info_row(component)) + for pin in component.pin_objects.values(): if component.should_show_pin(pin.id): pin_rows.append(gv_pin_row(pin, component)) @@ -271,6 +290,16 @@ def gv_pin_table(component) -> Table: return tbl +def gv_short_row_part(pin, connector) -> List: + short_row = []# Td("ADA"), Td("DAD") + for short, shPins in connector.shorts.items(): + if pin.index+1 in shPins: + short_row.append(Td("", port=f"p{pin.index+1}j")) + else: + short_row.append(Td("")) + return short_row + + def gv_pin_row(pin, connector) -> Tr: # ports in GraphViz are 1-indexed for more natural maping to pin/wire numbers has_pincolors = any([_pin.color for _pin in connector.pin_objects.values()]) @@ -279,6 +308,7 @@ def gv_pin_row(pin, connector) -> Tr: Td(pin.label, delete_if_empty=True), Td(str(pin.color) if pin.color else "", sides="TBL") if has_pincolors else None, Td(color_minitable(pin.color), sides="TBR") if has_pincolors else None, + gv_short_row_part(pin, connector), Td(pin.id, port=f"p{pin.index+1}r") if connector.ports_right else None, ] return Tr(cells) @@ -294,13 +324,37 @@ def gv_connector_loops(connector: Connector) -> List: loop_dir = "e" else: raise Exception("No side for loops") - for loop in connector.loops: - head = f"{connector.designator}:p{loop[0]}{loop_side}:{loop_dir}" - tail = f"{connector.designator}:p{loop[1]}{loop_side}:{loop_dir}" - loop_edges.append((head, tail)) + + for loop, loPins in connector.loops.items(): + comp = getAddCompFromRef(loop, connector) + loColor = "#000000" + if comp != None and comp.color != None: + loColor = comp.color.html + + for i in range(1, len(loPins)): + head = f"{connector.designator}:p{loPins[i - 1]}{loop_side}:{loop_dir}" + tail = f"{connector.designator}:p{loPins[i]}{loop_side}:{loop_dir}" + loop_edges.append((head, tail, loColor)) return loop_edges +def gv_connector_shorts(connector: Connector) -> List: + short_edges = [] + + for short, shPins in connector.shorts.items(): + comp = getAddCompFromRef(short, connector) + shColor = "#000000" + if comp != None and comp.color != None: + shColor = comp.color.html + + for i in range(1, len(shPins)): + head = f"{connector.designator}:p{shPins[i - 1]}j:c" + tail = f"{connector.designator}:p{shPins[i]}j:c" + short_edges.append((head, tail, shColor)) + return short_edges + + + def gv_conductor_table(cable) -> Table: rows = [] rows.append(Tr(Td(" "))) # spacer row on top @@ -371,7 +425,7 @@ def gv_wire_cell(wire: Union[WireClass, ShieldClass], colspan: int) -> Td: wire_inner_rows = [] for j, bgcolor in enumerate(color_list[::-1]): wire_inner_cell_attribs = { - "bgcolor": bgcolor if bgcolor != "" else "#000000", + "bgcolor": "#FFFFFF", # bgcolor if bgcolor != "" else "#000000", # TODO: More elegent solution for making black/whit space needed, since the wire is drawn as an actual edge "border": 0, "cellpadding": 0, "colspan": colspan, @@ -392,8 +446,10 @@ def gv_wire_cell(wire: Union[WireClass, ShieldClass], colspan: int) -> Td: return wire_outer_cell + dot.attr("edge", headclip="true", tailclip="true", style="bold") # TODO: ? -def gv_edge_wire(harness, cable, connection) -> Tuple[str, str, str, str, str]: + # color, l1, l2, r1, r2 +def gv_edge_wire(harness, cable, connection) -> Tuple[str, str, str, str, str]: if connection.via.color: # check if it's an actual wire and not a shield color = f"#000000:{connection.via.color.html_padded}:#000000" @@ -425,6 +481,24 @@ def gv_edge_wire(harness, cable, connection) -> Tuple[str, str, str, str, str]: return color, code_left_1, code_left_2, code_right_1, code_right_2 + # color, we, ww, +def gv_edge_wire_inside(cable) -> List[Tuple[str, str, str]]: + wires = [] + # print(cable.wire_objects) + for wire in cable.wire_objects.values(): + color = "#000000" + if wire.color: + # check if it's an actual wire and not a shield + color = f"#000000:{wire.color.html_padded}:#000000" + else: # it's a shield connection + color = "#000000" + + we = f"{wire.parent}:w{wire.index+1}:e" + ww = f"{wire.parent}:w{wire.index+1}:w" + + wires.append([color, we, ww]) + return wires + def parse_arrow_str(inp: str) -> ArrowDirection: if inp[0] == "<" and inp[-1] == ">": diff --git a/src/wireviz/wv_gvpr.gvpr b/src/wireviz/wv_gvpr.gvpr new file mode 100644 index 0000000..7bfced7 --- /dev/null +++ b/src/wireviz/wv_gvpr.gvpr @@ -0,0 +1,64 @@ +/******************************************************************* + + see https://forum.graphviz.org/t/straitening-one-line-throu-a-table/2196 and https://forum.graphviz.org/t/way-of-drawing-a-black-circle-inside-a-table-field/2273/12 + input must include pos values (must be output from one of the engines w/ -Tdot)# + Thanks to steveroush and FeRDNYC + +*******************************************************************/ +BEG_G{ + double x1,y1,x2,y2,x3,y3,x4,y4; + string ptSize, tok[int], pt[]; + int cnt, circ, i; + node_t aNode; + + circ=0; + +/*************************************** + $G.bb=""; + $G.nodesep=""; + $G.ranksep=""; + $G.splines="true"; +****************************************/ +} + +// This removes the label text but keeps the position +E[noLabel] { + $.label=""; // remove pesky label + // $.lp=""; // remove peskier label pos +} + +E[straight] { + cnt=tokens($.pos,tok," "); + $.oldpos=$.pos; + x1 = xOf(tok[0]); + y1 = yOf(tok[0]); + x4 = xOf(tok[cnt-1]); + y4 = yOf(tok[cnt-1]); + x2 = x1 + (x4-x1)/3.; + y2 = y1 + (y4-y1)/3.; + x3 = x1 + 2.*(x4-x1)/3.; + y3 = y1 + 2.*(y4-y1)/3.; + pos=sprintf("%.3f,%.3f %.3f,%.3f %.3f,%.3f %.3f,%.3f", x1,y1, x2,y2, x3,y3, x4,y4); + $.label=""; // remove pesky label + $.lp=""; // remove peskier label pos + + if (hasAttr($, "addPTS") && $.addPTS!="" && $.colorPTS!=""){ + // now we place point nodes at the edge ends + pt[1] = tok[0]; + pt[2] = tok[cnt-1]; + ptSize=$.addPTS; + for (pt[i]) { + if (i==2 && pt[1]==pt[2]) + continue; + aNode=node($G, "__CIRCLE__" + (string)++circ); + aNode.pos=pt[i]; + aNode.shape="point"; + aNode.width=ptSize; + aNode.height=ptSize; + aNode.style="filled"; + aNode.fillcolor=$.colorPTS; + aNode.color=$.colorPTS; + } + } + +} \ No newline at end of file diff --git a/src/wireviz/wv_harness.py b/src/wireviz/wv_harness.py index 95e920e..d90575f 100644 --- a/src/wireviz/wv_harness.py +++ b/src/wireviz/wv_harness.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- +import os +import shutil from collections import defaultdict -from dataclasses import dataclass, field +from dataclasses import dataclass, field, asdict from pathlib import Path from typing import List, Union +from distutils.spawn import find_executable from graphviz import Graph @@ -24,13 +27,16 @@ from wireviz.wv_dataclasses import ( Side, TopLevelGraphicalComponent, Tweak, + Image, ) from wireviz.wv_graphviz import ( apply_dot_tweaks, calculate_node_bgcolor, gv_connector_loops, + gv_connector_shorts, gv_edge_mate, gv_edge_wire, + gv_edge_wire_inside, gv_node_component, parse_arrow_str, set_dot_basics, @@ -40,8 +46,7 @@ from wireviz.wv_output import ( embed_svg_images_file, generate_html_output, ) -from wireviz.wv_utils import OLD_CONNECTOR_ATTR, bom2tsv, check_old, file_write_text - +from wireviz.wv_utils import bom2tsv, open_file_write, getAddCompFromRef @dataclass class Harness: @@ -322,9 +327,20 @@ class Harness: if len(connector.loops) > 0: dot.attr("edge", color="#000000") loops = gv_connector_loops(connector) - for head, tail in loops: - dot.edge(head, tail, label=" ") - # ^ workaround to avoid oversized loops + for head, tail, color in loops: + dot.edge(head, tail, color = color, label = " ", noLabel="noLabel") + + # generate edges for connector shorts + if len(connector.shorts) > 0: + dot.attr("edge", color="#000000") + shorts = gv_connector_shorts(connector) + for head, tail, color in shorts: + dot.edge(head, tail, + color=color, + straight="straight", + addPTS=".18", # Size of the point at the end of the straight line/edge, it also enables the drawing of it + colorPTS=color, + headclip="false", tailclip="false") # 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 @@ -360,6 +376,9 @@ class Harness: if not (r1, r2) == (None, None): dot.edge(r1, r2) + for color, we, ww in gv_edge_wire_inside(cable): + if not (we, ww) == (None, None): + dot.edge(we, ww, color=color, straight="straight") for mate in self.mates: color, dir, code_from, code_to = gv_edge_mate(mate) @@ -394,6 +413,22 @@ class Harness: def svg(self): # TODO?: Verify xml encoding="utf-8" in SVG? graph = self.graph return embed_svg_images(graph.pipe(format="svg").decode("utf-8"), Path.cwd()) + + def graphRender(self, type, filename, graph): + # Chack if the needed commands are existing + if find_executable("dot") and find_executable("gvpr") and find_executable("neato"): + # Set enviorments variable to path of this file + os.environ['GVPRPATH'] = str(Path(__file__).parent) + # Export the gv output to a temporay file + graph.save(filename=f"{filename}_tmp.gv") + # Run the vomand and generait the output + os.system(f"dot {filename}_tmp.gv | gvpr -q -cf wv_gvpr.gvpr | neato -n2 -T{type} -o {filename}.{type}") + # Remove the temporary file + os.remove(f"{filename}_tmp.gv") + else: + print('The "dot", "gvpr" and "neato" comand where not found on the system, use old methode of generaiton, this may lead to not wanted output.') + graph.render(filename=filename) # old rendering methode, befor jumper implementations + def output( self, @@ -412,13 +447,17 @@ class Harness: _filename = f"{filename}.tmp" if f == "svg" else filename # TODO: prevent rendering SVG twice when both SVG and HTML are specified graph.format = f - graph.render(filename=_filename, view=view, cleanup=cleanup) + self.graphRender(f, _filename, graph) # embed images into SVG output if "svg" in fmt or "html" in fmt: embed_svg_images_file(f"{filename}.tmp.svg") # GraphViz output if "gv" in fmt: graph.save(filename=f"{filename}.gv") + # Print the needed comand for generaitong an output + filename_str = str(filename) + shutil.copyfile(str(Path(__file__).parent).replace('\\', '/') + "/wv_gvpr.gvpr", filename_str + "_wv_gvpr.gvpr") + print(f"Use: dot {filename_str}.gv | gvpr -q -cf {filename_str}_wv_gvpr.gvpr | neato -n2 -T -o {filename_str}.") # BOM output bomlist = bom_list(self.bom) # bomlist = [[]] diff --git a/src/wireviz/wv_utils.py b/src/wireviz/wv_utils.py index 1d364e3..abf906d 100644 --- a/src/wireviz/wv_utils.py +++ b/src/wireviz/wv_utils.py @@ -241,3 +241,10 @@ def check_old(node: str, old_attr: dict, args: dict) -> None: for attr, descr in old_attr.items(): if attr in args: raise ValueError(f"'{attr}' in {node}: '{attr}' {descr}") + +# Returns a Additional Component from with the given +def getAddCompFromRef(reference, part): + #print(part.additional_components) + for comp in part.additional_components: + if reference in comp.references: + return comp; \ No newline at end of file