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 (
);
}
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(„