You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
187 lines
5.7 KiB
187 lines
5.7 KiB
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 }; |
|
} |
|
}
|
|
|