Compare commits
4 Commits
906a0a1075
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| cca03cd2f3 | |||
| 43c0ea46c6 | |||
| 7c3f8ddca6 | |||
| f3564cbbb0 |
@@ -23,3 +23,6 @@ Thumbs.db
|
|||||||
# Editor
|
# Editor
|
||||||
.vscode/
|
.vscode/
|
||||||
*.swp
|
*.swp
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|||||||
@@ -54,41 +54,87 @@ def get_layout(diagram_id: int, db: Session = Depends(get_db)):
|
|||||||
|
|
||||||
@router.post("/{diagram_id}/sync")
|
@router.post("/{diagram_id}/sync")
|
||||||
def sync_from_diagram(diagram_id: int, db: Session = Depends(get_db)):
|
def sync_from_diagram(diagram_id: int, db: Session = Depends(get_db)):
|
||||||
"""Import all devices from the main diagram as connector nodes.
|
"""Sync devices as connector nodes and build segments from wire connections.
|
||||||
Existing nodes (matched by device_id) are label-synced but not moved."""
|
|
||||||
|
Layout strategy:
|
||||||
|
- Connectors alternate above/below a central trunk line (y=100 / y=300)
|
||||||
|
- Segments are auto-created for each unique device-pair connected by wires,
|
||||||
|
with wire_ids populated so thickness reflects the bundle size.
|
||||||
|
Existing nodes are label-synced but not moved; existing segments are not duplicated.
|
||||||
|
"""
|
||||||
diagram = db.query(models.Diagram).filter_by(id=diagram_id).first()
|
diagram = db.query(models.Diagram).filter_by(id=diagram_id).first()
|
||||||
if not diagram:
|
if not diagram:
|
||||||
raise HTTPException(404, "Diagram not found")
|
raise HTTPException(404, "Diagram not found")
|
||||||
|
|
||||||
layout = _get_or_create(diagram_id, db)
|
layout = _get_or_create(diagram_id, db)
|
||||||
existing_dev_ids = {n.device_id for n in layout.nodes if n.device_id is not None}
|
existing_dev_ids = {n.device_id for n in layout.nodes if n.device_id is not None}
|
||||||
|
dev_to_node: dict = {n.device_id: n for n in layout.nodes if n.device_id is not None}
|
||||||
|
|
||||||
added = 0
|
added = 0
|
||||||
x = 80
|
new_x = 80 + len([n for n in layout.nodes if n.device_id is not None]) * 200
|
||||||
for dev in diagram.devices:
|
|
||||||
|
for i, dev in enumerate(diagram.devices):
|
||||||
|
label = dev.reference or dev.label or f"Dev {dev.id}"
|
||||||
if dev.id in existing_dev_ids:
|
if dev.id in existing_dev_ids:
|
||||||
# Keep label in sync if the node label is still the default
|
node = dev_to_node[dev.id]
|
||||||
node = next((n for n in layout.nodes if n.device_id == dev.id), None)
|
if not node.label or node.label == f"Dev {dev.id}":
|
||||||
if node:
|
node.label = label
|
||||||
new_label = dev.reference or dev.label or f"Dev {dev.id}"
|
else:
|
||||||
if not node.label or node.label == f"Dev {dev.id}":
|
# Alternate connectors above (y=80) and below (y=320) the trunk
|
||||||
node.label = new_label
|
row_y = 80 if added % 2 == 0 else 320
|
||||||
|
node = models.FormboardNode(
|
||||||
|
layout_id=layout.id,
|
||||||
|
node_type="connector",
|
||||||
|
device_id=dev.id,
|
||||||
|
x=new_x, y=row_y,
|
||||||
|
label=label,
|
||||||
|
)
|
||||||
|
db.add(node)
|
||||||
|
db.flush() # get node.id
|
||||||
|
dev_to_node[dev.id] = node
|
||||||
|
new_x += 200
|
||||||
|
added += 1
|
||||||
|
|
||||||
|
# Build segments from wire connections
|
||||||
|
# Group wires by (min_dev_id, max_dev_id) pair to avoid direction duplicates
|
||||||
|
from collections import defaultdict
|
||||||
|
pair_wires: dict = defaultdict(list)
|
||||||
|
for wire in diagram.wires:
|
||||||
|
a, b = wire.from_device_id, wire.to_device_id
|
||||||
|
if a and b and a != b:
|
||||||
|
pair_wires[(min(a, b), max(a, b))].append(wire.id)
|
||||||
|
|
||||||
|
# Find existing segments by node-pair to avoid duplicates
|
||||||
|
existing_seg_pairs = set()
|
||||||
|
for seg in layout.segments:
|
||||||
|
existing_seg_pairs.add((min(seg.from_node_id, seg.to_node_id),
|
||||||
|
max(seg.from_node_id, seg.to_node_id)))
|
||||||
|
|
||||||
|
segs_added = 0
|
||||||
|
for (dev_a, dev_b), wire_ids in pair_wires.items():
|
||||||
|
node_a = dev_to_node.get(dev_a)
|
||||||
|
node_b = dev_to_node.get(dev_b)
|
||||||
|
if not node_a or not node_b:
|
||||||
continue
|
continue
|
||||||
node = models.FormboardNode(
|
pair = (min(node_a.id, node_b.id), max(node_a.id, node_b.id))
|
||||||
|
if pair in existing_seg_pairs:
|
||||||
|
continue
|
||||||
|
seg = models.FormboardSegment(
|
||||||
layout_id=layout.id,
|
layout_id=layout.id,
|
||||||
node_type="connector",
|
from_node_id=node_a.id,
|
||||||
device_id=dev.id,
|
to_node_id=node_b.id,
|
||||||
x=x, y=180,
|
wire_ids=wire_ids,
|
||||||
label=dev.reference or dev.label or f"Dev {dev.id}",
|
fitting_type="open",
|
||||||
)
|
)
|
||||||
db.add(node)
|
db.add(seg)
|
||||||
x += 200
|
existing_seg_pairs.add(pair)
|
||||||
added += 1
|
segs_added += 1
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(layout)
|
db.refresh(layout)
|
||||||
return {
|
return {
|
||||||
"added": added,
|
"added": added,
|
||||||
|
"segs_added": segs_added,
|
||||||
"nodes": [_nd(n) for n in layout.nodes],
|
"nodes": [_nd(n) for n in layout.nodes],
|
||||||
"segments": [_sd(s) for s in layout.segments],
|
"segments": [_sd(s) for s in layout.segments],
|
||||||
}
|
}
|
||||||
|
|||||||
+1047
-18
File diff suppressed because it is too large
Load Diff
+2788
-543
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,56 @@
|
|||||||
"id": 4,
|
"id": 4,
|
||||||
"name": "New Diagram",
|
"name": "New Diagram",
|
||||||
"description": "",
|
"description": "",
|
||||||
"devices": [],
|
"sheets": [],
|
||||||
|
"wire_bundles": [],
|
||||||
|
"devices": [
|
||||||
|
{
|
||||||
|
"id": 62,
|
||||||
|
"sheet_id": null,
|
||||||
|
"device_type": "connector",
|
||||||
|
"label": "Connector",
|
||||||
|
"reference": "",
|
||||||
|
"x": 599.1646500326872,
|
||||||
|
"y": -1321.7662846344028,
|
||||||
|
"width": 100.0,
|
||||||
|
"height": 100.0,
|
||||||
|
"properties": {
|
||||||
|
"pinCount": 4,
|
||||||
|
"orientation": "right",
|
||||||
|
"partNumber": "",
|
||||||
|
"manufacturer": ""
|
||||||
|
},
|
||||||
|
"pins": [
|
||||||
|
{
|
||||||
|
"id": "pin_1",
|
||||||
|
"name": "1",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_2",
|
||||||
|
"name": "2",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 40
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_3",
|
||||||
|
"name": "3",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 60
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_4",
|
||||||
|
"name": "4",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"wires": []
|
"wires": []
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,530 @@
|
|||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"name": "VW bms",
|
||||||
|
"description": "",
|
||||||
|
"sheets": [],
|
||||||
|
"wire_bundles": [],
|
||||||
|
"devices": [
|
||||||
|
{
|
||||||
|
"id": 121,
|
||||||
|
"sheet_id": null,
|
||||||
|
"device_type": "connector",
|
||||||
|
"label": "1",
|
||||||
|
"reference": "",
|
||||||
|
"x": 460.0,
|
||||||
|
"y": -1130.0,
|
||||||
|
"width": 100.0,
|
||||||
|
"height": 236.0,
|
||||||
|
"properties": {
|
||||||
|
"pinCount": 4,
|
||||||
|
"orientation": "right",
|
||||||
|
"partNumber": "",
|
||||||
|
"manufacturer": ""
|
||||||
|
},
|
||||||
|
"pins": [
|
||||||
|
{
|
||||||
|
"id": "pin_1",
|
||||||
|
"name": "1",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 33.714285714285715
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_2",
|
||||||
|
"name": "2",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 67.42857142857143
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_3",
|
||||||
|
"name": "3",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 101.14285714285714
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_4",
|
||||||
|
"name": "4",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 134.85714285714286
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_5",
|
||||||
|
"name": "5",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 168.57142857142858
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_6",
|
||||||
|
"name": "6",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 202.28571428571428
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_7",
|
||||||
|
"name": "7",
|
||||||
|
"side": "left",
|
||||||
|
"x_offset": 0,
|
||||||
|
"y_offset": 33.714285714285715
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_8",
|
||||||
|
"name": "8",
|
||||||
|
"side": "left",
|
||||||
|
"x_offset": 0,
|
||||||
|
"y_offset": 67.42857142857143
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_9",
|
||||||
|
"name": "9",
|
||||||
|
"side": "left",
|
||||||
|
"x_offset": 0,
|
||||||
|
"y_offset": 101.14285714285714
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_10",
|
||||||
|
"name": "10",
|
||||||
|
"side": "left",
|
||||||
|
"x_offset": 0,
|
||||||
|
"y_offset": 134.85714285714286
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_11",
|
||||||
|
"name": "11",
|
||||||
|
"side": "left",
|
||||||
|
"x_offset": 0,
|
||||||
|
"y_offset": 168.57142857142858
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_12",
|
||||||
|
"name": "12",
|
||||||
|
"side": "left",
|
||||||
|
"x_offset": 0,
|
||||||
|
"y_offset": 202.28571428571428
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 124,
|
||||||
|
"sheet_id": null,
|
||||||
|
"device_type": "ground",
|
||||||
|
"label": "?",
|
||||||
|
"reference": "",
|
||||||
|
"x": 200.0,
|
||||||
|
"y": -750.0,
|
||||||
|
"width": 60.0,
|
||||||
|
"height": 50.0,
|
||||||
|
"properties": {
|
||||||
|
"groundType": "chassis",
|
||||||
|
"reference": ""
|
||||||
|
},
|
||||||
|
"pins": [
|
||||||
|
{
|
||||||
|
"id": "GND",
|
||||||
|
"name": "GND",
|
||||||
|
"side": "top",
|
||||||
|
"x_offset": 30,
|
||||||
|
"y_offset": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 125,
|
||||||
|
"sheet_id": null,
|
||||||
|
"device_type": "connector",
|
||||||
|
"label": "pigtail",
|
||||||
|
"reference": "",
|
||||||
|
"x": -56.85155707019593,
|
||||||
|
"y": -1024.63127707672,
|
||||||
|
"width": 100.0,
|
||||||
|
"height": 110.0,
|
||||||
|
"properties": {
|
||||||
|
"pinCount": 4,
|
||||||
|
"orientation": "right",
|
||||||
|
"partNumber": "",
|
||||||
|
"manufacturer": ""
|
||||||
|
},
|
||||||
|
"pins": [
|
||||||
|
{
|
||||||
|
"id": "pin_1",
|
||||||
|
"name": "1",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 18.333333333333332
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_2",
|
||||||
|
"name": "2",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 36.666666666666664
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_3",
|
||||||
|
"name": "3",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 55
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_4",
|
||||||
|
"name": "4",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 73.33333333333333
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_5",
|
||||||
|
"name": "5",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 91.66666666666667
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 126,
|
||||||
|
"sheet_id": null,
|
||||||
|
"device_type": "connector",
|
||||||
|
"label": "2",
|
||||||
|
"reference": "",
|
||||||
|
"x": 840.0,
|
||||||
|
"y": -1130.0,
|
||||||
|
"width": 100.0,
|
||||||
|
"height": 236.0,
|
||||||
|
"properties": {
|
||||||
|
"pinCount": 4,
|
||||||
|
"orientation": "right",
|
||||||
|
"partNumber": "",
|
||||||
|
"manufacturer": ""
|
||||||
|
},
|
||||||
|
"pins": [
|
||||||
|
{
|
||||||
|
"id": "pin_1",
|
||||||
|
"name": "1",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 33.714285714285715
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_2",
|
||||||
|
"name": "2",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 67.42857142857143
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_3",
|
||||||
|
"name": "3",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 101.14285714285714
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_4",
|
||||||
|
"name": "4",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 134.85714285714286
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_5",
|
||||||
|
"name": "5",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 168.57142857142858
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_6",
|
||||||
|
"name": "6",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 202.28571428571428
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_7",
|
||||||
|
"name": "7",
|
||||||
|
"side": "left",
|
||||||
|
"x_offset": 0,
|
||||||
|
"y_offset": 33.714285714285715
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_8",
|
||||||
|
"name": "8",
|
||||||
|
"side": "left",
|
||||||
|
"x_offset": 0,
|
||||||
|
"y_offset": 67.42857142857143
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_9",
|
||||||
|
"name": "9",
|
||||||
|
"side": "left",
|
||||||
|
"x_offset": 0,
|
||||||
|
"y_offset": 101.14285714285714
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_10",
|
||||||
|
"name": "10",
|
||||||
|
"side": "left",
|
||||||
|
"x_offset": 0,
|
||||||
|
"y_offset": 134.85714285714286
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_11",
|
||||||
|
"name": "11",
|
||||||
|
"side": "left",
|
||||||
|
"x_offset": 0,
|
||||||
|
"y_offset": 168.57142857142858
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_12",
|
||||||
|
"name": "12",
|
||||||
|
"side": "left",
|
||||||
|
"x_offset": 0,
|
||||||
|
"y_offset": 202.28571428571428
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 127,
|
||||||
|
"sheet_id": null,
|
||||||
|
"device_type": "connector",
|
||||||
|
"label": "3",
|
||||||
|
"reference": "",
|
||||||
|
"x": 1220.0,
|
||||||
|
"y": -1130.0,
|
||||||
|
"width": 100.0,
|
||||||
|
"height": 236.0,
|
||||||
|
"properties": {
|
||||||
|
"pinCount": 4,
|
||||||
|
"orientation": "right",
|
||||||
|
"partNumber": "",
|
||||||
|
"manufacturer": ""
|
||||||
|
},
|
||||||
|
"pins": [
|
||||||
|
{
|
||||||
|
"id": "pin_1",
|
||||||
|
"name": "1",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 33.714285714285715
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_2",
|
||||||
|
"name": "2",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 67.42857142857143
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_3",
|
||||||
|
"name": "3",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 101.14285714285714
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_4",
|
||||||
|
"name": "4",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 134.85714285714286
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_5",
|
||||||
|
"name": "5",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 168.57142857142858
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_6",
|
||||||
|
"name": "6",
|
||||||
|
"side": "right",
|
||||||
|
"x_offset": 100,
|
||||||
|
"y_offset": 202.28571428571428
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_7",
|
||||||
|
"name": "7",
|
||||||
|
"side": "left",
|
||||||
|
"x_offset": 0,
|
||||||
|
"y_offset": 33.714285714285715
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_8",
|
||||||
|
"name": "8",
|
||||||
|
"side": "left",
|
||||||
|
"x_offset": 0,
|
||||||
|
"y_offset": 67.42857142857143
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_9",
|
||||||
|
"name": "9",
|
||||||
|
"side": "left",
|
||||||
|
"x_offset": 0,
|
||||||
|
"y_offset": 101.14285714285714
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_10",
|
||||||
|
"name": "10",
|
||||||
|
"side": "left",
|
||||||
|
"x_offset": 0,
|
||||||
|
"y_offset": 134.85714285714286
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_11",
|
||||||
|
"name": "11",
|
||||||
|
"side": "left",
|
||||||
|
"x_offset": 0,
|
||||||
|
"y_offset": 168.57142857142858
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pin_12",
|
||||||
|
"name": "12",
|
||||||
|
"side": "left",
|
||||||
|
"x_offset": 0,
|
||||||
|
"y_offset": 202.28571428571428
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"wires": [
|
||||||
|
{
|
||||||
|
"id": 160,
|
||||||
|
"sheet_id": null,
|
||||||
|
"label": "",
|
||||||
|
"from_device_id": 121,
|
||||||
|
"from_pin": "pin_12",
|
||||||
|
"to_device_id": 125,
|
||||||
|
"to_pin": "pin_5",
|
||||||
|
"color_primary": "#8B4513",
|
||||||
|
"color_stripe": null,
|
||||||
|
"gauge": "18 AWG",
|
||||||
|
"length": null,
|
||||||
|
"length_unit": "in",
|
||||||
|
"notes": "",
|
||||||
|
"waypoints": [],
|
||||||
|
"twisted_pair": false,
|
||||||
|
"twist_pitch": 16.0,
|
||||||
|
"shielded": false,
|
||||||
|
"show_size_label": false,
|
||||||
|
"bundle_id": null,
|
||||||
|
"bundle_label": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 161,
|
||||||
|
"sheet_id": null,
|
||||||
|
"label": "",
|
||||||
|
"from_device_id": 121,
|
||||||
|
"from_pin": "pin_10",
|
||||||
|
"to_device_id": 125,
|
||||||
|
"to_pin": "pin_4",
|
||||||
|
"color_primary": "#007700",
|
||||||
|
"color_stripe": "#ff0000",
|
||||||
|
"gauge": "18 AWG",
|
||||||
|
"length": null,
|
||||||
|
"length_unit": "in",
|
||||||
|
"notes": "",
|
||||||
|
"waypoints": [],
|
||||||
|
"twisted_pair": false,
|
||||||
|
"twist_pitch": 16.0,
|
||||||
|
"shielded": false,
|
||||||
|
"show_size_label": false,
|
||||||
|
"bundle_id": null,
|
||||||
|
"bundle_label": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 162,
|
||||||
|
"sheet_id": null,
|
||||||
|
"label": "",
|
||||||
|
"from_device_id": 121,
|
||||||
|
"from_pin": "pin_9",
|
||||||
|
"to_device_id": 125,
|
||||||
|
"to_pin": "pin_3",
|
||||||
|
"color_primary": "#FF8C00",
|
||||||
|
"color_stripe": "#0000ff",
|
||||||
|
"gauge": "18 AWG",
|
||||||
|
"length": null,
|
||||||
|
"length_unit": "in",
|
||||||
|
"notes": "",
|
||||||
|
"waypoints": [],
|
||||||
|
"twisted_pair": false,
|
||||||
|
"twist_pitch": 16.0,
|
||||||
|
"shielded": false,
|
||||||
|
"show_size_label": false,
|
||||||
|
"bundle_id": null,
|
||||||
|
"bundle_label": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 163,
|
||||||
|
"sheet_id": null,
|
||||||
|
"label": "",
|
||||||
|
"from_device_id": 125,
|
||||||
|
"from_pin": "pin_2",
|
||||||
|
"to_device_id": 121,
|
||||||
|
"to_pin": "pin_8",
|
||||||
|
"color_primary": "#FF8C00",
|
||||||
|
"color_stripe": "#ff0000",
|
||||||
|
"gauge": "18 AWG",
|
||||||
|
"length": null,
|
||||||
|
"length_unit": "in",
|
||||||
|
"notes": "",
|
||||||
|
"waypoints": [],
|
||||||
|
"twisted_pair": false,
|
||||||
|
"twist_pitch": 16.0,
|
||||||
|
"shielded": false,
|
||||||
|
"show_size_label": false,
|
||||||
|
"bundle_id": null,
|
||||||
|
"bundle_label": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 164,
|
||||||
|
"sheet_id": null,
|
||||||
|
"label": "",
|
||||||
|
"from_device_id": 121,
|
||||||
|
"from_pin": "pin_7",
|
||||||
|
"to_device_id": 125,
|
||||||
|
"to_pin": "pin_1",
|
||||||
|
"color_primary": "#007700",
|
||||||
|
"color_stripe": "#cccccc",
|
||||||
|
"gauge": "18 AWG",
|
||||||
|
"length": null,
|
||||||
|
"length_unit": "in",
|
||||||
|
"notes": "",
|
||||||
|
"waypoints": [],
|
||||||
|
"twisted_pair": false,
|
||||||
|
"twist_pitch": 16.0,
|
||||||
|
"shielded": false,
|
||||||
|
"show_size_label": false,
|
||||||
|
"bundle_id": null,
|
||||||
|
"bundle_label": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 165,
|
||||||
|
"sheet_id": null,
|
||||||
|
"label": "",
|
||||||
|
"from_device_id": 121,
|
||||||
|
"from_pin": "pin_11",
|
||||||
|
"to_device_id": 124,
|
||||||
|
"to_pin": "GND",
|
||||||
|
"color_primary": "#007700",
|
||||||
|
"color_stripe": "#000000",
|
||||||
|
"gauge": "18 AWG",
|
||||||
|
"length": null,
|
||||||
|
"length_unit": "in",
|
||||||
|
"notes": "",
|
||||||
|
"waypoints": [],
|
||||||
|
"twisted_pair": false,
|
||||||
|
"twist_pitch": 16.0,
|
||||||
|
"shielded": false,
|
||||||
|
"show_size_label": false,
|
||||||
|
"bundle_id": null,
|
||||||
|
"bundle_label": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+450
-135
@@ -1,24 +1,232 @@
|
|||||||
|
:root {
|
||||||
|
--bg-inset: #0e0e1e;
|
||||||
|
--bg-body: #12121c;
|
||||||
|
--bg-canvas: #131320;
|
||||||
|
--bg-panel: #14142a;
|
||||||
|
--bg-modal: #16162a;
|
||||||
|
--bg-surface: #1a1a2e;
|
||||||
|
--bg-surface-2: #1a1a30;
|
||||||
|
--bg-surface-3: #1e1e34;
|
||||||
|
--bg-selected: #1e1e3a;
|
||||||
|
--bg-raised: #222238;
|
||||||
|
--border-faint: #26263f;
|
||||||
|
--border: #2a2a44;
|
||||||
|
--grid-dot: #2e2e50;
|
||||||
|
--border-strong: #3a3a5a;
|
||||||
|
--text-ghost: #33334f;
|
||||||
|
--text-faint: #444466;
|
||||||
|
--text-dim: #555577;
|
||||||
|
--text-muted: #8899bb;
|
||||||
|
--text: #c0c4e8;
|
||||||
|
--text-strong: #c8ccee;
|
||||||
|
--text-bright: #dde0f5;
|
||||||
|
--accent: #5566aa;
|
||||||
|
/* Auto-derived light-mode pairs. --cXXXXXX is a surface colour, --tXXXXXX
|
||||||
|
the same value used as text; the two flip differently because a pale
|
||||||
|
blue that works as a border is unreadable as text on white. Mid-tone
|
||||||
|
semantic colours (danger, status, wire colours) stay literal. */
|
||||||
|
--c090914: #090914;
|
||||||
|
--c0a1a14: #0a1a14;
|
||||||
|
--c0c0c1c: #0c0c1c;
|
||||||
|
--c0d0d20: #0d0d20;
|
||||||
|
--c0d1a28: #0d1a28;
|
||||||
|
--c0d2235: #0d2235;
|
||||||
|
--c0e0e1a: #0e0e1a;
|
||||||
|
--c0f2a1a: #0f2a1a;
|
||||||
|
--c10101e: #10101e;
|
||||||
|
--c111122: #111122;
|
||||||
|
--c111e30: #111e30;
|
||||||
|
--c141428: #141428;
|
||||||
|
--c151528: #151528;
|
||||||
|
--c18182e: #18182e;
|
||||||
|
--c1a1a38: #1a1a38;
|
||||||
|
--c1a1a3a: #1a1a3a;
|
||||||
|
--c1a1a40: #1a1a40;
|
||||||
|
--c1a2840: #1a2840;
|
||||||
|
--c1a2a1a: #1a2a1a;
|
||||||
|
--c1a2a4a: #1a2a4a;
|
||||||
|
--c1a3a2a: #1a3a2a;
|
||||||
|
--c1e1e36: #1e1e36;
|
||||||
|
--c1e1e38: #1e1e38;
|
||||||
|
--c1e1e40: #1e1e40;
|
||||||
|
--c1e2a4a: #1e2a4a;
|
||||||
|
--c1e3355: #1e3355;
|
||||||
|
--c1e3a6a: #1e3a6a;
|
||||||
|
--c1e4a2a: #1e4a2a;
|
||||||
|
--c22223a: #22223a;
|
||||||
|
--c22224a: #22224a;
|
||||||
|
--c223355: #223355;
|
||||||
|
--c23233c: #23233c;
|
||||||
|
--c264a8a: #264a8a;
|
||||||
|
--c2a1a4a: #2a1a4a;
|
||||||
|
--c2a2a50: #2a2a50;
|
||||||
|
--c2a2a5a: #2a2a5a;
|
||||||
|
--c2e2e54: #2e2e54;
|
||||||
|
--c333355: #333355;
|
||||||
|
--c334466: #334466;
|
||||||
|
--c336633: #336633;
|
||||||
|
--c3a1414: #3a1414;
|
||||||
|
--c3a3a66: #3a3a66;
|
||||||
|
--c3d3d63: #3d3d63;
|
||||||
|
--c554422: #554422;
|
||||||
|
--c662222: #662222;
|
||||||
|
--t6688cc: #6688cc;
|
||||||
|
--t7788aa: #7788aa;
|
||||||
|
--t77aaee: #77aaee;
|
||||||
|
--t8888aa: #8888aa;
|
||||||
|
--t88aadd: #88aadd;
|
||||||
|
--t88aaff: #88aaff;
|
||||||
|
--t88bbdd: #88bbdd;
|
||||||
|
--t88bbff: #88bbff;
|
||||||
|
--t88cc88: #88cc88;
|
||||||
|
--t88ee88: #88ee88;
|
||||||
|
--t9090cc: #9090cc;
|
||||||
|
--t9966ee: #9966ee;
|
||||||
|
--t9999cc: #9999cc;
|
||||||
|
--t99aacc: #99aacc;
|
||||||
|
--t99aadd: #99aadd;
|
||||||
|
--ta0a4c8: #a0a4c8;
|
||||||
|
--taaa: #aaa;
|
||||||
|
--taaccee: #aaccee;
|
||||||
|
--taaccff: #aaccff;
|
||||||
|
--tbbc: #bbc;
|
||||||
|
--tbbc0ee: #bbc0ee;
|
||||||
|
--tc0c8f0: #c0c8f0;
|
||||||
|
--td65a4a: #d65a4a;
|
||||||
|
--tdd6666: #dd6666;
|
||||||
|
--tddaa44: #ddaa44;
|
||||||
|
--tff6666: #ff6666;
|
||||||
|
--tff8888: #ff8888;
|
||||||
|
--tfff: #fff;
|
||||||
|
--t00cc88: #00cc88;
|
||||||
|
--t4caa77: #4caa77;
|
||||||
|
--t445566: #7f90ab;
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="light"] {
|
||||||
|
--bg-inset: #ffffff;
|
||||||
|
--bg-body: #eceef3;
|
||||||
|
--bg-canvas: #f7f8fa;
|
||||||
|
--bg-panel: #f1f3f8;
|
||||||
|
--bg-modal: #ffffff;
|
||||||
|
--bg-surface: #f3f4f9;
|
||||||
|
--bg-surface-2: #e8ebf3;
|
||||||
|
--bg-surface-3: #e4e7f0;
|
||||||
|
--bg-selected: #dde3f4;
|
||||||
|
--bg-raised: #e6e9f1;
|
||||||
|
--border-faint: #dbdfea;
|
||||||
|
--border: #ccd1e0;
|
||||||
|
--grid-dot: #c3c9d8;
|
||||||
|
--border-strong: #b2b8cb;
|
||||||
|
--text-ghost: #b8bdcc;
|
||||||
|
--text-faint: #969cb0;
|
||||||
|
--text-dim: #7b8296;
|
||||||
|
--text-muted: #59627a;
|
||||||
|
--text: #242939;
|
||||||
|
--text-strong: #151a29;
|
||||||
|
--text-bright: #0f1320;
|
||||||
|
--accent: #5a6cba;
|
||||||
|
--c090914: #f2f2f6;
|
||||||
|
--c0a1a14: #eef4f2;
|
||||||
|
--c0c0c1c: #ededf2;
|
||||||
|
--c0d0d20: #eaeaf1;
|
||||||
|
--c0d1a28: #e6eaef;
|
||||||
|
--c0d2235: #dee6ec;
|
||||||
|
--c0e0e1a: #ededf2;
|
||||||
|
--c0f2a1a: #e4eee8;
|
||||||
|
--c10101e: #ebebf0;
|
||||||
|
--c111122: #e8e8ee;
|
||||||
|
--c111e30: #e0e5eb;
|
||||||
|
--c141428: #e4e4eb;
|
||||||
|
--c151528: #e4e4eb;
|
||||||
|
--c18182e: #e0e0e8;
|
||||||
|
--c1a1a38: #dadae4;
|
||||||
|
--c1a1a3a: #d9d9e4;
|
||||||
|
--c1a1a40: #d5d5e3;
|
||||||
|
--c1a2840: #d5dae3;
|
||||||
|
--c1a2a1a: #e2e7e2;
|
||||||
|
--c1a2a4a: #d0d5e0;
|
||||||
|
--c1a3a2a: #d9e4de;
|
||||||
|
--c1e1e36: #dadae2;
|
||||||
|
--c1e1e38: #d9d9e2;
|
||||||
|
--c1e1e40: #d4d4e0;
|
||||||
|
--c1e2a4a: #cfd3de;
|
||||||
|
--c1e3355: #c8d0dc;
|
||||||
|
--c1e3a6a: #bdc6d7;
|
||||||
|
--c1e4a2a: #cfded3;
|
||||||
|
--c22223a: #d7d7df;
|
||||||
|
--c22224a: #cecedc;
|
||||||
|
--c223355: #c8ceda;
|
||||||
|
--c23233c: #d6d6de;
|
||||||
|
--c264a8a: #a9b5cc;
|
||||||
|
--c2a1a4a: #d5d0e0;
|
||||||
|
--c2a2a50: #c9c9d6;
|
||||||
|
--c2a2a5a: #c3c3d4;
|
||||||
|
--c2e2e54: #c6c6d3;
|
||||||
|
--c333355: #c4c4d0;
|
||||||
|
--c334466: #bac0cc;
|
||||||
|
--c336633: #baccba;
|
||||||
|
--c3a1414: #e7dada;
|
||||||
|
--c3a3a66: #b9b9c8;
|
||||||
|
--c3d3d63: #babac7;
|
||||||
|
--c554422: #dad4c8;
|
||||||
|
--c662222: #d6bebe;
|
||||||
|
--t6688cc: #375fb0;
|
||||||
|
--t7788aa: #5c7097;
|
||||||
|
--t77aaee: #1154ad;
|
||||||
|
--t8888aa: #5f5f87;
|
||||||
|
--t88aadd: #285497;
|
||||||
|
--t88aaff: #002fa4;
|
||||||
|
--t88bbdd: #286a97;
|
||||||
|
--t88bbff: #0046a4;
|
||||||
|
--t88cc88: #3b913b;
|
||||||
|
--t88ee88: #1c6b28;
|
||||||
|
--t9090cc: #3c3c89;
|
||||||
|
--t9966ee: #5110bc;
|
||||||
|
--t9999cc: #3e3e81;
|
||||||
|
--t99aacc: #3e5481;
|
||||||
|
--t99aadd: #2a4187;
|
||||||
|
--ta0a4c8: #444979;
|
||||||
|
--taaa: #666666;
|
||||||
|
--taaccee: #174b80;
|
||||||
|
--taaccff: #00378a;
|
||||||
|
--tbbc: #46465e;
|
||||||
|
--tbbc0ee: #192270;
|
||||||
|
--tc0c8f0: #17266d;
|
||||||
|
--td65a4a: #cb3c29;
|
||||||
|
--tdd6666: #b52424;
|
||||||
|
--tddaa44: #d29721;
|
||||||
|
--tff6666: #bf0000;
|
||||||
|
--tff8888: #a40000;
|
||||||
|
--tfff: #292929;
|
||||||
|
--t00cc88: #00663d;
|
||||||
|
--t4caa77: #1d6941;
|
||||||
|
--t445566: #4a5568;
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Reset & base ─────────────────────────────────────────────────────────── */
|
/* ── Reset & base ─────────────────────────────────────────────────────────── */
|
||||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
html, body, #app { height: 100%; overflow: hidden; font-family: "Segoe UI", system-ui, sans-serif; font-size: 13px; }
|
html, body, #app { height: 100%; overflow: hidden; font-family: "Segoe UI", system-ui, sans-serif; font-size: 13px; }
|
||||||
body { background: #12121c; color: #c8ccee; }
|
body { background: var(--bg-body); color: var(--text-strong); }
|
||||||
|
|
||||||
/* ── Layout ───────────────────────────────────────────────────────────────── */
|
/* ── Layout ───────────────────────────────────────────────────────────────── */
|
||||||
#app { display: flex; flex-direction: column; }
|
#app { display: flex; flex-direction: column; }
|
||||||
|
|
||||||
#toolbar {
|
#toolbar {
|
||||||
display: flex; align-items: center; gap: 8px; padding: 0 12px;
|
display: flex; align-items: center; gap: 8px; padding: 0 12px;
|
||||||
height: 48px; background: #0e0e1a; border-bottom: 1px solid #2a2a44;
|
height: 48px; background: var(--c0e0e1a); border-bottom: 1px solid var(--border);
|
||||||
flex-shrink: 0; z-index: 10;
|
flex-shrink: 0; z-index: 10;
|
||||||
}
|
}
|
||||||
.toolbar-left { display: flex; align-items: center; gap: 10px; min-width: 220px; }
|
.toolbar-left { display: flex; align-items: center; gap: 10px; min-width: 220px; }
|
||||||
.toolbar-center { display: flex; align-items: center; gap: 5px; flex: 1; justify-content: center; flex-wrap: wrap; }
|
.toolbar-center { display: flex; align-items: center; gap: 5px; flex: 1; justify-content: center; flex-wrap: wrap; }
|
||||||
.toolbar-right { display: flex; align-items: center; gap: 8px; min-width: 180px; justify-content: flex-end; }
|
.toolbar-right { display: flex; align-items: center; gap: 8px; min-width: 180px; justify-content: flex-end; }
|
||||||
.toolbar-label { font-size: 11px; color: #555577; white-space: nowrap; }
|
.toolbar-label { font-size: 11px; color: var(--text-dim); white-space: nowrap; }
|
||||||
|
|
||||||
#mode-indicator {
|
#mode-indicator {
|
||||||
text-align: center; font-size: 11px; color: #00cc88; background: #0a1a14;
|
text-align: center; font-size: 11px; color: var(--t00cc88); background: var(--c0a1a14);
|
||||||
border-bottom: 1px solid #1a3a2a; height: 22px; flex-shrink: 0;
|
border-bottom: 1px solid var(--c1a3a2a); height: 22px; flex-shrink: 0;
|
||||||
display: flex; align-items: center; justify-content: center;
|
display: flex; align-items: center; justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,33 +234,33 @@ body { background: #12121c; color: #c8ccee; }
|
|||||||
|
|
||||||
/* ── Sidebars ─────────────────────────────────────────────────────────────── */
|
/* ── Sidebars ─────────────────────────────────────────────────────────────── */
|
||||||
#left-sidebar {
|
#left-sidebar {
|
||||||
width: 240px; flex-shrink: 0; background: #14142a;
|
width: 240px; flex-shrink: 0; background: var(--bg-panel);
|
||||||
border-right: 1px solid #2a2a44; display: flex; flex-direction: column; overflow: hidden;
|
border-right: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden;
|
||||||
}
|
}
|
||||||
#right-sidebar {
|
#right-sidebar {
|
||||||
width: 248px; flex-shrink: 0; background: #14142a;
|
width: 248px; flex-shrink: 0; background: var(--bg-panel);
|
||||||
border-left: 1px solid #2a2a44; overflow-y: auto;
|
border-left: 1px solid var(--border); overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-section { display: flex; flex-direction: column; flex-shrink: 0; }
|
.sidebar-section { display: flex; flex-direction: column; flex-shrink: 0; }
|
||||||
.section-header {
|
.section-header {
|
||||||
padding: 7px 10px; font-size: 11px; font-weight: 600; letter-spacing: .05em;
|
padding: 7px 10px; font-size: 11px; font-weight: 600; letter-spacing: .05em;
|
||||||
text-transform: uppercase; color: #6666aa; background: #10101e;
|
text-transform: uppercase; color: #6666aa; background: var(--c10101e);
|
||||||
border-bottom: 1px solid #222238; display: flex; align-items: center; justify-content: space-between;
|
border-bottom: 1px solid var(--bg-raised); display: flex; align-items: center; justify-content: space-between;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Sidebar tabs ─────────────────────────────────────────────────────────── */
|
/* ── Sidebar tabs ─────────────────────────────────────────────────────────── */
|
||||||
.tab-bar {
|
.tab-bar {
|
||||||
display: flex; border-bottom: 1px solid #222238; background: #10101e; flex-shrink: 0;
|
display: flex; border-bottom: 1px solid var(--bg-raised); background: var(--c10101e); flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.sidebar-tab {
|
.sidebar-tab {
|
||||||
flex: 1; padding: 6px 4px; font-size: 11px; font-weight: 500; text-align: center;
|
flex: 1; padding: 6px 4px; font-size: 11px; font-weight: 500; text-align: center;
|
||||||
background: transparent; border: none; border-radius: 0; color: #555577;
|
background: transparent; border: none; border-radius: 0; color: var(--text-dim);
|
||||||
border-bottom: 2px solid transparent; cursor: pointer; transition: color .15s;
|
border-bottom: 2px solid transparent; cursor: pointer; transition: color .15s;
|
||||||
}
|
}
|
||||||
.sidebar-tab:hover { color: #9999cc; background: #18182e; }
|
.sidebar-tab:hover { color: var(--t9999cc); background: var(--c18182e); }
|
||||||
.sidebar-tab.active { color: #88aaff; border-bottom-color: #5577dd; }
|
.sidebar-tab.active { color: var(--t88aaff); border-bottom-color: #5577dd; }
|
||||||
|
|
||||||
.tab-panel { display: flex; flex-direction: column; }
|
.tab-panel { display: flex; flex-direction: column; }
|
||||||
|
|
||||||
@@ -63,98 +271,98 @@ body { background: #12121c; color: #c8ccee; }
|
|||||||
display: flex; justify-content: space-between; align-items: baseline;
|
display: flex; justify-content: space-between; align-items: baseline;
|
||||||
border: 1px solid transparent; margin-bottom: 2px;
|
border: 1px solid transparent; margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
.diagram-item:hover { background: #1e1e3a; border-color: #3a3a5a; }
|
.diagram-item:hover { background: var(--bg-selected); border-color: var(--border-strong); }
|
||||||
.diagram-item.active { background: #1a2a4a; border-color: #4466aa; }
|
.diagram-item.active { background: var(--c1a2a4a); border-color: #4466aa; }
|
||||||
.di-name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; font-size: 12px; cursor: pointer; }
|
.di-name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; font-size: 12px; cursor: pointer; }
|
||||||
.di-date { font-size: 10px; color: #555577; margin-left: 6px; white-space: nowrap; cursor: pointer; }
|
.di-date { font-size: 10px; color: var(--text-dim); margin-left: 6px; white-space: nowrap; cursor: pointer; }
|
||||||
.di-del-btn {
|
.di-del-btn {
|
||||||
flex-shrink: 0; opacity: 0; padding: 1px 5px; font-size: 13px; line-height: 1;
|
flex-shrink: 0; opacity: 0; padding: 1px 5px; font-size: 13px; line-height: 1;
|
||||||
background: transparent; border-color: transparent; color: #ff6666;
|
background: transparent; border-color: transparent; color: var(--tff6666);
|
||||||
transition: opacity .15s; margin-left: 4px;
|
transition: opacity .15s; margin-left: 4px;
|
||||||
}
|
}
|
||||||
.diagram-item:hover .di-del-btn { opacity: 1; }
|
.diagram-item:hover .di-del-btn { opacity: 1; }
|
||||||
.di-del-btn:hover { background: #3a1414; border-color: #aa3333; }
|
.di-del-btn:hover { background: var(--c3a1414); border-color: #aa3333; }
|
||||||
.di-dup-btn {
|
.di-dup-btn {
|
||||||
flex-shrink: 0; opacity: 0; padding: 1px 5px; font-size: 13px; line-height: 1;
|
flex-shrink: 0; opacity: 0; padding: 1px 5px; font-size: 13px; line-height: 1;
|
||||||
background: transparent; border-color: transparent; color: #88aaff;
|
background: transparent; border-color: transparent; color: var(--t88aaff);
|
||||||
transition: opacity .15s; margin-left: 4px;
|
transition: opacity .15s; margin-left: 4px;
|
||||||
}
|
}
|
||||||
.diagram-item:hover .di-dup-btn { opacity: 1; }
|
.diagram-item:hover .di-dup-btn { opacity: 1; }
|
||||||
.di-dup-btn:hover { background: #1a1a40; border-color: #4466aa; }
|
.di-dup-btn:hover { background: var(--c1a1a40); border-color: #4466aa; }
|
||||||
|
|
||||||
/* ── View tab bar ────────────────────────────────────────────────────────── */
|
/* ── View tab bar ────────────────────────────────────────────────────────── */
|
||||||
#view-tab-bar {
|
#view-tab-bar {
|
||||||
display: flex; align-items: center; gap: 2px;
|
display: flex; align-items: center; gap: 2px;
|
||||||
padding: 3px 8px; background: #0c0c1c; border-bottom: 1px solid #2a2a44;
|
padding: 3px 8px; background: var(--c0c0c1c); border-bottom: 1px solid var(--border);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.view-tab {
|
.view-tab {
|
||||||
padding: 3px 12px; font-size: 11px; border-radius: 4px 4px 0 0;
|
padding: 3px 12px; font-size: 11px; border-radius: 4px 4px 0 0;
|
||||||
background: #1a1a30; border: 1px solid #2a2a44; border-bottom: none;
|
background: var(--bg-surface-2); border: 1px solid var(--border); border-bottom: none;
|
||||||
color: #8888aa; cursor: pointer;
|
color: var(--t8888aa); cursor: pointer;
|
||||||
}
|
}
|
||||||
.view-tab { display: inline-flex; align-items: center; gap: 4px; }
|
.view-tab { display: inline-flex; align-items: center; gap: 4px; }
|
||||||
.view-tab.active { background: #1e2a4a; border-color: #4466aa; color: #c0c8f0; }
|
.view-tab.active { background: var(--c1e2a4a); border-color: #4466aa; color: var(--tc0c8f0); }
|
||||||
.view-tab:hover:not(.active) { background: #1e1e38; color: #aaa; }
|
.view-tab:hover:not(.active) { background: var(--c1e1e38); color: var(--taaa); }
|
||||||
.view-tab-close {
|
.view-tab-close {
|
||||||
font-size: 13px; line-height: 1; opacity: 0.4; padding: 0 2px;
|
font-size: 13px; line-height: 1; opacity: 0.4; padding: 0 2px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
.view-tab-close:hover { opacity: 1; background: rgba(255,80,80,0.25); color: #ff8888; }
|
.view-tab-close:hover { opacity: 1; background: rgba(255,80,80,0.25); color: var(--tff8888); }
|
||||||
.view-tab-sep { width: 1px; background: #2a2a44; height: 18px; margin: 0 4px; }
|
.view-tab-sep { width: 1px; background: var(--border); height: 18px; margin: 0 4px; }
|
||||||
.fb-tab { color: #6688aa; border-color: #1e3355; }
|
.fb-tab { color: #6688aa; border-color: var(--c1e3355); }
|
||||||
.fb-tab.active { background: #0d2235; border-color: #336699; color: #88bbdd; }
|
.fb-tab.active { background: var(--c0d2235); border-color: #336699; color: var(--t88bbdd); }
|
||||||
.fb-tab:hover:not(.active) { background: #0d1a28; color: #aaccee; }
|
.fb-tab:hover:not(.active) { background: var(--c0d1a28); color: var(--taaccee); }
|
||||||
|
|
||||||
#fb-toolbar {
|
#fb-toolbar {
|
||||||
display: flex; align-items: center; gap: 4px; flex-wrap: wrap;
|
display: flex; align-items: center; gap: 4px; flex-wrap: wrap;
|
||||||
padding: 4px 8px; background: #090914; border-bottom: 1px solid #1e3355;
|
padding: 4px 8px; background: var(--c090914); border-bottom: 1px solid var(--c1e3355);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.fb-mode-btn {
|
.fb-mode-btn {
|
||||||
padding: 3px 10px; font-size: 11px; border-radius: 4px;
|
padding: 3px 10px; font-size: 11px; border-radius: 4px;
|
||||||
background: #0d1a28; border: 1px solid #1e3355; color: #5577aa; cursor: pointer;
|
background: var(--c0d1a28); border: 1px solid var(--c1e3355); color: #5577aa; cursor: pointer;
|
||||||
}
|
}
|
||||||
.fb-mode-btn.active { background: #0d2235; border-color: #336699; color: #88bbdd; }
|
.fb-mode-btn.active { background: var(--c0d2235); border-color: #336699; color: var(--t88bbdd); }
|
||||||
.fb-mode-btn:hover:not(.active) { background: #111e30; color: #aaccee; }
|
.fb-mode-btn:hover:not(.active) { background: var(--c111e30); color: var(--taaccee); }
|
||||||
|
|
||||||
#fb-container { flex: 1; overflow: hidden; }
|
#fb-container { flex: 1; overflow: hidden; }
|
||||||
#fb-canvas { width: 100%; height: 100%; }
|
#fb-canvas { width: 100%; height: 100%; }
|
||||||
|
|
||||||
#btn-add-view {
|
#btn-add-view {
|
||||||
padding: 2px 8px; font-size: 14px; background: transparent;
|
padding: 2px 8px; font-size: 14px; background: transparent;
|
||||||
border: 1px dashed #333355; border-radius: 4px; color: #556; cursor: pointer;
|
border: 1px dashed var(--c333355); border-radius: 4px; color: #556; cursor: pointer;
|
||||||
}
|
}
|
||||||
#btn-add-view:hover { border-color: #5566aa; color: #99aadd; }
|
#btn-add-view:hover { border-color: var(--accent); color: var(--t99aadd); }
|
||||||
|
|
||||||
/* ── Device library ───────────────────────────────────────────────────────── */
|
/* ── Device library ───────────────────────────────────────────────────────── */
|
||||||
#device-library { padding: 6px; display: flex; flex-direction: column; gap: 3px; }
|
#device-library { padding: 6px; display: flex; flex-direction: column; gap: 3px; }
|
||||||
.lib-item {
|
.lib-item {
|
||||||
display: flex; align-items: center; gap: 8px; padding: 7px 8px;
|
display: flex; align-items: center; gap: 8px; padding: 7px 8px;
|
||||||
border: 1px solid #2a2a44; border-radius: 5px; cursor: grab; user-select: none;
|
border: 1px solid var(--border); border-radius: 5px; cursor: grab; user-select: none;
|
||||||
background: #18182e; transition: background .1s, border-color .1s;
|
background: var(--c18182e); transition: background .1s, border-color .1s;
|
||||||
}
|
}
|
||||||
.lib-item:hover { background: #22224a; border-color: #5555aa; }
|
.lib-item:hover { background: var(--c22224a); border-color: #5555aa; }
|
||||||
.lib-item:active { cursor: grabbing; }
|
.lib-item:active { cursor: grabbing; }
|
||||||
.lib-icon { font-size: 16px; width: 22px; text-align: center; }
|
.lib-icon { font-size: 16px; width: 22px; text-align: center; }
|
||||||
.lib-label { font-size: 12px; }
|
.lib-label { font-size: 12px; }
|
||||||
|
|
||||||
/* ── Connector library ────────────────────────────────────────────────────── */
|
/* ── Connector library ────────────────────────────────────────────────────── */
|
||||||
.lib-conn-item {
|
.lib-conn-item {
|
||||||
padding: 7px 8px; border: 1px solid #1e1e38; border-radius: 5px; cursor: grab;
|
padding: 7px 8px; border: 1px solid var(--c1e1e38); border-radius: 5px; cursor: grab;
|
||||||
background: #141428; margin-bottom: 3px; user-select: none; transition: background .1s, border-color .1s;
|
background: var(--c141428); margin-bottom: 3px; user-select: none; transition: background .1s, border-color .1s;
|
||||||
}
|
}
|
||||||
.lib-conn-item:hover { background: #1e1e40; border-color: #4444aa; }
|
.lib-conn-item:hover { background: var(--c1e1e40); border-color: #4444aa; }
|
||||||
.lib-conn-item:active { cursor: grabbing; }
|
.lib-conn-item:active { cursor: grabbing; }
|
||||||
.conn-name { font-size: 12px; font-weight: 500; color: #bbc0ee; }
|
.conn-name { font-size: 12px; font-weight: 500; color: var(--tbbc0ee); }
|
||||||
.conn-meta { font-size: 10px; color: #556688; margin-top: 1px; }
|
.conn-meta { font-size: 10px; color: #556688; margin-top: 1px; }
|
||||||
|
|
||||||
/* ── Canvas area ──────────────────────────────────────────────────────────── */
|
/* ── Canvas area ──────────────────────────────────────────────────────────── */
|
||||||
#canvas-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
#canvas-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
||||||
#canvas-container {
|
#canvas-container {
|
||||||
flex: 1; width: 100%;
|
flex: 1; width: 100%;
|
||||||
background-color: #131320;
|
background-color: var(--bg-canvas);
|
||||||
background-image: radial-gradient(circle, #2e2e50 1px, transparent 1px);
|
background-image: radial-gradient(circle, var(--grid-dot) 1px, transparent 1px);
|
||||||
background-size: 20px 20px;
|
background-size: 20px 20px;
|
||||||
}
|
}
|
||||||
#canvas-container.wire-mode { cursor: crosshair; }
|
#canvas-container.wire-mode { cursor: crosshair; }
|
||||||
@@ -169,207 +377,207 @@ body { background: #12121c; color: #c8ccee; }
|
|||||||
|
|
||||||
/* ── Buttons ──────────────────────────────────────────────────────────────── */
|
/* ── Buttons ──────────────────────────────────────────────────────────────── */
|
||||||
button {
|
button {
|
||||||
background: #22223a; border: 1px solid #3a3a5a; color: #c0c4e8;
|
background: var(--c22223a); border: 1px solid var(--border-strong); color: var(--text);
|
||||||
padding: 5px 11px; border-radius: 5px; cursor: pointer; font-size: 12px;
|
padding: 5px 11px; border-radius: 5px; cursor: pointer; font-size: 12px;
|
||||||
transition: background .12s, border-color .12s; white-space: nowrap;
|
transition: background .12s, border-color .12s; white-space: nowrap;
|
||||||
}
|
}
|
||||||
button:hover { background: #2e2e54; border-color: #5555aa; }
|
button:hover { background: var(--c2e2e54); border-color: #5555aa; }
|
||||||
button:active { background: #1a1a3a; }
|
button:active { background: var(--c1a1a3a); }
|
||||||
|
|
||||||
.mode-btn { padding: 5px 12px; }
|
.mode-btn { padding: 5px 12px; }
|
||||||
.mode-btn.active { background: #1e3a6a; border-color: #4488dd; color: #88bbff; }
|
.mode-btn.active { background: var(--c1e3a6a); border-color: #4488dd; color: var(--t88bbff); }
|
||||||
|
|
||||||
.route-btn { padding: 4px 10px; font-size: 11px; }
|
.route-btn { padding: 4px 10px; font-size: 11px; }
|
||||||
.route-btn.active { background: #1a2a4a; border-color: #336699; color: #77aaee; }
|
.route-btn.active { background: var(--c1a2a4a); border-color: #336699; color: var(--t77aaee); }
|
||||||
|
|
||||||
#btn-harness.active { background: #1a2a1a; border-color: #336633; color: #88ee88; }
|
#btn-harness.active { background: var(--c1a2a1a); border-color: var(--c336633); color: var(--t88ee88); }
|
||||||
|
|
||||||
.danger-btn { color: #ff6666; border-color: #662222; }
|
.danger-btn { color: var(--tff6666); border-color: var(--c662222); }
|
||||||
.danger-btn:hover { background: #3a1414; border-color: #aa3333; }
|
.danger-btn:hover { background: var(--c3a1414); border-color: #aa3333; }
|
||||||
|
|
||||||
.app-title { font-weight: 700; font-size: 15px; color: #88aaff; white-space: nowrap; }
|
.app-title { font-weight: 700; font-size: 15px; color: var(--t88aaff); white-space: nowrap; }
|
||||||
|
|
||||||
.diagram-name-input {
|
.diagram-name-input {
|
||||||
background: #1a1a2e; border: 1px solid #2a2a44; color: #c8ccee;
|
background: var(--bg-surface); border: 1px solid var(--border); color: var(--text-strong);
|
||||||
padding: 4px 8px; border-radius: 4px; font-size: 13px; width: 180px;
|
padding: 4px 8px; border-radius: 4px; font-size: 13px; width: 180px;
|
||||||
}
|
}
|
||||||
.diagram-name-input:focus { outline: none; border-color: #5566aa; }
|
.diagram-name-input:focus { outline: none; border-color: var(--accent); }
|
||||||
|
|
||||||
/* Export dropdown */
|
/* Export dropdown */
|
||||||
.export-wrap { position: relative; }
|
.export-wrap { position: relative; }
|
||||||
#export-menu {
|
#export-menu {
|
||||||
display: none; position: absolute; right: 0; top: calc(100% + 4px);
|
display: none; position: absolute; right: 0; top: calc(100% + 4px);
|
||||||
background: #1a1a2e; border: 1px solid #3a3a5a; border-radius: 6px;
|
background: var(--bg-surface); border: 1px solid var(--border-strong); border-radius: 6px;
|
||||||
min-width: 170px; z-index: 100; overflow: hidden; flex-direction: column;
|
min-width: 170px; z-index: 100; overflow: hidden; flex-direction: column;
|
||||||
}
|
}
|
||||||
#export-menu.open { display: flex; }
|
#export-menu.open { display: flex; }
|
||||||
#export-menu button { border: none; border-radius: 0; text-align: left; padding: 8px 14px; border-bottom: 1px solid #222238; }
|
#export-menu button { border: none; border-radius: 0; text-align: left; padding: 8px 14px; border-bottom: 1px solid var(--bg-raised); }
|
||||||
#export-menu button:last-child { border-bottom: none; }
|
#export-menu button:last-child { border-bottom: none; }
|
||||||
|
|
||||||
/* Saved indicator */
|
/* Saved indicator */
|
||||||
#saved-indicator {
|
#saved-indicator {
|
||||||
font-size: 11px; color: #4caa77; background: #0f2a1a;
|
font-size: 11px; color: var(--t4caa77); background: var(--c0f2a1a);
|
||||||
border: 1px solid #1e4a2a; padding: 4px 10px; border-radius: 4px;
|
border: 1px solid var(--c1e4a2a); padding: 4px 10px; border-radius: 4px;
|
||||||
opacity: 0; transition: opacity 0.3s; pointer-events: none; white-space: nowrap;
|
opacity: 0; transition: opacity 0.3s; pointer-events: none; white-space: nowrap;
|
||||||
}
|
}
|
||||||
#saved-indicator.show { opacity: 1; }
|
#saved-indicator.show { opacity: 1; }
|
||||||
|
|
||||||
/* ── Octopart search results ──────────────────────────────────────────────── */
|
/* ── Octopart search results ──────────────────────────────────────────────── */
|
||||||
#octopart-results {
|
#octopart-results {
|
||||||
margin-top: 5px; border: 1px solid #2a2a44; border-radius: 4px;
|
margin-top: 5px; border: 1px solid var(--border); border-radius: 4px;
|
||||||
background: #0e0e1e; max-height: 220px; overflow-y: auto;
|
background: var(--bg-inset); max-height: 220px; overflow-y: auto;
|
||||||
}
|
}
|
||||||
.op-result {
|
.op-result {
|
||||||
padding: 7px 9px; cursor: pointer; border-bottom: 1px solid #1a1a30;
|
padding: 7px 9px; cursor: pointer; border-bottom: 1px solid var(--bg-surface-2);
|
||||||
transition: background 0.1s;
|
transition: background 0.1s;
|
||||||
}
|
}
|
||||||
.op-result:last-child { border-bottom: none; }
|
.op-result:last-child { border-bottom: none; }
|
||||||
.op-result:hover { background: #1a1a38; }
|
.op-result:hover { background: var(--c1a1a38); }
|
||||||
.op-mpn { font-size: 11px; font-weight: 600; color: #c8ccee; }
|
.op-mpn { font-size: 11px; font-weight: 600; color: var(--text-strong); }
|
||||||
.op-mfr { font-size: 10px; color: #7788aa; margin-top: 1px; }
|
.op-mfr { font-size: 10px; color: var(--t7788aa); margin-top: 1px; }
|
||||||
.op-desc { font-size: 10px; color: #4a5566; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.op-desc { font-size: 10px; color: #4a5566; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
.op-ds { font-size: 10px; color: #4466aa; margin-top: 1px; }
|
.op-ds { font-size: 10px; color: #4466aa; margin-top: 1px; }
|
||||||
.op-no-results { padding: 10px; font-size: 11px; color: #444466; text-align: center; }
|
.op-no-results { padding: 10px; font-size: 11px; color: var(--text-faint); text-align: center; }
|
||||||
#btn-octopart-search.searching { opacity: 0.5; pointer-events: none; }
|
#btn-octopart-search.searching { opacity: 0.5; pointer-events: none; }
|
||||||
|
|
||||||
/* ── Properties panel ─────────────────────────────────────────────────────── */
|
/* ── Properties panel ─────────────────────────────────────────────────────── */
|
||||||
.prop-row { padding: 7px 10px; border-bottom: 1px solid #1e1e36; }
|
.prop-row { padding: 7px 10px; border-bottom: 1px solid var(--c1e1e36); }
|
||||||
.prop-row label {
|
.prop-row label {
|
||||||
display: block; font-size: 10px; font-weight: 600; text-transform: uppercase;
|
display: block; font-size: 10px; font-weight: 600; text-transform: uppercase;
|
||||||
letter-spacing: .06em; color: #555577; margin-bottom: 4px;
|
letter-spacing: .06em; color: var(--text-dim); margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
.prop-row input[type="text"], .prop-row input[type="number"],
|
.prop-row input[type="text"], .prop-row input[type="number"],
|
||||||
.prop-row select, .prop-row textarea {
|
.prop-row select, .prop-row textarea {
|
||||||
width: 100%; background: #0e0e1e; border: 1px solid #2a2a44;
|
width: 100%; background: var(--bg-inset); border: 1px solid var(--border);
|
||||||
color: #c0c4e8; padding: 5px 7px; border-radius: 4px; font-size: 12px; font-family: inherit;
|
color: var(--text); padding: 5px 7px; border-radius: 4px; font-size: 12px; font-family: inherit;
|
||||||
}
|
}
|
||||||
.prop-row input:focus, .prop-row select:focus, .prop-row textarea:focus { outline: none; border-color: #5566aa; }
|
.prop-row input:focus, .prop-row select:focus, .prop-row textarea:focus { outline: none; border-color: var(--accent); }
|
||||||
.prop-value { font-size: 12px; color: #99aacc; }
|
.prop-value { font-size: 12px; color: var(--t99aacc); }
|
||||||
.prop-value.mono { font-family: monospace; }
|
.prop-value.mono { font-family: monospace; }
|
||||||
|
|
||||||
.color-swatch { width: 36px; height: 28px; padding: 2px; cursor: pointer; border-radius: 4px; flex-shrink: 0; border: 1px solid #3a3a5a; }
|
.color-swatch { width: 36px; height: 28px; padding: 2px; cursor: pointer; border-radius: 4px; flex-shrink: 0; border: 1px solid var(--border-strong); }
|
||||||
|
|
||||||
/* Pin table */
|
/* Pin table */
|
||||||
.pin-table { width: 100%; border-collapse: collapse; font-size: 11px; }
|
.pin-table { width: 100%; border-collapse: collapse; font-size: 11px; }
|
||||||
.pin-table th { text-align: left; padding: 3px 4px; color: #555577; font-weight: 600; border-bottom: 1px solid #222238; }
|
.pin-table th { text-align: left; padding: 3px 4px; color: var(--text-dim); font-weight: 600; border-bottom: 1px solid var(--bg-raised); }
|
||||||
.pin-table td { padding: 2px 4px; border-bottom: 1px solid #1a1a30; vertical-align: middle; }
|
.pin-table td { padding: 2px 4px; border-bottom: 1px solid var(--bg-surface-2); vertical-align: middle; }
|
||||||
.pin-table .pin-side { color: #444466; font-size: 10px; }
|
.pin-table .pin-side { color: var(--text-faint); font-size: 10px; }
|
||||||
.pin-input { width: 100%; background: #0e0e1e; border: 1px solid #2a2a44; color: #c0c4e8; padding: 2px 4px; border-radius: 3px; font-family: monospace; font-size: 11px; }
|
.pin-input { width: 100%; background: var(--bg-inset); border: 1px solid var(--border); color: var(--text); padding: 2px 4px; border-radius: 3px; font-family: monospace; font-size: 11px; }
|
||||||
.pin-side-select { width: 100%; background: #0e0e1e; border: 1px solid #2a2a44; color: #c0c4e8; padding: 2px 3px; border-radius: 3px; font-size: 11px; cursor: pointer; }
|
.pin-side-select { width: 100%; background: var(--bg-inset); border: 1px solid var(--border); color: var(--text); padding: 2px 3px; border-radius: 3px; font-size: 11px; cursor: pointer; }
|
||||||
|
|
||||||
/* ── Connector library extras ─────────────────────────────────────────────── */
|
/* ── Connector library extras ─────────────────────────────────────────────── */
|
||||||
.lib-section-header {
|
.lib-section-header {
|
||||||
font-size: 10px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
|
font-size: 10px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
|
||||||
color: #555577; padding: 8px 8px 3px; border-top: 1px solid #1a1a30; margin-top: 4px;
|
color: var(--text-dim); padding: 8px 8px 3px; border-top: 1px solid var(--bg-surface-2); margin-top: 4px;
|
||||||
}
|
}
|
||||||
.lib-section-header:first-child { border-top: none; margin-top: 0; }
|
.lib-section-header:first-child { border-top: none; margin-top: 0; }
|
||||||
|
|
||||||
.conn-item-body { flex: 1; min-width: 0; }
|
.conn-item-body { flex: 1; min-width: 0; }
|
||||||
.conn-custom-badge {
|
.conn-custom-badge {
|
||||||
display: inline-block; font-size: 9px; font-weight: 600; letter-spacing: .06em;
|
display: inline-block; font-size: 9px; font-weight: 600; letter-spacing: .06em;
|
||||||
text-transform: uppercase; background: #2a1a4a; color: #9966ee;
|
text-transform: uppercase; background: var(--c2a1a4a); color: var(--t9966ee);
|
||||||
border: 1px solid #4422aa; border-radius: 3px; padding: 0 4px; vertical-align: middle;
|
border: 1px solid #4422aa; border-radius: 3px; padding: 0 4px; vertical-align: middle;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
.lib-conn-item { display: flex; align-items: center; gap: 6px; }
|
.lib-conn-item { display: flex; align-items: center; gap: 6px; }
|
||||||
.conn-edit-btn {
|
.conn-edit-btn {
|
||||||
flex-shrink: 0; opacity: 0; padding: 3px 6px; font-size: 12px;
|
flex-shrink: 0; opacity: 0; padding: 3px 6px; font-size: 12px;
|
||||||
background: #1e1e3a; border-color: #3a3a5a; transition: opacity .15s;
|
background: var(--bg-selected); border-color: var(--border-strong); transition: opacity .15s;
|
||||||
}
|
}
|
||||||
.lib-conn-item:hover .conn-edit-btn { opacity: 1; }
|
.lib-conn-item:hover .conn-edit-btn { opacity: 1; }
|
||||||
.conn-edit-btn:hover { background: #2a2a5a; border-color: #6666aa; }
|
.conn-edit-btn:hover { background: var(--c2a2a5a); border-color: #6666aa; }
|
||||||
|
|
||||||
/* ── Modal ───────────────────────────────────────────────────────────────── */
|
/* ── Modal ───────────────────────────────────────────────────────────────── */
|
||||||
#connector-modal, #drc-modal, #git-modal {
|
#connector-modal, #drc-modal, #git-modal, #pinout-modal, #pdm-modal, #theme-modal {
|
||||||
position: fixed; inset: 0; background: rgba(0,0,0,0.65); z-index: 1000;
|
position: fixed; inset: 0; background: rgba(0,0,0,0.65); z-index: 1000;
|
||||||
align-items: center; justify-content: center;
|
align-items: center; justify-content: center;
|
||||||
}
|
}
|
||||||
.modal-box {
|
.modal-box {
|
||||||
background: #16162a; border: 1px solid #3a3a5a; border-radius: 8px;
|
background: var(--bg-modal); border: 1px solid var(--border-strong); border-radius: 8px;
|
||||||
width: 480px; max-width: 94vw; max-height: 86vh;
|
width: 480px; max-width: 94vw; max-height: 86vh;
|
||||||
display: flex; flex-direction: column; box-shadow: 0 8px 40px rgba(0,0,0,0.7);
|
display: flex; flex-direction: column; box-shadow: 0 8px 40px rgba(0,0,0,0.7);
|
||||||
}
|
}
|
||||||
.modal-header {
|
.modal-header {
|
||||||
padding: 14px 18px 12px; border-bottom: 1px solid #222238; flex-shrink: 0;
|
padding: 14px 18px 12px; border-bottom: 1px solid var(--bg-raised); flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.modal-header h3 { font-size: 15px; font-weight: 600; color: #c8ccee; }
|
.modal-header h3 { font-size: 15px; font-weight: 600; color: var(--text-strong); }
|
||||||
.modal-body {
|
.modal-body {
|
||||||
padding: 14px 18px; overflow-y: auto; flex: 1;
|
padding: 14px 18px; overflow-y: auto; flex: 1;
|
||||||
display: flex; flex-direction: column; gap: 10px;
|
display: flex; flex-direction: column; gap: 10px;
|
||||||
}
|
}
|
||||||
.modal-footer {
|
.modal-footer {
|
||||||
padding: 10px 18px; border-top: 1px solid #222238;
|
padding: 10px 18px; border-top: 1px solid var(--bg-raised);
|
||||||
display: flex; align-items: center; gap: 8px; flex-shrink: 0;
|
display: flex; align-items: center; gap: 8px; flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── DRC ────────────────────────────────────────────────────────────────── */
|
/* ── DRC ────────────────────────────────────────────────────────────────── */
|
||||||
.drc-category { border: 1px solid #2a2a44; border-radius: 5px; overflow: hidden; }
|
.drc-category { border: 1px solid var(--border); border-radius: 5px; overflow: hidden; }
|
||||||
.drc-cat-header {
|
.drc-cat-header {
|
||||||
display: flex; align-items: center; gap: 8px;
|
display: flex; align-items: center; gap: 8px;
|
||||||
padding: 8px 12px; background: #1a1a30; cursor: pointer;
|
padding: 8px 12px; background: var(--bg-surface-2); cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
.drc-cat-header:hover { background: #1e1e3a; }
|
.drc-cat-header:hover { background: var(--bg-selected); }
|
||||||
.drc-cat-icon { font-size: 13px; }
|
.drc-cat-icon { font-size: 13px; }
|
||||||
.drc-cat-label { flex: 1; font-size: 12px; font-weight: 600; color: #c0c4e8; }
|
.drc-cat-label { flex: 1; font-size: 12px; font-weight: 600; color: var(--text); }
|
||||||
.drc-cat-count {
|
.drc-cat-count {
|
||||||
font-size: 11px; font-weight: 700; background: #2a2a50;
|
font-size: 11px; font-weight: 700; background: var(--c2a2a50);
|
||||||
padding: 1px 7px; border-radius: 10px; color: #9090cc;
|
padding: 1px 7px; border-radius: 10px; color: var(--t9090cc);
|
||||||
}
|
}
|
||||||
.drc-cat-body { padding: 4px 0; background: #0e0e1e; }
|
.drc-cat-body { padding: 4px 0; background: var(--bg-inset); }
|
||||||
.drc-row {
|
.drc-row {
|
||||||
padding: 5px 14px; font-size: 11px; color: #a0a4c8;
|
padding: 5px 14px; font-size: 11px; color: var(--ta0a4c8);
|
||||||
border-bottom: 1px solid #151528;
|
border-bottom: 1px solid var(--c151528);
|
||||||
}
|
}
|
||||||
.drc-row:last-child { border-bottom: none; }
|
.drc-row:last-child { border-bottom: none; }
|
||||||
.drc-none { color: #444466; font-style: italic; }
|
.drc-none { color: var(--text-faint); font-style: italic; }
|
||||||
|
|
||||||
/* ── Git modal ────────────────────────────────────────────────────────────── */
|
/* ── Git modal ────────────────────────────────────────────────────────────── */
|
||||||
.git-badge {
|
.git-badge {
|
||||||
font-size: 11px; padding: 2px 8px; border-radius: 10px;
|
font-size: 11px; padding: 2px 8px; border-radius: 10px;
|
||||||
background: #1a1a30; color: #8899bb; border: 1px solid #2a2a44;
|
background: var(--bg-surface-2); color: var(--text-muted); border: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
.git-badge.git-clean { color: #88cc88; border-color: #336633; }
|
.git-badge.git-clean { color: var(--t88cc88); border-color: var(--c336633); }
|
||||||
.git-badge.git-dirty { color: #ddaa44; border-color: #554422; }
|
.git-badge.git-dirty { color: var(--tddaa44); border-color: var(--c554422); }
|
||||||
.git-section { display: flex; flex-direction: column; gap: 5px; }
|
.git-section { display: flex; flex-direction: column; gap: 5px; }
|
||||||
.git-section-title {
|
.git-section-title {
|
||||||
font-size: 10px; font-weight: 600; text-transform: uppercase;
|
font-size: 10px; font-weight: 600; text-transform: uppercase;
|
||||||
letter-spacing: .06em; color: #555577;
|
letter-spacing: .06em; color: var(--text-dim);
|
||||||
}
|
}
|
||||||
.git-log-row {
|
.git-log-row {
|
||||||
display: grid; grid-template-columns: 52px 1fr 80px auto;
|
display: grid; grid-template-columns: 52px 1fr 80px auto;
|
||||||
align-items: center; gap: 8px;
|
align-items: center; gap: 8px;
|
||||||
padding: 4px 6px; border-radius: 3px; background: #0d0d20;
|
padding: 4px 6px; border-radius: 3px; background: var(--c0d0d20);
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
.git-log-row:hover { border-color: #2a2a44; }
|
.git-log-row:hover { border-color: var(--border); }
|
||||||
.git-hash { font-family: monospace; color: #6688cc; font-size: 11px; }
|
.git-hash { font-family: monospace; color: var(--t6688cc); font-size: 11px; }
|
||||||
.git-msg { color: #bbc; font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.git-msg { color: var(--tbbc); font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
.git-date { color: #445; font-size: 10px; text-align: right; }
|
.git-date { color: #445; font-size: 10px; text-align: right; }
|
||||||
.git-scope-btn {
|
.git-scope-btn {
|
||||||
font-size: 10px; padding: 2px 8px;
|
font-size: 10px; padding: 2px 8px;
|
||||||
background: #111122; border: 1px solid #2a2a44; color: #445566; cursor: pointer;
|
background: var(--c111122); border: 1px solid var(--border); color: var(--t445566); cursor: pointer;
|
||||||
}
|
}
|
||||||
.git-scope-btn:first-of-type { border-radius: 3px 0 0 3px; }
|
.git-scope-btn:first-of-type { border-radius: 3px 0 0 3px; }
|
||||||
.git-scope-btn:last-of-type { border-radius: 0 3px 3px 0; border-left: none; }
|
.git-scope-btn:last-of-type { border-radius: 0 3px 3px 0; border-left: none; }
|
||||||
.git-scope-btn.active { background: #1a2840; border-color: #334466; color: #88aadd; }
|
.git-scope-btn.active { background: var(--c1a2840); border-color: var(--c334466); color: var(--t88aadd); }
|
||||||
.git-scope-btn:disabled { opacity: 0.35; cursor: default; }
|
.git-scope-btn:disabled { opacity: 0.35; cursor: default; }
|
||||||
.git-restore-btn {
|
.git-restore-btn {
|
||||||
font-size: 10px; padding: 2px 6px;
|
font-size: 10px; padding: 2px 6px;
|
||||||
background: #1a2840; border: 1px solid #334466; border-radius: 3px;
|
background: var(--c1a2840); border: 1px solid var(--c334466); border-radius: 3px;
|
||||||
color: #88aadd; cursor: pointer; white-space: nowrap;
|
color: var(--t88aadd); cursor: pointer; white-space: nowrap;
|
||||||
}
|
}
|
||||||
.git-restore-btn:hover { background: #223355; color: #aaccff; }
|
.git-restore-btn:hover { background: var(--c223355); color: var(--taaccff); }
|
||||||
|
|
||||||
.cm-label {
|
.cm-label {
|
||||||
display: block; font-size: 10px; font-weight: 600; text-transform: uppercase;
|
display: block; font-size: 10px; font-weight: 600; text-transform: uppercase;
|
||||||
letter-spacing: .06em; color: #555577; margin-bottom: 4px;
|
letter-spacing: .06em; color: var(--text-dim); margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
.cm-input {
|
.cm-input {
|
||||||
width: 100%; background: #0e0e1e; border: 1px solid #2a2a44;
|
width: 100%; background: var(--bg-inset); border: 1px solid var(--border);
|
||||||
color: #c0c4e8; padding: 6px 8px; border-radius: 4px; font-size: 12px; font-family: inherit;
|
color: var(--text); padding: 6px 8px; border-radius: 4px; font-size: 12px; font-family: inherit;
|
||||||
}
|
}
|
||||||
.cm-input:focus { outline: none; border-color: #5566aa; }
|
.cm-input:focus { outline: none; border-color: var(--accent); }
|
||||||
.cm-row { display: flex; flex-direction: column; }
|
.cm-row { display: flex; flex-direction: column; }
|
||||||
.cm-row-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
.cm-row-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||||
|
|
||||||
@@ -380,28 +588,28 @@ button:active { background: #1a1a3a; }
|
|||||||
.pin-label-row { display: flex; align-items: center; gap: 5px; }
|
.pin-label-row { display: flex; align-items: center; gap: 5px; }
|
||||||
.pin-idx {
|
.pin-idx {
|
||||||
width: 22px; text-align: right; flex-shrink: 0;
|
width: 22px; text-align: right; flex-shrink: 0;
|
||||||
font-size: 10px; font-weight: 600; color: #555577; font-family: monospace;
|
font-size: 10px; font-weight: 600; color: var(--text-dim); font-family: monospace;
|
||||||
}
|
}
|
||||||
.pin-label-input {
|
.pin-label-input {
|
||||||
flex: 1; background: #0e0e1e; border: 1px solid #222238;
|
flex: 1; background: var(--bg-inset); border: 1px solid var(--bg-raised);
|
||||||
color: #c0c4e8; padding: 4px 6px; border-radius: 3px; font-size: 11px; font-family: monospace;
|
color: var(--text); padding: 4px 6px; border-radius: 3px; font-size: 11px; font-family: monospace;
|
||||||
}
|
}
|
||||||
.pin-label-input:focus { outline: none; border-color: #5566aa; }
|
.pin-label-input:focus { outline: none; border-color: var(--accent); }
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: #1e3a6a; border-color: #4488dd; color: #88bbff; font-weight: 600;
|
background: var(--c1e3a6a); border-color: #4488dd; color: var(--t88bbff); font-weight: 600;
|
||||||
}
|
}
|
||||||
.btn-primary:hover { background: #264a8a; border-color: #66aaff; }
|
.btn-primary:hover { background: var(--c264a8a); border-color: #66aaff; }
|
||||||
.btn-primary:disabled { opacity: 0.5; cursor: default; }
|
.btn-primary:disabled { opacity: 0.5; cursor: default; }
|
||||||
|
|
||||||
/* Pin remove button */
|
/* Pin remove button */
|
||||||
.pin-del-btn {
|
.pin-del-btn {
|
||||||
padding: 0 4px; font-size: 12px; line-height: 1.4;
|
padding: 0 4px; font-size: 12px; line-height: 1.4;
|
||||||
background: transparent; border-color: transparent; color: #ff6666;
|
background: transparent; border-color: transparent; color: var(--tff6666);
|
||||||
opacity: 0; transition: opacity .15s;
|
opacity: 0; transition: opacity .15s;
|
||||||
}
|
}
|
||||||
.pin-table tr:hover .pin-del-btn { opacity: 1; }
|
.pin-table tr:hover .pin-del-btn { opacity: 1; }
|
||||||
.pin-del-btn:hover { background: #3a1414; border-color: #aa3333; }
|
.pin-del-btn:hover { background: var(--c3a1414); border-color: #aa3333; }
|
||||||
|
|
||||||
/* Wire drag cursor hint */
|
/* Wire drag cursor hint */
|
||||||
#canvas-container.wire-drag { cursor: grabbing; }
|
#canvas-container.wire-drag { cursor: grabbing; }
|
||||||
@@ -409,26 +617,133 @@ button:active { background: #1a1a3a; }
|
|||||||
/* ── Context menu ─────────────────────────────────────────────────────────── */
|
/* ── Context menu ─────────────────────────────────────────────────────────── */
|
||||||
#ctx-menu {
|
#ctx-menu {
|
||||||
position: fixed; z-index: 9000; display: none;
|
position: fixed; z-index: 9000; display: none;
|
||||||
background: #1a1a2e; border: 1px solid #3a3a66; border-radius: 6px;
|
background: var(--bg-surface); border: 1px solid var(--c3a3a66); border-radius: 6px;
|
||||||
box-shadow: 0 6px 24px rgba(0,0,0,.55); padding: 4px 0; min-width: 170px;
|
box-shadow: 0 6px 24px rgba(0,0,0,.55); padding: 4px 0; min-width: 170px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
#ctx-menu.open { display: block; }
|
#ctx-menu.open { display: block; }
|
||||||
.ctx-item {
|
.ctx-item {
|
||||||
padding: 7px 14px; font-size: 12px; color: #c0c4e8; cursor: pointer;
|
padding: 7px 14px; font-size: 12px; color: var(--text); cursor: pointer;
|
||||||
display: flex; align-items: center; gap: 8px; white-space: nowrap;
|
display: flex; align-items: center; gap: 8px; white-space: nowrap;
|
||||||
}
|
}
|
||||||
.ctx-item:hover { background: #2a2a50; color: #fff; }
|
.ctx-item:hover { background: var(--c2a2a50); color: var(--tfff); }
|
||||||
.ctx-item.danger { color: #dd6666; }
|
.ctx-item.danger { color: var(--tdd6666); }
|
||||||
.ctx-item.danger:hover { background: #3a1414; color: #ff8888; }
|
.ctx-item.danger:hover { background: var(--c3a1414); color: var(--tff8888); }
|
||||||
.ctx-sep { height: 1px; background: #2a2a44; margin: 4px 0; }
|
.ctx-sep { height: 1px; background: var(--border); margin: 4px 0; }
|
||||||
|
|
||||||
/* Misc */
|
/* Misc */
|
||||||
input[type="checkbox"] { accent-color: #5588dd; width: 14px; height: 14px; cursor: pointer; }
|
input[type="checkbox"] { accent-color: #5588dd; width: 14px; height: 14px; cursor: pointer; }
|
||||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||||
::-webkit-scrollbar-track { background: transparent; }
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
::-webkit-scrollbar-thumb { background: #2a2a44; border-radius: 3px; }
|
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
||||||
::-webkit-scrollbar-thumb:hover { background: #3a3a66; }
|
::-webkit-scrollbar-thumb:hover { background: var(--c3a3a66); }
|
||||||
.muted { color: #444466; }
|
.muted { color: var(--text-faint); }
|
||||||
.small { font-size: 11px; }
|
.small { font-size: 11px; }
|
||||||
.sep { width: 1px; height: 22px; background: #2a2a44; margin: 0 3px; }
|
.sep { width: 1px; height: 22px; background: var(--border); margin: 0 3px; }
|
||||||
|
|
||||||
|
/* ── Fuse box configurator ─────────────────────────────────────────────── */
|
||||||
|
.pdm-capacity { display: flex; align-items: center; gap: 10px; }
|
||||||
|
.pdm-bar {
|
||||||
|
flex: 1; height: 8px; border-radius: 4px;
|
||||||
|
background: var(--bg-inset); border: 1px solid var(--border); overflow: hidden;
|
||||||
|
}
|
||||||
|
.pdm-bar-fill { height: 100%; width: 0; background: #4a7dd6; transition: width .15s ease; }
|
||||||
|
.pdm-bar-fill.warn { background: #d6a44a; }
|
||||||
|
.pdm-bar-fill.over { background: #d65a4a; }
|
||||||
|
.pdm-capacity-text {
|
||||||
|
font-size: 11px; color: var(--text-muted); font-variant-numeric: tabular-nums; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.pdm-add {
|
||||||
|
font-size: 11px; padding: 4px 9px; border-radius: 4px;
|
||||||
|
background: var(--bg-surface-2); border: 1px solid var(--border); color: var(--text); cursor: pointer;
|
||||||
|
}
|
||||||
|
.pdm-add:hover { background: var(--c23233c); border-color: var(--c3d3d63); }
|
||||||
|
.pdm-add:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
||||||
|
.pdm-table { width: 100%; border-collapse: collapse; font-size: 11.5px; }
|
||||||
|
.pdm-table th {
|
||||||
|
text-align: left; font-weight: 600; color: var(--text-muted); padding: 5px 6px;
|
||||||
|
border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--bg-panel);
|
||||||
|
}
|
||||||
|
.pdm-table td { padding: 3px 6px; border-bottom: 1px solid var(--bg-surface-3); }
|
||||||
|
.pdm-cavity {
|
||||||
|
font-family: ui-monospace, Consolas, monospace; color: var(--text-muted);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.pdm-table .cm-input { padding: 3px 6px; font-size: 11.5px; width: 100%; }
|
||||||
|
.pdm-del {
|
||||||
|
background: none; border: none; color: #8a6a6a; cursor: pointer; font-size: 13px; padding: 2px 5px;
|
||||||
|
}
|
||||||
|
.pdm-del:hover { color: var(--td65a4a); }
|
||||||
|
.pdm-warn { font-size: 11px; color: var(--td65a4a); }
|
||||||
|
|
||||||
|
/* ── Fuse box grid editor ──────────────────────────────────────────────── */
|
||||||
|
.pdm-split { display: grid; grid-template-columns: auto 1fr; gap: 14px; align-items: start; }
|
||||||
|
.pdm-grid-pane { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
.pdm-grid {
|
||||||
|
position: relative; background: var(--bg-inset);
|
||||||
|
border: 1px solid var(--border); border-radius: 6px; padding: 6px;
|
||||||
|
user-select: none; touch-action: none;
|
||||||
|
}
|
||||||
|
.pdm-grid:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
.pdm-cell {
|
||||||
|
position: absolute; box-sizing: border-box;
|
||||||
|
border: 1px dashed var(--border-faint); border-radius: 3px;
|
||||||
|
}
|
||||||
|
.pdm-cell-num {
|
||||||
|
position: absolute; font-size: 8px; color: var(--text-ghost);
|
||||||
|
top: 1px; left: 2px; pointer-events: none;
|
||||||
|
font-family: ui-monospace, Consolas, monospace;
|
||||||
|
}
|
||||||
|
.pdm-part {
|
||||||
|
position: absolute; box-sizing: border-box; border-radius: 4px;
|
||||||
|
border: 1px solid rgba(255,255,255,.22); cursor: grab;
|
||||||
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||||
|
font-size: 9.5px; line-height: 1.15; color: var(--tfff); text-align: center;
|
||||||
|
overflow: hidden; padding: 2px;
|
||||||
|
}
|
||||||
|
.pdm-part:active { cursor: grabbing; }
|
||||||
|
.pdm-part.sel { outline: 2px solid #e8edf7; outline-offset: -1px; z-index: 3; }
|
||||||
|
.pdm-part.bad { border-color: #ff6a58; box-shadow: 0 0 0 2px rgba(255,106,88,.5) inset; }
|
||||||
|
.pdm-part-tag { font-weight: 700; letter-spacing: .3px; }
|
||||||
|
.pdm-part-name {
|
||||||
|
font-size: 8.5px; opacity: .85; max-width: 100%;
|
||||||
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.pdm-term {
|
||||||
|
position: absolute; width: 13px; height: 13px; border-radius: 2px;
|
||||||
|
background: rgba(0,0,0,.45); border: 1px solid rgba(255,255,255,.3);
|
||||||
|
font-size: 7px; color: var(--tfff); display: flex; align-items: center; justify-content: center;
|
||||||
|
pointer-events: none; font-family: ui-monospace, Consolas, monospace;
|
||||||
|
}
|
||||||
|
.pdm-side { display: flex; flex-direction: column; min-width: 0; }
|
||||||
|
.pdm-pinout-wrap {
|
||||||
|
border: 1px solid var(--border); border-radius: 5px;
|
||||||
|
max-height: 288px; overflow-y: auto; background: var(--bg-inset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bus bars lie across the cavities they link, so they draw as a narrow strip
|
||||||
|
on top of the components they feed rather than as a filled tile. */
|
||||||
|
.pdm-bus {
|
||||||
|
position: absolute; box-sizing: border-box; border-radius: 3px;
|
||||||
|
border: 1px solid rgba(255,255,255,.45); cursor: grab; z-index: 4;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,.55);
|
||||||
|
}
|
||||||
|
.pdm-bus:active { cursor: grabbing; }
|
||||||
|
.pdm-bus.sel { outline: 2px solid #e8edf7; outline-offset: 1px; z-index: 5; }
|
||||||
|
.pdm-bus.bad { border-color: #ff6a58; box-shadow: 0 0 0 2px rgba(255,106,88,.6); }
|
||||||
|
.pdm-term.bus-fed { background: #b0563c; border-color: rgba(255,255,255,.55); }
|
||||||
|
|
||||||
|
/* ── Appearance panel ──────────────────────────────────────────────────── */
|
||||||
|
.theme-pick { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||||||
|
.theme-pick input[type="color"] {
|
||||||
|
width: 46px; height: 28px; padding: 0; cursor: pointer;
|
||||||
|
background: var(--bg-inset); border: 1px solid var(--border); border-radius: 4px;
|
||||||
|
}
|
||||||
|
.theme-inline { display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--text-muted); }
|
||||||
|
.theme-swatches { display: flex; gap: 5px; }
|
||||||
|
.theme-swatch {
|
||||||
|
width: 24px; height: 24px; border-radius: 4px; cursor: pointer;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
}
|
||||||
|
.theme-swatch:hover { outline: 2px solid var(--accent); outline-offset: 1px; }
|
||||||
|
.theme-swatch:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toolbar-right">
|
<div class="toolbar-right">
|
||||||
|
<button id="btn-theme" title="Appearance — theme and colours">◐ Theme</button>
|
||||||
<button id="btn-git" title="Git version control">⎇ Git</button>
|
<button id="btn-git" title="Git version control">⎇ Git</button>
|
||||||
<button id="btn-drc" title="Run design-rule check">⚠ DRC</button>
|
<button id="btn-drc" title="Run design-rule check">⚠ DRC</button>
|
||||||
<div id="saved-indicator">✓ Saved</div>
|
<div id="saved-indicator">✓ Saved</div>
|
||||||
@@ -230,6 +231,12 @@
|
|||||||
<button id="prop-add-pin" style="flex:1;font-size:11px;padding:3px 6px">+ Add Pin</button>
|
<button id="prop-add-pin" style="flex:1;font-size:11px;padding:3px 6px">+ Add Pin</button>
|
||||||
<button id="prop-save-connector" class="btn-primary" style="flex:1;font-size:11px;padding:3px 6px">⬆ Save to Library</button>
|
<button id="prop-save-connector" class="btn-primary" style="flex:1;font-size:11px;padding:3px 6px">⬆ Save to Library</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="margin-top:4px">
|
||||||
|
<button id="prop-pinout-btn" style="display:none;width:100%;font-size:11px;padding:3px 6px">⊙ View Pinout</button>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:4px">
|
||||||
|
<button id="prop-pdm-btn" class="btn-primary" style="display:none;width:100%;font-size:11px;padding:4px 6px">▦ Configure Fuse Box</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -459,6 +466,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Pinout Modal ───────────────────────────────────────────────────────────── -->
|
||||||
|
<div id="pinout-modal" style="display:none">
|
||||||
|
<div class="modal-box" style="min-width:580px;max-width:700px">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 id="pinout-modal-title">Connector Pinout</h3>
|
||||||
|
<span id="pinout-info" style="font-size:11px;color:#888;margin-left:10px"></span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" style="flex-direction:row;align-items:flex-start;padding:14px;gap:16px">
|
||||||
|
<div id="pinout-svg-container" style="flex-shrink:0"></div>
|
||||||
|
<div style="flex:1;display:flex;flex-direction:column;gap:6px;min-width:0">
|
||||||
|
<div style="font-size:11px;color:#8899cc;font-weight:bold;padding-bottom:2px;border-bottom:1px solid #2a2a4a">Pin Assignments</div>
|
||||||
|
<div id="pinout-table-container" style="overflow-y:auto;max-height:260px">
|
||||||
|
<table id="pinout-table" style="width:100%;border-collapse:collapse;font-size:11px;font-family:monospace">
|
||||||
|
<thead>
|
||||||
|
<tr style="color:#666688;text-align:left">
|
||||||
|
<th style="padding:3px 6px;width:36px">#</th>
|
||||||
|
<th style="padding:3px 6px">Note / Connection</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="pinout-table-body"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="pinout-print-btn">🖨 Print</button>
|
||||||
|
<button id="pinout-copy-btn">⎘ Copy Table</button>
|
||||||
|
<div style="flex:1"></div>
|
||||||
|
<button id="pinout-close" class="btn-primary">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ── Context Menu ──────────────────────────────────────────────────────────── -->
|
<!-- ── Context Menu ──────────────────────────────────────────────────────────── -->
|
||||||
<div id="ctx-menu">
|
<div id="ctx-menu">
|
||||||
<!-- populated dynamically by app.js -->
|
<!-- populated dynamically by app.js -->
|
||||||
@@ -479,6 +519,148 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="theme-modal" style="display:none">
|
||||||
|
<div class="modal-box" style="min-width:420px;max-width:520px">
|
||||||
|
<div class="modal-header"><h3>◐ Appearance</h3></div>
|
||||||
|
<div class="modal-body" style="padding:14px;gap:14px">
|
||||||
|
|
||||||
|
<div class="cm-row">
|
||||||
|
<label class="cm-label">Mode</label>
|
||||||
|
<div style="display:flex;gap:6px">
|
||||||
|
<button id="theme-dark" class="pdm-add" style="flex:1">Dark</button>
|
||||||
|
<button id="theme-light" class="pdm-add" style="flex:1">Light</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cm-row">
|
||||||
|
<label class="cm-label">Canvas background</label>
|
||||||
|
<div class="theme-pick">
|
||||||
|
<input id="theme-canvas-bg" type="color">
|
||||||
|
<div id="theme-bg-swatches" class="theme-swatches"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cm-row">
|
||||||
|
<label class="cm-label">Grid dots</label>
|
||||||
|
<div class="theme-pick"><input id="theme-grid-dot" type="color"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cm-row">
|
||||||
|
<label class="cm-label">Part outline</label>
|
||||||
|
<div class="theme-pick"><input id="theme-dev-stroke" type="color"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cm-row">
|
||||||
|
<label class="cm-label">Part fill</label>
|
||||||
|
<div class="theme-pick">
|
||||||
|
<input id="theme-dev-fill" type="color">
|
||||||
|
<label class="theme-inline">
|
||||||
|
<input id="theme-fill-bytype" type="checkbox"> Colour by device type
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="muted small" style="margin:0">
|
||||||
|
Wires keep their own colours — these control what they sit against.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="theme-reset">Reset to defaults</button>
|
||||||
|
<div style="flex:1"></div>
|
||||||
|
<button id="theme-close" class="btn-primary">Done</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fuse box configurator — edits the selected PDM device on the canvas -->
|
||||||
|
<div id="pdm-modal" style="display:none">
|
||||||
|
<div class="modal-box" style="min-width:720px;max-width:900px">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>▦ Fuse Box</h3>
|
||||||
|
<span id="pdm-linked-ref" class="git-badge"></span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" style="padding:12px;gap:10px">
|
||||||
|
|
||||||
|
<div class="cm-row-2col">
|
||||||
|
<div class="cm-row">
|
||||||
|
<label class="cm-label">Module</label>
|
||||||
|
<select id="pdm-module" class="cm-input"></select>
|
||||||
|
</div>
|
||||||
|
<div class="cm-row">
|
||||||
|
<label class="cm-label">Reference / label</label>
|
||||||
|
<input id="pdm-label" class="cm-input" type="text" placeholder="e.g. PDM1 — front">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">
|
||||||
|
<button id="pdm-add-fuse" class="pdm-add">+ Fuse</button>
|
||||||
|
<button id="pdm-add-spdt" class="pdm-add">+ Relay 5-pin</button>
|
||||||
|
<button id="pdm-add-spst" class="pdm-add">+ Relay 4-pin</button>
|
||||||
|
<button id="pdm-add-diode" class="pdm-add">+ Diode</button>
|
||||||
|
<button id="pdm-add-breaker" class="pdm-add">+ Breaker</button>
|
||||||
|
<button id="pdm-add-bus" class="pdm-add">+ Bus bar</button>
|
||||||
|
<div style="width:1px;height:18px;background:#2a2a44"></div>
|
||||||
|
<button id="pdm-rotate" class="pdm-add" title="Rotate selected (R)">⟳ Rotate</button>
|
||||||
|
<button id="pdm-delete" class="pdm-add" title="Remove selected (Del)">✕ Remove</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pdm-split">
|
||||||
|
|
||||||
|
<!-- Cavity grid -->
|
||||||
|
<div class="pdm-grid-pane">
|
||||||
|
<div id="pdm-grid" class="pdm-grid" tabindex="0"></div>
|
||||||
|
<div id="pdm-capacity" class="pdm-capacity">
|
||||||
|
<div class="pdm-bar"><div id="pdm-bar-fill" class="pdm-bar-fill"></div></div>
|
||||||
|
<span id="pdm-capacity-text" class="pdm-capacity-text"></span>
|
||||||
|
</div>
|
||||||
|
<p class="muted small" style="margin:0">Drag to move · R to rotate · Del to remove</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Selected component + full pinout -->
|
||||||
|
<div class="pdm-side">
|
||||||
|
<div id="pdm-sel" style="display:none">
|
||||||
|
<label class="cm-label">Circuit name</label>
|
||||||
|
<input id="pdm-sel-name" class="cm-input" type="text" placeholder="e.g. Coolant pump">
|
||||||
|
<div class="cm-row-2col" style="margin-top:6px">
|
||||||
|
<div class="cm-row">
|
||||||
|
<label class="cm-label">Component</label>
|
||||||
|
<select id="pdm-sel-type" class="cm-input"></select>
|
||||||
|
</div>
|
||||||
|
<div class="cm-row">
|
||||||
|
<label class="cm-label">Rating</label>
|
||||||
|
<select id="pdm-sel-rating" class="cm-input"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p id="pdm-sel-empty" class="muted small" style="padding:8px 0">
|
||||||
|
Select a component in the grid to name it.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<label class="cm-label" style="margin-top:8px">Pinout</label>
|
||||||
|
<div class="pdm-pinout-wrap">
|
||||||
|
<table class="pdm-table">
|
||||||
|
<thead>
|
||||||
|
<tr><th style="width:54px">Cavity</th><th style="width:46px">Term</th><th>Pin name</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="pdm-pinout"></tbody>
|
||||||
|
</table>
|
||||||
|
<p id="pdm-empty" class="muted small" style="padding:12px;text-align:center">
|
||||||
|
Empty box. Add a fuse or relay to begin.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<span id="pdm-warn" class="pdm-warn"></span>
|
||||||
|
<div style="flex:1"></div>
|
||||||
|
<button id="pdm-cancel">Cancel</button>
|
||||||
|
<button id="pdm-save" class="btn-primary">Apply to device</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="git-modal" style="display:none">
|
<div id="git-modal" style="display:none">
|
||||||
<div class="modal-box" style="min-width:520px;max-width:700px">
|
<div class="modal-box" style="min-width:520px;max-width:700px">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@@ -527,9 +709,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://unpkg.com/konva@9/konva.min.js"></script>
|
<script src="https://unpkg.com/konva@9/konva.min.js"></script>
|
||||||
|
<script src="/js/theme.js"></script>
|
||||||
<script src="/js/api.js"></script>
|
<script src="/js/api.js"></script>
|
||||||
<script src="/js/deviceTypes.js"></script>
|
<script src="/js/deviceTypes.js"></script>
|
||||||
<script src="/js/connectorLibrary.js"></script>
|
<script src="/js/connectorLibrary.js"></script>
|
||||||
|
<script src="/js/partsLibrary.js"></script>
|
||||||
|
<script src="/js/pdmLibrary.js"></script>
|
||||||
<script src="/js/canvas.js"></script>
|
<script src="/js/canvas.js"></script>
|
||||||
<script src="/js/formboard.js"></script>
|
<script src="/js/formboard.js"></script>
|
||||||
<script src="/js/app.js"></script>
|
<script src="/js/app.js"></script>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ async function apiFetch(path, options = {}) {
|
|||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
throw new Error(`API ${res.status}: ${text}`);
|
throw new Error(`API ${res.status}: ${text}`);
|
||||||
}
|
}
|
||||||
|
if (res.status === 204) return null;
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+913
-20
File diff suppressed because it is too large
Load Diff
+86
-50
@@ -173,7 +173,7 @@ class DiagramCanvas {
|
|||||||
// Loom background
|
// Loom background
|
||||||
this.harnessLayer.add(new Konva.Line({
|
this.harnessLayer.add(new Konva.Line({
|
||||||
points: [cx, cy, ex, ey],
|
points: [cx, cy, ex, ey],
|
||||||
stroke: '#0a0a18', strokeWidth: trunkW + 4,
|
stroke: Theme.loomShadow(), strokeWidth: trunkW + 4,
|
||||||
lineCap: 'round', listening: false,
|
lineCap: 'round', listening: false,
|
||||||
}));
|
}));
|
||||||
// Colored wire stripes
|
// Colored wire stripes
|
||||||
@@ -192,7 +192,7 @@ class DiagramCanvas {
|
|||||||
// Split-off circle at loom end
|
// Split-off circle at loom end
|
||||||
this.harnessLayer.add(new Konva.Circle({
|
this.harnessLayer.add(new Konva.Circle({
|
||||||
x: ex, y: ey, radius: trunkW / 2 + 4,
|
x: ex, y: ey, radius: trunkW / 2 + 4,
|
||||||
fill: '#12122a', stroke: '#4466cc', strokeWidth: 2, listening: false,
|
fill: Theme.harnessBg(), stroke: Theme.harnessEdge(), strokeWidth: 2, listening: false,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -238,7 +238,7 @@ class DiagramCanvas {
|
|||||||
// Trunk background
|
// Trunk background
|
||||||
this.harnessLayer.add(new Konva.Line({
|
this.harnessLayer.add(new Konva.Line({
|
||||||
points: [ax, ay, bx, by],
|
points: [ax, ay, bx, by],
|
||||||
stroke: '#0a0a18', strokeWidth: trunkW + 4,
|
stroke: Theme.loomShadow(), strokeWidth: trunkW + 4,
|
||||||
lineCap: 'round', lineJoin: 'round', listening: false,
|
lineCap: 'round', lineJoin: 'round', listening: false,
|
||||||
}));
|
}));
|
||||||
// Colored wire stripes
|
// Colored wire stripes
|
||||||
@@ -260,12 +260,12 @@ class DiagramCanvas {
|
|||||||
this.harnessLayer.add(new Konva.Rect({
|
this.harnessLayer.add(new Konva.Rect({
|
||||||
x: mx - 22, y: my - 9,
|
x: mx - 22, y: my - 9,
|
||||||
width: 44, height: 18, cornerRadius: 4,
|
width: 44, height: 18, cornerRadius: 4,
|
||||||
fill: '#12122a', stroke: '#4466cc', strokeWidth: 1.5, listening: false,
|
fill: Theme.harnessBg(), stroke: Theme.harnessEdge(), strokeWidth: 1.5, listening: false,
|
||||||
}));
|
}));
|
||||||
this.harnessLayer.add(new Konva.Text({
|
this.harnessLayer.add(new Konva.Text({
|
||||||
x: mx - 22, y: my - 6,
|
x: mx - 22, y: my - 6,
|
||||||
text: badgeText, width: 44, align: 'center',
|
text: badgeText, width: 44, align: 'center',
|
||||||
fontSize: 9, fontFamily: 'monospace', fill: '#99bbff', listening: false,
|
fontSize: 9, fontFamily: 'monospace', fill: Theme.harnessText(), listening: false,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -619,7 +619,7 @@ class DiagramCanvas {
|
|||||||
if (!g) return;
|
if (!g) return;
|
||||||
const d = this.deviceData.get(id);
|
const d = this.deviceData.get(id);
|
||||||
const isGroup = d?.device_type === "group";
|
const isGroup = d?.device_type === "group";
|
||||||
const offColor = isGroup ? (d.properties?.fillColor || "#2828a0") : "#5a5a8a";
|
const offColor = isGroup ? (d.properties?.fillColor || "#2828a0") : Theme.deviceStroke();
|
||||||
g.findOne("Rect").stroke(on ? "#4db8ff" : offColor);
|
g.findOne("Rect").stroke(on ? "#4db8ff" : offColor);
|
||||||
(isGroup ? this.groupLayer : this.deviceLayer).batchDraw();
|
(isGroup ? this.groupLayer : this.deviceLayer).batchDraw();
|
||||||
}
|
}
|
||||||
@@ -638,6 +638,13 @@ class DiagramCanvas {
|
|||||||
this.wireLayer.batchDraw();
|
this.wireLayer.batchDraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-render every device. Konva bakes colours in at draw time rather than
|
||||||
|
// reading CSS, so a theme change needs an explicit repaint.
|
||||||
|
repaintAll() {
|
||||||
|
[...this.deviceData.values()].forEach((d) => this.updateDevice(d));
|
||||||
|
this.stage.batchDraw();
|
||||||
|
}
|
||||||
|
|
||||||
clearSelection() {
|
clearSelection() {
|
||||||
this._netHighlightIds.forEach(id => this._wireHighlight(id, false));
|
this._netHighlightIds.forEach(id => this._wireHighlight(id, false));
|
||||||
this._netHighlightIds.clear();
|
this._netHighlightIds.clear();
|
||||||
@@ -1026,26 +1033,10 @@ class DiagramCanvas {
|
|||||||
|
|
||||||
// ── Device rendering ──────────────────────────────────────────────────────────
|
// ── Device rendering ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Fills, outline and label colours come from the theme so light mode can
|
||||||
|
// re-tint them; see theme.js. Konva cannot read CSS custom properties.
|
||||||
_deviceFill(type) {
|
_deviceFill(type) {
|
||||||
return {
|
return Theme.deviceFill(type);
|
||||||
connector: "#12253a",
|
|
||||||
terminal_block: "#122a1a",
|
|
||||||
component: "#1e1230",
|
|
||||||
splice: "#2a1e10",
|
|
||||||
label: "#22220e",
|
|
||||||
fuse: "#2a1c08",
|
|
||||||
relay: "#0a1628",
|
|
||||||
switch: "#0a2218",
|
|
||||||
bulb: "#24220a",
|
|
||||||
motor: "#1a0a28",
|
|
||||||
diode: "#28081a",
|
|
||||||
resistor: "#1a1a08",
|
|
||||||
capacitor: "#081a1a",
|
|
||||||
ground: "#0e140e",
|
|
||||||
power: "#1a0808",
|
|
||||||
cable: "#1a1a1a",
|
|
||||||
group: "rgba(40,40,80,0.35)",
|
|
||||||
}[type] || "#1e1e2e";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderDevice(device) {
|
_renderDevice(device) {
|
||||||
@@ -1072,7 +1063,7 @@ class DiagramCanvas {
|
|||||||
group.add(new Konva.Text({
|
group.add(new Konva.Text({
|
||||||
name: "device-label",
|
name: "device-label",
|
||||||
x: 8, y: 4, width: device.width - 16,
|
x: 8, y: 4, width: device.width - 16,
|
||||||
text: device.label, fontSize, fontFamily: "monospace", fill: "#ffffff",
|
text: device.label, fontSize, fontFamily: "monospace", fill: Theme.deviceText(),
|
||||||
fontStyle: "bold", listening: false,
|
fontStyle: "bold", listening: false,
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
@@ -1080,7 +1071,7 @@ class DiagramCanvas {
|
|||||||
const rect = new Konva.Rect({
|
const rect = new Konva.Rect({
|
||||||
name: "device-rect",
|
name: "device-rect",
|
||||||
width: device.width, height: device.height,
|
width: device.width, height: device.height,
|
||||||
fill: this._deviceFill(device.device_type), stroke: "#5a5a8a", strokeWidth: 2, cornerRadius: 4,
|
fill: this._deviceFill(device.device_type), stroke: Theme.deviceStroke(), strokeWidth: 2, cornerRadius: 4,
|
||||||
});
|
});
|
||||||
group.add(rect);
|
group.add(rect);
|
||||||
|
|
||||||
@@ -1089,7 +1080,7 @@ class DiagramCanvas {
|
|||||||
group.add(new Konva.Text({
|
group.add(new Konva.Text({
|
||||||
name: "device-label",
|
name: "device-label",
|
||||||
x: 8, y: 8, width: device.width - 16, height: device.height - 16,
|
x: 8, y: 8, width: device.width - 16, height: device.height - 16,
|
||||||
text: device.label, fontSize, fontFamily: "monospace", fill: "#dde0f5",
|
text: device.label, fontSize, fontFamily: "monospace", fill: Theme.deviceText(),
|
||||||
align: "left", verticalAlign: "top", wrap: "word",
|
align: "left", verticalAlign: "top", wrap: "word",
|
||||||
}));
|
}));
|
||||||
} else if (device.device_type === "cable") {
|
} else if (device.device_type === "cable") {
|
||||||
@@ -1160,25 +1151,25 @@ class DiagramCanvas {
|
|||||||
// Header jacket bar
|
// Header jacket bar
|
||||||
group.add(new Konva.Rect({ x: 2, y: 2, width: device.width - 4, height: 22, fill: jacket, cornerRadius: [3,3,0,0] }));
|
group.add(new Konva.Rect({ x: 2, y: 2, width: device.width - 4, height: 22, fill: jacket, cornerRadius: [3,3,0,0] }));
|
||||||
if (device.reference) {
|
if (device.reference) {
|
||||||
group.add(new Konva.Text({ x: 6, y: 6, text: device.reference, fontSize: 9, fontFamily: "monospace", fill: "#99aaee", fontStyle: "bold" }));
|
group.add(new Konva.Text({ x: 6, y: 6, text: device.reference, fontSize: 9, fontFamily: "monospace", fill: Theme.deviceRef(), fontStyle: "bold" }));
|
||||||
}
|
}
|
||||||
group.add(new Konva.Text({
|
group.add(new Konva.Text({
|
||||||
name: "device-label",
|
name: "device-label",
|
||||||
x: 4, y: 6, width: device.width - 8, align: "center",
|
x: 4, y: 6, width: device.width - 8, align: "center",
|
||||||
text: device.label, fontSize: 10, fontFamily: "monospace", fill: "#dde0f5",
|
text: device.label, fontSize: 10, fontFamily: "monospace", fill: Theme.deviceText(),
|
||||||
}));
|
}));
|
||||||
// Conductor rows
|
// Conductor rows
|
||||||
conductors.forEach((cond, i) => {
|
conductors.forEach((cond, i) => {
|
||||||
const rowY = 26 + i * 24;
|
const rowY = 26 + i * 24;
|
||||||
const color = cond.color || "#888888";
|
const color = cond.color || "#888888";
|
||||||
group.add(new Konva.Rect({ x: 1, y: rowY, width: device.width - 2, height: 24, fill: "#0e0e1a" }));
|
group.add(new Konva.Rect({ x: 1, y: rowY, width: device.width - 2, height: 24, fill: Theme.rowBg() }));
|
||||||
group.add(new Konva.Line({
|
group.add(new Konva.Line({
|
||||||
points: [14, rowY + 12, device.width - 14, rowY + 12],
|
points: [14, rowY + 12, device.width - 14, rowY + 12],
|
||||||
stroke: color, strokeWidth: 5, lineCap: "round", listening: false,
|
stroke: color, strokeWidth: 5, lineCap: "round", listening: false,
|
||||||
}));
|
}));
|
||||||
group.add(new Konva.Text({
|
group.add(new Konva.Text({
|
||||||
x: 16, y: rowY + 4, text: cond.name || String(i + 1),
|
x: 16, y: rowY + 4, text: cond.name || String(i + 1),
|
||||||
fontSize: 9, fontFamily: "monospace", fill: "#c8ccee", listening: false,
|
fontSize: 9, fontFamily: "monospace", fill: Theme.deviceText(), listening: false,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
// Footer jacket bar
|
// Footer jacket bar
|
||||||
@@ -1186,21 +1177,43 @@ class DiagramCanvas {
|
|||||||
group.add(new Konva.Text({
|
group.add(new Konva.Text({
|
||||||
name: "device-type",
|
name: "device-type",
|
||||||
x: 4, y: device.height - 14, width: device.width - 8, align: "right",
|
x: 4, y: device.height - 14, width: device.width - 8, align: "right",
|
||||||
text: "cable", fontSize: 8, fontFamily: "monospace", fill: "#888888",
|
text: "cable", fontSize: 8, fontFamily: "monospace", fill: Theme.deviceSubtext(),
|
||||||
|
}));
|
||||||
|
} else if (device.device_type === "connector" && device.properties?.shape === "circular") {
|
||||||
|
rect.visible(false);
|
||||||
|
// Body radius is intentionally smaller than pin radius (0.471) so wire
|
||||||
|
// endpoints sit outside the body and remain visible above it.
|
||||||
|
const r = Math.min(device.width, device.height) * 0.386;
|
||||||
|
const cx = device.width / 2, cy = device.height / 2;
|
||||||
|
group.add(new Konva.Circle({
|
||||||
|
name: "device-body",
|
||||||
|
x: cx, y: cy, radius: r,
|
||||||
|
fill: this._deviceFill("connector"), stroke: Theme.deviceStroke(), strokeWidth: 2,
|
||||||
|
}));
|
||||||
|
// Key notch anchored to body top edge
|
||||||
|
group.add(new Konva.Rect({
|
||||||
|
x: cx - 5, y: cy - r - 3, width: 10, height: 7, cornerRadius: 2,
|
||||||
|
fill: Theme.notch(), stroke: Theme.deviceStroke(), strokeWidth: 1, listening: false,
|
||||||
|
}));
|
||||||
|
group.add(new Konva.Text({
|
||||||
|
name: "device-label",
|
||||||
|
x: 4, y: cy - 6, width: device.width - 8,
|
||||||
|
text: device.label, fontSize: Math.min(10, fontSize),
|
||||||
|
fontFamily: "monospace", fill: Theme.deviceText(), align: "center",
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
if (device.reference) {
|
if (device.reference) {
|
||||||
group.add(new Konva.Text({ x: 6, y: 5, text: device.reference, fontSize: 10, fontFamily: "monospace", fill: "#99aaee", fontStyle: "bold" }));
|
group.add(new Konva.Text({ x: 6, y: 5, text: device.reference, fontSize: 10, fontFamily: "monospace", fill: Theme.deviceRef(), fontStyle: "bold" }));
|
||||||
}
|
}
|
||||||
group.add(new Konva.Text({
|
group.add(new Konva.Text({
|
||||||
name: "device-label",
|
name: "device-label",
|
||||||
x: 4, y: device.height / 2 - fontSize / 2 - 2, width: device.width - 8, align: "center",
|
x: 4, y: device.height / 2 - fontSize / 2 - 2, width: device.width - 8, align: "center",
|
||||||
text: device.label, fontSize, fontFamily: "monospace", fill: "#dde0f5", wrap: "word",
|
text: device.label, fontSize, fontFamily: "monospace", fill: Theme.deviceText(), wrap: "word",
|
||||||
}));
|
}));
|
||||||
group.add(new Konva.Text({
|
group.add(new Konva.Text({
|
||||||
name: "device-type",
|
name: "device-type",
|
||||||
x: 4, y: device.height - 14, width: device.width - 8, align: "right",
|
x: 4, y: device.height - 14, width: device.width - 8, align: "right",
|
||||||
text: device.device_type, fontSize: 8, fontFamily: "monospace", fill: "#444466",
|
text: device.device_type, fontSize: 8, fontFamily: "monospace", fill: Theme.deviceSubtext(),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} // end non-group else
|
} // end non-group else
|
||||||
@@ -1263,6 +1276,12 @@ class DiagramCanvas {
|
|||||||
this.selectDevice(device.id);
|
this.selectDevice(device.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
group.on("dblclick", (e) => {
|
||||||
|
if (this.mode !== "select") return;
|
||||||
|
e.cancelBubble = true;
|
||||||
|
this.cb.onPinoutRequested?.(device);
|
||||||
|
});
|
||||||
|
|
||||||
group.on("dragstart", () => {
|
group.on("dragstart", () => {
|
||||||
if (this.wireStart) { group.stopDrag(); return; }
|
if (this.wireStart) { group.stopDrag(); return; }
|
||||||
if (device.properties?.locked) { group.stopDrag(); return; }
|
if (device.properties?.locked) { group.stopDrag(); return; }
|
||||||
@@ -1463,7 +1482,7 @@ class DiagramCanvas {
|
|||||||
_addPin(group, device, pin) {
|
_addPin(group, device, pin) {
|
||||||
const circle = new Konva.Circle({
|
const circle = new Konva.Circle({
|
||||||
x: pin.x_offset, y: pin.y_offset, radius: 5,
|
x: pin.x_offset, y: pin.y_offset, radius: 5,
|
||||||
fill: "#0a0f1a", stroke: "#5566aa", strokeWidth: 1.5,
|
fill: Theme.pinFill(), stroke: Theme.pinStroke(), strokeWidth: 1.5,
|
||||||
});
|
});
|
||||||
const hit = new Konva.Circle({
|
const hit = new Konva.Circle({
|
||||||
x: pin.x_offset, y: pin.y_offset, radius: 14,
|
x: pin.x_offset, y: pin.y_offset, radius: 14,
|
||||||
@@ -1471,29 +1490,46 @@ class DiagramCanvas {
|
|||||||
});
|
});
|
||||||
hit._pinMeta = { deviceId: device.id, pinId: pin.id, side: pin.side };
|
hit._pinMeta = { deviceId: device.id, pinId: pin.id, side: pin.side };
|
||||||
|
|
||||||
// Position label inside the device body, clear of the pin circle (r=5, gap=3 → offset 8)
|
// Pin labels scale with the device font rather than sitting at a fixed 8px,
|
||||||
|
// so raising the font size makes the whole device readable, not just its
|
||||||
|
// title. The ratios below reproduce the previous look exactly at the
|
||||||
|
// default size of 12.
|
||||||
|
const baseFont = device.properties?.fontSize || 12;
|
||||||
|
const pinFont = Math.max(6, Math.round(baseFont * 0.7));
|
||||||
const GAP = 8;
|
const GAP = 8;
|
||||||
let lx, ly, lw, la;
|
const lw = Math.round(pinFont * 2.5);
|
||||||
|
const vc = Math.round(pinFont * 0.62); // vertical centring on the pin
|
||||||
|
let lx, ly, la;
|
||||||
switch (pin.side) {
|
switch (pin.side) {
|
||||||
case "right":
|
case "right":
|
||||||
lx = pin.x_offset - 28; ly = pin.y_offset - 5; lw = 20; la = "right"; break;
|
lx = pin.x_offset - (GAP + lw); ly = pin.y_offset - vc; la = "right"; break;
|
||||||
case "top":
|
case "top":
|
||||||
lx = pin.x_offset - 10; ly = pin.y_offset + GAP; lw = 20; la = "center"; break;
|
lx = pin.x_offset - lw / 2; ly = pin.y_offset + GAP; la = "center"; break;
|
||||||
case "bottom":
|
case "bottom":
|
||||||
lx = pin.x_offset - 10; ly = pin.y_offset - 13; lw = 20; la = "center"; break;
|
lx = pin.x_offset - lw / 2; ly = pin.y_offset - GAP - vc; la = "center"; break;
|
||||||
default: // left
|
default: // left
|
||||||
lx = pin.x_offset + GAP; ly = pin.y_offset - 5; lw = 20; la = "left";
|
lx = pin.x_offset + GAP; ly = pin.y_offset - vc; la = "left";
|
||||||
|
}
|
||||||
|
// Pass-through devices (bulkheads) carry the same signal on both faces, so
|
||||||
|
// one pin of the pair draws the name centred between them and the other
|
||||||
|
// draws nothing — one label per circuit rather than two.
|
||||||
|
if (!pin.hide_label) {
|
||||||
|
const centred = !!pin.center_label;
|
||||||
|
group.add(new Konva.Text({
|
||||||
|
x: centred ? 0 : lx,
|
||||||
|
y: centred ? pin.y_offset - vc : ly,
|
||||||
|
width: centred ? device.width : lw,
|
||||||
|
align: centred ? "center" : la,
|
||||||
|
text: pin.name, fontSize: pinFont, fontFamily: "monospace",
|
||||||
|
fill: Theme.deviceSubtext(), listening: false,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
group.add(new Konva.Text({
|
|
||||||
x: lx, y: ly, width: lw, align: la,
|
|
||||||
text: pin.name, fontSize: 8, fontFamily: "monospace", fill: "#556688",
|
|
||||||
}));
|
|
||||||
group.add(circle);
|
group.add(circle);
|
||||||
group.add(hit);
|
group.add(hit);
|
||||||
|
|
||||||
const highlightPin = (active) => {
|
const highlightPin = (active) => {
|
||||||
circle.fill(active ? "#003300" : "#0a0f1a");
|
circle.fill(active ? "#003300" : Theme.pinFill());
|
||||||
circle.stroke(active ? "#00dd00" : "#5566aa");
|
circle.stroke(active ? "#00dd00" : Theme.pinStroke());
|
||||||
circle.radius(active ? 7 : 5);
|
circle.radius(active ? 7 : 5);
|
||||||
this.deviceLayer.batchDraw();
|
this.deviceLayer.batchDraw();
|
||||||
};
|
};
|
||||||
@@ -1657,7 +1693,7 @@ class DiagramCanvas {
|
|||||||
lbl = new Konva.Text({
|
lbl = new Konva.Text({
|
||||||
x: mx + 3, y: my - 10,
|
x: mx + 3, y: my - 10,
|
||||||
text: labelText,
|
text: labelText,
|
||||||
fontSize: 9, fontFamily: "monospace", fill: "#aabbcc", listening: false,
|
fontSize: 9, fontFamily: "monospace", fill: Theme.wireLabel(), listening: false,
|
||||||
shadowColor: "#000", shadowBlur: 3, shadowOpacity: 0.8,
|
shadowColor: "#000", shadowBlur: 3, shadowOpacity: 0.8,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,6 +130,20 @@ const CONNECTOR_LIBRARY = {
|
|||||||
{ id: "can-j1939", name: "SAE J1939 9-pin", manufacturer: "Amphenol", partNumber: "HD10-9-1939P", pinCount: 9, description: "J1939 CAN heavy-vehicle datalink", pinLabels: ["−Battery","+Battery","GND","CAN H","Shield","GND","CAN L","NC","NC"] },
|
{ id: "can-j1939", name: "SAE J1939 9-pin", manufacturer: "Amphenol", partNumber: "HD10-9-1939P", pinCount: 9, description: "J1939 CAN heavy-vehicle datalink", pinLabels: ["−Battery","+Battery","GND","CAN H","Shield","GND","CAN L","NC","NC"] },
|
||||||
{ id: "can-obd2", name: "OBD-II 16-pin", manufacturer: "Standard", partNumber: "SAE J1962", pinCount: 16, description: "On-board diagnostics port", pinLabels: ["Mfr","Mfr","GND","Mfr","Sig GND","CAN H","K-Line","Mfr","Bat+","Mfr","Mfr","Mfr","Mfr","CAN L","L-Line","OBD+"] },
|
{ id: "can-obd2", name: "OBD-II 16-pin", manufacturer: "Standard", partNumber: "SAE J1962", pinCount: 16, description: "On-board diagnostics port", pinLabels: ["Mfr","Mfr","GND","Mfr","Sig GND","CAN H","K-Line","Mfr","Bat+","Mfr","Mfr","Mfr","Mfr","CAN L","L-Line","OBD+"] },
|
||||||
],
|
],
|
||||||
|
"Deutsch DRC (Round)": [
|
||||||
|
{
|
||||||
|
id: "drc-30p", name: "DRC04-30P", manufacturer: "TE Connectivity",
|
||||||
|
partNumber: "DRC04-30P-A191", pinCount: 30, shape: "circular",
|
||||||
|
pinRings: [14, 10, 6],
|
||||||
|
description: "30-pin circular environmental plug (pin contacts), IP67, 12–20 AWG",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "drc-30s", name: "DRC06-30S", manufacturer: "TE Connectivity",
|
||||||
|
partNumber: "DRC06-30S-A191", pinCount: 30, shape: "circular",
|
||||||
|
pinRings: [14, 10, 6],
|
||||||
|
description: "30-pin circular environmental socket (receptacle), IP67, 12–20 AWG",
|
||||||
|
},
|
||||||
|
],
|
||||||
"General Automotive / EV": [
|
"General Automotive / EV": [
|
||||||
{ id: "ga-motor3", name: "Motor Phase 3-pin", manufacturer: "varies", partNumber: "varies", pinCount: 3, description: "3-phase motor connections", pinLabels: ["U","V","W"] },
|
{ id: "ga-motor3", name: "Motor Phase 3-pin", manufacturer: "varies", partNumber: "varies", pinCount: 3, description: "3-phase motor connections", pinLabels: ["U","V","W"] },
|
||||||
{ id: "ga-res2", name: "Resolver 2-pin", manufacturer: "varies", partNumber: "varies", pinCount: 2, description: "Motor resolver position sensor", pinLabels: ["SIN","COS"] },
|
{ id: "ga-res2", name: "Resolver 2-pin", manufacturer: "varies", partNumber: "varies", pinCount: 2, description: "Motor resolver position sensor", pinLabels: ["SIN","COS"] },
|
||||||
@@ -155,15 +169,121 @@ function connectorToDevice(connId, diagramId) {
|
|||||||
if (!conn) return null;
|
if (!conn) return null;
|
||||||
|
|
||||||
const pinCount = conn.pinCount || 2;
|
const pinCount = conn.pinCount || 2;
|
||||||
const w = 120;
|
|
||||||
const h = Math.max(60, pinCount * 18 + 20);
|
if (conn.shape === "circular") {
|
||||||
const pins = Array.from({ length: pinCount }, (_, i) => ({
|
const size = 140;
|
||||||
id: `pin_${i + 1}`,
|
const cx = size / 2, cy = size / 2;
|
||||||
name: conn.pinLabels ? (conn.pinLabels[i] || String(i + 1)) : String(i + 1),
|
const pinR = size * 0.471; // pins outside the body circle (body uses 0.386 ratio)
|
||||||
side: "right",
|
|
||||||
x_offset: w,
|
const pins = Array.from({ length: pinCount }, (_, i) => {
|
||||||
y_offset: ((i + 1) / (pinCount + 1)) * h,
|
const angle = -Math.PI / 2 + (2 * Math.PI * i / pinCount);
|
||||||
}));
|
const x = cx + pinR * Math.cos(angle);
|
||||||
|
const y = cy + pinR * Math.sin(angle);
|
||||||
|
let side;
|
||||||
|
if (angle >= -3 * Math.PI / 4 && angle < -Math.PI / 4) side = "top";
|
||||||
|
else if (angle >= -Math.PI / 4 && angle < Math.PI / 4) side = "right";
|
||||||
|
else if (angle >= Math.PI / 4 && angle < 3 * Math.PI / 4) side = "bottom";
|
||||||
|
else side = "left";
|
||||||
|
return {
|
||||||
|
id: `pin_${i + 1}`,
|
||||||
|
name: conn.pinLabels ? (conn.pinLabels[i] || String(i + 1)) : String(i + 1),
|
||||||
|
side,
|
||||||
|
x_offset: Math.round(x * 10) / 10,
|
||||||
|
y_offset: Math.round(y * 10) / 10,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
diagram_id: diagramId,
|
||||||
|
device_type: "connector",
|
||||||
|
label: conn.name,
|
||||||
|
reference: "",
|
||||||
|
x: 200, y: 200,
|
||||||
|
width: size, height: size,
|
||||||
|
properties: {
|
||||||
|
pinCount,
|
||||||
|
shape: "circular",
|
||||||
|
pinRings: conn.pinRings || [],
|
||||||
|
orientation: "right",
|
||||||
|
partNumber: conn.partNumber || "",
|
||||||
|
manufacturer: conn.manufacturer || "",
|
||||||
|
connectorLibraryId: connId,
|
||||||
|
},
|
||||||
|
pins,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard rectangular connector.
|
||||||
|
// Parts carrying pinSpecs (see partsLibrary.js) get their standard wire
|
||||||
|
// function stamped onto each pin, so wires drawn off them always come out the
|
||||||
|
// same colour, stripe and gauge.
|
||||||
|
const specs = conn.pinSpecs || null;
|
||||||
|
|
||||||
|
// Pass-through parts (bulkheads) present both faces of the same connector as
|
||||||
|
// one device: every circuit gets an IN pin on the left and an OUT pin on the
|
||||||
|
// right at the same height, sharing a single centred label. Wiring both sides
|
||||||
|
// of a bulkhead then needs one device, not a mated pair.
|
||||||
|
if (conn.passThrough) {
|
||||||
|
const w = 240;
|
||||||
|
const h = Math.max(60, pinCount * 18 + 24);
|
||||||
|
const pins = [];
|
||||||
|
(specs || Array.from({ length: pinCount })).forEach((spec, i) => {
|
||||||
|
const y = ((i + 1) / (pinCount + 1)) * h;
|
||||||
|
const base = {
|
||||||
|
name: spec ? spec.name : String(i + 1),
|
||||||
|
pin_number: spec ? spec.pin : String(i + 1),
|
||||||
|
wire_fn: spec ? spec.fn : null,
|
||||||
|
wire_oem: spec && spec.oem ? spec.oem : null,
|
||||||
|
note: spec ? spec.note : "",
|
||||||
|
};
|
||||||
|
pins.push({ ...base, id: `pin_${i + 1}_in`, side: "left", x_offset: 0, y_offset: y, center_label: true });
|
||||||
|
pins.push({ ...base, id: `pin_${i + 1}_out`, side: "right", x_offset: w, y_offset: y, hide_label: true });
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
diagram_id: diagramId,
|
||||||
|
device_type: "connector",
|
||||||
|
label: conn.name,
|
||||||
|
reference: "",
|
||||||
|
x: 200, y: 200,
|
||||||
|
width: w, height: h,
|
||||||
|
properties: {
|
||||||
|
pinCount,
|
||||||
|
orientation: "right",
|
||||||
|
passThrough: true,
|
||||||
|
partNumber: conn.partNumber || "",
|
||||||
|
manufacturer: conn.manufacturer || "",
|
||||||
|
connectorLibraryId: connId,
|
||||||
|
standardPart: !!specs,
|
||||||
|
verifyPinout: !!conn.verify,
|
||||||
|
},
|
||||||
|
pins,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wide parts get two columns so a 20- or 32-way body stays readable.
|
||||||
|
const split = pinCount > 12;
|
||||||
|
const perSide = split ? Math.ceil(pinCount / 2) : pinCount;
|
||||||
|
const w = split ? 200 : 120;
|
||||||
|
const h = Math.max(60, perSide * 18 + 20);
|
||||||
|
|
||||||
|
const pins = Array.from({ length: pinCount }, (_, i) => {
|
||||||
|
const spec = specs ? specs[i] : null;
|
||||||
|
const onRight = !split || i >= perSide;
|
||||||
|
const idx = onRight && split ? i - perSide : i;
|
||||||
|
const count = onRight && split ? pinCount - perSide : perSide;
|
||||||
|
return {
|
||||||
|
id: `pin_${i + 1}`,
|
||||||
|
name: spec ? spec.name : (conn.pinLabels ? (conn.pinLabels[i] || String(i + 1)) : String(i + 1)),
|
||||||
|
side: onRight ? "right" : "left",
|
||||||
|
x_offset: onRight ? w : 0,
|
||||||
|
y_offset: ((idx + 1) / (count + 1)) * h,
|
||||||
|
// Standard-parts metadata — consumed by createWire() and the props panel.
|
||||||
|
pin_number: spec ? spec.pin : String(i + 1),
|
||||||
|
wire_fn: spec ? spec.fn : null,
|
||||||
|
wire_oem: spec && spec.oem ? spec.oem : null,
|
||||||
|
note: spec ? spec.note : "",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
diagram_id: diagramId,
|
diagram_id: diagramId,
|
||||||
@@ -178,6 +298,8 @@ function connectorToDevice(connId, diagramId) {
|
|||||||
partNumber: conn.partNumber || "",
|
partNumber: conn.partNumber || "",
|
||||||
manufacturer: conn.manufacturer || "",
|
manufacturer: conn.manufacturer || "",
|
||||||
connectorLibraryId: connId,
|
connectorLibraryId: connId,
|
||||||
|
standardPart: !!specs,
|
||||||
|
verifyPinout: !!conn.verify,
|
||||||
},
|
},
|
||||||
pins,
|
pins,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -80,9 +80,11 @@ class FormboardCanvas {
|
|||||||
const el = this.stage?.container()?.parentElement;
|
const el = this.stage?.container()?.parentElement;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
const w = el.clientWidth, h = el.clientHeight;
|
const w = el.clientWidth, h = el.clientHeight;
|
||||||
|
if (!w || !h) return;
|
||||||
this.stage.width(w); this.stage.height(h);
|
this.stage.width(w); this.stage.height(h);
|
||||||
this._bg.width(w); this._bg.height(h);
|
this._bg.width(w); this._bg.height(h);
|
||||||
this.bgLayer.batchDraw();
|
this.bgLayer.batchDraw();
|
||||||
|
this._applyTransform();
|
||||||
}
|
}
|
||||||
|
|
||||||
_s2w(sx, sy) { return { x: (sx - this.offsetX) / this.scale, y: (sy - this.offsetY) / this.scale }; }
|
_s2w(sx, sy) { return { x: (sx - this.offsetX) / this.scale, y: (sy - this.offsetY) / this.scale }; }
|
||||||
@@ -100,7 +102,7 @@ class FormboardCanvas {
|
|||||||
setMode(mode) {
|
setMode(mode) {
|
||||||
this.mode = mode;
|
this.mode = mode;
|
||||||
if (mode !== "connect") this._cancelConnect();
|
if (mode !== "connect") this._cancelConnect();
|
||||||
this.stage?.container().style.cursor =
|
if (this.stage) this.stage.container().style.cursor =
|
||||||
["add-branch", "add-splice", "connect"].includes(mode) ? "crosshair" : "default";
|
["add-branch", "add-splice", "connect"].includes(mode) ? "crosshair" : "default";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,8 +225,8 @@ class FormboardCanvas {
|
|||||||
this.segKonva.clear(); this.segData.clear();
|
this.segKonva.clear(); this.segData.clear();
|
||||||
this.selectedId = null; this.selectedType = null;
|
this.selectedId = null; this.selectedType = null;
|
||||||
|
|
||||||
(data.segments || []).forEach(s => { this.segData.set(s.id, s); this._renderSeg(s); });
|
|
||||||
(data.nodes || []).forEach(n => { this.nodeData.set(n.id, n); this._renderNode(n); });
|
(data.nodes || []).forEach(n => { this.nodeData.set(n.id, n); this._renderNode(n); });
|
||||||
|
(data.segments || []).forEach(s => { this.segData.set(s.id, s); this._renderSeg(s); });
|
||||||
this.segLayer.batchDraw(); this.nodeLayer.batchDraw();
|
this.segLayer.batchDraw(); this.nodeLayer.batchDraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,9 +378,11 @@ class FormboardCanvas {
|
|||||||
|
|
||||||
fitView() {
|
fitView() {
|
||||||
if (!this.nodeData.size) return;
|
if (!this.nodeData.size) return;
|
||||||
|
const sw = this.stage.width(), sh = this.stage.height();
|
||||||
|
if (!sw || !sh) return;
|
||||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||||
this.nodeData.forEach(n => { minX = Math.min(minX, n.x); minY = Math.min(minY, n.y); maxX = Math.max(maxX, n.x); maxY = Math.max(maxY, n.y); });
|
this.nodeData.forEach(n => { minX = Math.min(minX, n.x); minY = Math.min(minY, n.y); maxX = Math.max(maxX, n.x); maxY = Math.max(maxY, n.y); });
|
||||||
const pad = 120, sw = this.stage.width(), sh = this.stage.height();
|
const pad = 120;
|
||||||
this.scale = Math.min(sw / (maxX - minX + pad * 2), sh / (maxY - minY + pad * 2), 2);
|
this.scale = Math.min(sw / (maxX - minX + pad * 2), sh / (maxY - minY + pad * 2), 2);
|
||||||
this.offsetX = sw / 2 - ((minX + maxX) / 2) * this.scale;
|
this.offsetX = sw / 2 - ((minX + maxX) / 2) * this.scale;
|
||||||
this.offsetY = sh / 2 - ((minY + maxY) / 2) * this.scale;
|
this.offsetY = sh / 2 - ((minY + maxY) / 2) * this.scale;
|
||||||
|
|||||||
@@ -0,0 +1,558 @@
|
|||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Skudak standard parts + wire colour standard
|
||||||
|
//
|
||||||
|
// Loaded AFTER connectorLibrary.js. Merges standard parts into CONNECTOR_LIBRARY
|
||||||
|
// so they appear in the existing library panel, search and drag-to-canvas with
|
||||||
|
// no UI changes.
|
||||||
|
//
|
||||||
|
// The point of this file: every pin on a standard part declares the wire that
|
||||||
|
// belongs on it. Drag a wire off VCU GRAY pin 3 and it is Red/White 14 AWG every
|
||||||
|
// time, on every build, without anyone remembering to set it.
|
||||||
|
//
|
||||||
|
// Wire stock: ACDC Wire Supply TXL. Solids plus the 16 stocked stripe combos.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// ── TXL solid colours ────────────────────────────────────────────────────────
|
||||||
|
const TXL_SOLID = {
|
||||||
|
black: { name: "Black", hex: "#1A1A1A" },
|
||||||
|
brown: { name: "Brown", hex: "#6B3F1D" },
|
||||||
|
red: { name: "Red", hex: "#CC0000" },
|
||||||
|
orange: { name: "Orange", hex: "#FF8C00" },
|
||||||
|
yellow: { name: "Yellow", hex: "#FFD700" },
|
||||||
|
green: { name: "Green", hex: "#007700" },
|
||||||
|
darkGreen: { name: "Dark Green", hex: "#14532D" },
|
||||||
|
lightGreen: { name: "Light Green", hex: "#7CB342" },
|
||||||
|
blue: { name: "Blue", hex: "#0000CC" },
|
||||||
|
lightBlue: { name: "Light Blue", hex: "#4FA8DC" },
|
||||||
|
purple: { name: "Purple", hex: "#7B00CC" },
|
||||||
|
grey: { name: "Grey", hex: "#808080" },
|
||||||
|
white: { name: "White", hex: "#E8E8E8" },
|
||||||
|
pink: { name: "Pink", hex: "#FF69B4" },
|
||||||
|
tan: { name: "Tan", hex: "#D2B48C" },
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── TXL stocked stripe combinations ──────────────────────────────────────────
|
||||||
|
// Exactly the 16 combos carried by ACDC. Nothing outside this list is buildable.
|
||||||
|
const TXL_STRIPED = {
|
||||||
|
whiteRed: { name: "White / Red stripe", hex: "#E8E8E8", stripe: "#CC0000" },
|
||||||
|
whiteBlue: { name: "White / Blue stripe", hex: "#E8E8E8", stripe: "#0000CC" },
|
||||||
|
pinkGreen: { name: "Pink / Green stripe", hex: "#FF69B4", stripe: "#007700" },
|
||||||
|
tanBlack: { name: "Tan / Black stripe", hex: "#D2B48C", stripe: "#1A1A1A" },
|
||||||
|
lightGreenDark: { name: "Light Green / Dark Green stripe", hex: "#7CB342", stripe: "#14532D" },
|
||||||
|
yellowRed: { name: "Yellow / Red stripe", hex: "#FFD700", stripe: "#CC0000" },
|
||||||
|
orangeBlack: { name: "Orange / Black stripe", hex: "#FF8C00", stripe: "#1A1A1A" },
|
||||||
|
brownWhite: { name: "Brown / White stripe", hex: "#6B3F1D", stripe: "#E8E8E8" },
|
||||||
|
greenWhite: { name: "Green / White stripe", hex: "#007700", stripe: "#E8E8E8" },
|
||||||
|
purpleRed: { name: "Purple / Red stripe", hex: "#7B00CC", stripe: "#CC0000" },
|
||||||
|
lightBlueWhite: { name: "Light Blue / White stripe", hex: "#4FA8DC", stripe: "#E8E8E8" },
|
||||||
|
blackWhite: { name: "Black / White stripe", hex: "#1A1A1A", stripe: "#E8E8E8" },
|
||||||
|
blackRed: { name: "Black / Red stripe", hex: "#1A1A1A", stripe: "#CC0000" },
|
||||||
|
redBlack: { name: "Red / Black stripe", hex: "#CC0000", stripe: "#1A1A1A" },
|
||||||
|
blackYellow: { name: "Black / Yellow stripe", hex: "#1A1A1A", stripe: "#FFD700" },
|
||||||
|
redWhite: { name: "Red / White stripe", hex: "#CC0000", stripe: "#E8E8E8" },
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Function → wire standard ─────────────────────────────────────────────────
|
||||||
|
// This is the standard. A pin names a function; the function fixes the colour,
|
||||||
|
// stripe and gauge. Change it here and every build follows.
|
||||||
|
const WIRE_STANDARD = {
|
||||||
|
// Power. Brown is ground, matching the MEB loom, the Land Rover build and
|
||||||
|
// European practice — an in-house ground lands next to an OEM ground at the
|
||||||
|
// battery, so the two must not disagree.
|
||||||
|
KL30: { label: "Permanent 12 V (KL30)", ...TXL_SOLID.red, gauge: "14 AWG" },
|
||||||
|
KL15: { label: "Switched 12 V (KL15)", ...TXL_STRIPED.redBlack, gauge: "16 AWG" },
|
||||||
|
LOAD_12V: { label: "VCU-switched load feed", ...TXL_STRIPED.redWhite, gauge: "14 AWG" },
|
||||||
|
GND: { label: "Chassis ground", ...TXL_SOLID.brown, gauge: "14 AWG" },
|
||||||
|
GND_SIG: { label: "Signal ground", ...TXL_STRIPED.brownWhite, gauge: "18 AWG" },
|
||||||
|
V5_REF: { label: "5 V sensor reference", ...TXL_STRIPED.pinkGreen, gauge: "20 AWG" },
|
||||||
|
|
||||||
|
// Networks — CAN pairs are twisted
|
||||||
|
CAN1_H: { label: "CAN1 High (powertrain)", ...TXL_SOLID.yellow, gauge: "20 AWG", twisted: true },
|
||||||
|
CAN1_L: { label: "CAN1 Low (powertrain)", ...TXL_SOLID.green, gauge: "20 AWG", twisted: true },
|
||||||
|
CAN2_H: { label: "CAN2 High (HV pack)", ...TXL_STRIPED.yellowRed, gauge: "20 AWG", twisted: true },
|
||||||
|
CAN2_L: { label: "CAN2 Low (HV pack)", ...TXL_STRIPED.greenWhite, gauge: "20 AWG", twisted: true },
|
||||||
|
CAN3_H: { label: "CAN3 High (charge/body)", ...TXL_STRIPED.whiteBlue, gauge: "20 AWG", twisted: true },
|
||||||
|
CAN3_L: { label: "CAN3 Low (charge/body)", ...TXL_STRIPED.lightBlueWhite, gauge: "20 AWG", twisted: true },
|
||||||
|
LIN: { label: "LIN bus", ...TXL_SOLID.grey, gauge: "20 AWG" },
|
||||||
|
SHIELD: { label: "Shield / drain", ...TXL_STRIPED.blackYellow, gauge: "20 AWG" },
|
||||||
|
|
||||||
|
// Safety. HVIL is violet, not orange — orange is reserved (see below) so the
|
||||||
|
// interlock can never be mistaken for a live AC mains or OEM HV run.
|
||||||
|
HVIL_OUT: { label: "HVIL drive", ...TXL_SOLID.purple, gauge: "18 AWG" },
|
||||||
|
HVIL_RTN: { label: "HVIL return", ...TXL_STRIPED.purpleRed, gauge: "18 AWG" },
|
||||||
|
COIL: { label: "Contactor coil", ...TXL_SOLID.black, gauge: "16 AWG" },
|
||||||
|
WELD: { label: "Weld detect / aux contact", ...TXL_STRIPED.blackWhite, gauge: "20 AWG" },
|
||||||
|
INHIBIT: { label: "Enable / inhibit", ...TXL_STRIPED.blackRed, gauge: "20 AWG" },
|
||||||
|
|
||||||
|
// Signals
|
||||||
|
DIG_IN: { label: "Digital input", ...TXL_STRIPED.whiteRed, gauge: "20 AWG" },
|
||||||
|
DIG_OUT: { label: "Digital output", ...TXL_SOLID.darkGreen, gauge: "18 AWG" },
|
||||||
|
PWM_OUT: { label: "PWM output", ...TXL_SOLID.lightGreen, gauge: "18 AWG" },
|
||||||
|
FREQ_IN: { label: "PWM / frequency input", ...TXL_SOLID.lightBlue, gauge: "20 AWG" },
|
||||||
|
ANALOG_1: { label: "Analog signal 1", ...TXL_STRIPED.tanBlack, gauge: "20 AWG" },
|
||||||
|
ANALOG_2: { label: "Analog signal 2", ...TXL_SOLID.tan, gauge: "20 AWG" },
|
||||||
|
ANALOG_3: { label: "Analog signal 3", ...TXL_STRIPED.lightGreenDark, gauge: "20 AWG" },
|
||||||
|
FAULT: { label: "Fault / lamp output", ...TXL_SOLID.pink, gauge: "18 AWG" },
|
||||||
|
HV_SENSE: { label: "HV instrumentation", ...TXL_SOLID.blue, gauge: "18 AWG" },
|
||||||
|
// Quadrature channel B needs its own colour. Channel A is an ordinary
|
||||||
|
// FREQ_IN, but two identical wires in one shielded encoder bundle is how you
|
||||||
|
// end up with A and B swapped — which reverses the sensed direction of the
|
||||||
|
// motor. White is reclaimed from the old SPARE entry: an unassigned pin
|
||||||
|
// should get no wire at all, not a wire in "spare" colour.
|
||||||
|
ENC_B: { label: "Encoder channel B", ...TXL_SOLID.white, gauge: "20 AWG" },
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Reserved colours ─────────────────────────────────────────────────────────
|
||||||
|
// Deliberately NOT in WIRE_STANDARD, so nothing in-house is ever auto-assigned
|
||||||
|
// them. Orange belongs to AC mains and to OEM high-voltage looms; taking it for
|
||||||
|
// a signal would put a low-voltage wire in the colour that means "this can kill
|
||||||
|
// you". Set these by hand when drawing the runs they describe.
|
||||||
|
const RESERVED_COLOURS = {
|
||||||
|
AC_MAINS: { label: "AC mains (L1 / L2 / AC ground)", ...TXL_SOLID.orange, gauge: "6.0 mm²" },
|
||||||
|
OEM_HV: { label: "OEM high-voltage / HV-CAN", ...TXL_STRIPED.orangeBlack, gauge: "varies" },
|
||||||
|
};
|
||||||
|
|
||||||
|
function wireSpecFor(fnKey) {
|
||||||
|
const s = WIRE_STANDARD[fnKey];
|
||||||
|
if (!s) return null;
|
||||||
|
return {
|
||||||
|
fn: fnKey,
|
||||||
|
color_primary: s.hex,
|
||||||
|
color_stripe: s.stripe || null,
|
||||||
|
gauge: s.gauge,
|
||||||
|
twisted_pair: !!s.twisted,
|
||||||
|
label: s.label,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shorthand: p("3", "GPIOHP1", "LOAD_12V", "Coolant pump 12 V")
|
||||||
|
function p(pin, name, fn, note) {
|
||||||
|
return { pin, name, fn, note: note || "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── OEM harness colours ──────────────────────────────────────────────────────
|
||||||
|
// Where we splice into an existing factory harness the wire colour is not ours
|
||||||
|
// to choose — it has to match what is already in the loom. These override the
|
||||||
|
// WIRE_STANDARD function colours.
|
||||||
|
//
|
||||||
|
// VW MEB battery LV connector, recovered from the "VW bms" diagram
|
||||||
|
// (diagrams/0005_VW_bms.json). Colours are confirmed OEM; the signal each pin
|
||||||
|
// carries is NOT yet confirmed — the source diagram had no pin labels.
|
||||||
|
const OEM = {
|
||||||
|
meb: {
|
||||||
|
brown: { name: "Brown (VW ground)", hex: "#8B4513", stripe: null, gauge: "16 AWG" },
|
||||||
|
greenBlack: { name: "Green / Black stripe", hex: "#007700", stripe: "#000000", gauge: "18 AWG" },
|
||||||
|
greenRed: { name: "Green / Red stripe", hex: "#007700", stripe: "#FF0000", gauge: "18 AWG" },
|
||||||
|
greenWhite: { name: "Green / White stripe", hex: "#007700", stripe: "#CCCCCC", gauge: "18 AWG" },
|
||||||
|
orangeBlue: { name: "Orange / Blue stripe", hex: "#FF8C00", stripe: "#0000FF", gauge: "20 AWG" },
|
||||||
|
orangeRed: { name: "Orange / Red stripe", hex: "#FF8C00", stripe: "#FF0000", gauge: "20 AWG" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Shorthand for an OEM-coloured pin: po("9", "CAN H?", OEM.meb.orangeBlue, "…")
|
||||||
|
function po(pin, name, oemColor, note) {
|
||||||
|
return {
|
||||||
|
pin, name, fn: null, note: note || "",
|
||||||
|
oem: {
|
||||||
|
color_primary: oemColor.hex,
|
||||||
|
color_stripe: oemColor.stripe,
|
||||||
|
gauge: oemColor.gauge,
|
||||||
|
label: oemColor.name,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Standard parts
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const SKUDAK_PARTS = {
|
||||||
|
"Skudak VCU": [
|
||||||
|
{
|
||||||
|
id: "skudak-vcu-gray",
|
||||||
|
name: "Skudak VCU — GRAY",
|
||||||
|
manufacturer: "Skudak",
|
||||||
|
partNumber: "VCU-001 (GRAY)",
|
||||||
|
description: "VCUv2 rev A left receptacle — power, GPIOHP high-current channels, relay coils, LIN, wake, fault, aux input. Mates Molex 0334722007.",
|
||||||
|
pinCount: 20,
|
||||||
|
pinSpecs: [
|
||||||
|
p("1", "12V IN", "KL30", "Fused supply"),
|
||||||
|
p("2", "12V IN", "KL30", "Fused supply"),
|
||||||
|
p("3", "GPIOHP1", "LOAD_12V", "High-current switched output, 8 A"),
|
||||||
|
p("4", "GPIOHP3", "LOAD_12V", "High-current switched output, 8 A"),
|
||||||
|
p("5", "GPIOHP5", "LOAD_12V", "High-current switched output, 8 A"),
|
||||||
|
p("6", "GPIOHP7", "LOAD_12V", "High-current switched output, 8 A"),
|
||||||
|
p("7", "RLY1_A", "COIL", "Relay 1 coil, terminal A"),
|
||||||
|
p("8", "RLY2_A", "COIL", "Relay 2 coil, terminal A"),
|
||||||
|
p("9", "LIN", "LIN", "LIN bus"),
|
||||||
|
p("10", "WAKE", "KL15", "Ignition / wake input"),
|
||||||
|
p("11", "GND", "GND", "Ground"),
|
||||||
|
p("12", "GND", "GND", "Ground"),
|
||||||
|
p("13", "GPIOHP2", "LOAD_12V", "High-current switched output, 8 A"),
|
||||||
|
p("14", "GPIOHP4", "LOAD_12V", "High-current switched output, 8 A"),
|
||||||
|
p("15", "GPIOHP6", "LOAD_12V", "High-current switched output, 8 A"),
|
||||||
|
p("16", "GPIOHP8", "LOAD_12V", "High-current switched output, 8 A"),
|
||||||
|
p("17", "RLY1_B", "COIL", "Relay 1 coil, terminal B"),
|
||||||
|
p("18", "RLY2_B", "COIL", "Relay 2 coil, terminal B"),
|
||||||
|
p("19", "FAULT_OUT", "FAULT", "Latched check-engine output"),
|
||||||
|
p("20", "AUX_IN", "ANALOG_1", "0–12 V analog / frequency input"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "skudak-vcu-black",
|
||||||
|
name: "Skudak VCU — BLACK",
|
||||||
|
manufacturer: "Skudak",
|
||||||
|
partNumber: "VCU-001 (BLACK)",
|
||||||
|
description: "VCUv2 rev A right receptacle — 12 MPIO signal channels, HVIL, 3× CAN. Mates Molex 0334722006.",
|
||||||
|
pinCount: 20,
|
||||||
|
pinSpecs: [
|
||||||
|
p("1", "MPIO1", "PWM_OUT", "Source / sink / Hi-Z, PWM, analog or freq in"),
|
||||||
|
p("2", "MPIO2", "FREQ_IN", "Source / sink / Hi-Z"),
|
||||||
|
p("3", "MPIO3", "PWM_OUT", "Source / sink / Hi-Z"),
|
||||||
|
p("4", "MPIO4", "FREQ_IN", "Source / sink / Hi-Z"),
|
||||||
|
p("5", "MPIO5", "DIG_IN", "Source / sink / Hi-Z"),
|
||||||
|
p("6", "MPIO6", "DIG_IN", "Source / sink / Hi-Z"),
|
||||||
|
p("7", "HVIL OUT", "HVIL_OUT", "Interlock drive, VCU is master"),
|
||||||
|
p("8", "CAN1 H", "CAN1_H", "Powertrain bus"),
|
||||||
|
p("9", "CAN2 H", "CAN2_H", "HV pack bus"),
|
||||||
|
p("10", "CAN3 H", "CAN3_H", "Charge & body bus"),
|
||||||
|
p("11", "MPIO7", "DIG_OUT", "Source / sink / Hi-Z"),
|
||||||
|
p("12", "MPIO8", "DIG_OUT", "Source / sink / Hi-Z"),
|
||||||
|
p("13", "MPIO9", "DIG_IN", "Source / sink / Hi-Z"),
|
||||||
|
p("14", "MPIO10", "DIG_IN", "Source / sink / Hi-Z"),
|
||||||
|
p("15", "MPIO11", "ANALOG_2", "Source / sink / Hi-Z"),
|
||||||
|
p("16", "MPIO12", "ANALOG_3", "Source / sink / Hi-Z"),
|
||||||
|
p("17", "HVIL RTN", "HVIL_RTN", "Interlock return"),
|
||||||
|
p("18", "CAN1 L", "CAN1_L", "Powertrain bus"),
|
||||||
|
p("19", "CAN2 L", "CAN2_L", "HV pack bus"),
|
||||||
|
p("20", "CAN3 L", "CAN3_L", "Charge & body bus"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
"Skudak Standard Parts": [
|
||||||
|
{
|
||||||
|
id: "dilong-obc",
|
||||||
|
name: "Dilong OBC / DCDC",
|
||||||
|
manufacturer: "Dilong",
|
||||||
|
partNumber: "DA8KM22A",
|
||||||
|
description: "On-board charger + DC/DC. Controlled entirely over CAN — see Dilong_DA8KM22A_OBC_DCDC_Rev0.dbc. LV connector only; HV and AC are separate.",
|
||||||
|
pinCount: 6,
|
||||||
|
verify: true,
|
||||||
|
pinSpecs: [
|
||||||
|
p("1", "12V+", "KL30", "Permanent supply"),
|
||||||
|
p("2", "GND", "GND", "Ground"),
|
||||||
|
p("3", "CAN H", "CAN3_H", "Charge & body bus"),
|
||||||
|
p("4", "CAN L", "CAN3_L", "Charge & body bus"),
|
||||||
|
p("5", "ENABLE", "DIG_OUT", "Charge enable"),
|
||||||
|
p("6", "INTERLOCK", "HVIL_OUT", "Loop through HV connector"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "openinverter-ldu-23p",
|
||||||
|
name: "Openinverter LDU — 23-pin Tesla",
|
||||||
|
manufacturer: "openinverter / Damien Maguire",
|
||||||
|
partNumber: "Tesla LDU logic board",
|
||||||
|
description:
|
||||||
|
"Openinverter logic board in a Tesla Large Drive Unit, 23-way Tesla connector. " +
|
||||||
|
"The board drives precharge (pin 3) and the main contactor (pin 6) from its own outputs, and takes the pedal, " +
|
||||||
|
"shifter, brake, cruise and start as discrete inputs. All of those can move onto CAN instead " +
|
||||||
|
"(potmode=2/3/6, cruisemode=2, CANIOS bitfield) if you would rather the VCU own them. Pins 19 and 20 are unused.",
|
||||||
|
pinCount: 23,
|
||||||
|
pinSpecs: [
|
||||||
|
p("1", "IGN +12V", "KL15", "Ignition feed to the logic board"),
|
||||||
|
p("2", "BRAKE ON", "DIG_IN", "brake_in — required for shift lockout"),
|
||||||
|
p("3", "PRECHARGE RELAY", "COIL", "prec_out — board drives the precharge coil"),
|
||||||
|
p("4", "CAN HIGH", "CAN1_H", "Powertrain bus"),
|
||||||
|
p("5", "CAN LOW", "CAN1_L", "Powertrain bus"),
|
||||||
|
p("6", "MAIN CONTACTOR", "COIL", "dcsw_out — board drives the main contactor coil"),
|
||||||
|
p("7", "FORWARD", "DIG_IN", "fwd_in"),
|
||||||
|
p("8", "REVERSE", "DIG_IN", "rev_in"),
|
||||||
|
p("9", "ENC +5V", "V5_REF", "Encoder supply"),
|
||||||
|
p("10", "ENC A", "FREQ_IN", "Quadrature channel A"),
|
||||||
|
p("11", "GND", "GND", "Power ground"),
|
||||||
|
p("12", "ACCEL 5V", "V5_REF", "Pedal supply"),
|
||||||
|
p("13", "ACCEL INPUT", "ANALOG_1", "Pedal wiper"),
|
||||||
|
p("14", "BRAKE TRANSDUCER", "ANALOG_2", "Analog brake pressure"),
|
||||||
|
p("15", "ACCEL GND", "GND_SIG", "Pedal return"),
|
||||||
|
p("16", "ENC B", "ENC_B", "Quadrature channel B"),
|
||||||
|
p("17", "ENC GND", "GND_SIG", "Encoder return"),
|
||||||
|
p("18", "ENC SHIELD", "SHIELD", "Encoder cable drain"),
|
||||||
|
p("19", "—", null, "Unused"),
|
||||||
|
p("20", "—", null, "Unused"),
|
||||||
|
p("21", "CRUISE IN", "DIG_IN", "cruise_in"),
|
||||||
|
p("22", "GND", "GND", "Power ground"),
|
||||||
|
p("23", "START", "DIG_IN", "start_in"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "bmw-pedal",
|
||||||
|
name: "BMW Accelerator Pedal",
|
||||||
|
manufacturer: "BMW / Bosch",
|
||||||
|
partNumber: "BMW E-series",
|
||||||
|
description: "Dual-channel hall pedal. Channel 2 reads roughly half of channel 1 for plausibility. Verify pin order against your specific pedal before crimping.",
|
||||||
|
pinCount: 6,
|
||||||
|
verify: true,
|
||||||
|
pinSpecs: [
|
||||||
|
p("1", "SEN2 GND", "GND_SIG", "Channel 2 ground"),
|
||||||
|
p("2", "SEN2 +5V", "V5_REF", "Channel 2 supply"),
|
||||||
|
p("3", "SEN2 SIG", "ANALOG_2", "Channel 2 signal (half-scale)"),
|
||||||
|
p("4", "SEN1 SIG", "ANALOG_1", "Channel 1 signal (full-scale)"),
|
||||||
|
p("5", "SEN1 +5V", "V5_REF", "Channel 1 supply"),
|
||||||
|
p("6", "SEN1 GND", "GND_SIG", "Channel 1 ground"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "honeywell-cssv1500",
|
||||||
|
name: "Honeywell Current Sensor",
|
||||||
|
manufacturer: "Honeywell",
|
||||||
|
partNumber: "CSSV1500",
|
||||||
|
description: "CAN pack-current sensor. 24-bit high-precision + 16-bit low-precision current. Lives inside the battery enclosure.",
|
||||||
|
pinCount: 4,
|
||||||
|
pinSpecs: [
|
||||||
|
p("1", "12V+", "KL15", "Enclosure 12 V bus"),
|
||||||
|
p("2", "GND", "GND", "Enclosure ground"),
|
||||||
|
p("3", "CAN H", "CAN2_H", "HV pack bus"),
|
||||||
|
p("4", "CAN L", "CAN2_L", "HV pack bus"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "bender-iso175",
|
||||||
|
name: "Bender ISO175 IMD",
|
||||||
|
manufacturer: "Bender",
|
||||||
|
partNumber: "ISO175",
|
||||||
|
description: "Insulation monitoring device. CAN reporting; isolation below 100 kΩ aborts charging. Lives inside the battery enclosure.",
|
||||||
|
pinCount: 6,
|
||||||
|
pinSpecs: [
|
||||||
|
p("1", "12V+", "KL15", "Enclosure 12 V bus"),
|
||||||
|
p("2", "GND", "GND", "Enclosure ground"),
|
||||||
|
p("3", "CAN H", "CAN2_H", "HV pack bus"),
|
||||||
|
p("4", "CAN L", "CAN2_L", "HV pack bus"),
|
||||||
|
p("5", "HV+", "HV_SENSE", "Pack positive sense"),
|
||||||
|
p("6", "HV−", "HV_SENSE", "Pack negative sense"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "bosch-ibooster",
|
||||||
|
name: "Bosch iBooster",
|
||||||
|
manufacturer: "Bosch",
|
||||||
|
partNumber: "iBooster Gen2",
|
||||||
|
description: "Electromechanical brake booster. CAN-commanded with its own power feed. PINOUT UNVERIFIED — confirm against your generation and connector before building.",
|
||||||
|
pinCount: 8,
|
||||||
|
verify: true,
|
||||||
|
pinSpecs: [
|
||||||
|
p("1", "B+", "KL30", "Battery positive, heavy feed"),
|
||||||
|
p("2", "GND", "GND", "Power ground"),
|
||||||
|
p("3", "KL15", "KL15", "Ignition"),
|
||||||
|
p("4", "CAN H", "CAN1_H", "Chassis / powertrain bus"),
|
||||||
|
p("5", "CAN L", "CAN1_L", "Chassis / powertrain bus"),
|
||||||
|
p("6", "BLS", "DIG_IN", "Brake light switch out"),
|
||||||
|
p("7", "WAKE", "DIG_OUT", "Wake line"),
|
||||||
|
p("8", "GND SIG", "GND_SIG", "Signal ground"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "prius-eps",
|
||||||
|
name: "Toyota Prius EPS Column",
|
||||||
|
manufacturer: "Toyota",
|
||||||
|
partNumber: "Prius EPS",
|
||||||
|
description: "Electric power steering column, torque sensor + motor + ECU. PINOUT UNVERIFIED — confirm generation (Gen2 / Gen3) and connector before building.",
|
||||||
|
pinCount: 6,
|
||||||
|
verify: true,
|
||||||
|
pinSpecs: [
|
||||||
|
p("1", "B+", "KL30", "Heavy motor feed"),
|
||||||
|
p("2", "GND", "GND", "Power ground"),
|
||||||
|
p("3", "IG", "KL15", "Ignition / enable"),
|
||||||
|
p("4", "TRQ", "ANALOG_1", "Torque sensor signal"),
|
||||||
|
p("5", "SPD", "FREQ_IN", "Vehicle speed input"),
|
||||||
|
p("6", "DIAG", "DIG_IN", "Diagnostic line"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "volvo-ps-pump",
|
||||||
|
name: "Volvo Steering Pump",
|
||||||
|
manufacturer: "Volvo",
|
||||||
|
partNumber: "Electro-hydraulic PS",
|
||||||
|
description: "Electro-hydraulic power steering pump. Speed commanded by PWM or LIN depending on variant. PINOUT UNVERIFIED — confirm variant before building.",
|
||||||
|
pinCount: 4,
|
||||||
|
verify: true,
|
||||||
|
pinSpecs: [
|
||||||
|
p("1", "B+", "KL30", "Heavy motor feed"),
|
||||||
|
p("2", "GND", "GND", "Power ground"),
|
||||||
|
p("3", "CTRL", "PWM_OUT", "Speed command"),
|
||||||
|
p("4", "DIAG", "FREQ_IN", "Status / diagnostic feedback"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "instrument-cluster",
|
||||||
|
name: "Instrument Cluster",
|
||||||
|
manufacturer: "varies",
|
||||||
|
partNumber: "varies",
|
||||||
|
description: "Gauge cluster — switched 12 V and CAN only. VCU broadcasts SOC (0x3D0) and gauge control (0x3D2).",
|
||||||
|
pinCount: 4,
|
||||||
|
pinSpecs: [
|
||||||
|
p("1", "12V", "KL15", "Switched supply"),
|
||||||
|
p("2", "GND", "GND", "Ground"),
|
||||||
|
p("3", "CAN H", "CAN3_H", "Charge & body bus"),
|
||||||
|
p("4", "CAN L", "CAN3_L", "Charge & body bus"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "dnr-switch",
|
||||||
|
name: "DNR Switch",
|
||||||
|
manufacturer: "varies",
|
||||||
|
partNumber: "varies",
|
||||||
|
description: "Drive / Neutral / Reverse selector. Discrete switched-to-ground lines, one per position; firmware rejects any state where more than one is active.",
|
||||||
|
pinCount: 4,
|
||||||
|
pinSpecs: [
|
||||||
|
p("1", "COM", "GND", "Switch common to ground"),
|
||||||
|
p("2", "DRIVE", "DIG_IN", "Drive position"),
|
||||||
|
p("3", "NEUTRAL", "DIG_IN", "Neutral position"),
|
||||||
|
p("4", "REVERSE", "DIG_IN", "Reverse position"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
"Battery Enclosure": [
|
||||||
|
{
|
||||||
|
id: "batt-enclosure-32p",
|
||||||
|
name: "Battery Enclosure Bulkhead — 32-pin",
|
||||||
|
manufacturer: "TE Connectivity (Deutsch)",
|
||||||
|
partNumber: "TBD — confirm 32-way P/N",
|
||||||
|
description:
|
||||||
|
"Single bulkhead between the battery enclosure and the vehicle harness. Everything inside shares one 12 V bus and one ground, fed on pins 1–4. " +
|
||||||
|
"All contactor coils are LOW-SIDE switched by the VCU: coil+ ties to the internal 12 V bus and stays in the box, so only the coil− return crosses the bulkhead — " +
|
||||||
|
"one wire per contactor on pins 9–13. PROPOSED ALLOCATION — confirm the Deutsch part number and re-order to suit the keying.",
|
||||||
|
pinCount: 32,
|
||||||
|
verify: true,
|
||||||
|
// One device covering both faces of the bulkhead: 32 IN pins on the left,
|
||||||
|
// 32 OUT on the right, one label per circuit down the middle.
|
||||||
|
passThrough: true,
|
||||||
|
pinSpecs: [
|
||||||
|
p("1", "12V BUS", "KL15", "Enclosure 12 V bus feed"),
|
||||||
|
p("2", "12V BUS", "KL15", "Enclosure 12 V bus feed (paralleled)"),
|
||||||
|
p("3", "GND", "GND", "Enclosure ground"),
|
||||||
|
p("4", "GND", "GND", "Enclosure ground (paralleled)"),
|
||||||
|
p("5", "CAN2 H", "CAN2_H", "BMS + IMD + current sensor"),
|
||||||
|
p("6", "CAN2 L", "CAN2_L", "BMS + IMD + current sensor"),
|
||||||
|
p("7", "HVIL IN", "HVIL_OUT", "Interlock into enclosure"),
|
||||||
|
p("8", "HVIL OUT", "HVIL_RTN", "Interlock out of enclosure"),
|
||||||
|
// Coils are low-side switched by the VCU. Coil+ sits on the internal
|
||||||
|
// 12 V bus and never crosses the bulkhead, so each contactor takes one
|
||||||
|
// wire out, not a pair — the VCU sinks it to close the contactor.
|
||||||
|
p("9", "MAIN COIL −", "COIL", "Main (positive) contactor — VCU sinks to close"),
|
||||||
|
p("10", "NEG COIL −", "COIL", "Negative contactor — VCU sinks to close"),
|
||||||
|
p("11", "PRE COIL −", "COIL", "Precharge relay — VCU sinks to close"),
|
||||||
|
p("12", "AC COIL −", "COIL", "A/C contactor — VCU sinks to close"),
|
||||||
|
p("13", "HEAT COIL −", "COIL", "Heat contactor — VCU sinks to close"),
|
||||||
|
p("14", "MAIN WELD", "WELD", "Main contactor aux contact"),
|
||||||
|
p("15", "MAIN WELD RTN","WELD", "Main aux return — drop if the aux references the internal ground bus"),
|
||||||
|
p("16", "NEG WELD", "WELD", "Negative contactor aux contact"),
|
||||||
|
p("17", "NEG WELD RTN", "WELD", "Negative aux return — drop if the aux references the internal ground bus"),
|
||||||
|
p("18", "BMS WAKE", "INHIBIT", "BMS wake / enable"),
|
||||||
|
p("19", "PRE FEEDBACK", "ANALOG_1", "Precharge bus voltage feedback"),
|
||||||
|
p("20", "PACK TEMP", "ANALOG_2", "Spare pack thermistor"),
|
||||||
|
p("21", "SHIELD", "SHIELD", "CAN shield drain"),
|
||||||
|
p("22", "—", null, "Spare"),
|
||||||
|
p("23", "—", null, "Spare"),
|
||||||
|
p("24", "—", null, "Spare"),
|
||||||
|
p("25", "—", null, "Spare"),
|
||||||
|
p("26", "—", null, "Spare"),
|
||||||
|
p("27", "—", null, "Spare"),
|
||||||
|
p("28", "—", null, "Spare"),
|
||||||
|
p("29", "—", null, "Spare"),
|
||||||
|
p("30", "—", null, "Spare"),
|
||||||
|
p("31", "—", null, "Spare"),
|
||||||
|
p("32", "—", null, "Spare"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "contactor-coil-aux",
|
||||||
|
name: "HV Contactor (coil + aux)",
|
||||||
|
manufacturer: "varies",
|
||||||
|
partNumber: "varies",
|
||||||
|
description: "HV contactor with auxiliary weld-detect contacts. Use for main, negative, A/C and heat. Coil must have a built-in economizer. Inside the enclosure COIL A ties to the shared 12 V bus; only COIL B leaves via the bulkhead, where the VCU sinks it.",
|
||||||
|
pinCount: 4,
|
||||||
|
pinSpecs: [
|
||||||
|
p("1", "COIL A", "COIL", "Coil terminal A"),
|
||||||
|
p("2", "COIL B", "COIL", "Coil terminal B"),
|
||||||
|
p("3", "AUX A", "WELD", "Auxiliary contact — weld detect"),
|
||||||
|
p("4", "AUX B", "WELD", "Auxiliary contact — weld detect"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "precharge-relay",
|
||||||
|
name: "Precharge Relay",
|
||||||
|
manufacturer: "varies",
|
||||||
|
partNumber: "varies",
|
||||||
|
description: "Precharge relay and series resistor. Closes with the negative contactor, opens once the bus reaches threshold. COIL A ties to the enclosure 12 V bus; COIL B is the low-side return to the VCU.",
|
||||||
|
pinCount: 2,
|
||||||
|
pinSpecs: [
|
||||||
|
p("1", "COIL A", "COIL", "Coil terminal A"),
|
||||||
|
p("2", "COIL B", "COIL", "Coil terminal B"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "vw-meb-bms",
|
||||||
|
name: "VW MEB BMS — LV connector",
|
||||||
|
manufacturer: "Volkswagen",
|
||||||
|
partNumber: "MEB",
|
||||||
|
description:
|
||||||
|
"VW MEB battery management master, 12-way LV connector. Pins 7–12 carry OEM harness colours recovered from the VW bms diagram and must match the factory loom. " +
|
||||||
|
"SIGNAL ASSIGNMENT UNCONFIRMED — the source diagram recorded colours but no pin labels. Pins 1–6 were unused there.",
|
||||||
|
pinCount: 12,
|
||||||
|
verify: true,
|
||||||
|
pinSpecs: [
|
||||||
|
p("1", "—", null, "Unused in the recovered harness"),
|
||||||
|
p("2", "—", null, "Unused in the recovered harness"),
|
||||||
|
p("3", "—", null, "Unused in the recovered harness"),
|
||||||
|
p("4", "—", null, "Unused in the recovered harness"),
|
||||||
|
p("5", "—", null, "Unused in the recovered harness"),
|
||||||
|
p("6", "—", null, "Unused in the recovered harness"),
|
||||||
|
po("7", "OEM 7", OEM.meb.greenWhite, "OEM Green/White — signal unconfirmed"),
|
||||||
|
po("8", "CAN L?", OEM.meb.orangeRed, "OEM Orange/Red — VW HV-CAN pair with pin 9, polarity unconfirmed"),
|
||||||
|
po("9", "CAN H?", OEM.meb.orangeBlue, "OEM Orange/Blue — VW HV-CAN pair with pin 8, polarity unconfirmed"),
|
||||||
|
po("10", "OEM 10", OEM.meb.greenRed, "OEM Green/Red — signal unconfirmed"),
|
||||||
|
po("11", "GND", OEM.meb.greenBlack, "OEM Green/Black — ran to chassis ground in the recovered harness"),
|
||||||
|
po("12", "OEM 12", OEM.meb.brown, "OEM Brown — VW convention is ground; ran to the pigtail, not to chassis"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Merge into the connector library ─────────────────────────────────────────
|
||||||
|
// Parts become ordinary library entries, so search, category filter and
|
||||||
|
// drag-to-canvas all work with no changes to app.js.
|
||||||
|
for (const [category, parts] of Object.entries(SKUDAK_PARTS)) {
|
||||||
|
CONNECTOR_LIBRARY[category] = parts;
|
||||||
|
for (const part of parts) {
|
||||||
|
part.pinLabels = part.pinSpecs.map(s => s.name);
|
||||||
|
_CONN_BY_ID[part.id] = part;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Wire default lookup ──────────────────────────────────────────────────────
|
||||||
|
// Given a device and pin id, return the standard wire for that pin, or null.
|
||||||
|
function standardWireForPin(device, pinId) {
|
||||||
|
if (!device) return null;
|
||||||
|
const pin = (device.pins || []).find(p => p.id === pinId);
|
||||||
|
if (!pin) return null;
|
||||||
|
// An OEM colour is not ours to choose — it has to match the factory loom,
|
||||||
|
// so it wins over the in-house standard.
|
||||||
|
// The signal name is what makes a wire unambiguous on the bench. A connector
|
||||||
|
// can legitimately carry five digital inputs in the same colour; the pin name
|
||||||
|
// is what tells FORWARD from REVERSE.
|
||||||
|
const signal = pin.name && pin.name !== "—" ? pin.name : null;
|
||||||
|
if (pin.wire_oem) return { fn: "OEM", twisted_pair: false, signal, ...pin.wire_oem };
|
||||||
|
if (pin.wire_fn) {
|
||||||
|
const spec = wireSpecFor(pin.wire_fn);
|
||||||
|
return spec ? { ...spec, signal } : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose the palette for the wire-colour swatch picker.
|
||||||
|
const TXL_WIRE_COLORS = [
|
||||||
|
...Object.values(TXL_SOLID).map(c => ({ name: c.name, hex: c.hex, stripe: null })),
|
||||||
|
...Object.values(TXL_STRIPED).map(c => ({ name: c.name, hex: c.hex, stripe: c.stripe })),
|
||||||
|
];
|
||||||
@@ -0,0 +1,410 @@
|
|||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// GEP power distribution modules — fuse box grid model
|
||||||
|
//
|
||||||
|
// Loaded AFTER partsLibrary.js. Registers a `pdm` device type whose pins are
|
||||||
|
// derived from what is physically placed in the box, so arranging the grid in
|
||||||
|
// the fuse-box tool immediately changes what you can wire to on the canvas.
|
||||||
|
//
|
||||||
|
// GEP FRH / PDM modules are open-plan 280-footprint grids: the "way" count is
|
||||||
|
// the number of Metri-Pack 280 cavities and you place your own mix of
|
||||||
|
// components anywhere in them. GEP quote the FRH-A24 as taking up to 4 five-
|
||||||
|
// prong relays, or 6 four-prong relays, or 12 mini fuses — all three come to
|
||||||
|
// exactly 24 cavities on a 4x6 grid with the footprints below, which is where
|
||||||
|
// the geometry comes from.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const PDM_MODULES = {
|
||||||
|
"gep-frh-a24": {
|
||||||
|
id: "gep-frh-a24",
|
||||||
|
name: "GEP FRH-A24",
|
||||||
|
manufacturer: "GEP Power Products",
|
||||||
|
partNumber: "FRH-A24",
|
||||||
|
cols: 4, rows: 6,
|
||||||
|
description: "Sealed 24-cavity 280-footprint fuse / relay holder, IP66/IP67. ~60 x 50 x 60 mm.",
|
||||||
|
},
|
||||||
|
"gep-frh-a12": {
|
||||||
|
id: "gep-frh-a12",
|
||||||
|
name: "GEP FRH-A12",
|
||||||
|
manufacturer: "GEP Power Products",
|
||||||
|
partNumber: "FRH-A12",
|
||||||
|
cols: 4, rows: 3,
|
||||||
|
description: "Sealed 12-cavity 280-footprint fuse / relay holder.",
|
||||||
|
},
|
||||||
|
"gep-pdm-48": {
|
||||||
|
id: "gep-pdm-48",
|
||||||
|
name: "GEP PDM 48-way",
|
||||||
|
manufacturer: "GEP Power Products",
|
||||||
|
partNumber: "PDM-R4A01",
|
||||||
|
cols: 4, rows: 12,
|
||||||
|
description: "Sealed stackable 48-cavity 280-footprint power distribution module.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Component footprints ─────────────────────────────────────────────────────
|
||||||
|
// w/h are in cavities. Terminals sit at (c,r) inside the unrotated footprint;
|
||||||
|
// a 5-prong relay is a 2x3 block with one cavity unused, which is why four of
|
||||||
|
// them exactly fill a 24-way.
|
||||||
|
const PDM_COMPONENTS = {
|
||||||
|
fuse: {
|
||||||
|
key: "fuse",
|
||||||
|
label: "Mini fuse",
|
||||||
|
short: "FUSE",
|
||||||
|
w: 1, h: 2,
|
||||||
|
colour: "#4a7dd6",
|
||||||
|
ratings: ["2A", "3A", "5A", "7.5A", "10A", "15A", "20A", "25A", "30A"],
|
||||||
|
defaultRating: "10A",
|
||||||
|
terminals: [
|
||||||
|
{ t: "IN", c: 0, r: 0, fn: "KL30" },
|
||||||
|
{ t: "OUT", c: 0, r: 1, fn: "LOAD_12V" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
relay_spdt: {
|
||||||
|
key: "relay_spdt",
|
||||||
|
label: "Micro relay, 5-prong SPDT",
|
||||||
|
short: "RLY5",
|
||||||
|
w: 2, h: 3,
|
||||||
|
colour: "#8a5fd6",
|
||||||
|
ratings: ["20A/10A", "30A/20A", "40A/30A"],
|
||||||
|
defaultRating: "30A/20A",
|
||||||
|
terminals: [
|
||||||
|
{ t: "86", c: 0, r: 0, fn: "COIL" },
|
||||||
|
{ t: "85", c: 1, r: 0, fn: "COIL" },
|
||||||
|
{ t: "30", c: 0, r: 1, fn: "KL30" },
|
||||||
|
{ t: "87a", c: 1, r: 1, fn: "LOAD_12V" },
|
||||||
|
{ t: "87", c: 0, r: 2, fn: "LOAD_12V" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
relay_spst: {
|
||||||
|
key: "relay_spst",
|
||||||
|
label: "Micro relay, 4-prong SPST",
|
||||||
|
short: "RLY4",
|
||||||
|
w: 2, h: 2,
|
||||||
|
colour: "#6f4fc0",
|
||||||
|
ratings: ["20A", "30A", "40A"],
|
||||||
|
defaultRating: "30A",
|
||||||
|
terminals: [
|
||||||
|
{ t: "86", c: 0, r: 0, fn: "COIL" },
|
||||||
|
{ t: "85", c: 1, r: 0, fn: "COIL" },
|
||||||
|
{ t: "30", c: 0, r: 1, fn: "KL30" },
|
||||||
|
{ t: "87", c: 1, r: 1, fn: "LOAD_12V" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
diode: {
|
||||||
|
key: "diode",
|
||||||
|
label: "Mini diode",
|
||||||
|
short: "DIODE",
|
||||||
|
w: 1, h: 2,
|
||||||
|
colour: "#3f8f6a",
|
||||||
|
ratings: ["3A", "6A"],
|
||||||
|
defaultRating: "6A",
|
||||||
|
terminals: [
|
||||||
|
{ t: "A", c: 0, r: 0, fn: "LOAD_12V" },
|
||||||
|
{ t: "K", c: 0, r: 1, fn: "LOAD_12V" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Bus bars link a run of cavities into one node, so a single feed wire
|
||||||
|
// supplies every component blade sitting on the bar. They therefore SHARE
|
||||||
|
// cavities with those blades by design — `isBus` puts them on their own
|
||||||
|
// layer, colliding only with each other, and any input terminal landing on a
|
||||||
|
// bar stops needing its own wire.
|
||||||
|
bus2: {
|
||||||
|
key: "bus2", label: "Bus bar, 2-way", short: "BUS2", isBus: true,
|
||||||
|
w: 1, h: 2, colour: "#b0563c",
|
||||||
|
ratings: ["—"], defaultRating: "—",
|
||||||
|
terminals: [{ t: "FEED", c: 0, r: 0, fn: "KL30" }],
|
||||||
|
},
|
||||||
|
bus3: {
|
||||||
|
key: "bus3", label: "Bus bar, 3-way", short: "BUS3", isBus: true,
|
||||||
|
w: 1, h: 3, colour: "#b0563c",
|
||||||
|
ratings: ["—"], defaultRating: "—",
|
||||||
|
terminals: [{ t: "FEED", c: 0, r: 0, fn: "KL30" }],
|
||||||
|
},
|
||||||
|
bus4: {
|
||||||
|
key: "bus4", label: "Bus bar, 4-way", short: "BUS4", isBus: true,
|
||||||
|
w: 1, h: 4, colour: "#b0563c",
|
||||||
|
ratings: ["—"], defaultRating: "—",
|
||||||
|
terminals: [{ t: "FEED", c: 0, r: 0, fn: "KL30" }],
|
||||||
|
},
|
||||||
|
bus6: {
|
||||||
|
key: "bus6", label: "Bus bar, 6-way", short: "BUS6", isBus: true,
|
||||||
|
w: 1, h: 6, colour: "#b0563c",
|
||||||
|
ratings: ["—"], defaultRating: "—",
|
||||||
|
terminals: [{ t: "FEED", c: 0, r: 0, fn: "KL30" }],
|
||||||
|
},
|
||||||
|
breaker: {
|
||||||
|
key: "breaker",
|
||||||
|
label: "Mini circuit breaker",
|
||||||
|
short: "CB",
|
||||||
|
w: 1, h: 2,
|
||||||
|
colour: "#b5813a",
|
||||||
|
ratings: ["5A", "10A", "15A", "20A", "25A", "30A"],
|
||||||
|
defaultRating: "20A",
|
||||||
|
terminals: [
|
||||||
|
{ t: "IN", c: 0, r: 0, fn: "KL30" },
|
||||||
|
{ t: "OUT", c: 0, r: 1, fn: "LOAD_12V" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Rotation ─────────────────────────────────────────────────────────────────
|
||||||
|
// Clockwise. Returns the footprint size at that rotation.
|
||||||
|
function pdmSize(comp, rot) {
|
||||||
|
return (rot === 90 || rot === 270)
|
||||||
|
? { w: comp.h, h: comp.w }
|
||||||
|
: { w: comp.w, h: comp.h };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maps a terminal's cell inside the unrotated footprint to its cell inside the
|
||||||
|
// rotated one.
|
||||||
|
function pdmRotateCell(c, r, w, h, rot) {
|
||||||
|
switch (rot) {
|
||||||
|
case 90: return { c: h - 1 - r, r: c };
|
||||||
|
case 180: return { c: w - 1 - c, r: h - 1 - r };
|
||||||
|
case 270: return { c: r, r: w - 1 - c };
|
||||||
|
default: return { c, r };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _pdmSeq = 0;
|
||||||
|
function pdmNewCircuit(typeKey) {
|
||||||
|
const comp = PDM_COMPONENTS[typeKey] || PDM_COMPONENTS.fuse;
|
||||||
|
return {
|
||||||
|
id: `c${Date.now().toString(36)}${(_pdmSeq++).toString(36)}`,
|
||||||
|
type: comp.key,
|
||||||
|
rating: comp.defaultRating,
|
||||||
|
name: "",
|
||||||
|
col: 0, row: 0, rot: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every cavity a placement covers, in absolute grid coordinates.
|
||||||
|
function pdmCells(circuit) {
|
||||||
|
const comp = PDM_COMPONENTS[circuit.type];
|
||||||
|
if (!comp) return [];
|
||||||
|
const { w, h } = pdmSize(comp, circuit.rot || 0);
|
||||||
|
const cells = [];
|
||||||
|
for (let r = 0; r < h; r++) {
|
||||||
|
for (let c = 0; c < w; c++) cells.push({ c: circuit.col + c, r: circuit.row + r });
|
||||||
|
}
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Absolute cavity of each terminal, plus its 1-based cavity number.
|
||||||
|
function pdmTerminals(circuit, mod) {
|
||||||
|
const comp = PDM_COMPONENTS[circuit.type];
|
||||||
|
if (!comp) return [];
|
||||||
|
const rot = circuit.rot || 0;
|
||||||
|
return comp.terminals.map((t) => {
|
||||||
|
const m = pdmRotateCell(t.c, t.r, comp.w, comp.h, rot);
|
||||||
|
const c = circuit.col + m.c;
|
||||||
|
const r = circuit.row + m.r;
|
||||||
|
return { ...t, c, r, cavity: r * mod.cols + c + 1 };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation: overlaps and out-of-bounds. Both are physical impossibilities,
|
||||||
|
// so the tool refuses to save them rather than warning and letting them through.
|
||||||
|
function pdmIsBus(circuit) {
|
||||||
|
return !!PDM_COMPONENTS[circuit?.type]?.isBus;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Which bus bar, if any, covers this cavity.
|
||||||
|
function pdmBusAt(props, c, r) {
|
||||||
|
for (const ci of props.circuits || []) {
|
||||||
|
if (!pdmIsBus(ci)) continue;
|
||||||
|
if (pdmCells(ci).some((x) => x.c === c && x.r === r)) return ci;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pdmValidate(props) {
|
||||||
|
const mod = PDM_MODULES[props.moduleId] || PDM_MODULES["gep-frh-a24"];
|
||||||
|
const circuits = props.circuits || [];
|
||||||
|
// Two layers: components collide with components, bus bars with bus bars.
|
||||||
|
// A bar crossing a fuse blade is the whole point of a bar, not a clash.
|
||||||
|
const layers = { part: new Map(), bus: new Map() };
|
||||||
|
const errors = [];
|
||||||
|
const badIds = new Set();
|
||||||
|
const nameOf = (id) => {
|
||||||
|
const x = circuits.find((y) => y.id === id);
|
||||||
|
return x ? (x.name || PDM_COMPONENTS[x.type]?.short || "component") : "component";
|
||||||
|
};
|
||||||
|
|
||||||
|
circuits.forEach((ci) => {
|
||||||
|
const comp = PDM_COMPONENTS[ci.type];
|
||||||
|
if (!comp) return;
|
||||||
|
const { w, h } = pdmSize(comp, ci.rot || 0);
|
||||||
|
if (ci.col < 0 || ci.row < 0 || ci.col + w > mod.cols || ci.row + h > mod.rows) {
|
||||||
|
errors.push(`${ci.name || comp.short} hangs outside the box`);
|
||||||
|
badIds.add(ci.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const occupied = layers[comp.isBus ? "bus" : "part"];
|
||||||
|
pdmCells(ci).forEach(({ c, r }) => {
|
||||||
|
const key = `${c},${r}`;
|
||||||
|
if (occupied.has(key)) {
|
||||||
|
errors.push(
|
||||||
|
`${ci.name || comp.short} overlaps ${nameOf(occupied.get(key))} at cavity ${r * mod.cols + c + 1}`);
|
||||||
|
badIds.add(ci.id);
|
||||||
|
badIds.add(occupied.get(key));
|
||||||
|
} else {
|
||||||
|
occupied.set(key, ci.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
module: mod,
|
||||||
|
used: layers.part.size,
|
||||||
|
total: mod.cols * mod.rows,
|
||||||
|
busUsed: layers.bus.size,
|
||||||
|
errors: [...new Set(errors)],
|
||||||
|
badIds,
|
||||||
|
ok: errors.length === 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can this component sit here without overlapping anything or leaving the box?
|
||||||
|
function pdmFits(props, circuit, col, row, rot) {
|
||||||
|
const mod = PDM_MODULES[props.moduleId] || PDM_MODULES["gep-frh-a24"];
|
||||||
|
const comp = PDM_COMPONENTS[circuit.type];
|
||||||
|
if (!comp) return false;
|
||||||
|
const { w, h } = pdmSize(comp, rot);
|
||||||
|
if (col < 0 || row < 0 || col + w > mod.cols || row + h > mod.rows) return false;
|
||||||
|
// Only same-layer components block each other — a bus bar is meant to lie
|
||||||
|
// across the blades it feeds.
|
||||||
|
const bus = !!comp.isBus;
|
||||||
|
const taken = new Set();
|
||||||
|
(props.circuits || []).forEach((o) => {
|
||||||
|
if (o.id === circuit.id || pdmIsBus(o) !== bus) return;
|
||||||
|
pdmCells(o).forEach(({ c, r }) => taken.add(`${c},${r}`));
|
||||||
|
});
|
||||||
|
const probe = { ...circuit, col, row, rot };
|
||||||
|
return pdmCells(probe).every(({ c, r }) => !taken.has(`${c},${r}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp a placement so the whole footprint sits inside the grid. Overlap is
|
||||||
|
// deliberately allowed: an overlapping component is a state you must be able to
|
||||||
|
// drag your way out of, so the editor permits it and validation flags it in red
|
||||||
|
// rather than refusing the move and trapping you.
|
||||||
|
function pdmClamp(props, circuit, col, row, rot) {
|
||||||
|
const mod = PDM_MODULES[props.moduleId] || PDM_MODULES["gep-frh-a24"];
|
||||||
|
const comp = PDM_COMPONENTS[circuit.type];
|
||||||
|
if (!comp) return { col: 0, row: 0 };
|
||||||
|
const { w, h } = pdmSize(comp, rot ?? circuit.rot ?? 0);
|
||||||
|
return {
|
||||||
|
col: Math.max(0, Math.min(col, mod.cols - w)),
|
||||||
|
row: Math.max(0, Math.min(row, mod.rows - h)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull every component back inside the grid — used after a module change, so
|
||||||
|
// switching to a smaller or differently-shaped box never strands a tile off
|
||||||
|
// the edge where it cannot be clicked.
|
||||||
|
function pdmClampAll(props) {
|
||||||
|
(props.circuits || []).forEach((ci) => {
|
||||||
|
const p = pdmClamp(props, ci, ci.col, ci.row, ci.rot || 0);
|
||||||
|
ci.col = p.col;
|
||||||
|
ci.row = p.row;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// First free slot, scanning row-major. Returns null if the box is full.
|
||||||
|
function pdmAutoPlace(props, circuit) {
|
||||||
|
const mod = PDM_MODULES[props.moduleId] || PDM_MODULES["gep-frh-a24"];
|
||||||
|
for (let r = 0; r < mod.rows; r++) {
|
||||||
|
for (let c = 0; c < mod.cols; c++) {
|
||||||
|
for (const rot of [0, 90]) {
|
||||||
|
if (pdmFits(props, circuit, c, r, rot)) return { col: c, row: r, rot };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Canvas pins ──────────────────────────────────────────────────────────────
|
||||||
|
// One pin per terminal, named by cavity, carrying the wire standard. Coils go
|
||||||
|
// left, switched outputs right, so power reads across the device.
|
||||||
|
// Friendly suffixes so a named circuit reads as plain English on the pin.
|
||||||
|
// Relay terminals keep their standard numbers — "Main contactor 87" is what is
|
||||||
|
// printed on the relay, so renaming it would help nobody.
|
||||||
|
const PDM_TERM_LABEL = { IN: "in", OUT: "out", FEED: "feed" };
|
||||||
|
|
||||||
|
// The pin name a terminal ends up with. Naming a fuse "VCU" turns its pins into
|
||||||
|
// "VCU in" and "VCU out"; unnamed components fall back to the cavity number so
|
||||||
|
// they are still identifiable on the board.
|
||||||
|
function pdmPinName(circuit, comp, t) {
|
||||||
|
const suffix = PDM_TERM_LABEL[t.t] || t.t;
|
||||||
|
const name = (circuit.name || "").trim();
|
||||||
|
return name ? `${name} ${suffix}` : `${t.cavity}·${t.t}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pdmPins(props, w, h) {
|
||||||
|
const mod = PDM_MODULES[props.moduleId] || PDM_MODULES["gep-frh-a24"];
|
||||||
|
const left = [{ id: "FEED", name: "FEED", fn: "KL30", note: "Module bus input" }];
|
||||||
|
const right = [];
|
||||||
|
|
||||||
|
(props.circuits || []).forEach((ci) => {
|
||||||
|
const comp = PDM_COMPONENTS[ci.type];
|
||||||
|
if (!comp) return;
|
||||||
|
const tag = ci.name || `${comp.short}${comp.isBus ? "" : ` ${ci.rating}`}`;
|
||||||
|
|
||||||
|
pdmTerminals(ci, mod).forEach((t) => {
|
||||||
|
// A component input sitting on a bus bar is fed by the bar, so it needs
|
||||||
|
// no wire of its own — the bar's single FEED pin covers the whole run.
|
||||||
|
if (!comp.isBus && t.fn === "KL30") {
|
||||||
|
const bar = pdmBusAt(props, t.c, t.r);
|
||||||
|
if (bar) return;
|
||||||
|
}
|
||||||
|
const pin = {
|
||||||
|
id: `${ci.id}_${t.t}`,
|
||||||
|
name: pdmPinName(ci, comp, t),
|
||||||
|
fn: t.fn,
|
||||||
|
// The cavity stays in the note, so renaming a circuit never loses where
|
||||||
|
// it physically sits in the box.
|
||||||
|
note: comp.isBus
|
||||||
|
? `${tag} — cavity ${t.cavity}, feeds ${pdmCells(ci).map((x) => x.r * mod.cols + x.c + 1).join(", ")}`
|
||||||
|
: `${tag} (${ci.rating}) — cavity ${t.cavity}`,
|
||||||
|
};
|
||||||
|
(t.fn === "COIL" ? left : right).push(pin);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const place = (arr, side) =>
|
||||||
|
arr.map((p, i) => ({
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
side,
|
||||||
|
x_offset: side === "left" ? 0 : w,
|
||||||
|
y_offset: ((i + 1) / (arr.length + 1)) * h,
|
||||||
|
wire_fn: p.fn,
|
||||||
|
wire_oem: null,
|
||||||
|
note: p.note,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [...place(left, "left"), ...place(right, "right")];
|
||||||
|
}
|
||||||
|
|
||||||
|
function pdmDefaultProps() {
|
||||||
|
const m = PDM_MODULES["gep-frh-a24"];
|
||||||
|
return {
|
||||||
|
moduleId: m.id,
|
||||||
|
circuits: [],
|
||||||
|
partNumber: m.partNumber,
|
||||||
|
manufacturer: m.manufacturer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof DEVICE_TYPES !== "undefined") {
|
||||||
|
DEVICE_TYPES.pdm = {
|
||||||
|
label: "Fuse / Relay Box",
|
||||||
|
description: "GEP power distribution module — arrange the cavity grid in the fuse box tool",
|
||||||
|
icon: "▦",
|
||||||
|
defaultProps: pdmDefaultProps(),
|
||||||
|
defaultSize: (p) => {
|
||||||
|
const n = Math.max((p.circuits || []).length, 3);
|
||||||
|
return { w: 190, h: Math.max(90, n * 24 + 40) };
|
||||||
|
},
|
||||||
|
getPins: (p, w, h) => pdmPins(p, w, h),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Theme — light / dark plus user overrides for the colours that make wires hard
|
||||||
|
// to read: canvas background, grid dots, device fill and device outline.
|
||||||
|
//
|
||||||
|
// Loaded FIRST, before canvas.js, so the canvas can ask for its colours as it
|
||||||
|
// draws. App chrome is themed by CSS custom properties on <html data-theme>;
|
||||||
|
// Konva cannot read CSS variables, so device colours come from here instead.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const THEME_KEY = "wiredraw.theme";
|
||||||
|
|
||||||
|
const THEME_PRESETS = {
|
||||||
|
dark: {
|
||||||
|
canvasBg: "#131320",
|
||||||
|
gridDot: "#2e2e50",
|
||||||
|
deviceStroke: "#5a5a8a",
|
||||||
|
deviceText: "#dde0f5",
|
||||||
|
deviceSubtext: "#8890b8",
|
||||||
|
deviceRef: "#99aaee", // reference designator on the device body
|
||||||
|
pinFill: "#0a0f1a",
|
||||||
|
pinStroke: "#5566aa",
|
||||||
|
notch: "#333355", // connector key notch
|
||||||
|
rowBg: "#0e0e1a", // cable conductor rows
|
||||||
|
wireLabel: "#aabbcc", // wire tag, drawn on the canvas background
|
||||||
|
harnessBg: "#12122a",
|
||||||
|
harnessEdge: "#4466cc",
|
||||||
|
harnessText: "#99bbff",
|
||||||
|
loomShadow: "#0a0a18",
|
||||||
|
// Device fills are tinted per type so you can tell a relay from a fuse at a
|
||||||
|
// glance. Light mode derives pastel equivalents from these same hues rather
|
||||||
|
// than keeping a second hand-tuned table in sync.
|
||||||
|
lightness: null,
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
canvasBg: "#f7f8fa",
|
||||||
|
gridDot: "#c3c9d8",
|
||||||
|
deviceStroke: "#8089a8",
|
||||||
|
deviceText: "#1b2030",
|
||||||
|
deviceSubtext: "#5d657e",
|
||||||
|
deviceRef: "#2c4b96",
|
||||||
|
pinFill: "#ffffff",
|
||||||
|
pinStroke: "#5a6ba8",
|
||||||
|
notch: "#b9bed4",
|
||||||
|
rowBg: "#eceef5",
|
||||||
|
wireLabel: "#41506b",
|
||||||
|
harnessBg: "#dfe4f3",
|
||||||
|
harnessEdge: "#4466cc",
|
||||||
|
harnessText: "#26418f",
|
||||||
|
loomShadow: "#b9c0d6",
|
||||||
|
lightness: 0.88,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEVICE_HUES = {
|
||||||
|
connector: "#12253a",
|
||||||
|
terminal_block: "#122a1a",
|
||||||
|
component: "#1e1230",
|
||||||
|
splice: "#2a1e10",
|
||||||
|
label: "#22220e",
|
||||||
|
fuse: "#2a1c08",
|
||||||
|
relay: "#0a1628",
|
||||||
|
switch: "#0a2218",
|
||||||
|
bulb: "#24220a",
|
||||||
|
motor: "#1a0a28",
|
||||||
|
diode: "#28081a",
|
||||||
|
resistor: "#1a1a08",
|
||||||
|
capacitor: "#081a1a",
|
||||||
|
ground: "#0e140e",
|
||||||
|
power: "#1a0808",
|
||||||
|
cable: "#1a1a1a",
|
||||||
|
pdm: "#2a1408",
|
||||||
|
group: "rgba(40,40,80,0.35)",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── colour maths ─────────────────────────────────────────────────────────────
|
||||||
|
function _hexToRgb(hex) {
|
||||||
|
const h = hex.replace("#", "");
|
||||||
|
const s = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
|
||||||
|
return {
|
||||||
|
r: parseInt(s.slice(0, 2), 16),
|
||||||
|
g: parseInt(s.slice(2, 4), 16),
|
||||||
|
b: parseInt(s.slice(4, 6), 16),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function _rgbToHsl({ r, g, b }) {
|
||||||
|
r /= 255; g /= 255; b /= 255;
|
||||||
|
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
||||||
|
const l = (max + min) / 2;
|
||||||
|
let h = 0, s = 0;
|
||||||
|
if (max !== min) {
|
||||||
|
const d = max - min;
|
||||||
|
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||||
|
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
||||||
|
else if (max === g) h = ((b - r) / d + 2) / 6;
|
||||||
|
else h = ((r - g) / d + 4) / 6;
|
||||||
|
}
|
||||||
|
return { h, s, l };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-light a colour to a target lightness, keeping its hue so the per-type
|
||||||
|
// distinction survives the theme switch.
|
||||||
|
function _relight(hex, targetL, satScale = 0.55) {
|
||||||
|
if (!hex || hex.startsWith("rgba")) return hex;
|
||||||
|
const { h, s } = _rgbToHsl(_hexToRgb(hex));
|
||||||
|
return `hsl(${Math.round(h * 360)}, ${Math.round(Math.min(1, s * satScale) * 100)}%, ${Math.round(targetL * 100)}%)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── state ────────────────────────────────────────────────────────────────────
|
||||||
|
const Theme = {
|
||||||
|
_state: {
|
||||||
|
mode: "dark",
|
||||||
|
canvasBg: null, // null = follow the mode preset
|
||||||
|
gridDot: null,
|
||||||
|
deviceStroke: null,
|
||||||
|
deviceFill: null, // null = tint per device type
|
||||||
|
},
|
||||||
|
_listeners: [],
|
||||||
|
|
||||||
|
load() {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(THEME_KEY);
|
||||||
|
if (raw) Object.assign(this._state, JSON.parse(raw));
|
||||||
|
} catch { /* corrupt or unavailable storage — fall back to defaults */ }
|
||||||
|
if (!THEME_PRESETS[this._state.mode]) this._state.mode = "dark";
|
||||||
|
return this._state;
|
||||||
|
},
|
||||||
|
|
||||||
|
save() {
|
||||||
|
try { localStorage.setItem(THEME_KEY, JSON.stringify(this._state)); }
|
||||||
|
catch { /* private mode — theme just will not persist */ }
|
||||||
|
},
|
||||||
|
|
||||||
|
get() { return { ...this._state }; },
|
||||||
|
preset() { return THEME_PRESETS[this._state.mode]; },
|
||||||
|
|
||||||
|
set(patch) {
|
||||||
|
Object.assign(this._state, patch);
|
||||||
|
this.save();
|
||||||
|
this.apply();
|
||||||
|
this._listeners.forEach((fn) => fn(this._state));
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this._state = { mode: this._state.mode, canvasBg: null, gridDot: null, deviceStroke: null, deviceFill: null };
|
||||||
|
this.save();
|
||||||
|
this.apply();
|
||||||
|
this._listeners.forEach((fn) => fn(this._state));
|
||||||
|
},
|
||||||
|
|
||||||
|
onChange(fn) { this._listeners.push(fn); },
|
||||||
|
|
||||||
|
// Push the theme into CSS. The app chrome reads data-theme; the canvas
|
||||||
|
// background and grid are plain custom properties so a user override is a
|
||||||
|
// one-line change with no repaint logic.
|
||||||
|
apply() {
|
||||||
|
const p = this.preset();
|
||||||
|
const root = document.documentElement;
|
||||||
|
root.setAttribute("data-theme", this._state.mode);
|
||||||
|
root.style.setProperty("--bg-canvas", this._state.canvasBg || p.canvasBg);
|
||||||
|
root.style.setProperty("--grid-dot", this._state.gridDot || p.gridDot);
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── canvas colours (Konva cannot read CSS variables) ──
|
||||||
|
deviceFill(type) {
|
||||||
|
if (this._state.deviceFill) return this._state.deviceFill;
|
||||||
|
const base = DEVICE_HUES[type] || "#1e1e2e";
|
||||||
|
const p = this.preset();
|
||||||
|
return p.lightness == null ? base : _relight(base, p.lightness);
|
||||||
|
},
|
||||||
|
deviceStroke() { return this._state.deviceStroke || this.preset().deviceStroke; },
|
||||||
|
deviceText() { return this.preset().deviceText; },
|
||||||
|
deviceSubtext() { return this.preset().deviceSubtext; },
|
||||||
|
deviceRef() { return this.preset().deviceRef; },
|
||||||
|
pinFill() { return this.preset().pinFill; },
|
||||||
|
pinStroke() { return this.preset().pinStroke; },
|
||||||
|
notch() { return this.preset().notch; },
|
||||||
|
rowBg() { return this.preset().rowBg; },
|
||||||
|
wireLabel() { return this.preset().wireLabel; },
|
||||||
|
harnessBg() { return this.preset().harnessBg; },
|
||||||
|
harnessEdge() { return this.preset().harnessEdge; },
|
||||||
|
harnessText() { return this.preset().harnessText; },
|
||||||
|
loomShadow() { return this.preset().loomShadow; },
|
||||||
|
canvasBg() { return this._state.canvasBg || this.preset().canvasBg; },
|
||||||
|
};
|
||||||
|
|
||||||
|
Theme.load();
|
||||||
|
Theme.apply();
|
||||||
Reference in New Issue
Block a user