@hugobatist/smartcode 0.1.0
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/LICENSE +21 -0
- package/README.md +292 -0
- package/dist/cli.js +4324 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +374 -0
- package/dist/index.js +1167 -0
- package/dist/index.js.map +1 -0
- package/dist/static/annotations-panel.js +133 -0
- package/dist/static/annotations-svg.js +108 -0
- package/dist/static/annotations.css +367 -0
- package/dist/static/annotations.js +367 -0
- package/dist/static/app-init.js +497 -0
- package/dist/static/breakpoints.css +69 -0
- package/dist/static/breakpoints.js +197 -0
- package/dist/static/clipboard.js +94 -0
- package/dist/static/collapse-ui.js +325 -0
- package/dist/static/command-history.js +89 -0
- package/dist/static/context-menu.js +334 -0
- package/dist/static/custom-renderer.js +201 -0
- package/dist/static/dagre-layout.js +291 -0
- package/dist/static/diagram-dom.js +241 -0
- package/dist/static/diagram-editor.js +368 -0
- package/dist/static/editor-panel.js +107 -0
- package/dist/static/editor-popovers.js +187 -0
- package/dist/static/event-bus.js +57 -0
- package/dist/static/export.js +181 -0
- package/dist/static/file-tree.js +470 -0
- package/dist/static/ghost-paths.js +397 -0
- package/dist/static/heatmap.css +116 -0
- package/dist/static/heatmap.js +308 -0
- package/dist/static/icons.js +66 -0
- package/dist/static/inline-edit.js +294 -0
- package/dist/static/interaction-state.js +155 -0
- package/dist/static/interaction-tracker.js +93 -0
- package/dist/static/live.html +239 -0
- package/dist/static/main-layout.css +220 -0
- package/dist/static/main.css +334 -0
- package/dist/static/mcp-sessions.js +202 -0
- package/dist/static/modal.css +109 -0
- package/dist/static/modal.js +171 -0
- package/dist/static/node-drag.js +293 -0
- package/dist/static/pan-zoom.js +199 -0
- package/dist/static/renderer.js +280 -0
- package/dist/static/search.css +103 -0
- package/dist/static/search.js +304 -0
- package/dist/static/selection.js +353 -0
- package/dist/static/session-player.css +137 -0
- package/dist/static/session-player.js +411 -0
- package/dist/static/sidebar.css +248 -0
- package/dist/static/svg-renderer.js +313 -0
- package/dist/static/svg-shapes.js +218 -0
- package/dist/static/tokens.css +76 -0
- package/dist/static/vendor/dagre-bundle.js +43 -0
- package/dist/static/vendor/dagre.min.js +3 -0
- package/dist/static/vendor/graphlib.min.js +2 -0
- package/dist/static/viewport-transform.js +107 -0
- package/dist/static/workspace-switcher.js +202 -0
- package/dist/static/ws-client.js +71 -0
- package/dist/static/ws-handler.js +125 -0
- package/package.json +74 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SmartCode Node Drag -- drag selected nodes to reposition them on the canvas.
|
|
3
|
+
* Moves nodes visually by updating SVG transform. Positions reset on re-render
|
|
4
|
+
* (layout is still computed by dagre). This provides immediate visual feedback.
|
|
5
|
+
*
|
|
6
|
+
* Dependencies:
|
|
7
|
+
* - interaction-state.js (SmartCodeInteraction)
|
|
8
|
+
* - selection.js (SmartCodeSelection)
|
|
9
|
+
* - diagram-dom.js (DiagramDOM)
|
|
10
|
+
* - pan-zoom.js (SmartCodePanZoom)
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* SmartCodeNodeDrag.init();
|
|
14
|
+
*/
|
|
15
|
+
(function() {
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
var DRAG_THRESHOLD = 5; // px before drag starts (prevents false drags on click)
|
|
19
|
+
|
|
20
|
+
// ── Internal State ──
|
|
21
|
+
var isDragging = false;
|
|
22
|
+
var dragStarted = false;
|
|
23
|
+
var dragNodeId = null;
|
|
24
|
+
var dragNodeEl = null;
|
|
25
|
+
var startMouseX = 0;
|
|
26
|
+
var startMouseY = 0;
|
|
27
|
+
var startNodeX = 0;
|
|
28
|
+
var startNodeY = 0;
|
|
29
|
+
var currentOffsetX = 0;
|
|
30
|
+
var currentOffsetY = 0;
|
|
31
|
+
|
|
32
|
+
// Track position overrides so they survive re-selection (but not re-render)
|
|
33
|
+
var positionOverrides = new Map(); // nodeId -> { dx, dy }
|
|
34
|
+
|
|
35
|
+
// ── Coordinate Conversion ──
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Convert screen coordinates to SVG/graph coordinates,
|
|
39
|
+
* accounting for current zoom and pan.
|
|
40
|
+
*/
|
|
41
|
+
function screenToGraph(screenX, screenY) {
|
|
42
|
+
var pan = window.SmartCodePanZoom ? SmartCodePanZoom.getPan() : { panX: 0, panY: 0, zoom: 1 };
|
|
43
|
+
return {
|
|
44
|
+
x: (screenX - pan.panX) / pan.zoom,
|
|
45
|
+
y: (screenY - pan.panY) / pan.zoom
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Get Node's Current Transform ──
|
|
50
|
+
|
|
51
|
+
function getNodeTransform(el) {
|
|
52
|
+
var transform = el.getAttribute('transform') || '';
|
|
53
|
+
var match = transform.match(/translate\(\s*([-\d.]+)\s*,?\s*([-\d.]+)\s*\)/);
|
|
54
|
+
if (match) {
|
|
55
|
+
return { x: parseFloat(match[1]), y: parseFloat(match[2]) };
|
|
56
|
+
}
|
|
57
|
+
return { x: 0, y: 0 };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function setNodeTransform(el, x, y) {
|
|
61
|
+
el.setAttribute('transform', 'translate(' + x + ',' + y + ')');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Update Connected Edges ──
|
|
65
|
+
|
|
66
|
+
function updateConnectedEdges(nodeId, dx, dy) {
|
|
67
|
+
var svg = DiagramDOM.getSVG();
|
|
68
|
+
if (!svg) return;
|
|
69
|
+
|
|
70
|
+
// Find edges connected to this node
|
|
71
|
+
// Custom renderer: edges have data-source and data-target attributes
|
|
72
|
+
var edges = svg.querySelectorAll(
|
|
73
|
+
'[data-source="' + nodeId + '"], [data-target="' + nodeId + '"]'
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
for (var i = 0; i < edges.length; i++) {
|
|
77
|
+
var edge = edges[i];
|
|
78
|
+
var path = edge.querySelector('path');
|
|
79
|
+
if (!path) continue;
|
|
80
|
+
|
|
81
|
+
var source = edge.getAttribute('data-source');
|
|
82
|
+
var target = edge.getAttribute('data-target');
|
|
83
|
+
|
|
84
|
+
// Get the path data and adjust endpoints
|
|
85
|
+
var d = path.getAttribute('d');
|
|
86
|
+
if (!d) continue;
|
|
87
|
+
|
|
88
|
+
// Simple approach: translate the edge endpoint that corresponds to the moved node
|
|
89
|
+
if (source === nodeId && target === nodeId) {
|
|
90
|
+
// Self-loop: translate entire edge
|
|
91
|
+
if (!edge._originalTransform) edge._originalTransform = edge.getAttribute('transform') || '';
|
|
92
|
+
edge.setAttribute('transform', (edge._originalTransform + ' translate(' + dx + ',' + dy + ')').trim());
|
|
93
|
+
} else if (source === nodeId) {
|
|
94
|
+
// Move start point of the path
|
|
95
|
+
adjustPathStart(path, dx, dy);
|
|
96
|
+
} else if (target === nodeId) {
|
|
97
|
+
// Move end point of the path
|
|
98
|
+
adjustPathEnd(path, dx, dy);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Also handle Mermaid edges (they use different structure)
|
|
103
|
+
// Mermaid edges are harder to identify - skip for now, they'll update on re-render
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function adjustPathStart(path, dx, dy) {
|
|
107
|
+
if (!path._originalD) path._originalD = path.getAttribute('d');
|
|
108
|
+
var d = path._originalD;
|
|
109
|
+
// Parse first M command and adjust it
|
|
110
|
+
var adjusted = d.replace(/^M\s*([-\d.]+)\s*,?\s*([-\d.]+)/, function(match, x, y) {
|
|
111
|
+
return 'M' + (parseFloat(x) + dx) + ',' + (parseFloat(y) + dy);
|
|
112
|
+
});
|
|
113
|
+
path.setAttribute('d', adjusted);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function adjustPathEnd(path, dx, dy) {
|
|
117
|
+
if (!path._originalD) path._originalD = path.getAttribute('d');
|
|
118
|
+
var d = path._originalD;
|
|
119
|
+
// Find last coordinate pair and adjust it
|
|
120
|
+
// For bezier curves (C/Q), adjust the last control point and endpoint
|
|
121
|
+
var parts = d.split(/(?=[MLCQZ])/);
|
|
122
|
+
if (parts.length > 0) {
|
|
123
|
+
var lastPart = parts[parts.length - 1];
|
|
124
|
+
if (lastPart && lastPart.trim().startsWith('L')) {
|
|
125
|
+
parts[parts.length - 1] = lastPart.replace(/([-\d.]+)\s*,?\s*([-\d.]+)\s*$/, function(match, x, y) {
|
|
126
|
+
return (parseFloat(x) + dx) + ',' + (parseFloat(y) + dy);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
path.setAttribute('d', parts.join(''));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Event Handlers ──
|
|
134
|
+
|
|
135
|
+
function handleMouseDown(e) {
|
|
136
|
+
if (e.button !== 0) return;
|
|
137
|
+
|
|
138
|
+
// Only drag when a node is selected
|
|
139
|
+
if (!window.SmartCodeInteraction) return;
|
|
140
|
+
var fsmState = SmartCodeInteraction.getState();
|
|
141
|
+
if (fsmState !== 'selected') return;
|
|
142
|
+
|
|
143
|
+
var sel = window.SmartCodeSelection ? SmartCodeSelection.getSelected() : null;
|
|
144
|
+
if (!sel || sel.type !== 'node') return;
|
|
145
|
+
|
|
146
|
+
// Check if click is on the selected node
|
|
147
|
+
var clickedNode = DiagramDOM.extractNodeId(e.target);
|
|
148
|
+
if (!clickedNode || clickedNode.id !== sel.id) return;
|
|
149
|
+
|
|
150
|
+
// Start potential drag
|
|
151
|
+
isDragging = true;
|
|
152
|
+
dragStarted = false;
|
|
153
|
+
dragNodeId = sel.id;
|
|
154
|
+
dragNodeEl = DiagramDOM.findNodeElement(sel.id);
|
|
155
|
+
startMouseX = e.clientX;
|
|
156
|
+
startMouseY = e.clientY;
|
|
157
|
+
|
|
158
|
+
if (dragNodeEl) {
|
|
159
|
+
var currentTransform = getNodeTransform(dragNodeEl);
|
|
160
|
+
startNodeX = currentTransform.x;
|
|
161
|
+
startNodeY = currentTransform.y;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
currentOffsetX = 0;
|
|
165
|
+
currentOffsetY = 0;
|
|
166
|
+
|
|
167
|
+
e.preventDefault();
|
|
168
|
+
e.stopPropagation();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function handleMouseMove(e) {
|
|
172
|
+
if (!isDragging || !dragNodeEl) return;
|
|
173
|
+
|
|
174
|
+
var dx = e.clientX - startMouseX;
|
|
175
|
+
var dy = e.clientY - startMouseY;
|
|
176
|
+
|
|
177
|
+
if (!dragStarted) {
|
|
178
|
+
if (Math.abs(dx) <= DRAG_THRESHOLD && Math.abs(dy) <= DRAG_THRESHOLD) return;
|
|
179
|
+
dragStarted = true;
|
|
180
|
+
// Notify FSM
|
|
181
|
+
if (window.SmartCodeInteraction) SmartCodeInteraction.transition('drag_start');
|
|
182
|
+
document.body.style.cursor = 'grabbing';
|
|
183
|
+
|
|
184
|
+
// Store original edge paths for connected edges
|
|
185
|
+
storeOriginalEdgePaths(dragNodeId);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Convert screen delta to graph delta (account for zoom)
|
|
189
|
+
var pan = window.SmartCodePanZoom ? SmartCodePanZoom.getPan() : { zoom: 1 };
|
|
190
|
+
var graphDx = dx / pan.zoom;
|
|
191
|
+
var graphDy = dy / pan.zoom;
|
|
192
|
+
|
|
193
|
+
// Move the node
|
|
194
|
+
setNodeTransform(dragNodeEl, startNodeX + graphDx, startNodeY + graphDy);
|
|
195
|
+
currentOffsetX = graphDx;
|
|
196
|
+
currentOffsetY = graphDy;
|
|
197
|
+
|
|
198
|
+
// Update connected edges
|
|
199
|
+
updateConnectedEdges(dragNodeId, graphDx, graphDy);
|
|
200
|
+
|
|
201
|
+
e.preventDefault();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function handleMouseUp(e) {
|
|
205
|
+
if (!isDragging) return;
|
|
206
|
+
|
|
207
|
+
if (dragStarted) {
|
|
208
|
+
// Store the position override
|
|
209
|
+
var existing = positionOverrides.get(dragNodeId) || { dx: 0, dy: 0 };
|
|
210
|
+
positionOverrides.set(dragNodeId, {
|
|
211
|
+
dx: existing.dx + currentOffsetX,
|
|
212
|
+
dy: existing.dy + currentOffsetY
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Clean up _originalD and _originalTransform from edge elements
|
|
216
|
+
cleanupEdgeCustomProps(dragNodeId);
|
|
217
|
+
|
|
218
|
+
// Notify FSM
|
|
219
|
+
if (window.SmartCodeInteraction) SmartCodeInteraction.transition('drag_end');
|
|
220
|
+
document.body.style.cursor = '';
|
|
221
|
+
|
|
222
|
+
if (window.toast) {
|
|
223
|
+
window.toast('Node repositioned (visual only, resets on re-render)');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
isDragging = false;
|
|
228
|
+
dragStarted = false;
|
|
229
|
+
dragNodeId = null;
|
|
230
|
+
dragNodeEl = null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Remove custom properties (_originalD, _originalTransform) from edge elements */
|
|
234
|
+
function cleanupEdgeCustomProps(nodeId) {
|
|
235
|
+
var svg = DiagramDOM.getSVG();
|
|
236
|
+
if (!svg) return;
|
|
237
|
+
var edges = svg.querySelectorAll(
|
|
238
|
+
'[data-source="' + nodeId + '"], [data-target="' + nodeId + '"]'
|
|
239
|
+
);
|
|
240
|
+
for (var i = 0; i < edges.length; i++) {
|
|
241
|
+
var edge = edges[i];
|
|
242
|
+
delete edge._originalTransform;
|
|
243
|
+
var pathEl = edge.querySelector('path');
|
|
244
|
+
if (pathEl) delete pathEl._originalD;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function storeOriginalEdgePaths(nodeId) {
|
|
249
|
+
var svg = DiagramDOM.getSVG();
|
|
250
|
+
if (!svg) return;
|
|
251
|
+
var edges = svg.querySelectorAll(
|
|
252
|
+
'[data-source="' + nodeId + '"], [data-target="' + nodeId + '"]'
|
|
253
|
+
);
|
|
254
|
+
for (var i = 0; i < edges.length; i++) {
|
|
255
|
+
var path = edges[i].querySelector('path');
|
|
256
|
+
if (path && !path._originalD) {
|
|
257
|
+
path._originalD = path.getAttribute('d');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── Re-apply positions after render (for the current session) ──
|
|
263
|
+
|
|
264
|
+
function reapplyPositions() {
|
|
265
|
+
// Position overrides are visual-only and reset on diagram re-render.
|
|
266
|
+
// Clear the overrides since dagre has re-computed the layout.
|
|
267
|
+
positionOverrides.clear();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── Init ──
|
|
271
|
+
|
|
272
|
+
function init() {
|
|
273
|
+
var container = document.getElementById('preview-container');
|
|
274
|
+
if (!container) return;
|
|
275
|
+
|
|
276
|
+
// Use capture phase to intercept before pan-zoom
|
|
277
|
+
container.addEventListener('mousedown', handleMouseDown, true);
|
|
278
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
279
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
280
|
+
|
|
281
|
+
// Clear position overrides on re-render
|
|
282
|
+
if (window.SmartCodeEventBus) {
|
|
283
|
+
SmartCodeEventBus.on('diagram:rendered', reapplyPositions);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ── Public API ──
|
|
288
|
+
window.SmartCodeNodeDrag = {
|
|
289
|
+
init: init,
|
|
290
|
+
isDragging: function() { return dragStarted; },
|
|
291
|
+
getPositionOverrides: function() { return positionOverrides; },
|
|
292
|
+
};
|
|
293
|
+
})();
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SmartCode Pan/Zoom -- viewport transformation, scroll zoom, drag pan.
|
|
3
|
+
* Extracted from live.html (Phase 9 Plan 02).
|
|
4
|
+
*
|
|
5
|
+
* Dependencies: interaction-state.js (SmartCodeInteraction, optional)
|
|
6
|
+
* Dependents: renderer.js (calls zoomFit/applyTransform via window globals)
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* SmartCodePanZoom.zoomIn();
|
|
10
|
+
* SmartCodePanZoom.zoomOut();
|
|
11
|
+
* SmartCodePanZoom.zoomFit();
|
|
12
|
+
* SmartCodePanZoom.getPan(); // { panX, panY, zoom }
|
|
13
|
+
* SmartCodePanZoom.setPan(px, py);
|
|
14
|
+
*/
|
|
15
|
+
(function() {
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
// ── State ──
|
|
19
|
+
var zoom = 1;
|
|
20
|
+
var panX = 0, panY = 0;
|
|
21
|
+
var isPanning = false;
|
|
22
|
+
var panStarted = false; // true once movement exceeds threshold
|
|
23
|
+
var panStartX = 0, panStartY = 0;
|
|
24
|
+
var panStartPanX = 0, panStartPanY = 0;
|
|
25
|
+
|
|
26
|
+
// ── Movement threshold to prevent false pans on click ──
|
|
27
|
+
var PAN_THRESHOLD = 3; // pixels
|
|
28
|
+
|
|
29
|
+
// ── DOM refs (queried once at load time -- these elements are static) ──
|
|
30
|
+
var previewPanel = document.getElementById('previewPanel');
|
|
31
|
+
var container = document.getElementById('preview-container');
|
|
32
|
+
|
|
33
|
+
// ── Transform ──
|
|
34
|
+
function applyTransform() {
|
|
35
|
+
var preview = document.getElementById('preview');
|
|
36
|
+
preview.style.transform = 'translate(' + panX + 'px, ' + panY + 'px) scale(' + zoom + ')';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Mouse wheel zoom (trackpad-friendly: uses deltaY magnitude) ──
|
|
40
|
+
container.addEventListener('wheel', function(e) {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
// Clamp deltaY so trackpad inertia doesn't over-zoom
|
|
43
|
+
var clamped = Math.max(-60, Math.min(60, e.deltaY));
|
|
44
|
+
var factor = 1 - clamped * 0.002; // ~0.12% per pixel of delta
|
|
45
|
+
var newZoom = Math.min(Math.max(zoom * factor, 0.1), 5);
|
|
46
|
+
|
|
47
|
+
// Zoom toward cursor
|
|
48
|
+
var rect = container.getBoundingClientRect();
|
|
49
|
+
var mx = e.clientX - rect.left;
|
|
50
|
+
var my = e.clientY - rect.top;
|
|
51
|
+
|
|
52
|
+
panX = mx - (mx - panX) * (newZoom / zoom);
|
|
53
|
+
panY = my - (my - panY) * (newZoom / zoom);
|
|
54
|
+
zoom = newZoom;
|
|
55
|
+
|
|
56
|
+
applyTransform();
|
|
57
|
+
document.getElementById('zoomLabel').textContent = Math.round(zoom * 100) + '%';
|
|
58
|
+
}, { passive: false });
|
|
59
|
+
|
|
60
|
+
// ── Mouse drag pan (disabled in flag mode, editor mode, and FSM blocking states) ──
|
|
61
|
+
container.addEventListener('mousedown', function(e) {
|
|
62
|
+
if (e.button !== 0) return;
|
|
63
|
+
|
|
64
|
+
// Auto-recover: if FSM says we're in a blocking state but no actual overlay exists, reset
|
|
65
|
+
if (window.SmartCodeInteraction) {
|
|
66
|
+
var fsmState = SmartCodeInteraction.getState();
|
|
67
|
+
if (fsmState === 'editing' && !(window.SmartCodeInlineEdit && SmartCodeInlineEdit.isActive())) {
|
|
68
|
+
SmartCodeInteraction.forceState('idle');
|
|
69
|
+
}
|
|
70
|
+
if (fsmState === 'context-menu' && !document.querySelector('.context-menu')) {
|
|
71
|
+
SmartCodeInteraction.forceState('idle');
|
|
72
|
+
}
|
|
73
|
+
if (fsmState === 'dragging' && !(window.SmartCodeNodeDrag && SmartCodeNodeDrag.isDragging())) {
|
|
74
|
+
SmartCodeInteraction.forceState('idle');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check FSM blocking states (editing, context-menu)
|
|
79
|
+
if (window.SmartCodeInteraction && SmartCodeInteraction.isBlocking()) return;
|
|
80
|
+
if (window.SmartCodeInteraction && SmartCodeInteraction.getState() === 'dragging') return;
|
|
81
|
+
// Don't pan if clicking on a selected node (node-drag handles this)
|
|
82
|
+
if (window.SmartCodeInteraction && SmartCodeInteraction.getState() === 'selected') {
|
|
83
|
+
var sel = window.SmartCodeSelection ? SmartCodeSelection.getSelected() : null;
|
|
84
|
+
if (sel && sel.type === 'node') {
|
|
85
|
+
var clickedNode = window.DiagramDOM ? DiagramDOM.extractNodeId(e.target) : null;
|
|
86
|
+
if (clickedNode && clickedNode.id === sel.id) return; // Let node-drag handle it
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Keep existing checks for backward compat without FSM
|
|
90
|
+
if (window.SmartCodeAnnotations && SmartCodeAnnotations.getState().flagMode) return;
|
|
91
|
+
if (window.MmdEditor && MmdEditor.getState().mode) return;
|
|
92
|
+
isPanning = true;
|
|
93
|
+
panStarted = false;
|
|
94
|
+
panStartX = e.clientX;
|
|
95
|
+
panStartY = e.clientY;
|
|
96
|
+
panStartPanX = panX;
|
|
97
|
+
panStartPanY = panY;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
document.addEventListener('mousemove', function(e) {
|
|
101
|
+
if (!isPanning) return;
|
|
102
|
+
|
|
103
|
+
// Movement threshold: only start actual panning after PAN_THRESHOLD pixels
|
|
104
|
+
if (!panStarted) {
|
|
105
|
+
var dx = Math.abs(e.clientX - panStartX);
|
|
106
|
+
var dy = Math.abs(e.clientY - panStartY);
|
|
107
|
+
if (dx <= PAN_THRESHOLD && dy <= PAN_THRESHOLD) return;
|
|
108
|
+
// Threshold crossed: pan starts now
|
|
109
|
+
panStarted = true;
|
|
110
|
+
previewPanel.classList.add('grabbing');
|
|
111
|
+
// Notify FSM
|
|
112
|
+
if (window.SmartCodeInteraction) SmartCodeInteraction.transition('pan_start');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
panX = panStartPanX + (e.clientX - panStartX);
|
|
116
|
+
panY = panStartPanY + (e.clientY - panStartY);
|
|
117
|
+
applyTransform();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
document.addEventListener('mouseup', function() {
|
|
121
|
+
if (isPanning && panStarted) {
|
|
122
|
+
// Notify FSM that pan ended
|
|
123
|
+
if (window.SmartCodeInteraction) {
|
|
124
|
+
var sel = SmartCodeInteraction.getSelection();
|
|
125
|
+
SmartCodeInteraction.transition(sel.id ? 'pan_end_selected' : 'pan_end');
|
|
126
|
+
}
|
|
127
|
+
previewPanel.classList.remove('grabbing');
|
|
128
|
+
}
|
|
129
|
+
isPanning = false;
|
|
130
|
+
panStarted = false;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ── Zoom Fit ──
|
|
134
|
+
function zoomFit() {
|
|
135
|
+
var svg = document.querySelector('#preview svg');
|
|
136
|
+
if (!svg) return;
|
|
137
|
+
var rect = container.getBoundingClientRect();
|
|
138
|
+
var vb = svg.viewBox && svg.viewBox.baseVal;
|
|
139
|
+
var svgW = (vb && vb.width) || svg.getBoundingClientRect().width / zoom;
|
|
140
|
+
var svgH = (vb && vb.height) || svg.getBoundingClientRect().height / zoom;
|
|
141
|
+
|
|
142
|
+
if (svgW <= 0 || svgH <= 0) return;
|
|
143
|
+
|
|
144
|
+
var padFraction = 0.92;
|
|
145
|
+
var scaleX = (rect.width * padFraction) / svgW;
|
|
146
|
+
var scaleY = (rect.height * padFraction) / svgH;
|
|
147
|
+
zoom = Math.min(scaleX, scaleY, 2.5);
|
|
148
|
+
|
|
149
|
+
var scaledW = svgW * zoom;
|
|
150
|
+
var scaledH = svgH * zoom;
|
|
151
|
+
panX = (rect.width - scaledW) / 2;
|
|
152
|
+
panY = (rect.height - scaledH) / 2;
|
|
153
|
+
|
|
154
|
+
applyTransform();
|
|
155
|
+
document.getElementById('zoomLabel').textContent = Math.round(zoom * 100) + '%';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Zoom buttons (zoom toward viewport center, preserving pan position) ──
|
|
159
|
+
function zoomIn() {
|
|
160
|
+
var newZoom = Math.min(zoom * 1.15, 5);
|
|
161
|
+
var rect = container.getBoundingClientRect();
|
|
162
|
+
var cx = rect.width / 2;
|
|
163
|
+
var cy = rect.height / 2;
|
|
164
|
+
panX = cx - (cx - panX) * (newZoom / zoom);
|
|
165
|
+
panY = cy - (cy - panY) * (newZoom / zoom);
|
|
166
|
+
zoom = newZoom;
|
|
167
|
+
applyTransform();
|
|
168
|
+
document.getElementById('zoomLabel').textContent = Math.round(zoom * 100) + '%';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function zoomOut() {
|
|
172
|
+
var newZoom = Math.max(zoom * 0.85, 0.1);
|
|
173
|
+
var rect = container.getBoundingClientRect();
|
|
174
|
+
var cx = rect.width / 2;
|
|
175
|
+
var cy = rect.height / 2;
|
|
176
|
+
panX = cx - (cx - panX) * (newZoom / zoom);
|
|
177
|
+
panY = cy - (cy - panY) * (newZoom / zoom);
|
|
178
|
+
zoom = newZoom;
|
|
179
|
+
applyTransform();
|
|
180
|
+
document.getElementById('zoomLabel').textContent = Math.round(zoom * 100) + '%';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Public API ──
|
|
184
|
+
window.SmartCodePanZoom = {
|
|
185
|
+
getZoom: function() { return zoom; },
|
|
186
|
+
getPan: function() { return { panX: panX, panY: panY, zoom: zoom }; },
|
|
187
|
+
setPan: function(px, py) { panX = px; panY = py; applyTransform(); },
|
|
188
|
+
applyTransform: applyTransform,
|
|
189
|
+
zoomIn: zoomIn,
|
|
190
|
+
zoomOut: zoomOut,
|
|
191
|
+
zoomFit: zoomFit,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Backward compat -- inline onclick handlers and keyboard shortcuts call these directly
|
|
195
|
+
window.zoomIn = zoomIn;
|
|
196
|
+
window.zoomOut = zoomOut;
|
|
197
|
+
window.zoomFit = zoomFit;
|
|
198
|
+
window.applyTransform = applyTransform;
|
|
199
|
+
})();
|