broadfield-dev commited on
Commit
fa14350
·
verified ·
1 Parent(s): 5bed3d1

Update static/canvas.js

Browse files
Files changed (1) hide show
  1. static/canvas.js +166 -100
static/canvas.js CHANGED
@@ -1,5 +1,20 @@
1
- // Initialization
2
- const width = window.innerWidth - 400; // Subtract sidebar width
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  const height = window.innerHeight;
4
 
5
  const stage = new Konva.Stage({
@@ -12,16 +27,85 @@ const stage = new Konva.Stage({
12
  const layer = new Konva.Layer();
13
  stage.add(layer);
14
 
15
- // State
16
- let scale = 1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
- // Zoom Handler
19
  stage.on('wheel', (e) => {
20
  e.evt.preventDefault();
21
  const oldScale = stage.scaleX();
22
  const pointer = stage.getPointerPosition();
23
  const scaleBy = 1.05;
24
  const newScale = e.evt.deltaY > 0 ? oldScale / scaleBy : oldScale * scaleBy;
 
25
  stage.scale({ x: newScale, y: newScale });
26
 
27
  const newPos = {
@@ -35,20 +119,13 @@ stage.on('wheel', (e) => {
35
  window.addEventListener('resize', () => {
36
  stage.width(window.innerWidth - 400);
37
  stage.height(window.innerHeight);
 
38
  });
39
 
40
- // Logger
41
- function log(msg, type='info') {
42
- const consoleBody = document.getElementById('logOutput');
43
- const color = type === 'error' ? '#ff7675' : '#55efc4';
44
- consoleBody.innerHTML += `<div style="color:${color}">> ${msg}</div>`;
45
- consoleBody.scrollTop = consoleBody.scrollHeight;
46
- }
47
-
48
- // API Calls
49
  document.getElementById('btnVisualize').addEventListener('click', () => {
50
  const code = document.getElementById('codeInput').value;
51
- log('Parsing code structure...');
52
 
53
  fetch('/parse', {
54
  method: 'POST',
@@ -57,124 +134,111 @@ document.getElementById('btnVisualize').addEventListener('click', () => {
57
  })
58
  .then(res => res.json())
59
  .then(data => {
60
- if(data.error) {
61
- log(data.error, 'error');
62
- } else {
63
- // PASS 'code' HERE so we can slice it later
64
- drawGraph(data, code);
65
- log(`Graph generated: ${data.nodes.length} nodes.`);
66
- }
67
- })
68
- .catch(err => log('Network error', 'error'));
69
- });
70
-
71
-
72
- document.getElementById('btnDataset').addEventListener('click', () => {
73
- const code = document.getElementById('codeInput').value;
74
- log('Generating vector dataset entry...');
75
-
76
- fetch('/add_to_dataset', {
77
- method: 'POST',
78
- headers: { 'Content-Type': 'application/json' },
79
- body: JSON.stringify({ code: code })
80
- })
81
- .then(res => res.json())
82
- .then(data => {
83
- if(data.status === 'success') {
84
- log(`Dataset updated. ID: ${data.entry_id}`);
85
- } else {
86
- log(data.message, 'error');
87
  }
88
  });
89
  });
90
 
91
- // Drawing Logic
92
  function drawGraph(data, fullSourceCode) {
 
93
  layer.destroyChildren();
 
94
 
95
- if (data.nodes.length === 0) return;
96
-
97
- // OPTIMIZATION: Split code once into lines for O(1) access
98
- const sourceLines = fullSourceCode.split('\n');
99
 
100
  const nodeMap = {};
101
- const X_OFFSET = 100;
102
- const Y_START = 50;
103
- const INDENT_WIDTH = 60;
104
- const ROW_HEIGHT = 80;
105
-
106
- const colors = {
107
- 'function': '#a29bfe', 'class': '#e84393', 'if': '#fab1a0',
108
- 'for': '#fdcb6e', 'while': '#fdcb6e', 'return': '#55efc4',
109
- 'assigned_variable': '#74b9ff', 'import': '#b2bec3'
110
- };
111
-
112
  data.nodes.forEach((node, index) => {
113
- // Use short keys: node.lvl instead of node.level
114
- const x = X_OFFSET + (node.lvl * INDENT_WIDTH);
115
- const y = Y_START + (index * ROW_HEIGHT);
 
 
 
 
 
 
116
 
117
- const group = new Konva.Group({ x: x, y: y, draggable: false });
 
 
 
 
 
118
 
119
  const rect = new Konva.Rect({
120
- width: 200, height: 50,
 
121
  fill: '#2d3436',
122
  stroke: colors[node.type] || '#636e72',
123
- strokeWidth: 2, cornerRadius: 8,
124
- shadowColor: 'black', shadowBlur: 10, shadowOpacity: 0.3
 
 
 
 
 
125
  });
126
 
127
  const text = new Konva.Text({
128
  x: 10, y: 10,
129
- text: node.lbl, // Use short key: lbl
130
  fontSize: 14, fontFamily: 'JetBrains Mono', fill: '#fff',
131
- width: 180, ellipsis: true
 
 
132
  });
133
 
134
  const vecText = new Konva.Text({
135
  x: 10, y: 32,
136
- text: `V:[${node.vec[0]}, ${node.vec[2]}...]`, // Use short key: vec
137
- fontSize: 10, fontFamily: 'JetBrains Mono', fill: '#636e72'
 
138
  });
139
 
140
  group.add(rect);
141
  group.add(text);
142
  group.add(vecText);
143
 
144
- // OPTIMIZATION: Reconstruct source on click
145
  group.on('click', () => {
146
- // AST line numbers are 1-based, array is 0-based
147
- const startLine = node.loc[0] - 1;
148
- const endLine = node.loc[1];
149
-
150
- // Slice the specific lines from the main array
151
- const snippet = sourceLines.slice(startLine, endLine).join('\n');
152
- log(`Node Source:\n${snippet}`);
153
  });
154
 
155
- // Hover effects
156
- group.on('mouseover', () => { document.body.style.cursor = 'pointer'; rect.fill('#353b48'); layer.draw(); });
157
- group.on('mouseout', () => { document.body.style.cursor = 'default'; rect.fill('#2d3436'); layer.draw(); });
158
 
159
  layer.add(group);
 
 
160
  nodeMap[node.id] = { x, y };
 
 
 
 
 
 
161
  });
162
 
163
  // Draw Connections
 
164
  data.connections.forEach(conn => {
165
- // Use short keys: f (from), t (to)
166
- const fromNode = nodeMap[conn.f];
167
- const toNode = nodeMap[conn.t];
168
-
169
- if (fromNode && toNode) {
170
- const startX = fromNode.x + 20;
171
- const startY = fromNode.y + 50;
172
- const endX = toNode.x + 20;
173
- const endY = toNode.y;
174
-
175
  const line = new Konva.Line({
176
- points: [startX, startY, startX, endY - 10, endX, endY - 10, endX, endY],
177
- stroke: '#636e72', strokeWidth: 2, tension: 0.2, opacity: 0.6
 
178
  });
179
  layer.add(line);
180
  line.moveToBottom();
@@ -182,13 +246,15 @@ function drawGraph(data, fullSourceCode) {
182
  });
183
 
184
  layer.batchDraw();
185
-
186
- if (data.nodes.length > 0) {
187
- stage.x(50);
188
- stage.y(50);
189
- }
190
  }
191
 
192
-
193
- // Initial Call
194
- setTimeout(() => document.getElementById('btnVisualize').click(), 500);
 
 
 
 
 
 
1
+ // static/js/canvas.js
2
+
3
+ // --- Configuration ---
4
+ const CONFIG = {
5
+ // Only render nodes within this padding of the viewport
6
+ viewportPadding: 200,
7
+ // Throttle scroll events to run only every 16ms (60fps)
8
+ throttleMs: 16,
9
+ // Visual settings
10
+ nodeWidth: 200,
11
+ nodeHeight: 50,
12
+ indentWidth: 60,
13
+ rowHeight: 80
14
+ };
15
+
16
+ // --- Setup Stage ---
17
+ const width = window.innerWidth - 400; // Adjust for sidebar
18
  const height = window.innerHeight;
19
 
20
  const stage = new Konva.Stage({
 
27
  const layer = new Konva.Layer();
28
  stage.add(layer);
29
 
30
+ // Global State
31
+ let allNodes = []; // Array of { group, x, y, visible }
32
+ let sourceLines = []; // Cached source code lines
33
+ let isTicking = false; // For throttling
34
+
35
+ // --- 1. Optimization Core: Viewport Culling ---
36
+ function updateVisibleNodes() {
37
+ isTicking = false;
38
+
39
+ // Get the visible viewport in "World Coordinates" (accounting for zoom/pan)
40
+ const scale = stage.scaleX();
41
+ const stageX = stage.x();
42
+ const stageY = stage.y();
43
+
44
+ // The logic: Invert the transform to find what part of the world is visible
45
+ const viewX = -(stageX / scale) - CONFIG.viewportPadding;
46
+ const viewY = -(stageY / scale) - CONFIG.viewportPadding;
47
+ const viewW = (stage.width() / scale) + (CONFIG.viewportPadding * 2);
48
+ const viewH = (stage.height() / scale) + (CONFIG.viewportPadding * 2);
49
+
50
+ const viewRight = viewX + viewW;
51
+ const viewBottom = viewY + viewH;
52
+
53
+ // Batch updates to prevent multiple redraws
54
+ let nodesChanged = false;
55
+
56
+ // Fast loop (standard for-loop is faster than .forEach for massive arrays)
57
+ for (let i = 0; i < allNodes.length; i++) {
58
+ const node = allNodes[i];
59
+
60
+ // Simple Bounding Box Collision Check
61
+ const isVisible = (
62
+ node.x < viewRight &&
63
+ node.x + CONFIG.nodeWidth > viewX &&
64
+ node.y < viewBottom &&
65
+ node.y + CONFIG.nodeHeight > viewY
66
+ );
67
+
68
+ // Only touch the DOM/Konva object if state changes (Save CPU)
69
+ if (node.visible !== isVisible) {
70
+ node.group.visible(isVisible);
71
+ node.visible = isVisible;
72
+ nodesChanged = true;
73
+ }
74
+ }
75
+
76
+ if (nodesChanged) {
77
+ // layer.batchDraw() is more efficient than layer.draw()
78
+ layer.batchDraw();
79
+ }
80
+ }
81
+
82
+ // Request Animation Frame Wrapper (Throttling)
83
+ function requestUpdate() {
84
+ if (!isTicking) {
85
+ requestAnimationFrame(updateVisibleNodes);
86
+ isTicking = true;
87
+ }
88
+ }
89
+
90
+ // Bind optimization to interactions
91
+ stage.on('dragmove', requestUpdate);
92
+ stage.on('wheel', (e) => {
93
+ // ... (Zoom logic below) ...
94
+ // After zoom, we must update visibility
95
+ requestUpdate();
96
+ });
97
+
98
+
99
+ // --- 2. Standard Logic (Zoom & Draw) ---
100
 
101
+ // Zoom Logic
102
  stage.on('wheel', (e) => {
103
  e.evt.preventDefault();
104
  const oldScale = stage.scaleX();
105
  const pointer = stage.getPointerPosition();
106
  const scaleBy = 1.05;
107
  const newScale = e.evt.deltaY > 0 ? oldScale / scaleBy : oldScale * scaleBy;
108
+
109
  stage.scale({ x: newScale, y: newScale });
110
 
111
  const newPos = {
 
119
  window.addEventListener('resize', () => {
120
  stage.width(window.innerWidth - 400);
121
  stage.height(window.innerHeight);
122
+ requestUpdate();
123
  });
124
 
125
+ // API Listener
 
 
 
 
 
 
 
 
126
  document.getElementById('btnVisualize').addEventListener('click', () => {
127
  const code = document.getElementById('codeInput').value;
128
+ log('Parsing...');
129
 
130
  fetch('/parse', {
131
  method: 'POST',
 
134
  })
135
  .then(res => res.json())
136
  .then(data => {
137
+ if(data.error) log(data.error, 'error');
138
+ else {
139
+ drawGraph(data, code);
140
+ log(`Graph: ${data.nodes.length} nodes`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  }
142
  });
143
  });
144
 
 
145
  function drawGraph(data, fullSourceCode) {
146
+ // Cleanup old memory
147
  layer.destroyChildren();
148
+ allNodes = [];
149
 
150
+ // Cache source lines
151
+ sourceLines = fullSourceCode.split('\n');
 
 
152
 
153
  const nodeMap = {};
154
+
155
+ // --- Batch Create Nodes ---
 
 
 
 
 
 
 
 
 
156
  data.nodes.forEach((node, index) => {
157
+ const x = 100 + (node.lvl * CONFIG.indentWidth);
158
+ const y = 50 + (index * CONFIG.rowHeight);
159
+
160
+ // Styling
161
+ const colors = {
162
+ 'function': '#a29bfe', 'class': '#e84393', 'if': '#fab1a0',
163
+ 'for': '#fdcb6e', 'while': '#fdcb6e', 'return': '#55efc4',
164
+ 'assigned_variable': '#74b9ff', 'import': '#b2bec3'
165
+ };
166
 
167
+ const group = new Konva.Group({
168
+ x: x,
169
+ y: y,
170
+ // Optimization: Stop this group from catching mouse events if not needed
171
+ listening: true
172
+ });
173
 
174
  const rect = new Konva.Rect({
175
+ width: CONFIG.nodeWidth,
176
+ height: CONFIG.nodeHeight,
177
  fill: '#2d3436',
178
  stroke: colors[node.type] || '#636e72',
179
+ strokeWidth: 2,
180
+ cornerRadius: 8,
181
+ shadowColor: 'black',
182
+ shadowBlur: 10,
183
+ shadowOpacity: 0.3,
184
+ // Optimization: Perfect bounding box for hit detection
185
+ hitStrokeWidth: 0
186
  });
187
 
188
  const text = new Konva.Text({
189
  x: 10, y: 10,
190
+ text: node.lbl,
191
  fontSize: 14, fontFamily: 'JetBrains Mono', fill: '#fff',
192
+ width: 180, ellipsis: true,
193
+ // Optimization: Text doesn't need to listen to clicks, the Group/Rect handles it
194
+ listening: false
195
  });
196
 
197
  const vecText = new Konva.Text({
198
  x: 10, y: 32,
199
+ text: `V:[${node.vec[0]}, ${node.vec[2]}...]`,
200
+ fontSize: 10, fontFamily: 'JetBrains Mono', fill: '#636e72',
201
+ listening: false
202
  });
203
 
204
  group.add(rect);
205
  group.add(text);
206
  group.add(vecText);
207
 
208
+ // Interaction
209
  group.on('click', () => {
210
+ const start = node.loc[0] - 1;
211
+ const end = node.loc[1];
212
+ const snippet = sourceLines.slice(start, end).join('\n');
213
+ log(`Source:\n${snippet}`);
 
 
 
214
  });
215
 
216
+ // Hover
217
+ group.on('mouseover', () => { document.body.style.cursor = 'pointer'; rect.fill('#353b48'); layer.batchDraw(); });
218
+ group.on('mouseout', () => { document.body.style.cursor = 'default'; rect.fill('#2d3436'); layer.batchDraw(); });
219
 
220
  layer.add(group);
221
+
222
+ // Save Reference for Culling Logic
223
  nodeMap[node.id] = { x, y };
224
+ allNodes.push({
225
+ group: group,
226
+ x: x,
227
+ y: y,
228
+ visible: true
229
+ });
230
  });
231
 
232
  // Draw Connections
233
+ // Optimization: Draw connections on a generic "listening: false" shape to avoid hit-detection overhead
234
  data.connections.forEach(conn => {
235
+ const f = nodeMap[conn.f];
236
+ const t = nodeMap[conn.t];
237
+ if (f && t) {
 
 
 
 
 
 
 
238
  const line = new Konva.Line({
239
+ points: [f.x+20, f.y+50, f.x+20, t.y-10, t.x+20, t.y-10, t.x+20, t.y],
240
+ stroke: '#636e72', strokeWidth: 2, tension: 0.2, opacity: 0.5,
241
+ listening: false // Critical: Don't calculate mouse hits for lines
242
  });
243
  layer.add(line);
244
  line.moveToBottom();
 
246
  });
247
 
248
  layer.batchDraw();
249
+ // Initial cull calculation
250
+ updateVisibleNodes();
 
 
 
251
  }
252
 
253
+ function log(msg, type='info') {
254
+ const consoleBody = document.getElementById('logOutput');
255
+ const color = type === 'error' ? '#ff7675' : '#55efc4';
256
+ if(consoleBody) {
257
+ consoleBody.innerHTML += `<div style="color:${color}">> ${msg}</div>`;
258
+ consoleBody.scrollTop = consoleBody.scrollHeight;
259
+ }
260
+ }