// Note (memo) — a sticky-style annotation that can either float free OR be
// anchored to a specific node / edge. Anchored notes draw a dotted line back
// to their target so the relationship is visible.
//
// Shape:
// { id, x, y, text, color,
// anchor: { type:'node'|'edge', id:'...' } | null }
//
// The connector line is drawn by Canvas (since it has both note and anchor in
// scope); this component only renders the card itself + the delete button.
function Note({ note, selected, onSelect, onDrag, onDragEnd, onDelete, worldZoom }){
const moveState = React.useRef(null);
const onPointerDown = (e) => {
if (e.target.closest('.note-delete')) return;
e.stopPropagation();
onSelect(note.id);
moveState.current = { sx:e.clientX, sy:e.clientY, ox:note.x, oy:note.y, moved:false };
e.currentTarget.setPointerCapture(e.pointerId);
};
const onPointerMove = (e) => {
if (!moveState.current) return;
const dx = (e.clientX - moveState.current.sx) / (worldZoom || 1);
const dy = (e.clientY - moveState.current.sy) / (worldZoom || 1);
if (Math.abs(dx) + Math.abs(dy) > 2) moveState.current.moved = true;
onDrag(note.id, moveState.current.ox + dx, moveState.current.oy + dy);
};
const onPointerUp = (e) => {
if (!moveState.current) return;
const moved = moveState.current.moved;
moveState.current = null;
onDragEnd(note.id, moved);
try { e.currentTarget.releasePointerCapture(e.pointerId); } catch(_){}
};
const isAnchored = !!(note.anchor && note.anchor.type && note.anchor.id);
return (
{note.text || 'Note…'}
{/* Use Lucide SVG icon (not the "×" glyph) so the mark sits exactly at
the geometric center of the round button — matches Node delete-btn. */}
{e.stopPropagation(); onDelete(note.id);}} title="Delete">
{window.TourIcon ? : '×'}
);
}
// Resolve the world-coords anchor point of a note's link target.
// Returns null if the anchor target no longer exists (e.g. the linked
// node/edge was deleted) — the caller treats as "not anchored".
//
// For an edge-anchored note we point at the BOTTOM of the label pill (the
// pill is 26px tall, so anchor y = labelY + 14) — that way the dotted line
// reads as "this memo annotates this label" rather than crossing through it.
// `edgeOffset` is the same offset Canvas computes for parallel edges; pass
// the matching one so curved edges get an accurate label position.
function resolveNoteAnchor(note, nodes, edges, edgeOffsets){
const a = note?.anchor;
if (!a || !a.type || !a.id) return null;
if (a.type === 'node'){
const n = nodes.find(x => x.id === a.id);
return n ? { x: n.x, y: n.y } : null;
}
if (a.type === 'edge'){
const e = edges.find(x => x.id === a.id);
if (!e) return null;
if (window.computeEdgeLabelPosition){
const idx = edges.indexOf(e);
const off = edgeOffsets ? (edgeOffsets[idx] || 0) : 0;
const nodesById = Object.fromEntries(nodes.map(n => [n.id, n]));
// labelOffsetY = +14 → anchor sits on the pill's bottom edge so the
// dotted line approaches the label from below.
return window.computeEdgeLabelPosition(e, nodesById, off, 14);
}
// Fallback if the helper isn't loaded yet.
const from = nodes.find(n => n.id === e.from);
const to = nodes.find(n => n.id === e.to);
if (!from || !to) return null;
return { x: (from.x + to.x)/2, y: (from.y + to.y)/2 };
}
return null;
}
window.Note = Note;
window.resolveNoteAnchor = resolveNoteAnchor;