HX Editor

import { useState, useRef, useCallback } from „react“; const S32 = Math.sqrt(3) / 2; function hexPoints(cx, cy, r) { return { top: { x: cx, y: cy – r }, ur: { x: cx + r * S32, y: cy – r * 0.5 }, lr: { x: cx + r * S32, y: cy + r * 0.5 }, bot: { x: cx, y: cy + r }, ll: { x: cx – r * S32, y: cy + r * 0.5 }, ul: { x: cx – r * S32, y: cy – r * 0.5 }, }; } function pts(…ps) { return ps.map(p => `${Math.round(p.x)},${Math.round(p.y)}`).join(“ „); } function HexShape({ hex, isSelected, activeSection, onSectionClick, onDragStart }) { const { x, y, r, sections, id, borderColor, borderWidth } = hex; const p = hexPoints(x, y, r); const w = r * S32 * 2; const lx = x – r * S32; const bw = borderWidth ?? 2; const bc = borderColor ?? „#ffffff“; const secPolys = { top: pts(p.top, p.ur, p.ul), mid: pts(p.ur, p.lr, p.ll, p.ul), bot: pts(p.lr, p.bot, p.ll), }; const secBounds = { top: { x: lx, y: y – r, width: w, height: r * 0.5 }, mid: { x: lx, y: y – r * 0.5, width: w, height: r }, bot: { x: lx, y: y + r * 0.5, width: w, height: r * 0.5 }, }; const secCenters = { top: { x, y: y – r * 0.62 }, mid: { x, y }, bot: { x, y: y + r * 0.62 }, }; return ( {[„top“, „mid“, „bot“].map(sec => ( ))} {/* clip entire hex so border doesn’t bleed outside */} {/* Section fills + images */} {[„top“, „mid“, „bot“].map(sec => { const s = sections[sec]; const bounds = secBounds[sec]; const ring = isSelected && activeSection === sec; return ( { e.stopPropagation(); onSectionClick(sec); }} style={{ cursor: „pointer“ }}> {s.imageUrl && ( )} ); })} {/* Subtle section dividers (always neutral, not the border color) */} {/* Outer hex border only — drawn inside the clipping path so it never bleeds */} {bw > 0 && ( )} {/* Text labels */} {[„top“, „mid“, „bot“].map(sec => { const s = sections[sec]; if (!s.text) return null; const c = secCenters[sec]; const lines = s.text.split(„\n“).filter(Boolean); const lh = s.fontSize * 1.3; return lines.map((line, i) => ( {line} )); })} {/* Selection indicator */} {isSelected && ( )} ); } function MiniHex({ sections, borderColor, borderWidth, size = 28 }) { const r = size * 0.5; const h = size * 1.155; const cx = size / 2, cy = h / 2; const p = hexPoints(cx, cy, r); const bw = borderWidth ?? 2; const bc = borderColor ?? „#ffffff“; return ( {bw > 0 && ( )} ); } const BG_COLORS = [ „#EE538B“,“#D12771″,“#A11950″, „#4589FF“,“#0f62fe“,“#0043CE“, „#3DDBD9″,“#009D9A“,“#005D5D“, „#42BE65″,“#24A148″,“#198038“, „#BE95FF“,“#8A3FFC“,“#6929C4″, „#F1C21B“,“#D4A017″,“#8E6A00″, „#FF8389″,“#DA1E28″,“#A2191F“, „#8D8D8D“,“#393939″,“#161616″, „#ffffff“, ]; const TEXT_COLORS = [ „#ffffff“,“#f4f4f4″,“#e0e0e0″,“#c6c6c6″,“#a8a8a8″, „#6f6f6f“,“#525252″,“#393939″,“#262626″,“#161616″, „#EE538B“,“#0f62fe“,“#009D9A“,“#42BE65″,“#8A3FFC“, ]; const BORDER_COLORS = [ „#ffffff“,“#f4f4f4″,“#c6c6c6″,“#8D8D8D“,“#393939″,“#161616″, „#EE538B“,“#D12771″,“#0f62fe“,“#0043CE“,“#009D9A“,“#24A148″, „#8A3FFC“,“#DA1E28″,“#F1C21B“,“#000000″, ]; const SEC_LABELS = { top: „Oben — Dreieck“, mid: „Mitte — Rechteck“, bot: „Unten — Dreieck“ }; function mkSection(color, text = „“, fontSize = 18, textColor = „#ffffff“) { return { color, text, fontSize, textColor, imageUrl: null }; } function mkHex(id, x, y) { return { id, x, y, r: 120, borderColor: „#ffffff“, borderWidth: 2, sections: { top: mkSection(„#EE538B“), mid: mkSection(„#D12771“, „Neue\nWabe“, 20), bot: mkSection(„#A11950“), }, }; } let _id = 2; export default function HXEditor() { const [hexagons, setHexagons] = useState([{ id: 1, x: 300, y: 300, r: 130, borderColor: „#ffffff“, borderWidth: 2, sections: { top: mkSection(„#EE538B“), mid: mkSection(„#D12771“, „NAS\nGateway“, 22), bot: mkSection(„#A11950“, „Storwize V7000“, 14), }, }]); const [selId, setSelId] = useState(1); const [selSec, setSelSec] = useState(„mid“); const [drag, setDrag] = useState(null); const [library, setLibrary] = useState([]); const svgRef = useRef(null); const fileRef = useRef(null); const pendingSec = useRef({ id: null, sec: null }); const selHex = hexagons.find(h => h.id === selId); const curSec = selHex?.sections[selSec]; // Update a section property const upd = (hexId, sec, key, val) => setHexagons(prev => prev.map(h => h.id !== hexId ? h : { …h, sections: { …h.sections, [sec]: { …h.sections[sec], [key]: val } } } )); // Update a hex-level property (border etc.) const updHex = (hexId, key, val) => setHexagons(prev => prev.map(h => h.id !== hexId ? h : { …h, [key]: val })); const onDragStart = useCallback((hexId, e) => { e.preventDefault(); const svg = svgRef.current; const pt = svg.createSVGPoint(); pt.x = e.clientX; pt.y = e.clientY; const sp = pt.matrixTransform(svg.getScreenCTM().inverse()); const hex = hexagons.find(h => h.id === hexId); setDrag({ id: hexId, ox: sp.x – hex.x, oy: sp.y – hex.y }); setSelId(hexId); }, [hexagons]); const onMouseMove = useCallback(e => { if (!drag) return; const svg = svgRef.current; const pt = svg.createSVGPoint(); pt.x = e.clientX; pt.y = e.clientY; const sp = pt.matrixTransform(svg.getScreenCTM().inverse()); setHexagons(prev => prev.map(h => h.id === drag.id ? { …h, x: sp.x – drag.ox, y: sp.y – drag.oy } : h )); }, [drag]); const onMouseUp = useCallback(() => setDrag(null), []); const addHex = () => { const id = _id++; setHexagons(prev => […prev, mkHex(id, 160 + Math.random() * 560, 180 + Math.random() * 340)]); setSelId(id); setSelSec(„mid“); }; const delHex = () => { if (hexagons.length <= 1) return; const rest = hexagons.filter(h => h.id !== selId); setHexagons(rest); setSelId(rest[0].id); }; const saveToLibrary = () => { if (!selHex) return; const name = selHex.sections.mid.text.split(„\n“)[0].trim() || `Wabe ${library.length + 1}`; setLibrary(prev => […prev, { id: Date.now(), name, …JSON.parse(JSON.stringify({ sections: selHex.sections, borderColor: selHex.borderColor, borderWidth: selHex.borderWidth })) }]); }; const applyFromLibrary = (saved) => { if (!selId) return; setHexagons(prev => prev.map(h => h.id === selId ? { …h, sections: JSON.parse(JSON.stringify(saved.sections)), borderColor: saved.borderColor, borderWidth: saved.borderWidth } : h )); }; const openImagePicker = () => { pendingSec.current = { id: selId, sec: selSec }; fileRef.current?.click(); }; const handleImageImport = (e) => { const file = e.target.files[0]; if (!file) return; const { id, sec } = pendingSec.current; const reader = new FileReader(); reader.onload = ev => upd(id, sec, „imageUrl“, ev.target.result); reader.readAsDataURL(file); e.target.value = „“; }; const exportPNG = () => { const svg = svgRef.current; if (!svg) return; let src = new XMLSerializer().serializeToString(svg); if (!src.includes(‚xmlns=“http://www.w3.org/2000/svg“‚)) src = src.replace(„ { ctx.drawImage(img, 0, 0, W, H); URL.revokeObjectURL(url); const a = document.createElement(„a“); a.download = „HX-Export.png“; a.href = canvas.toDataURL(„image/png“); a.click(); }; img.onerror = () => URL.revokeObjectURL(url); img.src = url; }; const swatchRow = (colors, curColor, onPick, cols = 8) => (
{colors.map(c => (
onPick(c)} title={c} style={{ width: 21, height: 21, background: c, cursor: „pointer“, borderRadius: 2, flexShrink: 0, boxSizing: „border-box“, border: (curColor || „“).toLowerCase() === c.toLowerCase() ? „2.5px solid #161616“ : „1px solid rgba(0,0,0,0.18)“, }} /> ))}
); const S = { root: { display: „flex“, flexDirection: „column“, height: „100vh“, fontFamily: „‚IBM Plex Sans‘,’Helvetica Neue‘,Arial,sans-serif“, background: „#f4f4f4“, overflow: „hidden“ }, hdr: { background: „#161616“, color: „white“, height: 48, display: „flex“, alignItems: „center“, padding: „0 16px“, gap: 16, flexShrink: 0, borderBottom: „1px solid #393939“ }, body: { display: „flex“, flex: 1, overflow: „hidden“ }, left: { width: 264, background: „white“, borderRight: „1px solid #e0e0e0“, display: „flex“, flexDirection: „column“, flexShrink: 0, overflow: „hidden“ }, sec: { padding: „12px 14px“, borderBottom: „1px solid #e0e0e0“ }, lbl: { fontSize: 10, fontWeight: 700, color: „#8d8d8d“, textTransform: „uppercase“, letterSpacing: 1.5, marginBottom: 8, display: „block“ }, right: { width: 200, background: „white“, borderLeft: „1px solid #e0e0e0“, display: „flex“, flexDirection: „column“, flexShrink: 0 }, canvas: { flex: 1, overflow: „auto“, position: „relative“ }, }; return (
{/* Header */}
HX Editor
{[„HX-Edit“, „HX-Print“].map(t => ( ))}
{/* ─── Left panel ─────────────────────────── */}