-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy paththreads.js
More file actions
157 lines (138 loc) · 5.6 KB
/
Copy paththreads.js
File metadata and controls
157 lines (138 loc) · 5.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
// threads.js – SVG thread drawing between pins
import { THREAD_COLORS } from './cards.js';
// SVG element factory
const NS = 'http://www.w3.org/2000/svg';
function svgEl(tag) { return document.createElementNS(NS, tag); }
// Bezier path with stroke attributes
function makeStrokePath(x1, y1, cpX, cpY, x2, y2, stroke, width, dasharray, dashoffset) {
const p = svgEl('path');
p.setAttribute('d', `M ${x1} ${y1} Q ${cpX} ${cpY} ${x2} ${y2}`);
p.setAttribute('fill', 'none');
p.setAttribute('stroke', stroke);
p.setAttribute('stroke-width', String(width));
p.setAttribute('stroke-linecap', 'round');
if (dasharray) p.setAttribute('stroke-dasharray', dasharray);
if (dashoffset) p.setAttribute('stroke-dashoffset', String(dashoffset));
return p;
}
function resolveColor(c) {
return THREAD_COLORS[c] || c || '#cc3333';
}
// ── Render all threads ────────────────────────────────
export function renderAllThreads(svg, threads, pins, onThreadClick) {
[...svg.querySelectorAll('.thread-group')].forEach(el => el.remove());
// Add drop-shadow filter to <defs> once
const defs = svg.querySelector('defs');
if (defs && !defs.querySelector('#thread-shadow')) {
const filter = svgEl('filter');
filter.id = 'thread-shadow';
filter.setAttribute('x', '-30%'); filter.setAttribute('y', '-30%');
filter.setAttribute('width', '160%'); filter.setAttribute('height', '160%');
const ds = svgEl('feDropShadow');
ds.setAttribute('dx', '1'); ds.setAttribute('dy', '2.5');
ds.setAttribute('stdDeviation', '2.5');
ds.setAttribute('flood-color', '#000'); ds.setAttribute('flood-opacity', '0.38');
filter.appendChild(ds);
defs.appendChild(filter);
}
// Build pinMap once and reuse for all threads
const pinMap = buildPinMap(pins);
threads.forEach(th => {
const from = pinMap[th.fromPin];
const to = pinMap[th.toPin];
if (!from || !to) return;
renderThread(svg, th, from, to, onThreadClick);
});
}
// Build pinMap – also used by minimap via import
export function buildPinMap(pins) {
const map = {};
pins.forEach(p => { map[p.id] = p; });
return map;
}
export function renderThread(svg, thread, fromPin, toPin, onThreadClick) {
const g = svgEl('g');
g.classList.add('thread-group');
g.dataset.id = thread.id;
const x1 = fromPin.x, y1 = fromPin.y;
const x2 = toPin.x, y2 = toPin.y;
const midX = (x1 + x2) / 2;
const dist = Math.hypot(x2 - x1, y2 - y1);
const sag = Math.min(60, dist * 0.18);
const cpY = Math.max(y1, y2) + sag;
const cpX = midX;
const color = resolveColor(thread.color);
const width = thread.width || 1.8;
// Invisible wide hit-area for easier clicking
const hitPath = makeStrokePath(x1, y1, cpX, cpY, x2, y2, 'transparent', 12);
hitPath.style.pointerEvents = 'stroke';
hitPath.style.cursor = 'pointer';
if (onThreadClick) {
hitPath.addEventListener('click', e => { e.stopPropagation(); onThreadClick(thread.id); });
}
g.appendChild(hitPath);
// Visible stroke(s) inside a group with drop-shadow filter
const dash = `${8 * width / 1.8},${8 * width / 1.8}`;
const strokeG = svgEl('g');
strokeG.setAttribute('filter', 'url(#thread-shadow)');
strokeG.style.pointerEvents = 'none';
if (thread.striped && thread.stripeColor2) {
const color2 = resolveColor(thread.stripeColor2);
strokeG.appendChild(makeStrokePath(x1, y1, cpX, cpY, x2, y2, color, width, dash));
strokeG.appendChild(makeStrokePath(x1, y1, cpX, cpY, x2, y2, color2, width, dash, 8 * width / 1.8));
} else {
strokeG.appendChild(makeStrokePath(x1, y1, cpX, cpY, x2, y2, color, width));
}
g.appendChild(strokeG);
// Label
if (thread.label?.trim()) {
const labelX = cpX;
const labelY = (y1 + y2) / 2 + sag * 0.5;
const approxW = thread.label.length * 7 + 8;
const approxH = 16;
const bg = svgEl('rect');
bg.setAttribute('x', labelX - approxW / 2);
bg.setAttribute('y', labelY - approxH / 2);
bg.setAttribute('width', approxW);
bg.setAttribute('height', approxH);
bg.setAttribute('rx', '3');
bg.setAttribute('fill', 'rgba(255,250,240,0.88)');
bg.setAttribute('stroke', color);
bg.setAttribute('stroke-width', '0.8');
bg.style.pointerEvents = 'none';
const txt = svgEl('text');
txt.setAttribute('x', labelX);
txt.setAttribute('y', labelY);
txt.setAttribute('text-anchor', 'middle');
txt.setAttribute('dominant-baseline','middle');
txt.setAttribute('font-size', '11');
txt.setAttribute('font-family', 'Caveat, cursive');
txt.setAttribute('fill', '#1a1a1a');
txt.style.pointerEvents = 'none';
txt.textContent = thread.label;
g.appendChild(bg);
g.appendChild(txt);
}
svg.appendChild(g);
}
// ── Temporary thread drawn during drag ───────────────
export function drawTempThread(svg, x1, y1, x2, y2, color) {
let temp = svg.querySelector('#temp-thread');
if (!temp) {
temp = svgEl('path');
temp.id = 'temp-thread';
temp.setAttribute('fill', 'none');
temp.setAttribute('stroke-width', '2');
temp.setAttribute('stroke-linecap', 'round');
temp.setAttribute('stroke-dasharray', '6,4');
svg.appendChild(temp);
}
const midX = (x1 + x2) / 2;
const sag = Math.min(40, Math.hypot(x2 - x1, y2 - y1) * 0.15);
const cpY = Math.max(y1, y2) + sag;
temp.setAttribute('d', `M ${x1} ${y1} Q ${midX} ${cpY} ${x2} ${y2}`);
temp.setAttribute('stroke', resolveColor(color));
}
export function removeTempThread(svg) {
svg.querySelector('#temp-thread')?.remove();
}