-
-
-
-
+
+
+
X1
-
-
-X1
-
-JST SM
-
-male
-
-4-pin
-
-A
-
-1
-
-B
-
-2
-
-C
-
-3
-
-D
-
-4
+
+
+
+X1
+
+JST SM
+
+male
+
+4-pin
+
+A
+
+1
+
+B
+
+2
+
+C
+
+3
+
+D
+
+4
W1
-
-
-W1
-
-4x
-
-0.1 m
-
-X1:4:D
- 1:WH
-
-
-
-X1:3:C
- 2:BN
-
-
-
-X1:2:B
- 3:GN
-
-
-
-X1:1:A
- 4:YE
-
-
-
-
+
+
+
+W1
+
+4x
+
+0.1 m
+
+ X1:4:D
+
+1:WH
+
+
+
+
+
+ X1:3:C
+
+2:BN
+
+
+
+
+
+ X1:2:B
+
+3:GN
+
+S1
+
+
+
+ X1:1:A
+
+4:YE
+
+
+
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
-
+
-__S_1
-
-
-Splice
-
-CU
-
-
+AUTOGENERATED_S_1
+
+
+
+Splice
+
+CU
+
+
W2
-
-
-W2
-
-4x
-
-0.1 m
-
- 1:WH
-
-
-
- 2:BN
-
-
-
- 3:GN
-
-
-
- 4:YE
-
-
-
-
-
-
+
+
+
+W2
+
+4x
+
+0.1 m
+
+
+
+1:WH
+
+
+
+
+
+
+
+2:BN
+
+
+
+
+
+ S1
+
+3:GN
+
+
+
+
+
+
+
+4:YE
+
+
+
+
+
+
+
+
-__S_1:e--W2:w
-
-
-
+AUTOGENERATED_S_1:e--W2:w
+
+
+
-
+
-__S_2
-
-
-Splice
-
-CU
-
-
-
-
+AUTOGENERATED_S_2
+
+
+
+Splice
+
+CU
+
+
+
+
-__S_2:e--W2:w
-
-
-
+AUTOGENERATED_S_2:e--W2:w
+
+
+
S1
-
-
-Splice
-
-CU
-
-
+
+
+
+Splice
+
+CU
+
+
S1:e--W2:w
-
-
-
+
+
+
-
+
-__WIRE_1
-
-
-1x
-
-0.1 m
-
- 1:BK
-X2:4:D
-
-
-
-
-
-
+AUTOGENERATED_WIRE_1
+
+
+
+1x
+
+0.1 m
+
+ S1
+
+1:BK
+
+X2:4:D
+
+
+
+
+
+
-S1:e--__WIRE_1:w
-
-
-
+S1:e--AUTOGENERATED_WIRE_1:w
+
+
+
-
+
-__S_3
-
-
-Splice
-
-CU
-
-
-
-
+AUTOGENERATED_S_3
+
+
+
+Splice
+
+CU
+
+
+
+
-__S_3:e--W2:w
-
-
-
+AUTOGENERATED_S_3:e--W2:w
+
+
+
-
+
-__S_4
-
-
-Splice
-
-CU
-
-
+AUTOGENERATED_S_4
+
+
+
+Splice
+
+CU
+
+
W21
-
-
-W21
-
-4x
-
-0.1 m
-
- 1:WH
-X2:1:A
-
-
-
- 2:BN
-X2:2:B
-
-
-
- 3:GN
-X2:3:C
-
-
-
- 4:YE
-X2:4:D
-
-
-
-
-
-
+
+
+
+W21
+
+4x
+
+0.1 m
+
+
+
+1:WH
+
+X2:1:A
+
+
+
+
+
+2:BN
+
+X2:2:B
+
+
+
+
+
+3:GN
+
+X2:3:C
+
+
+
+
+
+4:YE
+
+X2:4:D
+
+
+
+
+
+
-__S_4:e--W21:w
-
-
-
+AUTOGENERATED_S_4:e--W21:w
+
+
+
-
+
-__S_5
-
-
-Splice
-
-CU
-
-
-
-
+AUTOGENERATED_S_5
+
+
+
+Splice
+
+CU
+
+
+
+
-__S_5:e--W21:w
-
-
-
+AUTOGENERATED_S_5:e--W21:w
+
+
+
-
+
-__S_6
-
-
-Splice
-
-CU
-
-
-
-
+AUTOGENERATED_S_6
+
+
+
+Splice
+
+CU
+
+
+
+
-__S_6:e--W21:w
-
-
-
+AUTOGENERATED_S_6:e--W21:w
+
+
+
-
+
-__S_7
-
-
-Splice
-
-CU
-
-
-
-
+AUTOGENERATED_S_7
+
+
+
+Splice
+
+CU
+
+
+
+
-__S_7:e--W21:w
-
-
-
+AUTOGENERATED_S_7:e--W21:w
+
+
+
X2
-
-
-X2
-
-JST SM
-
-female
-
-4-pin
-
-1
-
-A
-
-2
-
-B
-
-3
-
-C
-
-4
-
-D
+
+
+
+X2
+
+JST SM
+
+female
+
+4-pin
+
+1
+
+A
+
+2
+
+B
+
+3
+
+C
+
+4
+
+D
X3
-
-
-X3
-
-JST SM
-
-male
-
-4-pin
-
-A
-
-1
-
-B
-
-2
-
-C
-
-3
-
-D
-
-4
+
+
+
+X3
+
+JST SM
+
+male
+
+4-pin
+
+A
+
+1
+
+B
+
+2
+
+C
+
+3
+
+D
+
+4
X2:e--X3:w
-
-
-
-
+
+
+
W3
-
-
-W3
-
-4x
-
-0.1 m
-
-X3:1:A
- 1:WH
-
-
-
-X3:2:B
- 2:BN
-
-
-
-X3:3:C
- 3:GN
-
-
-
-X3:4:D
- 4:YE
-
-
-
-
+
+
+
+W3
+
+4x
+
+0.1 m
+
+ X3:1:A
+
+1:WH
+
+
+
+
+
+ X3:2:B
+
+2:BN
+
+
+
+
+
+ X3:3:C
+
+3:GN
+
+
+
+
+
+ X3:4:D
+
+4:YE
+
+
+
+
+
+
X3:e--W3:w
-
-
-
+
+
+
X3:e--W3:w
-
-
-
+
+
+
X3:e--W3:w
-
-
-
+
+
+
X3:e--W3:w
-
-
-
+
+
+
-
+
-__F_1
-
-
-Ferrule
-
-GY
-
-
+AUTOGENERATED_F_1
+
+
+
+Ferrule
+
+GY
+
+
X4
-
-
-X4
-
-Screw terminal connector
-
-4-pin
-
-GN
-
-
-
-1
-
-W
-
-2
-
-X
-
-3
-
-Y
-
-4
-
-Z
-
-
+
+
+
+X4
+
+Screw terminal connector
+
+4-pin
+
+GN
+
+
+
+1
+
+W
+
+2
+
+X
+
+3
+
+Y
+
+4
+
+Z
+
+
-__F_1:e--X4:w
-
-
+AUTOGENERATED_F_1:e--X4:w
+
+
-
+
-__F_2
-
-
-Ferrule
-
-GY
-
-
-
-
+AUTOGENERATED_F_2
+
+
+
+Ferrule
+
+GY
+
+
+
+
-__F_2:e--X4:w
-
-
+AUTOGENERATED_F_2:e--X4:w
+
+
-
+
-__F_3
-
-
-Ferrule
-
-GY
-
-
-
-
+AUTOGENERATED_F_3
+
+
+
+Ferrule
+
+GY
+
+
+
+
-__F_3:e--X4:w
-
-
+AUTOGENERATED_F_3:e--X4:w
+
+
-
+
-__F_4
-
-
-Ferrule
-
-GY
-
-
-
-
+AUTOGENERATED_F_4
+
+
+
+Ferrule
+
+GY
+
+
+
+
-__F_4:e--X4:w
-
-
+AUTOGENERATED_F_4:e--X4:w
+
+
-
+
-W1:e--__S_1:w
-
-
-
+W1:e--AUTOGENERATED_S_1:w
+
+
+
-
+
-W1:e--__S_2:w
-
-
-
+W1:e--AUTOGENERATED_S_2:w
+
+
+
W1:e--S1:w
-
-
-
+
+
+
-
+
-W1:e--__S_3:w
-
-
-
+W1:e--AUTOGENERATED_S_3:w
+
+
+
-
+
-W2:e--__S_4:w
-
-
-
+W2:e--AUTOGENERATED_S_4:w
+
+
+
-
+
-W2:e--__S_5:w
-
-
-
+W2:e--AUTOGENERATED_S_5:w
+
+
+
-
+
-W2:e--__S_6:w
-
-
-
+W2:e--AUTOGENERATED_S_6:w
+
+
+
-
+
-W2:e--__S_7:w
-
-
-
+W2:e--AUTOGENERATED_S_7:w
+
+
+
W21:e--X2:w
-
-
-
+
+
+
W21:e--X2:w
-
-
-
+
+
+
W21:e--X2:w
-
-
-
+
+
+
W21:e--X2:w
-
-
-
+
+
+
-
+
-W3:e--__F_1:w
-
-
-
+W3:e--AUTOGENERATED_F_1:w
+
+
+
-
+
-W3:e--__F_2:w
-
-
-
+W3:e--AUTOGENERATED_F_2:w
+
+
+
-
+
-W3:e--__F_3:w
-
-
-
+W3:e--AUTOGENERATED_F_3:w
+
+
+
-
+
-W3:e--__F_4:w
-
-
-
+W3:e--AUTOGENERATED_F_4:w
+
+
+
-
+
-__WIRE_1:e--X2:w
-
-
-
+AUTOGENERATED_WIRE_1:e--X2:w
+
+
+
@@ -715,61 +790,61 @@
Bill of Materials
- Id
- Description
+ #
Qty
Unit
+ Description
Designators
- 1
- Cable, 1 wires
- 0.1
- m
-
-
-
- 2
- Cable, 4 wires
- 0.4
- m
- W1, W2, W21, W3
-
-
- 3
- Connector, Ferrule, GY
+ 1
4
+ Connector, Ferrule, GY
- 4
- Connector, JST SM, female, 4 pins
+ 2
1
+ Connector, JST SM, female, 4 pins
X2
- 5
- Connector, JST SM, male, 4 pins
+ 3
2
+ Connector, JST SM, male, 4 pins
X1, X3
- 6
- Connector, Screw terminal connector, 4 pins, GN
+ 4
1
+ Connector, Screw terminal connector, 4 pins, GN
X4
- 7
- Connector, Splice, CU
+ 5
8
+ Connector, Splice, CU
+ S1
+
+
+ 6
+ 1
+ m
+ Cable, 1 wires
+
+ 7
+ 4
+ m
+ Cable, 4 wires
+ W1, W2, W21, W3
+
diff --git a/examples/ex14.png b/examples/ex14.png
index 8116ed379..d50b3f6cc 100644
Binary files a/examples/ex14.png and b/examples/ex14.png differ
diff --git a/examples/ex14.svg b/examples/ex14.svg
index 302b7e328..c97d90061 100644
--- a/examples/ex14.svg
+++ b/examples/ex14.svg
@@ -1,676 +1,751 @@
-
-
-
-
+
+
+
X1
-
-
-X1
-
-JST SM
-
-male
-
-4-pin
-
-A
-
-1
-
-B
-
-2
-
-C
-
-3
-
-D
-
-4
+
+
+
+X1
+
+JST SM
+
+male
+
+4-pin
+
+A
+
+1
+
+B
+
+2
+
+C
+
+3
+
+D
+
+4
W1
-
-
-W1
-
-4x
-
-0.1 m
-
-X1:4:D
- 1:WH
-
-
-
-X1:3:C
- 2:BN
-
-
-
-X1:2:B
- 3:GN
-
-
-
-X1:1:A
- 4:YE
-
-
-
-
+
+
+
+W1
+
+4x
+
+0.1 m
+
+ X1:4:D
+
+1:WH
+
+
+
+
+
+ X1:3:C
+
+2:BN
+
+
+
+
+
+ X1:2:B
+
+3:GN
+
+S1
+
+
+
+ X1:1:A
+
+4:YE
+
+
+
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
-
+
-__S_1
-
-
-Splice
-
-CU
-
-
+AUTOGENERATED_S_1
+
+
+
+Splice
+
+CU
+
+
W2
-
-
-W2
-
-4x
-
-0.1 m
-
- 1:WH
-
-
-
- 2:BN
-
-
-
- 3:GN
-
-
-
- 4:YE
-
-
-
-
-
-
+
+
+
+W2
+
+4x
+
+0.1 m
+
+
+
+1:WH
+
+
+
+
+
+
+
+2:BN
+
+
+
+
+
+ S1
+
+3:GN
+
+
+
+
+
+
+
+4:YE
+
+
+
+
+
+
+
+
-__S_1:e--W2:w
-
-
-
+AUTOGENERATED_S_1:e--W2:w
+
+
+
-
+
-__S_2
-
-
-Splice
-
-CU
-
-
-
-
+AUTOGENERATED_S_2
+
+
+
+Splice
+
+CU
+
+
+
+
-__S_2:e--W2:w
-
-
-
+AUTOGENERATED_S_2:e--W2:w
+
+
+
S1
-
-
-Splice
-
-CU
-
-
+
+
+
+Splice
+
+CU
+
+
S1:e--W2:w
-
-
-
+
+
+
-
+
-__WIRE_1
-
-
-1x
-
-0.1 m
-
- 1:BK
-X2:4:D
-
-
-
-
-
-
+AUTOGENERATED_WIRE_1
+
+
+
+1x
+
+0.1 m
+
+ S1
+
+1:BK
+
+X2:4:D
+
+
+
+
+
+
-S1:e--__WIRE_1:w
-
-
-
+S1:e--AUTOGENERATED_WIRE_1:w
+
+
+
-
+
-__S_3
-
-
-Splice
-
-CU
-
-
-
-
+AUTOGENERATED_S_3
+
+
+
+Splice
+
+CU
+
+
+
+
-__S_3:e--W2:w
-
-
-
+AUTOGENERATED_S_3:e--W2:w
+
+
+
-
+
-__S_4
-
-
-Splice
-
-CU
-
-
+AUTOGENERATED_S_4
+
+
+
+Splice
+
+CU
+
+
W21
-
-
-W21
-
-4x
-
-0.1 m
-
- 1:WH
-X2:1:A
-
-
-
- 2:BN
-X2:2:B
-
-
-
- 3:GN
-X2:3:C
-
-
-
- 4:YE
-X2:4:D
-
-
-
-
-
-
+
+
+
+W21
+
+4x
+
+0.1 m
+
+
+
+1:WH
+
+X2:1:A
+
+
+
+
+
+2:BN
+
+X2:2:B
+
+
+
+
+
+3:GN
+
+X2:3:C
+
+
+
+
+
+4:YE
+
+X2:4:D
+
+
+
+
+
+
-__S_4:e--W21:w
-
-
-
+AUTOGENERATED_S_4:e--W21:w
+
+
+
-
+
-__S_5
-
-
-Splice
-
-CU
-
-
-
-
+AUTOGENERATED_S_5
+
+
+
+Splice
+
+CU
+
+
+
+
-__S_5:e--W21:w
-
-
-
+AUTOGENERATED_S_5:e--W21:w
+
+
+
-
+
-__S_6
-
-
-Splice
-
-CU
-
-
-
-
+AUTOGENERATED_S_6
+
+
+
+Splice
+
+CU
+
+
+
+
-__S_6:e--W21:w
-
-
-
+AUTOGENERATED_S_6:e--W21:w
+
+
+
-
+
-__S_7
-
-
-Splice
-
-CU
-
-
-
-
+AUTOGENERATED_S_7
+
+
+
+Splice
+
+CU
+
+
+
+
-__S_7:e--W21:w
-
-
-
+AUTOGENERATED_S_7:e--W21:w
+
+
+
X2
-
-
-X2
-
-JST SM
-
-female
-
-4-pin
-
-1
-
-A
-
-2
-
-B
-
-3
-
-C
-
-4
-
-D
+
+
+
+X2
+
+JST SM
+
+female
+
+4-pin
+
+1
+
+A
+
+2
+
+B
+
+3
+
+C
+
+4
+
+D
X3
-
-
-X3
-
-JST SM
-
-male
-
-4-pin
-
-A
-
-1
-
-B
-
-2
-
-C
-
-3
-
-D
-
-4
+
+
+
+X3
+
+JST SM
+
+male
+
+4-pin
+
+A
+
+1
+
+B
+
+2
+
+C
+
+3
+
+D
+
+4
X2:e--X3:w
-
-
-
-
+
+
+
W3
-
-
-W3
-
-4x
-
-0.1 m
-
-X3:1:A
- 1:WH
-
-
-
-X3:2:B
- 2:BN
-
-
-
-X3:3:C
- 3:GN
-
-
-
-X3:4:D
- 4:YE
-
-
-
-
+
+
+
+W3
+
+4x
+
+0.1 m
+
+ X3:1:A
+
+1:WH
+
+
+
+
+
+ X3:2:B
+
+2:BN
+
+
+
+
+
+ X3:3:C
+
+3:GN
+
+
+
+
+
+ X3:4:D
+
+4:YE
+
+
+
+
+
+
X3:e--W3:w
-
-
-
+
+
+
X3:e--W3:w
-
-
-
+
+
+
X3:e--W3:w
-
-
-
+
+
+
X3:e--W3:w
-
-
-
+
+
+
-
+
-__F_1
-
-
-Ferrule
-
-GY
-
-
+AUTOGENERATED_F_1
+
+
+
+Ferrule
+
+GY
+
+
X4
-
-
-X4
-
-Screw terminal connector
-
-4-pin
-
-GN
-
-
-
-1
-
-W
-
-2
-
-X
-
-3
-
-Y
-
-4
-
-Z
-
-
+
+
+
+X4
+
+Screw terminal connector
+
+4-pin
+
+GN
+
+
+
+1
+
+W
+
+2
+
+X
+
+3
+
+Y
+
+4
+
+Z
+
+
-__F_1:e--X4:w
-
-
+AUTOGENERATED_F_1:e--X4:w
+
+
-
+
-__F_2
-
-
-Ferrule
-
-GY
-
-
-
-
+AUTOGENERATED_F_2
+
+
+
+Ferrule
+
+GY
+
+
+
+
-__F_2:e--X4:w
-
-
+AUTOGENERATED_F_2:e--X4:w
+
+
-
+
-__F_3
-
-
-Ferrule
-
-GY
-
-
-
-
+AUTOGENERATED_F_3
+
+
+
+Ferrule
+
+GY
+
+
+
+
-__F_3:e--X4:w
-
-
+AUTOGENERATED_F_3:e--X4:w
+
+
-
+
-__F_4
-
-
-Ferrule
-
-GY
-
-
-
-
+AUTOGENERATED_F_4
+
+
+
+Ferrule
+
+GY
+
+
+
+
-__F_4:e--X4:w
-
-
+AUTOGENERATED_F_4:e--X4:w
+
+
-
+
-W1:e--__S_1:w
-
-
-
+W1:e--AUTOGENERATED_S_1:w
+
+
+
-
+
-W1:e--__S_2:w
-
-
-
+W1:e--AUTOGENERATED_S_2:w
+
+
+
W1:e--S1:w
-
-
-
+
+
+
-
+
-W1:e--__S_3:w
-
-
-
+W1:e--AUTOGENERATED_S_3:w
+
+
+
-
+
-W2:e--__S_4:w
-
-
-
+W2:e--AUTOGENERATED_S_4:w
+
+
+
-
+
-W2:e--__S_5:w
-
-
-
+W2:e--AUTOGENERATED_S_5:w
+
+
+
-
+
-W2:e--__S_6:w
-
-
-
+W2:e--AUTOGENERATED_S_6:w
+
+
+
-
+
-W2:e--__S_7:w
-
-
-
+W2:e--AUTOGENERATED_S_7:w
+
+
+
W21:e--X2:w
-
-
-
+
+
+
W21:e--X2:w
-
-
-
+
+
+
W21:e--X2:w
-
-
-
+
+
+
W21:e--X2:w
-
-
-
+
+
+
-
+
-W3:e--__F_1:w
-
-
-
+W3:e--AUTOGENERATED_F_1:w
+
+
+
-
+
-W3:e--__F_2:w
-
-
-
+W3:e--AUTOGENERATED_F_2:w
+
+
+
-
+
-W3:e--__F_3:w
-
-
-
+W3:e--AUTOGENERATED_F_3:w
+
+
+
-
+
-W3:e--__F_4:w
-
-
-
+W3:e--AUTOGENERATED_F_4:w
+
+
+
-
+
-__WIRE_1:e--X2:w
-
-
-
+AUTOGENERATED_WIRE_1:e--X2:w
+
+
+
diff --git a/examples/readme.md b/examples/readme.md
index 88a4389ca..cd2c8442e 100644
--- a/examples/readme.md
+++ b/examples/readme.md
@@ -3,61 +3,85 @@
## Example 01

-[Source](ex01.yml) - [Bill of Materials](ex01.bom.tsv)
+[Source](ex01.yml) - [Bill of Materials](ex01.tsv)
## Example 02

-[Source](ex02.yml) - [Bill of Materials](ex02.bom.tsv)
+[Source](ex02.yml) - [Bill of Materials](ex02.tsv)
## Example 03

-[Source](ex03.yml) - [Bill of Materials](ex03.bom.tsv)
+[Source](ex03.yml) - [Bill of Materials](ex03.tsv)
## Example 04

-[Source](ex04.yml) - [Bill of Materials](ex04.bom.tsv)
+[Source](ex04.yml) - [Bill of Materials](ex04.tsv)
## Example 05

-[Source](ex05.yml) - [Bill of Materials](ex05.bom.tsv)
+[Source](ex05.yml) - [Bill of Materials](ex05.tsv)
## Example 06

-[Source](ex06.yml) - [Bill of Materials](ex06.bom.tsv)
+[Source](ex06.yml) - [Bill of Materials](ex06.tsv)
## Example 07

-[Source](ex07.yml) - [Bill of Materials](ex07.bom.tsv)
+[Source](ex07.yml) - [Bill of Materials](ex07.tsv)
## Example 08

-[Source](ex08.yml) - [Bill of Materials](ex08.bom.tsv)
+[Source](ex08.yml) - [Bill of Materials](ex08.tsv)
## Example 09

-[Source](ex09.yml) - [Bill of Materials](ex09.bom.tsv)
+[Source](ex09.yml) - [Bill of Materials](ex09.tsv)
## Example 10

