Use color objects in WireViz

This commit is contained in:
Daniel Rojas 2021-10-20 20:14:10 +02:00 committed by Laurier Loiselle
parent c23430e4a2
commit c2fb59f937
No known key found for this signature in database
GPG Key ID: 345920CC72089A3F
7 changed files with 313 additions and 462 deletions

View File

@ -5,7 +5,13 @@ from enum import Enum
from itertools import zip_longest
from typing import Dict, List, Optional, Tuple, Union
from wireviz.wv_colors import COLOR_CODES, Color, ColorMode, Colors, ColorScheme
from wireviz.wv_colors import (
COLOR_CODES,
ColorOutputMode,
MultiColor,
SingleColor,
get_color_by_colorcode_index,
)
from wireviz.wv_helper import aspect_ratio, awg_equiv, mm2_equiv
# Each type alias have their legal values described in comments - validation might be implemented in the future
@ -60,12 +66,12 @@ class Metadata(dict):
@dataclass
class Options:
fontname: PlainText = "arial"
bgcolor: Color = "WH"
bgcolor_node: Optional[Color] = "WH"
bgcolor_connector: Optional[Color] = None
bgcolor_cable: Optional[Color] = None
bgcolor_bundle: Optional[Color] = None
color_mode: ColorMode = "SHORT"
bgcolor: SingleColor = "WH" # will be converted to SingleColor in __post_init__
bgcolor_node: SingleColor = "WH"
bgcolor_connector: SingleColor = None
bgcolor_cable: SingleColor = None
bgcolor_bundle: SingleColor = None
color_mode: ColorOutputMode = ColorOutputMode.EN_UPPER
mini_bom_mode: bool = True
template_separator: str = "."
_pad: int = 0
@ -74,6 +80,13 @@ class Options:
_image_paths: [List] = field(default_factory=list)
def __post_init__(self):
self.bgcolor = SingleColor(self.bgcolor)
self.bgcolor_node = SingleColor(self.bgcolor_node)
self.bgcolor_connector = SingleColor(self.bgcolor_connector)
self.bgcolor_cable = SingleColor(self.bgcolor_cable)
self.bgcolor_bundle = SingleColor(self.bgcolor_bundle)
if not self.bgcolor_node:
self.bgcolor_node = self.bgcolor
if not self.bgcolor_connector:
@ -99,13 +112,15 @@ class Image:
width: Optional[int] = None
height: Optional[int] = None
fixedsize: Optional[bool] = None
bgcolor: Optional[Color] = None
bgcolor: SingleColor = None
# Contents of the text cell <td> just below the image cell:
caption: Optional[MultilineHypertext] = None
# See also HTML doc at https://graphviz.org/doc/info/shapes.html#html
def __post_init__(self):
self.bgcolor = SingleColor(self.bgcolor)
if self.fixedsize is None:
# Default True if any dimension specified unless self.scale also is specified.
self.fixedsize = (self.width or self.height) and self.scale is None
@ -141,7 +156,10 @@ class AdditionalComponent:
qty: float = 1
unit: Optional[str] = None
qty_multiplier: Union[ConnectorMultiplier, CableMultiplier, None] = None
bgcolor: Optional[Color] = None
bgcolor: SingleColor = None
def __post_init__(self):
self.bgcolor = SingleColor(self.bgcolor)
@property
def description(self) -> str:
@ -159,7 +177,7 @@ class PinClass:
index: int
id: str
label: str
color: str
color: MultiColor
parent: str # designator of parent connector
_anonymous: bool = False # true for pins on autogenerated connectors
_simple: bool = False # true for simple connector
@ -178,7 +196,7 @@ class WireClass:
index: int
id: str
label: str
color: str
color: MultiColor
parent: str # designator of parent cable/bundle
# gauge: Gauge
# pn: str
@ -203,8 +221,8 @@ class Connection:
@dataclass
class Connector(Component):
name: Designator
bgcolor: Optional[Color] = None
bgcolor_title: Optional[Color] = None
bgcolor: SingleColor = None
bgcolor_title: SingleColor = None
manufacturer: Optional[MultilineHypertext] = None
mpn: Optional[MultilineHypertext] = None
supplier: Optional[MultilineHypertext] = None
@ -219,8 +237,8 @@ class Connector(Component):
notes: Optional[MultilineHypertext] = None
pins: List[Pin] = field(default_factory=list)
pinlabels: List[Pin] = field(default_factory=list)
pincolors: List[Color] = field(default_factory=list)
color: Optional[Color] = None
pincolors: List[str] = field(default_factory=list)
color: MultiColor = None
show_name: Optional[bool] = None
show_pincount: Optional[bool] = None
hide_disconnected_pins: bool = False
@ -238,6 +256,10 @@ class Connector(Component):
def __post_init__(self) -> None:
self.bgcolor = SingleColor(self.bgcolor)
self.bgcolor_title = SingleColor(self.bgcolor_title)
self.color = SingleColor(self.color)
if isinstance(self.image, dict):
self.image = Image(**self.image)
@ -280,7 +302,7 @@ class Connector(Component):
index=pin_index,
id=pin_id,
label=pin_label,
color=pin_color,
color=MultiColor(pin_color),
parent=self.name,
_anonymous=self.is_autogenerated,
_simple=self.style == "simple",
@ -341,8 +363,8 @@ class Connector(Component):
@dataclass
class Cable(Component):
name: Designator
bgcolor: Optional[Color] = None
bgcolor_title: Optional[Color] = None
bgcolor: SingleColor = None
bgcolor_title: SingleColor = None
manufacturer: Union[MultilineHypertext, List[MultilineHypertext], None] = None
mpn: Union[MultilineHypertext, List[MultilineHypertext], None] = None
supplier: Union[MultilineHypertext, List[MultilineHypertext], None] = None
@ -355,14 +377,14 @@ class Cable(Component):
show_equiv: bool = False
length: float = 0
length_unit: Optional[str] = None
color: Optional[Color] = None
color: MultiColor = None
wirecount: Optional[int] = None
shield: Union[bool, Color] = False
shield: Union[bool, MultiColor] = False
image: Optional[Image] = None
notes: Optional[MultilineHypertext] = None
colors: List[Colors] = field(default_factory=list)
colors: List[str] = field(default_factory=list)
wirelabels: List[Wire] = field(default_factory=list)
color_code: Optional[ColorScheme] = None
color_code: Optional[str] = None
show_name: Optional[bool] = None
show_wirecount: bool = True
show_wirenumbers: Optional[bool] = None
@ -393,6 +415,10 @@ class Cable(Component):
def __post_init__(self) -> None:
self.bgcolor = SingleColor(self.bgcolor)
self.bgcolor_title = SingleColor(self.bgcolor_title)
self.color = SingleColor(self.color)
if isinstance(self.image, dict):
self.image = Image(**self.image)
@ -441,21 +467,20 @@ class Cable(Component):
if self.wirecount: # number of wires explicitly defined
if self.colors: # use custom color palette (partly or looped if needed)
pass
self.colors = [
self.colors[i % len(self.colors)] for i in range(self.wirecount)
]
elif self.color_code:
# use standard color palette (partly or looped if needed)
if self.color_code not in COLOR_CODES:
raise Exception("Unknown color code")
self.colors = COLOR_CODES[self.color_code]
self.colors = [
get_color_by_colorcode_index(self.color_code, i)
for i in range(self.wirecount)
]
else: # no colors defined, add dummy colors
self.colors = [""] * self.wirecount
# make color code loop around if more wires than colors
if self.wirecount > len(self.colors):
m = self.wirecount // len(self.colors) + 1
self.colors = self.colors * int(m)
# cut off excess after looping
self.colors = self.colors[: self.wirecount]
else: # wirecount implicit in length of color list
if not self.colors:
raise Exception(
@ -491,7 +516,7 @@ class Cable(Component):
index=wire_index, # TODO: wire_id
id=wire_index + 1, # TODO: wire_id
label=wire_label,
color=wire_color,
color=MultiColor(wire_color),
parent=self.name,
)
)
@ -504,7 +529,9 @@ class Cable(Component):
index=index_offset,
id="s",
label="Shield",
color=self.shield if isinstance(self.shield, str) else None,
color=MultiColor(self.shield)
if isinstance(self.shield, str)
else MultiColor(None),
parent=self.name,
)
)

