Add basic options and metadata (#214)

This commit is contained in:
kvid 2021-08-25 19:46:37 +02:00 committed by GitHub
parent e212fc9058
commit 92354e6852
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 151 additions and 41 deletions

View File

@ -3,6 +3,12 @@
## Main sections
```yaml
metadata: # dictionary of meta-information describing the harness
<key> : <value> # any number of key value pairs (see below)
...
options: # dictionary of common attributes for the whole harness
<str> : <value> # optional harness attributes (see below)
...
connectors: # dictionary of all used connectors
<str> : # unique connector designator/name
... # connector attributes (see below)
@ -31,6 +37,55 @@ additional_bom_items: # custom items to add to BOM
```
## Metadata entries
```yaml
# Meta-information describing the harness
# Each key/value pair replaces all key references in
# the HTML output template with the belonging value.
# Typical keys are 'title', 'description', and 'notes',
# but any key is accepted. Unused keys are ignored.
<key> : <value> # Any valid YAML syntax is accepted
# If no value is specified for 'title', then the
# output filename without extension is used.
```
## Options
```yaml
# Common attributes for the whole harness.
# All entries are optional and have default values.
# Background color of diagram and HTML output
bgcolor: <color> # Default = 'WH'
# Background color of other diagram elements
bgcolor_node: <color> # Default = 'WH'
bgcolor_connector: <color> # Default = bgcolor_node
bgcolor_cable: <color> # Default = bgcolor_node
bgcolor_bundle: <color> # Default = bgcolor_cable
# How to display colors as text in the diagram
# 'full' : Lowercase full color name
# 'FULL' : Uppercase full color name
# 'hex' : Lowercase hexadecimal values
# 'HEX' : Uppercase hexadecimal values
# 'short': Lowercase short color name
# 'SHORT': Uppercase short color name
# 'ger' : Lowercase short German color name
# 'GER' : Uppercase short German color name
color_mode: <str> # Default = 'SHORT'
# Fontname to use in diagram and HTML output
fontname: <str> # Default = 'arial'
# If True, show only a BOM entry reference together with basic info
# about additional components inside the diagram node (connector/cable box).
# If False, show all info about additional components inside the diagram node.
mini_bom_mode: <bool> # Default = True
```
## Connector attributes
```yaml

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Optional, List, Tuple, Union
from typing import Dict, List, Optional, Tuple, Union
from dataclasses import dataclass, field, InitVar
from pathlib import Path
@ -20,6 +20,7 @@ ConnectorMultiplier = PlainText # = Literal['pincount', 'populated']
CableMultiplier = PlainText # = Literal['wirecount', 'terminations', 'length', 'total_length']
ImageScale = PlainText # = Literal['false', 'true', 'width', 'height', 'both']
Color = PlainText # Two-letter color name = Literal[wv_colors._color_hex.keys()]
ColorMode = PlainText # = Literal['full', 'FULL', 'hex', 'HEX', 'short', 'SHORT', 'ger', 'GER']
ColorScheme = PlainText # Color scheme name = Literal[wv_colors.COLOR_CODES.keys()]
# Type combinations
@ -30,6 +31,33 @@ Wire = Union[int, PlainText] # Wire number or Literal['s'] for shield
NoneOrMorePinIndices = Union[PinIndex, Tuple[PinIndex, ...], None] # None, one, or a tuple of zero-based pin indices
OneOrMoreWires = Union[Wire, Tuple[Wire, ...]] # One or a tuple of wires
# Metadata can contain whatever is needed by the HTML generation/template.
MetadataKeys = PlainText # Literal['title', 'description', 'notes', ...]
class Metadata(dict):
pass
@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'
mini_bom_mode: bool = True
def __post_init__(self):
if not self.bgcolor_node:
self.bgcolor_node = self.bgcolor
if not self.bgcolor_connector:
self.bgcolor_connector = self.bgcolor_node
if not self.bgcolor_cable:
self.bgcolor_cable = self.bgcolor_node
if not self.bgcolor_bundle:
self.bgcolor_bundle = self.bgcolor_cable
@dataclass
class Image:

View File

@ -4,13 +4,14 @@
from graphviz import Graph
from collections import Counter
from typing import List, Union
from dataclasses import dataclass
from pathlib import Path
from itertools import zip_longest
import re
from wireviz import wv_colors, __version__, APP_NAME, APP_URL
from wireviz.DataClasses import Connector, Cable
from wireviz.wv_colors import get_color_hex
from wireviz.DataClasses import Metadata, Options, Connector, Cable
from wireviz.wv_colors import get_color_hex, translate_color
from wireviz.wv_gv_html import nested_html_table, html_colorbar, html_image, \
html_caption, remove_links, html_line_breaks
from wireviz.wv_bom import manufacturer_info_field, component_table_entry, \
@ -20,11 +21,12 @@ from wireviz.wv_helper import awg_equiv, mm2_equiv, tuplelist2tsv, flatten2d, \
open_file_read, open_file_write
@dataclass
class Harness:
metadata: Metadata
options: Options
def __init__(self):
self.color_mode = 'SHORT'
self.mini_bom_mode = True
def __post_init__(self):
self.connectors = {}
self.cables = {}
self._bom = [] # Internal Cache for generated bom
@ -91,18 +93,19 @@ class Harness:
dot = Graph()
dot.body.append(f'// Graph generated by {APP_NAME} {__version__}')
dot.body.append(f'// {APP_URL}')
font = 'arial'
dot.attr('graph', rankdir='LR',
ranksep='2',
bgcolor='white',
bgcolor=wv_colors.translate_color(self.options.bgcolor, "HEX"),
nodesep='0.33',
fontname=font)
dot.attr('node', shape='record',
fontname=self.options.fontname)
dot.attr('node',
shape='none',
width='0', height='0', margin='0', # Actual size of the node is entirely determined by the label.
style='filled',
fillcolor='white',
fontname=font)
fillcolor=wv_colors.translate_color(self.options.bgcolor_node, "HEX"),
fontname=self.options.fontname)
dot.attr('edge', style='bold',
fontname=font)
fontname=self.options.fontname)
# prepare ports on connectors depending on which side they will connect
for _, cable in self.cables.items():
@ -126,7 +129,8 @@ class Harness:
[html_line_breaks(connector.type),
html_line_breaks(connector.subtype),
f'{connector.pincount}-pin' if connector.show_pincount else None,
connector.color, html_colorbar(connector.color)],
translate_color(connector.color, self.options.color_mode) if connector.color else None,
html_colorbar(connector.color)],
'<!-- connector table -->' if connector.style != 'simple' else None,
[html_image(connector.image)],
[html_caption(connector.image)]]
@ -148,7 +152,7 @@ class Harness:
pinhtml.append(f' <td>{pinlabel}</td>')
if connector.pincolors:
if pincolor in wv_colors._color_hex.keys():
pinhtml.append(f' <td sides="tbl">{pincolor}</td>')
pinhtml.append(f' <td sides="tbl">{translate_color(pincolor, self.options.color_mode)}</td>')
pinhtml.append( ' <td sides="tbr">')
pinhtml.append( ' <table border="0" cellborder="1"><tr>')
pinhtml.append(f' <td bgcolor="{wv_colors.translate_color(pincolor, "HEX")}" width="8" height="8" fixedsize="true"></td>')
@ -166,7 +170,8 @@ class Harness:
html = [row.replace('<!-- connector table -->', '\n'.join(pinhtml)) for row in html]
html = '\n'.join(html)
dot.node(connector.name, label=f'<\n{html}\n>', shape='none', margin='0', style='filled', fillcolor='white')
dot.node(connector.name, label=f'<\n{html}\n>', shape='box', style='filled',
fillcolor=translate_color(self.options.bgcolor_connector, "HEX"))
if len(connector.loops) > 0:
dot.attr('edge', color='#000000:#ffffff:#000000')
@ -211,7 +216,8 @@ class Harness:
f'{cable.gauge} {cable.gauge_unit}{awg_fmt}' if cable.gauge else None,
'+ S' if cable.shield else None,
f'{cable.length} {cable.length_unit}' if cable.length > 0 else None,
cable.color, html_colorbar(cable.color)],
translate_color(cable.color, self.options.color_mode) if cable.color else None,
html_colorbar(cable.color)],
'<!-- wire table -->',
[html_image(cable.image)],
[html_caption(cable.image)]]
@ -232,7 +238,7 @@ class Harness:
wireinfo = []
if cable.show_wirenumbers:
wireinfo.append(str(i))
colorstr = wv_colors.translate_color(connection_color, self.color_mode)
colorstr = wv_colors.translate_color(connection_color, self.options.color_mode)
if colorstr:
wireinfo.append(colorstr)
if cable.wirelabels:
@ -332,9 +338,11 @@ class Harness:
to_string = ''
html = [row.replace(f'<!-- {connection.via_port}_out -->', to_string) for row in html]
style, bgcolor = ('filled,dashed', self.options.bgcolor_bundle) if cable.category == 'bundle' else \
('filled', self.options.bgcolor_cable)
html = '\n'.join(html)
dot.node(cable.name, label=f'<\n{html}\n>', shape='box',
style='filled,dashed' if cable.category == 'bundle' else '', margin='0', fillcolor='white')
style=style, fillcolor=translate_color(bgcolor, "HEX"))
return dot
@ -368,7 +376,7 @@ class Harness:
with open_file_write(f'{filename}.bom.tsv') as file:
file.write(tuplelist2tsv(bomlist))
# HTML output
generate_html_output(filename, bomlist)
generate_html_output(filename, bomlist, self.metadata, self.options)
def bom(self):
if not self._bom:

View File

@ -13,6 +13,7 @@ if __name__ == '__main__':
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from wireviz import __version__
from wireviz.DataClasses import Metadata, Options
from wireviz.Harness import Harness
from wireviz.wv_helper import expand, open_file_read
@ -34,7 +35,12 @@ def parse(yaml_input: str, file_out: (str, Path) = None, return_types: (None, st
yaml_data = yaml.safe_load(yaml_input)
harness = Harness()
harness = Harness(
metadata = Metadata(**yaml_data.get('metadata', {})),
options = Options(**yaml_data.get('options', {})),
)
if 'title' not in harness.metadata:
harness.metadata['title'] = Path(file_out).stem
# add items
sections = ['connectors', 'cables', 'connections']

View File

@ -6,6 +6,7 @@ from itertools import groupby
from typing import Any, Dict, List, Optional, Tuple, Union
from wireviz.DataClasses import AdditionalComponent, Connector, Cable
from wireviz.wv_colors import translate_color
from wireviz.wv_gv_html import html_line_breaks
from wireviz.wv_helper import clean_whitespace
@ -32,7 +33,7 @@ def get_additional_component_table(harness: "Harness", component: Union[Connecto
'qty': part.qty * component.get_qty_multiplier(part.qty_multiplier),
'unit': part.unit,
}
if harness.mini_bom_mode:
if harness.options.mini_bom_mode:
id = get_bom_index(harness.bom(), bom_entry_key({**asdict(part), 'description': part.description}))
rows.append(component_table_entry(f'#{id} ({part.type.rstrip()})', **common_args))
else:
@ -69,7 +70,7 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]:
+ (f', {connector.type}' if connector.type else '')
+ (f', {connector.subtype}' if connector.subtype else '')
+ (f', {connector.pincount} pins' if connector.show_pincount else '')
+ (f', {connector.color}' if connector.color else ''))
+ (f', {translate_color(connector.color, harness.options.color_mode)}' if connector.color else ''))
bom_entries.append({
'description': description, 'designators': connector.name if connector.show_name else None,
**optional_fields(connector),
@ -88,7 +89,8 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]:
+ (f', {cable.type}' if cable.type else '')
+ (f', {cable.wirecount}')
+ (f' x {cable.gauge} {cable.gauge_unit}' if cable.gauge else ' wires')
+ (' shielded' if cable.shield else ''))
+ ( ' shielded' if cable.shield else '')
+ (f', {translate_color(cable.color, harness.options.color_mode)}' if cable.color else ''))
bom_entries.append({
'description': description, 'qty': cable.length, 'unit': cable.length_unit, 'designators': cable.name if cable.show_name else None,
**optional_fields(cable),
@ -99,7 +101,7 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]:
description = ('Wire'
+ (f', {cable.type}' if cable.type else '')
+ (f', {cable.gauge} {cable.gauge_unit}' if cable.gauge else '')
+ (f', {color}' if color else ''))
+ (f', {translate_color(color, harness.options.color_mode)}' if color else ''))
bom_entries.append({
'description': description, 'qty': cable.length, 'unit': cable.length_unit, 'designators': cable.name if cable.show_name else None,
**{k: index_if_list(v, index) for k, v in optional_fields(cable).items()},

View File

@ -2,21 +2,28 @@
# -*- coding: utf-8 -*-
from pathlib import Path
from typing import List, Union
import re
from wireviz import __version__, APP_NAME, APP_URL
from wireviz import __version__, APP_NAME, APP_URL, wv_colors
from wireviz.DataClasses import Metadata, Options
from wireviz.wv_helper import flatten2d, open_file_read, open_file_write
def generate_html_output(filename: (str, Path), bom_list):
def generate_html_output(filename: Union[str, Path], bom_list: List[List[str]], metadata: Metadata, options: Options):
with open_file_write(f'{filename}.html') as file:
file.write('<!DOCTYPE html>\n')
file.write('<html lang="en"><head>\n')
file.write(' <meta charset="UTF-8">\n')
file.write(f' <meta name="generator" content="{APP_NAME} {__version__} - {APP_URL}">\n')
file.write(f' <title>{APP_NAME} Diagram and BOM</title>\n')
file.write('</head><body style="font-family:Arial">\n')
file.write(f' <title>{metadata["title"]}</title>\n')
file.write(f'</head><body style="font-family:{options.fontname};background-color:'
f'{wv_colors.translate_color(options.bgcolor, "HEX")}">\n')
file.write('<h1>Diagram</h1>')
file.write(f'<h1>{metadata["title"]}</h1>\n')
description = metadata.get('description')
if description:
file.write(f'<p>{description}</p>\n')
file.write('<h2>Diagram</h2>\n')
with open_file_read(f'{filename}.svg') as svg:
file.write(re.sub(
'^<[?]xml [^?>]*[?]>[^<]*<!DOCTYPE [^>]*>',
@ -25,20 +32,24 @@ def generate_html_output(filename: (str, Path), bom_list):
for svgdata in svg:
file.write(svgdata)
file.write('<h1>Bill of Materials</h1>')
file.write('<h2>Bill of Materials</h2>\n')
listy = flatten2d(bom_list)
file.write('<table style="border:1px solid #000000; font-size: 14pt; border-spacing: 0px">')
file.write('<tr>')
file.write('<table style="border:1px solid #000000; font-size: 14pt; border-spacing: 0px">\n')
file.write(' <tr>\n')
for item in listy[0]:
file.write(f'<th style="text-align:left; border:1px solid #000000; padding: 8px">{item}</th>')
file.write('</tr>')
file.write(f' <th style="text-align:left; border:1px solid #000000; padding: 8px">{item}</th>\n')
file.write(' </tr>\n')
for row in listy[1:]:
file.write('<tr>')
file.write(' <tr>\n')
for i, item in enumerate(row):
item_str = item.replace('\u00b2', '&sup2;')
align = 'text-align:right; ' if listy[0][i] == 'Qty' else ''
file.write(f'<td style="{align}border:1px solid #000000; padding: 4px">{item_str}</td>')
file.write('</tr>')
file.write('</table>')
align = '; text-align:right' if listy[0][i] == 'Qty' else ''
file.write(f' <td style="border:1px solid #000000; padding: 4px{align}">{item_str}</td>\n')
file.write(' </tr>\n')
file.write('</table>\n')
file.write('</body></html>')
notes = metadata.get('notes')
if notes:
file.write(f'<h2>Notes</h2>\n<p>{notes}</p>\n')
file.write('</body></html>\n')