@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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +292 -0
  3. package/dist/cli.js +4324 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/index.d.ts +374 -0
  6. package/dist/index.js +1167 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/static/annotations-panel.js +133 -0
  9. package/dist/static/annotations-svg.js +108 -0
  10. package/dist/static/annotations.css +367 -0
  11. package/dist/static/annotations.js +367 -0
  12. package/dist/static/app-init.js +497 -0
  13. package/dist/static/breakpoints.css +69 -0
  14. package/dist/static/breakpoints.js +197 -0
  15. package/dist/static/clipboard.js +94 -0
  16. package/dist/static/collapse-ui.js +325 -0
  17. package/dist/static/command-history.js +89 -0
  18. package/dist/static/context-menu.js +334 -0
  19. package/dist/static/custom-renderer.js +201 -0
  20. package/dist/static/dagre-layout.js +291 -0
  21. package/dist/static/diagram-dom.js +241 -0
  22. package/dist/static/diagram-editor.js +368 -0
  23. package/dist/static/editor-panel.js +107 -0
  24. package/dist/static/editor-popovers.js +187 -0
  25. package/dist/static/event-bus.js +57 -0
  26. package/dist/static/export.js +181 -0
  27. package/dist/static/file-tree.js +470 -0
  28. package/dist/static/ghost-paths.js +397 -0
  29. package/dist/static/heatmap.css +116 -0
  30. package/dist/static/heatmap.js +308 -0
  31. package/dist/static/icons.js +66 -0
  32. package/dist/static/inline-edit.js +294 -0
  33. package/dist/static/interaction-state.js +155 -0
  34. package/dist/static/interaction-tracker.js +93 -0
  35. package/dist/static/live.html +239 -0
  36. package/dist/static/main-layout.css +220 -0
  37. package/dist/static/main.css +334 -0
  38. package/dist/static/mcp-sessions.js +202 -0
  39. package/dist/static/modal.css +109 -0
  40. package/dist/static/modal.js +171 -0
  41. package/dist/static/node-drag.js +293 -0
  42. package/dist/static/pan-zoom.js +199 -0
  43. package/dist/static/renderer.js +280 -0
  44. package/dist/static/search.css +103 -0
  45. package/dist/static/search.js +304 -0
  46. package/dist/static/selection.js +353 -0
  47. package/dist/static/session-player.css +137 -0
  48. package/dist/static/session-player.js +411 -0
  49. package/dist/static/sidebar.css +248 -0
  50. package/dist/static/svg-renderer.js +313 -0
  51. package/dist/static/svg-shapes.js +218 -0
  52. package/dist/static/tokens.css +76 -0
  53. package/dist/static/vendor/dagre-bundle.js +43 -0
  54. package/dist/static/vendor/dagre.min.js +3 -0
  55. package/dist/static/vendor/graphlib.min.js +2 -0
  56. package/dist/static/viewport-transform.js +107 -0
  57. package/dist/static/workspace-switcher.js +202 -0
  58. package/dist/static/ws-client.js +71 -0
  59. package/dist/static/ws-handler.js +125 -0
  60. 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
+ })();