/* * pitch.jsx — Tactical board: pitch SVG, markers, arrows, * tool selection, undo/redo, and board tabs. * * ── DEPENDS ON (from app.jsx, loaded before this file) ── * Globals: C, uid * React hooks: useState, useRef, useCallback, useEffect, useReducer * * ── EXPORTS (to global scope) ── * TacticalBoard — used by App in app.jsx */ /* ═══════════════════════════════════════════════════════════ BOARD HISTORY REDUCER ═══════════════════════════════════════════════════════════ */ function boardReducer(state, action) { switch (action.type) { case "INIT": return { current: action.board, history: [], future: [] }; case "DO": return { current: action.board, history: state.history.slice(-30).concat([state.current]), future: [], }; case "UNDO": { if (!state.history.length) return state; return { current: state.history[state.history.length - 1], history: state.history.slice(0, -1), future: [state.current].concat(state.future), }; } case "REDO": { if (!state.future.length) return state; return { current: state.future[0], history: state.history.concat([state.current]), future: state.future.slice(1), }; } default: return state; } } /* ═══════════════════════════════════════════════════════════ ACTION MENU Expands horizontally to the left so it doesn't drop behind the pitch SVG. ═══════════════════════════════════════════════════════════ */ function ActionMenu(_ref) { var onUndo = _ref.onUndo, onRedo = _ref.onRedo, onClear = _ref.onClear, canUndo = _ref.canUndo, canRedo = _ref.canRedo; var _s = useState(false), open = _s[0], setOpen = _s[1]; var ref = useRef(null); useEffect(function () { if (!open) return; var handler = function (e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener("pointerdown", handler); return function () { document.removeEventListener("pointerdown", handler); }; }, [open]); var items = [ { label: "↩ Undo", action: onUndo, disabled: !canUndo }, { label: "↪ Redo", action: onRedo, disabled: !canRedo }, { label: "⌫ Clear All", action: onClear, color: C.red }, ]; return (
{open && (
{items.map(function (item, i) { return ( ); })}
)}
); } /* ═══════════════════════════════════════════════════════════ PITCH SVG ═══════════════════════════════════════════════════════════ */ function PitchSVG(_ref) { var children = _ref.children, svgRef = _ref.svgRef, onPointerDown = _ref.onPointerDown, onPointerMove = _ref.onPointerMove, onPointerUp = _ref.onPointerUp; return ( {children} ); } /* ═══════════════════════════════════════════════════════════ TACTICAL BOARD ═══════════════════════════════════════════════════════════ */ function TacticalBoard(_ref) { var boards = _ref.boards, activeBoard = _ref.activeBoard, onUpdate = _ref.onUpdate, onSetActive = _ref.onSetActive, onAddBoard = _ref.onAddBoard, onRenameBoard = _ref.onRenameBoard, onDeleteBoard = _ref.onDeleteBoard; var svgRef = useRef(null); var _t = useState("o"), tool = _t[0], setTool = _t[1]; var _a = useState(null), arrowDraft = _a[0], setArrowDraft = _a[1]; var _e = useState(null), editingName = _e[0], setEditingName = _e[1]; var _n = useState(null), newMarkerId = _n[0], setNewMarkerId = _n[1]; var _d = useState(null), dragPos = _d[0], setDragPos = _d[1]; var dragRef = useRef(null); var boardObj = boards.find(function (b) { return b.id === activeBoard; }) || boards[0]; var _br = useReducer(boardReducer, { current: { markers: boardObj ? boardObj.markers : [], arrows: boardObj ? boardObj.arrows : [] }, history: [], future: [], }), boardState = _br[0], dispatch = _br[1]; var prevBoardId = useRef(activeBoard); useEffect(function () { if (activeBoard !== prevBoardId.current) { var b = boards.find(function (bb) { return bb.id === activeBoard; }) || boards[0]; if (b) dispatch({ type: "INIT", board: { markers: b.markers, arrows: b.arrows } }); prevBoardId.current = activeBoard; } }, [activeBoard, boards]); var cur = boardState.current; var syncToParent = useCallback(function (newBoard) { if (boardObj) onUpdate(boardObj.id, { ...boardObj, markers: newBoard.markers, arrows: newBoard.arrows }); }, [boardObj, onUpdate]); var doAction = useCallback(function (newBoard) { dispatch({ type: "DO", board: newBoard }); syncToParent(newBoard); }, [syncToParent]); var handleUndo = useCallback(function () { if (!boardState.history.length) return; var prev = boardState.history[boardState.history.length - 1]; dispatch({ type: "UNDO" }); syncToParent(prev); }, [boardState.history, syncToParent]); var handleRedo = useCallback(function () { if (!boardState.future.length) return; var next = boardState.future[0]; dispatch({ type: "REDO" }); syncToParent(next); }, [boardState.future, syncToParent]); var handleClear = useCallback(function () { var empty = { markers: [], arrows: [] }; doAction(empty); }, [doAction]); useEffect(function () { if (!newMarkerId) return; var t = setTimeout(function () { setNewMarkerId(null); }, 500); return function () { clearTimeout(t); }; }, [newMarkerId]); var getSVGPoint = useCallback(function (e) { var svg = svgRef.current; if (!svg) return { x: 0, y: 0 }; var pt = svg.createSVGPoint(); pt.x = e.clientX; pt.y = e.clientY; var transformed = pt.matrixTransform(svg.getScreenCTM().inverse()); return { x: transformed.x, y: transformed.y }; }, []); var hitMarker = useCallback(function (pt, markers, r) { r = r || 20; return markers.slice().reverse().find(function (m) { return Math.hypot(m.x - pt.x, m.y - pt.y) < r; }); }, []); var hitArrow = useCallback(function (pt, arrows) { return arrows.slice().reverse().find(function (a) { var mx = (a.x1 + a.x2) / 2, my = (a.y1 + a.y2) / 2; return Math.hypot(mx - pt.x, my - pt.y) < 25; }); }, []); var handlePointerDown = useCallback(function (e) { e.preventDefault(); if (svgRef.current && svgRef.current.setPointerCapture) svgRef.current.setPointerCapture(e.pointerId); var pt = getSVGPoint(e); if (tool === "x" || tool === "o") { var id = uid(); var newBoard = { ...cur, markers: cur.markers.concat([{ id: id, type: tool, x: pt.x, y: pt.y }]) }; doAction(newBoard); setNewMarkerId(id); } else if (tool === "arrow") { setArrowDraft({ x1: pt.x, y1: pt.y, x2: pt.x, y2: pt.y }); } else if (tool === "select") { var hit = hitMarker(pt, cur.markers); if (hit) { dragRef.current = { id: hit.id, ox: pt.x - hit.x, oy: pt.y - hit.y }; setDragPos({ id: hit.id, x: hit.x, y: hit.y }); } } else if (tool === "erase") { var hitM = hitMarker(pt, cur.markers); if (hitM) { doAction({ ...cur, markers: cur.markers.filter(function (m) { return m.id !== hitM.id; }) }); return; } var hitA = hitArrow(pt, cur.arrows); if (hitA) { doAction({ ...cur, arrows: cur.arrows.filter(function (a) { return a.id !== hitA.id; }) }); } } }, [tool, cur, doAction, getSVGPoint, hitMarker, hitArrow]); var handlePointerMove = useCallback(function (e) { e.preventDefault(); var pt = getSVGPoint(e); if (dragRef.current && tool === "select") { setDragPos({ id: dragRef.current.id, x: pt.x - dragRef.current.ox, y: pt.y - dragRef.current.oy }); } else if (arrowDraft && tool === "arrow") { setArrowDraft(function (a) { return a ? { ...a, x2: pt.x, y2: pt.y } : null; }); } }, [tool, arrowDraft, getSVGPoint]); var handlePointerUp = useCallback(function (e) { if (arrowDraft && tool === "arrow") { if (Math.hypot(arrowDraft.x2 - arrowDraft.x1, arrowDraft.y2 - arrowDraft.y1) > 15) { doAction({ ...cur, arrows: cur.arrows.concat([{ id: uid(), x1: arrowDraft.x1, y1: arrowDraft.y1, x2: arrowDraft.x2, y2: arrowDraft.y2 }]) }); } setArrowDraft(null); } if (dragRef.current && dragPos) { var finalMarkers = cur.markers.map(function (m) { return m.id === dragPos.id ? { ...m, x: dragPos.x, y: dragPos.y } : m; }); doAction({ ...cur, markers: finalMarkers }); dragRef.current = null; setDragPos(null); } if (svgRef.current && svgRef.current.releasePointerCapture && e) svgRef.current.releasePointerCapture(e.pointerId); }, [arrowDraft, tool, cur, dragPos, doAction]); var tools = [ { id: "select", label: "Move", icon: "✋" }, { id: "o", label: "Us", icon: "○", color: C.blue }, { id: "x", label: "Opp", icon: "✕", color: C.red }, { id: "arrow", label: "Arrow", icon: "→", color: C.yellow }, { id: "erase", label: "Erase", icon: "⌫" }, ]; var cursorMap = { select: "grab", x: "crosshair", o: "crosshair", arrow: "crosshair", erase: "pointer" }; var displayMarkers = cur.markers.map(function (m) { if (dragPos && dragPos.id === m.id) return { ...m, x: dragPos.x, y: dragPos.y }; return m; }); return (
{/* Toolbar */}
{tools.map(function (t) { return ( ); })}
0} canRedo={boardState.future.length > 0} />
{/* Pitch */}
{/* Arrows */} {cur.arrows.map(function (a) { return ( ); })} {arrowDraft && ( )} {/* Markers */} {displayMarkers.map(function (m, idx) { var oNum = displayMarkers.filter(function (mm, j) { return mm.type === "o" && j <= idx; }).length; var isNew = m.id === newMarkerId; var isDragging = dragPos && dragPos.id === m.id; return ( {isNew && ( )} {m.type === "o" ? ( <> {oNum} ) : ( <> )} ); })}
{/* Board tabs */}
{boards.map(function (b) { return ( ); })} Double-tap to rename
); }