// 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 || 'メモ…'}
{/* 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="削除"> {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;