Add Jumper code

This commit is contained in:
Tobias Falk 2025-03-12 22:38:52 +01:00
parent 206be6bbed
commit 3e4353e62a
5 changed files with 237 additions and 26 deletions

View File

@ -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):

View File

@ -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] == ">":

64
src/wireviz/wv_gvpr.gvpr Normal file
View File

@ -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;
}
}
}

View File

@ -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)
@ -395,6 +414,22 @@ class Harness:
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,
filename: Union[str, Path],
@ -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<type> -o {filename_str}.<type>")
# BOM output
bomlist = bom_list(self.bom)
# bomlist = [[]]

View File

@ -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 <part> with the given <reference>
def getAddCompFromRef(reference, part):
#print(part.additional_components)
for comp in part.additional_components:
if reference in comp.references:
return comp;