Skip to content

Commit b385b37

Browse files
committed
Improved node port layout and connection handling with precise positioning
1 parent 2f2ea2d commit b385b37

File tree

2 files changed

+177
-75
lines changed

2 files changed

+177
-75
lines changed

src/VisualScripting.js

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,25 @@ const VisualScripting = () => {
118118

119119
const clickedPort = findClickedPort(x, y);
120120
if (clickedPort) {
121-
setConnecting(clickedPort);
121+
const node = nodes.find(n => n.id === clickedPort.nodeId);
122+
const { width } = renderer.getNodeDimensions(node, canvasRef.current.getContext('2d'));
123+
124+
const triangleWidth = 6;
125+
const triangleHeight = 10;
126+
const portOffset = 5;
127+
128+
const portY = node.y + 35 + (clickedPort.index * 14);
129+
const portYMiddle = portY + (triangleHeight / 2);
130+
131+
const portX = clickedPort.isInput
132+
? node.x - portOffset - (triangleWidth / 2)
133+
: node.x + width + portOffset + (triangleWidth / 2);
134+
135+
setConnecting({
136+
...clickedPort,
137+
portY: portYMiddle,
138+
portX
139+
});
122140
} else {
123141
const clickedNode = findClickedNode(x, y);
124142
if (clickedNode) {
@@ -226,33 +244,36 @@ const VisualScripting = () => {
226244
};
227245

228246
const findClickedPort = (x, y) => {
229-
const PORT_WIDTH = 6; // Width of the gray arrow
230-
const PORT_HEIGHT = 10; // Height of the gray arrow
231-
const PORT_OFFSET = 5; // Distance from node border
232-
const SCALE_MULTIPLIER = 1.5; // Scale multiplier for hit detection
247+
const PORT_WIDTH = 6;
248+
const PORT_HEIGHT = 10;
249+
const PORT_OFFSET = 5;
250+
const SCALE_MULTIPLIER = 1.5;
251+
const VERTICAL_OFFSET = 2;
233252

234253
for (const node of nodes) {
235254
const nodeType = nodeTypes[node.type];
236-
const { width, portStartY } = renderer.getNodeDimensions(node, canvasRef.current.getContext('2d'));
255+
const { width } = renderer.getNodeDimensions(node, canvasRef.current.getContext('2d'));
256+
257+
let currentHeight = 35; // Start after title (25 + 10 padding)
237258

238259
// Check input ports
239260
for (let i = 0; i < nodeType.inputs.length; i++) {
240-
const portX = node.x - PORT_OFFSET - PORT_WIDTH; // Position of gray arrow
241-
const portY = node.y + portStartY + i * 20 - PORT_HEIGHT / 2;
261+
const portX = node.x - PORT_OFFSET - PORT_WIDTH;
262+
const portY = node.y + currentHeight + (i * 14);
242263

243264
if (x >= portX && x <= portX + PORT_WIDTH * SCALE_MULTIPLIER &&
244-
y >= portY && y <= portY + PORT_HEIGHT * SCALE_MULTIPLIER) {
265+
y >= portY - VERTICAL_OFFSET && y <= portY + PORT_HEIGHT + VERTICAL_OFFSET) {
245266
return { nodeId: node.id, isInput: true, index: i };
246267
}
247268
}
248269

249270
// Check output ports
250271
for (let i = 0; i < nodeType.outputs.length; i++) {
251-
const portX = node.x + width + PORT_OFFSET; // Position of gray arrow
252-
const portY = node.y + portStartY + i * 20 - PORT_HEIGHT / 2;
272+
const portX = node.x + width + PORT_OFFSET;
273+
const portY = node.y + currentHeight + (i * 14);
253274

254275
if (x >= portX && x <= portX + PORT_WIDTH * SCALE_MULTIPLIER &&
255-
y >= portY && y <= portY + PORT_HEIGHT * SCALE_MULTIPLIER) {
276+
y >= portY - VERTICAL_OFFSET && y <= portY + PORT_HEIGHT + VERTICAL_OFFSET) {
256277
return { nodeId: node.id, isInput: false, index: i };
257278
}
258279
}

src/engine/Renderer.js

Lines changed: 144 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -49,30 +49,77 @@ class Renderer {
4949

5050
getNodeDimensions(node, ctx) {
5151
const nodeType = nodeTypes[node.type];
52+
const maxPorts = Math.max(nodeType.inputs.length, nodeType.outputs.length);
53+
54+
// Set font for measurements
5255
ctx.font = `600 14px ${FONT_FAMILY}`;
53-
const titleWidth = ctx.measureText(node.type).width;
54-
55-
ctx.font = `400 13px ${FONT_FAMILY}`;
56-
const descriptionLines = this.renderDescription ? this.wrapText(ctx, nodeType.description, 180) : [];
57-
const descriptionHeight = this.renderDescription ? descriptionLines.length * 14 : 0;
58-
59-
const inputsHeight = nodeType.inputs.length * 20;
60-
const outputsHeight = nodeType.outputs.length * 20;
61-
const propertiesHeight = nodeType.properties ? nodeType.properties.reduce((height, prop) => {
62-
const isVisible = prop.visible === undefined ||
63-
(typeof prop.visible === 'function' ?
64-
prop.visible(node.properties) :
65-
prop.visible);
66-
return height + (isVisible ? 20 : 0);
67-
}, 0) : 0;
68-
69-
const width = Math.max(200, titleWidth + 20, ...(this.renderDescription ? descriptionLines.map(line => ctx.measureText(line).width + 20) : []));
70-
const height = 35 + descriptionHeight + Math.max(inputsHeight, outputsHeight) + propertiesHeight;
71-
56+
57+
// Calculate title height
58+
const titleHeight = 25;
59+
60+
// Calculate ports area height with tighter spacing
61+
const portSpacing = 14;
62+
const portVerticalGap = 5; // Gap between input and output sections
63+
const portsHeight = maxPorts > 0 ? (maxPorts - 1) * portSpacing + 20 + portVerticalGap : 0;
64+
65+
// Calculate properties height - only for visible properties
66+
let propertiesHeight = 0;
67+
if (nodeType.properties) {
68+
const visibleProps = nodeType.properties.filter(prop => {
69+
if (prop.type === 'array') return false;
70+
if (prop.visible === undefined) return true;
71+
if (typeof prop.visible === 'function') {
72+
return prop.visible(node.properties);
73+
}
74+
return prop.visible;
75+
});
76+
77+
propertiesHeight = visibleProps.length * 16;
78+
}
79+
80+
// Calculate required width based on port names
81+
let maxInputWidth = 0;
82+
let maxOutputWidth = 0;
83+
84+
ctx.font = `500 12px ${FONT_FAMILY}`;
85+
86+
// Measure input port names
87+
nodeType.inputs.forEach(input => {
88+
const textWidth = ctx.measureText(input.name).width;
89+
maxInputWidth = Math.max(maxInputWidth, textWidth);
90+
});
91+
92+
// Measure output port names
93+
nodeType.outputs.forEach(output => {
94+
const textWidth = ctx.measureText(output.name).width;
95+
maxOutputWidth = Math.max(maxOutputWidth, textWidth);
96+
});
97+
98+
// Calculate total width needed with guaranteed minimum spacing
99+
const portPadding = 40;
100+
const centerPadding = 40;
101+
const inputSection = maxInputWidth + portPadding;
102+
const outputSection = maxOutputWidth + portPadding;
103+
104+
const width = inputSection + outputSection + centerPadding
105+
106+
// Calculate total height with minimal padding
107+
const height = titleHeight +
108+
(maxPorts > 0 ? portsHeight : 0) +
109+
(propertiesHeight > 0 ? propertiesHeight + 10 : 0) +
110+
5;
111+
112+
// Calculate where ports should start (right after title)
113+
const portStartY = titleHeight;
114+
72115
return {
73116
width,
74117
height,
75-
portStartY: 35 + descriptionHeight
118+
portStartY,
119+
maxInputWidth,
120+
maxOutputWidth,
121+
inputSection,
122+
outputSection
76123
};
77124
}
78125

@@ -136,11 +183,11 @@ class Renderer {
136183
}
137184

138185
const startPort = edge.start.isInput
139-
? { x: startNode.x, y: startNode.y + startDims.portStartY + edge.start.index * 20 }
140-
: { x: startNode.x + startDims.width, y: startNode.y + startDims.portStartY + edge.start.index * 20 };
186+
? { x: startNode.x, y: startNode.y + startDims.portStartY + (edge.start.index * 14) + 8 }
187+
: { x: startNode.x + startDims.width, y: startNode.y + startDims.portStartY + (edge.start.index * 14) + 8 };
141188
const endPort = edge.end.isInput
142-
? { x: endNode.x, y: endNode.y + endDims.portStartY + edge.end.index * 20 }
143-
: { x: endNode.x + endDims.width, y: endNode.y + endDims.portStartY + edge.end.index * 20 };
189+
? { x: endNode.x, y: endNode.y + endDims.portStartY + (edge.end.index * 14) + 8 }
190+
: { x: endNode.x + endDims.width, y: endNode.y + endDims.portStartY + (edge.end.index * 14) + 8 };
144191

145192
// Calculate control points for the Bezier curve
146193
const dx = endPort.x - startPort.x;
@@ -177,16 +224,17 @@ class Renderer {
177224
drawPortIcon(ctx, x, y, isInput) {
178225
const offset = 5; // Distance from node border
179226
const arrowX = isInput ? x - offset : x + offset;
227+
const portY = y;
180228

181229
ctx.beginPath();
182230
if (isInput) {
183-
ctx.moveTo(arrowX - 6, y - 5);
184-
ctx.lineTo(arrowX - 6, y + 5);
185-
ctx.lineTo(arrowX, y);
231+
ctx.moveTo(arrowX - 6, portY - 5);
232+
ctx.lineTo(arrowX - 6, portY + 5);
233+
ctx.lineTo(arrowX, portY);
186234
} else {
187-
ctx.moveTo(arrowX, y - 5);
188-
ctx.lineTo(arrowX, y + 5);
189-
ctx.lineTo(arrowX + 6, y);
235+
ctx.moveTo(arrowX, portY - 5);
236+
ctx.lineTo(arrowX, portY + 5);
237+
ctx.lineTo(arrowX + 6, portY);
190238
}
191239
ctx.closePath();
192240
ctx.strokeStyle = '#999999';
@@ -225,7 +273,7 @@ class Renderer {
225273

226274
drawNodes(ctx, nodes, edges, selectedNodes) {
227275
nodes.forEach(node => {
228-
const { width, height } = this.getNodeDimensions(node, ctx);
276+
const { width, height, inputSection, outputSection } = this.getNodeDimensions(node, ctx);
229277

230278
if (!this.isRectInView(node.x, node.y, width, height, ctx.canvas.width, ctx.canvas.height)) {
231279
return;
@@ -290,10 +338,9 @@ class Renderer {
290338

291339
// Input ports
292340
nodeType.inputs.forEach((input, i) => {
293-
const portY = node.y + currentHeight + i * 20;
341+
const portY = node.y + currentHeight + (i * 14);
294342
const isControl = input.type === 'control';
295343

296-
// Check if port is connected
297344
const isPortConnected = edges.some(edge =>
298345
edge.end.nodeId === node.id &&
299346
edge.end.index === i &&
@@ -306,12 +353,13 @@ class Renderer {
306353

307354
this.drawLabelArrow(ctx, node.x + 15, portY, isControl);
308355
ctx.fillStyle = 'white';
309-
ctx.fillText(input.name, node.x + 35, portY + 5);
356+
ctx.fillText(input.name, node.x + 35, portY + 4);
310357
});
311358

312-
// Output ports
359+
// Output ports - removed the initial gap
313360
nodeType.outputs.forEach((output, i) => {
314-
const portY = node.y + currentHeight + i * 20;
361+
// Calculate portY without the conditional gap
362+
const portY = node.y + currentHeight + (i * 14);
315363
const isControl = output.type === 'control';
316364

317365
const isPortConnected = edges.some(edge =>
@@ -326,31 +374,36 @@ class Renderer {
326374

327375
ctx.fillStyle = 'white';
328376
const textWidth = ctx.measureText(output.name).width;
329-
ctx.fillText(output.name, node.x + width - textWidth - 35, portY + 5);
377+
ctx.fillText(output.name, node.x + width - textWidth - 35, portY + 4);
330378
this.drawLabelArrow(ctx, node.x + width - 25, portY, isControl);
331379
});
332380

333-
currentHeight += Math.max(nodeType.inputs.length, nodeType.outputs.length) * 15;
381+
// Add the gap after drawing all ports if needed
382+
currentHeight += Math.max(nodeType.inputs.length, nodeType.outputs.length) * 14;
383+
if (nodeType.inputs.length > 0 && nodeType.outputs.length > 0) {
384+
currentHeight += 5; // Only add gap if both inputs and outputs exist
385+
}
334386

335-
// Node properties
387+
// Node properties - only render non-array properties
336388
if (nodeType.properties) {
337389
ctx.fillStyle = 'white';
338-
ctx.font = `500 13px ${FONT_FAMILY}`;
390+
ctx.font = `500 12px ${FONT_FAMILY}`; // Slightly smaller font for properties
339391

340-
nodeType.properties.forEach((prop, index) => {
341-
// Check if property should be visible
392+
nodeType.properties.forEach(prop => {
393+
// Skip array type properties and check visibility
394+
if (prop.type === 'array') return;
395+
342396
const isVisible = prop.visible === undefined ||
343397
(typeof prop.visible === 'function' ?
344398
prop.visible(node.properties) :
345399
prop.visible);
346400

347-
if (isVisible && prop.type !== 'array') { // Skip array type properties
401+
if (isVisible) {
348402
let displayValue = node.properties[prop.name] !== undefined ? node.properties[prop.name] : prop.default;
349-
// Skip rendering if the value is an object
350403
if (typeof displayValue === 'object') return;
351404

352405
const text = `${prop.name}: ${displayValue}`;
353-
currentHeight += 20;
406+
currentHeight += 16; // Reduced from 20
354407
ctx.fillText(text, node.x + 10, node.y + currentHeight);
355408
}
356409
});
@@ -359,26 +412,54 @@ class Renderer {
359412
}
360413

361414
drawConnectionLine(ctx, connecting, mousePosition, nodes) {
362-
const startNode = nodes.find(n => n.id === connecting.nodeId);
363-
if (startNode) {
364-
const { width } = this.getNodeDimensions(startNode, ctx);
365-
const startX = connecting.isInput ? startNode.x : startNode.x + width;
366-
const startY = startNode.y + this.getNodeDimensions(startNode, ctx).portStartY + connecting.index * 20;
367-
const endX = mousePosition.x;
368-
const endY = mousePosition.y;
415+
if (!connecting) return;
416+
417+
const node = nodes.find(n => n.id === connecting.nodeId);
418+
if (!node) return;
419+
420+
const { width } = this.getNodeDimensions(node, ctx);
421+
const portY = node.y + this.getNodeDimensions(node, ctx).portStartY + (connecting.index * 14) + 8;
422+
423+
// Calculate X position to start from middle of port
424+
const portOffset = 5;
425+
let portX;
426+
427+
if (connecting.isInput) {
428+
if (nodeTypes[node.type].inputs[connecting.index].type === 'control') {
429+
// For control input ports: start from middle of triangle
430+
const triangleWidth = 6;
431+
portX = node.x - portOffset - (triangleWidth / 2);
432+
} else {
433+
// For data input ports: start from middle of circle
434+
portX = node.x - portOffset;
435+
}
436+
} else {
437+
if (nodeTypes[node.type].outputs[connecting.index].type === 'control') {
438+
// For control output ports: start from middle of triangle
439+
const triangleWidth = 6;
440+
portX = node.x + width + portOffset + (triangleWidth / 2);
441+
} else {
442+
// For data output ports: start from middle of circle
443+
portX = node.x + width + portOffset + 5; // +5 to match the circle x position in drawLabelArrow
444+
}
445+
}
369446

370-
// Calculate control points for the Bezier curve
371-
const dx = endX - startX;
372-
const controlPoint1 = { x: startX + dx * 0.5, y: startY };
373-
const controlPoint2 = { x: endX - dx * 0.5, y: endY };
447+
const startX = portX;
448+
const startY = portY;
449+
const endX = mousePosition.x;
450+
const endY = mousePosition.y;
374451

375-
ctx.beginPath();
376-
ctx.moveTo(startX, startY);
377-
ctx.bezierCurveTo(controlPoint1.x, controlPoint1.y, controlPoint2.x, controlPoint2.y, endX, endY);
378-
ctx.strokeStyle = '#FFFF00';
379-
ctx.lineWidth = 2;
380-
ctx.stroke();
381-
}
452+
// Calculate control points for the Bezier curve
453+
const dx = endX - startX;
454+
const controlPoint1 = { x: startX + dx * 0.5, y: startY };
455+
const controlPoint2 = { x: endX - dx * 0.5, y: endY };
456+
457+
ctx.beginPath();
458+
ctx.moveTo(startX, startY);
459+
ctx.bezierCurveTo(controlPoint1.x, controlPoint1.y, controlPoint2.x, controlPoint2.y, endX, endY);
460+
ctx.strokeStyle = '#999999';
461+
ctx.lineWidth = 2;
462+
ctx.stroke();
382463
}
383464

384465
setDarkTheme(isDarkTheme) {

0 commit comments

Comments
 (0)