Use color objects in WireViz

This commit is contained in:
Daniel Rojas 2021-10-20 20:14:10 +02:00 committed by KV
parent 1b31a6f044
commit e95f4bd53b
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 itertools import zip_longest
from typing import Dict, List, Optional, Tuple, Union 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 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 # 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 @dataclass
class Options: class Options:
fontname: PlainText = "arial" fontname: PlainText = "arial"
bgcolor: Color = "WH" bgcolor: SingleColor = "WH" # will be converted to SingleColor in __post_init__
bgcolor_node: Optional[Color] = "WH" bgcolor_node: SingleColor = "WH"
bgcolor_connector: Optional[Color] = None bgcolor_connector: SingleColor = None
bgcolor_cable: Optional[Color] = None bgcolor_cable: SingleColor = None
bgcolor_bundle: Optional[Color] = None bgcolor_bundle: SingleColor = None
color_mode: ColorMode = "SHORT" color_mode: ColorOutputMode = ColorOutputMode.EN_UPPER
mini_bom_mode: bool = True mini_bom_mode: bool = True
template_separator: str = "." template_separator: str = "."
_pad: int = 0 _pad: int = 0
@ -74,6 +80,13 @@ class Options:
_image_paths: [List] = field(default_factory=list) _image_paths: [List] = field(default_factory=list)
def __post_init__(self): 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: if not self.bgcolor_node:
self.bgcolor_node = self.bgcolor self.bgcolor_node = self.bgcolor
if not self.bgcolor_connector: if not self.bgcolor_connector:
@ -99,13 +112,15 @@ class Image:
width: Optional[int] = None width: Optional[int] = None
height: Optional[int] = None height: Optional[int] = None
fixedsize: Optional[bool] = None fixedsize: Optional[bool] = None
bgcolor: Optional[Color] = None bgcolor: SingleColor = 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): def __post_init__(self):
self.bgcolor = SingleColor(self.bgcolor)
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
@ -141,7 +156,10 @@ 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 bgcolor: SingleColor = None
def __post_init__(self):
self.bgcolor = SingleColor(self.bgcolor)
@property @property
def description(self) -> str: def description(self) -> str:
@ -159,7 +177,7 @@ class PinClass:
index: int index: int
id: str id: str
label: str label: str
color: str color: MultiColor
parent: str # designator of parent connector parent: str # designator of parent connector
_anonymous: bool = False # true for pins on autogenerated connectors _anonymous: bool = False # true for pins on autogenerated connectors
_simple: bool = False # true for simple connector _simple: bool = False # true for simple connector
@ -178,7 +196,7 @@ class WireClass:
index: int index: int
id: str id: str
label: str label: str
color: str color: MultiColor
parent: str # designator of parent cable/bundle parent: str # designator of parent cable/bundle
# gauge: Gauge # gauge: Gauge
# pn: str # pn: str
@ -203,8 +221,8 @@ class Connection:
@dataclass @dataclass
class Connector(Component): class Connector(Component):
name: Designator name: Designator
bgcolor: Optional[Color] = None bgcolor: SingleColor = None
bgcolor_title: Optional[Color] = None bgcolor_title: SingleColor = 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
@ -219,8 +237,8 @@ class Connector(Component):
notes: Optional[MultilineHypertext] = None notes: Optional[MultilineHypertext] = None
pins: List[Pin] = field(default_factory=list) pins: List[Pin] = field(default_factory=list)
pinlabels: List[Pin] = field(default_factory=list) pinlabels: List[Pin] = field(default_factory=list)
pincolors: List[Color] = field(default_factory=list) pincolors: List[str] = field(default_factory=list)
color: Optional[Color] = None color: MultiColor = None
show_name: Optional[bool] = None show_name: Optional[bool] = None
show_pincount: Optional[bool] = None show_pincount: Optional[bool] = None
hide_disconnected_pins: bool = False hide_disconnected_pins: bool = False
@ -238,6 +256,10 @@ class Connector(Component):
def __post_init__(self) -> None: 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): if isinstance(self.image, dict):
self.image = Image(**self.image) self.image = Image(**self.image)
@ -280,7 +302,7 @@ class Connector(Component):
index=pin_index, index=pin_index,
id=pin_id, id=pin_id,
label=pin_label, label=pin_label,
color=pin_color, color=MultiColor(pin_color),
parent=self.name, parent=self.name,
_anonymous=self.is_autogenerated, _anonymous=self.is_autogenerated,
_simple=self.style == "simple", _simple=self.style == "simple",
@ -345,8 +367,8 @@ class Connector(Component):
@dataclass @dataclass
class Cable(Component): class Cable(Component):
name: Designator name: Designator
bgcolor: Optional[Color] = None bgcolor: SingleColor = None
bgcolor_title: Optional[Color] = None bgcolor_title: SingleColor = 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
@ -359,14 +381,14 @@ class Cable(Component):
show_equiv: bool = False show_equiv: bool = False
length: float = 0 length: float = 0
length_unit: Optional[str] = None length_unit: Optional[str] = None
color: Optional[Color] = None color: MultiColor = None
wirecount: Optional[int] = None wirecount: Optional[int] = None
shield: Union[bool, Color] = False shield: Union[bool, MultiColor] = False
image: Optional[Image] = None image: Optional[Image] = None
notes: Optional[MultilineHypertext] = 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) wirelabels: List[Wire] = field(default_factory=list)
color_code: Optional[ColorScheme] = None color_code: Optional[str] = None
show_name: Optional[bool] = None show_name: Optional[bool] = None
show_wirecount: bool = True show_wirecount: bool = True
show_wirenumbers: Optional[bool] = None show_wirenumbers: Optional[bool] = None
@ -397,6 +419,10 @@ class Cable(Component):
def __post_init__(self) -> None: 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): if isinstance(self.image, dict):
self.image = Image(**self.image) self.image = Image(**self.image)
@ -445,21 +471,20 @@ class Cable(Component):
if self.wirecount: # number of wires explicitly defined if self.wirecount: # number of wires explicitly defined
if self.colors: # use custom color palette (partly or looped if needed) 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: elif self.color_code:
# use standard color palette (partly or looped if needed) # use standard color palette (partly or looped if needed)
if self.color_code not in COLOR_CODES: if self.color_code not in COLOR_CODES:
raise Exception("Unknown color code") 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 else: # no colors defined, add dummy colors
self.colors = [""] * self.wirecount 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 else: # wirecount implicit in length of color list
if not self.colors: if not self.colors:
raise Exception( raise Exception(
@ -495,7 +520,7 @@ class Cable(Component):
index=wire_index, # TODO: wire_id index=wire_index, # TODO: wire_id
id=wire_index + 1, # TODO: wire_id id=wire_index + 1, # TODO: wire_id
label=wire_label, label=wire_label,
color=wire_color, color=MultiColor(wire_color),
parent=self.name, parent=self.name,
) )
) )
@ -508,7 +533,9 @@ class Cable(Component):
index=index_offset, index=index_offset,
id="s", id="s",
label="Shield", 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, parent=self.name,
) )
) )

View File

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

View File

@ -4,8 +4,7 @@ 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
from wireviz.wv_colors import translate_color
from wireviz.wv_gv_html import html_line_breaks from wireviz.wv_gv_html import html_line_breaks
from wireviz.wv_helper import clean_whitespace, pn_info_string 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.subtype}" if connector.subtype else "")
+ (f", {connector.pincount} pins" if connector.show_pincount 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 if connector.color
else "" else ""
) )
@ -132,7 +131,7 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]:
) )
+ (" shielded" if cable.shield else "") + (" 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 if cable.color
else "" else ""
) )
@ -154,7 +153,7 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]:
+ (f", {cable.type}" if cable.type else "") + (f", {cable.type}" if cable.type else "")
+ (f", {cable.gauge} {cable.gauge_unit}" if cable.gauge 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 if color
else "" else ""
) )
@ -236,7 +235,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, bgcolor: Optional[str] = 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,

View File

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

View File

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