-[Source](ex10.yml) - [Bill of Materials](ex10.bom.tsv)
+[Source](ex10.yml) - [Bill of Materials](ex10.tsv)
+
+
+## Example 11
+
+
+[Source](ex11.yml) - [Bill of Materials](ex11.tsv)
+
+
+## Example 12
+
+
+[Source](ex12.yml) - [Bill of Materials](ex12.tsv)
+
+
+## Example 13
+
+
+[Source](ex13.yml) - [Bill of Materials](ex13.tsv)
+
+
+## Example 14
+
+
+[Source](ex14.yml) - [Bill of Materials](ex14.tsv)
## Example 11
diff --git a/requirements.txt b/requirements.txt
index 07564c32e..9405dd187 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,3 +3,4 @@ graphviz
pillow
pyyaml
setuptools
+tabulate
diff --git a/setup.py b/setup.py
index b8577d53a..356d7dba0 100644
--- a/setup.py
+++ b/setup.py
@@ -15,18 +15,20 @@
author="Daniel Rojas",
# author_email='',
description="Easily document cables and wiring harnesses",
- long_description=open(README_PATH).read(),
+ long_description=README_PATH.read_text(),
long_description_content_type="text/markdown",
install_requires=[
"click",
- "pyyaml",
- "pillow",
"graphviz",
+ "pillow",
+ "pyyaml",
+ "tabulate",
],
license="GPLv3",
keywords="cable connector hardware harness wiring wiring-diagram wiring-harness",
url=APP_URL,
package_dir={"": "src"},
+ package_data={CMD_NAME: ["templates/*.html"]},
packages=find_packages("src"),
entry_points={
"console_scripts": [
@@ -37,7 +39,6 @@
"Development Status :: 4 - Beta",
"Environment :: Console",
"Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
diff --git a/src/wireviz/DataClasses.py b/src/wireviz/DataClasses.py
deleted file mode 100644
index 5b4bb068e..000000000
--- a/src/wireviz/DataClasses.py
+++ /dev/null
@@ -1,439 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from dataclasses import InitVar, dataclass, field
-from enum import Enum, auto
-from pathlib import Path
-from typing import Dict, List, Optional, Tuple, Union
-
-from wireviz.wv_colors import COLOR_CODES, Color, ColorMode, Colors, ColorScheme
-from wireviz.wv_helper import aspect_ratio, int2tuple
-
-# Each type alias have their legal values described in comments - validation might be implemented in the future
-PlainText = str # Text not containing HTML tags nor newlines
-Hypertext = str # Text possibly including HTML hyperlinks that are removed in all outputs except HTML output
-MultilineHypertext = (
- str # Hypertext possibly also including newlines to break lines in diagram output
-)
-
-Designator = PlainText # Case insensitive unique name of connector or cable
-
-# Literal type aliases below are commented to avoid requiring python 3.8
-ConnectorMultiplier = PlainText # = Literal['pincount', 'populated', 'unpopulated']
-CableMultiplier = (
- PlainText # = Literal['wirecount', 'terminations', 'length', 'total_length']
-)
-ImageScale = PlainText # = Literal['false', 'true', 'width', 'height', 'both']
-
-# Type combinations
-Pin = Union[int, PlainText] # Pin identifier
-PinIndex = int # Zero-based pin index
-Wire = Union[int, PlainText] # Wire number or Literal['s'] for shield
-NoneOrMorePins = Union[
- Pin, Tuple[Pin, ...], None
-] # None, one, or a tuple of pin identifiers
-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', ...]
-
-
-Side = Enum("Side", "LEFT RIGHT")
-
-
-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
- template_separator: str = "."
-
- 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 Tweak:
- override: Optional[Dict[Designator, Dict[str, Optional[str]]]] = None
- append: Union[str, List[str], None] = None
-
-
-@dataclass
-class Image:
- # Attributes of the image object
:
- src: str
- scale: Optional[ImageScale] = None
- # Attributes of the image cell
containing the image:
- width: Optional[int] = None
- height: Optional[int] = None
- fixedsize: Optional[bool] = None
- bgcolor: Optional[Color] = None
- # Contents of the text cell 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):
-
- 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
-
- if self.scale is None:
- if not self.width and not self.height:
- self.scale = "false"
- elif self.width and self.height:
- self.scale = "both"
- else:
- self.scale = "true" # When only one dimension is specified.
-
- if self.fixedsize:
- # If only one dimension is specified, compute the other
- # because Graphviz requires both when fixedsize=True.
- if self.height:
- if not self.width:
- self.width = self.height * aspect_ratio(self.src)
- else:
- if self.width:
- self.height = self.width / aspect_ratio(self.src)
-
-
-@dataclass
-class AdditionalComponent:
- type: MultilineHypertext
- subtype: Optional[MultilineHypertext] = None
- manufacturer: Optional[MultilineHypertext] = None
- mpn: Optional[MultilineHypertext] = None
- supplier: Optional[MultilineHypertext] = None
- spn: Optional[MultilineHypertext] = None
- pn: Optional[Hypertext] = None
- qty: float = 1
- unit: Optional[str] = None
- qty_multiplier: Union[ConnectorMultiplier, CableMultiplier, None] = None
- bgcolor: Optional[Color] = None
-
- @property
- def description(self) -> str:
- t = self.type.rstrip()
- st = f", {self.subtype.rstrip()}" if self.subtype else ""
- t = t + st
- return t
-
-
-@dataclass
-class Connector:
- name: Designator
- bgcolor: Optional[Color] = None
- bgcolor_title: Optional[Color] = None
- manufacturer: Optional[MultilineHypertext] = None
- mpn: Optional[MultilineHypertext] = None
- supplier: Optional[MultilineHypertext] = None
- spn: Optional[MultilineHypertext] = None
- pn: Optional[Hypertext] = None
- style: Optional[str] = None
- category: Optional[str] = None
- type: Optional[MultilineHypertext] = None
- subtype: Optional[MultilineHypertext] = None
- pincount: Optional[int] = None
- image: Optional[Image] = None
- 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
- show_name: Optional[bool] = None
- show_pincount: Optional[bool] = None
- hide_disconnected_pins: bool = False
- loops: List[List[Pin]] = field(default_factory=list)
- ignore_in_bom: bool = False
- additional_components: List[AdditionalComponent] = field(default_factory=list)
-
- def __post_init__(self) -> None:
-
- if isinstance(self.image, dict):
- self.image = Image(**self.image)
-
- self.ports_left = False
- self.ports_right = False
- self.visible_pins = {}
-
- if self.style == "simple":
- if self.pincount and self.pincount > 1:
- raise Exception(
- "Connectors with style set to simple may only have one pin"
- )
- self.pincount = 1
-
- if not self.pincount:
- self.pincount = max(
- len(self.pins), len(self.pinlabels), len(self.pincolors)
- )
- if not self.pincount:
- raise Exception(
- "You need to specify at least one, pincount, pins, pinlabels, or pincolors"
- )
-
- # create default list for pins (sequential) if not specified
- if not self.pins:
- self.pins = list(range(1, self.pincount + 1))
-
- if len(self.pins) != len(set(self.pins)):
- raise Exception("Pins are not unique")
-
- if self.show_name is None:
- # hide designators for simple and for auto-generated connectors by default
- self.show_name = self.style != "simple" and self.name[0:2] != "__"
-
- if self.show_pincount is None:
- # hide pincount for simple (1 pin) connectors by default
- self.show_pincount = self.style != "simple"
-
- for loop in self.loops:
- # TODO: allow using pin labels in addition to pin numbers, just like when defining regular connections
- # TODO: include properties of wire used to create the loop
- if len(loop) != 2:
- raise Exception("Loops must be between exactly two pins!")
- for pin in loop:
- if pin not in self.pins:
- raise Exception(
- f'Unknown loop pin "{pin}" for connector "{self.name}"!'
- )
- # Make sure loop connected pins are not hidden.
- self.activate_pin(pin, None)
-
- for i, item in enumerate(self.additional_components):
- if isinstance(item, dict):
- self.additional_components[i] = AdditionalComponent(**item)
-
- def activate_pin(self, pin: Pin, side: Side) -> None:
- self.visible_pins[pin] = True
- if side == Side.LEFT:
- self.ports_left = True
- elif side == Side.RIGHT:
- self.ports_right = True
-
- def get_qty_multiplier(self, qty_multiplier: Optional[ConnectorMultiplier]) -> int:
- if not qty_multiplier:
- return 1
- elif qty_multiplier == "pincount":
- return self.pincount
- elif qty_multiplier == "populated":
- return sum(self.visible_pins.values())
- elif qty_multiplier == "unpopulated":
- return max(0, self.pincount - sum(self.visible_pins.values()))
- else:
- raise ValueError(
- f"invalid qty multiplier parameter for connector {qty_multiplier}"
- )
-
-
-@dataclass
-class Cable:
- name: Designator
- bgcolor: Optional[Color] = None
- bgcolor_title: Optional[Color] = None
- manufacturer: Union[MultilineHypertext, List[MultilineHypertext], None] = None
- mpn: Union[MultilineHypertext, List[MultilineHypertext], None] = None
- supplier: Union[MultilineHypertext, List[MultilineHypertext], None] = None
- spn: Union[MultilineHypertext, List[MultilineHypertext], None] = None
- pn: Union[Hypertext, List[Hypertext], None] = None
- category: Optional[str] = None
- type: Optional[MultilineHypertext] = None
- gauge: Optional[float] = None
- gauge_unit: Optional[str] = None
- show_equiv: bool = False
- length: float = 0
- length_unit: Optional[str] = None
- color: Optional[Color] = None
- wirecount: Optional[int] = None
- shield: Union[bool, Color] = False
- image: Optional[Image] = None
- notes: Optional[MultilineHypertext] = None
- colors: List[Colors] = field(default_factory=list)
- wirelabels: List[Wire] = field(default_factory=list)
- color_code: Optional[ColorScheme] = None
- show_name: Optional[bool] = None
- show_wirecount: bool = True
- show_wirenumbers: Optional[bool] = None
- ignore_in_bom: bool = False
- additional_components: List[AdditionalComponent] = field(default_factory=list)
-
- def __post_init__(self) -> None:
-
- if isinstance(self.image, dict):
- self.image = Image(**self.image)
-
- if isinstance(self.gauge, str): # gauge and unit specified
- try:
- g, u = self.gauge.split(" ")
- except Exception:
- raise Exception(
- f"Cable {self.name} gauge={self.gauge} - Gauge must be a number, or number and unit separated by a space"
- )
- self.gauge = g
-
- if self.gauge_unit is not None:
- print(
- f"Warning: Cable {self.name} gauge_unit={self.gauge_unit} is ignored because its gauge contains {u}"
- )
- if u.upper() == "AWG":
- self.gauge_unit = u.upper()
- else:
- self.gauge_unit = u.replace("mm2", "mm\u00B2")
-
- elif self.gauge is not None: # gauge specified, assume mm2
- if self.gauge_unit is None:
- self.gauge_unit = "mm\u00B2"
- else:
- pass # gauge not specified
-
- if isinstance(self.length, str): # length and unit specified
- try:
- L, u = self.length.split(" ")
- L = float(L)
- except Exception:
- raise Exception(
- f"Cable {self.name} length={self.length} - Length must be a number, or number and unit separated by a space"
- )
- self.length = L
- if self.length_unit is not None:
- print(
- f"Warning: Cable {self.name} length_unit={self.length_unit} is ignored because its length contains {u}"
- )
- self.length_unit = u
- elif not isinstance(self.length, (int, float)):
- raise Exception(f"Cable {self.name} length has a non-numeric value")
- elif self.length_unit is None:
- self.length_unit = "m"
-
- self.connections = []
-
- if self.wirecount: # number of wires explicitly defined
- if self.colors: # use custom color palette (partly or looped if needed)
- pass
- 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]
- 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(
- "Unknown number of wires. Must specify wirecount or colors (implicit length)"
- )
- self.wirecount = len(self.colors)
-
- if self.wirelabels:
- if self.shield and "s" in self.wirelabels:
- raise Exception(
- '"s" may not be used as a wire label for a shielded cable.'
- )
-
- # if lists of part numbers are provided check this is a bundle and that it matches the wirecount.
- for idfield in [self.manufacturer, self.mpn, self.supplier, self.spn, self.pn]:
- if isinstance(idfield, list):
- if self.category == "bundle":
- # check the length
- if len(idfield) != self.wirecount:
- raise Exception("lists of part data must match wirecount")
- else:
- raise Exception("lists of part data are only supported for bundles")
-
- if self.show_name is None:
- # hide designators for auto-generated cables by default
- self.show_name = self.name[0:2] != "__"
-
- if self.show_wirenumbers is None:
- # by default, show wire numbers for cables, hide for bundles
- self.show_wirenumbers = self.category != "bundle"
-
- for i, item in enumerate(self.additional_components):
- if isinstance(item, dict):
- self.additional_components[i] = AdditionalComponent(**item)
-
- # The *_pin arguments accept a tuple, but it seems not in use with the current code.
- def connect(
- self,
- from_name: Optional[Designator],
- from_pin: NoneOrMorePinIndices,
- via_wire: OneOrMoreWires,
- to_name: Optional[Designator],
- to_pin: NoneOrMorePinIndices,
- ) -> None:
-
- from_pin = int2tuple(from_pin)
- via_wire = int2tuple(via_wire)
- to_pin = int2tuple(to_pin)
- if len(from_pin) != len(to_pin):
- raise Exception("from_pin must have the same number of elements as to_pin")
- for i, _ in enumerate(from_pin):
- self.connections.append(
- Connection(from_name, from_pin[i], via_wire[i], to_name, to_pin[i])
- )
-
- def get_qty_multiplier(self, qty_multiplier: Optional[CableMultiplier]) -> float:
- if not qty_multiplier:
- return 1
- elif qty_multiplier == "wirecount":
- return self.wirecount
- elif qty_multiplier == "terminations":
- return len(self.connections)
- elif qty_multiplier == "length":
- return self.length
- elif qty_multiplier == "total_length":
- return self.length * self.wirecount
- else:
- raise ValueError(
- f"invalid qty multiplier parameter for cable {qty_multiplier}"
- )
-
-
-@dataclass
-class Connection:
- from_name: Optional[Designator]
- from_pin: Optional[Pin]
- via_port: Wire
- to_name: Optional[Designator]
- to_pin: Optional[Pin]
-
-
-@dataclass
-class MatePin:
- from_name: Designator
- from_pin: Pin
- to_name: Designator
- to_pin: Pin
- shape: str
-
-
-@dataclass
-class MateComponent:
- from_name: Designator
- to_name: Designator
- shape: str
diff --git a/src/wireviz/Harness.py b/src/wireviz/Harness.py
deleted file mode 100644
index 30468a6a2..000000000
--- a/src/wireviz/Harness.py
+++ /dev/null
@@ -1,723 +0,0 @@
-# -*- coding: utf-8 -*-
-
-import re
-from collections import Counter
-from dataclasses import dataclass
-from itertools import zip_longest
-from pathlib import Path
-from typing import Any, List, Union
-
-from graphviz import Graph
-
-from wireviz import APP_NAME, APP_URL, __version__, wv_colors
-from wireviz.DataClasses import (
- Cable,
- Connector,
- MateComponent,
- MatePin,
- Metadata,
- Options,
- Tweak,
- Side,
-)
-from wireviz.svgembed import embed_svg_images_file
-from wireviz.wv_bom import (
- HEADER_MPN,
- HEADER_PN,
- HEADER_SPN,
- bom_list,
- component_table_entry,
- generate_bom,
- get_additional_component_table,
- pn_info_string,
-)
-from wireviz.wv_colors import get_color_hex, translate_color
-from wireviz.wv_gv_html import (
- html_bgcolor,
- html_bgcolor_attr,
- html_caption,
- html_colorbar,
- html_image,
- html_line_breaks,
- nested_html_table,
- remove_links,
-)
-from wireviz.wv_helper import (
- awg_equiv,
- flatten2d,
- is_arrow,
- mm2_equiv,
- open_file_read,
- open_file_write,
- tuplelist2tsv,
-)
-from wireviz.wv_html import generate_html_output
-
-OLD_CONNECTOR_ATTR = {
- "pinout": "was renamed to 'pinlabels' in v0.2",
- "pinnumbers": "was renamed to 'pins' in v0.2",
- "autogenerate": "is replaced with new syntax in v0.4",
-}
-
-def check_old(node: str, old_attr: dict, args: dict) -> None:
- """Raise exception for any outdated attributes in args."""
- for attr, descr in old_attr.items():
- if attr in args:
- raise ValueError(f"'{attr}' in {node}: '{attr}' {descr}")
-
-@dataclass
-class Harness:
- metadata: Metadata
- options: Options
- tweak: Tweak
-
- def __post_init__(self):
- self.connectors = {}
- self.cables = {}
- self.mates = []
- self._bom = [] # Internal Cache for generated bom
- self.additional_bom_items = []
-
- def add_connector(self, name: str, *args, **kwargs) -> None:
- check_old(f"Connector '{name}'", OLD_CONNECTOR_ATTR, kwargs)
- self.connectors[name] = Connector(name, *args, **kwargs)
-
- def add_cable(self, name: str, *args, **kwargs) -> None:
- self.cables[name] = Cable(name, *args, **kwargs)
-
- def add_mate_pin(self, from_name, from_pin, to_name, to_pin, arrow_type) -> None:
- self.mates.append(MatePin(from_name, from_pin, to_name, to_pin, arrow_type))
- self.connectors[from_name].activate_pin(from_pin, Side.RIGHT)
- self.connectors[to_name].activate_pin(to_pin, Side.LEFT)
-
- def add_mate_component(self, from_name, to_name, arrow_type) -> None:
- self.mates.append(MateComponent(from_name, to_name, arrow_type))
-
- def add_bom_item(self, item: dict) -> None:
- self.additional_bom_items.append(item)
-
- def connect(
- self,
- from_name: str,
- from_pin: (int, str),
- via_name: str,
- via_wire: (int, str),
- to_name: str,
- to_pin: (int, str),
- ) -> None:
- # check from and to connectors
- for name, pin in zip([from_name, to_name], [from_pin, to_pin]):
- if name is not None and name in self.connectors:
- connector = self.connectors[name]
- # check if provided name is ambiguous
- if pin in connector.pins and pin in connector.pinlabels:
- if connector.pins.index(pin) != connector.pinlabels.index(pin):
- raise Exception(
- f"{name}:{pin} is defined both in pinlabels and pins, for different pins."
- )
- # TODO: Maybe issue a warning if present in both lists but referencing the same pin?
- if pin in connector.pinlabels:
- if connector.pinlabels.count(pin) > 1:
- raise Exception(f"{name}:{pin} is defined more than once.")
- index = connector.pinlabels.index(pin)
- pin = connector.pins[index] # map pin name to pin number
- if name == from_name:
- from_pin = pin
- if name == to_name:
- to_pin = pin
- if not pin in connector.pins:
- raise Exception(f"{name}:{pin} not found.")
-
- # check via cable
- if via_name in self.cables:
- cable = self.cables[via_name]
- # check if provided name is ambiguous
- if via_wire in cable.colors and via_wire in cable.wirelabels:
- if cable.colors.index(via_wire) != cable.wirelabels.index(via_wire):
- raise Exception(
- f"{via_name}:{via_wire} is defined both in colors and wirelabels, for different wires."
- )
- # TODO: Maybe issue a warning if present in both lists but referencing the same wire?
- if via_wire in cable.colors:
- if cable.colors.count(via_wire) > 1:
- raise Exception(
- f"{via_name}:{via_wire} is used for more than one wire."
- )
- # list index starts at 0, wire IDs start at 1
- via_wire = cable.colors.index(via_wire) + 1
- elif via_wire in cable.wirelabels:
- if cable.wirelabels.count(via_wire) > 1:
- raise Exception(
- f"{via_name}:{via_wire} is used for more than one wire."
- )
- via_wire = (
- cable.wirelabels.index(via_wire) + 1
- ) # list index starts at 0, wire IDs start at 1
-
- # perform the actual connection
- self.cables[via_name].connect(from_name, from_pin, via_wire, to_name, to_pin)
- if from_name in self.connectors:
- self.connectors[from_name].activate_pin(from_pin, Side.RIGHT)
- if to_name in self.connectors:
- self.connectors[to_name].activate_pin(to_pin, Side.LEFT)
-
- def create_graph(self) -> Graph:
- dot = Graph()
- dot.body.append(f"// Graph generated by {APP_NAME} {__version__}\n")
- dot.body.append(f"// {APP_URL}\n")
- dot.attr(
- "graph",
- rankdir="LR",
- ranksep="2",
- bgcolor=wv_colors.translate_color(self.options.bgcolor, "HEX"),
- nodesep="0.33",
- 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=wv_colors.translate_color(self.options.bgcolor_node, "HEX"),
- fontname=self.options.fontname,
- )
- dot.attr("edge", style="bold", fontname=self.options.fontname)
-
- for connector in self.connectors.values():
-
- # If no wires connected (except maybe loop wires)?
- if not (connector.ports_left or connector.ports_right):
- connector.ports_left = True # Use left side pins.
-
- html = []
- # fmt: off
- rows = [[f'{html_bgcolor(connector.bgcolor_title)}{remove_links(connector.name)}'
- if connector.show_name else None],
- [pn_info_string(HEADER_PN, None, remove_links(connector.pn)),
- html_line_breaks(pn_info_string(HEADER_MPN, connector.manufacturer, connector.mpn)),
- html_line_breaks(pn_info_string(HEADER_SPN, connector.supplier, connector.spn))],
- [html_line_breaks(connector.type),
- html_line_breaks(connector.subtype),
- f'{connector.pincount}-pin' if connector.show_pincount else None,
- translate_color(connector.color, self.options.color_mode) if connector.color else None,
- html_colorbar(connector.color)],
- '' if connector.style != 'simple' else None,
- [html_image(connector.image)],
- [html_caption(connector.image)]]
- # fmt: on
-
- rows.extend(get_additional_component_table(self, connector))
- rows.append([html_line_breaks(connector.notes)])
- html.extend(nested_html_table(rows, html_bgcolor_attr(connector.bgcolor)))
-
- if connector.style != "simple":
- pinhtml = []
- pinhtml.append(
- ''
- )
-
- for pinindex, (pinname, pinlabel, pincolor) in enumerate(
- zip_longest(
- connector.pins, connector.pinlabels, connector.pincolors
- )
- ):
- if (
- connector.hide_disconnected_pins
- and not connector.visible_pins.get(pinname, False)
- ):
- continue
-
- pinhtml.append(" ")
- if connector.ports_left:
- pinhtml.append(f' {pinname} ')
- if pinlabel:
- pinhtml.append(f" {pinlabel} ")
- if connector.pincolors:
- if pincolor in wv_colors._color_hex.keys():
- # fmt: off
- pinhtml.append(f' {translate_color(pincolor, self.options.color_mode)} ')
- pinhtml.append( ' ')
- pinhtml.append( ' ')
- pinhtml.append(f' ')
- pinhtml.append( '
')
- pinhtml.append( ' ')
- # fmt: on
- else:
- pinhtml.append(' ')
-
- if connector.ports_right:
- pinhtml.append(f' {pinname} ')
- pinhtml.append(" ")
-
- pinhtml.append("
")
-
- html = [
- row.replace("", "\n".join(pinhtml))
- for row in html
- ]
-
- html = "\n".join(html)
- 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")
- if connector.ports_left:
- loop_side = "l"
- loop_dir = "w"
- elif connector.ports_right:
- loop_side = "r"
- loop_dir = "e"
- else:
- raise Exception("No side for loops")
- for loop in connector.loops:
- dot.edge(
- f"{connector.name}:p{loop[0]}{loop_side}:{loop_dir}",
- f"{connector.name}:p{loop[1]}{loop_side}:{loop_dir}",
- )
-
- # 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
- for cable in self.cables.values()
- for colorstr in cable.colors
- )
-
- for cable in self.cables.values():
-
- html = []
-
- awg_fmt = ""
- if cable.show_equiv:
- # Only convert units we actually know about, i.e. currently
- # mm2 and awg --- other units _are_ technically allowed,
- # and passed through as-is.
- if cable.gauge_unit == "mm\u00B2":
- awg_fmt = f" ({awg_equiv(cable.gauge)} AWG)"
- elif cable.gauge_unit.upper() == "AWG":
- awg_fmt = f" ({mm2_equiv(cable.gauge)} mm\u00B2)"
-
- # fmt: off
- rows = [[f'{html_bgcolor(cable.bgcolor_title)}{remove_links(cable.name)}'
- if cable.show_name else None],
- [pn_info_string(HEADER_PN, None,
- remove_links(cable.pn)) if not isinstance(cable.pn, list) else None,
- html_line_breaks(pn_info_string(HEADER_MPN,
- cable.manufacturer if not isinstance(cable.manufacturer, list) else None,
- cable.mpn if not isinstance(cable.mpn, list) else None)),
- html_line_breaks(pn_info_string(HEADER_SPN,
- cable.supplier if not isinstance(cable.supplier, list) else None,
- cable.spn if not isinstance(cable.spn, list) else None))],
- [html_line_breaks(cable.type),
- f'{cable.wirecount}x' if cable.show_wirecount else None,
- 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,
- translate_color(cable.color, self.options.color_mode) if cable.color else None,
- html_colorbar(cable.color)],
- '',
- [html_image(cable.image)],
- [html_caption(cable.image)]]
- # fmt: on
-
- rows.extend(get_additional_component_table(self, cable))
- rows.append([html_line_breaks(cable.notes)])
- html.extend(nested_html_table(rows, html_bgcolor_attr(cable.bgcolor)))
-
- wirehtml = []
- # conductor table
- wirehtml.append('')
- wirehtml.append(" ")
-
- for i, (connection_color, wirelabel) in enumerate(
- zip_longest(cable.colors, cable.wirelabels), 1
- ):
- wirehtml.append(" ")
- wirehtml.append(f" ")
- wirehtml.append(f" ")
-
- wireinfo = []
- if cable.show_wirenumbers:
- wireinfo.append(str(i))
- colorstr = wv_colors.translate_color(
- connection_color, self.options.color_mode
- )
- if colorstr:
- wireinfo.append(colorstr)
- if cable.wirelabels:
- wireinfo.append(wirelabel if wirelabel is not None else "")
- wirehtml.append(f' {":".join(wireinfo)}')
-
- wirehtml.append(f" ")
- wirehtml.append(f" ")
- wirehtml.append(" ")
-
- # fmt: off
- bgcolors = ['#000000'] + get_color_hex(connection_color, pad=pad) + ['#000000']
- wirehtml.append(f" ")
- wirehtml.append(f' ')
- wirehtml.append(' ')
- for j, bgcolor in enumerate(bgcolors[::-1]): # Reverse to match the curved wires when more than 2 colors
- wirehtml.append(f' ')
- wirehtml.append("
")
- wirehtml.append(" ")
- wirehtml.append(" ")
- # fmt: on
-
- # for bundles, individual wires can have part information
- if cable.category == "bundle":
- # create a list of wire parameters
- wireidentification = []
- if isinstance(cable.pn, list):
- wireidentification.append(
- pn_info_string(
- HEADER_PN, None, remove_links(cable.pn[i - 1])
- )
- )
- manufacturer_info = pn_info_string(
- HEADER_MPN,
- (
- cable.manufacturer[i - 1]
- if isinstance(cable.manufacturer, list)
- else None
- ),
- cable.mpn[i - 1] if isinstance(cable.mpn, list) else None,
- )
- supplier_info = pn_info_string(
- HEADER_SPN,
- (
- cable.supplier[i - 1]
- if isinstance(cable.supplier, list)
- else None
- ),
- cable.spn[i - 1] if isinstance(cable.spn, list) else None,
- )
- if manufacturer_info:
- wireidentification.append(html_line_breaks(manufacturer_info))
- if supplier_info:
- wireidentification.append(html_line_breaks(supplier_info))
- # print parameters into a table row under the wire
- if len(wireidentification) > 0:
- # fmt: off
- wirehtml.append(' ')
- wirehtml.append(' ')
- for attrib in wireidentification:
- wirehtml.append(f" {attrib} ")
- wirehtml.append("
")
- wirehtml.append(" ")
- # fmt: on
-
- if cable.shield:
- wirehtml.append(" ") # spacer
- wirehtml.append(" ")
- wirehtml.append(" ")
- wirehtml.append(" Shield ")
- wirehtml.append(" ")
- wirehtml.append(" ")
- if isinstance(cable.shield, str):
- # shield is shown with specified color and black borders
- shield_color_hex = wv_colors.get_color_hex(cable.shield)[0]
- attributes = (
- f'height="6" bgcolor="{shield_color_hex}" border="2" sides="tb"'
- )
- else:
- # shield is shown as a thin black wire
- attributes = f'height="2" bgcolor="#000000" border="0"'
- # fmt: off
- wirehtml.append(f' ')
- # fmt: on
-
- wirehtml.append(" ")
- wirehtml.append("
")
-
- html = [
- row.replace("", "\n".join(wirehtml)) for row in html
- ]
-
- # connections
- for connection in cable.connections:
- if isinstance(connection.via_port, int):
- # check if it's an actual wire and not a shield
- dot.attr(
- "edge",
- color=":".join(
- ["#000000"]
- + wv_colors.get_color_hex(
- cable.colors[connection.via_port - 1], pad=pad
- )
- + ["#000000"]
- ),
- )
- else: # it's a shield connection
- # shield is shown with specified color and black borders, or as a thin black wire otherwise
- dot.attr(
- "edge",
- color=(
- ":".join(["#000000", shield_color_hex, "#000000"])
- if isinstance(cable.shield, str)
- else "#000000"
- ),
- )
- if connection.from_pin is not None: # connect to left
- from_connector = self.connectors[connection.from_name]
- from_pin_index = from_connector.pins.index(connection.from_pin)
- from_port_str = (
- f":p{from_pin_index+1}r"
- if from_connector.style != "simple"
- else ""
- )
- code_left_1 = f"{connection.from_name}{from_port_str}:e"
- code_left_2 = f"{cable.name}:w{connection.via_port}:w"
- dot.edge(code_left_1, code_left_2)
- if from_connector.show_name:
- from_info = [
- str(connection.from_name),
- str(connection.from_pin),
- ]
- if from_connector.pinlabels:
- pinlabel = from_connector.pinlabels[from_pin_index]
- if pinlabel != "":
- from_info.append(pinlabel)
- from_string = ":".join(from_info)
- else:
- from_string = ""
- html = [
- row.replace(f"", from_string)
- for row in html
- ]
- if connection.to_pin is not None: # connect to right
- to_connector = self.connectors[connection.to_name]
- to_pin_index = to_connector.pins.index(connection.to_pin)
- to_port_str = (
- f":p{to_pin_index+1}l" if to_connector.style != "simple" else ""
- )
- code_right_1 = f"{cable.name}:w{connection.via_port}:e"
- code_right_2 = f"{connection.to_name}{to_port_str}:w"
- dot.edge(code_right_1, code_right_2)
- if to_connector.show_name:
- to_info = [str(connection.to_name), str(connection.to_pin)]
- if to_connector.pinlabels:
- pinlabel = to_connector.pinlabels[to_pin_index]
- if pinlabel != "":
- to_info.append(pinlabel)
- to_string = ":".join(to_info)
- else:
- to_string = ""
- html = [
- row.replace(f"", 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=style,
- fillcolor=translate_color(bgcolor, "HEX"),
- )
-
- def typecheck(name: str, value: Any, expect: type) -> None:
- if not isinstance(value, expect):
- raise Exception(
- f"Unexpected value type of {name}: Expected {expect}, got {type(value)}\n{value}"
- )
-
- # TODO?: Differ between override attributes and HTML?
- if self.tweak.override is not None:
- typecheck("tweak.override", self.tweak.override, dict)
- for k, d in self.tweak.override.items():
- typecheck(f"tweak.override.{k} key", k, str)
- typecheck(f"tweak.override.{k} value", d, dict)
- for a, v in d.items():
- typecheck(f"tweak.override.{k}.{a} key", a, str)
- typecheck(f"tweak.override.{k}.{a} value", v, (str, type(None)))
-
- # Override generated attributes of selected entries matching tweak.override.
- for i, entry in enumerate(dot.body):
- if isinstance(entry, str):
- # Find a possibly quoted keyword after leading TAB(s) and followed by [ ].
- match = re.match(
- r'^\t*(")?((?(1)[^"]|[^ "])+)(?(1)") \[.*\]$', entry, re.S
- )
- keyword = match and match[2]
- if keyword in self.tweak.override.keys():
- for attr, value in self.tweak.override[keyword].items():
- if value is None:
- entry, n_subs = re.subn(
- f'( +)?{attr}=("[^"]*"|[^] ]*)(?(1)| *)', "", entry
- )
- if n_subs < 1:
- print(
- f"Harness.create_graph() warning: {attr} not found in {keyword}!"
- )
- elif n_subs > 1:
- print(
- f"Harness.create_graph() warning: {attr} removed {n_subs} times in {keyword}!"
- )
- continue
-
- if len(value) == 0 or " " in value:
- value = value.replace('"', r"\"")
- value = f'"{value}"'
- entry, n_subs = re.subn(
- f'{attr}=("[^"]*"|[^] ]*)', f"{attr}={value}", entry
- )
- if n_subs < 1:
- # If attr not found, then append it
- entry = re.sub(r"\]$", f" {attr}={value}]", entry)
- elif n_subs > 1:
- print(
- f"Harness.create_graph() warning: {attr} overridden {n_subs} times in {keyword}!"
- )
-
- dot.body[i] = entry
-
- if self.tweak.append is not None:
- if isinstance(self.tweak.append, list):
- for i, element in enumerate(self.tweak.append, 1):
- typecheck(f"tweak.append[{i}]", element, str)
- dot.body.extend(self.tweak.append)
- else:
- typecheck("tweak.append", self.tweak.append, str)
- dot.body.append(self.tweak.append)
-
- for mate in self.mates:
- if mate.shape[0] == "<" and mate.shape[-1] == ">":
- dir = "both"
- elif mate.shape[0] == "<":
- dir = "back"
- elif mate.shape[-1] == ">":
- dir = "forward"
- else:
- dir = "none"
-
- if isinstance(mate, MatePin):
- color = "#000000"
- elif isinstance(mate, MateComponent):
- color = "#000000:#000000"
- else:
- raise Exception(f"{mate} is an unknown mate")
-
- from_connector = self.connectors[mate.from_name]
- if (
- isinstance(mate, MatePin)
- and self.connectors[mate.from_name].style != "simple"
- ):
- from_pin_index = from_connector.pins.index(mate.from_pin)
- from_port_str = f":p{from_pin_index+1}r"
- else: # MateComponent or style == 'simple'
- from_port_str = ""
- if (
- isinstance(mate, MatePin)
- and self.connectors[mate.to_name].style != "simple"
- ):
- to_pin_index = to_connector.pins.index(mate.to_pin)
- to_port_str = (
- f":p{to_pin_index+1}l"
- if isinstance(mate, MatePin)
- and self.connectors[mate.to_name].style != "simple"
- else ""
- )
- else: # MateComponent or style == 'simple'
- to_port_str = ""
- code_from = f"{mate.from_name}{from_port_str}:e"
- to_connector = self.connectors[mate.to_name]
- code_to = f"{mate.to_name}{to_port_str}:w"
-
- dot.attr("edge", color=color, style="dashed", dir=dir)
- dot.edge(code_from, code_to)
-
- return dot
-
- # cache for the GraphViz Graph object
- # do not access directly, use self.graph instead
- _graph = None
-
- @property
- def graph(self):
- if not self._graph: # no cached graph exists, generate one
- self._graph = self.create_graph()
- return self._graph # return cached graph
-
- @property
- def png(self):
- from io import BytesIO
-
- graph = self.graph
- data = BytesIO()
- data.write(graph.pipe(format="png"))
- data.seek(0)
- return data.read()
-
- @property
- def svg(self):
- graph = self.graph
- return embed_svg_images(graph.pipe(format="svg").decode("utf-8"), Path.cwd())
-
- def output(
- self,
- filename: (str, Path),
- view: bool = False,
- cleanup: bool = True,
- fmt: tuple = ("html", "png", "svg", "tsv"),
- ) -> None:
- # graphical output
- graph = self.graph
- svg_already_exists = Path(
- f"{filename}.svg"
- ).exists() # if SVG already exists, do not delete later
- # graphical output
- for f in fmt:
- if f in ("png", "svg", "html"):
- if f == "html": # if HTML format is specified,
- f = "svg" # generate SVG for embedding into HTML
- # SVG file will be renamed/deleted later
- _filename = f"{filename}.tmp" if f == "svg" else filename
- # TODO: prevent rendering SVG twice when both SVG and HTML are specified
- graph.format = f
- graph.render(filename=_filename, view=view, cleanup=cleanup)
- # embed images into SVG output
- if "svg" in fmt or "html" in fmt:
- embed_svg_images_file(f"{filename}.tmp.svg")
- # GraphViz output
- if "gv" in fmt:
- graph.save(filename=f"{filename}.gv")
- # BOM output
- bomlist = bom_list(self.bom())
- if "tsv" in fmt:
- open_file_write(f"{filename}.bom.tsv").write(tuplelist2tsv(bomlist))
- if "csv" in fmt:
- # TODO: implement CSV output (preferrably using CSV library)
- print("CSV output is not yet supported")
- # HTML output
- if "html" in fmt:
- generate_html_output(filename, bomlist, self.metadata, self.options)
- # PDF output
- if "pdf" in fmt:
- # TODO: implement PDF output
- print("PDF output is not yet supported")
- # delete SVG if not needed
- if "html" in fmt and not "svg" in fmt:
- # SVG file was just needed to generate HTML
- Path(f"{filename}.tmp.svg").unlink()
- elif "svg" in fmt:
- Path(f"{filename}.tmp.svg").replace(f"{filename}.svg")
-
- def bom(self):
- if not self._bom:
- self._bom = generate_bom(self)
- return self._bom
diff --git a/src/wireviz/__init__.py b/src/wireviz/__init__.py
index f869ecf46..aab6422df 100644
--- a/src/wireviz/__init__.py
+++ b/src/wireviz/__init__.py
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
# Please don't import anything in this file to avoid issues when it is imported in setup.py
-__version__ = "0.5-dev"
+__version__ = "0.5-dev+refactor"
CMD_NAME = "wireviz" # Lower case command and module name
APP_NAME = "WireViz" # Application name in texts meant to be human readable
-APP_URL = "https://github.com/formatc1702/WireViz"
+APP_URL = "https://github.com/wireviz/WireViz"
diff --git a/src/wireviz/svgembed.py b/src/wireviz/svgembed.py
deleted file mode 100644
index ab6b9f1ed..000000000
--- a/src/wireviz/svgembed.py
+++ /dev/null
@@ -1,52 +0,0 @@
-# -*- coding: utf-8 -*-
-
-import base64
-import re
-from pathlib import Path
-from typing import Union
-
-mime_subtype_replacements = {"jpg": "jpeg", "tif": "tiff"}
-
-
-def embed_svg_images(svg_in: str, base_path: Union[str, Path] = Path.cwd()) -> str:
- images_b64 = {} # cache of base64-encoded images
-
- def image_tag(pre: str, url: str, post: str) -> str:
- return f''
-
- def replace(match: re.Match) -> str:
- imgurl = match["URL"]
- if not imgurl in images_b64: # only encode/cache every unique URL once
- imgurl_abs = (Path(base_path) / imgurl).resolve()
- image = imgurl_abs.read_bytes()
- images_b64[imgurl] = base64.b64encode(image).decode("utf-8")
- return image_tag(
- match["PRE"] or "",
- f"data:image/{get_mime_subtype(imgurl)};base64, {images_b64[imgurl]}",
- match["POST"] or "",
- )
-
- pattern = re.compile(
- image_tag(r"(?P [^>]*?)?", r'(?P[^"]*?)', r"(?P [^>]*?)?"),
- re.IGNORECASE,
- )
- return pattern.sub(replace, svg_in)
-
-
-def get_mime_subtype(filename: Union[str, Path]) -> str:
- mime_subtype = Path(filename).suffix.lstrip(".").lower()
- if mime_subtype in mime_subtype_replacements:
- mime_subtype = mime_subtype_replacements[mime_subtype]
- return mime_subtype
-
-
-def embed_svg_images_file(
- filename_in: Union[str, Path], overwrite: bool = True
-) -> None:
- filename_in = Path(filename_in).resolve()
- filename_out = filename_in.with_suffix(".b64.svg")
- filename_out.write_text(
- embed_svg_images(filename_in.read_text(), filename_in.parent)
- )
- if overwrite:
- filename_out.replace(filename_in)
diff --git a/src/wireviz/templates/README.md b/src/wireviz/templates/README.md
new file mode 100644
index 000000000..693ea3c7b
--- /dev/null
+++ b/src/wireviz/templates/README.md
@@ -0,0 +1,52 @@
+# HTML Output Templates
+
+This is the standard folder where WireViz looks for an HTML output template file.
+
+## Which HTML Output Template File is Used?
+
+A named HTML output template can optionally be specified as
+`metadata.template.name` in the YAML input:
+```yaml
+metadata:
+ template:
+ name: din-6771
+```
+In the case above, WireViz will search for a template file named
+`din-6771.html` in these folders:
+1. In the same folder as the YAML input file.
+2. In this standard template folder.
+
+If no HTML output template is specified, the `simple` template is assumed
+(i.e. filename `simple.html`, and in this case,
+only the standard template folder is searched).
+
+## Placeholders in HTML Output Templates
+
+HTML output template files might contain placeholders that will be replaced by
+generated text by WireViz when producing HTML output based on such a template.
+A placeholder starts with ``.
+Note that there must be one single space between `--` and `%` at both ends.
+
+| Placeholder | Replaced by |
+| --- | --- |
+| `` | The application name, version, and URL |
+| `` | The value of `options.fontname` |
+| `` | The HEX color translation of `options.bgcolor` |
+| `` | The output path and filename without extension |
+| `` | The output filename without path nor extension |
+| `` | BOM as HTML table with headers at top |
+| `` | Reversed BOM as HTML table with headers at bottom |
+| `` | `1` (multi-page documents not yet supported) |
+| `` | `1` (multi-page documents not yet supported) |
+| `` | Embedded SVG diagram as valid HTML |
+| `` | Embedded base64 encoded PNG diagram as URI |
+| `` | String or numeric value of `metadata.{item}` |
+| `` | Category number `{i}` within dict value of `metadata.{item}` |
+| `` | Value of `metadata.{item}.{category}.{key}` |
+| `` | Value of `metadata.template.sheetsize` |
+
+Note that `{item}`, `{category}` and `{key}` in the description above can be
+any valid YAML key, and `{i}` is an integer representing the 1-based index of
+category entries in a dict `metadata.{item}` entry.
+The `{` and `}` characters are not literally part of the syntax, just used in
+this documentation to enclose the variable parts of the keywords.
diff --git a/src/wireviz/templates/din-6771.html b/src/wireviz/templates/din-6771.html
index 547a340c8..05f563f22 100644
--- a/src/wireviz/templates/din-6771.html
+++ b/src/wireviz/templates/din-6771.html
@@ -1,18 +1,23 @@
+
-
+
+
-
+
-
+
+
\ No newline at end of file
diff --git a/src/wireviz/build_examples.py b/src/wireviz/tools/build_examples.py
similarity index 92%
rename from src/wireviz/build_examples.py
rename to src/wireviz/tools/build_examples.py
index e54d0f5cc..7ad295a0c 100755
--- a/src/wireviz/build_examples.py
+++ b/src/wireviz/tools/build_examples.py
@@ -7,13 +7,12 @@
from pathlib import Path
script_path = Path(__file__).absolute()
-
-sys.path.insert(0, str(script_path.parent.parent)) # to find wireviz module
-from wv_helper import open_file_append, open_file_read, open_file_write
+sys.path.insert(0, str(script_path.parent.parent.parent)) # to find wireviz module
from wireviz import APP_NAME, __version__, wireviz
+from wireviz.wv_utils import open_file_append, open_file_read, open_file_write
-dir = script_path.parent.parent.parent
+dir = script_path.parent.parent.parent.parent
readme = "readme.md"
groups = {
"examples": {
@@ -32,10 +31,11 @@
"path": dir / "examples",
"prefix": "demo",
},
+ **{p.stem: {"path": p} for p in (dir / "tests").glob("**")},
}
input_extensions = [".yml"]
-extensions_not_containing_graphviz_output = [".gv", ".bom.tsv"]
+extensions_not_containing_graphviz_output = [".gv", ".tsv"]
extensions_containing_graphviz_output = [".png", ".svg", ".html"]
generated_extensions = (
extensions_not_containing_graphviz_output + extensions_containing_graphviz_output
@@ -44,7 +44,7 @@
def collect_filenames(description, groupkey, ext_list):
path = groups[groupkey]["path"]
- patterns = [f"{groups[groupkey]['prefix']}*{ext}" for ext in ext_list]
+ patterns = [f"{groups[groupkey].get('prefix', '')}*{ext}" for ext in ext_list]
if ext_list != input_extensions and readme in groups[groupkey]:
patterns.append(readme)
print(f'{description} {groupkey} in "{path}"')
@@ -88,7 +88,7 @@ def build_generated(groupkeys):
out.write(f"\n\n")
out.write(
- f"[Source]({yaml_file.name}) - [Bill of Materials]({yaml_file.stem}.bom.tsv)\n\n\n"
+ f"[Source]({yaml_file.name}) - [Bill of Materials]({yaml_file.stem}.tsv)\n\n\n"
)
@@ -98,7 +98,7 @@ def clean_generated(groupkeys):
for filename in collect_filenames("Cleaning", key, generated_extensions):
if filename.is_file():
print(f' rm "{filename}"')
- Path(filename).unlink()
+ filename.unlink()
def compare_generated(groupkeys, branch="", include_graphviz_output=False):
diff --git a/src/wireviz/wireviz.py b/src/wireviz/wireviz.py
index bc974e868..fac305ebd 100755
--- a/src/wireviz/wireviz.py
+++ b/src/wireviz/wireviz.py
@@ -1,7 +1,9 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
+import platform
import sys
+from errno import EINVAL, ENAMETOOLONG
from pathlib import Path
from typing import Any, Dict, List, Tuple, Union
@@ -10,16 +12,18 @@
if __name__ == "__main__":
sys.path.insert(0, str(Path(__file__).parent.parent)) # add src/wireviz to PATH
-from wireviz.DataClasses import Metadata, Options, Tweak
-from wireviz.Harness import Harness
-from wireviz.wv_helper import (
+from wireviz.wv_dataclasses import AUTOGENERATED_PREFIX, Metadata, Options, Tweak
+from wireviz.wv_harness import Harness
+from wireviz.wv_utils import (
expand,
+ file_read_text,
get_single_key_and_value,
is_arrow,
- open_file_read,
smart_file_resolve,
)
+from . import APP_NAME
+
def parse(
inp: Union[Path, str, Dict],
@@ -34,7 +38,7 @@ def parse(
and outputs the result as one or more files and/or as a function return value
Accepted inputs:
- * A path to a YAML source file to parse
+ * A Path object or a path-like string pointing to a YAML source file to parse
* A string containing the YAML data to parse
* A Python Dict containing the pre-parsed YAML data
@@ -44,10 +48,8 @@ def parse(
* "harness": the diagram as a Harness Python object
Supported output formats:
- * "csv": the BOM, as a comma-separated text file
* "gv": the diagram, as a GraphViz source file
* "html": the diagram and (depending on the template) the BOM, as a HTML file
- * "png": the diagram, as a PNG raster image
* "pdf": the diagram and (depending on the template) the BOM, as a PDF file
* "svg": the diagram, as a SVG vector image
* "tsv": the BOM, as a tab-separated text file
@@ -81,11 +83,16 @@ def parse(
* SVG data
* a Harness object
"""
+ # TODO: add CSV and PDF to docstring once they are supported
if not output_formats and not return_types:
raise Exception("No output formats or return types specified")
yaml_data, yaml_file = _get_yaml_data_and_path(inp)
+ if not isinstance(yaml_data, dict):
+ raise TypeError(
+ f"Expected a dict as top-level YAML input, but got: {type(yaml_data)}"
+ )
if output_formats:
# need to write data to file, determine output directory and filename
output_dir = _get_output_dir(yaml_file, output_dir)
@@ -93,7 +100,8 @@ def parse(
output_file = output_dir / output_name
if yaml_file:
- # if reading from file, ensure that input file's parent directory is included in image_paths
+ # if reading from file, ensure that input file's parent directory
+ # is included in image_paths
default_image_path = yaml_file.parent.resolve()
if not default_image_path in [Path(x).resolve() for x in image_paths]:
image_paths.append(default_image_path)
@@ -117,9 +125,7 @@ def parse(
# When title is not given, either deduce it from filename, or use default text.
if "title" not in harness.metadata:
- harness.metadata["title"] = (
- Path(yaml_file).stem if yaml_file else "WireViz diagram and BOM"
- )
+ harness.metadata["title"] = output_name or f"{APP_NAME} diagram and BOM"
# add items
# parse YAML input file ====================================================
@@ -131,7 +137,8 @@ def parse(
if len(yaml_data[sec]) > 0: # section has contents
if ty == dict:
for key, attribs in yaml_data[sec].items():
- # The Image dataclass might need to open an image file with a relative path.
+ # The Image dataclass might need to open
+ # an image file with a relative path.
image = attribs.get("image")
if isinstance(image, dict):
image_path = image["src"]
@@ -167,12 +174,16 @@ def resolve_designator(inp, separator):
autogenerated_designators[template] = (
autogenerated_designators.get(template, 0) + 1
)
- designator = f"__{template}_{autogenerated_designators[template]}"
+ designator = (
+ f"{AUTOGENERATED_PREFIX}"
+ f"{template}_{autogenerated_designators[template]}"
+ )
# check if redefining existing component to different template
if designator in designators_and_templates:
if designators_and_templates[designator] != template:
raise Exception(
- f"Trying to redefine {designator} from {designators_and_templates[designator]} to {template}"
+ f"Trying to redefine {designator}"
+ f" from {designators_and_templates[designator]} to {template}"
)
else:
designators_and_templates[designator] = template
@@ -284,7 +295,7 @@ def alternate_type(): # flip between connector and cable/arrow
# generate new connector instance from template
check_type(designator, template, "connector")
harness.add_connector(
- name=designator, **template_connectors[template]
+ designator=designator, **template_connectors[template]
)
elif designator in harness.cables: # existing cable instance
@@ -292,7 +303,9 @@ def alternate_type(): # flip between connector and cable/arrow
elif template in template_cables.keys():
# generate new cable instance from template
check_type(designator, template, "cable/arrow")
- harness.add_cable(name=designator, **template_cables[template])
+ harness.add_cable(
+ designator=designator, **template_cables[template]
+ )
elif is_arrow(designator):
check_type(designator, template, "cable/arrow")
@@ -302,7 +315,8 @@ def alternate_type(): # flip between connector and cable/arrow
f"{template} is an unknown template/designator/arrow."
)
- alternate_type() # entries in connection set must alternate between connectors and cables/arrows
+ # entries in connection set must alternate between connectors and cables/arrows
+ alternate_type()
# transpose connection set list
# before: one item per component, one subitem per connection in set
@@ -370,11 +384,13 @@ def alternate_type(): # flip between connector and cable/arrow
)
print(", ".join(forgotten_components))
- # harness population completed =============================================
-
if "additional_bom_items" in yaml_data:
for line in yaml_data["additional_bom_items"]:
- harness.add_bom_item(line)
+ harness.add_additional_bom_item(line)
+
+ # harness population completed =============================================
+
+ harness.populate_bom()
if output_formats:
harness.output(filename=output_file, fmt=output_formats, view=False)
@@ -397,20 +413,25 @@ def alternate_type(): # flip between connector and cable/arrow
return tuple(returns) if len(returns) != 1 else returns[0]
-def _get_yaml_data_and_path(inp: Union[str, Path, Dict]) -> (Dict, Path):
+def _get_yaml_data_and_path(inp: Union[str, Path, Dict]) -> Tuple[Dict, Path]:
# determine whether inp is a file path, a YAML string, or a Dict
if not isinstance(inp, Dict): # received a str or a Path
try:
yaml_path = Path(inp).expanduser().resolve(strict=True)
# if no FileNotFoundError exception happens, get file contents
- yaml_str = open_file_read(yaml_path).read()
- except (FileNotFoundError, OSError) as e:
- # if inp is a long YAML string, Pathlib will raise OSError: [errno.ENAMETOOLONG]
- # when trying to expand and resolve it as a path.
- # Catch this error, but raise any others
- from errno import ENAMETOOLONG
-
- if type(e) is OSError and e.errno != ENAMETOOLONG:
+ yaml_str = file_read_text(yaml_path)
+ except (FileNotFoundError, OSError, ValueError) as e:
+ # if inp is a long YAML string, Pathlib will normally raise
+ # FileNotFoundError or OSError(errno = ENAMETOOLONG) when
+ # trying to expand and resolve it as a path, but in Windows
+ # might ValueError or OSError(errno = EINVAL or None) be raised
+ # instead in some cases (depending on the Python version).
+ # Catch these specific errors, but raise any others.
+
+ if type(e) is OSError and e.errno not in (EINVAL, ENAMETOOLONG, None):
+ print(
+ f"OSError(errno={e.errno}) in Python {sys.version} at {platform.platform()}"
+ )
raise e
# file does not exist; assume inp is a YAML string
yaml_str = inp
diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py
index 27ee59b09..cf5c04f76 100644
--- a/src/wireviz/wv_bom.py
+++ b/src/wireviz/wv_bom.py
@@ -1,276 +1,87 @@
# -*- coding: utf-8 -*-
-from dataclasses import asdict
-from itertools import groupby
-from typing import Any, Dict, List, Optional, Tuple, Union
+from collections import namedtuple
+from dataclasses import dataclass
+from enum import Enum, IntEnum
+from typing import List, Optional, Union
-from wireviz.DataClasses import AdditionalComponent, Cable, Color, Connector
-from wireviz.wv_colors import translate_color
-from wireviz.wv_gv_html import html_bgcolor_attr, html_line_breaks
-from wireviz.wv_helper import clean_whitespace
+import tabulate as tabulate_module
-BOM_COLUMNS_ALWAYS = ("id", "description", "qty", "unit", "designators")
-BOM_COLUMNS_OPTIONAL = ("pn", "manufacturer", "mpn", "supplier", "spn")
-BOM_COLUMNS_IN_KEY = ("description", "unit") + BOM_COLUMNS_OPTIONAL
+from wireviz.wv_utils import html_line_breaks
-HEADER_PN = "P/N"
-HEADER_MPN = "MPN"
-HEADER_SPN = "SPN"
+BOM_HASH_FIELDS = "description qty_unit amount partnumbers"
-BOMKey = Tuple[str, ...]
-BOMColumn = str # = Literal[*BOM_COLUMNS_ALWAYS, *BOM_COLUMNS_OPTIONAL]
-BOMEntry = Dict[BOMColumn, Union[str, int, float, List[str], None]]
+BomEntry = namedtuple("BomEntry", "category qty designators")
+BomHash = namedtuple("BomHash", BOM_HASH_FIELDS)
+BomHashList = namedtuple("BomHashList", BOM_HASH_FIELDS)
+PartNumberInfo = namedtuple("PartNumberInfo", "pn manufacturer mpn supplier spn")
-def optional_fields(part: Union[Connector, Cable, AdditionalComponent]) -> BOMEntry:
- """Return part field values for the optional BOM columns as a dict."""
- part = asdict(part)
- return {field: part.get(field) for field in BOM_COLUMNS_OPTIONAL}
+# TODO: different BOM modes
+# BomMode
+# "normal" # no bubbles, full PN info in GV node
+# "bubbles" # = "full" -> maximum info in GV node
+# "hide PN info"
+# "PN crossref" = "PN bubbles" + "hide PN info"
+# "additionally: BOM table in GV graph label (#227)"
+# "title block in GV graph label"
-def get_additional_component_table(
- harness: "Harness", component: Union[Connector, Cable]
-) -> List[str]:
- """Return a list of diagram node table row strings with additional components."""
- rows = []
- if component.additional_components:
- rows.append(["Additional components"])
- # Ignore components that have qty 0
- for part in [
- part
- for part in component.additional_components
- if component.get_qty_multiplier(part.qty_multiplier)
- ]:
- common_args = {
- "qty": part.qty * component.get_qty_multiplier(part.qty_multiplier),
- "unit": part.unit,
- "bgcolor": part.bgcolor,
- }
- 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:
- rows.append(
- component_table_entry(
- part.description, **common_args, **optional_fields(part)
- )
- )
- return rows
-
-
-def get_additional_component_bom(component: Union[Connector, Cable]) -> List[BOMEntry]:
- """Return a list of BOM entries with additional components."""
- bom_entries = []
- # Ignore components that have qty 0
- for part in [
- part
- for part in component.additional_components
- if component.get_qty_multiplier(part.qty_multiplier)
- ]:
- bom_entries.append(
- {
- "description": part.description,
- "qty": part.qty * component.get_qty_multiplier(part.qty_multiplier),
- "unit": part.unit,
- "designators": component.name if component.show_name else None,
- **optional_fields(part),
- }
- )
- return bom_entries
-
-
-def bom_entry_key(entry: BOMEntry) -> BOMKey:
- """Return a tuple of string values from the dict that must be equal to join BOM entries."""
- if "key" not in entry:
- entry["key"] = tuple(
- clean_whitespace(make_str(entry.get(c))) for c in BOM_COLUMNS_IN_KEY
- )
- return entry["key"]
-
+BomCategory = IntEnum( # to enforce ordering in BOM
+ "BomEntry", "CONNECTOR CABLE WIRE ADDITIONAL_INSIDE ADDITIONAL_OUTSIDE"
+)
+QtyMultiplierConnector = Enum(
+ "QtyMultiplierConnector", "PINCOUNT POPULATED UNPOPULATED CONNECTIONS"
+)
+QtyMultiplierCable = Enum(
+ "QtyMultiplierCable", "WIRECOUNT TERMINATION LENGTH TOTAL_LENGTH"
+)
-def generate_bom(harness: "Harness") -> List[BOMEntry]:
- """Return a list of BOM entries generated from the harness."""
- from wireviz.Harness import Harness # Local import to avoid circular imports
+PART_NUMBER_HEADERS = PartNumberInfo(
+ pn="P/N", manufacturer=None, mpn="MPN", supplier=None, spn="SPN"
+)
- bom_entries = []
- # connectors
- for connector in harness.connectors.values():
- if not connector.ignore_in_bom:
- description = (
- "Connector"
- + (f", {connector.type}" if connector.type else "")
- + (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)}"
- if connector.color
- else ""
- )
- )
- bom_entries.append(
- {
- "description": description,
- "designators": connector.name if connector.show_name else None,
- **optional_fields(connector),
- }
- )
-
- # add connectors aditional components to bom
- bom_entries.extend(get_additional_component_bom(connector))
-
- # cables
- # TODO: If category can have other non-empty values than 'bundle', maybe it should be part of description?
- for cable in harness.cables.values():
- if not cable.ignore_in_bom:
- if cable.category != "bundle":
- # process cable as a single entity
- description = (
- "Cable"
- + (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 "")
- + (
- 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),
- }
- )
- else:
- # add each wire from the bundle to the bom
- for index, color in enumerate(cable.colors):
- description = (
- "Wire"
- + (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)}"
- 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()
- },
- }
- )
-
- # add cable/bundles aditional components to bom
- bom_entries.extend(get_additional_component_bom(cable))
- # add harness aditional components to bom directly, as they both are List[BOMEntry]
- bom_entries.extend(harness.additional_bom_items)
+def partnumbers2list(
+ partnumbers: PartNumberInfo, parent_partnumbers: PartNumberInfo = None
+) -> List[str]:
+ if parent_partnumbers is None:
+ _is_toplevel = True
+ parent_partnumbers = partnumbers
+ else:
+ _is_toplevel = False
- # remove line breaks if present and cleanup any resulting whitespace issues
- bom_entries = [
- {k: clean_whitespace(v) for k, v in entry.items()} for entry in bom_entries
- ]
+ # Note: != operator used as XOR in the following section (https://stackoverflow.com/a/433161)
- # deduplicate bom
- bom = []
- for _, group in groupby(sorted(bom_entries, key=bom_entry_key), key=bom_entry_key):
- group_entries = list(group)
- designators = sum(
- (make_list(entry.get("designators")) for entry in group_entries), []
- )
- total_qty = sum(entry.get("qty", 1) for entry in group_entries)
- bom.append(
- {
- **group_entries[0],
- "qty": round(total_qty, 3),
- "designators": sorted(set(designators)),
- }
+ if _is_toplevel != isinstance(parent_partnumbers.pn, List):
+ # top level and not a list, or wire level and list
+ cell_pn = pn_info_string(PART_NUMBER_HEADERS.pn, None, partnumbers.pn)
+ else:
+ # top level and list -> do per wire later
+ # wire level and not list -> already done at top level
+ cell_pn = None
+
+ if _is_toplevel != isinstance(parent_partnumbers.mpn, List):
+ # TODO: edge case: different manufacturers, but same MPN?
+ cell_mpn = pn_info_string(
+ PART_NUMBER_HEADERS.mpn, partnumbers.manufacturer, partnumbers.mpn
)
+ else:
+ cell_mpn = None
- # add an incrementing id to each bom entry
- return [{**entry, "id": index} for index, entry in enumerate(bom, 1)]
-
-
-def get_bom_index(bom: List[BOMEntry], target: BOMKey) -> int:
- """Return id of BOM entry or raise exception if not found."""
- for entry in bom:
- if bom_entry_key(entry) == target:
- return entry["id"]
- raise Exception("Internal error: No BOM entry found matching: " + "|".join(target))
-
-
-def bom_list(bom: List[BOMEntry]) -> List[List[str]]:
- """Return list of BOM rows as lists of column strings with headings in top row."""
- keys = list(BOM_COLUMNS_ALWAYS) # Always include this fixed set of BOM columns.
- for fieldname in BOM_COLUMNS_OPTIONAL:
- # Include only those optional BOM columns that are in use.
- if any(entry.get(fieldname) for entry in bom):
- keys.append(fieldname)
- # Custom mapping from internal name to BOM column headers.
- # Headers not specified here are generated by capitilising the internal name.
- bom_headings = {
- "pn": HEADER_PN,
- "mpn": HEADER_MPN,
- "spn": HEADER_SPN,
- }
- return [
- [bom_headings.get(k, k.capitalize()) for k in keys]
- ] + [ # Create header row with key names
- [make_str(entry.get(k)) for k in keys] for entry in bom
- ] # Create string list for each entry row
-
+ if _is_toplevel != isinstance(parent_partnumbers.spn, List):
+ # TODO: edge case: different suppliers, but same SPN?
+ cell_spn = pn_info_string(
+ PART_NUMBER_HEADERS.spn, partnumbers.supplier, partnumbers.spn
+ )
+ else:
+ cell_spn = None
-def component_table_entry(
- type: str,
- qty: Union[int, float],
- unit: Optional[str] = None,
- bgcolor: Optional[Color] = None,
- pn: Optional[str] = None,
- manufacturer: Optional[str] = None,
- mpn: Optional[str] = None,
- supplier: Optional[str] = None,
- spn: Optional[str] = None,
-) -> str:
- """Return a diagram node table row string with an additional component."""
- part_number_list = [
- pn_info_string(HEADER_PN, None, pn),
- pn_info_string(HEADER_MPN, manufacturer, mpn),
- pn_info_string(HEADER_SPN, supplier, spn),
- ]
- output = (
- f"{qty}"
- + (f" {unit}" if unit else "")
- + f" x {type}"
- + ("
" if any(part_number_list) else "")
- + (", ".join([pn for pn in part_number_list if pn]))
- )
- # format the above output as left aligned text in a single visible cell
- # indent is set to two to match the indent in the generated html table
- return f"""
- {html_line_breaks(output)}
-
"""
+ cell_contents = [cell_pn, cell_mpn, cell_spn]
+ if any(cell_contents):
+ return [html_line_breaks(cell) for cell in cell_contents]
+ else:
+ return None
def pn_info_string(
@@ -284,16 +95,51 @@ def pn_info_string(
return None
-def index_if_list(value: Any, index: int) -> Any:
- """Return the value indexed if it is a list, or simply the value otherwise."""
- return value[index] if isinstance(value, list) else value
-
-
-def make_list(value: Any) -> list:
- """Return value if a list, empty list if None, or single element list otherwise."""
- return value if isinstance(value, list) else [] if value is None else [value]
+def bom_list(bom):
+ headers = (
+ "# Qty Unit Description Amount Unit Designators "
+ "P/N Manufacturer MPN Supplier SPN Category".split(" ")
+ )
+ rows = []
+ rows.append(headers)
+ # fill rows
+ for hash, entry in bom.items():
+ cells = [
+ entry["id"],
+ entry["qty"],
+ hash.qty_unit,
+ hash.description,
+ hash.amount.number if hash.amount else None,
+ hash.amount.unit if hash.amount else None,
+ ", ".join(sorted(entry["designators"])),
+ ]
+ if hash.partnumbers:
+ cells.extend(
+ [
+ hash.partnumbers.pn,
+ hash.partnumbers.manufacturer,
+ hash.partnumbers.mpn,
+ hash.partnumbers.supplier,
+ hash.partnumbers.spn,
+ ]
+ )
+ else:
+ cells.extend([None, None, None, None, None])
+ # cells.extend([f"{entry['category']} ({entry['category'].name})"]) # for debugging
+ rows.append(cells)
+ # remove empty columns
+ transposed = list(map(list, zip(*rows)))
+ transposed = [
+ column
+ for column in transposed
+ if any([cell is not None for cell in column[1:]])
+ # ^ ignore header cell in check
+ ]
+ rows = list(map(list, zip(*transposed)))
+ return rows
-def make_str(value: Any) -> str:
- """Return comma separated elements if a list, empty string if None, or value as a string otherwise."""
- return ", ".join(str(element) for element in make_list(value))
+def print_bom_table(bom):
+ print()
+ print(tabulate_module.tabulate(bom_list(bom), headers="firstrow"))
+ print()
diff --git a/src/wireviz/wv_cli.py b/src/wireviz/wv_cli.py
index afb024947..a1117ab6d 100644
--- a/src/wireviz/wv_cli.py
+++ b/src/wireviz/wv_cli.py
@@ -11,21 +11,24 @@
import wireviz.wireviz as wv
from wireviz import APP_NAME, __version__
-from wireviz.wv_helper import open_file_read
+from wireviz.wv_utils import file_read_text
format_codes = {
- # "c": "csv",
+ # "c": "csv", # TODO: support CSV
"g": "gv",
"h": "html",
"p": "png",
- # "P": "pdf",
+ # "P": "pdf", # TODO: support PDF
"s": "svg",
"t": "tsv",
}
-epilog = "The -f or --format option accepts a string containing one or more of the "
-epilog += "following characters to specify which file types to output:\n"
-epilog += ", ".join([f"{key} ({value.upper()})" for key, value in format_codes.items()])
+
+epilog = (
+ "The -f or --format option accepts a string containing one or more of the "
+ "following characters to specify which file types to output:\n"
+ + f", ".join([f"{key} ({value.upper()})" for key, value in format_codes.items()])
+)
@click.command(
@@ -62,7 +65,10 @@
"--output-name",
default=None,
type=str,
- help="File name (without extension) to use for output files, if different from input file name.",
+ help=(
+ "File name (without extension) to use for output files, "
+ "if different from input file name."
+ ),
)
@click.option(
"-V",
@@ -75,7 +81,7 @@ def wireviz(file, format, prepend, output_dir, output_name, version):
"""
Parses the provided FILE and generates the specified outputs.
"""
- print()
+ print() # blank line before execution
print(f"{APP_NAME} {__version__}")
if version:
return # print version number only and exit
@@ -109,9 +115,11 @@ def wireviz(file, format, prepend, output_dir, output_name, version):
prepend_file = Path(prepend_file)
if not prepend_file.exists():
raise Exception(f"File does not exist:\n{prepend_file}")
+ if not prepend_file.is_file():
+ raise Exception(f"Path is not a file:\n{prepend_file}")
print("Prepend file:", prepend_file)
- prepend_input += open_file_read(prepend_file).read() + "\n"
+ prepend_input += file_read_text(prepend_file) + "\n"
else:
prepend_input = ""
@@ -120,6 +128,8 @@ def wireviz(file, format, prepend, output_dir, output_name, version):
file = Path(file)
if not file.exists():
raise Exception(f"File does not exist:\n{file}")
+ if not file.is_file():
+ raise Exception(f"Path is not a file:\n{file}")
# file_out = file.with_suffix("") if not output_file else output_file
_output_dir = file.parent if not output_dir else output_dir
@@ -130,7 +140,7 @@ def wireviz(file, format, prepend, output_dir, output_name, version):
"Output file: ", f"{Path(_output_dir / _output_name)}.{output_formats_str}"
)
- yaml_input = open_file_read(file).read()
+ yaml_input = file_read_text(file)
file_dir = file.parent
yaml_input = prepend_input + yaml_input
@@ -146,7 +156,7 @@ def wireviz(file, format, prepend, output_dir, output_name, version):
image_paths=list(image_paths),
)
- print()
+ print() # blank line after execution
if __name__ == "__main__":
diff --git a/src/wireviz/wv_colors.py b/src/wireviz/wv_colors.py
index 62957f98b..f7247b7d3 100644
--- a/src/wireviz/wv_colors.py
+++ b/src/wireviz/wv_colors.py
@@ -1,6 +1,205 @@
# -*- 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
+
+padding_amount = 1
+
+ColorOutputMode = Enum(
+ "ColorOutputMode", "EN_LOWER EN_UPPER DE_LOWER DE_UPPER HTML_LOWER HTML_UPPER"
+)
+
+color_output_mode = ColorOutputMode.EN_UPPER
+
+KnownColor = namedtuple("KnownColor", "html code_de full_en full_de")
+
+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("#00aa00", "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"),
+}
+
+
+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]
+
+
+@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):
+ # treat None as a known color
+ 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 - why not?
+ elif not isinstance(inp, str):
+ raise Exception(f"Unknown single color {inp}!")
+ else:
+ inp_upper = inp.upper()
+ if inp_upper in known_colors.keys():
+ self._code_en = inp_upper
+ self._html = known_colors[inp_upper].html
+ else:
+ try: # Maybe inp is an int as string?
+ inp = f"#{int(inp, 0):06x}"
+ except ValueError:
+ pass # assume it's a valid HTML color name
+ 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 or integer (type check done inside)
+ self.colors.append(SingleColor(item))
+ elif isinstance(inp, SingleColor): # single color
+ self.colors = [inp]
+ elif isinstance(inp, int):
+ self.colors = [SingleColor(inp)]
+ elif not isinstance(inp, str):
+ raise Exception(f"Unknown multi-color {inp}!")
+ elif ":" in inp: # split input into list
+ self.colors = [SingleColor(item) for item in inp.split(":")]
+ else:
+ if 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]
+ return
+ # assume it's a valid 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(self)}")
+ return [str(color) for color in out]
+
+ @property
+ def html_padded(self):
+ return ":".join(self.html_padded_list)
+
COLOR_CODES = {
# fmt: off
@@ -38,164 +237,3 @@
"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 == "" or input is None:
- return ""
- 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()
diff --git a/src/wireviz/wv_dataclasses.py b/src/wireviz/wv_dataclasses.py
new file mode 100644
index 000000000..ff7fa4b77
--- /dev/null
+++ b/src/wireviz/wv_dataclasses.py
@@ -0,0 +1,827 @@
+# -*- coding: utf-8 -*-
+
+from collections import namedtuple
+from dataclasses import dataclass, field
+from enum import Enum
+from itertools import zip_longest
+from typing import Any, Dict, List, Optional, Tuple, Union
+
+from wireviz.wv_bom import (
+ BomHash,
+ BomHashList,
+ PartNumberInfo,
+ QtyMultiplierCable,
+ QtyMultiplierConnector,
+)
+from wireviz.wv_colors import (
+ COLOR_CODES,
+ ColorOutputMode,
+ MultiColor,
+ SingleColor,
+ get_color_by_colorcode_index,
+)
+from wireviz.wv_utils import (
+ NumberAndUnit,
+ aspect_ratio,
+ awg_equiv,
+ mm2_equiv,
+ parse_number_and_unit,
+ remove_links,
+)
+
+# Each type alias have their legal values described in comments
+# - validation might be implemented in the future
+PlainText = str # Text not containing HTML tags nor newlines
+Hypertext = str # Text possibly including HTML hyperlinks that are removed in all outputs except HTML output
+MultilineHypertext = (
+ str # Hypertext possibly also including newlines to break lines in diagram output
+)
+
+Designator = PlainText # Case insensitive unique name of connector or cable
+
+# Literal type aliases below are commented to avoid requiring python 3.8
+ImageScale = PlainText # = Literal['false', 'true', 'width', 'height', 'both']
+
+# Type combinations
+Pin = Union[int, PlainText] # Pin identifier
+PinIndex = int # Zero-based pin index
+Wire = Union[int, PlainText] # Wire number or Literal['s'] for shield
+NoneOrMorePins = Union[
+ Pin, Tuple[Pin, ...], None
+] # None, one, or a tuple of pin identifiers
+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', ...]
+
+
+Side = Enum("Side", "LEFT RIGHT")
+ArrowDirection = Enum("ArrowDirection", "NONE BACK FORWARD BOTH")
+ArrowWeight = Enum("ArrowWeight", "SINGLE DOUBLE")
+
+AUTOGENERATED_PREFIX = "AUTOGENERATED_"
+
+
+@dataclass
+class Arrow:
+ direction: ArrowDirection
+ weight: ArrowWeight
+
+
+class Metadata(dict):
+ pass
+
+
+@dataclass
+class Options:
+ fontname: PlainText = "arial"
+ 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_output_mode: ColorOutputMode = ColorOutputMode.EN_UPPER
+ mini_bom_mode: bool = True
+ template_separator: str = "."
+ _pad: int = 0
+ # TODO: resolve template and image paths during rendering, not during YAML parsing
+ _template_paths: List = field(default_factory=list)
+ _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:
+ 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 Tweak:
+ override: Optional[Dict[Designator, Dict[str, Optional[str]]]] = None
+ append: Union[str, List[str], None] = None
+
+
+@dataclass
+class Image:
+ # Attributes of the image object
:
+ src: str
+ scale: Optional[ImageScale] = None
+ # Attributes of the image cell
containing the image:
+ width: Optional[int] = None
+ height: Optional[int] = None
+ fixedsize: Optional[bool] = None
+ bgcolor: SingleColor = None
+ # Contents of the text cell 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
+
+ if self.scale is None:
+ if not self.width and not self.height:
+ self.scale = "false"
+ elif self.width and self.height:
+ self.scale = "both"
+ else:
+ self.scale = "true" # When only one dimension is specified.
+
+ if self.fixedsize:
+ # If only one dimension is specified, compute the other
+ # because Graphviz requires both when fixedsize=True.
+ if self.height:
+ if not self.width:
+ self.width = self.height * aspect_ratio(self.src)
+ else:
+ if self.width:
+ self.height = self.width / aspect_ratio(self.src)
+
+
+@dataclass
+class PinClass:
+ index: int
+ id: str
+ label: str
+ color: MultiColor
+ parent: str # designator of parent connector
+ _num_connections = 0 # incremented in Connector.connect()
+ _anonymous: bool = False # true for pins on autogenerated connectors
+ _simple: bool = False # true for simple connector
+
+ def __str__(self):
+ snippets = [ # use str() for each in case they are int or other non-str
+ str(self.parent) if not self._anonymous else "",
+ str(self.id) if not self._anonymous and not self._simple else "",
+ str(self.label) if self.label else "",
+ ]
+ return ":".join([snip for snip in snippets if snip != ""])
+
+
+@dataclass
+class Component:
+ category: Optional[str] = None # currently only used by cables, to define bundles
+ type: Union[MultilineHypertext, List[MultilineHypertext]] = None
+ subtype: Union[MultilineHypertext, List[MultilineHypertext]] = None
+
+ # part number
+ partnumbers: PartNumberInfo = None # filled by __post_init__()
+ # the following are provided for user convenience and should not be accessed later.
+ # their contents are loaded into partnumbers during the child class __post_init__()
+ pn: str = None
+ manufacturer: str = None
+ mpn: str = None
+ supplier: str = None
+ spn: str = None
+ # BOM info
+ qty: Optional[Union[None, int, float]] = None
+ amount: Optional[NumberAndUnit] = None
+ sum_amounts_in_bom: bool = True
+ ignore_in_bom: bool = False
+ bom_id: Optional[str] = None # to be filled after harness is built
+
+ def __post_init__(self):
+ partnos = [self.pn, self.manufacturer, self.mpn, self.supplier, self.spn]
+ partnos = [remove_links(entry) for entry in partnos]
+ partnos = tuple(partnos)
+ self.partnumbers = PartNumberInfo(*partnos)
+ self.amount = parse_number_and_unit(self.amount, None)
+
+ @property
+ def bom_hash(self) -> BomHash:
+ if isinstance(self, AdditionalComponent):
+ _amount = self.amount_computed
+ else:
+ _amount = self.amount
+
+ if self.sum_amounts_in_bom:
+ _hash = BomHash(
+ description=self.description,
+ qty_unit=_amount.unit if _amount else None,
+ amount=None,
+ partnumbers=self.partnumbers,
+ )
+ else:
+ _hash = BomHash(
+ description=self.description,
+ qty_unit=None,
+ amount=_amount,
+ partnumbers=self.partnumbers,
+ )
+ return _hash
+
+ @property
+ def has_pn_info(self) -> bool:
+ return any([self.pn, self.manufacturer, self.mpn, self.supplier, self.spn])
+
+ @property
+ def description(self) -> str:
+ return f"{self.type}{', ' + self.subtype if self.subtype else ''}"
+
+
+@dataclass
+class AdditionalBomItem(Component):
+ designators: Optional[str] = None
+
+ @property
+ def additional_components(self):
+ # An additional item may not have further nested additional comonents.
+ # This property is currently needed for objects in the same list as
+ # TopLevelGraphicalComponent objects in a Harness method.
+ return []
+
+
+@dataclass
+class GraphicalComponent(Component): # abstract class
+ bgcolor: Optional[SingleColor] = None
+
+ def __post_init__(self):
+ super().__post_init__()
+ self.bgcolor = SingleColor(self.bgcolor)
+
+
+@dataclass
+class AdditionalComponent(GraphicalComponent):
+ qty_multiplier: Union[QtyMultiplierConnector, QtyMultiplierCable, int] = 1
+ qty_computed: Optional[int] = None
+ explicit_qty: bool = True
+ amount_computed: Optional[NumberAndUnit] = None
+ note: str = None
+
+ def __post_init__(self):
+ super().__post_init__()
+
+ if isinstance(self.qty_multiplier, float) or isinstance(
+ self.qty_multiplier, int
+ ):
+ pass
+ else:
+ self.qty_multiplier = self.qty_multiplier.upper()
+ if self.qty_multiplier in QtyMultiplierConnector.__members__.keys():
+ self.qty_multiplier = QtyMultiplierConnector[self.qty_multiplier]
+ elif self.qty_multiplier in QtyMultiplierCable.__members__.keys():
+ self.qty_multiplier = QtyMultiplierCable[self.qty_multiplier]
+ else:
+ raise Exception(f"Unknown qty multiplier: {self.qty_multiplier}")
+
+ if self.qty is None and self.qty_multiplier in [
+ QtyMultiplierCable.TOTAL_LENGTH,
+ QtyMultiplierCable.LENGTH,
+ 1,
+ ]: # simplify add.comp. table in parent node for implicit qty 1
+ self.qty = 1
+ self.explicit_qty = False
+
+
+@dataclass
+class TopLevelGraphicalComponent(GraphicalComponent): # abstract class
+ # component properties
+ designator: Designator = None
+ color: Optional[MultiColor] = None
+ image: Optional[Image] = None
+ additional_parameters: Optional[Dict] = None
+ additional_components: List[AdditionalComponent] = field(default_factory=list)
+ notes: Optional[MultilineHypertext] = None
+ # rendering options
+ bgcolor_title: Optional[SingleColor] = None
+ show_name: Optional[bool] = None
+
+
+@dataclass
+class Connector(TopLevelGraphicalComponent):
+ # connector-specific properties
+ style: Optional[str] = None
+ loops: List[List[Pin]] = field(default_factory=list)
+ # pin information in particular
+ pincount: Optional[int] = None
+ pins: List[Pin] = field(default_factory=list) # legacy
+ pinlabels: List[Pin] = field(default_factory=list) # legacy
+ pincolors: List[str] = field(default_factory=list) # legacy
+ pin_objects: Dict[Any, PinClass] = field(default_factory=dict) # new
+ # rendering option
+ show_pincount: Optional[bool] = None
+ hide_disconnected_pins: bool = False
+
+ @property
+ def is_autogenerated(self):
+ return self.designator.startswith(AUTOGENERATED_PREFIX)
+
+ @property
+ def description(self) -> str:
+ substrs = [
+ "Connector",
+ self.type,
+ self.subtype,
+ f"{self.pincount} pins" if self.show_pincount else None,
+ str(self.color) if self.color else None,
+ ]
+ return ", ".join([str(s) for s in substrs if s is not None and s != ""])
+
+ def should_show_pin(self, pin_id):
+ return (
+ not self.hide_disconnected_pins
+ or self.pin_objects[pin_id]._num_connections > 0
+ )
+
+ @property
+ def unit(self): # for compatibility with BOM hashing
+ return None # connectors do not support units.
+
+ def __post_init__(self) -> None:
+ super().__post_init__()
+
+ self.bgcolor_title = SingleColor(self.bgcolor_title)
+ self.color = MultiColor(self.color)
+
+ # connectors do not support custom qty or amount
+ if self.qty is None:
+ self.qty = 1
+ if self.qty != 1:
+ raise Exception("Connector qty != 1 not supported")
+ if self.amount is not None:
+ raise Exception("Connector amount not supported")
+
+ if isinstance(self.image, dict):
+ self.image = Image(**self.image)
+
+ self.ports_left = False
+ self.ports_right = False
+ self.visible_pins = {}
+
+ if self.style == "simple":
+ if self.pincount and self.pincount > 1:
+ raise Exception(
+ "Connectors with style set to simple may only have one pin"
+ )
+ self.pincount = 1
+
+ if not self.pincount:
+ self.pincount = max(
+ len(self.pins), len(self.pinlabels), len(self.pincolors)
+ )
+ if not self.pincount:
+ raise Exception(
+ "You need to specify at least one: "
+ "pincount, pins, pinlabels, or pincolors"
+ )
+
+ # create default list for pins (sequential) if not specified
+ if not self.pins:
+ self.pins = list(range(1, self.pincount + 1))
+
+ if len(self.pins) != len(set(self.pins)):
+ raise Exception("Pins are not unique")
+
+ # all checks have passed
+ pin_tuples = zip_longest(
+ self.pins,
+ self.pinlabels,
+ self.pincolors,
+ )
+ for pin_index, (pin_id, pin_label, pin_color) in enumerate(pin_tuples):
+ self.pin_objects[pin_id] = PinClass(
+ index=pin_index,
+ id=pin_id,
+ label=pin_label,
+ color=MultiColor(pin_color),
+ parent=self.designator,
+ _anonymous=self.is_autogenerated,
+ _simple=self.style == "simple",
+ )
+
+ if self.show_name is None:
+ self.show_name = self.style != "simple" and not self.is_autogenerated
+
+ if self.show_pincount is None:
+ # hide pincount for simple (1 pin) connectors by default
+ self.show_pincount = self.style != "simple"
+
+ for loop in self.loops:
+ # TODO: allow using pin labels in addition to pin numbers,
+ # just like when defining regular connections
+ # TODO: include properties of wire used to create the loop
+ if len(loop) != 2:
+ raise Exception("Loops must be between exactly two pins!")
+ for pin in loop:
+ if pin not in self.pins:
+ raise Exception(
+ f'Unknown loop pin "{pin}" for connector "{self.name}"!'
+ )
+ # Make sure loop connected pins are not hidden.
+ # side=None, determine side to show loops during rendering
+ self.activate_pin(pin, side=None, is_connection=True)
+
+ for i, item in enumerate(self.additional_components):
+ if isinstance(item, dict):
+ self.additional_components[i] = AdditionalComponent(**item)
+
+ def activate_pin(self, pin_id, side: Side = None, is_connection=True) -> None:
+ if is_connection:
+ self.pin_objects[pin_id]._num_connections += 1
+ if side == Side.LEFT:
+ self.ports_left = True
+ elif side == Side.RIGHT:
+ self.ports_right = True
+
+ def compute_qty_multipliers(self):
+ # do not run before all connections in harness have been made!
+ num_populated_pins = len(
+ [pin for pin in self.pin_objects.values() if pin._num_connections > 0]
+ )
+ num_connections = sum(
+ [pin._num_connections for pin in self.pin_objects.values()]
+ )
+ qty_multipliers_computed = {
+ "PINCOUNT": self.pincount,
+ "POPULATED": num_populated_pins,
+ "UNPOPULATED": max(0, self.pincount - num_populated_pins),
+ "CONNECTIONS": num_connections,
+ }
+ for subitem in self.additional_components:
+ if isinstance(subitem.qty_multiplier, QtyMultiplierConnector):
+ computed_factor = qty_multipliers_computed[subitem.qty_multiplier.name]
+ elif isinstance(subitem.qty_multiplier, QtyMultiplierCable):
+ raise Exception("Used a cable multiplier in a connector!")
+ else: # int or float
+ computed_factor = subitem.qty_multiplier
+
+ if subitem.qty is not None:
+ subitem.qty_computed = subitem.qty * computed_factor
+ else:
+ subitem.qty_computed = computed_factor
+ subitem.amount_computed = subitem.amount
+
+
+@dataclass
+class WireClass:
+ parent: str # designator of parent cable/bundle
+ # wire-specific properties
+ index: int
+ id: str
+ label: str
+ color: MultiColor
+ # ...
+ bom_id: Optional[str] = None # to be filled after harness is built
+ # inheritable from parent cable
+ type: Union[MultilineHypertext, List[MultilineHypertext]] = None
+ subtype: Union[MultilineHypertext, List[MultilineHypertext]] = None
+ gauge: Optional[NumberAndUnit] = None
+ length: Optional[NumberAndUnit] = None
+ ignore_in_bom: Optional[bool] = False
+ sum_amounts_in_bom: bool = True
+ partnumbers: PartNumberInfo = None
+
+ @property
+ def bom_hash(self) -> BomHash:
+ if self.sum_amounts_in_bom:
+ _hash = BomHash(
+ description=self.description,
+ qty_unit=self.length.unit if self.length else None,
+ amount=None,
+ partnumbers=self.partnumbers,
+ )
+ else:
+ _hash = BomHash(
+ description=self.description,
+ qty_unit=None,
+ amount=self.length,
+ partnumbers=self.partnumbers,
+ )
+ return _hash
+
+ @property
+ def gauge_str(self):
+ if not self.gauge:
+ return None
+ actual_gauge = f"{self.gauge.number} {self.gauge.unit}"
+ actual_gauge = actual_gauge.replace("mm2", "mm\u00B2")
+ return actual_gauge
+
+ @property
+ def description(self) -> str:
+ substrs = [
+ "Wire",
+ self.type,
+ self.subtype,
+ self.gauge_str,
+ str(self.color) if self.color else None,
+ ]
+ desc = ", ".join([s for s in substrs if s is not None and s != ""])
+ return desc
+
+
+@dataclass
+class ShieldClass(WireClass):
+ pass # TODO, for wires with multiple shields more shield details, ...
+
+
+@dataclass
+class Connection:
+ from_: PinClass = None
+ via: Union[WireClass, ShieldClass] = None
+ to: PinClass = None
+
+
+@dataclass
+class Cable(TopLevelGraphicalComponent):
+ # cable-specific properties
+ gauge: Optional[NumberAndUnit] = None
+ length: Optional[NumberAndUnit] = None
+ color_code: Optional[str] = None
+ # wire information in particular
+ wirecount: Optional[int] = None
+ shield: Union[bool, MultiColor] = False
+ colors: List[str] = field(default_factory=list) # legacy
+ wirelabels: List[Wire] = field(default_factory=list) # legacy
+ wire_objects: Dict[Any, WireClass] = field(default_factory=dict) # new
+ # internal
+ _connections: List[Connection] = field(default_factory=list)
+ # rendering options
+ show_name: Optional[bool] = None
+ show_equiv: bool = False
+ show_wirecount: bool = True
+ show_wirenumbers: Optional[bool] = None
+
+ @property
+ def is_autogenerated(self):
+ return self.designator.startswith(AUTOGENERATED_PREFIX)
+
+ @property
+ def unit(self): # for compatibility with parent class
+ return self.length
+
+ @property
+ def gauge_str(self):
+ if not self.gauge:
+ return None
+ actual_gauge = f"{self.gauge.number} {self.gauge.unit}"
+ actual_gauge = actual_gauge.replace("mm2", "mm\u00B2")
+ return actual_gauge
+
+ @property
+ def gauge_str_with_equiv(self):
+ if not self.gauge:
+ return None
+ actual_gauge = self.gauge_str
+ equivalent_gauge = ""
+ if self.show_equiv:
+ # convert unit if known
+ if self.gauge.unit == "mm2":
+ equivalent_gauge = f" ({awg_equiv(self.gauge.number)} AWG)"
+ elif self.gauge.unit.upper() == "AWG":
+ equivalent_gauge = f" ({mm2_equiv(self.gauge.number)} mm2)"
+ out = f"{actual_gauge}{equivalent_gauge}"
+ out = out.replace("mm2", "mm\u00B2")
+ return out
+
+ @property
+ def length_str(self):
+ if not self.length:
+ return None
+ out = f"{self.length.number} {self.length.unit}"
+ return out
+
+ @property
+ def bom_hash(self):
+ if self.category == "bundle":
+ # This line should never be reached, since caller checks
+ # whether item is a bundle and if so, calls bom_hash
+ # for each individual wire instead
+ raise Exception("Do this at the wire level!")
+ else:
+ return super().bom_hash
+
+ @property
+ def description(self) -> str:
+ if self.category == "bundle":
+ raise Exception("Do this at the wire level!")
+ else:
+ substrs = [
+ ("", "Cable"),
+ (", ", self.type),
+ (", ", self.subtype),
+ (", ", self.wirecount),
+ (" ", f"x {self.gauge_str}" if self.gauge else "wires"),
+ (" ", "shielded" if self.shield else None),
+ (", ", str(self.color) if self.color else None),
+ ]
+ desc = "".join(
+ [f"{s[0]}{s[1]}" for s in substrs if s[1] is not None and s[1] != ""]
+ )
+ return desc
+
+ def _get_wire_partnumber(self, idx) -> PartNumberInfo:
+ def _get_correct_element(inp, idx):
+ return inp[idx] if isinstance(inp, List) else inp
+
+ # TODO: possibly make more robust/elegant
+ if self.category == "bundle":
+ return PartNumberInfo(
+ _get_correct_element(self.partnumbers.pn, idx),
+ _get_correct_element(self.partnumbers.manufacturer, idx),
+ _get_correct_element(self.partnumbers.mpn, idx),
+ _get_correct_element(self.partnumbers.supplier, idx),
+ _get_correct_element(self.partnumbers.spn, idx),
+ )
+ else:
+ return None # non-bundles do not support lists of part data
+
+ def __post_init__(self) -> None:
+ super().__post_init__()
+
+ self.bgcolor_title = SingleColor(self.bgcolor_title)
+ self.color = MultiColor(self.color)
+
+ # cables do not support custom qty or amount
+ if self.qty is None:
+ self.qty = 1
+ if self.qty != 1:
+ raise Exception("Cable qty != 1 not supported")
+
+ if isinstance(self.image, dict):
+ self.image = Image(**self.image)
+
+ # TODO:
+ # allow gauge, length, and other fields to be lists too (like part numbers),
+ # and assign them the same way to bundles.
+
+ self.gauge = parse_number_and_unit(self.gauge, "mm2")
+ self.length = parse_number_and_unit(self.length, "m")
+ self.amount = self.length # for BOM
+
+ if self.wirecount: # number of wires explicitly defined
+ if self.colors: # use custom color palette (partly or looped if needed)
+ 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 = [
+ 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
+
+ else: # wirecount implicit in length of color list
+ if not self.colors:
+ raise Exception(
+ "Unknown number of wires. "
+ "Must specify wirecount or colors (implicit length)"
+ )
+ self.wirecount = len(self.colors)
+
+ if self.wirelabels:
+ if self.shield and "s" in self.wirelabels:
+ raise Exception(
+ '"s" may not be used as a wire label for a shielded cable.'
+ )
+
+ # if lists of part numbers are provided,
+ # check this is a bundle and that it matches the wirecount.
+ for idfield in [self.manufacturer, self.mpn, self.supplier, self.spn, self.pn]:
+ if isinstance(idfield, list):
+ if self.category == "bundle":
+ # check the length
+ if len(idfield) != self.wirecount:
+ raise Exception("lists of part data must match wirecount")
+ else:
+ raise Exception("lists of part data are only supported for bundles")
+
+ # all checks have passed
+ wire_tuples = zip_longest(
+ # TODO: self.wire_ids
+ self.colors,
+ self.wirelabels,
+ )
+ for wire_index, (wire_color, wire_label) in enumerate(wire_tuples):
+ id = wire_index + 1
+ self.wire_objects[id] = WireClass(
+ parent=self.designator,
+ # wire-specific properties
+ index=wire_index, # TODO: wire_id
+ id=id, # TODO: wire_id
+ label=wire_label,
+ color=MultiColor(wire_color),
+ # inheritable from parent cable
+ type=self.type,
+ subtype=self.subtype,
+ gauge=self.gauge,
+ length=self.length,
+ sum_amounts_in_bom=self.sum_amounts_in_bom,
+ ignore_in_bom=self.ignore_in_bom,
+ partnumbers=self._get_wire_partnumber(wire_index),
+ )
+
+ if self.shield:
+ index_offset = len(self.wire_objects)
+ # TODO: add support for multiple shields
+ id = "s"
+ self.wire_objects[id] = ShieldClass(
+ index=index_offset,
+ id=id,
+ label="Shield",
+ color=(
+ MultiColor(self.shield)
+ if isinstance(self.shield, str)
+ else MultiColor(None)
+ ),
+ parent=self.designator,
+ )
+
+ if self.show_name is None:
+ self.show_name = not self.is_autogenerated
+
+ if self.show_wirenumbers is None:
+ # by default, show wire numbers for cables, hide for bundles
+ self.show_wirenumbers = self.category != "bundle"
+
+ for i, item in enumerate(self.additional_components):
+ if isinstance(item, dict):
+ self.additional_components[i] = AdditionalComponent(**item)
+
+ def _connect(
+ self,
+ from_pin_obj: List[PinClass],
+ via_wire_id: str,
+ to_pin_obj: List[PinClass],
+ ) -> None:
+ via_wire_obj = self.wire_objects[via_wire_id]
+ self._connections.append(Connection(from_pin_obj, via_wire_obj, to_pin_obj))
+
+ def compute_qty_multipliers(self):
+ # do not run before all connections in harness have been made!
+ total_length = sum(
+ [
+ wire.length.number if wire.length else 0
+ for wire in self.wire_objects.values()
+ ]
+ )
+ qty_multipliers_computed = {
+ "WIRECOUNT": len(self.wire_objects),
+ # "TERMINATIONS": ___, # TODO
+ "LENGTH": self.length.number if self.length else 0,
+ "TOTAL_LENGTH": total_length,
+ }
+ for subitem in self.additional_components:
+ if isinstance(subitem.qty_multiplier, QtyMultiplierCable):
+ computed_factor = qty_multipliers_computed[subitem.qty_multiplier.name]
+ if subitem.qty_multiplier.name in ["LENGTH", "TOTAL_LENGTH"]:
+ # since length can have a unit, use amount fields to hold
+ if subitem.amount is not None:
+ raise Exception(
+ f"No amount may be specified when using "
+ f"{subitem.qty_multiplier.name} as a multiplier."
+ )
+ subitem.qty_computed = subitem.qty if subitem.qty else 1
+ subitem.amount_computed = NumberAndUnit(
+ computed_factor, self.length.unit
+ )
+ else:
+ # multiplier unrelated to length, therefore no unit
+ if subitem.qty is not None:
+ subitem.qty_computed = subitem.qty * computed_factor
+ else:
+ subitem.qty_computed = computed_factor
+ subitem.amount_computed = subitem.amount
+
+ elif isinstance(subitem.qty_multiplier, QtyMultiplierConnector):
+ raise Exception("Used a connector multiplier in a cable!")
+ else: # int or float
+ if subitem.qty is not None:
+ subitem.qty_computed = subitem.qty * subitem.qty_multiplier
+ else:
+ subitem.qty_computed = subitem.qty_multiplier
+ subitem.amount_computed = subitem.amount
+
+
+@dataclass
+class MatePin:
+ from_: PinClass
+ to: PinClass
+ arrow: Arrow
+
+
+@dataclass
+class MateComponent:
+ from_: str # Designator
+ to: str # Designator
+ arrow: Arrow
diff --git a/src/wireviz/wv_graphviz.py b/src/wireviz/wv_graphviz.py
new file mode 100644
index 000000000..60d89ed1f
--- /dev/null
+++ b/src/wireviz/wv_graphviz.py
@@ -0,0 +1,631 @@
+# -*- coding: utf-8 -*-
+
+import re
+import warnings
+from itertools import zip_longest
+from typing import Any, List, Optional, Tuple, Union
+
+from wireviz import APP_NAME, APP_URL, __version__
+from wireviz.wv_bom import partnumbers2list
+from wireviz.wv_colors import MultiColor
+from wireviz.wv_dataclasses import (
+ ArrowDirection,
+ ArrowWeight,
+ Cable,
+ Component,
+ Connector,
+ MateComponent,
+ MatePin,
+ Options,
+ PartNumberInfo,
+ ShieldClass,
+ WireClass,
+)
+from wireviz.wv_html import Img, Table, Td, Tr
+from wireviz.wv_utils import html_line_breaks, remove_links
+
+
+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):
+ component.ports_left = True # Use left side pins by default
+
+ # generate all rows to be shown in the node
+ if component.show_name:
+ str_name = f"{remove_links(component.designator)}"
+ line_name = Td(str_name, bgcolor=component.bgcolor_title.html)
+ else:
+ line_name = None
+
+ line_pn = partnumbers2list(component.partnumbers)
+
+ is_simple_connector = (
+ isinstance(component, Connector) and component.style == "simple"
+ )
+
+ if isinstance(component, Connector):
+ line_info = [
+ bom_bubble(component.bom_id),
+ html_line_breaks(component.type),
+ html_line_breaks(component.subtype),
+ f"{component.pincount}-pin" if component.show_pincount else None,
+ str(component.color) if component.color else None,
+ ]
+ elif isinstance(component, Cable):
+ line_info = [
+ bom_bubble(component.bom_id) if component.category != "bundle" else None,
+ html_line_breaks(component.type),
+ f"{component.wirecount}x" if component.show_wirecount else None,
+ component.gauge_str_with_equiv,
+ "+ S" if component.shield else None,
+ component.length_str,
+ str(component.color) if component.color else None,
+ ]
+
+ if component.additional_parameters:
+ line_additional_parameters = nested_table_dict(component.additional_parameters)
+ else:
+ line_additional_parameters = []
+
+ if component.color:
+ line_info.extend(colorbar_cells(component.color))
+
+ line_image, line_image_caption = image_and_caption_cells(component)
+ line_additional_component_table = gv_additional_component_table(component)
+ line_notes = [Td(html_line_breaks(component.notes), balign="left")]
+
+ if isinstance(component, Connector):
+ if component.style != "simple":
+ line_ports = gv_pin_table(component)
+ else:
+ line_ports = None
+ elif isinstance(component, Cable):
+ line_ports = gv_conductor_table(component)
+
+ lines = [
+ line_name,
+ line_pn,
+ line_info,
+ line_additional_parameters,
+ line_ports,
+ line_image,
+ line_image_caption,
+ line_additional_component_table,
+ line_notes,
+ ]
+
+ tbl = nested_table(lines)
+ if is_simple_connector:
+ # Simple connectors have no pin table, and therefore, no ports to attach wires to.
+ # Manually assign left and right ports here if required.
+ # Use table itself for right port, and the first cell for left port.
+ # Even if the table only has one cell, two separate ports can still be assigned.
+ tbl.update_attribs(port="p1r")
+ first_cell_in_tbl = tbl.contents[0].contents
+ first_cell_in_tbl.update_attribs(port="p1l")
+
+ return tbl
+
+
+def gv_additional_component_table(component):
+ if not component.additional_components:
+ return None
+
+ rows = []
+ for subitem in component.additional_components:
+ if subitem.explicit_qty:
+ text_qty, unit_qty = subitem.qty_computed, "x"
+ if subitem.amount_computed is not None:
+ text_desc = f"{subitem.amount_computed.number} {subitem.amount_computed.unit} {subitem.description}"
+ else:
+ text_desc = f"{subitem.description}"
+ else:
+ if subitem.amount_computed is not None:
+ text_qty, unit_qty = (
+ subitem.amount_computed.number,
+ subitem.amount_computed.unit,
+ )
+ else:
+ text_qty, unit_qty = "1", "x"
+ text_desc = subitem.description
+
+ firstline = [
+ Td(bom_bubble(subitem.bom_id)),
+ Td(text_qty, align="right"),
+ Td(unit_qty, align="left"),
+ Td(text_desc, align="left"),
+ Td(f"{subitem.note if subitem.note else ''}", align="left"),
+ ]
+ rows.append(Tr(firstline))
+
+ if subitem.has_pn_info:
+ pn_list = partnumbers2list(subitem.partnumbers)
+ secondline = [
+ Td("", colspan=3),
+ Td(", ".join(pn for pn in pn_list if pn), align="left"),
+ Td(""),
+ ]
+ rows.append(Tr(secondline))
+
+ return Table(rows, border=1, cellborder=0, cellpadding=3, cellspacing=0)
+
+
+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 component.bgcolor.html
+ elif isinstance(component, Connector) and harness_options.bgcolor_connector:
+ return harness_options.bgcolor_connector.html
+ elif (
+ isinstance(component, Cable)
+ and component.category == "bundle"
+ and harness_options.bgcolor_bundle
+ ):
+ return harness_options.bgcolor_bundle.html
+ elif isinstance(component, Cable) and harness_options.bgcolor_cable:
+ return harness_options.bgcolor_cable.html
+
+
+def bom_bubble(id) -> Table:
+ if id is None:
+ return None
+ else:
+ # TODO: activate BOM bubbles
+ return None
+ # size and style of BOM bubble is optimized to be a rounded square,
+ # big enough to hold any two-digit ID without GraphViz warnings
+ text = id
+ # text = f'{id} '
+ return Table(
+ Tr(
+ Td(
+ text,
+ border=1,
+ cellpadding=0,
+ fixedsize="true",
+ style="rounded",
+ height=20,
+ width=20,
+ # bgcolor="#000000",
+ )
+ ),
+ border=0,
+ )
+
+
+def make_list_of_cells(inp) -> List[Td]:
+ # inp may be List,
+ if isinstance(inp, List):
+ # ensure all list items are Td
+ list_out = [item if isinstance(item, Td) else Td(item) for item in inp]
+ return list_out
+ else:
+ if inp is None:
+ return []
+ if isinstance(inp, Td):
+ return [inp]
+ else:
+ return [Td(inp)]
+
+
+def nested_table(lines: List[Td]) -> Table:
+ cell_lists = [make_list_of_cells(line) for line in lines]
+ rows = []
+
+ for lst in cell_lists:
+ if len(lst) == 0:
+ continue # no cells in list
+ cells = [item for item in lst if item.contents is not None]
+ if len(cells) == 0:
+ continue # no cells in list, or all cells are None
+ if (
+ len(cells) == 1
+ and isinstance(cells[0].contents, Table)
+ and not "!" in cells[0].contents.attribs.get("id", "")
+ ):
+ # cell content is already a table, no need to re-wrap it;
+ # unless explicitly asked to by a "!" in the ID field
+ # as used by image_and_caption_cells()
+ inner_table = cells[0].contents
+ else:
+ # nest cell content inside a table
+ inner_table = Table(
+ Tr(cells), border=0, cellborder=1, cellpadding=3, cellspacing=0
+ )
+ rows.append(Tr(Td(inner_table)))
+
+ if len(rows) == 0: # create dummy row to avoid GraphViz errors due to empty
+ inner_table = Table(
+ Tr(Td("")), border=0, cellborder=1, cellpadding=3, cellspacing=0
+ )
+ rows = [Tr(Td(inner_table))]
+ tbl = Table(rows, border=0, cellspacing=0, cellpadding=0)
+ return tbl
+
+
+def nested_table_dict(d: dict) -> Table:
+ rows = []
+ for k, v in d.items():
+ rows.append(
+ Tr(
+ [
+ Td(k, align="left", balign="left", valign="top"),
+ Td(html_line_breaks(v), align="left", balign="left"),
+ ]
+ )
+ )
+ return Table(rows, border=0, cellborder=1, cellpadding=3, cellspacing=0)
+
+
+def gv_pin_table(component) -> Table:
+ pin_rows = []
+ for pin in component.pin_objects.values():
+ if component.should_show_pin(pin.id):
+ pin_rows.append(gv_pin_row(pin, component))
+ if len(pin_rows) == 0:
+ # TODO: write test for empty pin tables, and for unconnected connectors that hide disconnected pins
+ pass
+ tbl = Table(pin_rows, border=0, cellborder=1, cellpadding=3, cellspacing=0)
+ return tbl
+
+
+def gv_pin_row(pin, connector) -> Tr:
+ # ports in GraphViz are 1-indexed for more natural maping to pin/wire numbers
+ has_pincolors = any([_pin.color for _pin in connector.pin_objects.values()])
+ cells = [
+ Td(pin.id, port=f"p{pin.index+1}l") if connector.ports_left else None,
+ Td(pin.label, delete_if_empty=True),
+ Td(str(pin.color) if pin.color else "", sides="TBL") if has_pincolors else None,
+ Td(color_minitable(pin.color), sides="TBR") if has_pincolors else None,
+ Td(pin.id, port=f"p{pin.index+1}r") if connector.ports_right else None,
+ ]
+ return Tr(cells)
+
+
+def gv_connector_loops(connector: Connector) -> List:
+ loop_edges = []
+ if connector.ports_left:
+ loop_side = "l"
+ loop_dir = "w"
+ elif connector.ports_right:
+ loop_side = "r"
+ loop_dir = "e"
+ else:
+ raise Exception("No side for loops")
+ for loop in connector.loops:
+ head = f"{connector.designator}:p{loop[0]}{loop_side}:{loop_dir}"
+ tail = f"{connector.designator}:p{loop[1]}{loop_side}:{loop_dir}"
+ loop_edges.append((head, tail))
+ return loop_edges
+
+
+def gv_conductor_table(cable) -> Table:
+ rows = []
+ rows.append(Tr(Td(" "))) # spacer row on top
+
+ inserted_break_inbetween = False
+ for wire in cable.wire_objects.values():
+ # insert blank space between wires and shields
+ if isinstance(wire, ShieldClass) and not inserted_break_inbetween:
+ rows.append(Tr(Td(" "))) # spacer row between wires and shields
+ inserted_break_inbetween = True
+
+ # row above the wire
+ wireinfo = []
+ if cable.show_wirenumbers and not isinstance(wire, ShieldClass):
+ wireinfo.append(str(wire.id))
+ wireinfo.append(str(wire.color))
+ wireinfo.append(wire.label)
+
+ ins, outs = [], []
+ for conn in cable._connections:
+ if conn.via.id == wire.id:
+ if conn.from_ is not None:
+ ins.append(str(conn.from_))
+ if conn.to is not None:
+ outs.append(str(conn.to))
+
+ cells_above = [
+ Td(" " + ", ".join(ins), align="left"),
+ Td(" "), # increase cell spacing here
+ Td(bom_bubble(wire.bom_id)) if cable.category == "bundle" else None,
+ Td(":".join([wi for wi in wireinfo if wi is not None and wi != ""])),
+ Td(" "), # increase cell spacing here
+ Td(", ".join(outs) + " ", align="right"),
+ ]
+ cells_above = [cell for cell in cells_above if cell is not None]
+ rows.append(Tr(cells_above))
+
+ # the wire itself
+ rows.append(Tr(gv_wire_cell(wire, len(cells_above))))
+
+ # row below the wire
+ if wire.partnumbers:
+ cells_below = partnumbers2list(
+ wire.partnumbers, parent_partnumbers=cable.partnumbers
+ )
+ if cells_below is not None and len(cells_below) > 0:
+ table_below = (
+ Table(
+ Tr([Td(cell) for cell in cells_below]),
+ border=0,
+ cellborder=0,
+ cellspacing=0,
+ ),
+ )
+ rows.append(Tr(Td(table_below, colspan=len(cells_above))))
+
+ rows.append(Tr(Td(" "))) # spacer row on bottom
+ tbl = Table(rows, border=0, cellborder=0, cellspacing=0)
+ return tbl
+
+
+def gv_wire_cell(wire: Union[WireClass, ShieldClass], colspan: int) -> Td:
+ if wire.color:
+ 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 "#000000",
+ "border": 0,
+ "cellpadding": 0,
+ "colspan": colspan,
+ "height": 2,
+ }
+ wire_inner_rows.append(Tr(Td("", **wire_inner_cell_attribs)))
+ wire_inner_table = Table(wire_inner_rows, border=0, cellborder=0, cellspacing=0)
+ wire_outer_cell_attribs = {
+ "border": 0,
+ "cellspacing": 0,
+ "cellpadding": 0,
+ "colspan": colspan,
+ "height": 2 * len(color_list),
+ "port": f"w{wire.index+1}",
+ }
+ # ports in GraphViz are 1-indexed for more natural maping to pin/wire numbers
+ wire_outer_cell = Td(wire_inner_table, **wire_outer_cell_attribs)
+
+ return wire_outer_cell
+
+
+def gv_edge_wire(harness, cable, connection) -> Tuple[str, str, str, str, str]:
+ if connection.via.color:
+ # check if it's an actual wire and not a shield
+ color = f"#000000:{connection.via.color.html_padded}:#000000"
+ else: # it's a shield connection
+ color = "#000000"
+
+ if connection.from_ is not None: # connect to left
+ from_port_str = (
+ f":p{connection.from_.index+1}r"
+ if harness.connectors[connection.from_.parent].style != "simple"
+ else ""
+ )
+ code_left_1 = f"{connection.from_.parent}{from_port_str}:e"
+ code_left_2 = f"{connection.via.parent}:w{connection.via.index+1}:w"
+ # ports in GraphViz are 1-indexed for more natural maping to pin/wire numbers
+ else:
+ code_left_1, code_left_2 = None, None
+
+ if connection.to is not None: # connect to right
+ to_port_str = (
+ f":p{connection.to.index+1}l"
+ if harness.connectors[connection.to.parent].style != "simple"
+ else ""
+ )
+ code_right_1 = f"{connection.via.parent}:w{connection.via.index+1}:e"
+ code_right_2 = f"{connection.to.parent}{to_port_str}:w"
+ else:
+ code_right_1, code_right_2 = None, None
+
+ return color, code_left_1, code_left_2, code_right_1, code_right_2
+
+
+def parse_arrow_str(inp: str) -> ArrowDirection:
+ if inp[0] == "<" and inp[-1] == ">":
+ return ArrowDirection.BOTH
+ elif inp[0] == "<":
+ return ArrowDirection.BACK
+ elif inp[-1] == ">":
+ return ArrowDirection.FORWARD
+ else:
+ return ArrowDirection.NONE
+
+
+def gv_edge_mate(mate) -> Tuple[str, str, str, str]:
+ if mate.arrow.weight == ArrowWeight.SINGLE:
+ color = "#000000"
+ elif mate.arrow.weight == ArrowWeight.DOUBLE:
+ color = "#000000:#000000"
+
+ dir = mate.arrow.direction.name.lower()
+
+ if isinstance(mate, MatePin):
+ from_pin_index = mate.from_.index
+ from_port_str = f":p{from_pin_index+1}r"
+ from_designator = mate.from_.parent
+ to_pin_index = mate.to.index
+ to_port_str = f":p{to_pin_index+1}l"
+ to_designator = mate.to.parent
+ elif isinstance(mate, MateComponent):
+ from_designator = mate.from_
+ from_port_str = ""
+ to_designator = mate.to
+ to_port_str = ""
+ else:
+ raise Exception(f"Unknown type of mate:\n{mate}")
+
+ code_from = f"{from_designator}{from_port_str}:e"
+ code_to = f"{to_designator}{to_port_str}:w"
+
+ return color, dir, code_from, code_to
+
+
+def colorbar_cells(color, mini=False) -> List[Td]:
+ cells = []
+ mini = {"height": 8, "width": 8, "fixedsize": "true"} if mini else {}
+ for index, subcolor in enumerate(color.colors):
+ sides_l = "L" if index == 0 else ""
+ sides_r = "R" if index == len(color.colors) - 1 else ""
+ sides = "TB" + sides_l + sides_r
+ cells.append(Td("", bgcolor=subcolor.html, sides=sides, **mini))
+ return cells
+
+
+def color_minitable(color: Optional[MultiColor]) -> Union[Table, str]:
+ if color is None or len(color) == 0:
+ return ""
+
+ cells = colorbar_cells(color, mini=True)
+
+ return Table(
+ Tr(cells),
+ border=0,
+ cellborder=1,
+ cellspacing=0,
+ height=8,
+ width=8 * len(cells),
+ fixedsize="true",
+ )
+
+
+def image_and_caption_cells(component: Component) -> Tuple[Td, Td]:
+ if not component.image:
+ return (None, None)
+
+ image_tag = Img(scale=component.image.scale, src=component.image.src)
+ image_cell_inner = Td(image_tag, flat=True)
+ if component.image.fixedsize:
+ # further nest the image in a table with width/height/fixedsize parameters,
+ # and place that table in a cell
+ image_cell_inner.update_attribs(**html_size_attr_dict(component.image))
+ image_cell = Td(
+ Table(Tr(image_cell_inner), border=0, cellborder=0, cellspacing=0, id="!")
+ )
+ else:
+ image_cell = image_cell_inner
+
+ image_cell.update_attribs(
+ balign="left",
+ bgcolor=component.image.bgcolor.html,
+ sides="TLR" if component.image.caption else None,
+ )
+
+ if component.image.caption:
+ caption_cell = Td(
+ f"{html_line_breaks(component.image.caption)}", balign="left", sides="BLR"
+ )
+ else:
+ caption_cell = None
+ return (image_cell, caption_cell)
+
+
+def html_size_attr_dict(image):
+ # Return Graphviz HTML attributes to specify minimum or fixed size of a TABLE or TD object
+ pass
+
+ attr_dict = {}
+ if image:
+ if image.width:
+ attr_dict["width"] = image.width
+ if image.height:
+ attr_dict["height"] = image.height
+ if image.fixedsize:
+ attr_dict["fixedsize"] = "true"
+ return attr_dict
+
+
+def set_dot_basics(dot, options):
+ dot.body.append(f"// Graph generated by {APP_NAME} {__version__}\n")
+ dot.body.append(f"// {APP_URL}\n")
+ dot.attr(
+ "graph",
+ rankdir="LR",
+ ranksep="2",
+ bgcolor=options.bgcolor.html,
+ nodesep="0.33",
+ fontname=options.fontname,
+ ) # TODO: Add graph attribute: charset="utf-8",
+ 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=options.bgcolor_node.html,
+ fontname=options.fontname,
+ )
+ dot.attr("edge", style="bold", fontname=options.fontname)
+
+
+def apply_dot_tweaks(dot, tweak):
+ def typecheck(name: str, value: Any, expect: type) -> None:
+ if not isinstance(value, expect):
+ raise Exception(
+ f"Unexpected value type of {name}: "
+ f"Expected {expect}, got {type(value)}\n{value}"
+ )
+
+ # TODO?: Differ between override attributes and HTML?
+ if tweak.override is not None:
+ typecheck("tweak.override", tweak.override, dict)
+ for k, d in tweak.override.items():
+ typecheck(f"tweak.override.{k} key", k, str)
+ typecheck(f"tweak.override.{k} value", d, dict)
+ for a, v in d.items():
+ typecheck(f"tweak.override.{k}.{a} key", a, str)
+ typecheck(f"tweak.override.{k}.{a} value", v, (str, type(None)))
+
+ # Override generated attributes of selected entries matching tweak.override.
+ for i, entry in enumerate(dot.body):
+ if not isinstance(entry, str):
+ continue
+ # Find a possibly quoted keyword after leading TAB(s) and followed by [ ].
+ match = re.match(r'^\t*(")?((?(1)[^"]|[^ "])+)(?(1)") \[.*\]$', entry, re.S)
+ keyword = match and match[2]
+ if not keyword in tweak.override.keys():
+ continue
+
+ for attr, value in tweak.override[keyword].items():
+ if value is None:
+ entry, n_subs = re.subn(
+ f'( +)?{attr}=("[^"]*"|[^] ]*)(?(1)| *)', "", entry
+ )
+ if n_subs < 1:
+ warnings.warn(f"tweak: {attr} not found in {keyword}!")
+ elif n_subs > 1:
+ warnings.warn(
+ f"tweak: {attr} removed {n_subs} times in {keyword}!"
+ )
+ continue
+
+ if len(value) == 0 or " " in value:
+ value = value.replace('"', r"\"")
+ value = f'"{value}"'
+ entry, n_subs = re.subn(
+ f'{attr}=("[^"]*"|[^] ]*)', f"{attr}={value}", entry
+ )
+ if n_subs < 1:
+ # If attr not found, then append it
+ entry = re.sub(r"\]$", f" {attr}={value}]", entry)
+ elif n_subs > 1:
+ warnings.warn(
+ f"tweak: {attr} overridden {n_subs} times in {keyword}!"
+ )
+
+ dot.body[i] = entry
+
+ if tweak.append is not None:
+ if isinstance(tweak.append, list):
+ for i, element in enumerate(tweak.append, 1):
+ typecheck(f"tweak.append[{i}]", element, str)
+ dot.body.extend(tweak.append)
+ else:
+ typecheck("tweak.append", tweak.append, str)
+ dot.body.append(tweak.append)
diff --git a/src/wireviz/wv_gv_html.py b/src/wireviz/wv_gv_html.py
deleted file mode 100644
index ec80aa748..000000000
--- a/src/wireviz/wv_gv_html.py
+++ /dev/null
@@ -1,111 +0,0 @@
-# -*- coding: utf-8 -*-
-
-import re
-from typing import List, Optional, Union
-
-from wireviz.DataClasses import Color
-from wireviz.wv_colors import translate_color
-from wireviz.wv_helper import remove_links
-
-
-def nested_html_table(
- rows: List[Union[str, List[Optional[str]], None]], table_attrs: str = ""
-) -> str:
- # input: list, each item may be scalar or list
- # output: a parent table with one child table per parent item that is list, and one cell per parent item that is scalar
- # purpose: create the appearance of one table, where cell widths are independent between rows
- # attributes in any leading inside a list are injected into to the preceeding tag
- html = []
- html.append(
- f''
- )
-
- num_rows = 0
- for row in rows:
- if isinstance(row, List):
- if len(row) > 0 and any(row):
- html.append(" ")
- # fmt: off
- html.append(' ')
- # fmt: on
- for cell in row:
- if cell is not None:
- # Inject attributes to the preceeding tag where needed
- # fmt: off
- html.append(f' {cell} '.replace(">
")
- html.append(" ")
- num_rows = num_rows + 1
- elif row is not None:
- html.append(" ")
- html.append(f" {row}")
- html.append(" ")
- num_rows = num_rows + 1
- if num_rows == 0: # empty table
- # generate empty cell to avoid GraphViz errors
- html.append(" ")
- html.append("
")
- return html
-
-
-def html_bgcolor_attr(color: Color) -> str:
- """Return attributes for bgcolor or '' if no color."""
- return f' bgcolor="{translate_color(color, "HEX")}"' if color else ""
-
-
-def html_bgcolor(color: Color, _extra_attr: str = "") -> str:
- """Return attributes prefix for bgcolor or '' if no color."""
- return f"" if color else ""
-
-
-def html_colorbar(color: Color) -> str:
- """Return attributes prefix for bgcolor and minimum width or None if no color."""
- return html_bgcolor(color, ' width="4"') if color else None
-
-
-def html_image(image):
- from wireviz.DataClasses import Image
-
- if not image:
- return None
- # The leading attributes belong to the preceeding tag. See where used below.
- html = f'{html_size_attr(image)}> '
- if image.fixedsize:
- # Close the preceeding tag and enclose the image cell in a table without
- # borders to avoid narrow borders when the fixed width < the node width.
- html = f""">
-
- """
- return f"""{html_line_breaks(image.caption)}'
- if image and image.caption
- else None
- )
-
-
-def html_size_attr(image):
- from wireviz.DataClasses import Image
-
- # Return Graphviz HTML attributes to specify minimum or fixed size of a TABLE or TD object
- return (
- (
- (f' width="{image.width}"' if image.width else "")
- + (f' height="{image.height}"' if image.height else "")
- + (' fixedsize="true"' if image.fixedsize else "")
- )
- if image
- else ""
- )
-
-
-def html_line_breaks(inp):
- return remove_links(inp).replace("\n", " ") if isinstance(inp, str) else inp
diff --git a/src/wireviz/wv_harness.py b/src/wireviz/wv_harness.py
new file mode 100644
index 000000000..95e920e32
--- /dev/null
+++ b/src/wireviz/wv_harness.py
@@ -0,0 +1,444 @@
+# -*- coding: utf-8 -*-
+
+from collections import defaultdict
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import List, Union
+
+from graphviz import Graph
+
+import wireviz.wv_colors
+from wireviz.wv_bom import BomCategory, BomEntry, bom_list, print_bom_table
+from wireviz.wv_dataclasses import (
+ AUTOGENERATED_PREFIX,
+ AdditionalBomItem,
+ Arrow,
+ ArrowWeight,
+ Cable,
+ Component,
+ Connector,
+ MateComponent,
+ MatePin,
+ Metadata,
+ Options,
+ Side,
+ TopLevelGraphicalComponent,
+ Tweak,
+)
+from wireviz.wv_graphviz import (
+ apply_dot_tweaks,
+ calculate_node_bgcolor,
+ gv_connector_loops,
+ gv_edge_mate,
+ gv_edge_wire,
+ gv_node_component,
+ parse_arrow_str,
+ set_dot_basics,
+)
+from wireviz.wv_output import (
+ embed_svg_images,
+ embed_svg_images_file,
+ generate_html_output,
+)
+from wireviz.wv_utils import OLD_CONNECTOR_ATTR, bom2tsv, check_old, file_write_text
+
+
+@dataclass
+class Harness:
+ metadata: Metadata
+ options: Options
+ tweak: Tweak
+ additional_bom_items: List[AdditionalBomItem] = field(default_factory=list)
+
+ def __post_init__(self):
+ self.connectors = {}
+ self.cables = {}
+ self.mates = []
+ self.bom = defaultdict(dict)
+ self.additional_bom_items = []
+
+ def add_connector(self, designator: str, *args, **kwargs) -> None:
+ check_old(f"Connector '{designator}'", OLD_CONNECTOR_ATTR, kwargs)
+ conn = Connector(designator=designator, *args, **kwargs)
+ self.connectors[designator] = conn
+
+ def add_cable(self, designator: str, *args, **kwargs) -> None:
+ cbl = Cable(designator=designator, *args, **kwargs)
+ self.cables[designator] = cbl
+
+ def add_additional_bom_item(self, item: dict) -> None:
+ new_item = AdditionalBomItem(**item)
+ self.additional_bom_items.append(new_item)
+
+ def add_mate_pin(self, from_name, from_pin, to_name, to_pin, arrow_str) -> None:
+ from_con = self.connectors[from_name]
+ from_pin_obj = from_con.pin_objects[from_pin]
+ to_con = self.connectors[to_name]
+ to_pin_obj = to_con.pin_objects[to_pin]
+ arrow = Arrow(direction=parse_arrow_str(arrow_str), weight=ArrowWeight.SINGLE)
+
+ self.mates.append(MatePin(from_pin_obj, to_pin_obj, arrow))
+ self.connectors[from_name].activate_pin(
+ from_pin, Side.RIGHT, is_connection=False
+ )
+ self.connectors[to_name].activate_pin(to_pin, Side.LEFT, is_connection=False)
+
+ def add_mate_component(self, from_name, to_name, arrow_str) -> None:
+ arrow = Arrow(direction=parse_arrow_str(arrow_str), weight=ArrowWeight.SINGLE)
+ self.mates.append(MateComponent(from_name, to_name, arrow))
+
+ def populate_bom(self): # called once harness creation is complete
+ # helper lists
+ all_toplevel_items = (
+ list(self.connectors.values())
+ + list(self.cables.values())
+ + self.additional_bom_items
+ )
+ all_subitems = [
+ subitem
+ for item in all_toplevel_items
+ for subitem in item.additional_components
+ ]
+ all_bom_relevant_items = (
+ list(self.connectors.values())
+ + [cable for cable in self.cables.values() if cable.category != "bundle"]
+ + [
+ wire
+ for cable in self.cables.values()
+ if cable.category == "bundle"
+ for wire in cable.wire_objects.values()
+ ]
+ + all_subitems
+ )
+
+ # add items to BOM
+ for item in all_toplevel_items:
+ self._add_to_internal_bom(item) # nested subitems are also handled
+ # sort BOM by category first, then alphabetically by description within category
+ self.bom = dict(
+ sorted(
+ self.bom.items(),
+ key=lambda x: (
+ x[1]["category"],
+ x[0].description,
+ ), # x[0] = key, x[1] = value
+ )
+ )
+ # assign BOM IDs
+ for id, key in enumerate(self.bom.keys(), 1):
+ self.bom[key]["id"] = id
+ # set BOM IDs within components (for BOM bubbles)
+ for item in all_bom_relevant_items:
+ if item.ignore_in_bom:
+ continue
+ if not item.bom_hash in self.bom:
+ print(f"{item}'s hash' not found in BOM dict.") # Should not happen
+ continue
+ item.bom_id = self.bom[item.bom_hash]["id"]
+
+ def _add_to_internal_bom(self, item: Component):
+ if item.ignore_in_bom:
+ return
+
+ def _add(hash, qty, designator=None, category=None):
+ bom_entry = self.bom[hash]
+ # initialize missing fields
+ if not "qty" in bom_entry:
+ bom_entry["qty"] = 0
+ if not "designators" in bom_entry:
+ bom_entry["designators"] = set()
+ # update fields
+ bom_entry["qty"] += qty
+ if designator is None:
+ designator_list = []
+ elif isinstance(designator, list):
+ designator_list = designator
+ else:
+ designator_list = [designator]
+ for des in designator_list:
+ if des and not des.startswith(AUTOGENERATED_PREFIX):
+ bom_entry["designators"].add(des)
+ bom_entry["category"] = category
+
+ if isinstance(item, TopLevelGraphicalComponent):
+ if isinstance(item, Connector):
+ cat = BomCategory.CONNECTOR
+ elif isinstance(item, Cable):
+ if item.category == "bundle":
+ cat = BomCategory.WIRE
+ else:
+ cat = BomCategory.CABLE
+ else:
+ cat = ""
+
+ if item.category == "bundle":
+ # wires of a bundle are added as individual BOM entries
+ for subitem in item.wire_objects.values():
+ _add(
+ hash=subitem.bom_hash,
+ qty=item.qty, # should be 1
+ designator=item.designator, # inherit from parent item
+ category=cat,
+ )
+ else:
+ _add(
+ hash=item.bom_hash,
+ qty=item.qty, # should be 1
+ designator=item.designator,
+ category=cat,
+ )
+
+ if item.additional_components:
+ item.compute_qty_multipliers()
+
+ for comp in item.additional_components:
+ if comp.ignore_in_bom:
+ continue
+
+ if comp.sum_amounts_in_bom:
+ if comp.amount_computed:
+ total_qty = comp.qty_computed * comp.amount_computed.number
+ else:
+ total_qty = comp.qty_computed
+ else:
+ total_qty = comp.qty_computed
+ _add(
+ hash=comp.bom_hash,
+ designator=item.designator,
+ qty=total_qty,
+ # no explicit qty specified; assume qty = 1
+ # used to simplify add.comp. table within parent node
+ # e.g. show "10 mm Heatshrink" instead of "1x 10 mm Heatshrink"
+ category=BomCategory.ADDITIONAL_INSIDE,
+ )
+ elif isinstance(item, AdditionalBomItem):
+ cat = BomCategory.ADDITIONAL_OUTSIDE
+ _add(
+ hash=item.bom_hash,
+ qty=item.qty,
+ designator=None,
+ category=cat,
+ )
+ else:
+ raise Exception(f"Unknown type of item:\n{item}")
+
+ def connect(
+ self,
+ from_name: str,
+ from_pin: Union[int, str],
+ via_name: str,
+ via_wire: Union[int, str],
+ to_name: str,
+ to_pin: Union[int, str],
+ ) -> None:
+ # check from and to connectors
+ for name, pin in zip([from_name, to_name], [from_pin, to_pin]):
+ if name is not None and name in self.connectors:
+ connector = self.connectors[name]
+ # check if provided name is ambiguous
+ if pin in connector.pins and pin in connector.pinlabels:
+ if connector.pins.index(pin) != connector.pinlabels.index(pin):
+ raise Exception(
+ f"{name}:{pin} is defined both in pinlabels and pins, "
+ "for different pins."
+ )
+ # TODO: Maybe issue a warning if present in both lists
+ # but referencing the same pin?
+ if pin in connector.pinlabels:
+ if connector.pinlabels.count(pin) > 1:
+ raise Exception(f"{name}:{pin} is defined more than once.")
+ index = connector.pinlabels.index(pin)
+ pin = connector.pins[index] # map pin name to pin number
+ if name == from_name:
+ from_pin = pin
+ if name == to_name:
+ to_pin = pin
+ if not pin in connector.pins:
+ raise Exception(f"{name}:{pin} not found.")
+
+ # check via cable
+ if via_name in self.cables:
+ cable = self.cables[via_name]
+ # check if provided name is ambiguous
+ if via_wire in cable.colors and via_wire in cable.wirelabels:
+ if cable.colors.index(via_wire) != cable.wirelabels.index(via_wire):
+ raise Exception(
+ f"{via_name}:{via_wire} is defined both in colors and wirelabels, "
+ "for different wires."
+ )
+ # TODO: Maybe issue a warning if present in both lists
+ # but referencing the same wire?
+ if via_wire in cable.colors:
+ if cable.colors.count(via_wire) > 1:
+ raise Exception(
+ f"{via_name}:{via_wire} is used for more than one wire."
+ )
+ # list index starts at 0, wire IDs start at 1
+ via_wire = cable.colors.index(via_wire) + 1
+ elif via_wire in cable.wirelabels:
+ if cable.wirelabels.count(via_wire) > 1:
+ raise Exception(
+ f"{via_name}:{via_wire} is used for more than one wire."
+ )
+ via_wire = (
+ cable.wirelabels.index(via_wire) + 1
+ ) # list index starts at 0, wire IDs start at 1
+
+ # perform the actual connection
+ if from_name is not None:
+ from_con = self.connectors[from_name]
+ from_pin_obj = from_con.pin_objects[from_pin]
+ else:
+ from_pin_obj = None
+ if to_name is not None:
+ to_con = self.connectors[to_name]
+ to_pin_obj = to_con.pin_objects[to_pin]
+ else:
+ to_pin_obj = None
+
+ self.cables[via_name]._connect(from_pin_obj, via_wire, to_pin_obj)
+ if from_name in self.connectors:
+ self.connectors[from_name].activate_pin(from_pin, Side.RIGHT)
+ if to_name in self.connectors:
+ self.connectors[to_name].activate_pin(to_pin, Side.LEFT)
+
+ def create_graph(self) -> Graph:
+ dot = Graph()
+ set_dot_basics(dot, self.options)
+
+ for connector in self.connectors.values():
+ # generate connector node
+ gv_html = gv_node_component(connector)
+ gv_html.update_attribs(
+ bgcolor=calculate_node_bgcolor(connector, self.options)
+ )
+ dot.node(
+ connector.designator,
+ label=f"<\n{gv_html}\n>",
+ shape="box",
+ style="filled",
+ )
+ # generate edges for connector loops
+ if len(connector.loops) > 0:
+ dot.attr("edge", color="#000000")
+ loops = gv_connector_loops(connector)
+ for head, tail in loops:
+ dot.edge(head, tail, label=" ")
+ # ^ workaround to avoid oversized loops
+
+ # 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
+ wire_is_multicolor = [
+ len(wire.color) > 1
+ for cable in self.cables.values()
+ for wire in cable.wire_objects.values()
+ ]
+ if any(wire_is_multicolor):
+ 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)
+ gv_html.update_attribs(bgcolor=calculate_node_bgcolor(cable, self.options))
+ style = "filled,dashed" if cable.category == "bundle" else "filled"
+ dot.node(
+ cable.designator,
+ label=f"<\n{gv_html}\n>",
+ shape="box",
+ style=style,
+ )
+
+ # generate wire edges between component nodes and cable nodes
+ for connection in cable._connections:
+ color, l1, l2, r1, r2 = gv_edge_wire(self, cable, connection)
+ dot.attr("edge", color=color)
+ if not (l1, l2) == (None, None):
+ dot.edge(l1, l2)
+ if not (r1, r2) == (None, None):
+ dot.edge(r1, r2)
+
+ for mate in self.mates:
+ color, dir, code_from, code_to = gv_edge_mate(mate)
+
+ dot.attr("edge", color=color, style="dashed", dir=dir)
+ dot.edge(code_from, code_to)
+
+ apply_dot_tweaks(dot, self.tweak)
+
+ return dot
+
+ # cache for the GraphViz Graph object
+ # do not access directly, use self.graph instead
+ _graph = None
+
+ @property
+ def graph(self):
+ if not self._graph: # no cached graph exists, generate one
+ self._graph = self.create_graph()
+ return self._graph # return cached graph
+
+ @property
+ def png(self):
+ from io import BytesIO
+
+ graph = self.graph
+ data = BytesIO()
+ data.write(graph.pipe(format="png"))
+ data.seek(0)
+ return data.read()
+
+ @property
+ def svg(self): # TODO?: Verify xml encoding="utf-8" in SVG?
+ graph = self.graph
+ return embed_svg_images(graph.pipe(format="svg").decode("utf-8"), Path.cwd())
+
+ def output(
+ self,
+ filename: Union[str, Path],
+ view: bool = False,
+ cleanup: bool = True,
+ fmt: tuple = ("html", "png", "svg", "tsv"),
+ ) -> None:
+ # graphical output
+ graph = self.graph
+ for f in fmt:
+ if f in ("png", "svg", "html"):
+ if f == "html": # if HTML format is specified,
+ f = "svg" # generate SVG for embedding into HTML
+ # SVG file will be renamed/deleted later
+ _filename = f"{filename}.tmp" if f == "svg" else filename
+ # TODO: prevent rendering SVG twice when both SVG and HTML are specified
+ graph.format = f
+ graph.render(filename=_filename, view=view, cleanup=cleanup)
+ # embed images into SVG output
+ if "svg" in fmt or "html" in fmt:
+ embed_svg_images_file(f"{filename}.tmp.svg")
+ # GraphViz output
+ if "gv" in fmt:
+ graph.save(filename=f"{filename}.gv")
+ # BOM output
+ bomlist = bom_list(self.bom)
+ # bomlist = [[]]
+ if "tsv" in fmt:
+ tsv = bom2tsv(bomlist)
+ file_write_text(f"{filename}.bom.tsv", tsv)
+
+ if "csv" in fmt:
+ # TODO: implement CSV output (preferrably using CSV library)
+ print("CSV output is not yet supported")
+ # HTML output
+ if "html" in fmt:
+ generate_html_output(filename, bomlist, self.metadata, self.options)
+ # PDF output
+ if "pdf" in fmt:
+ # TODO: implement PDF output
+ print("PDF output is not yet supported")
+ # delete SVG if not needed
+ if "html" in fmt and not "svg" in fmt:
+ # SVG file was just needed to generate HTML
+ Path(f"{filename}.tmp.svg").unlink()
+ elif "svg" in fmt:
+ Path(f"{filename}.tmp.svg").replace(f"{filename}.svg")
diff --git a/src/wireviz/wv_html.py b/src/wireviz/wv_html.py
index 763da9d7d..1c4b750e0 100644
--- a/src/wireviz/wv_html.py
+++ b/src/wireviz/wv_html.py
@@ -1,119 +1,125 @@
# -*- coding: utf-8 -*-
-import re
-from pathlib import Path
-from typing import Dict, List, Union
-
-from wireviz import APP_NAME, APP_URL, __version__, wv_colors
-from wireviz.DataClasses import Metadata, Options
-from wireviz.wv_gv_html import html_line_breaks
-from wireviz.wv_helper import (
- flatten2d,
- open_file_read,
- open_file_write,
- smart_file_resolve,
-)
-
-
-def generate_html_output(
- filename: Union[str, Path],
- bom_list: List[List[str]],
- metadata: Metadata,
- options: Options,
-):
-
- # load HTML template
- templatename = metadata.get("template", {}).get("name")
- if templatename:
- # if relative path to template was provided, check directory of YAML file first, fall back to built-in template directory
- templatefile = smart_file_resolve(
- f"{templatename}.html",
- [Path(filename).parent, Path(__file__).parent / "templates"],
- )
- else:
- # fall back to built-in simple template if no template was provided
- templatefile = Path(__file__).parent / "templates/simple.html"
-
- html = open_file_read(templatefile).read()
-
- # embed SVG diagram
- with open_file_read(f"{filename}.tmp.svg") as file:
- svgdata = re.sub(
- "^<[?]xml [^?>]*[?]>[^<]*]*>",
- "",
- file.read(),
- 1,
- )
-
- # generate BOM table
- bom = flatten2d(bom_list)
-
- # generate BOM header (may be at the top or bottom of the table)
- bom_header_html = " \n"
- for item in bom[0]:
- th_class = f"bom_col_{item.lower()}"
- bom_header_html = f'{bom_header_html} {item} \n'
- bom_header_html = f"{bom_header_html} \n"
-
- # generate BOM contents
- bom_contents = []
- for row in bom[1:]:
- row_html = " \n"
- for i, item in enumerate(row):
- td_class = f"bom_col_{bom[0][i].lower()}"
- row_html = f'{row_html} {item} \n'
- row_html = f"{row_html} \n"
- bom_contents.append(row_html)
-
- bom_html = (
- '\n' + bom_header_html + "".join(bom_contents) + "
\n"
- )
- bom_html_reversed = (
- '\n'
- + "".join(list(reversed(bom_contents)))
- + bom_header_html
- + "
\n"
- )
-
- # prepare simple replacements
- replacements = {
- "": f"{APP_NAME} {__version__} - {APP_URL}",
- "": options.fontname,
- "": wv_colors.translate_color(options.bgcolor, "hex"),
- "": svgdata,
- "": bom_html,
- "": bom_html_reversed,
- "": "1", # TODO: handle multi-page documents
- "": "1", # TODO: handle multi-page documents
- }
-
- # prepare metadata replacements
- if metadata:
- for item, contents in metadata.items():
- if isinstance(contents, (str, int, float)):
- replacements[f""] = html_line_breaks(str(contents))
- elif isinstance(contents, Dict): # useful for authors, revisions
- for index, (category, entry) in enumerate(contents.items()):
- if isinstance(entry, Dict):
- replacements[f""] = str(category)
- for entry_key, entry_value in entry.items():
- replacements[f""] = (
- html_line_breaks(str(entry_value))
- )
-
- replacements['"sheetsize_default"'] = '"{}"'.format(
- metadata.get("template", {}).get("sheetsize", "")
- )
- # include quotes so no replacement happens within
-
+
tutorial01
Diagram
@@ -30,139 +30,126 @@ Diagram
-
-
-
-
+
+
+
X1
-
-
-X1
-
-4-pin
-
-1
-
-2
-
-3
-
-4
+
+
+
+X1
+
+4-pin
+
+1
+
+2
+
+3
+
+4
W1
-
-
-W1
-
-4x
-
-1 m
-
-X1:1
- 1
-X2:1
-
-
-
-X1:2
- 2
-X2:2
-
-
-
-X1:3
- 3
-X2:3
-
-
-
-X1:4
- 4
-X2:4
-
-
-
-
+
+
+
+W1
+
+4x
+
+1 m
+
+ X1:1
+
+1
+
+X2:1
+
+ X1:2
+
+2
+
+X2:2
+
+ X1:3
+
+3
+
+X2:3
+
+ X1:4
+
+4
+
+X2:4
+
+
X1:e--W1:w
-
-
-
+
X1:e--W1:w
-
-
-
+
X1:e--W1:w
-
-
-
+
X1:e--W1:w
-
-
-
+
X2
-
-
-X2
-
-4-pin
-
-1
-
-2
-
-3
-
-4
+
+
+
+X2
+
+4-pin
+
+1
+
+2
+
+3
+
+4
W1:e--X2:w
-
-
-
+
W1:e--X2:w
-
-
-
+
W1:e--X2:w
-
-
-
+
W1:e--X2:w
-
-
-
+
@@ -178,26 +165,26 @@ Bill of Materials
- Id
- Description
+ #
Qty
Unit
+ Description
Designators
- 1
- Cable, 4 wires
- 1
- m
- W1
-
-
- 2
- Connector, 4 pins
+ 1
2
+ Connector, 4 pins
X1, X2
+
+ 2
+ 1
+ m
+ Cable, 4 wires
+ W1
+
diff --git a/tutorial/tutorial01.png b/tutorial/tutorial01.png
index b94a39028..e93b3dec3 100644
Binary files a/tutorial/tutorial01.png and b/tutorial/tutorial01.png differ
diff --git a/tutorial/tutorial01.svg b/tutorial/tutorial01.svg
index 644ce4c08..1c871c13e 100644
--- a/tutorial/tutorial01.svg
+++ b/tutorial/tutorial01.svg
@@ -1,139 +1,126 @@
-
-
-
-
+
+
+
X1
-
-
-X1
-
-4-pin
-
-1
-
-2
-
-3
-
-4
+
+
+
+X1
+
+4-pin
+
+1
+
+2
+
+3
+
+4
W1
-
-
-W1
-
-4x
-
-1 m
-
-X1:1
- 1
-X2:1
-
-
-
-X1:2
- 2
-X2:2
-
-
-
-X1:3
- 3
-X2:3
-
-
-
-X1:4
- 4
-X2:4
-
-
-
-
+
+
+
+W1
+
+4x
+
+1 m
+
+ X1:1
+
+1
+
+X2:1
+
+ X1:2
+
+2
+
+X2:2
+
+ X1:3
+
+3
+
+X2:3
+
+ X1:4
+
+4
+
+X2:4
+
+
X1:e--W1:w
-
-
-
+
X1:e--W1:w
-
-
-
+
X1:e--W1:w
-
-
-
+
X1:e--W1:w
-
-
-
+
X2
-
-
-X2
-
-4-pin
-
-1
-
-2
-
-3
-
-4
+
+
+
+X2
+
+4-pin
+
+1
+
+2
+
+3
+
+4
W1:e--X2:w
-
-
-
+
W1:e--X2:w
-
-
-
+
W1:e--X2:w
-
-
-
+
W1:e--X2:w
-
-
-
+
diff --git a/tutorial/tutorial02.bom.tsv b/tutorial/tutorial02.bom.tsv
index 582b78a25..da57a6a14 100644
--- a/tutorial/tutorial02.bom.tsv
+++ b/tutorial/tutorial02.bom.tsv
@@ -1,3 +1,3 @@
-Id Description Qty Unit Designators
-1 Cable, 4 x 0.25 mm² 1 m W1
-2 Connector, Molex KK 254, female, 4 pins 2 X1, X2
+# Qty Unit Description Designators
+1 2 Connector, Molex KK 254, female, 4 pins X1, X2
+2 1 m Cable, 4 x 0.25 mm² W1
diff --git a/tutorial/tutorial02.gv b/tutorial/tutorial02.gv
index 1ea5546f6..227eaae31 100644
--- a/tutorial/tutorial02.gv
+++ b/tutorial/tutorial02.gv
@@ -1,169 +1,235 @@
graph {
-// Graph generated by WireViz 0.4
-// https://github.com/formatc1702/WireViz
+// Graph generated by WireViz 0.5-dev+refactor
+// https://github.com/wireviz/WireViz
graph [bgcolor="#FFFFFF" fontname=arial nodesep=0.33 rankdir=LR ranksep=2]
node [fillcolor="#FFFFFF" fontname=arial height=0 margin=0 shape=none style=filled width=0]
edge [fontname=arial style=bold]
X1 [label=<
-
-
-
-
-
-
- Molex KK 254
- female
- 4-pin
-
-
-
-
-
- 1
-
-
- 2
-
-
- 3
-
-
- 4
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Molex KK 254
+ female
+ 4-pin
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+ 2
+
+
+
+ 3
+
+
+
+ 4
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style=filled]
+> shape=box style=filled]
X2 [label=<
-
-
-
-
-
-
- Molex KK 254
- female
- 4-pin
-
-
-
-
-
- 1
-
-
- 2
-
-
- 3
-
-
- 4
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Molex KK 254
+ female
+ 4-pin
+
+
+
+
+
+
+
+
+ 1
+
+
+
+ 2
+
+
+
+ 3
+
+
+
+ 4
+
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style=filled]
- edge [color="#000000:#ffffff:#000000"]
+> shape=box style=filled]
+ W1 [label=<
+
+
+
+
+
+
+
+
+
+
+ 4x
+ 0.25 mm² (24 AWG)
+ 1 m
+
+
+
+
+
+
+
+
+
+
+
+ X1:1
+
+ 1:WH
+
+ X2:1
+
+
+
+
+
+
+
+ X1:2
+
+ 2:BN
+
+ X2:2
+
+
+
+
+
+
+
+ X1:3
+
+ 3:GN
+
+ X2:4
+
+
+
+
+
+
+
+ X1:4
+
+ 4:YE
+
+ X2:3
+
+
+
+
+
+
+
+
+
+
+
+
+
+> shape=box style=filled]
+ edge [color="#000000:#FFFFFF:#000000"]
X1:p1r:e -- W1:w1:w
W1:w1:e -- X2:p1l:w
edge [color="#000000:#895956:#000000"]
X1:p2r:e -- W1:w2:w
W1:w2:e -- X2:p2l:w
- edge [color="#000000:#00ff00:#000000"]
+ edge [color="#000000:#00AA00:#000000"]
X1:p3r:e -- W1:w3:w
W1:w3:e -- X2:p4l:w
- edge [color="#000000:#ffff00:#000000"]
+ edge [color="#000000:#FFFF00:#000000"]
X1:p4r:e -- W1:w4:w
W1:w4:e -- X2:p3l:w
- W1 [label=<
-
-
-
-
-
-
- 4x
- 0.25 mm² (24 AWG)
- 1 m
-
-
-
-
-
-
- X1:1
-
- 1:WH
-
- X2:1
-
-
-
-
-
-
-
- X1:2
-
- 2:BN
-
- X2:2
-
-
-
-
-
-
-
- X1:3
-
- 3:GN
-
- X2:4
-
-
-
-
-
-
-
- X1:4
-
- 4:YE
-
- X2:3
-
-
-
-
-
-
-
-
-
-
-> fillcolor="#FFFFFF" shape=box style=filled]
}
diff --git a/tutorial/tutorial02.html b/tutorial/tutorial02.html
index 385d895f9..e43e2f20a 100644
--- a/tutorial/tutorial02.html
+++ b/tutorial/tutorial02.html
@@ -1,7 +1,7 @@
-
+
tutorial02
-
+
tutorial02
Diagram
@@ -30,149 +30,160 @@ Diagram
-
-
-
-
+
+
+
X1
-
-
-X1
-
-Molex KK 254
-
-female
-
-4-pin
-
-1
-
-2
-
-3
-
-4
+
+
+
+X1
+
+Molex KK 254
+
+female
+
+4-pin
+
+1
+
+2
+
+3
+
+4
W1
-
-
-W1
-
-4x
-
-0.25 mm² (24 AWG)
-
-1 m
-
-X1:1
- 1:WH
-X2:1
-
-
-
-X1:2
- 2:BN
-X2:2
-
-
-
-X1:3
- 3:GN
-X2:4
-
-
-
-X1:4
- 4:YE
-X2:3
-
-
-
-
+
+
+
+W1
+
+4x
+
+0.25 mm² (24 AWG)
+
+1 m
+
+ X1:1
+
+1:WH
+
+X2:1
+
+
+
+ X1:2
+
+2:BN
+
+X2:2
+
+
+
+ X1:3
+
+3:GN
+
+X2:4
+
+
+
+ X1:4
+
+4:YE
+
+X2:3
+
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X2
-
-
-X2
-
-Molex KK 254
-
-female
-
-4-pin
-
-1
-
-2
-
-3
-
-4
+
+
+
+X2
+
+Molex KK 254
+
+female
+
+4-pin
+
+1
+
+2
+
+3
+
+4
W1:e--X2:w
-
-
-
+
+
+
W1:e--X2:w
-
-
-
+
+
+
W1:e--X2:w
-
-
-
+
+
+
W1:e--X2:w
-
-
-
+
+
+
@@ -188,26 +199,26 @@ Bill of Materials
- Id
- Description
+ #
Qty
Unit
+ Description
Designators
- 1
- Cable, 4 x 0.25 mm²
- 1
- m
- W1
-
-
- 2
- Connector, Molex KK 254, female, 4 pins
+ 1
2
+ Connector, Molex KK 254, female, 4 pins
X1, X2
+
+ 2
+ 1
+ m
+ Cable, 4 x 0.25 mm²
+ W1
+
diff --git a/tutorial/tutorial02.png b/tutorial/tutorial02.png
index bba6a5bc9..ba6902c98 100644
Binary files a/tutorial/tutorial02.png and b/tutorial/tutorial02.png differ
diff --git a/tutorial/tutorial02.svg b/tutorial/tutorial02.svg
index 348b3d801..786d59933 100644
--- a/tutorial/tutorial02.svg
+++ b/tutorial/tutorial02.svg
@@ -1,149 +1,160 @@
-
-
-
-
+
+
+
X1
-
-
-X1
-
-Molex KK 254
-
-female
-
-4-pin
-
-1
-
-2
-
-3
-
-4
+
+
+
+X1
+
+Molex KK 254
+
+female
+
+4-pin
+
+1
+
+2
+
+3
+
+4
W1
-
-
-W1
-
-4x
-
-0.25 mm² (24 AWG)
-
-1 m
-
-X1:1
- 1:WH
-X2:1
-
-
-
-X1:2
- 2:BN
-X2:2
-
-
-
-X1:3
- 3:GN
-X2:4
-
-
-
-X1:4
- 4:YE
-X2:3
-
-
-
-
+
+
+
+W1
+
+4x
+
+0.25 mm² (24 AWG)
+
+1 m
+
+ X1:1
+
+1:WH
+
+X2:1
+
+
+
+ X1:2
+
+2:BN
+
+X2:2
+
+
+
+ X1:3
+
+3:GN
+
+X2:4
+
+
+
+ X1:4
+
+4:YE
+
+X2:3
+
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X2
-
-
-X2
-
-Molex KK 254
-
-female
-
-4-pin
-
-1
-
-2
-
-3
-
-4
+
+
+
+X2
+
+Molex KK 254
+
+female
+
+4-pin
+
+1
+
+2
+
+3
+
+4
W1:e--X2:w
-
-
-
+
+
+
W1:e--X2:w
-
-
-
+
+
+
W1:e--X2:w
-
-
-
+
+
+
W1:e--X2:w
-
-
-
+
+
+
diff --git a/tutorial/tutorial03.bom.tsv b/tutorial/tutorial03.bom.tsv
index 51f8e3668..93fe12dee 100644
--- a/tutorial/tutorial03.bom.tsv
+++ b/tutorial/tutorial03.bom.tsv
@@ -1,3 +1,3 @@
-Id Description Qty Unit Designators
-1 Cable, 4 x 0.25 mm² shielded 1 m W1
-2 Connector, Molex KK 254, female, 4 pins 2 X1, X2
+# Qty Unit Description Designators
+1 2 Connector, Molex KK 254, female, 4 pins X1, X2
+2 1 m Cable, 4 x 0.25 mm² shielded W1
diff --git a/tutorial/tutorial03.gv b/tutorial/tutorial03.gv
index f37096323..91fa7d62a 100644
--- a/tutorial/tutorial03.gv
+++ b/tutorial/tutorial03.gv
@@ -1,187 +1,257 @@
graph {
-// Graph generated by WireViz 0.4
-// https://github.com/formatc1702/WireViz
+// Graph generated by WireViz 0.5-dev+refactor
+// https://github.com/wireviz/WireViz
graph [bgcolor="#FFFFFF" fontname=arial nodesep=0.33 rankdir=LR ranksep=2]
node [fillcolor="#FFFFFF" fontname=arial height=0 margin=0 shape=none style=filled width=0]
edge [fontname=arial style=bold]
X1 [label=<
-
-
-
-
-
-
- Molex KK 254
- female
- 4-pin
-
-
-
-
-
- GND
- 1
-
-
- VCC
- 2
-
-
- RX
- 3
-
-
- TX
- 4
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Molex KK 254
+ female
+ 4-pin
+
+
+
+
+
+
+
+
+ GND
+ 1
+
+
+ VCC
+ 2
+
+
+ RX
+ 3
+
+
+ TX
+ 4
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style=filled]
+> shape=box style=filled]
X2 [label=<
-
-
-
-
-
-
- Molex KK 254
- female
- 4-pin
-
-
-
-
-
- 1
- GND
-
-
- 2
- VCC
-
-
- 3
- RX
-
-
- 4
- TX
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Molex KK 254
+ female
+ 4-pin
+
+
+
+
+
+
+
+
+ 1
+ GND
+
+
+ 2
+ VCC
+
+
+ 3
+ RX
+
+
+ 4
+ TX
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style=filled]
- edge [color="#000000:#ffffff:#000000"]
+> shape=box style=filled]
+ W1 [label=<
+
+
+
+
+
+
+
+
+
+
+ 4x
+ 0.25 mm² (24 AWG)
+ + S
+ 1 m
+
+
+
+
+
+
+
+
+
+
+
+ X1:1:GND
+
+ 1:WH
+
+ X2:1:GND
+
+
+
+
+
+
+
+ X1:2:VCC
+
+ 2:BN
+
+ X2:2:VCC
+
+
+
+
+
+
+
+ X1:3:RX
+
+ 3:GN
+
+ X2:4:TX
+
+
+
+
+
+
+
+ X1:4:TX
+
+ 4:YE
+
+ X2:3:RX
+
+
+
+
+
+
+
+
+
+
+ X1:1:GND
+
+ Shield
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+> shape=box style=filled]
+ edge [color="#000000:#FFFFFF:#000000"]
X1:p1r:e -- W1:w1:w
W1:w1:e -- X2:p1l:w
edge [color="#000000:#895956:#000000"]
X1:p2r:e -- W1:w2:w
W1:w2:e -- X2:p2l:w
- edge [color="#000000:#00ff00:#000000"]
+ edge [color="#000000:#00AA00:#000000"]
X1:p3r:e -- W1:w3:w
W1:w3:e -- X2:p4l:w
- edge [color="#000000:#ffff00:#000000"]
+ edge [color="#000000:#FFFF00:#000000"]
X1:p4r:e -- W1:w4:w
W1:w4:e -- X2:p3l:w
edge [color="#000000"]
- X1:p1r:e -- W1:ws:w
- W1 [label=<
-
-
-
-
-
-
- 4x
- 0.25 mm² (24 AWG)
- + S
- 1 m
-
-
-
-
-
-
- X1:1:GND
-
- 1:WH
-
- X2:1:GND
-
-
-
-
-
-
-
- X1:2:VCC
-
- 2:BN
-
- X2:2:VCC
-
-
-
-
-
-
-
- X1:3:RX
-
- 3:GN
-
- X2:4:TX
-
-
-
-
-
-
-
- X1:4:TX
-
- 4:YE
-
- X2:3:RX
-
-
-
-
-
-
-
-
- X1:1:GND
- Shield
-
-
-
-
-
-
-
-> fillcolor="#FFFFFF" shape=box style=filled]
+ X1:p1r:e -- W1:w5:w
}
diff --git a/tutorial/tutorial03.html b/tutorial/tutorial03.html
index b56e399d0..b76440ff0 100644
--- a/tutorial/tutorial03.html
+++ b/tutorial/tutorial03.html
@@ -1,7 +1,7 @@
-
+
tutorial03
-
+
tutorial03
Diagram
@@ -30,176 +30,190 @@ Diagram
-
-
-
-
+
+
+
X1
-
-
-X1
-
-Molex KK 254
-
-female
-
-4-pin
-
-GND
-
-1
-
-VCC
-
-2
-
-RX
-
-3
-
-TX
-
-4
+
+
+
+X1
+
+Molex KK 254
+
+female
+
+4-pin
+
+GND
+
+1
+
+VCC
+
+2
+
+RX
+
+3
+
+TX
+
+4
W1
-
-
-W1
-
-4x
-
-0.25 mm² (24 AWG)
-
-+ S
-
-1 m
-
-X1:1:GND
- 1:WH
-X2:1:GND
-
-
-
-X1:2:VCC
- 2:BN
-X2:2:VCC
-
-
-
-X1:3:RX
- 3:GN
-X2:4:TX
-
-
-
-X1:4:TX
- 4:YE
-X2:3:RX
-
-
-
-
-X1:1:GND
-Shield
-
-
+
+
+
+W1
+
+4x
+
+0.25 mm² (24 AWG)
+
++ S
+
+1 m
+
+ X1:1:GND
+
+1:WH
+
+X2:1:GND
+
+
+
+ X1:2:VCC
+
+2:BN
+
+X2:2:VCC
+
+
+
+ X1:3:RX
+
+3:GN
+
+X2:4:TX
+
+
+
+ X1:4:TX
+
+4:YE
+
+X2:3:RX
+
+
+
+
+ X1:1:GND
+
+Shield
+
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
+
X2
-
-
-X2
-
-Molex KK 254
-
-female
-
-4-pin
-
-1
-
-GND
-
-2
-
-VCC
-
-3
-
-RX
-
-4
-
-TX
+
+
+
+X2
+
+Molex KK 254
+
+female
+
+4-pin
+
+1
+
+GND
+
+2
+
+VCC
+
+3
+
+RX
+
+4
+
+TX
W1:e--X2:w
-
-
-
+
+
+
W1:e--X2:w
-
-
-
+
+
+
W1:e--X2:w
-
-
-
+
+
+
W1:e--X2:w
-
-
-
+
+
+
@@ -215,26 +229,26 @@ Bill of Materials
- Id
- Description
+ #
Qty
Unit
+ Description
Designators
- 1
- Cable, 4 x 0.25 mm² shielded
- 1
- m
- W1
-
-
- 2
- Connector, Molex KK 254, female, 4 pins
+ 1
2
+ Connector, Molex KK 254, female, 4 pins
X1, X2
+
+ 2
+ 1
+ m
+ Cable, 4 x 0.25 mm² shielded
+ W1
+
diff --git a/tutorial/tutorial03.png b/tutorial/tutorial03.png
index 4bd8c393f..9bca81725 100644
Binary files a/tutorial/tutorial03.png and b/tutorial/tutorial03.png differ
diff --git a/tutorial/tutorial03.svg b/tutorial/tutorial03.svg
index 5344fb800..aef9b8537 100644
--- a/tutorial/tutorial03.svg
+++ b/tutorial/tutorial03.svg
@@ -1,176 +1,190 @@
-
-
-
-
+
+
+
X1
-
-
-X1
-
-Molex KK 254
-
-female
-
-4-pin
-
-GND
-
-1
-
-VCC
-
-2
-
-RX
-
-3
-
-TX
-
-4
+
+
+
+X1
+
+Molex KK 254
+
+female
+
+4-pin
+
+GND
+
+1
+
+VCC
+
+2
+
+RX
+
+3
+
+TX
+
+4
W1
-
-
-W1
-
-4x
-
-0.25 mm² (24 AWG)
-
-+ S
-
-1 m
-
-X1:1:GND
- 1:WH
-X2:1:GND
-
-
-
-X1:2:VCC
- 2:BN
-X2:2:VCC
-
-
-
-X1:3:RX
- 3:GN
-X2:4:TX
-
-
-
-X1:4:TX
- 4:YE
-X2:3:RX
-
-
-
-
-X1:1:GND
-Shield
-
-
+
+
+
+W1
+
+4x
+
+0.25 mm² (24 AWG)
+
++ S
+
+1 m
+
+ X1:1:GND
+
+1:WH
+
+X2:1:GND
+
+
+
+ X1:2:VCC
+
+2:BN
+
+X2:2:VCC
+
+
+
+ X1:3:RX
+
+3:GN
+
+X2:4:TX
+
+
+
+ X1:4:TX
+
+4:YE
+
+X2:3:RX
+
+
+
+
+ X1:1:GND
+
+Shield
+
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
+
X2
-
-
-X2
-
-Molex KK 254
-
-female
-
-4-pin
-
-1
-
-GND
-
-2
-
-VCC
-
-3
-
-RX
-
-4
-
-TX
+
+
+
+X2
+
+Molex KK 254
+
+female
+
+4-pin
+
+1
+
+GND
+
+2
+
+VCC
+
+3
+
+RX
+
+4
+
+TX
W1:e--X2:w
-
-
-
+
+
+
W1:e--X2:w
-
-
-
+
+
+
W1:e--X2:w
-
-
-
+
+
+
W1:e--X2:w
-
-
-
+
+
+
diff --git a/tutorial/tutorial04.bom.tsv b/tutorial/tutorial04.bom.tsv
index e2c3835d0..20975dfef 100644
--- a/tutorial/tutorial04.bom.tsv
+++ b/tutorial/tutorial04.bom.tsv
@@ -1,4 +1,4 @@
-Id Description Qty Unit Designators
-1 Cable, 4 x 24 AWG 0.4 m W1, W2
-2 Connector, Molex KK 254, female, 4 pins 2 X2, X3
-3 Connector, Molex KK 254, male, 4 pins 1 X1
+# Qty Unit Description Designators
+1 2 Connector, Molex KK 254, female, 4 pins X2, X3
+2 1 Connector, Molex KK 254, male, 4 pins X1
+3 2 m Cable, 4 x 24 AWG W1, W2
diff --git a/tutorial/tutorial04.gv b/tutorial/tutorial04.gv
index fd45c1ff3..f506f2b26 100644
--- a/tutorial/tutorial04.gv
+++ b/tutorial/tutorial04.gv
@@ -1,340 +1,466 @@
graph {
-// Graph generated by WireViz 0.4
-// https://github.com/formatc1702/WireViz
+// Graph generated by WireViz 0.5-dev+refactor
+// https://github.com/wireviz/WireViz
graph [bgcolor="#FFFFFF" fontname=arial nodesep=0.33 rankdir=LR ranksep=2]
node [fillcolor="#FFFFFF" fontname=arial height=0 margin=0 shape=none style=filled width=0]
edge [fontname=arial style=bold]
X1 [label=<
-
-
-
-
-
-
- Molex KK 254
- male
- 4-pin
-
-
-
-
-
- GND
- 1
-
-
- VCC
- 2
-
-
- SCL
- 3
-
-
- SDA
- 4
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Molex KK 254
+ male
+ 4-pin
+
+
+
+
+
+
+
+
+ GND
+ 1
+
+
+ VCC
+ 2
+
+
+ SCL
+ 3
+
+
+ SDA
+ 4
+
+
+
+
+
+
+
+
+ to microcontroller
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style=filled]
+> shape=box style=filled]
X2 [label=<
-
-
-
-
-
-
- Molex KK 254
- female
- 4-pin
-
-
-
-
-
- 1
- GND
- 1
-
-
- 2
- VCC
- 2
-
-
- 3
- SCL
- 3
-
-
- 4
- SDA
- 4
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Molex KK 254
+ female
+ 4-pin
+
+
+
+
+
+
+
+
+ 1
+ GND
+ 1
+
+
+ 2
+ VCC
+ 2
+
+
+ 3
+ SCL
+ 3
+
+
+ 4
+ SDA
+ 4
+
+
+
+
+
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style=filled]
+> shape=box style=filled]
X3 [label=<
-
-
-
-
-
-
- Molex KK 254
- female
- 4-pin
-
-
-
-
-
- 1
- GND
-
-
- 2
- VCC
-
-
- 3
- SCL
-
-
- 4
- SDA
-
-
-
-
-
- to temperature sensor
-
-
+
+
+
+
+
+
+
+
+
+
+ Molex KK 254
+ female
+ 4-pin
+
+
+
+
+
+
+
+
+ 1
+ GND
+
+
+ 2
+ VCC
+
+
+ 3
+ SCL
+
+
+ 4
+ SDA
+
+
+
+
+
+
+
+
+ to temperature sensor
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style=filled]
+> shape=box style=filled]
+ W1 [label=<
+
+
+
+
+
+
+
+
+
+
+ 4x
+ 24 AWG
+ 0.3 m
+
+
+
+
+
+
+
+
+
+
+
+ X1:1:GND
+
+ 1:BN
+
+ X2:1:GND
+
+
+
+
+
+
+
+ X1:2:VCC
+
+ 2:RD
+
+ X2:2:VCC
+
+
+
+
+
+
+
+ X1:3:SCL
+
+ 3:OG
+
+ X2:3:SCL
+
+
+
+
+
+
+
+ X1:4:SDA
+
+ 4:YE
+
+ X2:4:SDA
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This cable is a bit longer
+
+
+
+
+
+> shape=box style=filled]
edge [color="#000000:#895956:#000000"]
X1:p1r:e -- W1:w1:w
W1:w1:e -- X2:p1l:w
- edge [color="#000000:#ff0000:#000000"]
+ edge [color="#000000:#FF0000:#000000"]
X1:p2r:e -- W1:w2:w
W1:w2:e -- X2:p2l:w
- edge [color="#000000:#ff8000:#000000"]
+ edge [color="#000000:#FF8000:#000000"]
X1:p3r:e -- W1:w3:w
W1:w3:e -- X2:p3l:w
- edge [color="#000000:#ffff00:#000000"]
+ edge [color="#000000:#FFFF00:#000000"]
X1:p4r:e -- W1:w4:w
W1:w4:e -- X2:p4l:w
- W1 [label=<
-
-
-
-
-
-
-
-
-
-
-
- X1:1:GND
-
- 1:BN
-
- X2:1:GND
-
-
-
-
-
-
-
- X1:2:VCC
-
- 2:RD
-
- X2:2:VCC
-
-
-
-
-
-
-
- X1:3:SCL
-
- 3:OG
-
- X2:3:SCL
-
-
-
-
-
-
-
- X1:4:SDA
-
- 4:YE
-
- X2:4:SDA
-
-
-
-
-
-
-
-
-
-
-
- This cable is a bit longer
-
-
+ W2 [label=<
+
+
+
+
+
+
+
+
+
+
+ 4x
+ 24 AWG
+ 0.1 m
+
+
+
+
+
+
+
+
+
+
+
+ X2:1:GND
+
+ 1:BN
+
+ X3:1:GND
+
+
+
+
+
+
+
+ X2:2:VCC
+
+ 2:RD
+
+ X3:2:VCC
+
+
+
+
+
+
+
+ X2:3:SCL
+
+ 3:OG
+
+ X3:3:SCL
+
+
+
+
+
+
+
+ X2:4:SDA
+
+ 4:YE
+
+ X3:4:SDA
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This cable is a bit shorter
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style=filled]
+> shape=box style=filled]
edge [color="#000000:#895956:#000000"]
X2:p1r:e -- W2:w1:w
W2:w1:e -- X3:p1l:w
- edge [color="#000000:#ff0000:#000000"]
+ edge [color="#000000:#FF0000:#000000"]
X2:p2r:e -- W2:w2:w
W2:w2:e -- X3:p2l:w
- edge [color="#000000:#ff8000:#000000"]
+ edge [color="#000000:#FF8000:#000000"]
X2:p3r:e -- W2:w3:w
W2:w3:e -- X3:p3l:w
- edge [color="#000000:#ffff00:#000000"]
+ edge [color="#000000:#FFFF00:#000000"]
X2:p4r:e -- W2:w4:w
W2:w4:e -- X3:p4l:w
- W2 [label=<
-
-
-
-
-
-
-
-
-
-
-
- X2:1:GND
-
- 1:BN
-
- X3:1:GND
-
-
-
-
-
-
-
- X2:2:VCC
-
- 2:RD
-
- X3:2:VCC
-
-
-
-
-
-
-
- X2:3:SCL
-
- 3:OG
-
- X3:3:SCL
-
-
-
-
-
-
-
- X2:4:SDA
-
- 4:YE
-
- X3:4:SDA
-
-
-
-
-
-
-
-
-
-
-
- This cable is a bit shorter
-
-
-
-> fillcolor="#FFFFFF" shape=box style=filled]
}
diff --git a/tutorial/tutorial04.html b/tutorial/tutorial04.html
index ef77c9c91..41c4c8cda 100644
--- a/tutorial/tutorial04.html
+++ b/tutorial/tutorial04.html
@@ -1,7 +1,7 @@
-
+
tutorial04
-
+
tutorial04
Diagram
@@ -30,307 +30,328 @@ Diagram
-
-
-
-
+
+
+
X1
-
-
-X1
-
-Molex KK 254
-
-male
-
-4-pin
-
-GND
-
-1
-
-VCC
-
-2
-
-SCL
-
-3
-
-SDA
-
-4
-
-to microcontroller
+
+
+
+X1
+
+Molex KK 254
+
+male
+
+4-pin
+
+GND
+
+1
+
+VCC
+
+2
+
+SCL
+
+3
+
+SDA
+
+4
+
+to microcontroller
W1
-
-
-W1
-
-4x
-
-24 AWG
-
-0.3 m
-
-X1:1:GND
- 1:BN
-X2:1:GND
-
-
-
-X1:2:VCC
- 2:RD
-X2:2:VCC
-
-
-
-X1:3:SCL
- 3:OG
-X2:3:SCL
-
-
-
-X1:4:SDA
- 4:YE
-X2:4:SDA
-
-
-
-
-
-This cable is a bit longer
+
+
+
+W1
+
+4x
+
+24 AWG
+
+0.3 m
+
+ X1:1:GND
+
+1:BN
+
+X2:1:GND
+
+
+
+ X1:2:VCC
+
+2:RD
+
+X2:2:VCC
+
+
+
+ X1:3:SCL
+
+3:OG
+
+X2:3:SCL
+
+
+
+ X1:4:SDA
+
+4:YE
+
+X2:4:SDA
+
+
+
+
+
+This cable is a bit longer
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X2
-
-
-X2
-
-Molex KK 254
-
-female
-
-4-pin
-
-1
-
-GND
-
-1
-
-2
-
-VCC
-
-2
-
-3
-
-SCL
-
-3
-
-4
-
-SDA
-
-4
-
-to accelerometer
+
+
+
+X2
+
+Molex KK 254
+
+female
+
+4-pin
+
+1
+
+GND
+
+1
+
+2
+
+VCC
+
+2
+
+3
+
+SCL
+
+3
+
+4
+
+SDA
+
+4
+
+to accelerometer
W2
-
-
-W2
-
-4x
-
-24 AWG
-
-0.1 m
-
-X2:1:GND
- 1:BN
-X3:1:GND
-
-
-
-X2:2:VCC
- 2:RD
-X3:2:VCC
-
-
-
-X2:3:SCL
- 3:OG
-X3:3:SCL
-
-
-
-X2:4:SDA
- 4:YE
-X3:4:SDA
-
-
-
-
-
-This cable is a bit shorter
+
+
+
+W2
+
+4x
+
+24 AWG
+
+0.1 m
+
+ X2:1:GND
+
+1:BN
+
+X3:1:GND
+
+
+
+ X2:2:VCC
+
+2:RD
+
+X3:2:VCC
+
+
+
+ X2:3:SCL
+
+3:OG
+
+X3:3:SCL
+
+
+
+ X2:4:SDA
+
+4:YE
+
+X3:4:SDA
+
+
+
+
+
+This cable is a bit shorter
X2:e--W2:w
-
-
-
+
+
+
X2:e--W2:w
-
-
-
+
+
+
X2:e--W2:w
-
-
-
+
+
+
X2:e--W2:w
-
-
-
+
+
+
X3
-
-
-X3
-
-Molex KK 254
-
-female
-
-4-pin
-
-1
-
-GND
-
-2
-
-VCC
-
-3
-
-SCL
-
-4
-
-SDA
-
-to temperature sensor
+
+
+
+X3
+
+Molex KK 254
+
+female
+
+4-pin
+
+1
+
+GND
+
+2
+
+VCC
+
+3
+
+SCL
+
+4
+
+SDA
+
+to temperature sensor
W1:e--X2:w
-
-
-
+
+
+
W1:e--X2:w
-
-
-
+
+
+
W1:e--X2:w
-
-
-
+
+
+
W1:e--X2:w
-
-
-
+
+
+
W2:e--X3:w
-
-
-
+
+
+
W2:e--X3:w
-
-
-
+
+
+
W2:e--X3:w
-
-
-
+
+
+
W2:e--X3:w
-
-
-
+
+
+
@@ -346,33 +367,33 @@ Bill of Materials
- Id
- Description
+ #
Qty
Unit
+ Description
Designators
- 1
- Cable, 4 x 24 AWG
- 0.4
- m
- W1, W2
-
-
- 2
- Connector, Molex KK 254, female, 4 pins
+ 1
2
+ Connector, Molex KK 254, female, 4 pins
X2, X3
- 3
- Connector, Molex KK 254, male, 4 pins
+ 2
1
+ Connector, Molex KK 254, male, 4 pins
X1
+
+ 3
+ 2
+ m
+ Cable, 4 x 24 AWG
+ W1, W2
+
diff --git a/tutorial/tutorial04.png b/tutorial/tutorial04.png
index 6f7975a7c..125074bae 100644
Binary files a/tutorial/tutorial04.png and b/tutorial/tutorial04.png differ
diff --git a/tutorial/tutorial04.svg b/tutorial/tutorial04.svg
index e101c7340..bb56c9deb 100644
--- a/tutorial/tutorial04.svg
+++ b/tutorial/tutorial04.svg
@@ -1,307 +1,328 @@
-
-
-
-
+
+
+
X1
-
-
-X1
-
-Molex KK 254
-
-male
-
-4-pin
-
-GND
-
-1
-
-VCC
-
-2
-
-SCL
-
-3
-
-SDA
-
-4
-
-to microcontroller
+
+
+
+X1
+
+Molex KK 254
+
+male
+
+4-pin
+
+GND
+
+1
+
+VCC
+
+2
+
+SCL
+
+3
+
+SDA
+
+4
+
+to microcontroller
W1
-
-
-W1
-
-4x
-
-24 AWG
-
-0.3 m
-
-X1:1:GND
- 1:BN
-X2:1:GND
-
-
-
-X1:2:VCC
- 2:RD
-X2:2:VCC
-
-
-
-X1:3:SCL
- 3:OG
-X2:3:SCL
-
-
-
-X1:4:SDA
- 4:YE
-X2:4:SDA
-
-
-
-
-
-This cable is a bit longer
+
+
+
+W1
+
+4x
+
+24 AWG
+
+0.3 m
+
+ X1:1:GND
+
+1:BN
+
+X2:1:GND
+
+
+
+ X1:2:VCC
+
+2:RD
+
+X2:2:VCC
+
+
+
+ X1:3:SCL
+
+3:OG
+
+X2:3:SCL
+
+
+
+ X1:4:SDA
+
+4:YE
+
+X2:4:SDA
+
+
+
+
+
+This cable is a bit longer
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X2
-
-
-X2
-
-Molex KK 254
-
-female
-
-4-pin
-
-1
-
-GND
-
-1
-
-2
-
-VCC
-
-2
-
-3
-
-SCL
-
-3
-
-4
-
-SDA
-
-4
-
-to accelerometer
+
+
+
+X2
+
+Molex KK 254
+
+female
+
+4-pin
+
+1
+
+GND
+
+1
+
+2
+
+VCC
+
+2
+
+3
+
+SCL
+
+3
+
+4
+
+SDA
+
+4
+
+to accelerometer
W2
-
-
-W2
-
-4x
-
-24 AWG
-
-0.1 m
-
-X2:1:GND
- 1:BN
-X3:1:GND
-
-
-
-X2:2:VCC
- 2:RD
-X3:2:VCC
-
-
-
-X2:3:SCL
- 3:OG
-X3:3:SCL
-
-
-
-X2:4:SDA
- 4:YE
-X3:4:SDA
-
-
-
-
-
-This cable is a bit shorter
+
+
+
+W2
+
+4x
+
+24 AWG
+
+0.1 m
+
+ X2:1:GND
+
+1:BN
+
+X3:1:GND
+
+
+
+ X2:2:VCC
+
+2:RD
+
+X3:2:VCC
+
+
+
+ X2:3:SCL
+
+3:OG
+
+X3:3:SCL
+
+
+
+ X2:4:SDA
+
+4:YE
+
+X3:4:SDA
+
+
+
+
+
+This cable is a bit shorter
X2:e--W2:w
-
-
-
+
+
+
X2:e--W2:w
-
-
-
+
+
+
X2:e--W2:w
-
-
-
+
+
+
X2:e--W2:w
-
-
-
+
+
+
X3
-
-
-X3
-
-Molex KK 254
-
-female
-
-4-pin
-
-1
-
-GND
-
-2
-
-VCC
-
-3
-
-SCL
-
-4
-
-SDA
-
-to temperature sensor
+
+
+
+X3
+
+Molex KK 254
+
+female
+
+4-pin
+
+1
+
+GND
+
+2
+
+VCC
+
+3
+
+SCL
+
+4
+
+SDA
+
+to temperature sensor
W1:e--X2:w
-
-
-
+
+
+
W1:e--X2:w
-
-
-
+
+
+
W1:e--X2:w
-
-
-
+
+
+
W1:e--X2:w
-
-
-
+
+
+
W2:e--X3:w
-
-
-
+
+
+
W2:e--X3:w
-
-
-
+
+
+
W2:e--X3:w
-
-
-
+
+
+
W2:e--X3:w
-
-
-
+
+
+
diff --git a/tutorial/tutorial05.bom.tsv b/tutorial/tutorial05.bom.tsv
index 607f55b57..3c01ee36a 100644
--- a/tutorial/tutorial05.bom.tsv
+++ b/tutorial/tutorial05.bom.tsv
@@ -1,6 +1,6 @@
-Id Description Qty Unit Designators
-1 Connector, Crimp ferrule, 0.5 mm², OG 4
-2 Connector, Molex 8981, female, 4 pins 1 X1
-3 Wire, 0.5 mm², BK 0.6 m W1
-4 Wire, 0.5 mm², RD 0.3 m W1
-5 Wire, 0.5 mm², YE 0.3 m W1
+# Qty Unit Description Designators
+1 4 Connector, Crimp ferrule, 0.5 mm², OG
+2 1 Connector, Molex 8981, female, 4 pins X1
+3 2 m Wire, 0.5 mm², BK W1
+4 1 m Wire, 0.5 mm², RD W1
+5 1 m Wire, 0.5 mm², YE W1
diff --git a/tutorial/tutorial05.gv b/tutorial/tutorial05.gv
index 32af80d60..2aab2d162 100644
--- a/tutorial/tutorial05.gv
+++ b/tutorial/tutorial05.gv
@@ -1,189 +1,257 @@
graph {
-// Graph generated by WireViz 0.4
-// https://github.com/formatc1702/WireViz
+// Graph generated by WireViz 0.5-dev+refactor
+// https://github.com/wireviz/WireViz
graph [bgcolor="#FFFFFF" fontname=arial nodesep=0.33 rankdir=LR ranksep=2]
node [fillcolor="#FFFFFF" fontname=arial height=0 margin=0 shape=none style=filled width=0]
edge [fontname=arial style=bold]
- __F1_1 [label=<
-
-
-
- Crimp ferrule
- 0.5 mm²
- OG
-
-
-
+ AUTOGENERATED_F1_1 [label=<
+
+
+
+
+
+ Crimp ferrule
+ 0.5 mm²
+ OG
+
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style=filled]
- __F1_2 [label=<
-
-
-
- Crimp ferrule
- 0.5 mm²
- OG
-
-
-
+> shape=box style=filled]
+ AUTOGENERATED_F1_2 [label=<
+
+
+
+
+
+ Crimp ferrule
+ 0.5 mm²
+ OG
+
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style=filled]
- __F1_3 [label=<
-
-
-
- Crimp ferrule
- 0.5 mm²
- OG
-
-
-
+> shape=box style=filled]
+ AUTOGENERATED_F1_3 [label=<
+
+
+
+
+
+ Crimp ferrule
+ 0.5 mm²
+ OG
+
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style=filled]
- __F1_4 [label=<
-
-
-
- Crimp ferrule
- 0.5 mm²
- OG
-
-
-
+> shape=box style=filled]
+ AUTOGENERATED_F1_4 [label=<
+
+
+
+
+
+ Crimp ferrule
+ 0.5 mm²
+ OG
+
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style=filled]
+> shape=box style=filled]
X1 [label=<
-
-
-
-
-
-
- Molex 8981
- female
- 4-pin
-
-
-
-
-
- 1
- +12V
-
-
- 2
- GND
-
-
- 3
- GND
-
-
- 4
- +5V
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Molex 8981
+ female
+ 4-pin
+
+
+
+
+
+
+
+
+ 1
+ +12V
+
+
+ 2
+ GND
+
+
+ 3
+ GND
+
+
+ 4
+ +5V
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style=filled]
- edge [color="#000000:#ffff00:#000000"]
- __F1_1:e -- W1:w1:w
+> shape=box style=filled]
+ W1 [label=<
+
+
+
+
+
+
+
+
+
+
+ 4x
+ 0.5 mm²
+ 0.3 m
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ YE
+
+ X1:1:+12V
+
+
+
+
+
+
+
+
+
+
+ BK
+
+ X1:2:GND
+
+
+
+
+
+
+
+
+
+
+ BK
+
+ X1:3:GND
+
+
+
+
+
+
+
+
+
+
+ RD
+
+ X1:4:+5V
+
+
+
+
+
+
+
+
+
+
+
+
+
+> shape=box style="filled,dashed"]
+ edge [color="#000000:#FFFF00:#000000"]
+ AUTOGENERATED_F1_1:e -- W1:w1:w
W1:w1:e -- X1:p1l:w
edge [color="#000000:#000000:#000000"]
- __F1_2:e -- W1:w2:w
+ AUTOGENERATED_F1_2:e -- W1:w2:w
W1:w2:e -- X1:p2l:w
edge [color="#000000:#000000:#000000"]
- __F1_3:e -- W1:w3:w
+ AUTOGENERATED_F1_3:e -- W1:w3:w
W1:w3:e -- X1:p3l:w
- edge [color="#000000:#ff0000:#000000"]
- __F1_4:e -- W1:w4:w
+ edge [color="#000000:#FF0000:#000000"]
+ AUTOGENERATED_F1_4:e -- W1:w4:w
W1:w4:e -- X1:p4l:w
- W1 [label=<
-
-
-
-
-
-
-
-
-
-
-
-
-
- YE
-
- X1:1:+12V
-
-
-
-
-
-
-
-
-
- BK
-
- X1:2:GND
-
-
-
-
-
-
-
-
-
- BK
-
- X1:3:GND
-
-
-
-
-
-
-
-
-
- RD
-
- X1:4:+5V
-
-
-
-
-
-
-
-
-
-
-> fillcolor="#FFFFFF" shape=box style="filled,dashed"]
}
diff --git a/tutorial/tutorial05.html b/tutorial/tutorial05.html
index 0fb1f6d94..80c7f2ac5 100644
--- a/tutorial/tutorial05.html
+++ b/tutorial/tutorial05.html
@@ -1,7 +1,7 @@
-
+
tutorial05
-
+
tutorial05
Diagram
@@ -30,17 +30,18 @@ Diagram
-
-
-
-
-
+
+
+
+
-__F1_1
+AUTOGENERATED_F1_1
+
Crimp ferrule
@@ -53,49 +54,63 @@ Diagram
W1
-
-
-W1
-
-4x
-
-0.5 mm²
-
-0.3 m
-
- YE
-X1:1:+12V
-
-
-
- BK
-X1:2:GND
-
-
-
- BK
-X1:3:GND
-
-
-
- RD
-X1:4:+5V
-
-
-
-
+
+
+
+W1
+
+4x
+
+0.5 mm²
+
+0.3 m
+
+
+
+YE
+
+X1:1:+12V
+
+
+
+
+
+BK
+
+X1:2:GND
+
+
+
+
+
+BK
+
+X1:3:GND
+
+
+
+
+
+RD
+
+X1:4:+5V
+
+
+
+
-
+
-__F1_1:e--W1:w
-
-
-
+AUTOGENERATED_F1_1:e--W1:w
+
+
+
-
+
-__F1_2
+AUTOGENERATED_F1_2
+
Crimp ferrule
@@ -105,17 +120,18 @@ Diagram
-
+
-__F1_2:e--W1:w
-
-
-
+AUTOGENERATED_F1_2:e--W1:w
+
+
+
-
+
-__F1_3
+AUTOGENERATED_F1_3
+
Crimp ferrule
@@ -125,17 +141,18 @@ Diagram
-
+
-__F1_3:e--W1:w
-
-
-
+AUTOGENERATED_F1_3:e--W1:w
+
+
+
-
+
-__F1_4
+AUTOGENERATED_F1_4
+
Crimp ferrule
@@ -145,69 +162,70 @@ Diagram
-
+
-__F1_4:e--W1:w
-
-
-
+AUTOGENERATED_F1_4:e--W1:w
+
+
+
X1
-
-
-X1
-
-Molex 8981
-
-female
-
-4-pin
-
-1
-
-+12V
-
-2
-
-GND
-
-3
-
-GND
-
-4
-
-+5V
+
+
+
+X1
+
+Molex 8981
+
+female
+
+4-pin
+
+1
+
++12V
+
+2
+
+GND
+
+3
+
+GND
+
+4
+
++5V
W1:e--X1:w
-
-
-
+
+
+
W1:e--X1:w
-
-
-
+
+
+
W1:e--X1:w
-
-
-
+
+
+
W1:e--X1:w
-
-
-
+
+
+
@@ -223,45 +241,45 @@ Bill of Materials
- Id
- Description
+ #
Qty
Unit
+ Description
Designators
- 1
- Connector, Crimp ferrule, 0.5 mm², OG
+ 1
4
+ Connector, Crimp ferrule, 0.5 mm², OG
- 2
- Connector, Molex 8981, female, 4 pins
+ 2
1
+ Connector, Molex 8981, female, 4 pins
X1
- 3
- Wire, 0.5 mm², BK
- 0.6
+ 3
+ 2
m
+ Wire, 0.5 mm², BK
W1
- 4
- Wire, 0.5 mm², RD
- 0.3
+ 4
+ 1
m
+ Wire, 0.5 mm², RD
W1
- 5
- Wire, 0.5 mm², YE
- 0.3
+ 5
+ 1
m
+ Wire, 0.5 mm², YE
W1
diff --git a/tutorial/tutorial05.png b/tutorial/tutorial05.png
index 44e745ad1..d8a8eeb11 100644
Binary files a/tutorial/tutorial05.png and b/tutorial/tutorial05.png differ
diff --git a/tutorial/tutorial05.svg b/tutorial/tutorial05.svg
index 4f36ca370..8995d4904 100644
--- a/tutorial/tutorial05.svg
+++ b/tutorial/tutorial05.svg
@@ -1,17 +1,18 @@
-
-
-
-
-
+
+
+
+
-__F1_1
+AUTOGENERATED_F1_1
+
Crimp ferrule
@@ -24,49 +25,63 @@
W1
-
-
-W1
-
-4x
-
-0.5 mm²
-
-0.3 m
-
- YE
-X1:1:+12V
-
-
-
- BK
-X1:2:GND
-
-
-
- BK
-X1:3:GND
-
-
-
- RD
-X1:4:+5V
-
-
-
-
+
+
+
+W1
+
+4x
+
+0.5 mm²
+
+0.3 m
+
+
+
+YE
+
+X1:1:+12V
+
+
+
+
+
+BK
+
+X1:2:GND
+
+
+
+
+
+BK
+
+X1:3:GND
+
+
+
+
+
+RD
+
+X1:4:+5V
+
+
+
+
-
+
-__F1_1:e--W1:w
-
-
-
+AUTOGENERATED_F1_1:e--W1:w
+
+
+
-
+
-__F1_2
+AUTOGENERATED_F1_2
+
Crimp ferrule
@@ -76,17 +91,18 @@
-
+
-__F1_2:e--W1:w
-
-
-
+AUTOGENERATED_F1_2:e--W1:w
+
+
+
-
+
-__F1_3
+AUTOGENERATED_F1_3
+
Crimp ferrule
@@ -96,17 +112,18 @@
-
+
-__F1_3:e--W1:w
-
-
-
+AUTOGENERATED_F1_3:e--W1:w
+
+
+
-
+
-__F1_4
+AUTOGENERATED_F1_4
+
Crimp ferrule
@@ -116,69 +133,70 @@
-
+
-__F1_4:e--W1:w
-
-
-
+AUTOGENERATED_F1_4:e--W1:w
+
+
+
X1
-
-
-X1
-
-Molex 8981
-
-female
-
-4-pin
-
-1
-
-+12V
-
-2
-
-GND
-
-3
-
-GND
-
-4
-
-+5V
+
+
+
+X1
+
+Molex 8981
+
+female
+
+4-pin
+
+1
+
++12V
+
+2
+
+GND
+
+3
+
+GND
+
+4
+
++5V
W1:e--X1:w
-
-
-
+
+
+
W1:e--X1:w
-
-
-
+
+
+
W1:e--X1:w
-
-
-
+
+
+
W1:e--X1:w
-
-
-
+
+
+
diff --git a/tutorial/tutorial06.bom.tsv b/tutorial/tutorial06.bom.tsv
index b86b323c5..8dd2bff65 100644
--- a/tutorial/tutorial06.bom.tsv
+++ b/tutorial/tutorial06.bom.tsv
@@ -1,7 +1,7 @@
-Id Description Qty Unit Designators
-1 Connector, Crimp ferrule, 0.5 mm², OG 2
-2 Connector, Crimp ferrule, 1.0 mm², YE 1
-3 Connector, Molex 8981, female, 4 pins 1 X1
-4 Wire, 0.5 mm², BK 0.6 m W1
-5 Wire, 0.5 mm², RD 0.3 m W1
-6 Wire, 0.5 mm², YE 0.3 m W1
+# Qty Unit Description Designators
+1 2 Connector, Crimp ferrule, 0.5 mm², OG
+2 1 Connector, Crimp ferrule, 1.0 mm², YE F1
+3 1 Connector, Molex 8981, female, 4 pins X1
+4 2 m Wire, 0.5 mm², BK W1
+5 1 m Wire, 0.5 mm², RD W1
+6 1 m Wire, 0.5 mm², YE W1
diff --git a/tutorial/tutorial06.gv b/tutorial/tutorial06.gv
index 1e42f262f..31bd8f591 100644
--- a/tutorial/tutorial06.gv
+++ b/tutorial/tutorial06.gv
@@ -1,83 +1,233 @@
graph {
-// Graph generated by WireViz 0.4
-// https://github.com/formatc1702/WireViz
+// Graph generated by WireViz 0.5-dev+refactor
+// https://github.com/wireviz/WireViz
graph [bgcolor="#FFFFFF" fontname=arial nodesep=0.33 rankdir=LR ranksep=2]
node [fillcolor="#FFFFFF" fontname=arial height=0 margin=0 shape=none style=filled width=0]
edge [fontname=arial style=bold]
- __F_05_1 [label=<
-
-
-
- Crimp ferrule
- 0.5 mm²
- OG
-
-
-
+ AUTOGENERATED_F_05_1 [label=<
+
+
+
+
+
+ Crimp ferrule
+ 0.5 mm²
+ OG
+
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style=filled]
+> shape=box style=filled]
F1 [label=<
-
-
-
- Crimp ferrule
- 1.0 mm²
- YE
-
-
-
+
+
+
+
+
+ Crimp ferrule
+ 1.0 mm²
+ YE
+
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style=filled]
- __F_05_2 [label=<
-
-
-
- Crimp ferrule
- 0.5 mm²
- OG
-
-
-
+> shape=box style=filled]
+ AUTOGENERATED_F_05_2 [label=<
+
+
+
+
+
+ Crimp ferrule
+ 0.5 mm²
+ OG
+
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style=filled]
+> shape=box style=filled]
X1 [label=<
-
-
-
-
-
-
- Molex 8981
- female
- 4-pin
-
-
-
-
-
- 1
- +12V
-
-
- 2
- GND
-
-
- 3
- GND
-
-
- 4
- +5V
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Molex 8981
+ female
+ 4-pin
+
+
+
+
+
+
+
+
+ 1
+ +12V
+
+
+ 2
+ GND
+
+
+ 3
+ GND
+
+
+ 4
+ +5V
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style=filled]
- edge [color="#000000:#ffff00:#000000"]
- __F_05_1:e -- W1:w1:w
+> shape=box style=filled]
+ W1 [label=<
+
+
+
+
+
+
+
+
+
+
+ 4x
+ 0.5 mm²
+ 0.3 m
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ YE
+
+ X1:1:+12V
+
+
+
+
+
+
+
+ F1
+
+
+ BK
+
+ X1:2:GND
+
+
+
+
+
+
+
+ F1
+
+
+ BK
+
+ X1:3:GND
+
+
+
+
+
+
+
+
+
+
+ RD
+
+ X1:4:+5V
+
+
+
+
+
+
+
+
+
+
+
+
+
+> shape=box style="filled,dashed"]
+ edge [color="#000000:#FFFF00:#000000"]
+ AUTOGENERATED_F_05_1:e -- W1:w1:w
W1:w1:e -- X1:p1l:w
edge [color="#000000:#000000:#000000"]
F1:e -- W1:w2:w
@@ -85,93 +235,7 @@ graph {
edge [color="#000000:#000000:#000000"]
F1:e -- W1:w3:w
W1:w3:e -- X1:p3l:w
- edge [color="#000000:#ff0000:#000000"]
- __F_05_2:e -- W1:w4:w
+ edge [color="#000000:#FF0000:#000000"]
+ AUTOGENERATED_F_05_2:e -- W1:w4:w
W1:w4:e -- X1:p4l:w
- W1 [label=<
-
-
-
-
-
-
-
-
-
-
-
-
-
- YE
-
- X1:1:+12V
-
-
-
-
-
-
-
-
-
- BK
-
- X1:2:GND
-
-
-
-
-
-
-
-
-
- BK
-
- X1:3:GND
-
-
-
-
-
-
-
-
-
- RD
-
- X1:4:+5V
-
-
-
-
-
-
-
-
-
-
-> fillcolor="#FFFFFF" shape=box style="filled,dashed"]
}
diff --git a/tutorial/tutorial06.html b/tutorial/tutorial06.html
index 18589ba84..cb52865e1 100644
--- a/tutorial/tutorial06.html
+++ b/tutorial/tutorial06.html
@@ -1,7 +1,7 @@
-
+
tutorial06
-
+
tutorial06
Diagram
@@ -30,171 +30,188 @@ Diagram
-
-
-
-
-
+
+
+
+
-__F_05_1
-
-
-Crimp ferrule
-
-0.5 mm²
-
-OG
-
-
+AUTOGENERATED_F_05_1
+
+
+
+Crimp ferrule
+
+0.5 mm²
+
+OG
+
+
W1
-
-
-W1
-
-4x
-
-0.5 mm²
-
-0.3 m
-
- YE
-X1:1:+12V
-
-
-
- BK
-X1:2:GND
-
-
-
- BK
-X1:3:GND
-
-
-
- RD
-X1:4:+5V
-
-
-
-
+
+
+
+W1
+
+4x
+
+0.5 mm²
+
+0.3 m
+
+
+
+YE
+
+X1:1:+12V
+
+
+
+ F1
+
+BK
+
+X1:2:GND
+
+
+
+ F1
+
+BK
+
+X1:3:GND
+
+
+
+
+
+RD
+
+X1:4:+5V
+
+
+
+
-
+
-__F_05_1:e--W1:w
-
-
-
+AUTOGENERATED_F_05_1:e--W1:w
+
+
+
F1
-
-
-Crimp ferrule
-
-1.0 mm²
-
-YE
-
-
+
+
+
+Crimp ferrule
+
+1.0 mm²
+
+YE
+
+
F1:e--W1:w
-
-
-
+
+
+
F1:e--W1:w
-
-
-
+
+
+
-
+
-__F_05_2
-
-
-Crimp ferrule
-
-0.5 mm²
-
-OG
-
-
+AUTOGENERATED_F_05_2
+
+
+
+Crimp ferrule
+
+0.5 mm²
+
+OG
+
+
-
+
-__F_05_2:e--W1:w
-
-
-
+AUTOGENERATED_F_05_2:e--W1:w
+
+
+
X1
-
-
-X1
-
-Molex 8981
-
-female
-
-4-pin
-
-1
-
-+12V
-
-2
-
-GND
-
-3
-
-GND
-
-4
-
-+5V
+
+
+
+X1
+
+Molex 8981
+
+female
+
+4-pin
+
+1
+
++12V
+
+2
+
+GND
+
+3
+
+GND
+
+4
+
++5V
W1:e--X1:w
-
-
-
+
+
+
W1:e--X1:w
-
-
-
+
+
+
W1:e--X1:w
-
-
-
+
+
+
W1:e--X1:w
-
-
-
+
+
+
@@ -210,52 +227,52 @@ Bill of Materials
- Id
- Description
+ #
Qty
Unit
+ Description
Designators
- 1
- Connector, Crimp ferrule, 0.5 mm², OG
+ 1
2
+ Connector, Crimp ferrule, 0.5 mm², OG
- 2
- Connector, Crimp ferrule, 1.0 mm², YE
+ 2
1
-
+ Connector, Crimp ferrule, 1.0 mm², YE
+ F1
- 3
- Connector, Molex 8981, female, 4 pins
+ 3
1
+ Connector, Molex 8981, female, 4 pins
X1
- 4
- Wire, 0.5 mm², BK
- 0.6
+ 4
+ 2
m
+ Wire, 0.5 mm², BK
W1
- 5
- Wire, 0.5 mm², RD
- 0.3
+ 5
+ 1
m
+ Wire, 0.5 mm², RD
W1
- 6
- Wire, 0.5 mm², YE
- 0.3
+ 6
+ 1
m
+ Wire, 0.5 mm², YE
W1
diff --git a/tutorial/tutorial06.png b/tutorial/tutorial06.png
index 43e00cbd0..55711813a 100644
Binary files a/tutorial/tutorial06.png and b/tutorial/tutorial06.png differ
diff --git a/tutorial/tutorial06.svg b/tutorial/tutorial06.svg
index 973fc238f..6800d7f0f 100644
--- a/tutorial/tutorial06.svg
+++ b/tutorial/tutorial06.svg
@@ -1,171 +1,188 @@
-
-
-
-
-
+
+
+
+
-__F_05_1
-
-
-Crimp ferrule
-
-0.5 mm²
-
-OG
-
-
+AUTOGENERATED_F_05_1
+
+
+
+Crimp ferrule
+
+0.5 mm²
+
+OG
+
+
W1
-
-
-W1
-
-4x
-
-0.5 mm²
-
-0.3 m
-
- YE
-X1:1:+12V
-
-
-
- BK
-X1:2:GND
-
-
-
- BK
-X1:3:GND
-
-
-
- RD
-X1:4:+5V
-
-
-
-
+
+
+
+W1
+
+4x
+
+0.5 mm²
+
+0.3 m
+
+
+
+YE
+
+X1:1:+12V
+
+
+
+ F1
+
+BK
+
+X1:2:GND
+
+
+
+ F1
+
+BK
+
+X1:3:GND
+
+
+
+
+
+RD
+
+X1:4:+5V
+
+
+
+
-
+
-__F_05_1:e--W1:w
-
-
-
+AUTOGENERATED_F_05_1:e--W1:w
+
+
+
F1
-
-
-Crimp ferrule
-
-1.0 mm²
-
-YE
-
-
+
+
+
+Crimp ferrule
+
+1.0 mm²
+
+YE
+
+
F1:e--W1:w
-
-
-
+
+
+
F1:e--W1:w
-
-
-
+
+
+
-
+
-__F_05_2
-
-
-Crimp ferrule
-
-0.5 mm²
-
-OG
-
-
+AUTOGENERATED_F_05_2
+
+
+
+Crimp ferrule
+
+0.5 mm²
+
+OG
+
+
-
+
-__F_05_2:e--W1:w
-
-
-
+AUTOGENERATED_F_05_2:e--W1:w
+
+
+
X1
-
-
-X1
-
-Molex 8981
-
-female
-
-4-pin
-
-1
-
-+12V
-
-2
-
-GND
-
-3
-
-GND
-
-4
-
-+5V
+
+
+
+X1
+
+Molex 8981
+
+female
+
+4-pin
+
+1
+
++12V
+
+2
+
+GND
+
+3
+
+GND
+
+4
+
++5V
W1:e--X1:w
-
-
-
+
+
+
W1:e--X1:w
-
-
-
+
+
+
W1:e--X1:w
-
-
-
+
+
+
W1:e--X1:w
-
-
-
+
+
+
diff --git a/tutorial/tutorial07.bom.tsv b/tutorial/tutorial07.bom.tsv
index 0c3d0dd22..62377db6b 100644
--- a/tutorial/tutorial07.bom.tsv
+++ b/tutorial/tutorial07.bom.tsv
@@ -1,6 +1,6 @@
-Id Description Qty Unit Designators
-1 Connector, Molex KK 254, female, 4 pins 6 X1, X2, X3, X4, X5, X6
-2 Wire, 0.25 mm², PK 1.0 m W1, W2, W3, W4, W5
-3 Wire, 0.25 mm², TQ 1.0 m W1, W2, W3, W4, W5
-4 Wire, 0.25 mm², VT 1.0 m W1, W2, W3, W4, W5
-5 Wire, 0.25 mm², YE 1.0 m W1, W2, W3, W4, W5
+# Qty Unit Description Designators
+1 6 Connector, Molex KK 254, female, 4 pins X1, X2, X3, X4, X5, X6
+2 5 m Wire, 0.25 mm², PK W1, W2, W3, W4, W5
+3 5 m Wire, 0.25 mm², TQ W1, W2, W3, W4, W5
+4 5 m Wire, 0.25 mm², VT W1, W2, W3, W4, W5
+5 5 m Wire, 0.25 mm², YE W1, W2, W3, W4, W5
diff --git a/tutorial/tutorial07.gv b/tutorial/tutorial07.gv
index ff6568854..84c378290 100644
--- a/tutorial/tutorial07.gv
+++ b/tutorial/tutorial07.gv
@@ -1,713 +1,983 @@
graph {
-// Graph generated by WireViz 0.4
-// https://github.com/formatc1702/WireViz
+// Graph generated by WireViz 0.5-dev+refactor
+// https://github.com/wireviz/WireViz
graph [bgcolor="#FFFFFF" fontname=arial nodesep=0.33 rankdir=LR ranksep=2]
node [fillcolor="#FFFFFF" fontname=arial height=0 margin=0 shape=none style=filled width=0]
edge [fontname=arial style=bold]
X1 [label=<
-
-
-
-
-
-
- Molex KK 254
- female
- 4-pin
-
-
-
-
-
- GND
- 1
-
-
- VCC
- 2
-
-
- SCL
- 3
-
-
- SDA
- 4
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Molex KK 254
+ female
+ 4-pin
+
+
+
+
+
+
+
+
+ GND
+ 1
+
+
+ VCC
+ 2
+
+
+ SCL
+ 3
+
+
+ SDA
+ 4
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style=filled]
+> shape=box style=filled]
X2 [label=<
-
-
-
-
-
-
- Molex KK 254
- female
- 4-pin
-
-
-
-
-
- 1
- GND
-
-
- 2
- VCC
-
-
- 3
- SCL
-
-
- 4
- SDA
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Molex KK 254
+ female
+ 4-pin
+
+
+
+
+
+
+
+
+ 1
+ GND
+
+
+ 2
+ VCC
+
+
+ 3
+ SCL
+
+
+ 4
+ SDA
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style=filled]
+> shape=box style=filled]
X3 [label=<
-
-
-
-
-
-
- Molex KK 254
- female
- 4-pin
-
-
-
-
-
- GND
- 1
-
-
- VCC
- 2
-
-
- SCL
- 3
-
-
- SDA
- 4
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Molex KK 254
+ female
+ 4-pin
+
+
+
+
+
+
+
+
+ GND
+ 1
+
+
+ VCC
+ 2
+
+
+ SCL
+ 3
+
+
+ SDA
+ 4
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style=filled]
+> shape=box style=filled]
X4 [label=<
-
-
-
-
-
-
- Molex KK 254
- female
- 4-pin
-
-
-
-
-
- 1
- GND
-
-
- 2
- VCC
-
-
- 3
- SCL
-
-
- 4
- SDA
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Molex KK 254
+ female
+ 4-pin
+
+
+
+
+
+
+
+
+ 1
+ GND
+
+
+ 2
+ VCC
+
+
+ 3
+ SCL
+
+
+ 4
+ SDA
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style=filled]
+> shape=box style=filled]
X5 [label=<
-
-
-
-
-
-
- Molex KK 254
- female
- 4-pin
-
-
-
-
-
- GND
- 1
-
-
- VCC
- 2
-
-
- SCL
- 3
-
-
- SDA
- 4
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Molex KK 254
+ female
+ 4-pin
+
+
+
+
+
+
+
+
+ GND
+ 1
+
+
+ VCC
+ 2
+
+
+ SCL
+ 3
+
+
+ SDA
+ 4
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style=filled]
+> shape=box style=filled]
X6 [label=<
-
-
-
-
-
-
- Molex KK 254
- female
- 4-pin
-
-
-
-
-
- 1
- GND
-
-
- 2
- VCC
-
-
- 3
- SCL
-
-
- 4
- SDA
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Molex KK 254
+ female
+ 4-pin
+
+
+
+
+
+
+
+
+ 1
+ GND
+
+
+ 2
+ VCC
+
+
+ 3
+ SCL
+
+
+ 4
+ SDA
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style=filled]
- edge [color="#000000:#00ffff:#000000"]
+> shape=box style=filled]
+ W1 [label=<
+
+
+
+
+
+
+
+
+
+
+ 4x
+ 0.25 mm²
+ 0.2 m
+
+
+
+
+
+
+
+
+
+
+
+ X1:1:GND
+
+
+ TQ
+
+ X2:1:GND
+
+
+
+
+
+
+
+ X1:2:VCC
+
+
+ PK
+
+ X2:2:VCC
+
+
+
+
+
+
+
+ X1:3:SCL
+
+
+ YE
+
+ X2:3:SCL
+
+
+
+
+
+
+
+ X1:4:SDA
+
+
+ VT
+
+ X2:4:SDA
+
+
+
+
+
+
+
+
+
+
+
+
+
+> shape=box style="filled,dashed"]
+ edge [color="#000000:#00FFFF:#000000"]
X1:p1r:e -- W1:w1:w
W1:w1:e -- X2:p1l:w
- edge [color="#000000:#ff66cc:#000000"]
+ edge [color="#000000:#FF66CC:#000000"]
X1:p2r:e -- W1:w2:w
W1:w2:e -- X2:p2l:w
- edge [color="#000000:#ffff00:#000000"]
+ edge [color="#000000:#FFFF00:#000000"]
X1:p3r:e -- W1:w3:w
W1:w3:e -- X2:p3l:w
- edge [color="#000000:#8000ff:#000000"]
+ edge [color="#000000:#8000FF:#000000"]
X1:p4r:e -- W1:w4:w
W1:w4:e -- X2:p4l:w
- W1 [label=<
-
-
-
-
-
-
- 4x
- 0.25 mm²
- 0.2 m
-
-
-
-
-
-
- X1:1:GND
-
- TQ
-
- X2:1:GND
-
-
-
-
-
-
-
- X1:2:VCC
-
- PK
-
- X2:2:VCC
-
-
-
-
-
-
-
- X1:3:SCL
-
- YE
-
- X2:3:SCL
-
-
-
-
-
-
-
- X1:4:SDA
-
- VT
-
- X2:4:SDA
-
-
-
-
-
-
-
-
-
+ W2 [label=<
+
+
+
+
+
+
+
+
+
+
+ 4x
+ 0.25 mm²
+ 0.2 m
+
+
+
+
+
+
+
+
+
+
+
+ X3:1:GND
+
+
+ TQ
+
+ X2:1:GND
+
+
+
+
+
+
+
+ X3:2:VCC
+
+
+ PK
+
+ X2:2:VCC
+
+
+
+
+
+
+
+ X3:3:SCL
+
+
+ YE
+
+ X2:3:SCL
+
+
+
+
+
+
+
+ X3:4:SDA
+
+
+ VT
+
+ X2:4:SDA
+
+
+
+
+
+
+
+
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style="filled,dashed"]
- edge [color="#000000:#00ffff:#000000"]
+> shape=box style="filled,dashed"]
+ edge [color="#000000:#00FFFF:#000000"]
X3:p1r:e -- W2:w1:w
W2:w1:e -- X2:p1l:w
- edge [color="#000000:#ff66cc:#000000"]
+ edge [color="#000000:#FF66CC:#000000"]
X3:p2r:e -- W2:w2:w
W2:w2:e -- X2:p2l:w
- edge [color="#000000:#ffff00:#000000"]
+ edge [color="#000000:#FFFF00:#000000"]
X3:p3r:e -- W2:w3:w
W2:w3:e -- X2:p3l:w
- edge [color="#000000:#8000ff:#000000"]
+ edge [color="#000000:#8000FF:#000000"]
X3:p4r:e -- W2:w4:w
W2:w4:e -- X2:p4l:w
- W2 [label=<
-
-
-
-
-
-
- 4x
- 0.25 mm²
- 0.2 m
-
-
-
-
-
-
- X3:1:GND
-
- TQ
-
- X2:1:GND
-
-
-
-
-
-
-
- X3:2:VCC
-
- PK
-
- X2:2:VCC
-
-
-
-
-
-
-
- X3:3:SCL
-
- YE
-
- X2:3:SCL
-
-
-
-
-
-
-
- X3:4:SDA
-
- VT
-
- X2:4:SDA
-
-
-
-
-
-
-
-
-
+ W3 [label=<
+
+
+
+
+
+
+
+
+
+
+ 4x
+ 0.25 mm²
+ 0.2 m
+
+
+
+
+
+
+
+
+
+
+
+ X3:1:GND
+
+
+ TQ
+
+ X4:1:GND
+
+
+
+
+
+
+
+ X3:2:VCC
+
+
+ PK
+
+ X4:2:VCC
+
+
+
+
+
+
+
+ X3:3:SCL
+
+
+ YE
+
+ X4:3:SCL
+
+
+
+
+
+
+
+ X3:4:SDA
+
+
+ VT
+
+ X4:4:SDA
+
+
+
+
+
+
+
+
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style="filled,dashed"]
- edge [color="#000000:#00ffff:#000000"]
+> shape=box style="filled,dashed"]
+ edge [color="#000000:#00FFFF:#000000"]
X3:p1r:e -- W3:w1:w
W3:w1:e -- X4:p1l:w
- edge [color="#000000:#ff66cc:#000000"]
+ edge [color="#000000:#FF66CC:#000000"]
X3:p2r:e -- W3:w2:w
W3:w2:e -- X4:p2l:w
- edge [color="#000000:#ffff00:#000000"]
+ edge [color="#000000:#FFFF00:#000000"]
X3:p3r:e -- W3:w3:w
W3:w3:e -- X4:p3l:w
- edge [color="#000000:#8000ff:#000000"]
+ edge [color="#000000:#8000FF:#000000"]
X3:p4r:e -- W3:w4:w
W3:w4:e -- X4:p4l:w
- W3 [label=<
-
-
-
-
-
-
- 4x
- 0.25 mm²
- 0.2 m
-
-
-
-
-
-
- X3:1:GND
-
- TQ
-
- X4:1:GND
-
-
-
-
-
-
-
- X3:2:VCC
-
- PK
-
- X4:2:VCC
-
-
-
-
-
-
-
- X3:3:SCL
-
- YE
-
- X4:3:SCL
-
-
-
-
-
-
-
- X3:4:SDA
-
- VT
-
- X4:4:SDA
-
-
-
-
-
-
-
-
-
+ W4 [label=<
+
+
+
+
+
+
+
+
+
+
+ 4x
+ 0.25 mm²
+ 0.2 m
+
+
+
+
+
+
+
+
+
+
+
+ X5:1:GND
+
+
+ TQ
+
+ X4:1:GND
+
+
+
+
+
+
+
+ X5:2:VCC
+
+
+ PK
+
+ X4:2:VCC
+
+
+
+
+
+
+
+ X5:3:SCL
+
+
+ YE
+
+ X4:3:SCL
+
+
+
+
+
+
+
+ X5:4:SDA
+
+
+ VT
+
+ X4:4:SDA
+
+
+
+
+
+
+
+
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style="filled,dashed"]
- edge [color="#000000:#00ffff:#000000"]
+> shape=box style="filled,dashed"]
+ edge [color="#000000:#00FFFF:#000000"]
X5:p1r:e -- W4:w1:w
W4:w1:e -- X4:p1l:w
- edge [color="#000000:#ff66cc:#000000"]
+ edge [color="#000000:#FF66CC:#000000"]
X5:p2r:e -- W4:w2:w
W4:w2:e -- X4:p2l:w
- edge [color="#000000:#ffff00:#000000"]
+ edge [color="#000000:#FFFF00:#000000"]
X5:p3r:e -- W4:w3:w
W4:w3:e -- X4:p3l:w
- edge [color="#000000:#8000ff:#000000"]
+ edge [color="#000000:#8000FF:#000000"]
X5:p4r:e -- W4:w4:w
W4:w4:e -- X4:p4l:w
- W4 [label=<
-
-
-
-
-
-
- 4x
- 0.25 mm²
- 0.2 m
-
-
-
-
-
-
- X5:1:GND
-
- TQ
-
- X4:1:GND
-
-
-
-
-
-
-
- X5:2:VCC
-
- PK
-
- X4:2:VCC
-
-
-
-
-
-
-
- X5:3:SCL
-
- YE
-
- X4:3:SCL
-
-
-
-
-
-
-
- X5:4:SDA
-
- VT
-
- X4:4:SDA
-
-
-
-
-
-
-
-
-
+ W5 [label=<
+
+
+
+
+
+
+
+
+
+
+ 4x
+ 0.25 mm²
+ 0.2 m
+
+
+
+
+
+
+
+
+
+
+
+ X5:1:GND
+
+
+ TQ
+
+ X6:1:GND
+
+
+
+
+
+
+
+ X5:2:VCC
+
+
+ PK
+
+ X6:2:VCC
+
+
+
+
+
+
+
+ X5:3:SCL
+
+
+ YE
+
+ X6:3:SCL
+
+
+
+
+
+
+
+ X5:4:SDA
+
+
+ VT
+
+ X6:4:SDA
+
+
+
+
+
+
+
+
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style="filled,dashed"]
- edge [color="#000000:#00ffff:#000000"]
+> shape=box style="filled,dashed"]
+ edge [color="#000000:#00FFFF:#000000"]
X5:p1r:e -- W5:w1:w
W5:w1:e -- X6:p1l:w
- edge [color="#000000:#ff66cc:#000000"]
+ edge [color="#000000:#FF66CC:#000000"]
X5:p2r:e -- W5:w2:w
W5:w2:e -- X6:p2l:w
- edge [color="#000000:#ffff00:#000000"]
+ edge [color="#000000:#FFFF00:#000000"]
X5:p3r:e -- W5:w3:w
W5:w3:e -- X6:p3l:w
- edge [color="#000000:#8000ff:#000000"]
+ edge [color="#000000:#8000FF:#000000"]
X5:p4r:e -- W5:w4:w
W5:w4:e -- X6:p4l:w
- W5 [label=<
-
-
-
-
-
-
- 4x
- 0.25 mm²
- 0.2 m
-
-
-
-
-
-
- X5:1:GND
-
- TQ
-
- X6:1:GND
-
-
-
-
-
-
-
- X5:2:VCC
-
- PK
-
- X6:2:VCC
-
-
-
-
-
-
-
- X5:3:SCL
-
- YE
-
- X6:3:SCL
-
-
-
-
-
-
-
- X5:4:SDA
-
- VT
-
- X6:4:SDA
-
-
-
-
-
-
-
-
-
-
-> fillcolor="#FFFFFF" shape=box style="filled,dashed"]
}
diff --git a/tutorial/tutorial07.html b/tutorial/tutorial07.html
index ffb350afa..91b02e317 100644
--- a/tutorial/tutorial07.html
+++ b/tutorial/tutorial07.html
@@ -1,7 +1,7 @@
-
+
tutorial07
-
+
tutorial07
Diagram
@@ -30,661 +30,712 @@ Diagram
-
-
-
-
+
+
+
X1
-
-
-X1
-
-Molex KK 254
-
-female
-
-4-pin
-
-GND
-
-1
-
-VCC
-
-2
-
-SCL
-
-3
-
-SDA
-
-4
+
+
+
+X1
+
+Molex KK 254
+
+female
+
+4-pin
+
+GND
+
+1
+
+VCC
+
+2
+
+SCL
+
+3
+
+SDA
+
+4
W1
-
-
-W1
-
-4x
-
-0.25 mm²
-
-0.2 m
-
-X1:1:GND
- TQ
-X2:1:GND
-
-
-
-X1:2:VCC
- PK
-X2:2:VCC
-
-
-
-X1:3:SCL
- YE
-X2:3:SCL
-
-
-
-X1:4:SDA
- VT
-X2:4:SDA
-
-
-
-
+
+
+
+W1
+
+4x
+
+0.25 mm²
+
+0.2 m
+
+ X1:1:GND
+
+TQ
+
+X2:1:GND
+
+
+
+ X1:2:VCC
+
+PK
+
+X2:2:VCC
+
+
+
+ X1:3:SCL
+
+YE
+
+X2:3:SCL
+
+
+
+ X1:4:SDA
+
+VT
+
+X2:4:SDA
+
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X2
-
-
-X2
-
-Molex KK 254
-
-female
-
-4-pin
-
-1
-
-GND
-
-2
-
-VCC
-
-3
-
-SCL
-
-4
-
-SDA
+
+
+
+X2
+
+Molex KK 254
+
+female
+
+4-pin
+
+1
+
+GND
+
+2
+
+VCC
+
+3
+
+SCL
+
+4
+
+SDA
X3
-
-
-X3
-
-Molex KK 254
-
-female
-
-4-pin
-
-GND
-
-1
-
-VCC
-
-2
-
-SCL
-
-3
-
-SDA
-
-4
+
+
+
+X3
+
+Molex KK 254
+
+female
+
+4-pin
+
+GND
+
+1
+
+VCC
+
+2
+
+SCL
+
+3
+
+SDA
+
+4
W2
-
-
-W2
-
-4x
-
-0.25 mm²
-
-0.2 m
-
-X3:1:GND
- TQ
-X2:1:GND
-
-
-
-X3:2:VCC
- PK
-X2:2:VCC
-
-
-
-X3:3:SCL
- YE
-X2:3:SCL
-
-
-
-X3:4:SDA
- VT
-X2:4:SDA
-
-
-
-
+
+
+
+W2
+
+4x
+
+0.25 mm²
+
+0.2 m
+
+ X3:1:GND
+
+TQ
+
+X2:1:GND
+
+
+
+ X3:2:VCC
+
+PK
+
+X2:2:VCC
+
+
+
+ X3:3:SCL
+
+YE
+
+X2:3:SCL
+
+
+
+ X3:4:SDA
+
+VT
+
+X2:4:SDA
+
+
+
+
X3:e--W2:w
-
-
-
+
+
+
X3:e--W2:w
-
-
-
+
+
+
X3:e--W2:w
-
-
-
+
+
+
X3:e--W2:w
-
-
-
+
+
+
W3
-
-
-W3
-
-4x
-
-0.25 mm²
-
-0.2 m
-
-X3:1:GND
- TQ
-X4:1:GND
-
-
-
-X3:2:VCC
- PK
-X4:2:VCC
-
-
-
-X3:3:SCL
- YE
-X4:3:SCL
-
-
-
-X3:4:SDA
- VT
-X4:4:SDA
-
-
-
-
+
+
+
+W3
+
+4x
+
+0.25 mm²
+
+0.2 m
+
+ X3:1:GND
+
+TQ
+
+X4:1:GND
+
+
+
+ X3:2:VCC
+
+PK
+
+X4:2:VCC
+
+
+
+ X3:3:SCL
+
+YE
+
+X4:3:SCL
+
+
+
+ X3:4:SDA
+
+VT
+
+X4:4:SDA
+
+
+
+
X3:e--W3:w
-
-
-
+
+
+
X3:e--W3:w
-
-
-
+
+
+
X3:e--W3:w
-
-
-
+
+
+
X3:e--W3:w
-
-
-
+
+
+
X4
-
-
-X4
-
-Molex KK 254
-
-female
-
-4-pin
-
-1
-
-GND
-
-2
-
-VCC
-
-3
-
-SCL
-
-4
-
-SDA
+
+
+
+X4
+
+Molex KK 254
+
+female
+
+4-pin
+
+1
+
+GND
+
+2
+
+VCC
+
+3
+
+SCL
+
+4
+
+SDA
X5
-
-
-X5
-
-Molex KK 254
-
-female
-
-4-pin
-
-GND
-
-1
-
-VCC
-
-2
-
-SCL
-
-3
-
-SDA
-
-4
+
+
+
+X5
+
+Molex KK 254
+
+female
+
+4-pin
+
+GND
+
+1
+
+VCC
+
+2
+
+SCL
+
+3
+
+SDA
+
+4
W4
-
-
-W4
-
-4x
-
-0.25 mm²
-
-0.2 m
-
-X5:1:GND
- TQ
-X4:1:GND
-
-
-
-X5:2:VCC
- PK
-X4:2:VCC
-
-
-
-X5:3:SCL
- YE
-X4:3:SCL
-
-
-
-X5:4:SDA
- VT
-X4:4:SDA
-
-
-
-
+
+
+
+W4
+
+4x
+
+0.25 mm²
+
+0.2 m
+
+ X5:1:GND
+
+TQ
+
+X4:1:GND
+
+
+
+ X5:2:VCC
+
+PK
+
+X4:2:VCC
+
+
+
+ X5:3:SCL
+
+YE
+
+X4:3:SCL
+
+
+
+ X5:4:SDA
+
+VT
+
+X4:4:SDA
+
+
+
+
X5:e--W4:w
-
-
-
+
+
+
X5:e--W4:w
-
-
-
+
+
+
X5:e--W4:w
-
-
-
+
+
+
X5:e--W4:w
-
-
-
+
+
+
W5
-
-
-W5
-
-4x
-
-0.25 mm²
-
-0.2 m
-
-X5:1:GND
- TQ
-X6:1:GND
-
-
-
-X5:2:VCC
- PK
-X6:2:VCC
-
-
-
-X5:3:SCL
- YE
-X6:3:SCL
-
-
-
-X5:4:SDA
- VT
-X6:4:SDA
-
-
-
-
+
+
+
+W5
+
+4x
+
+0.25 mm²
+
+0.2 m
+
+ X5:1:GND
+
+TQ
+
+X6:1:GND
+
+
+
+ X5:2:VCC
+
+PK
+
+X6:2:VCC
+
+
+
+ X5:3:SCL
+
+YE
+
+X6:3:SCL
+
+
+
+ X5:4:SDA
+
+VT
+
+X6:4:SDA
+
+
+
+
X5:e--W5:w
-
-
-
+
+
+
X5:e--W5:w
-
-
-
+
+
+
X5:e--W5:w
-
-
-
+
+
+
X5:e--W5:w
-
-
-
+
+
+
X6
-
-
-X6
-
-Molex KK 254
-
-female
-
-4-pin
-
-1
-
-GND
-
-2
-
-VCC
-
-3
-
-SCL
-
-4
-
-SDA
+
+
+
+X6
+
+Molex KK 254
+
+female
+
+4-pin
+
+1
+
+GND
+
+2
+
+VCC
+
+3
+
+SCL
+
+4
+
+SDA
W1:e--X2:w
-
-
-
+
+
+
W1:e--X2:w
-
-
-
+
+
+
W1:e--X2:w
-
-
-
+
+
+
W1:e--X2:w
-
-
-
+
+
+
W2:e--X2:w
-
-
-
+
+
+
W2:e--X2:w
-
-
-
+
+
+
W2:e--X2:w
-
-
-
+
+
+
W2:e--X2:w
-
-
-
+
+
+
W3:e--X4:w
-
-
-
+
+
+
W3:e--X4:w
-
-
-
+
+
+
W3:e--X4:w
-
-
-
+
+
+
W3:e--X4:w
-
-
-
+
+
+
W4:e--X4:w
-
-
-
+
+
+
W4:e--X4:w
-
-
-
+
+
+
W4:e--X4:w
-
-
-
+
+
+
W4:e--X4:w
-
-
-
+
+
+
W5:e--X6:w
-
-
-
+
+
+
W5:e--X6:w
-
-
-
+
+
+
W5:e--X6:w
-
-
-
+
+
+
W5:e--X6:w
-
-
-
+
+
+
@@ -700,45 +751,45 @@ Bill of Materials
- Id
- Description
+ #
Qty
Unit
+ Description
Designators
- 1
- Connector, Molex KK 254, female, 4 pins
+ 1
6
+ Connector, Molex KK 254, female, 4 pins
X1, X2, X3, X4, X5, X6
- 2
- Wire, 0.25 mm², PK
- 1.0
+ 2
+ 5
m
+ Wire, 0.25 mm², PK
W1, W2, W3, W4, W5
- 3
- Wire, 0.25 mm², TQ
- 1.0
+ 3
+ 5
m
+ Wire, 0.25 mm², TQ
W1, W2, W3, W4, W5
- 4
- Wire, 0.25 mm², VT
- 1.0
+ 4
+ 5
m
+ Wire, 0.25 mm², VT
W1, W2, W3, W4, W5
- 5
- Wire, 0.25 mm², YE
- 1.0
+ 5
+ 5
m
+ Wire, 0.25 mm², YE
W1, W2, W3, W4, W5
diff --git a/tutorial/tutorial07.png b/tutorial/tutorial07.png
index c919e02b9..171353ae6 100644
Binary files a/tutorial/tutorial07.png and b/tutorial/tutorial07.png differ
diff --git a/tutorial/tutorial07.svg b/tutorial/tutorial07.svg
index a278a8741..ecbf9e0ed 100644
--- a/tutorial/tutorial07.svg
+++ b/tutorial/tutorial07.svg
@@ -1,661 +1,712 @@
-
-
-
-
+
+
+
X1
-
-
-X1
-
-Molex KK 254
-
-female
-
-4-pin
-
-GND
-
-1
-
-VCC
-
-2
-
-SCL
-
-3
-
-SDA
-
-4
+
+
+
+X1
+
+Molex KK 254
+
+female
+
+4-pin
+
+GND
+
+1
+
+VCC
+
+2
+
+SCL
+
+3
+
+SDA
+
+4
W1
-
-
-W1
-
-4x
-
-0.25 mm²
-
-0.2 m
-
-X1:1:GND
- TQ
-X2:1:GND
-
-
-
-X1:2:VCC
- PK
-X2:2:VCC
-
-
-
-X1:3:SCL
- YE
-X2:3:SCL
-
-
-
-X1:4:SDA
- VT
-X2:4:SDA
-
-
-
-
+
+
+
+W1
+
+4x
+
+0.25 mm²
+
+0.2 m
+
+ X1:1:GND
+
+TQ
+
+X2:1:GND
+
+
+
+ X1:2:VCC
+
+PK
+
+X2:2:VCC
+
+
+
+ X1:3:SCL
+
+YE
+
+X2:3:SCL
+
+
+
+ X1:4:SDA
+
+VT
+
+X2:4:SDA
+
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X1:e--W1:w
-
-
-
+
+
+
X2
-
-
-X2
-
-Molex KK 254
-
-female
-
-4-pin
-
-1
-
-GND
-
-2
-
-VCC
-
-3
-
-SCL
-
-4
-
-SDA
+
+
+
+X2
+
+Molex KK 254
+
+female
+
+4-pin
+
+1
+
+GND
+
+2
+
+VCC
+
+3
+
+SCL
+
+4
+
+SDA
X3
-
-
-X3
-
-Molex KK 254
-
-female
-
-4-pin
-
-GND
-
-1
-
-VCC
-
-2
-
-SCL
-
-3
-
-SDA
-
-4
+
+
+
+X3
+
+Molex KK 254
+
+female
+
+4-pin
+
+GND
+
+1
+
+VCC
+
+2
+
+SCL
+
+3
+
+SDA
+
+4
W2
-
-
-W2
-
-4x
-
-0.25 mm²
-
-0.2 m
-
-X3:1:GND
- TQ
-X2:1:GND
-
-
-
-X3:2:VCC
- PK
-X2:2:VCC
-
-
-
-X3:3:SCL
- YE
-X2:3:SCL
-
-
-
-X3:4:SDA
- VT
-X2:4:SDA
-
-
-
-
+
+
+
+W2
+
+4x
+
+0.25 mm²
+
+0.2 m
+
+ X3:1:GND
+
+TQ
+
+X2:1:GND
+
+
+
+ X3:2:VCC
+
+PK
+
+X2:2:VCC
+
+
+
+ X3:3:SCL
+
+YE
+
+X2:3:SCL
+
+
+
+ X3:4:SDA
+
+VT
+
+X2:4:SDA
+
+
+
+
X3:e--W2:w
-
-
-
+
+
+
X3:e--W2:w
-
-
-
+
+
+
X3:e--W2:w
-
-
-
+
+
+
X3:e--W2:w
-
-
-
+
+
+
W3
-
-
-W3
-
-4x
-
-0.25 mm²
-
-0.2 m
-
-X3:1:GND
- TQ
-X4:1:GND
-
-
-
-X3:2:VCC
- PK
-X4:2:VCC
-
-
-
-X3:3:SCL
- YE
-X4:3:SCL
-
-
-
-X3:4:SDA
- VT
-X4:4:SDA
-
-
-
-
+
+
+
+W3
+
+4x
+
+0.25 mm²
+
+0.2 m
+
+ X3:1:GND
+
+TQ
+
+X4:1:GND
+
+
+
+ X3:2:VCC
+
+PK
+
+X4:2:VCC
+
+
+
+ X3:3:SCL
+
+YE
+
+X4:3:SCL
+
+
+
+ X3:4:SDA
+
+VT
+
+X4:4:SDA
+
+
+
+
X3:e--W3:w
-
-
-
+
+
+
X3:e--W3:w
-
-
-
+
+
+
X3:e--W3:w
-
-
-
+
+
+
X3:e--W3:w
-
-
-
+
+
+
X4
-
-
-X4
-
-Molex KK 254
-
-female
-
-4-pin
-
-1
-
-GND
-
-2
-
-VCC
-
-3
-
-SCL
-
-4
-
-SDA
+
+
+
+X4
+
+Molex KK 254
+
+female
+
+4-pin
+
+1
+
+GND
+
+2
+
+VCC
+
+3
+
+SCL
+
+4
+
+SDA
X5
-
-
-X5
-
-Molex KK 254
-
-female
-
-4-pin
-
-GND
-
-1
-
-VCC
-
-2
-
-SCL
-
-3
-
-SDA
-
-4
+
+
+
+X5
+
+Molex KK 254
+
+female
+
+4-pin
+
+GND
+
+1
+
+VCC
+
+2
+
+SCL
+
+3
+
+SDA
+
+4
W4
-
-
-W4
-
-4x
-
-0.25 mm²
-
-0.2 m
-
-X5:1:GND
- TQ
-X4:1:GND
-
-
-
-X5:2:VCC
- PK
-X4:2:VCC
-
-
-
-X5:3:SCL
- YE
-X4:3:SCL
-
-
-
-X5:4:SDA
- VT
-X4:4:SDA
-
-
-
-
+
+
+
+W4
+
+4x
+
+0.25 mm²
+
+0.2 m
+
+ X5:1:GND
+
+TQ
+
+X4:1:GND
+
+
+
+ X5:2:VCC
+
+PK
+
+X4:2:VCC
+
+
+
+ X5:3:SCL
+
+YE
+
+X4:3:SCL
+
+
+
+ X5:4:SDA
+
+VT
+
+X4:4:SDA
+
+
+
+
X5:e--W4:w
-
-
-
+
+
+
X5:e--W4:w
-
-
-
+
+
+
X5:e--W4:w
-
-
-
+
+
+
X5:e--W4:w
-
-
-
+
+
+
W5
-
-
-W5
-
-4x
-
-0.25 mm²
-
-0.2 m
-
-X5:1:GND
- TQ
-X6:1:GND
-
-
-
-X5:2:VCC
- PK
-X6:2:VCC
-
-
-
-X5:3:SCL
- YE
-X6:3:SCL
-
-
-
-X5:4:SDA
- VT
-X6:4:SDA
-
-
-
-
+
+
+
+W5
+
+4x
+
+0.25 mm²
+
+0.2 m
+
+ X5:1:GND
+
+TQ
+
+X6:1:GND
+
+
+
+ X5:2:VCC
+
+PK
+
+X6:2:VCC
+
+
+
+ X5:3:SCL
+
+YE
+
+X6:3:SCL
+
+
+
+ X5:4:SDA
+
+VT
+
+X6:4:SDA
+
+
+
+
X5:e--W5:w
-
-
-
+
+
+
X5:e--W5:w
-
-
-
+
+
+
X5:e--W5:w
-
-
-
+
+
+
X5:e--W5:w
-
-
-
+
+
+
X6
-
-
-X6
-
-Molex KK 254
-
-female
-
-4-pin
-
-1
-
-GND
-
-2
-
-VCC
-
-3
-
-SCL
-
-4
-
-SDA
+
+
+
+X6
+
+Molex KK 254
+
+female
+
+4-pin
+
+1
+
+GND
+
+2
+
+VCC
+
+3
+
+SCL
+
+4
+
+SDA
W1:e--X2:w
-
-
-
+
+
+
W1:e--X2:w
-
-
-
+
+
+
W1:e--X2:w
-
-
-
+
+
+
W1:e--X2:w
-
-
-
+
+
+
W2:e--X2:w
-
-
-
+
+
+
W2:e--X2:w
-
-
-
+
+
+
W2:e--X2:w
-
-
-
+
+
+
W2:e--X2:w
-
-
-
+
+
+
W3:e--X4:w
-
-
-
+
+
+
W3:e--X4:w
-
-
-
+
+
+
W3:e--X4:w
-
-
-
+
+
+
W3:e--X4:w
-
-
-
+
+
+
W4:e--X4:w
-
-
-
+
+
+
W4:e--X4:w
-
-
-
+
+
+
W4:e--X4:w
-
-
-
+
+
+
W4:e--X4:w
-
-
-
+
+
+
W5:e--X6:w
-
-
-
+
+
+
W5:e--X6:w
-
-
-
+
+
+
W5:e--X6:w
-
-
-
+
+
+
W5:e--X6:w
-
-
-
+
+
+
diff --git a/tutorial/tutorial08.bom.tsv b/tutorial/tutorial08.bom.tsv
index d425c9696..6ad266379 100644
--- a/tutorial/tutorial08.bom.tsv
+++ b/tutorial/tutorial08.bom.tsv
@@ -1,11 +1,11 @@
-Id Description Qty Unit Designators P/N Manufacturer MPN Supplier SPN
-1 Cable, 4 x 0.25 mm² 1 m W1 CAB1 CablesCo ABC123 Cables R Us 999-888-777
-2 Connector, Molex KK 254, female, 4 pins 2 X1, X3 Molex 22013047 Digimouse 1234
-3 Connector, Molex KK 254, female, 4 pins 1 X2 CON4 Molex 22013047 Digimouse 1234
-4 Crimp, Molex KK 254, 22-30 AWG 12 X1, X2, X3 Molex 08500030
-5 Label, pinout information 2 X2, X3 Label-ID-1 Brady B-499
-6 Sleve, Braided nylon, black, 3mm 1 m W2 SLV-1
-7 Test 3 X1, X2, X3 ABC Molex 45454 Mousikey 9999
-8 Wire, 0.25 mm², BK 2 m W2 WIRE2 WiresCo W1-BK WireShack 1002
-9 Wire, 0.25 mm², RD 1 m W2 WIRE3 WiresCo W1-RD WireShack 1009
-10 Wire, 0.25 mm², YE 1 m W2 WIRE1 WiresCo W1-YE WireShack 1001
+# Qty Unit Description Designators P/N Manufacturer MPN Supplier SPN
+1 2 Connector, Molex KK 254, female, 4 pins X1, X3 Molex 22013047 Digimouse 1234
+2 1 Connector, Molex KK 254, female, 4 pins X2 CON4 Molex 22013047 Digimouse 1234
+3 1 m Cable, 4 x 0.25 mm² W1 CAB1 CablesCo ABC123 Cables R Us 999-888-777
+4 2 m Wire, 0.25 mm², BK W2 WIRE2 WiresCo W1-BK WireShack 1002
+5 1 m Wire, 0.25 mm², RD W2 WIRE3 WiresCo W1-RD WireShack 1009
+6 1 m Wire, 0.25 mm², YE W2 WIRE1 WiresCo W1-YE WireShack 1001
+7 12 Crimp, Molex KK 254, 22-30 AWG X1, X2, X3 Molex 08500030
+8 1 m Sleeve, Braided nylon, black, 3mm W2 SLV-1
+9 3 Test X1, X2, X3 ABC Molex 45454 Mousikey 9999
+10 2 Label, pinout information Label-ID-1 Brady B-499
diff --git a/tutorial/tutorial08.gv b/tutorial/tutorial08.gv
index e0c9af4f8..dcbabb19e 100644
--- a/tutorial/tutorial08.gv
+++ b/tutorial/tutorial08.gv
@@ -1,275 +1,606 @@
graph {
-// Graph generated by WireViz 0.4
-// https://github.com/formatc1702/WireViz
+// Graph generated by WireViz 0.5-dev+refactor
+// https://github.com/wireviz/WireViz
graph [bgcolor="#FFFFFF" fontname=arial nodesep=0.33 rankdir=LR ranksep=2]
node [fillcolor="#FFFFFF" fontname=arial height=0 margin=0 shape=none style=filled width=0]
edge [fontname=arial style=bold]
X1 [label=<
-
-
-
-
-
-
- Molex: 22013047
- Digimouse: 1234
-
-
-
-
- Molex KK 254
- female
- 4-pin
-
-
-
-
-
- 1
-
-
- 2
-
-
- 3
-
-
- 4
-
-
-
-
-
- Additional components
-
-
-
-
- 4 x Crimp, Molex KK 254, 22-30 AWG Molex: 08500030
-
-
-
-
- 1 x Test P/N: ABC, Molex: 45454, Mousikey: 9999
-
-
+
+
+
+
+
+
+
+
+
+
+ Molex: 22013047
+ Digimouse: 1234
+
+
+
+
+
+
+
+
+ Molex KK 254
+ female
+ 4-pin
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+ 2
+
+
+
+ 3
+
+
+
+ 4
+
+
+
+
+
+
+
+
+
+ 4
+ x
+ Crimp, Molex KK 254, 22-30 AWG
+
+
+
+
+ Molex: 08500030
+
+
+
+
+ 1
+ x
+ Test
+
+
+
+
+ P/N: ABC, Molex: 45454, Mousikey: 9999
+
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style=filled]
+> shape=box style=filled]
X2 [label=<
-
-
-
-
-
-
- P/N: CON4
- Molex: 22013047
- Digimouse: 1234
-
-
-
-
- Molex KK 254
- female
- 4-pin
-
-
-
-
-
- 1
-
-
- 2
-
-
- 3
-
-
- 4
-
-
-
-
-
- Additional components
-
-
-
-
- 4 x Crimp, Molex KK 254, 22-30 AWG Molex: 08500030
-
-
-
-
- 1 x Test P/N: ABC, Molex: 45454, Mousikey: 9999
-
-
+
+
+
+
+
+
+
+
+
+
+ P/N: CON4
+ Molex: 22013047
+ Digimouse: 1234
+
+
+
+
+
+
+
+
+ Molex KK 254
+ female
+ 4-pin
+
+
+
+
+
+
+
+
+ 1
+
+
+
+ 2
+
+
+
+ 3
+
+
+
+ 4
+
+
+
+
+
+
+
+
+
+
+ 4
+ x
+ Crimp, Molex KK 254, 22-30 AWG
+
+
+
+
+ Molex: 08500030
+
+
+
+
+ 1
+ x
+ Test
+
+
+
+
+ P/N: ABC, Molex: 45454, Mousikey: 9999
+
+
+
+
+
-> fillcolor="#FFFFFF" shape=box style=filled]
+> shape=box style=filled]
X3 [label=<
-
-
-
-
-
-
- Molex: 22013047
- Digimouse: 1234
-
-
-
-
- Molex KK 254
- female
- 4-pin
-
-
-
-
-
- 1
-
-
- 2
-
-
- 3
-
-
- 4
-
-
-
-
-
- Additional components
-
-
-
-
- 4 x Crimp, Molex KK 254, 22-30 AWG