View File

@ -5,6 +5,7 @@ from pathlib import Path
from graphviz import Graph
import wireviz.wv_colors
from wireviz.DataClasses import (
Arrow,
ArrowWeight,
@ -152,7 +153,7 @@ class Harness:
for connector in self.connectors.values():
# generate connector node
gv_html = gv_node_component(connector, self.options)
gv_html = gv_node_component(connector)
bgcolor = calculate_node_bgcolor(connector, self.options)
dot.node(
connector.name,
@ -163,25 +164,27 @@ class Harness:
)
# generate edges for connector loops
if len(connector.loops) > 0:
dot.attr("edge", color="#000000:#ffffff:#000000")
dot.attr("edge", color="#000000")
loops = gv_connector_loops(connector)
for head, tail in loops:
dot.edge(head, tail)
# 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
multicolor_wires = [
len(wire.color) > 1
for cable in self.cables.values()
for colorstr in cable.colors
)
self.options._pad = pad
for wire in cable.wire_objects
]
if any(multicolor_wires):
wireviz.wv_colors.padding_amount = 3
else:
wireviz.wv_colors.padding_amount = 1
for cable in self.cables.values():
# generate cable node
# TODO: PN info for bundles (per wire)
gv_html = gv_node_component(cable, self.options)
gv_html = gv_node_component(cable)
bgcolor = calculate_node_bgcolor(cable, self.options)
style = "filled,dashed" if cable.category == "bundle" else "filled"
dot.node(

View File

@ -4,8 +4,7 @@ from dataclasses import asdict
from itertools import groupby
from typing import Any, Dict, List, Optional, Tuple, Union
from wireviz.DataClasses import AdditionalComponent, Cable, Color, Connector
from wireviz.wv_colors import translate_color
from wireviz.DataClasses import AdditionalComponent, Cable, Connector
from wireviz.wv_gv_html import html_line_breaks
from wireviz.wv_helper import clean_whitespace, pn_info_string
@ -99,7 +98,7 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]:
+ (f", {connector.subtype}" if connector.subtype else "")
+ (f", {connector.pincount} pins" if connector.show_pincount else "")
+ (
f", {translate_color(connector.color, harness.options.color_mode)}"
f", xxx" # {translate_color(connector.color, harness.options.color_mode)}
if connector.color
else ""
)
@ -132,7 +131,7 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]:
)
+ (" shielded" if cable.shield else "")
+ (
f", {translate_color(cable.color, harness.options.color_mode)}"
f", xxx" # {translate_color(cable.color, harness.options.color_mode)}
if cable.color
else ""
)
@ -154,7 +153,7 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]:
+ (f", {cable.type}" if cable.type else "")
+ (f", {cable.gauge} {cable.gauge_unit}" if cable.gauge else "")
+ (
f", {translate_color(color, harness.options.color_mode)}"
f", xxx" # {translate_color(color, harness.options.color_mode)}
if color
else ""
)
@ -236,7 +235,7 @@ def component_table_entry(
type: str,
qty: Union[int, float],
unit: Optional[str] = None,
bgcolor: Optional[Color] = None,
bgcolor: Optional[str] = None,
pn: Optional[str] = None,
manufacturer: Optional[str] = None,
mpn: Optional[str] = None,

View File

@ -1,6 +1,206 @@
# -*- coding: utf-8 -*-
from typing import Dict, List
from collections import namedtuple
from dataclasses import dataclass, field
from enum import Enum
from typing import List
KnownColor = namedtuple("KnownColor", "html code_de full_en full_de")
ColorOutputMode = Enum(
"ColorOutputMode", "EN_LOWER EN_UPPER DE_LOWER DE_UPPER HTML_LOWER HTML_UPPER"
)
known_colors = { # v--------v--------- for future use
"BK": KnownColor("#000000", "sw", "black", "schwarz"),
"WH": KnownColor("#ffffff", "ws", "white", "weiß"),
"GY": KnownColor("#999999", "gr", "grey", "grau"),
"PK": KnownColor("#ff66cc", "rs", "pink", "rosa"),
"RD": KnownColor("#ff0000", "rt", "red", "rot"),
"OG": KnownColor("#ff8000", "or", "orange", "orange"),
"YE": KnownColor("#ffff00", "ge", "yellow", "gelb"),
"OL": KnownColor("#708000", "ol", "olive green", "olivgrün"),
"GN": KnownColor("#00ff00", "gn", "green", "grün"),
"TQ": KnownColor("#00ffff", "tk", "turquoise", "türkis"),
"LB": KnownColor("#a0dfff", "hb", "light blue", "hellblau"),
"BU": KnownColor("#0066ff", "bl", "blue", "blau"),
"VT": KnownColor("#8000ff", "vi", "violet", "violett"),
"BN": KnownColor("#895956", "br", "brown", "braun"),
"BG": KnownColor("#ceb673", "bg", "beige", "beige"),
"IV": KnownColor("#f5f0d0", "eb", "ivory", "elfenbein"),
"SL": KnownColor("#708090", "si", "slate", "schiefer"),
"CU": KnownColor("#d6775e", "ku", "copper", "Kupfer"),
"SN": KnownColor("#aaaaaa", "vz", "tin", "verzinkt"),
"SR": KnownColor("#84878c", "ag", "silver", "Silber"),
"GD": KnownColor("#ffcf80", "au", "gold", "Gold"),
}
color_output_mode = ColorOutputMode.EN_UPPER
padding_amount = 1
def convert_case(inp):
if "_LOWER" in color_output_mode.name:
return inp.lower()
elif "_UPPER" in color_output_mode.name:
return inp.upper()
else: # currently not used
return inp
def get_color_by_colorcode_index(color_code: str, index: int) -> str:
num_colors_in_code = len(COLOR_CODES[color_code])
actual_index = index % num_colors_in_code # wrap around if index is out of bounds
return COLOR_CODES[color_code][actual_index]
# # make color code loop around if more wires than colors
# if self.wirecount > len(self.colors):
# m = self.wirecount // len(self.colors) + 1
# self.colors = self.colors * int(m)
# # cut off excess after looping
# self.colors = self.colors[: self.wirecount]
@dataclass
class SingleColor:
_code_en: str
_html: str
@property
def code_en(self):
return convert_case(self._code_en) if self._code_en else None
@property
def code_de(self):
return (
convert_case(known_colors[self._code_en.upper()].code_de)
if self._code_en
else None
)
@property
def html(self):
return convert_case(self._html) if self._code_en else None
@property
def known(self):
return (
self.code_en.upper() in known_colors.keys() if self._code_en else True
) # ?
def __init__(self, inp):
if inp is None:
self._html = None
self._code_en = None
elif isinstance(inp, int):
hex_str = f"#{inp:06x}"
self._html = hex_str
self._code_en = hex_str # do not perform reverse lookup
elif inp.upper() in known_colors.keys():
inp_upper = inp.upper()
self._code_en = inp_upper
self._html = known_colors[inp_upper].html
else: # assume valid HTML color
self._html = inp
self._code_en = inp
@property
def html_padded(self):
return ":".join([self.html] * padding_amount)
def __bool__(self):
return self._code_en is not None
def __str__(self):
if self._html is None:
return ""
elif self.known and "EN_" in color_output_mode.name:
return self.code_en
elif self.known and "DE_" in color_output_mode.name:
return self.code_de
else:
return self.html
@dataclass
class MultiColor:
colors: List[SingleColor] = field(default_factory=list)
def __init__(self, inp):
self.colors = []
if inp is None:
pass
elif isinstance(inp, List): # input is already a list
for item in inp:
if item is None:
pass
elif isinstance(item, SingleColor):
self.colors.append(item)
else: # string
self.colors.append(SingleColor(item))
elif isinstance(inp, SingleColor): # single color
self.colors = [inp]
else: # split input into list
if ":" in str(inp):
self.colors = [SingleColor(item) for item in inp.split(":")]
else:
if isinstance(inp, int):
self.colors = [SingleColor(inp)]
elif len(inp) % 2 == 0:
items = [inp[i : i + 2] for i in range(0, len(inp), 2)]
known = [item.upper() in known_colors.keys() for item in items]
if all(known):
self.colors = [SingleColor(item) for item in items]
else: # assume it's a HTML color name
self.colors = [SingleColor(inp)]
else: # assume it's a HTML color name
self.colors = [SingleColor(inp)]
def __len__(self):
return len(self.colors)
def __bool__(self):
return len(self.colors) >= 1
def __str__(self):
if "EN_" in color_output_mode.name or "DE_" in color_output_mode.name:
joiner = "" if self.all_known else ":"
elif "HTML_" in color_output_mode.name:
joiner = ":"
else:
joiner = "???"
return joiner.join([str(color) for color in self.colors])
@property
def all_known(self):
return all([color.known for color in self.colors])
@property
def html(self):
return ":".join([color.html for color in self.colors])
@property
def html_padded_list(self):
# padding only properly works for padding_amount 1 or 3
if padding_amount == 1:
out = [color.html for color in self.colors]
elif len(self) == 0:
out = []
elif len(self) == 1:
out = [self.colors[0].html for i in range(3)]
elif len(self) == 2:
out = [self.colors[0].html, self.colors[1].html, self.colors[0].html]
elif len(self) == 3:
out = [color.html for color in self.colors]
else:
raise Exception(f"Padding not supported for len {len(selfq)}")
return [str(color) for color in out]
@property
def html_padded(self):
return ":".join(self.html_padded_list)
COLOR_CODES = {
# fmt: off
@ -38,166 +238,3 @@ COLOR_CODES = {
"T568A": ["WHGN", "GN", "WHOG", "BU", "WHBU", "OG", "WHBN", "BN"],
"T568B": ["WHOG", "OG", "WHGN", "BU", "WHBU", "GN", "WHBN", "BN"],
}
# Convention: Color names should be 2 letters long, to allow for multicolored wires
_color_hex = {
"BK": "#000000",
"WH": "#ffffff",
"GY": "#999999",
"PK": "#ff66cc",
"RD": "#ff0000",
"OG": "#ff8000",
"YE": "#ffff00",
"OL": "#708000", # olive green
"GN": "#00ff00",
"TQ": "#00ffff",
"LB": "#a0dfff", # light blue
"BU": "#0066ff",
"VT": "#8000ff",
"BN": "#895956",
"BG": "#ceb673", # beige
"IV": "#f5f0d0", # ivory
"SL": "#708090",
"CU": "#d6775e", # Faux-copper look, for bare CU wire
"SN": "#aaaaaa", # Silvery look for tinned bare wire
"SR": "#84878c", # Darker silver for silvered wire
"GD": "#ffcf80", # Golden color for gold
}
_color_full = {
"BK": "black",
"WH": "white",
"GY": "grey",
"PK": "pink",
"RD": "red",
"OG": "orange",
"YE": "yellow",
"OL": "olive green",
"GN": "green",
"TQ": "turquoise",
"LB": "light blue",
"BU": "blue",
"VT": "violet",
"BN": "brown",
"BG": "beige",
"IV": "ivory",
"SL": "slate",
"CU": "copper",
"SN": "tin",
"SR": "silver",
"GD": "gold",
}
_color_ger = {
"BK": "sw",
"WH": "ws",
"GY": "gr",
"PK": "rs",
"RD": "rt",
"OG": "or",
"YE": "ge",
"OL": "ol", # olivgrün
"GN": "gn",
"TQ": "tk",
"LB": "hb", # hellblau
"BU": "bl",
"VT": "vi",
"BN": "br",
"BG": "bg", # beige
"IV": "eb", # elfenbeinfarben
"SL": "si", # Schiefer
"CU": "ku", # Kupfer
"SN": "vz", # verzinkt
"SR": "ag", # Silber
"GD": "au", # Gold
}
color_default = "#ffffff"
_hex_digits = set("0123456789abcdefABCDEF")
# Literal type aliases below are commented to avoid requiring python 3.8
Color = str # Two-letter color name = Literal[_color_hex.keys()]
Colors = str # One or more two-letter color names (Color) concatenated into one string
ColorMode = (
str # = Literal['full', 'FULL', 'hex', 'HEX', 'short', 'SHORT', 'ger', 'GER']
)
ColorScheme = str # Color scheme name = Literal[COLOR_CODES.keys()]
def get_color_hex(input: Colors, pad: bool = False) -> List[str]:
"""Return list of hex colors from either a string of color names or :-separated hex colors."""
if input is None or input == "":
return [color_default]
elif input[0] == "#": # Hex color(s)
output = input.split(":")
for i, c in enumerate(output):
if c[0] != "#" or not all(d in _hex_digits for d in c[1:]):
if c != input:
c += f" in input: {input}"
print(f"Invalid hex color: {c}")
output[i] = color_default
else: # Color name(s)
def lookup(c: str) -> str:
try:
return _color_hex[c]
except KeyError:
if c != input:
c += f" in input: {input}"
print(f"Unknown color name: {c}")
return color_default
output = [lookup(input[i : i + 2]) for i in range(0, len(input), 2)]
if len(output) == 2: # Give wires with EXACTLY 2 colors that striped look.
output += output[:1]
elif pad and len(output) == 1: # Hacky style fix: Give single color wires
output *= 3 # a triple-up so that wires are the same size.
return output
def get_color_translation(translate: Dict[Color, str], input: Colors) -> List[str]:
"""Return list of colors translations from either a string of color names or :-separated hex colors."""
def from_hex(hex_input: str) -> str:
for color, hex in _color_hex.items():
if hex == hex_input:
return translate[color]
return f'({",".join(str(int(hex_input[i:i+2], 16)) for i in range(1, 6, 2))})'
return (
[from_hex(h) for h in input.lower().split(":")]
if input[0] == "#"
else [translate.get(input[i : i + 2], "??") for i in range(0, len(input), 2)]
)
def translate_color(input: Colors, color_mode: ColorMode) -> str:
if input == "":
return ""
if input is None:
return None
upper = color_mode.isupper()
if not (color_mode.isupper() or color_mode.islower()):
raise Exception("Unknown color mode capitalization")
color_mode = color_mode.lower()
if color_mode == "full":
output = "/".join(get_color_translation(_color_full, input))
elif color_mode == "hex":
output = ":".join(get_color_hex(input, pad=False))
elif color_mode == "ger":
output = "".join(get_color_translation(_color_ger, input))
elif color_mode == "short":
output = input
else:
raise Exception("Unknown color mode")
if upper:
return output.upper()
else:
return output.lower()

View File

@ -1,199 +0,0 @@
# -*- coding: utf-8 -*-
from collections import namedtuple
from dataclasses import dataclass, field
from enum import Enum
from typing import List
KnownColor = namedtuple("KnownColor", "html code_de full_en full_de")
ColorOutputMode = Enum("ColorOutputMode", "EN_LOWER EN_UPPER DE_LOWER DE_UPPER HTML_LOWER HTML_UPPER")
known_colors = {
"BK": KnownColor("#000000", "sw", "black", "schwarz"),
"WH": KnownColor("#ffffff", "ws", "white", "weiß"),
"GY": KnownColor("#999999", "gr", "grey", "grau"),
"PK": KnownColor("#ff66cc", "rs", "pink", "rosa"),
"RD": KnownColor("#ff0000", "rt", "red", "rot"),
"OG": KnownColor("#ff8000", "or", "orange", "orange"),
"YE": KnownColor("#ffff00", "ge", "yellow", "gelb"),
"OL": KnownColor("#708000", "ol", "olive green", "olivgrün"),
"GN": KnownColor("#00ff00", "gn", "green", "grün"),
"TQ": KnownColor("#00ffff", "tk", "turquoise", "türkis"),
"LB": KnownColor("#a0dfff", "hb", "light blue", "hellblau"),
"BU": KnownColor("#0066ff", "bl", "blue", "blau"),
"VT": KnownColor("#8000ff", "vi", "violet", "violett"),
"BN": KnownColor("#895956", "br", "brown", "braun"),
"BG": KnownColor("#ceb673", "bg", "beige", "beige"),
"IV": KnownColor("#f5f0d0", "eb", "ivory", "elfenbein"),
"SL": KnownColor("#708090", "si", "slate", "schiefer"),
"CU": KnownColor("#d6775e", "ku", "copper", "Kupfer"),
"SN": KnownColor("#aaaaaa", "vz", "tin", "verzinkt"),
"SR": KnownColor("#84878c", "ag", "silver", "Silber"),
"GD": KnownColor("#ffcf80", "au", "gold", "Gold"),
}
color_output_mode = ColorOutputMode.EN_UPPER
padding_amount = 1
def convert_case(inp):
if "_LOWER" in color_output_mode.name:
return inp.lower()
elif "_UPPER" in color_output_mode.name:
return inp.upper()
else: # currently not used
return inp
@dataclass
class SingleColor():
_code_en: str
_code_de: str
_html: str
@property
def code_en(self):
return convert_case(self._code_en)
# repeat for _code_de, _html
@property
def code_de(self):
return convert_case(self._code_de)
# repeat for _code_de, _html
@property
def html(self):
return convert_case(self._html)
# repeat for _code_de, _html
@property
def known(self):
return self.code_en.upper() in known_colors.keys()
def __init__(self, inp):
if isinstance(inp, int):
hex_str = f"#{inp:06x}"
self._html = hex_str
self._code_en = hex_str # do not perform reverse lookup
self._code_de = hex_str
elif inp.upper() in known_colors.keys():
inp_upper = inp.upper()
self._code_en = inp_upper
self._code_de = known_colors[inp_upper].code_de
self._html = known_colors[inp_upper].html
else: # assume valid HTML color
self._html = inp
self._code_en = inp
self._code_de = inp
@property
def html_padded(self):
return ":".join([self.html] * padding_amount)
def __str__(self):
if self.known and "EN_" in color_output_mode.name:
return self.code_en
elif self.known and "DE_" in color_output_mode.name:
return self.code_de
else:
return self.html
@dataclass
class MultiColor():
colors: List[SingleColor] = field(default_factory=list)
def __init__(self, inp):
self.colors = []
if isinstance(inp, List): # input is already a list
for item in inp:
if isinstance(item, SingleColor):
self.colors.append(item)
else: # string
self.colors.append(SingleColor(item))
elif isinstance(inp, SingleColor): # single color
self.colors = [inp]
else: # split input into list
if ":" in str(inp):
self.colors = [SingleColor(item) for item in inp.split(":")]
else:
if isinstance(inp, int):
self.colors = [SingleColor(inp)]
elif len(inp) % 2 == 0:
items = [inp[i:i+2] for i in range(0, len(inp), 2)]
known = [item.upper() in known_colors.keys() for item in items]
if all(known):
self.colors = [SingleColor(item) for item in items]
else: # assume it's a HTML color name
self.colors = [SingleColor(inp)]
else: # assume it's a HTML color name
self.colors = [SingleColor(inp)]
def __len__(self):
return len(self.colors)
def __str__(self):
if "EN_" in color_output_mode.name or "DE_" in color_output_mode.name:
joiner = "" if self.all_known else ":"
elif "HTML_" in color_output_mode.name:
joiner = ":"
else:
joiner = "???"
return joiner.join([str(color) for color in self.colors])
@property
def all_known(self):
return all([color.known for color in self.colors])
@property
def html(self):
return ":".join([color.html for color in self.colors])
@property
def html_padded(self):
if len(self) == 0:
out = []
elif len(self) == 1:
out = [self.colors[0] for i in range(3)]
elif len(self) == 2:
out = [self.colors[0], self.colors[1], self.colors[0]]
elif len(self) == 3:
out = self.colors
else:
raise Exception(f"Padding not supported for len {len(selfq)}")
return ":".join([str(color) for color in out])
COLOR_CODES = {
# fmt: off
"DIN": [
"WH", "BN", "GN", "YE", "GY", "PK", "BU", "RD", "BK", "VT", "GYPK", "RDBU",
"WHGN", "BNGN", "WHYE", "YEBN", "WHGY", "GYBN", "WHPK", "PKBN", "WHBU", "BNBU",
"WHRD", "BNRD", "WHBK", "BNBK", "GYGN", "YEGY", "PKGN", "YEPK", "GNBU", "YEBU",
"GNRD", "YERD", "GNBK", "YEBK", "GYBU", "PKBU", "GYRD", "PKRD", "GYBK", "PKBK",
"BUBK", "RDBK", "WHBNBK", "YEGNBK", "GYPKBK", "RDBUBK", "WHGNBK", "BNGNBK",
"WHYEBK", "YEBNBK", "WHGYBK", "GYBNBK", "WHPKBK", "PKBNBK", "WHBUBK",
"BNBUBK", "WHRDBK", "BNRDBK",
],
# fmt: on
"IEC": ["BN", "RD", "OG", "YE", "GN", "BU", "VT", "GY", "WH", "BK"],
"BW": ["BK", "WH"],
# 25-pair color code - see also https://en.wikipedia.org/wiki/25-pair_color_code
# 5 major colors (WH,RD,BK,YE,VT) combined with 5 minor colors (BU,OG,GN,BN,SL).
# Each POTS pair tip (+) had major/minor color, and ring (-) had minor/major color.
# fmt: off
"TEL": [ # 25x2: Ring and then tip of each pair
"BUWH", "WHBU", "OGWH", "WHOG", "GNWH", "WHGN", "BNWH", "WHBN", "SLWH", "WHSL",
"BURD", "RDBU", "OGRD", "RDOG", "GNRD", "RDGN", "BNRD", "RDBN", "SLRD", "RDSL",
"BUBK", "BKBU", "OGBK", "BKOG", "GNBK", "BKGN", "BNBK", "BKBN", "SLBK", "BKSL",
"BUYE", "YEBU", "OGYE", "YEOG", "GNYE", "YEGN", "BNYE", "YEBN", "SLYE", "YESL",
"BUVT", "VTBU", "OGVT", "VTOG", "GNVT", "VTGN", "BNVT", "VTBN", "SLVT", "VTSL",
],
"TELALT": [ # 25x2: Tip and then ring of each pair
"WHBU", "BU", "WHOG", "OG", "WHGN", "GN", "WHBN", "BN", "WHSL", "SL",
"RDBU", "BURD", "RDOG", "OGRD", "RDGN", "GNRD", "RDBN", "BNRD", "RDSL", "SLRD",
"BKBU", "BUBK", "BKOG", "OGBK", "BKGN", "GNBK", "BKBN", "BNBK", "BKSL", "SLBK",
"YEBU", "BUYE", "YEOG", "OGYE", "YEGN", "GNYE", "YEBN", "BNYE", "YESL", "SLYE",
"VTBU", "BUVT", "VTOG", "OGVT", "VTGN", "GNVT", "VTBN", "BNVT", "VTSL", "SLVT",
],
# fmt: on
"T568A": ["WHGN", "GN", "WHOG", "BU", "WHBU", "OG", "WHBN", "BN"],
"T568B": ["WHOG", "OG", "WHGN", "BU", "WHBU", "GN", "WHBN", "BN"],
}

View File

@ -17,7 +17,6 @@ from wireviz.DataClasses import (
ShieldClass,
WireClass,
)
from wireviz.wv_colors import get_color_hex, translate_color
from wireviz.wv_helper import pn_info_string, remove_links
from wireviz.wv_table_util import * # TODO: explicitly import each needed tag later
@ -26,9 +25,7 @@ HEADER_MPN = "MPN"
HEADER_SPN = "SPN"
def gv_node_component(
component: Component, harness_options: Options, pad=None
) -> Table:
def gv_node_component(component: Component) -> Table:
# If no wires connected (except maybe loop wires)?
if isinstance(component, Connector):
if not (component.ports_left or component.ports_right):
@ -52,8 +49,8 @@ def gv_node_component(
html_line_breaks(component.type),
html_line_breaks(component.subtype),
f"{component.pincount}-pin" if component.show_pincount else None,
translate_color(component.color, harness_options.color_mode),
colorbar_cell(component.color),
str(component.color) if component.color else None,
colorbar_cell(component.color) if component.color else None,
]
elif isinstance(component, Cable):
line_info = [
@ -64,10 +61,12 @@ def gv_node_component(
f"{component.length} {component.length_unit}"
if component.length > 0
else None,
translate_color(component.color, harness_options.color_mode),
colorbar_cell(component.color),
str(component.color) if component.color else None,
colorbar_cell(component.color) if component.color else None,
]
x = colorbar_cell(component.color) if component.color else None
line_image, line_image_caption = image_and_caption_cells(component)
# line_additional_component_table = get_additional_component_table(self, connector)
line_additional_component_table = None
@ -79,7 +78,7 @@ def gv_node_component(
else:
line_ports = None
elif isinstance(component, Cable):
line_ports = gv_conductor_table(component, harness_options)
line_ports = gv_conductor_table(component)
lines = [
line_name,
@ -109,17 +108,17 @@ def calculate_node_bgcolor(component, harness_options):
# assign component node bgcolor at the GraphViz node level
# instead of at the HTML table level for better rendering of node outline
if component.bgcolor:
return translate_color(component.bgcolor, "HEX")
return component.bgcolor.html
elif isinstance(component, Connector) and harness_options.bgcolor_connector:
return translate_color(harness_options.bgcolor_connector, "HEX")
return harness_options.bgcolor_connector.html
elif (
isinstance(component, Cable)
and component.category == "bundle"
and harness_options.bgcolor_bundle
):
return translate_color(harness_options.bgcolor_bundle, "HEX")
return harness_options.bgcolor_bundle.html
elif isinstance(component, Cable) and harness_options.bgcolor_cable:
return translate_color(harness_options.bgcolor_cable, "HEX")
return harness_options.bgcolor_cable.html
def make_list_of_cells(inp) -> List[Td]:
@ -170,31 +169,25 @@ def nested_table(lines: List[Td]) -> Table:
def gv_pin_table(component) -> Table:
pin_tuples = zip_longest(
component.pins,
component.pinlabels,
component.pincolors,
)
pin_rows = []
for pinindex, (pinname, pinlabel, pincolor) in enumerate(pin_tuples):
if component.should_show_pin(pinname):
pin_rows.append(
gv_pin_row(pinindex, pinname, pinlabel, pincolor, component)
)
for pin in component.pin_objects:
if component.should_show_pin(pin.id): # TODO remove True
pin_rows.append(gv_pin_row(pin, component))
tbl = Table(pin_rows, border=0, cellborder=1, cellpadding=3, cellspacing=0)
return tbl
def gv_pin_row(pin_index, pin_name, pin_label, pin_color, connector) -> Tr:
def gv_pin_row(pin, connector) -> Tr:
# ports in GraphViz are 1-indexed for more natural maping to pin/wire numbers
cell_pin_left = Td(pin_name, port=f"p{pin_index+1}l")
cell_pin_label = Td(pin_label, delete_if_empty=True)
cell_pin_right = Td(pin_name, port=f"p{pin_index+1}r")
cell_pin_left = Td(pin.id, port=f"p{pin.index+1}l")
cell_pin_label = Td(pin.label, delete_if_empty=True)
cell_pin_color = Td(str(pin.color), delete_if_empty=True)
cell_pin_right = Td(pin.id, port=f"p{pin.index+1}r")
cells = [
cell_pin_left if connector.ports_left else None,
cell_pin_label,
cell_pin_color, # TODO: generate proper color mini-table too
cell_pin_right if connector.ports_right else None,
]
return Tr(cells)
@ -217,7 +210,7 @@ def gv_connector_loops(connector: Connector) -> List:
return loop_edges
def gv_conductor_table(cable, harness_options) -> Table:
def gv_conductor_table(cable) -> Table:
rows = []
rows.append(Tr(Td("&nbsp;"))) # spacer row on top
@ -233,7 +226,7 @@ def gv_conductor_table(cable, harness_options) -> Table:
wireinfo = []
if cable.show_wirenumbers and not isinstance(wire, ShieldClass):
wireinfo.append(str(wire.id))
wireinfo.append(translate_color(wire.color, harness_options.color_mode))
wireinfo.append(str(wire.color))
wireinfo.append(wire.label)
ins, outs = [], []
@ -252,7 +245,7 @@ def gv_conductor_table(cable, harness_options) -> Table:
rows.append(Tr(cells_above))
# the wire itself
rows.append(Tr(gv_wire_cell(wire, padding=harness_options._pad)))
rows.append(Tr(gv_wire_cell(wire)))
# row below the wire
# TODO: PN stuff for bundles
@ -263,16 +256,17 @@ def gv_conductor_table(cable, harness_options) -> Table:
return tbl
def gv_wire_cell(wire: Union[WireClass, ShieldClass], padding) -> Td:
def gv_wire_cell(wire: Union[WireClass, ShieldClass]) -> Td:
# import pudb; pudb.set_trace()
if wire.color:
color_list = ["#000000"] + get_color_hex(wire.color, pad=padding) + ["#000000"]
color_list = ["#000000"] + wire.color.html_padded_list + ["#000000"]
else:
color_list = ["#000000"]
wire_inner_rows = []
for j, bgcolor in enumerate(color_list[::-1]):
wire_inner_cell_attribs = {
"bgcolor": bgcolor if bgcolor != "" else "BK",
"bgcolor": bgcolor if bgcolor != "" else "#000000",
"border": 0,
"cellpadding": 0,
"colspan": 3,
@ -339,16 +333,9 @@ def wire_pn_stuff():
def gv_edge_wire(harness, cable, connection) -> (str, str, str):
if connection.via.color:
# check if it's an actual wire and not a shield
wire_color = get_color_hex(connection.via.color, pad=harness.options._pad)
color = ":".join(["#000000"] + wire_color + ["#000000"])
color = f"#000000:{connection.via.color.html_padded}:#000000"
else: # it's a shield connection
# shield is shown with specified color and black borders, or as a thin black wire otherwise
if connection.via.color:
shield_color_hex = get_color_hex(connection.via.color)[0]
shield_color_str = ":".join(["#000000", shield_color_hex, "#000000"])
else:
shield_color_str = "#000000"
color = shield_color_str
color = "#000000"
if connection.from_ is not None: # connect to left
from_port_str = (
@ -417,7 +404,7 @@ def gv_edge_mate(mate) -> (str, str, str, str):
def colored_cell(contents, bgcolor) -> Td:
return Td(contents, bgcolor=translate_color(bgcolor, "HEX"))
return Td(contents, bgcolor=bgcolor.html)
def part_number_str_list(component: Component) -> List[str]:
@ -433,10 +420,7 @@ def part_number_str_list(component: Component) -> List[str]:
def colorbar_cell(color) -> Td:
if color:
return Td("", bgcolor=translate_color(color, "HEX"), width=4)
else:
return None
return Td("", bgcolor=color.html, width=4)
def image_and_caption_cells(component: Component) -> (Td, Td):
@ -456,7 +440,7 @@ def image_and_caption_cells(component: Component) -> (Td, Td):
image_cell.update_attribs(
balign="left",
bgcolor=translate_color(component.image.bgcolor, "HEX"),
bgcolor=component.image.bgcolor.html,
sides="TLR" if component.image.caption else None,
)
@ -495,7 +479,7 @@ def set_dot_basics(dot, options):
"graph",
rankdir="LR",
ranksep="2",
bgcolor=translate_color(options.bgcolor, "HEX"),
bgcolor=options.bgcolor.html,
nodesep="0.33",
fontname=options.fontname,
)
@ -506,7 +490,7 @@ def set_dot_basics(dot, options):
height="0",
margin="0", # Actual size of the node is entirely determined by the label.
style="filled",
fillcolor=translate_color(options.bgcolor_node, "HEX"),
fillcolor=options.bgcolor_node.html,
fontname=options.fontname,
)
dot.attr("edge", style="bold", fontname=options.fontname)

View File

@ -79,7 +79,7 @@ def generate_html_output(
replacements = {
"<!-- %generator% -->": f"{APP_NAME} {__version__} - {APP_URL}",
"<!-- %fontname% -->": options.fontname,
"<!-- %bgcolor% -->": wv_colors.translate_color(options.bgcolor, "hex"),
"<!-- %bgcolor% -->": options.bgcolor.html,
"<!-- %diagram% -->": svgdata,
"<!-- %bom% -->": bom_html,
"<!-- %bom_reversed% -->": bom_html_reversed,