diff --git a/static/node.js b/static/node.js index fbf2a3a..eb91d9c 100644 --- a/static/node.js +++ b/static/node.js @@ -2,186 +2,186 @@ import { Connection } from './connection.js'; import { generateNodeId, createSVGElement } from './utils.js'; export class ComponentNode { - constructor(type, x, y, app, props = {}) { - this.id = generateNodeId(); - this.type = type; - this.app = app; - this.props = { - label: type, - replication: 1, - cacheTTL: 0, - instanceSize: 'medium', - ...props - }; - - this.group = createSVGElement('g', { class: 'dropped', 'data-type': type }); - - const rect = createSVGElement('rect', { - x, - y, - width: 0, // will be updated after measuring text - height: app.componentSize.height, - fill: '#121212', - stroke: '#00ff88', - 'stroke-width': 1, - rx: 4, - ry: 4 - }); - - this.text = createSVGElement('text', { - x: x + app.componentSize.width / 2, - y: y + app.componentSize.height / 2 + 5, - 'text-anchor': 'middle', - 'font-size': 16, - fill: '#ccc' - }); - - this.text.textContent = this.props.label; - - // Temporarily add text to canvas to measure its width - app.canvas.appendChild(this.text); - const textWidth = this.text.getBBox().width; - const padding = 20; - const finalWidth = textWidth + padding; - - // Update rect width and center text - rect.setAttribute('width', finalWidth); - this.text.setAttribute('x', x + finalWidth / 2); - - // Clean up temporary text - app.canvas.removeChild(this.text); - - this.group.appendChild(rect); - this.group.appendChild(this.text); - this.group.__nodeObj = this; - - this.initDrag(); - - this.group.addEventListener('click', (e) => { - e.stopPropagation(); - if (app.arrowMode) { - Connection.handleClick(this, app); - } else { - // Use observer to notify node selection - app.nodeSelectionSubject.notifyNodeSelected(this); - app.selectedNode = this; // Keep app state in sync for now - } - }); - - this.group.addEventListener('dblclick', (e) => { - e.stopPropagation(); - if (!app.arrowMode) { - // Use observer pattern instead of direct call - app.propertiesPanelSubject.notifyPropertiesPanelRequested(this); - } - }); - - app.canvas.appendChild(this.group); // ✅ now correctly adding full group - app.placedComponents.push(this); - app.runButton.disabled = false; - - this.x = x; - this.y = y; - } - - initDrag() { - let offsetX, offsetY; - - const onMouseMove = (e) => { - const pt = this.app.canvas.createSVGPoint(); - pt.x = e.clientX; - pt.y = e.clientY; - const svgP = pt.matrixTransform(this.app.canvas.getScreenCTM().inverse()); - - const newX = svgP.x - offsetX; - const newY = svgP.y - offsetY; - - this.group.setAttribute('transform', `translate(${newX}, ${newY})`); - - this.x = newX; - this.y = newY; - - this.app.updateConnectionsFor(this); - }; - - const onMouseUp = () => { - window.removeEventListener('mousemove', onMouseMove); - window.removeEventListener('mouseup', onMouseUp); - }; - - this.group.addEventListener('mousedown', (e) => { - e.preventDefault(); - const pt = this.app.canvas.createSVGPoint(); - pt.x = e.clientX; - pt.y = e.clientY; - const svgP = pt.matrixTransform(this.app.canvas.getScreenCTM().inverse()); - - const ctm = this.group.getCTM(); - offsetX = svgP.x - ctm.e; - offsetY = svgP.y - ctm.f; - - window.addEventListener('mousemove', onMouseMove); - window.addEventListener('mouseup', onMouseUp); - }); - } - - updateLabel(newLabel) { - this.props.label = newLabel; - this.text.textContent = newLabel; - const textWidth = this.text.getBBox().width; - const padding = 20; - const finalWidth = textWidth + padding; - - this.group.querySelector('rect').setAttribute('width', finalWidth); - this.text.setAttribute('x', parseFloat(this.group.querySelector('rect').getAttribute('x')) + finalWidth / 2); - - } - - getCenter() { - const bbox = this.group.getBBox(); - const ctm = this.group.getCTM(); - const x = ctm.e + bbox.x + bbox.width / 2; - const y = ctm.f + bbox.y + bbox.height / 2; - return { x, y }; - } - - select() { - // Use observer to clear previous selection and select this node - if (this.app.selectedNode && this.app.selectedNode !== this) { - this.app.nodeSelectionSubject.notifyNodeDeselected(this.app.selectedNode); - } - this.group.classList.add('selected'); - this.app.selectedNode = this; - } - - deselect() { - this.group.classList.remove('selected'); - if (this.app.selectedNode === this) { - this.app.selectedNode = null; - } - } - - getConnectionPointToward(otherNode) { - const bbox = this.group.getBBox(); - const ctm = this.group.getCTM(); - - const centerX = ctm.e + bbox.x + bbox.width / 2; - const centerY = ctm.f + bbox.y + bbox.height / 2; - - const otherCenter = otherNode.getCenter(); - - let edgeX = centerX; - let edgeY = centerY; - - const dx = otherCenter.x - centerX; - const dy = otherCenter.y - centerY; - - if (Math.abs(dx) > Math.abs(dy)) { - edgeX += dx > 0 ? bbox.width / 2 : -bbox.width / 2; - } else { - edgeY += dy > 0 ? bbox.height / 2 : -bbox.height / 2; - } - - return { x: edgeX, y: edgeY }; - } + constructor(type, x, y, app, props = {}) { + this.id = generateNodeId(); + this.type = type; + this.app = app; + this.props = { + label: type, + replication: 1, + cacheTTL: 0, + instanceSize: 'medium', + ...props + }; + + this.group = createSVGElement('g', { class: 'dropped', 'data-type': type }); + + const rect = createSVGElement('rect', { + x, + y, + width: 0, // will be updated after measuring text + height: app.componentSize.height, + fill: '#121212', + stroke: '#00ff88', + 'stroke-width': 1, + rx: 4, + ry: 4 + }); + + this.text = createSVGElement('text', { + x: x + app.componentSize.width / 2, + y: y + app.componentSize.height / 2 + 5, + 'text-anchor': 'middle', + 'font-size': 16, + fill: '#ccc' + }); + + this.text.textContent = this.props.label; + + // Temporarily add text to canvas to measure its width + app.canvas.appendChild(this.text); + const textWidth = this.text.getBBox().width; + const padding = 20; + const finalWidth = textWidth + padding; + + // Update rect width and center text + rect.setAttribute('width', finalWidth); + this.text.setAttribute('x', x + finalWidth / 2); + + // Clean up temporary text + app.canvas.removeChild(this.text); + + this.group.appendChild(rect); + this.group.appendChild(this.text); + this.group.__nodeObj = this; + + this.initDrag(); + + this.group.addEventListener('click', (e) => { + e.stopPropagation(); + if (app.arrowMode) { + Connection.handleClick(this, app); + } else { + // Use observer to notify node selection + app.nodeSelectionSubject.notifyNodeSelected(this); + app.selectedNode = this; // Keep app state in sync for now + } + }); + + this.group.addEventListener('dblclick', (e) => { + e.stopPropagation(); + if (!app.arrowMode) { + // Use observer pattern instead of direct call + //app.propertiesPanelSubject.notifyPropertiesPanelRequested(this); + } + }); + + app.canvas.appendChild(this.group); // ✅ now correctly adding full group + app.placedComponents.push(this); + app.runButton.disabled = false; + + this.x = x; + this.y = y; + } + + initDrag() { + let offsetX, offsetY; + + const onMouseMove = (e) => { + const pt = this.app.canvas.createSVGPoint(); + pt.x = e.clientX; + pt.y = e.clientY; + const svgP = pt.matrixTransform(this.app.canvas.getScreenCTM().inverse()); + + const newX = svgP.x - offsetX; + const newY = svgP.y - offsetY; + + this.group.setAttribute('transform', `translate(${newX}, ${newY})`); + + this.x = newX; + this.y = newY; + + this.app.updateConnectionsFor(this); + }; + + const onMouseUp = () => { + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('mouseup', onMouseUp); + }; + + this.group.addEventListener('mousedown', (e) => { + e.preventDefault(); + const pt = this.app.canvas.createSVGPoint(); + pt.x = e.clientX; + pt.y = e.clientY; + const svgP = pt.matrixTransform(this.app.canvas.getScreenCTM().inverse()); + + const ctm = this.group.getCTM(); + offsetX = svgP.x - ctm.e; + offsetY = svgP.y - ctm.f; + + window.addEventListener('mousemove', onMouseMove); + window.addEventListener('mouseup', onMouseUp); + }); + } + + updateLabel(newLabel) { + this.props.label = newLabel; + this.text.textContent = newLabel; + const textWidth = this.text.getBBox().width; + const padding = 20; + const finalWidth = textWidth + padding; + + this.group.querySelector('rect').setAttribute('width', finalWidth); + this.text.setAttribute('x', parseFloat(this.group.querySelector('rect').getAttribute('x')) + finalWidth / 2); + + } + + getCenter() { + const bbox = this.group.getBBox(); + const ctm = this.group.getCTM(); + const x = ctm.e + bbox.x + bbox.width / 2; + const y = ctm.f + bbox.y + bbox.height / 2; + return { x, y }; + } + + select() { + // Use observer to clear previous selection and select this node + if (this.app.selectedNode && this.app.selectedNode !== this) { + this.app.nodeSelectionSubject.notifyNodeDeselected(this.app.selectedNode); + } + this.group.classList.add('selected'); + this.app.selectedNode = this; + } + + deselect() { + this.group.classList.remove('selected'); + if (this.app.selectedNode === this) { + this.app.selectedNode = null; + } + } + + getConnectionPointToward(otherNode) { + const bbox = this.group.getBBox(); + const ctm = this.group.getCTM(); + + const centerX = ctm.e + bbox.x + bbox.width / 2; + const centerY = ctm.f + bbox.y + bbox.height / 2; + + const otherCenter = otherNode.getCenter(); + + let edgeX = centerX; + let edgeY = centerY; + + const dx = otherCenter.x - centerX; + const dy = otherCenter.y - centerY; + + if (Math.abs(dx) > Math.abs(dy)) { + edgeX += dx > 0 ? bbox.width / 2 : -bbox.width / 2; + } else { + edgeY += dy > 0 ? bbox.height / 2 : -bbox.height / 2; + } + + return { x: edgeX, y: edgeY }; + } }