@shumoku/renderer 0.2.1 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/html/index.d.ts +25 -0
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +742 -158
- package/dist/html/index.js.map +1 -1
- package/dist/html/navigation.d.ts +54 -0
- package/dist/html/navigation.d.ts.map +1 -0
- package/dist/html/navigation.js +210 -0
- package/dist/html/navigation.js.map +1 -0
- package/dist/html/runtime.d.ts +2 -1
- package/dist/html/runtime.d.ts.map +1 -1
- package/dist/html/runtime.js +245 -482
- package/dist/html/runtime.js.map +1 -1
- package/dist/html/spotlight.d.ts +9 -0
- package/dist/html/spotlight.d.ts.map +1 -0
- package/dist/html/spotlight.js +119 -0
- package/dist/html/spotlight.js.map +1 -0
- package/dist/html/tooltip.d.ts +14 -0
- package/dist/html/tooltip.d.ts.map +1 -0
- package/dist/html/tooltip.js +133 -0
- package/dist/html/tooltip.js.map +1 -0
- package/dist/html/viewbox.d.ts +14 -0
- package/dist/html/viewbox.d.ts.map +1 -0
- package/dist/html/viewbox.js +21 -0
- package/dist/html/viewbox.js.map +1 -0
- package/dist/iife-string.d.ts +2 -0
- package/dist/iife-string.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/shumoku-interactive.iife.js +25 -20
- package/dist/svg.d.ts +27 -0
- package/dist/svg.d.ts.map +1 -1
- package/dist/svg.js +202 -101
- package/dist/svg.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/build-iife-string.ts +26 -19
- package/src/html/index.ts +880 -226
- package/src/html/navigation.ts +256 -0
- package/src/html/runtime.ts +412 -654
- package/src/html/spotlight.ts +135 -0
- package/src/html/tooltip.ts +141 -0
- package/src/html/viewbox.ts +28 -0
- package/src/index.ts +25 -22
- package/src/svg.ts +1640 -1502
- package/src/types.ts +127 -125
package/dist/html/runtime.js
CHANGED
|
@@ -1,227 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Interactive Runtime -
|
|
2
|
+
* Interactive Runtime - Mobile-first pan/zoom with tap tooltips and spotlight effect
|
|
3
|
+
* Google Maps style touch: 1 finger = page scroll (in HTML) / pan (here), 2 fingers = pinch zoom
|
|
3
4
|
*/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
function getTooltip() {
|
|
9
|
-
if (!tooltip) {
|
|
10
|
-
tooltip = document.createElement('div');
|
|
11
|
-
tooltip.style.cssText = `
|
|
12
|
-
position: fixed;
|
|
13
|
-
z-index: 10000;
|
|
14
|
-
padding: 6px 10px;
|
|
15
|
-
background: #1e293b;
|
|
16
|
-
color: #fff;
|
|
17
|
-
font-size: 12px;
|
|
18
|
-
border-radius: 4px;
|
|
19
|
-
pointer-events: none;
|
|
20
|
-
opacity: 0;
|
|
21
|
-
transition: opacity 0.15s;
|
|
22
|
-
font-family: system-ui, sans-serif;
|
|
23
|
-
max-width: 300px;
|
|
24
|
-
white-space: pre-line;
|
|
25
|
-
`;
|
|
26
|
-
document.body.appendChild(tooltip);
|
|
27
|
-
}
|
|
28
|
-
return tooltip;
|
|
29
|
-
}
|
|
30
|
-
function showTooltip(text, x, y) {
|
|
31
|
-
const t = getTooltip();
|
|
32
|
-
t.textContent = text;
|
|
33
|
-
// Position tooltip, keeping it within viewport
|
|
34
|
-
const pad = 12;
|
|
35
|
-
let left = x + pad;
|
|
36
|
-
let top = y + pad;
|
|
37
|
-
// Adjust if tooltip would go off-screen
|
|
38
|
-
requestAnimationFrame(() => {
|
|
39
|
-
const rect = t.getBoundingClientRect();
|
|
40
|
-
if (left + rect.width > window.innerWidth - pad) {
|
|
41
|
-
left = x - rect.width - pad;
|
|
42
|
-
}
|
|
43
|
-
if (top + rect.height > window.innerHeight - pad) {
|
|
44
|
-
top = y - rect.height - pad;
|
|
45
|
-
}
|
|
46
|
-
t.style.left = `${Math.max(pad, left)}px`;
|
|
47
|
-
t.style.top = `${Math.max(pad, top)}px`;
|
|
48
|
-
});
|
|
49
|
-
t.style.left = `${left}px`;
|
|
50
|
-
t.style.top = `${top}px`;
|
|
51
|
-
t.style.opacity = '1';
|
|
52
|
-
}
|
|
53
|
-
function hideTooltip() {
|
|
54
|
-
if (tooltip) {
|
|
55
|
-
tooltip.style.opacity = '0';
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
function getOverlay() {
|
|
59
|
-
if (!overlay) {
|
|
60
|
-
overlay = document.createElement('div');
|
|
61
|
-
overlay.style.cssText = `
|
|
62
|
-
position: fixed;
|
|
63
|
-
top: 0;
|
|
64
|
-
left: 0;
|
|
65
|
-
right: 0;
|
|
66
|
-
bottom: 0;
|
|
67
|
-
background: rgba(0, 0, 0, 0.5);
|
|
68
|
-
pointer-events: none;
|
|
69
|
-
opacity: 0;
|
|
70
|
-
transition: opacity 0.15s ease;
|
|
71
|
-
z-index: 9998;
|
|
72
|
-
`;
|
|
73
|
-
document.body.appendChild(overlay);
|
|
74
|
-
}
|
|
75
|
-
return overlay;
|
|
76
|
-
}
|
|
77
|
-
let highlightContainer = null;
|
|
78
|
-
let currentMiniSvg = null;
|
|
79
|
-
function getHighlightContainer() {
|
|
80
|
-
if (!highlightContainer) {
|
|
81
|
-
highlightContainer = document.createElement('div');
|
|
82
|
-
highlightContainer.style.cssText = `
|
|
83
|
-
position: fixed;
|
|
84
|
-
top: 0;
|
|
85
|
-
left: 0;
|
|
86
|
-
pointer-events: none;
|
|
87
|
-
z-index: 9999;
|
|
88
|
-
`;
|
|
89
|
-
document.body.appendChild(highlightContainer);
|
|
90
|
-
}
|
|
91
|
-
return highlightContainer;
|
|
92
|
-
}
|
|
93
|
-
function updateHighlightPosition() {
|
|
94
|
-
if (!currentHighlight || !currentMiniSvg)
|
|
95
|
-
return;
|
|
96
|
-
// Check if element is still in DOM (React may have replaced it)
|
|
97
|
-
if (!document.contains(currentHighlight)) {
|
|
98
|
-
// Element was removed, clear highlight
|
|
99
|
-
highlightElement(null);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
const svg = currentHighlight.closest('svg');
|
|
103
|
-
if (!svg)
|
|
104
|
-
return;
|
|
105
|
-
// Use getBoundingClientRect for screen position
|
|
106
|
-
const rect = svg.getBoundingClientRect();
|
|
107
|
-
// Update viewBox to match current zoom level
|
|
108
|
-
const viewBox = svg.getAttribute('viewBox');
|
|
109
|
-
if (viewBox) {
|
|
110
|
-
currentMiniSvg.setAttribute('viewBox', viewBox);
|
|
111
|
-
}
|
|
112
|
-
currentMiniSvg.style.left = `${rect.left}px`;
|
|
113
|
-
currentMiniSvg.style.top = `${rect.top}px`;
|
|
114
|
-
currentMiniSvg.style.width = `${rect.width}px`;
|
|
115
|
-
currentMiniSvg.style.height = `${rect.height}px`;
|
|
116
|
-
}
|
|
117
|
-
function highlightElement(el) {
|
|
118
|
-
// Skip if already highlighting the same element
|
|
119
|
-
if (el === currentHighlight) {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
// Remove previous highlight
|
|
123
|
-
if (currentHighlight) {
|
|
124
|
-
currentHighlight.classList.remove('shumoku-highlight');
|
|
125
|
-
}
|
|
126
|
-
// Clear previous mini SVG
|
|
127
|
-
const container = getHighlightContainer();
|
|
128
|
-
container.innerHTML = '';
|
|
129
|
-
currentMiniSvg = null;
|
|
130
|
-
currentHighlight = el;
|
|
131
|
-
const ov = getOverlay();
|
|
132
|
-
if (el) {
|
|
133
|
-
el.classList.add('shumoku-highlight');
|
|
134
|
-
const svg = el.closest('svg');
|
|
135
|
-
if (svg) {
|
|
136
|
-
const viewBox = svg.getAttribute('viewBox');
|
|
137
|
-
if (viewBox) {
|
|
138
|
-
// Clone the highlighted element
|
|
139
|
-
const clone = el.cloneNode(true);
|
|
140
|
-
clone.classList.remove('shumoku-highlight');
|
|
141
|
-
// Create mini SVG with same viewBox
|
|
142
|
-
const miniSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
143
|
-
miniSvg.setAttribute('viewBox', viewBox);
|
|
144
|
-
miniSvg.style.cssText = `
|
|
145
|
-
position: absolute;
|
|
146
|
-
overflow: visible;
|
|
147
|
-
filter: drop-shadow(0 0 4px #fff) drop-shadow(0 0 8px #fff) drop-shadow(0 0 12px rgba(100, 150, 255, 0.8));
|
|
148
|
-
`;
|
|
149
|
-
// Copy defs (for gradients, patterns, etc.)
|
|
150
|
-
const defs = svg.querySelector('defs');
|
|
151
|
-
if (defs) {
|
|
152
|
-
miniSvg.appendChild(defs.cloneNode(true));
|
|
153
|
-
}
|
|
154
|
-
miniSvg.appendChild(clone);
|
|
155
|
-
container.appendChild(miniSvg);
|
|
156
|
-
currentMiniSvg = miniSvg;
|
|
157
|
-
// Update position
|
|
158
|
-
updateHighlightPosition();
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
ov.style.opacity = '1';
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
ov.style.opacity = '0';
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
function injectHighlightStyles() {
|
|
168
|
-
// No styles needed for current approach
|
|
169
|
-
}
|
|
170
|
-
function getTooltipInfo(el) {
|
|
171
|
-
// Port tooltip (ports are now separate layer)
|
|
172
|
-
const port = el.closest('.port[data-port]');
|
|
173
|
-
if (port) {
|
|
174
|
-
const portId = port.getAttribute('data-port') || '';
|
|
175
|
-
const deviceId = port.getAttribute('data-port-device') || '';
|
|
176
|
-
return { text: `${deviceId}:${portId}`, element: port };
|
|
177
|
-
}
|
|
178
|
-
// Link tooltip
|
|
179
|
-
const linkGroup = el.closest('.link-group[data-link-id]');
|
|
180
|
-
if (linkGroup) {
|
|
181
|
-
const from = linkGroup.getAttribute('data-link-from') || '';
|
|
182
|
-
const to = linkGroup.getAttribute('data-link-to') || '';
|
|
183
|
-
const bw = linkGroup.getAttribute('data-link-bandwidth');
|
|
184
|
-
const vlan = linkGroup.getAttribute('data-link-vlan');
|
|
185
|
-
let text = `${from} ↔ ${to}`;
|
|
186
|
-
if (bw)
|
|
187
|
-
text += `\n${bw}`;
|
|
188
|
-
if (vlan)
|
|
189
|
-
text += `\nVLAN: ${vlan}`;
|
|
190
|
-
return { text, element: linkGroup };
|
|
191
|
-
}
|
|
192
|
-
// Device tooltip (single label only)
|
|
193
|
-
const node = el.closest('.node[data-id]');
|
|
194
|
-
if (node) {
|
|
195
|
-
const json = node.getAttribute('data-device-json');
|
|
196
|
-
let text;
|
|
197
|
-
if (json) {
|
|
198
|
-
try {
|
|
199
|
-
const data = JSON.parse(json);
|
|
200
|
-
text = data.label || data.id;
|
|
201
|
-
}
|
|
202
|
-
catch {
|
|
203
|
-
text = node.getAttribute('data-id') || '';
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
else {
|
|
207
|
-
text = node.getAttribute('data-id') || '';
|
|
208
|
-
}
|
|
209
|
-
return { text, element: node };
|
|
210
|
-
}
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
213
|
-
function parseViewBox(svg) {
|
|
214
|
-
const vb = svg.getAttribute('viewBox');
|
|
215
|
-
if (!vb)
|
|
216
|
-
return null;
|
|
217
|
-
const parts = vb.split(/\s+|,/).map(Number);
|
|
218
|
-
if (parts.length !== 4 || parts.some(Number.isNaN))
|
|
219
|
-
return null;
|
|
220
|
-
return { x: parts[0], y: parts[1], width: parts[2], height: parts[3] };
|
|
221
|
-
}
|
|
222
|
-
function setViewBox(svg, vb) {
|
|
223
|
-
svg.setAttribute('viewBox', `${vb.x} ${vb.y} ${vb.width} ${vb.height}`);
|
|
224
|
-
}
|
|
5
|
+
import { destroySpotlight, getCurrentHighlight, highlightElement, updateHighlightPosition, } from './spotlight.js';
|
|
6
|
+
import { destroyTooltip, getTooltipInfo, hideTooltip, showTooltip } from './tooltip.js';
|
|
7
|
+
import { cloneViewBox, parseViewBox, setViewBox } from './viewbox.js';
|
|
8
|
+
const ZOOM_FACTOR = 1.2;
|
|
225
9
|
export function initInteractive(options) {
|
|
226
10
|
const target = typeof options.target === 'string' ? document.querySelector(options.target) : options.target;
|
|
227
11
|
if (!target)
|
|
@@ -229,26 +13,23 @@ export function initInteractive(options) {
|
|
|
229
13
|
const svg = target.closest('svg') || target.querySelector('svg') || target;
|
|
230
14
|
if (!(svg instanceof SVGSVGElement))
|
|
231
15
|
throw new Error('SVG element not found');
|
|
232
|
-
// Inject highlight styles
|
|
233
|
-
injectHighlightStyles();
|
|
234
|
-
// Pan/Zoom settings
|
|
235
16
|
const panZoomEnabled = options.panZoom?.enabled ?? true;
|
|
236
17
|
const minScale = options.panZoom?.minScale ?? 0.1;
|
|
237
18
|
const maxScale = options.panZoom?.maxScale ?? 10;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
originalViewBox
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
19
|
+
let originalViewBox = parseViewBox(svg);
|
|
20
|
+
if (!originalViewBox) {
|
|
21
|
+
const bbox = svg.getBBox();
|
|
22
|
+
originalViewBox = { x: bbox.x, y: bbox.y, width: bbox.width, height: bbox.height };
|
|
23
|
+
setViewBox(svg, originalViewBox, updateHighlightPosition);
|
|
24
|
+
}
|
|
25
|
+
let tooltipActive = false;
|
|
26
|
+
const mouseDrag = {
|
|
27
|
+
active: false,
|
|
28
|
+
startX: 0,
|
|
29
|
+
startY: 0,
|
|
30
|
+
startViewBox: null,
|
|
249
31
|
};
|
|
250
|
-
|
|
251
|
-
// Calculate current scale
|
|
32
|
+
let pinchState = null;
|
|
252
33
|
const getScale = () => {
|
|
253
34
|
if (!originalViewBox)
|
|
254
35
|
return 1;
|
|
@@ -257,252 +38,244 @@ export function initInteractive(options) {
|
|
|
257
38
|
return 1;
|
|
258
39
|
return originalViewBox.width / current.width;
|
|
259
40
|
};
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
let isPanning = false;
|
|
264
|
-
let panStartX = 0;
|
|
265
|
-
let panStartY = 0;
|
|
266
|
-
let panStartViewBox = null;
|
|
267
|
-
// Pinch state
|
|
268
|
-
let initialPinchDistance = 0;
|
|
269
|
-
let pinchStartViewBox = null;
|
|
270
|
-
let pinchCenter = null;
|
|
271
|
-
// Mouse move handler (desktop hover)
|
|
272
|
-
const handleMouseMove = (e) => {
|
|
273
|
-
if (isPanning)
|
|
274
|
-
return;
|
|
275
|
-
if (isTouchDevice)
|
|
276
|
-
return; // Skip on touch devices
|
|
277
|
-
const me = e;
|
|
278
|
-
const info = getTooltipInfo(me.target);
|
|
279
|
-
if (info) {
|
|
280
|
-
showTooltip(info.text, me.clientX, me.clientY);
|
|
281
|
-
highlightElement(info.element);
|
|
41
|
+
const resetView = () => {
|
|
42
|
+
if (originalViewBox) {
|
|
43
|
+
setViewBox(svg, originalViewBox, updateHighlightPosition);
|
|
282
44
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
45
|
+
};
|
|
46
|
+
// ============================================
|
|
47
|
+
// Touch Events (Mobile: 2-finger for pan/zoom)
|
|
48
|
+
// ============================================
|
|
49
|
+
const getTouchDistance = (touches) => {
|
|
50
|
+
if (touches.length < 2)
|
|
51
|
+
return 0;
|
|
52
|
+
const dx = touches[1].clientX - touches[0].clientX;
|
|
53
|
+
const dy = touches[1].clientY - touches[0].clientY;
|
|
54
|
+
return Math.hypot(dx, dy);
|
|
55
|
+
};
|
|
56
|
+
const getTouchCenter = (touches) => ({
|
|
57
|
+
x: (touches[0].clientX + touches[1].clientX) / 2,
|
|
58
|
+
y: (touches[0].clientY + touches[1].clientY) / 2,
|
|
59
|
+
});
|
|
60
|
+
const handleTouchStart = (e) => {
|
|
61
|
+
if (e.touches.length >= 2 && panZoomEnabled) {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
const dist = getTouchDistance(e.touches);
|
|
64
|
+
const center = getTouchCenter(e.touches);
|
|
65
|
+
const vb = parseViewBox(svg);
|
|
66
|
+
if (vb) {
|
|
67
|
+
const rect = svg.getBoundingClientRect();
|
|
68
|
+
pinchState = {
|
|
69
|
+
initialDist: dist,
|
|
70
|
+
startViewBox: cloneViewBox(vb),
|
|
71
|
+
centerX: vb.x + vb.width * ((center.x - rect.left) / rect.width),
|
|
72
|
+
centerY: vb.y + vb.height * ((center.y - rect.top) / rect.height),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (tooltipActive) {
|
|
76
|
+
hideTooltip();
|
|
77
|
+
highlightElement(null);
|
|
78
|
+
tooltipActive = false;
|
|
79
|
+
}
|
|
286
80
|
}
|
|
287
81
|
};
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
82
|
+
const handleTouchMove = (e) => {
|
|
83
|
+
if (e.touches.length >= 2 && pinchState && panZoomEnabled) {
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
const dist = getTouchDistance(e.touches);
|
|
86
|
+
const center = getTouchCenter(e.touches);
|
|
87
|
+
if (dist === 0 || pinchState.initialDist === 0)
|
|
88
|
+
return;
|
|
89
|
+
const scale = dist / pinchState.initialDist;
|
|
90
|
+
const newWidth = pinchState.startViewBox.width / scale;
|
|
91
|
+
const newHeight = pinchState.startViewBox.height / scale;
|
|
92
|
+
if (!originalViewBox)
|
|
93
|
+
return;
|
|
94
|
+
const newScale = originalViewBox.width / newWidth;
|
|
95
|
+
if (newScale < minScale || newScale > maxScale)
|
|
96
|
+
return;
|
|
97
|
+
const rect = svg.getBoundingClientRect();
|
|
98
|
+
const mx = (center.x - rect.left) / rect.width;
|
|
99
|
+
const my = (center.y - rect.top) / rect.height;
|
|
100
|
+
setViewBox(svg, {
|
|
101
|
+
x: pinchState.centerX - newWidth * mx,
|
|
102
|
+
y: pinchState.centerY - newHeight * my,
|
|
103
|
+
width: newWidth,
|
|
104
|
+
height: newHeight,
|
|
105
|
+
}, updateHighlightPosition);
|
|
106
|
+
}
|
|
296
107
|
};
|
|
297
|
-
|
|
108
|
+
const handleTouchEnd = (e) => {
|
|
109
|
+
if (e.touches.length < 2) {
|
|
110
|
+
pinchState = null;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
// ============================================
|
|
114
|
+
// Mouse Events (Desktop)
|
|
115
|
+
// ============================================
|
|
298
116
|
const handleMouseDown = (e) => {
|
|
299
|
-
if (!panZoomEnabled)
|
|
117
|
+
if (e.button !== 0 || !panZoomEnabled)
|
|
300
118
|
return;
|
|
301
|
-
const
|
|
302
|
-
if (
|
|
303
|
-
return; // Left button only
|
|
304
|
-
// Don't start pan if clicking on interactive element
|
|
305
|
-
const info = getTooltipInfo(me.target);
|
|
306
|
-
if (info)
|
|
119
|
+
const vb = parseViewBox(svg);
|
|
120
|
+
if (!vb)
|
|
307
121
|
return;
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
122
|
+
mouseDrag.active = true;
|
|
123
|
+
mouseDrag.startX = e.clientX;
|
|
124
|
+
mouseDrag.startY = e.clientY;
|
|
125
|
+
mouseDrag.startViewBox = cloneViewBox(vb);
|
|
312
126
|
svg.style.cursor = 'grabbing';
|
|
313
|
-
|
|
127
|
+
if (tooltipActive) {
|
|
128
|
+
hideTooltip();
|
|
129
|
+
highlightElement(null);
|
|
130
|
+
tooltipActive = false;
|
|
131
|
+
}
|
|
314
132
|
};
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
133
|
+
const handleMouseMove = (e) => {
|
|
134
|
+
if (mouseDrag.active && mouseDrag.startViewBox && panZoomEnabled) {
|
|
135
|
+
const dx = e.clientX - mouseDrag.startX;
|
|
136
|
+
const dy = e.clientY - mouseDrag.startY;
|
|
137
|
+
const rect = svg.getBoundingClientRect();
|
|
138
|
+
const scaleX = mouseDrag.startViewBox.width / rect.width;
|
|
139
|
+
const scaleY = mouseDrag.startViewBox.height / rect.height;
|
|
140
|
+
setViewBox(svg, {
|
|
141
|
+
x: mouseDrag.startViewBox.x - dx * scaleX,
|
|
142
|
+
y: mouseDrag.startViewBox.y - dy * scaleY,
|
|
143
|
+
width: mouseDrag.startViewBox.width,
|
|
144
|
+
height: mouseDrag.startViewBox.height,
|
|
145
|
+
}, updateHighlightPosition);
|
|
146
|
+
}
|
|
147
|
+
else if (!mouseDrag.active) {
|
|
148
|
+
// Hover: show tooltip and highlight
|
|
149
|
+
const info = getTooltipInfo(e.target);
|
|
150
|
+
if (info) {
|
|
151
|
+
showTooltip(info.text, e.clientX, e.clientY);
|
|
152
|
+
highlightElement(info.element);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
hideTooltip();
|
|
156
|
+
highlightElement(null);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
332
159
|
};
|
|
333
|
-
// Mouse up - end pan
|
|
334
160
|
const handleMouseUp = () => {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
161
|
+
mouseDrag.active = false;
|
|
162
|
+
mouseDrag.startViewBox = null;
|
|
163
|
+
svg.style.cursor = '';
|
|
164
|
+
};
|
|
165
|
+
const handleMouseLeave = () => {
|
|
166
|
+
if (!mouseDrag.active && !tooltipActive) {
|
|
167
|
+
hideTooltip();
|
|
168
|
+
highlightElement(null);
|
|
339
169
|
}
|
|
340
170
|
};
|
|
341
|
-
// Wheel zoom
|
|
342
171
|
const handleWheel = (e) => {
|
|
343
172
|
if (!panZoomEnabled)
|
|
344
173
|
return;
|
|
345
|
-
|
|
346
|
-
we.preventDefault();
|
|
174
|
+
e.preventDefault();
|
|
347
175
|
const vb = parseViewBox(svg);
|
|
348
176
|
if (!vb || !originalViewBox)
|
|
349
177
|
return;
|
|
350
178
|
const rect = svg.getBoundingClientRect();
|
|
351
|
-
const
|
|
352
|
-
const
|
|
353
|
-
const
|
|
354
|
-
const mouseY = vb.y + vb.height * mouseYRatio;
|
|
355
|
-
const zoomFactor = we.deltaY > 0 ? ZOOM_FACTOR : 1 / ZOOM_FACTOR;
|
|
179
|
+
const mouseX = vb.x + vb.width * ((e.clientX - rect.left) / rect.width);
|
|
180
|
+
const mouseY = vb.y + vb.height * ((e.clientY - rect.top) / rect.height);
|
|
181
|
+
const zoomFactor = e.deltaY > 0 ? ZOOM_FACTOR : 1 / ZOOM_FACTOR;
|
|
356
182
|
const newWidth = vb.width * zoomFactor;
|
|
357
183
|
const newHeight = vb.height * zoomFactor;
|
|
358
|
-
// Check scale limits
|
|
359
184
|
const newScale = originalViewBox.width / newWidth;
|
|
360
185
|
if (newScale < minScale || newScale > maxScale)
|
|
361
186
|
return;
|
|
187
|
+
const xRatio = (e.clientX - rect.left) / rect.width;
|
|
188
|
+
const yRatio = (e.clientY - rect.top) / rect.height;
|
|
362
189
|
setViewBox(svg, {
|
|
363
|
-
x: mouseX - newWidth *
|
|
364
|
-
y: mouseY - newHeight *
|
|
190
|
+
x: mouseX - newWidth * xRatio,
|
|
191
|
+
y: mouseY - newHeight * yRatio,
|
|
365
192
|
width: newWidth,
|
|
366
193
|
height: newHeight,
|
|
367
|
-
});
|
|
194
|
+
}, updateHighlightPosition);
|
|
368
195
|
};
|
|
369
|
-
//
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
panStartX = touch.clientX;
|
|
382
|
-
panStartY = touch.clientY;
|
|
383
|
-
panStartViewBox = parseViewBox(svg);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
else if (te.touches.length === 2 && panZoomEnabled) {
|
|
387
|
-
// Two touches - start pinch
|
|
388
|
-
isPanning = false;
|
|
389
|
-
const t1 = te.touches[0];
|
|
390
|
-
const t2 = te.touches[1];
|
|
391
|
-
initialPinchDistance = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY);
|
|
392
|
-
pinchStartViewBox = parseViewBox(svg);
|
|
393
|
-
// Calculate pinch center in viewBox coordinates
|
|
394
|
-
const rect = svg.getBoundingClientRect();
|
|
395
|
-
const centerX = (t1.clientX + t2.clientX) / 2;
|
|
396
|
-
const centerY = (t1.clientY + t2.clientY) / 2;
|
|
397
|
-
const vb = pinchStartViewBox;
|
|
398
|
-
if (vb) {
|
|
399
|
-
const xRatio = (centerX - rect.left) / rect.width;
|
|
400
|
-
const yRatio = (centerY - rect.top) / rect.height;
|
|
401
|
-
pinchCenter = {
|
|
402
|
-
x: vb.x + vb.width * xRatio,
|
|
403
|
-
y: vb.y + vb.height * yRatio,
|
|
404
|
-
};
|
|
196
|
+
// ============================================
|
|
197
|
+
// Hierarchical Navigation
|
|
198
|
+
// ============================================
|
|
199
|
+
const handleSubgraphClick = (e) => {
|
|
200
|
+
const target = e.target;
|
|
201
|
+
const subgraph = target.closest('.subgraph[data-has-sheet]');
|
|
202
|
+
if (subgraph) {
|
|
203
|
+
const sheetId = subgraph.getAttribute('data-sheet-id');
|
|
204
|
+
if (sheetId) {
|
|
205
|
+
e.preventDefault();
|
|
206
|
+
e.stopPropagation();
|
|
207
|
+
dispatchNavigateEvent(sheetId);
|
|
405
208
|
}
|
|
406
209
|
}
|
|
407
210
|
};
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const rect = svg.getBoundingClientRect();
|
|
415
|
-
const dx = touch.clientX - panStartX;
|
|
416
|
-
const dy = touch.clientY - panStartY;
|
|
417
|
-
const scaleX = panStartViewBox.width / rect.width;
|
|
418
|
-
const scaleY = panStartViewBox.height / rect.height;
|
|
419
|
-
setViewBox(svg, {
|
|
420
|
-
x: panStartViewBox.x - dx * scaleX,
|
|
421
|
-
y: panStartViewBox.y - dy * scaleY,
|
|
422
|
-
width: panStartViewBox.width,
|
|
423
|
-
height: panStartViewBox.height,
|
|
424
|
-
});
|
|
425
|
-
te.preventDefault();
|
|
426
|
-
}
|
|
427
|
-
else if (te.touches.length === 2 && pinchStartViewBox && pinchCenter && originalViewBox) {
|
|
428
|
-
// Pinch zoom
|
|
429
|
-
const t1 = te.touches[0];
|
|
430
|
-
const t2 = te.touches[1];
|
|
431
|
-
const distance = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY);
|
|
432
|
-
const scale = distance / initialPinchDistance;
|
|
433
|
-
const newWidth = pinchStartViewBox.width / scale;
|
|
434
|
-
const newHeight = pinchStartViewBox.height / scale;
|
|
435
|
-
// Check scale limits
|
|
436
|
-
const newScale = originalViewBox.width / newWidth;
|
|
437
|
-
if (newScale < minScale || newScale > maxScale)
|
|
438
|
-
return;
|
|
439
|
-
// Zoom towards pinch center
|
|
440
|
-
const rect = svg.getBoundingClientRect();
|
|
441
|
-
const centerX = (t1.clientX + t2.clientX) / 2;
|
|
442
|
-
const centerY = (t1.clientY + t2.clientY) / 2;
|
|
443
|
-
const xRatio = (centerX - rect.left) / rect.width;
|
|
444
|
-
const yRatio = (centerY - rect.top) / rect.height;
|
|
445
|
-
setViewBox(svg, {
|
|
446
|
-
x: pinchCenter.x - newWidth * xRatio,
|
|
447
|
-
y: pinchCenter.y - newHeight * yRatio,
|
|
448
|
-
width: newWidth,
|
|
449
|
-
height: newHeight,
|
|
450
|
-
});
|
|
451
|
-
te.preventDefault();
|
|
452
|
-
}
|
|
211
|
+
const dispatchNavigateEvent = (sheetId) => {
|
|
212
|
+
const event = new CustomEvent('shumoku:navigate', {
|
|
213
|
+
detail: { sheetId },
|
|
214
|
+
bubbles: true,
|
|
215
|
+
});
|
|
216
|
+
document.dispatchEvent(event);
|
|
453
217
|
};
|
|
454
|
-
//
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
218
|
+
// ============================================
|
|
219
|
+
// Tap for tooltip (touch devices)
|
|
220
|
+
// ============================================
|
|
221
|
+
let tapStart = null;
|
|
222
|
+
const handleTouchStartForTap = (e) => {
|
|
223
|
+
if (e.touches.length === 1) {
|
|
224
|
+
tapStart = {
|
|
225
|
+
x: e.touches[0].clientX,
|
|
226
|
+
y: e.touches[0].clientY,
|
|
227
|
+
time: Date.now(),
|
|
228
|
+
};
|
|
462
229
|
}
|
|
463
|
-
else
|
|
464
|
-
|
|
465
|
-
pinchStartViewBox = null;
|
|
466
|
-
pinchCenter = null;
|
|
467
|
-
isPanning = true;
|
|
468
|
-
const touch = te.touches[0];
|
|
469
|
-
panStartX = touch.clientX;
|
|
470
|
-
panStartY = touch.clientY;
|
|
471
|
-
panStartViewBox = parseViewBox(svg);
|
|
230
|
+
else {
|
|
231
|
+
tapStart = null;
|
|
472
232
|
}
|
|
473
233
|
};
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
return;
|
|
478
|
-
if (isPanning)
|
|
234
|
+
const handleTouchEndForTap = (e) => {
|
|
235
|
+
if (!tapStart || e.touches.length > 0) {
|
|
236
|
+
tapStart = null;
|
|
479
237
|
return;
|
|
480
|
-
const me = e;
|
|
481
|
-
const targetEl = e.target;
|
|
482
|
-
const info = getTooltipInfo(targetEl);
|
|
483
|
-
if (info) {
|
|
484
|
-
// Show tooltip at tap position
|
|
485
|
-
showTooltip(info.text, me.clientX, me.clientY);
|
|
486
|
-
highlightElement(info.element);
|
|
487
|
-
touchTooltipActive = true;
|
|
488
|
-
e.preventDefault();
|
|
489
238
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
239
|
+
const touch = e.changedTouches[0];
|
|
240
|
+
const dx = touch.clientX - tapStart.x;
|
|
241
|
+
const dy = touch.clientY - tapStart.y;
|
|
242
|
+
const dt = Date.now() - tapStart.time;
|
|
243
|
+
if (Math.hypot(dx, dy) < 10 && dt < 300) {
|
|
244
|
+
const targetEl = document.elementFromPoint(touch.clientX, touch.clientY);
|
|
245
|
+
if (targetEl) {
|
|
246
|
+
// Check for hierarchical navigation first
|
|
247
|
+
const subgraph = targetEl.closest('.subgraph[data-has-sheet]');
|
|
248
|
+
if (subgraph) {
|
|
249
|
+
const sheetId = subgraph.getAttribute('data-sheet-id');
|
|
250
|
+
if (sheetId) {
|
|
251
|
+
dispatchNavigateEvent(sheetId);
|
|
252
|
+
tapStart = null;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Otherwise show tooltip
|
|
257
|
+
const info = getTooltipInfo(targetEl);
|
|
258
|
+
if (info) {
|
|
259
|
+
showTooltip(info.text, touch.clientX, touch.clientY);
|
|
260
|
+
highlightElement(info.element);
|
|
261
|
+
tooltipActive = true;
|
|
262
|
+
}
|
|
263
|
+
else if (tooltipActive) {
|
|
264
|
+
hideTooltip();
|
|
265
|
+
highlightElement(null);
|
|
266
|
+
tooltipActive = false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
495
269
|
}
|
|
270
|
+
tapStart = null;
|
|
496
271
|
};
|
|
497
|
-
//
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
};
|
|
501
|
-
// Track viewBox changes for pan/zoom
|
|
272
|
+
// ============================================
|
|
273
|
+
// Track viewBox changes for smooth highlight during pan/zoom
|
|
274
|
+
// ============================================
|
|
502
275
|
let rafId = null;
|
|
503
276
|
let lastViewBox = '';
|
|
504
277
|
const trackViewBox = () => {
|
|
505
|
-
if (
|
|
278
|
+
if (getCurrentHighlight()) {
|
|
506
279
|
const viewBox = svg.getAttribute('viewBox') || '';
|
|
507
280
|
if (viewBox !== lastViewBox) {
|
|
508
281
|
lastViewBox = viewBox;
|
|
@@ -511,61 +284,50 @@ export function initInteractive(options) {
|
|
|
511
284
|
}
|
|
512
285
|
rafId = requestAnimationFrame(trackViewBox);
|
|
513
286
|
};
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
const resetView = () => {
|
|
517
|
-
if (originalViewBox) {
|
|
518
|
-
setViewBox(svg, originalViewBox);
|
|
519
|
-
}
|
|
287
|
+
const handlePositionUpdate = () => {
|
|
288
|
+
updateHighlightPosition();
|
|
520
289
|
};
|
|
521
|
-
//
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
//
|
|
526
|
-
svg.addEventListener('
|
|
527
|
-
svg.addEventListener('mouseleave', handleMouseLeave);
|
|
528
|
-
svg.addEventListener('mousedown', handleMouseDown);
|
|
529
|
-
svg.addEventListener('click', handleTap);
|
|
530
|
-
svg.addEventListener('wheel', handleWheel, { passive: false });
|
|
531
|
-
svg.addEventListener('touchstart', handleTouchStart, { passive: true });
|
|
290
|
+
// Start tracking
|
|
291
|
+
rafId = requestAnimationFrame(trackViewBox);
|
|
292
|
+
// ============================================
|
|
293
|
+
// Setup Event Listeners
|
|
294
|
+
// ============================================
|
|
295
|
+
svg.addEventListener('touchstart', handleTouchStart, { passive: false });
|
|
532
296
|
svg.addEventListener('touchmove', handleTouchMove, { passive: false });
|
|
533
297
|
svg.addEventListener('touchend', handleTouchEnd);
|
|
534
|
-
|
|
535
|
-
|
|
298
|
+
svg.addEventListener('touchcancel', handleTouchEnd);
|
|
299
|
+
svg.addEventListener('touchstart', handleTouchStartForTap, { passive: true });
|
|
300
|
+
svg.addEventListener('touchend', handleTouchEndForTap);
|
|
301
|
+
svg.addEventListener('mousedown', handleMouseDown);
|
|
302
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
536
303
|
document.addEventListener('mouseup', handleMouseUp);
|
|
304
|
+
svg.addEventListener('mouseleave', handleMouseLeave);
|
|
305
|
+
svg.addEventListener('wheel', handleWheel, { passive: false });
|
|
306
|
+
// Hierarchical navigation: click on subgraph with sheet reference
|
|
307
|
+
svg.addEventListener('click', handleSubgraphClick);
|
|
537
308
|
// Listen for scroll/resize to update highlight position
|
|
538
309
|
window.addEventListener('scroll', handlePositionUpdate, true);
|
|
539
310
|
window.addEventListener('resize', handlePositionUpdate);
|
|
540
311
|
return {
|
|
541
312
|
destroy: () => {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
svg.removeEventListener('mousedown', handleMouseDown);
|
|
545
|
-
svg.removeEventListener('click', handleTap);
|
|
546
|
-
svg.removeEventListener('wheel', handleWheel);
|
|
313
|
+
if (rafId !== null)
|
|
314
|
+
cancelAnimationFrame(rafId);
|
|
547
315
|
svg.removeEventListener('touchstart', handleTouchStart);
|
|
548
316
|
svg.removeEventListener('touchmove', handleTouchMove);
|
|
549
317
|
svg.removeEventListener('touchend', handleTouchEnd);
|
|
550
|
-
|
|
318
|
+
svg.removeEventListener('touchcancel', handleTouchEnd);
|
|
319
|
+
svg.removeEventListener('touchstart', handleTouchStartForTap);
|
|
320
|
+
svg.removeEventListener('touchend', handleTouchEndForTap);
|
|
321
|
+
svg.removeEventListener('mousedown', handleMouseDown);
|
|
322
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
551
323
|
document.removeEventListener('mouseup', handleMouseUp);
|
|
324
|
+
svg.removeEventListener('mouseleave', handleMouseLeave);
|
|
325
|
+
svg.removeEventListener('wheel', handleWheel);
|
|
326
|
+
svg.removeEventListener('click', handleSubgraphClick);
|
|
552
327
|
window.removeEventListener('scroll', handlePositionUpdate, true);
|
|
553
328
|
window.removeEventListener('resize', handlePositionUpdate);
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
highlightElement(null);
|
|
557
|
-
if (tooltip) {
|
|
558
|
-
tooltip.remove();
|
|
559
|
-
tooltip = null;
|
|
560
|
-
}
|
|
561
|
-
if (overlay) {
|
|
562
|
-
overlay.remove();
|
|
563
|
-
overlay = null;
|
|
564
|
-
}
|
|
565
|
-
if (highlightContainer) {
|
|
566
|
-
highlightContainer.remove();
|
|
567
|
-
highlightContainer = null;
|
|
568
|
-
}
|
|
329
|
+
destroyTooltip();
|
|
330
|
+
destroySpotlight();
|
|
569
331
|
},
|
|
570
332
|
showDeviceModal: () => { },
|
|
571
333
|
hideModal: () => { },
|
|
@@ -573,10 +335,11 @@ export function initInteractive(options) {
|
|
|
573
335
|
hideTooltip: () => {
|
|
574
336
|
hideTooltip();
|
|
575
337
|
highlightElement(null);
|
|
576
|
-
|
|
338
|
+
tooltipActive = false;
|
|
577
339
|
},
|
|
578
340
|
resetView,
|
|
579
341
|
getScale,
|
|
342
|
+
navigateToSheet: dispatchNavigateEvent,
|
|
580
343
|
};
|
|
581
344
|
}
|
|
582
345
|
//# sourceMappingURL=runtime.js.map
|