@zuilib/text-editor 0.7.1 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/DRAWING_FORMAT.md +3 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2088 -2111
- package/dist/styles.css +17 -29
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1543,2152 +1543,2159 @@ function moveGroup(shapes, selected, clickedId) {
|
|
|
1543
1543
|
return new Map(shapes.filter((s) => ids.has(s.id)).map((s) => [s.id, s]));
|
|
1544
1544
|
}
|
|
1545
1545
|
|
|
1546
|
-
// src/drawing/
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1546
|
+
// src/drawing/skeleton.ts
|
|
1547
|
+
var COLOR_PRESETS = {
|
|
1548
|
+
/** No outline, light surface; text stays dark */
|
|
1549
|
+
plain: { stroke: "transparent", fill: "#f1f3f5" },
|
|
1550
|
+
/** Solid dark card; text renders light */
|
|
1551
|
+
black: { stroke: "#1e1e1e", fill: "#1e1e1e" },
|
|
1552
|
+
gray: { stroke: "#1e1e1e", fill: "transparent" },
|
|
1553
|
+
red: { stroke: "#e03131", fill: "#ffc9c9" },
|
|
1554
|
+
green: { stroke: "#2f9e44", fill: "#b2f2bb" },
|
|
1555
|
+
blue: { stroke: "#1971c2", fill: "#a5d8ff" },
|
|
1556
|
+
orange: { stroke: "#f08c00", fill: "#ffec99" },
|
|
1557
|
+
purple: { stroke: "#7048e8", fill: "#d0bfff" }
|
|
1558
|
+
};
|
|
1559
|
+
var COLOR_NAMES = Object.keys(COLOR_PRESETS);
|
|
1560
|
+
var TEXT_WRAP_WIDTH = 260;
|
|
1561
|
+
var SLOT_GAP = 6;
|
|
1562
|
+
var RANK_GAP = 90;
|
|
1563
|
+
var STACK_GAP = 40;
|
|
1564
|
+
var LAYOUT_ORIGIN = 32;
|
|
1565
|
+
var CANVAS_MARGIN = 32;
|
|
1566
|
+
var MIN_AUTO_CANVAS_HEIGHT = 120;
|
|
1567
|
+
var DEFAULT_STROKE2 = "#1e1e1e";
|
|
1568
|
+
var STROKE_WIDTH = 2;
|
|
1569
|
+
function isDrawingSkeleton(value) {
|
|
1570
|
+
return isRecord2(value) && Array.isArray(value.boxes);
|
|
1567
1571
|
}
|
|
1568
|
-
function
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1572
|
+
function parseDrawingSkeleton(json) {
|
|
1573
|
+
try {
|
|
1574
|
+
const parsed = JSON.parse(json);
|
|
1575
|
+
if (!isDrawingSkeleton(parsed)) return EMPTY_DRAWING;
|
|
1576
|
+
return expandSkeleton(normalizeSkeleton(parsed));
|
|
1577
|
+
} catch {
|
|
1578
|
+
return EMPTY_DRAWING;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
function expandSkeleton(skeleton) {
|
|
1582
|
+
const boxes = dedupeById(skeleton.boxes);
|
|
1583
|
+
const boxTypes = new Map(boxes.map((box) => [box.id, box.type ?? "rect"]));
|
|
1584
|
+
const sizes = new Map(boxes.map((box) => [box.id, measureBox(box, boxTypes.get(box.id))]));
|
|
1585
|
+
const boxIds = new Set(boxTypes.keys());
|
|
1586
|
+
const connectors = (skeleton.connectors ?? []).filter(
|
|
1587
|
+
(c) => boxIds.has(endId(c.from)) && boxIds.has(endId(c.to)) && endId(c.from) !== endId(c.to)
|
|
1588
|
+
);
|
|
1589
|
+
const positions = layoutBoxes(boxes, sizes, connectors, skeleton.direction ?? "right");
|
|
1590
|
+
const boxShapes = boxes.map(
|
|
1591
|
+
(box) => expandBox(box, boxTypes.get(box.id), sizes.get(box.id), positions.get(box.id))
|
|
1586
1592
|
);
|
|
1593
|
+
const centers = new Map(boxShapes.map((shape) => [shape.id, shapeCenter(shape)]));
|
|
1594
|
+
const used = new Set(boxIds);
|
|
1595
|
+
const nextId = (prefix) => {
|
|
1596
|
+
let i = used.size + 1;
|
|
1597
|
+
while (used.has(`${prefix}${i}`)) i += 1;
|
|
1598
|
+
const id = `${prefix}${i}`;
|
|
1599
|
+
used.add(id);
|
|
1600
|
+
return id;
|
|
1601
|
+
};
|
|
1602
|
+
const connectorShapes = connectors.map((c) => expandConnector(c, centers, nextId("c")));
|
|
1603
|
+
const textShapes = (skeleton.texts ?? []).map((t) => expandText(t, nextId("t")));
|
|
1604
|
+
const shapes = resolveBindings([...boxShapes, ...connectorShapes, ...textShapes]);
|
|
1605
|
+
const bottom = shapes.reduce((max, shape) => {
|
|
1606
|
+
const b = bbox(shape);
|
|
1607
|
+
return Math.max(max, b.y + b.h);
|
|
1608
|
+
}, 0);
|
|
1609
|
+
return normalizeDrawingData({
|
|
1610
|
+
version: 2,
|
|
1611
|
+
...skeleton.canvasWidth !== void 0 ? { canvasWidth: skeleton.canvasWidth } : {},
|
|
1612
|
+
canvasHeight: skeleton.canvasHeight ?? Math.max(MIN_AUTO_CANVAS_HEIGHT, Math.ceil(bottom + CANVAS_MARGIN)),
|
|
1613
|
+
...skeleton.width !== void 0 ? { width: skeleton.width } : {},
|
|
1614
|
+
shapes
|
|
1615
|
+
});
|
|
1587
1616
|
}
|
|
1588
|
-
function
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
const
|
|
1602
|
-
const
|
|
1603
|
-
const
|
|
1604
|
-
const
|
|
1605
|
-
const
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
onClick: () => onStroke(color)
|
|
1616
|
-
},
|
|
1617
|
-
color
|
|
1618
|
-
))
|
|
1619
|
-
] }),
|
|
1620
|
-
showFillSwatches && /* @__PURE__ */ jsxs2("div", { className: "zui-drawing-props-group", "aria-label": "Fill color", children: [
|
|
1621
|
-
/* @__PURE__ */ jsx2("span", { className: "zui-drawing-swatch-label", children: "Fill" }),
|
|
1622
|
-
FILL_COLORS.map((color) => /* @__PURE__ */ jsx2(
|
|
1623
|
-
Swatch,
|
|
1624
|
-
{
|
|
1625
|
-
color,
|
|
1626
|
-
kind: "Fill",
|
|
1627
|
-
active: fill === color,
|
|
1628
|
-
onClick: () => onFill(color)
|
|
1629
|
-
},
|
|
1630
|
-
color
|
|
1631
|
-
))
|
|
1632
|
-
] }),
|
|
1633
|
-
connectors.length > 0 && /* @__PURE__ */ jsxs2("div", { className: "zui-drawing-props-group", "aria-label": "Connector routing", children: [
|
|
1634
|
-
/* @__PURE__ */ jsx2(OptionButton, { label: "Straight connector", active: allStraight, onClick: () => onRouting(false), children: UI_ICONS.straight }),
|
|
1635
|
-
/* @__PURE__ */ jsx2(OptionButton, { label: "Elbow connector (auto-routed)", active: allElbow, onClick: () => onRouting(true), children: UI_ICONS.elbow })
|
|
1636
|
-
] }),
|
|
1637
|
-
arrows.length > 0 && /* @__PURE__ */ jsxs2("div", { className: "zui-drawing-props-group", "aria-label": "Arrow direction", children: [
|
|
1638
|
-
/* @__PURE__ */ jsx2(OptionButton, { label: "One-way arrow", active: allOneWay, onClick: () => onDirection(false), children: UI_ICONS.oneWay }),
|
|
1639
|
-
/* @__PURE__ */ jsx2(OptionButton, { label: "Two-way arrow", active: allTwoWay, onClick: () => onDirection(true), children: UI_ICONS.twoWay })
|
|
1640
|
-
] }),
|
|
1641
|
-
selection.length > 0 && /* @__PURE__ */ jsx2("div", { className: "zui-drawing-props-group", children: /* @__PURE__ */ jsx2(
|
|
1642
|
-
OptionButton,
|
|
1643
|
-
{
|
|
1644
|
-
label: selection.length > 1 ? `Delete ${selection.length} shapes` : "Delete shape",
|
|
1645
|
-
className: "zui-drawing-tool-danger",
|
|
1646
|
-
onClick: onDelete,
|
|
1647
|
-
children: UI_ICONS.trash
|
|
1648
|
-
}
|
|
1649
|
-
) })
|
|
1650
|
-
] });
|
|
1617
|
+
function measureBox(box, type) {
|
|
1618
|
+
const def = BOX_DEFINITIONS[type];
|
|
1619
|
+
const probe = {
|
|
1620
|
+
id: "",
|
|
1621
|
+
type,
|
|
1622
|
+
x: 0,
|
|
1623
|
+
y: 0,
|
|
1624
|
+
w: def.defaultSize.w,
|
|
1625
|
+
h: def.defaultSize.h,
|
|
1626
|
+
stroke: "",
|
|
1627
|
+
fill: "",
|
|
1628
|
+
strokeWidth: 0
|
|
1629
|
+
};
|
|
1630
|
+
const area = def.textArea(probe);
|
|
1631
|
+
const ratioW = area.w / def.defaultSize.w;
|
|
1632
|
+
const ratioH = area.h / def.defaultSize.h;
|
|
1633
|
+
const wrapWidth = box.w !== void 0 ? Math.max(20, box.w * ratioW) : TEXT_WRAP_WIDTH;
|
|
1634
|
+
const slots = def.textSlots.flatMap((slot) => {
|
|
1635
|
+
const size = slotSize(box, slot, wrapWidth);
|
|
1636
|
+
return size ? [size] : [];
|
|
1637
|
+
});
|
|
1638
|
+
const innerW = slots.reduce((max, s) => Math.max(max, s.w), 0);
|
|
1639
|
+
const innerH = slots.reduce((sum, s) => sum + s.h, 0) + Math.max(0, slots.length - 1) * SLOT_GAP;
|
|
1640
|
+
return {
|
|
1641
|
+
w: box.w ?? Math.max(def.defaultSize.w, Math.ceil(innerW / ratioW)),
|
|
1642
|
+
h: box.h ?? Math.max(def.defaultSize.h, Math.ceil(innerH / ratioH))
|
|
1643
|
+
};
|
|
1651
1644
|
}
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1645
|
+
function slotSize(box, slot, wrapWidth) {
|
|
1646
|
+
const value = box[slot];
|
|
1647
|
+
if (!value) return null;
|
|
1648
|
+
const fontSize = slot === "text" ? FONT_SIZE : SMALL_FONT_SIZE;
|
|
1649
|
+
return textBoxSize(wrapText(value, wrapWidth, fontSize).join("\n"), fontSize);
|
|
1650
|
+
}
|
|
1651
|
+
function layoutBoxes(boxes, sizes, connectors, direction) {
|
|
1652
|
+
const positions = /* @__PURE__ */ new Map();
|
|
1653
|
+
const auto = boxes.filter((box) => box.x === void 0 || box.y === void 0);
|
|
1654
|
+
const autoIds = new Set(auto.map((box) => box.id));
|
|
1655
|
+
const edges = connectors.map((c) => ({ from: endId(c.from), to: endId(c.to) })).filter((e) => e.from !== e.to && autoIds.has(e.from) && autoIds.has(e.to));
|
|
1656
|
+
const ranks = rankNodes(
|
|
1657
|
+
auto.map((box) => box.id),
|
|
1658
|
+
acyclicEdges(
|
|
1659
|
+
auto.map((box) => box.id),
|
|
1660
|
+
edges
|
|
1661
|
+
)
|
|
1662
|
+
);
|
|
1663
|
+
const rows = [];
|
|
1664
|
+
for (const box of auto) {
|
|
1665
|
+
const rank = ranks.get(box.id) ?? 0;
|
|
1666
|
+
(rows[rank] ?? (rows[rank] = [])).push(box.id);
|
|
1667
|
+
}
|
|
1668
|
+
const main = direction === "right" ? "w" : "h";
|
|
1669
|
+
const cross = direction === "right" ? "h" : "w";
|
|
1670
|
+
const rowMain = rows.map((row) => row.reduce((max, id) => Math.max(max, sizes.get(id)[main]), 0));
|
|
1671
|
+
const rowCross = rows.map(
|
|
1672
|
+
(row) => row.reduce((sum, id) => sum + sizes.get(id)[cross], 0) + Math.max(0, row.length - 1) * STACK_GAP
|
|
1673
|
+
);
|
|
1674
|
+
const maxCross = rowCross.reduce((max, c) => Math.max(max, c), 0);
|
|
1675
|
+
let mainOffset = LAYOUT_ORIGIN;
|
|
1676
|
+
rows.forEach((row, rank) => {
|
|
1677
|
+
let crossOffset = LAYOUT_ORIGIN + (maxCross - rowCross[rank]) / 2;
|
|
1678
|
+
for (const id of row) {
|
|
1679
|
+
const size = sizes.get(id);
|
|
1680
|
+
const mainPos = Math.round(mainOffset + (rowMain[rank] - size[main]) / 2);
|
|
1681
|
+
const crossPos = Math.round(crossOffset);
|
|
1682
|
+
positions.set(
|
|
1683
|
+
id,
|
|
1684
|
+
direction === "right" ? { x: mainPos, y: crossPos } : { x: crossPos, y: mainPos }
|
|
1685
|
+
);
|
|
1686
|
+
crossOffset += size[cross] + STACK_GAP;
|
|
1687
|
+
}
|
|
1688
|
+
mainOffset += rowMain[rank] + RANK_GAP;
|
|
1689
|
+
});
|
|
1690
|
+
for (const box of boxes) {
|
|
1691
|
+
const computed = positions.get(box.id) ?? { x: LAYOUT_ORIGIN, y: LAYOUT_ORIGIN };
|
|
1692
|
+
positions.set(box.id, { x: box.x ?? computed.x, y: box.y ?? computed.y });
|
|
1693
|
+
}
|
|
1694
|
+
return positions;
|
|
1695
|
+
}
|
|
1696
|
+
function acyclicEdges(ids, edges) {
|
|
1697
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
1698
|
+
for (const edge of edges) (outgoing.get(edge.from) ?? outgoing.set(edge.from, []).get(edge.from)).push(edge);
|
|
1699
|
+
const state = /* @__PURE__ */ new Map();
|
|
1700
|
+
const kept = [];
|
|
1701
|
+
const visit = (id) => {
|
|
1702
|
+
state.set(id, "visiting");
|
|
1703
|
+
for (const edge of outgoing.get(id) ?? []) {
|
|
1704
|
+
const target = state.get(edge.to);
|
|
1705
|
+
if (target === "visiting") continue;
|
|
1706
|
+
kept.push(edge);
|
|
1707
|
+
if (target === void 0) visit(edge.to);
|
|
1708
|
+
}
|
|
1709
|
+
state.set(id, "done");
|
|
1669
1710
|
};
|
|
1670
|
-
|
|
1711
|
+
for (const id of ids) if (!state.has(id)) visit(id);
|
|
1712
|
+
return kept;
|
|
1671
1713
|
}
|
|
1672
|
-
function
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
const q = points[i + 1];
|
|
1686
|
-
if (Math.hypot(q.x - p.x, q.y - p.y) < 28) return [];
|
|
1687
|
-
if (isZ && i === 1) return [];
|
|
1688
|
-
return [{ index: i, x: (p.x + q.x) / 2, y: (p.y + q.y) / 2 }];
|
|
1689
|
-
});
|
|
1690
|
-
return /* @__PURE__ */ jsxs3("g", { className: "zui-drawing-selection", children: [
|
|
1691
|
-
ghosts.map(({ index, x, y }) => /* @__PURE__ */ jsx3(
|
|
1692
|
-
"circle",
|
|
1693
|
-
{
|
|
1694
|
-
className: "zui-drawing-handle zui-drawing-ghost",
|
|
1695
|
-
cx: x,
|
|
1696
|
-
cy: y,
|
|
1697
|
-
r: 4.5,
|
|
1698
|
-
strokeWidth: 1.5,
|
|
1699
|
-
strokeDasharray: "2 2",
|
|
1700
|
-
style: { cursor: "copy" },
|
|
1701
|
-
onPointerDown: (e) => onWaypointAdd(e, shape.id, index, { x, y }),
|
|
1702
|
-
children: /* @__PURE__ */ jsx3("title", { children: "Drag to add a bend" })
|
|
1703
|
-
},
|
|
1704
|
-
`ghost-${index}`
|
|
1705
|
-
)),
|
|
1706
|
-
waypoints.map((p, i) => /* @__PURE__ */ jsx3(
|
|
1707
|
-
"g",
|
|
1708
|
-
{
|
|
1709
|
-
onDoubleClick: (e) => {
|
|
1710
|
-
e.stopPropagation();
|
|
1711
|
-
onWaypointRemove(shape.id, i);
|
|
1712
|
-
},
|
|
1713
|
-
children: /* @__PURE__ */ jsx3(
|
|
1714
|
-
Handle,
|
|
1715
|
-
{
|
|
1716
|
-
x: p.x,
|
|
1717
|
-
y: p.y,
|
|
1718
|
-
square: true,
|
|
1719
|
-
cursor: "move",
|
|
1720
|
-
title: "Drag to move, double-click to remove",
|
|
1721
|
-
onPointerDown: (e) => onHandlePointerDown(e, { mode: "waypoint", id: shape.id, index: i })
|
|
1722
|
-
}
|
|
1723
|
-
)
|
|
1724
|
-
},
|
|
1725
|
-
`wp-${i}`
|
|
1726
|
-
)),
|
|
1727
|
-
elbowMid && /* @__PURE__ */ jsx3(
|
|
1728
|
-
Handle,
|
|
1729
|
-
{
|
|
1730
|
-
x: elbowMid.x,
|
|
1731
|
-
y: elbowMid.y,
|
|
1732
|
-
square: true,
|
|
1733
|
-
cursor: elbowVertical ? "ew-resize" : "ns-resize",
|
|
1734
|
-
title: "Slide the middle segment",
|
|
1735
|
-
onPointerDown: (e) => onHandlePointerDown(e, {
|
|
1736
|
-
mode: "elbow",
|
|
1737
|
-
id: shape.id,
|
|
1738
|
-
a: points[0],
|
|
1739
|
-
d: points[3],
|
|
1740
|
-
vertical: elbowVertical
|
|
1741
|
-
})
|
|
1742
|
-
}
|
|
1743
|
-
),
|
|
1744
|
-
["start", "end"].map((end) => {
|
|
1745
|
-
const p = end === "start" ? points[0] : points[points.length - 1];
|
|
1746
|
-
return /* @__PURE__ */ jsx3(
|
|
1747
|
-
Handle,
|
|
1748
|
-
{
|
|
1749
|
-
x: p.x,
|
|
1750
|
-
y: p.y,
|
|
1751
|
-
cursor: "crosshair",
|
|
1752
|
-
onPointerDown: (e) => onHandlePointerDown(e, { mode: "endpoint", id: shape.id, end, orig: shape })
|
|
1753
|
-
},
|
|
1754
|
-
end
|
|
1755
|
-
);
|
|
1756
|
-
})
|
|
1757
|
-
] });
|
|
1758
|
-
}
|
|
1759
|
-
const b = bbox(shape);
|
|
1760
|
-
const pad = 5;
|
|
1761
|
-
const corners = [
|
|
1762
|
-
{ corner: "nw", x: b.x - pad, y: b.y - pad },
|
|
1763
|
-
{ corner: "ne", x: b.x + b.w + pad, y: b.y - pad },
|
|
1764
|
-
{ corner: "sw", x: b.x - pad, y: b.y + b.h + pad },
|
|
1765
|
-
{ corner: "se", x: b.x + b.w + pad, y: b.y + b.h + pad }
|
|
1766
|
-
];
|
|
1767
|
-
const resizable = shape.type !== "text";
|
|
1768
|
-
return /* @__PURE__ */ jsxs3("g", { className: "zui-drawing-selection", children: [
|
|
1769
|
-
/* @__PURE__ */ jsx3(SelectionFrame, { rect: b, pad }),
|
|
1770
|
-
resizable && corners.map(({ corner, x, y }) => /* @__PURE__ */ jsx3(
|
|
1771
|
-
Handle,
|
|
1772
|
-
{
|
|
1773
|
-
x,
|
|
1774
|
-
y,
|
|
1775
|
-
square: true,
|
|
1776
|
-
cursor: corner === "nw" || corner === "se" ? "nwse-resize" : "nesw-resize",
|
|
1777
|
-
onPointerDown: (e) => onHandlePointerDown(e, { mode: "resize", id: shape.id, corner, orig: shape })
|
|
1778
|
-
},
|
|
1779
|
-
corner
|
|
1780
|
-
))
|
|
1781
|
-
] });
|
|
1714
|
+
function rankNodes(ids, edges) {
|
|
1715
|
+
const incoming = /* @__PURE__ */ new Map();
|
|
1716
|
+
for (const edge of edges) (incoming.get(edge.to) ?? incoming.set(edge.to, []).get(edge.to)).push(edge.from);
|
|
1717
|
+
const ranks = /* @__PURE__ */ new Map();
|
|
1718
|
+
const rankOf = (id) => {
|
|
1719
|
+
const known = ranks.get(id);
|
|
1720
|
+
if (known !== void 0) return known;
|
|
1721
|
+
const rank = (incoming.get(id) ?? []).reduce((max, from) => Math.max(max, rankOf(from) + 1), 0);
|
|
1722
|
+
ranks.set(id, rank);
|
|
1723
|
+
return rank;
|
|
1724
|
+
};
|
|
1725
|
+
for (const id of ids) rankOf(id);
|
|
1726
|
+
return ranks;
|
|
1782
1727
|
}
|
|
1783
|
-
function
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1728
|
+
function expandBox(box, type, size, position) {
|
|
1729
|
+
const preset = box.color ? COLOR_PRESETS[box.color] : void 0;
|
|
1730
|
+
const fill = box.fill ?? preset?.fill ?? BOX_DEFINITIONS[type].defaultFill ?? "transparent";
|
|
1731
|
+
return {
|
|
1732
|
+
id: box.id,
|
|
1733
|
+
type,
|
|
1734
|
+
x: position.x,
|
|
1735
|
+
y: position.y,
|
|
1736
|
+
w: size.w,
|
|
1737
|
+
h: size.h,
|
|
1738
|
+
stroke: box.stroke ?? preset?.stroke ?? DEFAULT_STROKE2,
|
|
1739
|
+
fill,
|
|
1740
|
+
strokeWidth: STROKE_WIDTH,
|
|
1741
|
+
...box.text ? { text: box.text } : {},
|
|
1742
|
+
...box.label ? { label: box.label } : {},
|
|
1743
|
+
...box.footer ? { footer: box.footer } : {}
|
|
1744
|
+
};
|
|
1799
1745
|
}
|
|
1800
|
-
function
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
width: r.w + 6,
|
|
1821
|
-
height: r.h + 6,
|
|
1822
|
-
rx: 4,
|
|
1823
|
-
fill: "none",
|
|
1824
|
-
stroke: "currentColor",
|
|
1825
|
-
strokeWidth: 1,
|
|
1826
|
-
opacity: 0.45
|
|
1827
|
-
},
|
|
1828
|
-
shapes[i].id
|
|
1829
|
-
)),
|
|
1830
|
-
/* @__PURE__ */ jsx3(SelectionFrame, { rect: unionRects(rects), pad: 8 })
|
|
1831
|
-
] });
|
|
1746
|
+
function expandConnector(connector, centers, fallbackId) {
|
|
1747
|
+
const type = connector.type ?? "arrow";
|
|
1748
|
+
const from = centers.get(endId(connector.from));
|
|
1749
|
+
const to = centers.get(endId(connector.to));
|
|
1750
|
+
return {
|
|
1751
|
+
id: connector.id ?? fallbackId,
|
|
1752
|
+
type,
|
|
1753
|
+
x: from.x,
|
|
1754
|
+
y: from.y,
|
|
1755
|
+
w: to.x - from.x,
|
|
1756
|
+
h: to.y - from.y,
|
|
1757
|
+
stroke: connector.stroke ?? (connector.color ? COLOR_PRESETS[connector.color].stroke : DEFAULT_STROKE2),
|
|
1758
|
+
fill: "transparent",
|
|
1759
|
+
strokeWidth: STROKE_WIDTH,
|
|
1760
|
+
startBinding: toBinding(connector.from),
|
|
1761
|
+
endBinding: toBinding(connector.to),
|
|
1762
|
+
...connector.text ? { text: connector.text } : {},
|
|
1763
|
+
...type === "arrow" && connector.bidirectional ? { bidirectional: true } : {},
|
|
1764
|
+
...connector.routing === "elbow" ? { routing: "elbow" } : {}
|
|
1765
|
+
};
|
|
1832
1766
|
}
|
|
1833
|
-
function
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
pointerEvents: "none"
|
|
1848
|
-
}
|
|
1849
|
-
);
|
|
1767
|
+
function expandText(text, fallbackId) {
|
|
1768
|
+
const size = textBoxSize(text.text);
|
|
1769
|
+
return {
|
|
1770
|
+
id: text.id ?? fallbackId,
|
|
1771
|
+
type: "text",
|
|
1772
|
+
x: text.x,
|
|
1773
|
+
y: text.y,
|
|
1774
|
+
w: size.w,
|
|
1775
|
+
h: size.h,
|
|
1776
|
+
stroke: text.stroke ?? (text.color ? COLOR_PRESETS[text.color].stroke : DEFAULT_STROKE2),
|
|
1777
|
+
fill: "transparent",
|
|
1778
|
+
strokeWidth: STROKE_WIDTH,
|
|
1779
|
+
text: text.text
|
|
1780
|
+
};
|
|
1850
1781
|
}
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1782
|
+
function endId(end) {
|
|
1783
|
+
return typeof end === "string" ? end : end.id;
|
|
1784
|
+
}
|
|
1785
|
+
function toBinding(end) {
|
|
1786
|
+
if (typeof end === "string" || !end.side) return { id: endId(end) };
|
|
1787
|
+
return { id: end.id, fixedPoint: SIDE_FIXED_POINTS[end.side] };
|
|
1788
|
+
}
|
|
1789
|
+
function shapeCenter(shape) {
|
|
1859
1790
|
const b = bbox(shape);
|
|
1860
|
-
|
|
1861
|
-
switch (shape.type) {
|
|
1862
|
-
case "rect":
|
|
1863
|
-
return /* @__PURE__ */ jsx4(
|
|
1864
|
-
"rect",
|
|
1865
|
-
{
|
|
1866
|
-
...stroke,
|
|
1867
|
-
x: b.x,
|
|
1868
|
-
y: b.y,
|
|
1869
|
-
width: b.w,
|
|
1870
|
-
height: b.h,
|
|
1871
|
-
rx: Math.min(8, b.w / 4, b.h / 4),
|
|
1872
|
-
fill
|
|
1873
|
-
}
|
|
1874
|
-
);
|
|
1875
|
-
case "ellipse":
|
|
1876
|
-
return /* @__PURE__ */ jsx4(
|
|
1877
|
-
"ellipse",
|
|
1878
|
-
{
|
|
1879
|
-
...stroke,
|
|
1880
|
-
cx: b.x + b.w / 2,
|
|
1881
|
-
cy: b.y + b.h / 2,
|
|
1882
|
-
rx: b.w / 2,
|
|
1883
|
-
ry: b.h / 2,
|
|
1884
|
-
fill
|
|
1885
|
-
}
|
|
1886
|
-
);
|
|
1887
|
-
case "diamond":
|
|
1888
|
-
return /* @__PURE__ */ jsx4(
|
|
1889
|
-
"polygon",
|
|
1890
|
-
{
|
|
1891
|
-
...stroke,
|
|
1892
|
-
points: `${n(b.x + b.w / 2)},${n(b.y)} ${n(b.x + b.w)},${n(b.y + b.h / 2)} ${n(b.x + b.w / 2)},${n(b.y + b.h)} ${n(b.x)},${n(b.y + b.h / 2)}`,
|
|
1893
|
-
fill
|
|
1894
|
-
}
|
|
1895
|
-
);
|
|
1896
|
-
case "note": {
|
|
1897
|
-
const f = NOTE_FOLD(b);
|
|
1898
|
-
const r = b.x + b.w;
|
|
1899
|
-
const btm = b.y + b.h;
|
|
1900
|
-
return /* @__PURE__ */ jsxs4("g", { children: [
|
|
1901
|
-
/* @__PURE__ */ jsx4(
|
|
1902
|
-
"path",
|
|
1903
|
-
{
|
|
1904
|
-
...stroke,
|
|
1905
|
-
d: `M ${n(b.x)} ${n(b.y)} H ${n(r)} V ${n(btm - f)} L ${n(r - f)} ${n(btm)} H ${n(b.x)} Z`,
|
|
1906
|
-
fill
|
|
1907
|
-
}
|
|
1908
|
-
),
|
|
1909
|
-
/* @__PURE__ */ jsx4(
|
|
1910
|
-
"path",
|
|
1911
|
-
{
|
|
1912
|
-
...stroke,
|
|
1913
|
-
d: `M ${n(r - f)} ${n(btm)} V ${n(btm - f)} H ${n(r)}`,
|
|
1914
|
-
fill: "rgba(0,0,0,0.08)"
|
|
1915
|
-
}
|
|
1916
|
-
)
|
|
1917
|
-
] });
|
|
1918
|
-
}
|
|
1919
|
-
case "cylinder": {
|
|
1920
|
-
const ry = CYLINDER_RY(b);
|
|
1921
|
-
const rx = b.w / 2;
|
|
1922
|
-
const cx = b.x + rx;
|
|
1923
|
-
return /* @__PURE__ */ jsxs4("g", { children: [
|
|
1924
|
-
/* @__PURE__ */ jsx4(
|
|
1925
|
-
"path",
|
|
1926
|
-
{
|
|
1927
|
-
...stroke,
|
|
1928
|
-
d: `M ${n(b.x)} ${n(b.y + ry)} V ${n(b.y + b.h - ry)} A ${n(rx)} ${n(ry)} 0 0 0 ${n(b.x + b.w)} ${n(b.y + b.h - ry)} V ${n(b.y + ry)}`,
|
|
1929
|
-
fill
|
|
1930
|
-
}
|
|
1931
|
-
),
|
|
1932
|
-
/* @__PURE__ */ jsx4("ellipse", { ...stroke, cx, cy: b.y + ry, rx, ry, fill })
|
|
1933
|
-
] });
|
|
1934
|
-
}
|
|
1935
|
-
case "cloud": {
|
|
1936
|
-
const p = (u, v) => `${n(b.x + u * b.w)} ${n(b.y + v * b.h)}`;
|
|
1937
|
-
return /* @__PURE__ */ jsx4(
|
|
1938
|
-
"path",
|
|
1939
|
-
{
|
|
1940
|
-
...stroke,
|
|
1941
|
-
d: `M ${p(0.22, 0.86)} C ${p(0.04, 0.88)} ${p(0, 0.58)} ${p(0.16, 0.5)} C ${p(0.08, 0.26)} ${p(0.3, 0.12)} ${p(0.42, 0.28)} C ${p(0.5, 0.02)} ${p(0.76, 0.04)} ${p(0.78, 0.3)} C ${p(0.98, 0.26)} ${p(1.04, 0.56)} ${p(0.88, 0.64)} C ${p(1, 0.8)} ${p(0.9, 0.9)} ${p(0.76, 0.86)} Z`,
|
|
1942
|
-
fill
|
|
1943
|
-
}
|
|
1944
|
-
);
|
|
1945
|
-
}
|
|
1946
|
-
case "queue": {
|
|
1947
|
-
const rx = QUEUE_RX(b);
|
|
1948
|
-
const ry = b.h / 2;
|
|
1949
|
-
return /* @__PURE__ */ jsxs4("g", { children: [
|
|
1950
|
-
/* @__PURE__ */ jsx4(
|
|
1951
|
-
"path",
|
|
1952
|
-
{
|
|
1953
|
-
...stroke,
|
|
1954
|
-
d: `M ${n(b.x + rx)} ${n(b.y)} H ${n(b.x + b.w - rx)} V ${n(b.y + b.h)} H ${n(b.x + rx)} A ${n(rx)} ${n(ry)} 0 0 1 ${n(b.x + rx)} ${n(b.y)} Z`,
|
|
1955
|
-
fill
|
|
1956
|
-
}
|
|
1957
|
-
),
|
|
1958
|
-
/* @__PURE__ */ jsx4(
|
|
1959
|
-
"ellipse",
|
|
1960
|
-
{
|
|
1961
|
-
...stroke,
|
|
1962
|
-
cx: b.x + b.w - rx,
|
|
1963
|
-
cy: b.y + ry,
|
|
1964
|
-
rx,
|
|
1965
|
-
ry,
|
|
1966
|
-
fill
|
|
1967
|
-
}
|
|
1968
|
-
)
|
|
1969
|
-
] });
|
|
1970
|
-
}
|
|
1971
|
-
case "actor": {
|
|
1972
|
-
const figH = b.h * ACTOR_FIGURE_RATIO;
|
|
1973
|
-
const cx = b.x + b.w / 2;
|
|
1974
|
-
const r = Math.max(4, Math.min(figH * 0.16, b.w * 0.2));
|
|
1975
|
-
const neck = b.y + r * 2;
|
|
1976
|
-
const hip = b.y + figH * 0.62;
|
|
1977
|
-
const armY = neck + figH * 0.12;
|
|
1978
|
-
const reach = Math.min(b.w * 0.32, figH * 0.3);
|
|
1979
|
-
return /* @__PURE__ */ jsxs4("g", { children: [
|
|
1980
|
-
/* @__PURE__ */ jsx4("circle", { ...stroke, cx, cy: b.y + r, r, fill }),
|
|
1981
|
-
/* @__PURE__ */ jsx4(
|
|
1982
|
-
"path",
|
|
1983
|
-
{
|
|
1984
|
-
...stroke,
|
|
1985
|
-
fill: "none",
|
|
1986
|
-
d: `M ${n(cx)} ${n(neck)} V ${n(hip)} M ${n(cx - reach)} ${n(armY)} H ${n(cx + reach)} M ${n(cx)} ${n(hip)} L ${n(cx - reach)} ${n(b.y + figH)} M ${n(cx)} ${n(hip)} L ${n(cx + reach)} ${n(b.y + figH)}`
|
|
1987
|
-
}
|
|
1988
|
-
)
|
|
1989
|
-
] });
|
|
1990
|
-
}
|
|
1991
|
-
default:
|
|
1992
|
-
return null;
|
|
1993
|
-
}
|
|
1791
|
+
return { x: b.x + b.w / 2, y: b.y + b.h / 2 };
|
|
1994
1792
|
}
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
1793
|
+
function dedupeById(items) {
|
|
1794
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1795
|
+
return items.filter((item) => seen.has(item.id) ? false : (seen.add(item.id), true));
|
|
1796
|
+
}
|
|
1797
|
+
function normalizeSkeleton(raw) {
|
|
1798
|
+
const value = raw;
|
|
1799
|
+
const boxes = value.boxes.flatMap((b) => {
|
|
1800
|
+
const box = normalizeBox(b);
|
|
1801
|
+
return box ? [box] : [];
|
|
1802
|
+
});
|
|
1803
|
+
const connectors = Array.isArray(value.connectors) ? value.connectors.flatMap((c) => {
|
|
1804
|
+
const connector = normalizeConnector(c);
|
|
1805
|
+
return connector ? [connector] : [];
|
|
1806
|
+
}) : [];
|
|
1807
|
+
const texts = Array.isArray(value.texts) ? value.texts.flatMap((t) => {
|
|
1808
|
+
const text = normalizeText(t);
|
|
1809
|
+
return text ? [text] : [];
|
|
1810
|
+
}) : [];
|
|
1811
|
+
const canvasWidth = finiteNumber(value.canvasWidth);
|
|
1812
|
+
const canvasHeight = finiteNumber(value.canvasHeight);
|
|
2003
1813
|
return {
|
|
2004
|
-
|
|
2005
|
-
|
|
1814
|
+
direction: value.direction === "down" ? "down" : "right",
|
|
1815
|
+
...canvasWidth !== void 0 ? { canvasWidth } : {},
|
|
1816
|
+
...canvasHeight !== void 0 ? { canvasHeight } : {},
|
|
1817
|
+
...isBlockWidth(value.width) ? { width: value.width } : {},
|
|
1818
|
+
boxes,
|
|
1819
|
+
connectors,
|
|
1820
|
+
texts
|
|
2006
1821
|
};
|
|
2007
1822
|
}
|
|
2008
|
-
function
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
return
|
|
1823
|
+
function normalizeBox(value) {
|
|
1824
|
+
if (!isRecord2(value) || !nonEmptyString(value.id)) return null;
|
|
1825
|
+
if (value.type !== void 0 && !(typeof value.type === "string" && isBoxType(value.type))) {
|
|
1826
|
+
return null;
|
|
1827
|
+
}
|
|
1828
|
+
return compact({
|
|
1829
|
+
id: value.id,
|
|
1830
|
+
type: value.type,
|
|
1831
|
+
label: nonEmptyString(value.label) ? value.label : void 0,
|
|
1832
|
+
text: nonEmptyString(value.text) ? value.text : void 0,
|
|
1833
|
+
footer: nonEmptyString(value.footer) ? value.footer : void 0,
|
|
1834
|
+
x: finiteNumber(value.x),
|
|
1835
|
+
y: finiteNumber(value.y),
|
|
1836
|
+
w: positiveNumber(value.w),
|
|
1837
|
+
h: positiveNumber(value.h),
|
|
1838
|
+
color: colorName(value.color),
|
|
1839
|
+
stroke: nonEmptyString(value.stroke) ? value.stroke : void 0,
|
|
1840
|
+
fill: nonEmptyString(value.fill) ? value.fill : void 0
|
|
1841
|
+
});
|
|
2014
1842
|
}
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
textAnchor: "middle"
|
|
2035
|
-
};
|
|
2036
|
-
return /* @__PURE__ */ jsxs5("g", { children: [
|
|
2037
|
-
slots.includes("label") && hideField !== "label" && (shape.label ? labelLines.map((line, i) => /* @__PURE__ */ createElement(
|
|
2038
|
-
"text",
|
|
2039
|
-
{
|
|
2040
|
-
...common,
|
|
2041
|
-
key: i,
|
|
2042
|
-
x: cx,
|
|
2043
|
-
y: area.y + SMALL_FONT_SIZE + i * smallLineH,
|
|
2044
|
-
fontSize: SMALL_FONT_SIZE,
|
|
2045
|
-
fontWeight: 600,
|
|
2046
|
-
letterSpacing: 0.3,
|
|
2047
|
-
opacity: 0.85,
|
|
2048
|
-
style: textStyle
|
|
2049
|
-
},
|
|
2050
|
-
line
|
|
2051
|
-
)) : hints && /* @__PURE__ */ jsx5(
|
|
2052
|
-
"text",
|
|
2053
|
-
{
|
|
2054
|
-
...common,
|
|
2055
|
-
x: cx,
|
|
2056
|
-
y: area.y + SMALL_FONT_SIZE,
|
|
2057
|
-
fontSize: SMALL_FONT_SIZE,
|
|
2058
|
-
opacity: 0.3,
|
|
2059
|
-
style: hintStyle,
|
|
2060
|
-
children: "Label"
|
|
2061
|
-
}
|
|
2062
|
-
)),
|
|
2063
|
-
slots.includes("text") && hideField !== "text" && (shape.text ? mainLines.map((line, i) => /* @__PURE__ */ createElement(
|
|
2064
|
-
"text",
|
|
2065
|
-
{
|
|
2066
|
-
...common,
|
|
2067
|
-
key: i,
|
|
2068
|
-
x: cx,
|
|
2069
|
-
y: mainStart + i * mainLineH,
|
|
2070
|
-
fontSize: FONT_SIZE,
|
|
2071
|
-
style: textStyle
|
|
2072
|
-
},
|
|
2073
|
-
line
|
|
2074
|
-
)) : hints && /* @__PURE__ */ jsx5(
|
|
2075
|
-
"text",
|
|
2076
|
-
{
|
|
2077
|
-
...common,
|
|
2078
|
-
x: cx,
|
|
2079
|
-
y: mainStart,
|
|
2080
|
-
fontSize: FONT_SIZE,
|
|
2081
|
-
opacity: 0.3,
|
|
2082
|
-
style: hintStyle,
|
|
2083
|
-
children: "Text"
|
|
2084
|
-
}
|
|
2085
|
-
)),
|
|
2086
|
-
slots.includes("footer") && hideField !== "footer" && (shape.footer ? footerLines.map((line, i) => /* @__PURE__ */ createElement(
|
|
2087
|
-
"text",
|
|
2088
|
-
{
|
|
2089
|
-
...common,
|
|
2090
|
-
key: i,
|
|
2091
|
-
x: cx,
|
|
2092
|
-
y: area.y + area.h - 3 - (footerLines.length - 1 - i) * smallLineH,
|
|
2093
|
-
fontSize: SMALL_FONT_SIZE,
|
|
2094
|
-
opacity: 0.65,
|
|
2095
|
-
style: textStyle
|
|
2096
|
-
},
|
|
2097
|
-
line
|
|
2098
|
-
)) : hints && /* @__PURE__ */ jsx5(
|
|
2099
|
-
"text",
|
|
2100
|
-
{
|
|
2101
|
-
...common,
|
|
2102
|
-
x: cx,
|
|
2103
|
-
y: area.y + area.h - 3,
|
|
2104
|
-
fontSize: SMALL_FONT_SIZE,
|
|
2105
|
-
opacity: 0.3,
|
|
2106
|
-
style: hintStyle,
|
|
2107
|
-
children: "Footer"
|
|
2108
|
-
}
|
|
2109
|
-
))
|
|
2110
|
-
] });
|
|
1843
|
+
function normalizeConnector(value) {
|
|
1844
|
+
if (!isRecord2(value)) return null;
|
|
1845
|
+
const from = normalizeEnd(value.from);
|
|
1846
|
+
const to = normalizeEnd(value.to);
|
|
1847
|
+
if (!from || !to) return null;
|
|
1848
|
+
if (value.type !== void 0 && !(typeof value.type === "string" && isConnectorType(value.type))) {
|
|
1849
|
+
return null;
|
|
1850
|
+
}
|
|
1851
|
+
return compact({
|
|
1852
|
+
id: nonEmptyString(value.id) ? value.id : void 0,
|
|
1853
|
+
type: value.type,
|
|
1854
|
+
from,
|
|
1855
|
+
to,
|
|
1856
|
+
text: nonEmptyString(value.text) ? value.text : void 0,
|
|
1857
|
+
bidirectional: value.bidirectional === true ? true : void 0,
|
|
1858
|
+
routing: value.routing === "elbow" ? "elbow" : void 0,
|
|
1859
|
+
color: colorName(value.color),
|
|
1860
|
+
stroke: nonEmptyString(value.stroke) ? value.stroke : void 0
|
|
1861
|
+
});
|
|
2111
1862
|
}
|
|
2112
|
-
function
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
const lines = wrapText(text, CONNECTOR_LABEL_MAX_WIDTH, CONNECTOR_FONT_SIZE);
|
|
2118
|
-
const size = textBoxSize(lines.join("\n"), CONNECTOR_FONT_SIZE);
|
|
2119
|
-
const { x: midX, y: midY } = connectorMidpoint(points);
|
|
2120
|
-
const lineH = CONNECTOR_FONT_SIZE * LINE_HEIGHT;
|
|
2121
|
-
const startY = midY - (lines.length - 1) * lineH / 2 + CONNECTOR_FONT_SIZE * 0.35;
|
|
2122
|
-
return /* @__PURE__ */ jsxs5("g", { children: [
|
|
2123
|
-
/* @__PURE__ */ jsx5(
|
|
2124
|
-
"rect",
|
|
2125
|
-
{
|
|
2126
|
-
x: midX - size.w / 2 - 5,
|
|
2127
|
-
y: midY - size.h / 2 - 3,
|
|
2128
|
-
width: size.w + 10,
|
|
2129
|
-
height: size.h + 6,
|
|
2130
|
-
rx: 5,
|
|
2131
|
-
className: "zui-drawing-label-plate",
|
|
2132
|
-
opacity: 0.94
|
|
2133
|
-
}
|
|
2134
|
-
),
|
|
2135
|
-
lines.map((line, i) => /* @__PURE__ */ jsx5(
|
|
2136
|
-
"text",
|
|
2137
|
-
{
|
|
2138
|
-
x: midX,
|
|
2139
|
-
y: startY + i * lineH,
|
|
2140
|
-
fill: shape.stroke,
|
|
2141
|
-
fontSize: CONNECTOR_FONT_SIZE,
|
|
2142
|
-
textAnchor: "middle",
|
|
2143
|
-
style: textStyle,
|
|
2144
|
-
children: line
|
|
2145
|
-
},
|
|
2146
|
-
i
|
|
2147
|
-
))
|
|
2148
|
-
] });
|
|
1863
|
+
function normalizeEnd(value) {
|
|
1864
|
+
if (nonEmptyString(value)) return value;
|
|
1865
|
+
if (!isRecord2(value) || !nonEmptyString(value.id)) return null;
|
|
1866
|
+
const side = value.side;
|
|
1867
|
+
return typeof side === "string" && side in SIDE_FIXED_POINTS ? { id: value.id, side } : { id: value.id };
|
|
2149
1868
|
}
|
|
2150
|
-
function
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
return /* @__PURE__ */ jsxs5("g", { children: [
|
|
2164
|
-
/* @__PURE__ */ jsx5(BoxGeometry, { shape, stroke }),
|
|
2165
|
-
/* @__PURE__ */ jsx5(BoxTexts, { shape, hideField, showHints })
|
|
2166
|
-
] });
|
|
2167
|
-
}
|
|
2168
|
-
if (isConnectorType(shape.type)) {
|
|
2169
|
-
const pts = points ?? [
|
|
2170
|
-
{ x: shape.x, y: shape.y },
|
|
2171
|
-
{ x: shape.x + shape.w, y: shape.y + shape.h }
|
|
2172
|
-
];
|
|
2173
|
-
return /* @__PURE__ */ jsxs5("g", { children: [
|
|
2174
|
-
/* @__PURE__ */ jsx5("path", { ...stroke, d: polylinePath(pts), fill: "none" }),
|
|
2175
|
-
arrowHeads(shape, pts).map((d, i) => /* @__PURE__ */ jsx5("path", { ...stroke, d, fill: "none" }, i)),
|
|
2176
|
-
shape.text && hideField !== "text" && /* @__PURE__ */ jsx5(ConnectorLabel, { shape, points: pts })
|
|
2177
|
-
] });
|
|
2178
|
-
}
|
|
2179
|
-
return /* @__PURE__ */ jsx5(
|
|
2180
|
-
"text",
|
|
2181
|
-
{
|
|
2182
|
-
x: shape.x,
|
|
2183
|
-
y: shape.y + FONT_SIZE,
|
|
2184
|
-
fill: shape.stroke,
|
|
2185
|
-
fontSize: FONT_SIZE,
|
|
2186
|
-
style: { userSelect: "none", whiteSpace: "pre" },
|
|
2187
|
-
children: (shape.text ?? "").split("\n").map((line, i) => /* @__PURE__ */ jsx5("tspan", { x: shape.x, dy: i === 0 ? 0 : FONT_SIZE * LINE_HEIGHT, children: line }, i))
|
|
2188
|
-
}
|
|
2189
|
-
);
|
|
1869
|
+
function normalizeText(value) {
|
|
1870
|
+
if (!isRecord2(value) || !nonEmptyString(value.text)) return null;
|
|
1871
|
+
const x = finiteNumber(value.x);
|
|
1872
|
+
const y = finiteNumber(value.y);
|
|
1873
|
+
if (x === void 0 || y === void 0) return null;
|
|
1874
|
+
return compact({
|
|
1875
|
+
id: nonEmptyString(value.id) ? value.id : void 0,
|
|
1876
|
+
x,
|
|
1877
|
+
y,
|
|
1878
|
+
text: value.text,
|
|
1879
|
+
color: colorName(value.color),
|
|
1880
|
+
stroke: nonEmptyString(value.stroke) ? value.stroke : void 0
|
|
1881
|
+
});
|
|
2190
1882
|
}
|
|
2191
|
-
function
|
|
2192
|
-
|
|
2193
|
-
points
|
|
2194
|
-
}) {
|
|
2195
|
-
if (isConnectorType(shape.type) && points) {
|
|
2196
|
-
return /* @__PURE__ */ jsx5("path", { d: polylinePath(points), fill: "none", stroke: "transparent", strokeWidth: 16 });
|
|
2197
|
-
}
|
|
2198
|
-
const b = bbox(shape);
|
|
2199
|
-
return /* @__PURE__ */ jsx5(
|
|
2200
|
-
"rect",
|
|
2201
|
-
{
|
|
2202
|
-
x: b.x - 2,
|
|
2203
|
-
y: b.y - 2,
|
|
2204
|
-
width: Math.max(b.w, 8) + 4,
|
|
2205
|
-
height: Math.max(b.h, 8) + 4,
|
|
2206
|
-
fill: "transparent",
|
|
2207
|
-
stroke: "none"
|
|
2208
|
-
}
|
|
2209
|
-
);
|
|
1883
|
+
function isRecord2(value) {
|
|
1884
|
+
return typeof value === "object" && value !== null;
|
|
2210
1885
|
}
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
onCommit(shape.id, field, valueRef.current);
|
|
2248
|
-
} else if (e.key === "Escape") {
|
|
2249
|
-
e.preventDefault();
|
|
2250
|
-
onCommit(shape.id, field, original);
|
|
1886
|
+
function nonEmptyString(value) {
|
|
1887
|
+
return typeof value === "string" && value !== "";
|
|
1888
|
+
}
|
|
1889
|
+
function finiteNumber(value) {
|
|
1890
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1891
|
+
}
|
|
1892
|
+
function positiveNumber(value) {
|
|
1893
|
+
const n2 = finiteNumber(value);
|
|
1894
|
+
return n2 !== void 0 && n2 > 0 ? n2 : void 0;
|
|
1895
|
+
}
|
|
1896
|
+
function colorName(value) {
|
|
1897
|
+
return typeof value === "string" && COLOR_NAMES.includes(value) ? value : void 0;
|
|
1898
|
+
}
|
|
1899
|
+
function compact(value) {
|
|
1900
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
|
|
1901
|
+
}
|
|
1902
|
+
var COLOR_SCHEMA = {
|
|
1903
|
+
enum: COLOR_NAMES,
|
|
1904
|
+
description: "Named color preset: outline plus, for boxes, a matching fill. `plain` has no outline and a light fill, `black` is a dark card with light text, `gray` is a dark outline on transparent (the default); the rest pair a colored outline with its pastel fill."
|
|
1905
|
+
};
|
|
1906
|
+
var END_SCHEMA = {
|
|
1907
|
+
oneOf: [
|
|
1908
|
+
{
|
|
1909
|
+
type: "string",
|
|
1910
|
+
description: "Id of a box. The connector attaches to its outline, aimed at the other end"
|
|
1911
|
+
},
|
|
1912
|
+
{
|
|
1913
|
+
type: "object",
|
|
1914
|
+
required: ["id"],
|
|
1915
|
+
additionalProperties: false,
|
|
1916
|
+
properties: {
|
|
1917
|
+
id: { type: "string", description: "Id of a box" },
|
|
1918
|
+
side: {
|
|
1919
|
+
enum: ["top", "right", "bottom", "left"],
|
|
1920
|
+
description: "Pin the endpoint to the middle of this side of the box instead of auto-aiming"
|
|
1921
|
+
}
|
|
2251
1922
|
}
|
|
2252
|
-
}
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
1923
|
+
}
|
|
1924
|
+
],
|
|
1925
|
+
description: "Box a connector end attaches to: a bare box id, or {id, side} to pick the attach edge"
|
|
1926
|
+
};
|
|
1927
|
+
var DRAWING_SKELETON_JSON_SCHEMA = {
|
|
1928
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1929
|
+
title: "DrawingSkeleton",
|
|
1930
|
+
description: "Compact intent description of a diagram. Boxes are sized from their text and laid out automatically along the connector flow; connectors are drawn between box outlines. Only say what is on the canvas \u2014 geometry is derived",
|
|
1931
|
+
type: "object",
|
|
1932
|
+
required: ["boxes"],
|
|
1933
|
+
additionalProperties: false,
|
|
1934
|
+
properties: {
|
|
1935
|
+
direction: {
|
|
1936
|
+
enum: ["right", "down"],
|
|
1937
|
+
description: 'Auto-layout flow direction. Boxes connected a\u2192b are placed in successive ranks along this axis ("right" = columns left to right, "down" = rows top to bottom). Default "right"'
|
|
1938
|
+
},
|
|
1939
|
+
canvasWidth: {
|
|
1940
|
+
type: "number",
|
|
1941
|
+
minimum: 120,
|
|
1942
|
+
description: "Logical canvas width in px. Omit for a fluid canvas in CSS pixels"
|
|
1943
|
+
},
|
|
1944
|
+
canvasHeight: {
|
|
1945
|
+
type: "number",
|
|
1946
|
+
minimum: 80,
|
|
1947
|
+
description: "Canvas height in px. Omit to fit the content"
|
|
1948
|
+
},
|
|
1949
|
+
width: {
|
|
1950
|
+
enum: ["full", "text", "content"],
|
|
1951
|
+
description: 'Block width: "text" aligns the drawing with the text column (same as "full" unless the app sets a text measure); "content" fits the drawing to its shapes and left-aligns it with the text; omit to span the editor'
|
|
1952
|
+
},
|
|
1953
|
+
boxes: {
|
|
1954
|
+
type: "array",
|
|
1955
|
+
items: { $ref: "#/definitions/box" },
|
|
1956
|
+
description: "Card-like shapes carrying up to three text slots. Order is the render order and the stacking order within a layout rank"
|
|
1957
|
+
},
|
|
1958
|
+
connectors: {
|
|
1959
|
+
type: "array",
|
|
1960
|
+
items: { $ref: "#/definitions/connector" },
|
|
1961
|
+
description: "Arrows and lines between boxes. Connectors whose from/to id does not match a box are dropped"
|
|
1962
|
+
},
|
|
1963
|
+
texts: {
|
|
1964
|
+
type: "array",
|
|
1965
|
+
items: { $ref: "#/definitions/text" },
|
|
1966
|
+
description: "Free-floating annotations not attached to any box. Prefer box slots (label/text/footer) when the text belongs to a box"
|
|
1967
|
+
}
|
|
1968
|
+
},
|
|
1969
|
+
definitions: {
|
|
1970
|
+
box: {
|
|
1971
|
+
type: "object",
|
|
1972
|
+
required: ["id"],
|
|
1973
|
+
additionalProperties: false,
|
|
1974
|
+
properties: {
|
|
1975
|
+
id: { type: "string", description: "Unique within the drawing; connectors reference it" },
|
|
1976
|
+
type: {
|
|
1977
|
+
enum: BOX_TYPES,
|
|
1978
|
+
description: 'Shape: rect (default), ellipse, diamond (decision), note (sticky note, yellow by default), cylinder (database), cloud (external system), queue (message queue), actor (stick figure; only the "text" slot renders, below the figure)'
|
|
1979
|
+
},
|
|
1980
|
+
label: { type: "string", description: "Small bold heading at the top of the box" },
|
|
1981
|
+
text: {
|
|
1982
|
+
type: "string",
|
|
1983
|
+
description: 'Main content, centered. Wraps automatically; "\\n" forces a line break'
|
|
1984
|
+
},
|
|
1985
|
+
footer: { type: "string", description: "Small dim line at the bottom of the box" },
|
|
1986
|
+
x: { type: "number", description: "Top-left x in px. Omit (with y) to let the layout place the box" },
|
|
1987
|
+
y: { type: "number", description: "Top-left y in px. Omit (with x) to let the layout place the box" },
|
|
1988
|
+
w: {
|
|
1989
|
+
type: "number",
|
|
1990
|
+
exclusiveMinimum: 0,
|
|
1991
|
+
description: "Width in px. Omit to size from the text (never smaller than the type default)"
|
|
1992
|
+
},
|
|
1993
|
+
h: {
|
|
1994
|
+
type: "number",
|
|
1995
|
+
exclusiveMinimum: 0,
|
|
1996
|
+
description: "Height in px. Omit to size from the text (never smaller than the type default)"
|
|
1997
|
+
},
|
|
1998
|
+
color: COLOR_SCHEMA,
|
|
1999
|
+
stroke: { type: "string", description: "CSS color of the outline and text; overrides the preset" },
|
|
2000
|
+
fill: {
|
|
2001
|
+
type: "string",
|
|
2002
|
+
description: 'CSS color of the interior ("transparent" for none); overrides the preset'
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
},
|
|
2006
|
+
connector: {
|
|
2007
|
+
type: "object",
|
|
2008
|
+
required: ["from", "to"],
|
|
2009
|
+
additionalProperties: false,
|
|
2010
|
+
properties: {
|
|
2011
|
+
id: { type: "string", description: "Optional; generated when omitted" },
|
|
2012
|
+
type: {
|
|
2013
|
+
enum: CONNECTOR_TYPES,
|
|
2014
|
+
description: 'arrow (default; arrowhead at "to") or line (no arrowheads)'
|
|
2015
|
+
},
|
|
2016
|
+
from: END_SCHEMA,
|
|
2017
|
+
to: END_SCHEMA,
|
|
2018
|
+
text: { type: "string", description: "Label rendered at the middle of the connector" },
|
|
2019
|
+
bidirectional: {
|
|
2020
|
+
type: "boolean",
|
|
2021
|
+
description: "Arrow only: arrowheads on both ends"
|
|
2022
|
+
},
|
|
2023
|
+
routing: {
|
|
2024
|
+
enum: ["straight", "elbow"],
|
|
2025
|
+
description: "straight (default) draws a direct line; elbow draws a right-angled path routed around the boxes"
|
|
2026
|
+
},
|
|
2027
|
+
color: COLOR_SCHEMA,
|
|
2028
|
+
stroke: { type: "string", description: "CSS color of the line and label; overrides the preset" }
|
|
2029
|
+
}
|
|
2030
|
+
},
|
|
2031
|
+
text: {
|
|
2032
|
+
type: "object",
|
|
2033
|
+
required: ["x", "y", "text"],
|
|
2034
|
+
additionalProperties: false,
|
|
2035
|
+
properties: {
|
|
2036
|
+
id: { type: "string", description: "Optional; generated when omitted" },
|
|
2037
|
+
x: { type: "number", description: "Top-left x of the first line, in px" },
|
|
2038
|
+
y: { type: "number", description: "Top-left y of the first line, in px" },
|
|
2039
|
+
text: { type: "string", description: 'Content; "\\n" forces a line break' },
|
|
2040
|
+
color: COLOR_SCHEMA,
|
|
2041
|
+
stroke: { type: "string", description: "CSS color of the text; overrides the preset" }
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2319
2044
|
}
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2045
|
+
};
|
|
2046
|
+
|
|
2047
|
+
// src/drawing/canvas/PropertyBar.tsx
|
|
2048
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
2049
|
+
var COLOR_NAMES2 = Object.keys(COLOR_PRESETS);
|
|
2050
|
+
function Swatch({
|
|
2051
|
+
name,
|
|
2052
|
+
active,
|
|
2053
|
+
onClick
|
|
2054
|
+
}) {
|
|
2055
|
+
const preset = COLOR_PRESETS[name];
|
|
2056
|
+
return /* @__PURE__ */ jsx2(
|
|
2057
|
+
"button",
|
|
2323
2058
|
{
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
onBlur: () => onCommit(shape.id, field, value),
|
|
2332
|
-
onPointerDown: (e) => e.stopPropagation()
|
|
2059
|
+
type: "button",
|
|
2060
|
+
title: `Color: ${name}`,
|
|
2061
|
+
"aria-label": `Color ${name}`,
|
|
2062
|
+
"aria-pressed": active,
|
|
2063
|
+
className: `zui-drawing-swatch ${active ? "is-active" : ""}`,
|
|
2064
|
+
style: { backgroundColor: preset.fill, borderColor: preset.stroke },
|
|
2065
|
+
onClick
|
|
2333
2066
|
}
|
|
2334
2067
|
);
|
|
2335
2068
|
}
|
|
2336
|
-
|
|
2337
|
-
// src/drawing/canvas/Toolbar.tsx
|
|
2338
|
-
import { useEffect as useEffect6, useRef as useRef3, useState as useState2 } from "react";
|
|
2339
|
-
|
|
2340
|
-
// src/components/blockWidthOptions.tsx
|
|
2341
|
-
import { Fragment, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2342
|
-
var BLOCK_WIDTH_OPTIONS = [
|
|
2343
|
-
{
|
|
2344
|
-
width: "full",
|
|
2345
|
-
label: "Full width",
|
|
2346
|
-
icon: /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
2347
|
-
/* @__PURE__ */ jsx7("path", { d: "M3 4v12M17 4v12" }),
|
|
2348
|
-
/* @__PURE__ */ jsx7("path", { d: "M6 10h8M8.5 7.5L6 10l2.5 2.5M11.5 7.5L14 10l-2.5 2.5" })
|
|
2349
|
-
] })
|
|
2350
|
-
},
|
|
2351
|
-
{
|
|
2352
|
-
width: "text",
|
|
2353
|
-
label: "Text width",
|
|
2354
|
-
icon: /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
2355
|
-
/* @__PURE__ */ jsx7("path", { d: "M3 4v12M17 4v12" }),
|
|
2356
|
-
/* @__PURE__ */ jsx7("path", { d: "M6.5 7h7M6.5 10h7M6.5 13h4.5" })
|
|
2357
|
-
] })
|
|
2358
|
-
},
|
|
2359
|
-
{
|
|
2360
|
-
width: "content",
|
|
2361
|
-
label: "Content width",
|
|
2362
|
-
icon: /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
2363
|
-
/* @__PURE__ */ jsx7("path", { d: "M3 4v12M17 4v12" }),
|
|
2364
|
-
/* @__PURE__ */ jsx7("path", { d: "M5.5 10h3M11.5 10h3M6.5 7.5L9 10l-2.5 2.5M13.5 7.5L11 10l2.5 2.5" })
|
|
2365
|
-
] })
|
|
2366
|
-
}
|
|
2367
|
-
];
|
|
2368
|
-
|
|
2369
|
-
// src/drawing/canvas/Toolbar.tsx
|
|
2370
|
-
import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2371
|
-
var PRIMARY_TOOLS = [
|
|
2372
|
-
{ tool: "select", label: "Select" },
|
|
2373
|
-
{ tool: "rect", label: "Rectangle" },
|
|
2374
|
-
{ tool: "ellipse", label: "Ellipse" },
|
|
2375
|
-
{ tool: "diamond", label: "Diamond" },
|
|
2376
|
-
{ tool: "arrow", label: "Arrow" },
|
|
2377
|
-
{ tool: "line", label: "Line" },
|
|
2378
|
-
{ tool: "text", label: "Text" }
|
|
2379
|
-
];
|
|
2380
|
-
var MORE_SHAPES = ["note", "cylinder", "cloud", "queue", "actor"];
|
|
2381
|
-
function ToolButton({
|
|
2069
|
+
function OptionButton({
|
|
2382
2070
|
label,
|
|
2383
2071
|
active,
|
|
2384
2072
|
onClick,
|
|
2385
2073
|
children,
|
|
2386
|
-
className
|
|
2387
|
-
pressed
|
|
2074
|
+
className
|
|
2388
2075
|
}) {
|
|
2389
|
-
return /* @__PURE__ */
|
|
2076
|
+
return /* @__PURE__ */ jsx2(
|
|
2390
2077
|
"button",
|
|
2391
2078
|
{
|
|
2392
2079
|
type: "button",
|
|
2393
2080
|
title: label,
|
|
2394
2081
|
"aria-label": label,
|
|
2395
|
-
"aria-pressed":
|
|
2082
|
+
"aria-pressed": active,
|
|
2396
2083
|
className: `zui-drawing-tool ${active ? "is-active" : ""} ${className ?? ""}`,
|
|
2397
2084
|
onClick,
|
|
2398
|
-
children: /* @__PURE__ */
|
|
2085
|
+
children: /* @__PURE__ */ jsx2(Icon, { children })
|
|
2399
2086
|
}
|
|
2400
2087
|
);
|
|
2401
2088
|
}
|
|
2402
|
-
function
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
const
|
|
2412
|
-
const
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
}
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
return /* @__PURE__ */ jsxs7(Fragment2, { children: [
|
|
2431
|
-
/* @__PURE__ */ jsxs7("div", { className: "zui-drawing-toolbar-group", role: "toolbar", "aria-label": "Drawing tools", children: [
|
|
2432
|
-
PRIMARY_TOOLS.map(({ tool: t, label }) => /* @__PURE__ */ jsx8(ToolButton, { label, active: tool === t, onClick: () => onToolChange(t), children: SHAPE_ICONS[t] }, t)),
|
|
2433
|
-
/* @__PURE__ */ jsxs7("div", { className: "zui-drawing-more", ref: moreRef, children: [
|
|
2434
|
-
/* @__PURE__ */ jsxs7(
|
|
2435
|
-
"button",
|
|
2436
|
-
{
|
|
2437
|
-
type: "button",
|
|
2438
|
-
title: "More shapes",
|
|
2439
|
-
"aria-label": "More shapes",
|
|
2440
|
-
"aria-haspopup": "menu",
|
|
2441
|
-
"aria-expanded": moreOpen,
|
|
2442
|
-
className: `zui-drawing-tool zui-drawing-tool-more ${moreActive ? "is-active" : ""}`,
|
|
2443
|
-
onClick: () => setMoreOpen((open) => !open),
|
|
2444
|
-
children: [
|
|
2445
|
-
/* @__PURE__ */ jsx8(Icon, { children: moreIcon }),
|
|
2446
|
-
/* @__PURE__ */ jsx8("span", { className: "zui-drawing-caret", "aria-hidden": "true" })
|
|
2447
|
-
]
|
|
2448
|
-
}
|
|
2449
|
-
),
|
|
2450
|
-
moreOpen && /* @__PURE__ */ jsx8("div", { className: "zui-drawing-popover", role: "menu", children: MORE_SHAPES.map((type) => /* @__PURE__ */ jsxs7(
|
|
2451
|
-
"button",
|
|
2452
|
-
{
|
|
2453
|
-
type: "button",
|
|
2454
|
-
role: "menuitemradio",
|
|
2455
|
-
"aria-checked": tool === type,
|
|
2456
|
-
className: `zui-drawing-popover-item ${tool === type ? "is-active" : ""}`,
|
|
2457
|
-
onClick: () => {
|
|
2458
|
-
setLastMore(type);
|
|
2459
|
-
onToolChange(type);
|
|
2460
|
-
setMoreOpen(false);
|
|
2461
|
-
},
|
|
2462
|
-
children: [
|
|
2463
|
-
/* @__PURE__ */ jsx8(Icon, { size: 18, children: SHAPE_ICONS[type] }),
|
|
2464
|
-
/* @__PURE__ */ jsx8("span", { children: BOX_DEFINITIONS[type].label })
|
|
2465
|
-
]
|
|
2466
|
-
},
|
|
2467
|
-
type
|
|
2468
|
-
)) })
|
|
2469
|
-
] })
|
|
2089
|
+
function PropertyBar({
|
|
2090
|
+
selection,
|
|
2091
|
+
stroke,
|
|
2092
|
+
fill,
|
|
2093
|
+
onColor,
|
|
2094
|
+
onRouting,
|
|
2095
|
+
onDirection,
|
|
2096
|
+
onDelete
|
|
2097
|
+
}) {
|
|
2098
|
+
const connectors = selection.filter((s) => isConnectorType(s.type));
|
|
2099
|
+
const arrows = selection.filter((s) => s.type === "arrow");
|
|
2100
|
+
const allElbow = connectors.length > 0 && connectors.every((s) => s.routing === "elbow");
|
|
2101
|
+
const allStraight = connectors.length > 0 && connectors.every((s) => s.routing !== "elbow");
|
|
2102
|
+
const allTwoWay = arrows.length > 0 && arrows.every((s) => s.bidirectional);
|
|
2103
|
+
const allOneWay = arrows.length > 0 && arrows.every((s) => !s.bidirectional);
|
|
2104
|
+
return /* @__PURE__ */ jsxs2("div", { className: "zui-drawing-props", onPointerDown: (e) => e.stopPropagation(), children: [
|
|
2105
|
+
/* @__PURE__ */ jsx2("div", { className: "zui-drawing-props-group", role: "radiogroup", "aria-label": "Color", children: COLOR_NAMES2.map((name) => /* @__PURE__ */ jsx2(
|
|
2106
|
+
Swatch,
|
|
2107
|
+
{
|
|
2108
|
+
name,
|
|
2109
|
+
active: COLOR_PRESETS[name].stroke === stroke && COLOR_PRESETS[name].fill === fill,
|
|
2110
|
+
onClick: () => onColor(name)
|
|
2111
|
+
},
|
|
2112
|
+
name
|
|
2113
|
+
)) }),
|
|
2114
|
+
connectors.length > 0 && /* @__PURE__ */ jsxs2("div", { className: "zui-drawing-props-group", "aria-label": "Connector routing", children: [
|
|
2115
|
+
/* @__PURE__ */ jsx2(OptionButton, { label: "Straight connector", active: allStraight, onClick: () => onRouting(false), children: UI_ICONS.straight }),
|
|
2116
|
+
/* @__PURE__ */ jsx2(OptionButton, { label: "Elbow connector (auto-routed)", active: allElbow, onClick: () => onRouting(true), children: UI_ICONS.elbow })
|
|
2470
2117
|
] }),
|
|
2471
|
-
/* @__PURE__ */
|
|
2472
|
-
/* @__PURE__ */
|
|
2473
|
-
|
|
2118
|
+
arrows.length > 0 && /* @__PURE__ */ jsxs2("div", { className: "zui-drawing-props-group", "aria-label": "Arrow direction", children: [
|
|
2119
|
+
/* @__PURE__ */ jsx2(OptionButton, { label: "One-way arrow", active: allOneWay, onClick: () => onDirection(false), children: UI_ICONS.oneWay }),
|
|
2120
|
+
/* @__PURE__ */ jsx2(OptionButton, { label: "Two-way arrow", active: allTwoWay, onClick: () => onDirection(true), children: UI_ICONS.twoWay })
|
|
2121
|
+
] }),
|
|
2122
|
+
selection.length > 0 && /* @__PURE__ */ jsx2("div", { className: "zui-drawing-props-group", children: /* @__PURE__ */ jsx2(
|
|
2123
|
+
OptionButton,
|
|
2124
|
+
{
|
|
2125
|
+
label: selection.length > 1 ? `Delete ${selection.length} shapes` : "Delete shape",
|
|
2126
|
+
className: "zui-drawing-tool-danger",
|
|
2127
|
+
onClick: onDelete,
|
|
2128
|
+
children: UI_ICONS.trash
|
|
2129
|
+
}
|
|
2130
|
+
) })
|
|
2131
|
+
] });
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
// src/drawing/canvas/SelectionOverlay.tsx
|
|
2135
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
2136
|
+
var HANDLE = 7;
|
|
2137
|
+
function Handle({
|
|
2138
|
+
x,
|
|
2139
|
+
y,
|
|
2140
|
+
cursor,
|
|
2141
|
+
square,
|
|
2142
|
+
onPointerDown,
|
|
2143
|
+
title
|
|
2144
|
+
}) {
|
|
2145
|
+
const props = {
|
|
2146
|
+
className: "zui-drawing-handle",
|
|
2147
|
+
strokeWidth: 1.5,
|
|
2148
|
+
style: { cursor },
|
|
2149
|
+
onPointerDown
|
|
2150
|
+
};
|
|
2151
|
+
return square ? /* @__PURE__ */ jsx3("rect", { ...props, x: x - HANDLE / 2, y: y - HANDLE / 2, width: HANDLE, height: HANDLE, rx: 2, children: title && /* @__PURE__ */ jsx3("title", { children: title }) }) : /* @__PURE__ */ jsx3("circle", { ...props, cx: x, cy: y, r: HANDLE / 2 + 0.5, children: title && /* @__PURE__ */ jsx3("title", { children: title }) });
|
|
2152
|
+
}
|
|
2153
|
+
function SelectionOverlay({
|
|
2154
|
+
shape,
|
|
2155
|
+
points,
|
|
2156
|
+
onHandlePointerDown,
|
|
2157
|
+
onWaypointAdd,
|
|
2158
|
+
onWaypointRemove
|
|
2159
|
+
}) {
|
|
2160
|
+
if (isConnectorType(shape.type)) {
|
|
2161
|
+
const waypoints = shape.waypoints ?? [];
|
|
2162
|
+
const isZ = shape.routing === "elbow" && !waypoints.length && points.length === 4;
|
|
2163
|
+
const elbowMid = isZ ? { x: (points[1].x + points[2].x) / 2, y: (points[1].y + points[2].y) / 2 } : null;
|
|
2164
|
+
const elbowVertical = isZ && Math.abs(points[1].x - points[2].x) < 0.01;
|
|
2165
|
+
const ghosts = points.slice(0, -1).flatMap((p, i) => {
|
|
2166
|
+
const q = points[i + 1];
|
|
2167
|
+
if (Math.hypot(q.x - p.x, q.y - p.y) < 28) return [];
|
|
2168
|
+
if (isZ && i === 1) return [];
|
|
2169
|
+
return [{ index: i, x: (p.x + q.x) / 2, y: (p.y + q.y) / 2 }];
|
|
2170
|
+
});
|
|
2171
|
+
return /* @__PURE__ */ jsxs3("g", { className: "zui-drawing-selection", children: [
|
|
2172
|
+
ghosts.map(({ index, x, y }) => /* @__PURE__ */ jsx3(
|
|
2173
|
+
"circle",
|
|
2474
2174
|
{
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2175
|
+
className: "zui-drawing-handle zui-drawing-ghost",
|
|
2176
|
+
cx: x,
|
|
2177
|
+
cy: y,
|
|
2178
|
+
r: 4.5,
|
|
2179
|
+
strokeWidth: 1.5,
|
|
2180
|
+
strokeDasharray: "2 2",
|
|
2181
|
+
style: { cursor: "copy" },
|
|
2182
|
+
onPointerDown: (e) => onWaypointAdd(e, shape.id, index, { x, y }),
|
|
2183
|
+
children: /* @__PURE__ */ jsx3("title", { children: "Drag to add a bend" })
|
|
2184
|
+
},
|
|
2185
|
+
`ghost-${index}`
|
|
2186
|
+
)),
|
|
2187
|
+
waypoints.map((p, i) => /* @__PURE__ */ jsx3(
|
|
2188
|
+
"g",
|
|
2189
|
+
{
|
|
2190
|
+
onDoubleClick: (e) => {
|
|
2191
|
+
e.stopPropagation();
|
|
2192
|
+
onWaypointRemove(shape.id, i);
|
|
2483
2193
|
},
|
|
2484
|
-
children:
|
|
2194
|
+
children: /* @__PURE__ */ jsx3(
|
|
2195
|
+
Handle,
|
|
2196
|
+
{
|
|
2197
|
+
x: p.x,
|
|
2198
|
+
y: p.y,
|
|
2199
|
+
square: true,
|
|
2200
|
+
cursor: "move",
|
|
2201
|
+
title: "Drag to move, double-click to remove",
|
|
2202
|
+
onPointerDown: (e) => onHandlePointerDown(e, { mode: "waypoint", id: shape.id, index: i })
|
|
2203
|
+
}
|
|
2204
|
+
)
|
|
2205
|
+
},
|
|
2206
|
+
`wp-${i}`
|
|
2207
|
+
)),
|
|
2208
|
+
elbowMid && /* @__PURE__ */ jsx3(
|
|
2209
|
+
Handle,
|
|
2210
|
+
{
|
|
2211
|
+
x: elbowMid.x,
|
|
2212
|
+
y: elbowMid.y,
|
|
2213
|
+
square: true,
|
|
2214
|
+
cursor: elbowVertical ? "ew-resize" : "ns-resize",
|
|
2215
|
+
title: "Slide the middle segment",
|
|
2216
|
+
onPointerDown: (e) => onHandlePointerDown(e, {
|
|
2217
|
+
mode: "elbow",
|
|
2218
|
+
id: shape.id,
|
|
2219
|
+
a: points[0],
|
|
2220
|
+
d: points[3],
|
|
2221
|
+
vertical: elbowVertical
|
|
2222
|
+
})
|
|
2485
2223
|
}
|
|
2486
2224
|
),
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2225
|
+
["start", "end"].map((end) => {
|
|
2226
|
+
const p = end === "start" ? points[0] : points[points.length - 1];
|
|
2227
|
+
return /* @__PURE__ */ jsx3(
|
|
2228
|
+
Handle,
|
|
2229
|
+
{
|
|
2230
|
+
x: p.x,
|
|
2231
|
+
y: p.y,
|
|
2232
|
+
cursor: "crosshair",
|
|
2233
|
+
onPointerDown: (e) => onHandlePointerDown(e, { mode: "endpoint", id: shape.id, end, orig: shape })
|
|
2234
|
+
},
|
|
2235
|
+
end
|
|
2236
|
+
);
|
|
2237
|
+
})
|
|
2238
|
+
] });
|
|
2239
|
+
}
|
|
2240
|
+
const b = bbox(shape);
|
|
2241
|
+
const pad = 5;
|
|
2242
|
+
const corners = [
|
|
2243
|
+
{ corner: "nw", x: b.x - pad, y: b.y - pad },
|
|
2244
|
+
{ corner: "ne", x: b.x + b.w + pad, y: b.y - pad },
|
|
2245
|
+
{ corner: "sw", x: b.x - pad, y: b.y + b.h + pad },
|
|
2246
|
+
{ corner: "se", x: b.x + b.w + pad, y: b.y + b.h + pad }
|
|
2247
|
+
];
|
|
2248
|
+
const resizable = shape.type !== "text";
|
|
2249
|
+
return /* @__PURE__ */ jsxs3("g", { className: "zui-drawing-selection", children: [
|
|
2250
|
+
/* @__PURE__ */ jsx3(SelectionFrame, { rect: b, pad }),
|
|
2251
|
+
resizable && corners.map(({ corner, x, y }) => /* @__PURE__ */ jsx3(
|
|
2252
|
+
Handle,
|
|
2253
|
+
{
|
|
2254
|
+
x,
|
|
2255
|
+
y,
|
|
2256
|
+
square: true,
|
|
2257
|
+
cursor: corner === "nw" || corner === "se" ? "nwse-resize" : "nesw-resize",
|
|
2258
|
+
onPointerDown: (e) => onHandlePointerDown(e, { mode: "resize", id: shape.id, corner, orig: shape })
|
|
2259
|
+
},
|
|
2260
|
+
corner
|
|
2261
|
+
))
|
|
2499
2262
|
] });
|
|
2500
2263
|
}
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
let right = 0;
|
|
2516
|
-
for (const shape of shapes) {
|
|
2517
|
-
if (isConnectorType(shape.type)) {
|
|
2518
|
-
for (const p of paths.get(shape.id) ?? []) right = Math.max(right, p.x);
|
|
2519
|
-
} else {
|
|
2520
|
-
const b = bbox(shape);
|
|
2521
|
-
right = Math.max(right, b.x + b.w);
|
|
2264
|
+
function SelectionFrame({ rect, pad }) {
|
|
2265
|
+
return /* @__PURE__ */ jsx3(
|
|
2266
|
+
"rect",
|
|
2267
|
+
{
|
|
2268
|
+
x: rect.x - pad,
|
|
2269
|
+
y: rect.y - pad,
|
|
2270
|
+
width: rect.w + pad * 2,
|
|
2271
|
+
height: rect.h + pad * 2,
|
|
2272
|
+
rx: 6,
|
|
2273
|
+
fill: "none",
|
|
2274
|
+
stroke: "currentColor",
|
|
2275
|
+
strokeWidth: 1,
|
|
2276
|
+
strokeDasharray: "5 4",
|
|
2277
|
+
pointerEvents: "none"
|
|
2522
2278
|
}
|
|
2523
|
-
|
|
2524
|
-
return Math.max(MIN_CONTENT_WIDTH, Math.ceil(right + CONTENT_WIDTH_PADDING));
|
|
2279
|
+
);
|
|
2525
2280
|
}
|
|
2526
|
-
function
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2281
|
+
function GroupSelectionOverlay({
|
|
2282
|
+
shapes,
|
|
2283
|
+
paths
|
|
2284
|
+
}) {
|
|
2285
|
+
if (!shapes.length) return null;
|
|
2286
|
+
const rects = shapes.map((s) => {
|
|
2287
|
+
if (!isConnectorType(s.type)) return bbox(s);
|
|
2288
|
+
const pts = paths.get(s.id) ?? [];
|
|
2289
|
+
const xs = pts.map((p) => p.x);
|
|
2290
|
+
const ys = pts.map((p) => p.y);
|
|
2291
|
+
const x = Math.min(...xs);
|
|
2292
|
+
const y = Math.min(...ys);
|
|
2293
|
+
return { x, y, w: Math.max(...xs) - x, h: Math.max(...ys) - y };
|
|
2294
|
+
});
|
|
2295
|
+
return /* @__PURE__ */ jsxs3("g", { className: "zui-drawing-selection", pointerEvents: "none", children: [
|
|
2296
|
+
rects.map((r, i) => /* @__PURE__ */ jsx3(
|
|
2297
|
+
"rect",
|
|
2298
|
+
{
|
|
2299
|
+
x: r.x - 3,
|
|
2300
|
+
y: r.y - 3,
|
|
2301
|
+
width: r.w + 6,
|
|
2302
|
+
height: r.h + 6,
|
|
2303
|
+
rx: 4,
|
|
2304
|
+
fill: "none",
|
|
2305
|
+
stroke: "currentColor",
|
|
2306
|
+
strokeWidth: 1,
|
|
2307
|
+
opacity: 0.45
|
|
2308
|
+
},
|
|
2309
|
+
shapes[i].id
|
|
2310
|
+
)),
|
|
2311
|
+
/* @__PURE__ */ jsx3(SelectionFrame, { rect: unionRects(rects), pad: 8 })
|
|
2312
|
+
] });
|
|
2532
2313
|
}
|
|
2533
|
-
function
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
const svgRef = useRef4(null);
|
|
2549
|
-
const dragRef = useRef4(null);
|
|
2550
|
-
const pendingTextEditRef = useRef4(null);
|
|
2551
|
-
const pendingPointRef = useRef4(null);
|
|
2552
|
-
const frameRef = useRef4(null);
|
|
2553
|
-
const lastCommittedRef = useRef4(serializeDrawingData(data));
|
|
2554
|
-
const shapesRef = useRef4(shapes);
|
|
2555
|
-
shapesRef.current = shapes;
|
|
2556
|
-
const heightRef = useRef4(canvasHeight);
|
|
2557
|
-
heightRef.current = canvasHeight;
|
|
2558
|
-
const widthRef = useRef4(width);
|
|
2559
|
-
widthRef.current = width;
|
|
2560
|
-
const selectedRef = useRef4(selectedIds);
|
|
2561
|
-
selectedRef.current = selectedIds;
|
|
2562
|
-
const paths = useMemo(() => computePaths(shapes), [shapes]);
|
|
2563
|
-
const canvasWidth = data.canvasWidth;
|
|
2564
|
-
const logicalWidth = canvasWidth ?? (width === "content" ? contentWidth(shapes, paths) : null);
|
|
2565
|
-
useEffect7(() => {
|
|
2566
|
-
const incoming = serializeDrawingData(data);
|
|
2567
|
-
if (incoming !== lastCommittedRef.current) {
|
|
2568
|
-
lastCommittedRef.current = incoming;
|
|
2569
|
-
setShapes(data.shapes);
|
|
2570
|
-
setCanvasHeight(data.canvasHeight);
|
|
2571
|
-
setWidth(data.width ?? "full");
|
|
2572
|
-
setSelectedIds(EMPTY_SET);
|
|
2573
|
-
setEditingText(null);
|
|
2574
|
-
}
|
|
2575
|
-
}, [data]);
|
|
2576
|
-
useEffect7(() => {
|
|
2577
|
-
const svg = svgRef.current;
|
|
2578
|
-
if (!logicalWidth || !svg || typeof ResizeObserver === "undefined") {
|
|
2579
|
-
setScale(1);
|
|
2580
|
-
return;
|
|
2581
|
-
}
|
|
2582
|
-
const update = () => {
|
|
2583
|
-
const rect = svg.getBoundingClientRect();
|
|
2584
|
-
setScale(rect.width > 0 ? rect.width / logicalWidth : 1);
|
|
2585
|
-
};
|
|
2586
|
-
update();
|
|
2587
|
-
const observer = new ResizeObserver(update);
|
|
2588
|
-
observer.observe(svg);
|
|
2589
|
-
return () => observer.disconnect();
|
|
2590
|
-
}, [logicalWidth]);
|
|
2591
|
-
const commit = useCallback(
|
|
2592
|
-
(nextShapes, options) => {
|
|
2593
|
-
const nextWidth = options?.width ?? widthRef.current;
|
|
2594
|
-
const explicitFull = options?.width === void 0 && data.width !== void 0;
|
|
2595
|
-
const payload = {
|
|
2596
|
-
version: 2,
|
|
2597
|
-
...canvasWidth ? { canvasWidth } : {},
|
|
2598
|
-
canvasHeight: options?.height ?? heightRef.current,
|
|
2599
|
-
...nextWidth !== "full" || explicitFull ? { width: nextWidth } : {},
|
|
2600
|
-
shapes: nextShapes
|
|
2601
|
-
};
|
|
2602
|
-
const json = serializeDrawingData(payload);
|
|
2603
|
-
if (json === lastCommittedRef.current) return;
|
|
2604
|
-
lastCommittedRef.current = json;
|
|
2605
|
-
editor.update(() => {
|
|
2606
|
-
const node = $getNodeByKey2(nodeKey);
|
|
2607
|
-
if ($isDrawingNode(node)) node.setData(payload);
|
|
2608
|
-
});
|
|
2609
|
-
},
|
|
2610
|
-
[editor, nodeKey, canvasWidth, data.width]
|
|
2611
|
-
);
|
|
2612
|
-
const updateShapes = useCallback(
|
|
2613
|
-
(updater, options) => {
|
|
2614
|
-
setShapes((prev) => {
|
|
2615
|
-
const next = resolveBindings(updater(prev));
|
|
2616
|
-
if (options?.commit) commit(next);
|
|
2617
|
-
return next;
|
|
2618
|
-
});
|
|
2619
|
-
},
|
|
2620
|
-
[commit]
|
|
2621
|
-
);
|
|
2622
|
-
const getPoint = useCallback((e) => {
|
|
2623
|
-
const svg = svgRef.current;
|
|
2624
|
-
if (!svg) return { x: 0, y: 0 };
|
|
2625
|
-
const ctm = svg.getScreenCTM();
|
|
2626
|
-
if (!ctm) {
|
|
2627
|
-
const rect = svg.getBoundingClientRect();
|
|
2628
|
-
return { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
|
2314
|
+
function MarqueeOverlay({ rect }) {
|
|
2315
|
+
return /* @__PURE__ */ jsx3(
|
|
2316
|
+
"rect",
|
|
2317
|
+
{
|
|
2318
|
+
className: "zui-drawing-selection",
|
|
2319
|
+
x: rect.x,
|
|
2320
|
+
y: rect.y,
|
|
2321
|
+
width: rect.w,
|
|
2322
|
+
height: rect.h,
|
|
2323
|
+
rx: 3,
|
|
2324
|
+
fill: "currentColor",
|
|
2325
|
+
fillOpacity: 0.08,
|
|
2326
|
+
stroke: "currentColor",
|
|
2327
|
+
strokeWidth: 1,
|
|
2328
|
+
pointerEvents: "none"
|
|
2629
2329
|
}
|
|
2630
|
-
const inverse = ctm.inverse();
|
|
2631
|
-
return {
|
|
2632
|
-
x: inverse.a * e.clientX + inverse.c * e.clientY + inverse.e,
|
|
2633
|
-
y: inverse.b * e.clientX + inverse.d * e.clientY + inverse.f
|
|
2634
|
-
};
|
|
2635
|
-
}, []);
|
|
2636
|
-
const selection = useMemo(
|
|
2637
|
-
() => shapes.filter((s) => selectedIds.has(s.id)),
|
|
2638
|
-
[shapes, selectedIds]
|
|
2639
2330
|
);
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
// src/drawing/shapes/render.tsx
|
|
2334
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2335
|
+
var n = (v) => Number.isInteger(v) ? String(v) : v.toFixed(2);
|
|
2336
|
+
function BoxGeometry({
|
|
2337
|
+
shape,
|
|
2338
|
+
stroke
|
|
2339
|
+
}) {
|
|
2340
|
+
const b = bbox(shape);
|
|
2341
|
+
const fill = shape.fill;
|
|
2342
|
+
switch (shape.type) {
|
|
2343
|
+
case "rect":
|
|
2344
|
+
return /* @__PURE__ */ jsx4(
|
|
2345
|
+
"rect",
|
|
2346
|
+
{
|
|
2347
|
+
...stroke,
|
|
2348
|
+
x: b.x,
|
|
2349
|
+
y: b.y,
|
|
2350
|
+
width: b.w,
|
|
2351
|
+
height: b.h,
|
|
2352
|
+
rx: Math.min(8, b.w / 4, b.h / 4),
|
|
2353
|
+
fill
|
|
2354
|
+
}
|
|
2355
|
+
);
|
|
2356
|
+
case "ellipse":
|
|
2357
|
+
return /* @__PURE__ */ jsx4(
|
|
2358
|
+
"ellipse",
|
|
2359
|
+
{
|
|
2360
|
+
...stroke,
|
|
2361
|
+
cx: b.x + b.w / 2,
|
|
2362
|
+
cy: b.y + b.h / 2,
|
|
2363
|
+
rx: b.w / 2,
|
|
2364
|
+
ry: b.h / 2,
|
|
2365
|
+
fill
|
|
2366
|
+
}
|
|
2367
|
+
);
|
|
2368
|
+
case "diamond":
|
|
2369
|
+
return /* @__PURE__ */ jsx4(
|
|
2370
|
+
"polygon",
|
|
2371
|
+
{
|
|
2372
|
+
...stroke,
|
|
2373
|
+
points: `${n(b.x + b.w / 2)},${n(b.y)} ${n(b.x + b.w)},${n(b.y + b.h / 2)} ${n(b.x + b.w / 2)},${n(b.y + b.h)} ${n(b.x)},${n(b.y + b.h / 2)}`,
|
|
2374
|
+
fill
|
|
2375
|
+
}
|
|
2376
|
+
);
|
|
2377
|
+
case "note": {
|
|
2378
|
+
const f = NOTE_FOLD(b);
|
|
2379
|
+
const r = b.x + b.w;
|
|
2380
|
+
const btm = b.y + b.h;
|
|
2381
|
+
return /* @__PURE__ */ jsxs4("g", { children: [
|
|
2382
|
+
/* @__PURE__ */ jsx4(
|
|
2383
|
+
"path",
|
|
2663
2384
|
{
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
y: point.y - FONT_SIZE / 2,
|
|
2668
|
-
w: size.w,
|
|
2669
|
-
h: size.h,
|
|
2670
|
-
stroke,
|
|
2671
|
-
fill: "transparent",
|
|
2672
|
-
strokeWidth: 2,
|
|
2673
|
-
text: ""
|
|
2385
|
+
...stroke,
|
|
2386
|
+
d: `M ${n(b.x)} ${n(b.y)} H ${n(r)} V ${n(btm - f)} L ${n(r - f)} ${n(btm)} H ${n(b.x)} Z`,
|
|
2387
|
+
fill
|
|
2674
2388
|
}
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
setSelectedIds(EMPTY_SET);
|
|
2686
|
-
updateShapes((prev) => [
|
|
2687
|
-
...prev,
|
|
2688
|
-
{ id, type: tool, x: point.x, y: point.y, w: 0, h: 0, stroke, fill: shapeFill, strokeWidth: 2 }
|
|
2689
|
-
]);
|
|
2690
|
-
},
|
|
2691
|
-
[isEditable, editingText, getPoint, tool, stroke, fill, updateShapes, startTextEditing]
|
|
2692
|
-
);
|
|
2693
|
-
const handleShapePointerDown = useCallback(
|
|
2694
|
-
(e, shape) => {
|
|
2695
|
-
if (!isEditable || tool !== "select" || editingText || e.button !== 0) return;
|
|
2696
|
-
e.stopPropagation();
|
|
2697
|
-
const point = getPoint(e);
|
|
2698
|
-
capture(e);
|
|
2699
|
-
const selected = selectedRef.current;
|
|
2700
|
-
if (e.shiftKey) {
|
|
2701
|
-
if (selected.has(shape.id)) {
|
|
2702
|
-
const next2 = new Set(selected);
|
|
2703
|
-
next2.delete(shape.id);
|
|
2704
|
-
setSelectedIds(next2);
|
|
2705
|
-
return;
|
|
2706
|
-
}
|
|
2707
|
-
const next = new Set(selected).add(shape.id);
|
|
2708
|
-
setSelectedIds(next);
|
|
2709
|
-
dragRef.current = {
|
|
2710
|
-
mode: "move",
|
|
2711
|
-
clickedId: shape.id,
|
|
2712
|
-
origin: point,
|
|
2713
|
-
origs: moveGroup(shapesRef.current, next, shape.id),
|
|
2714
|
-
wasSelected: false
|
|
2715
|
-
};
|
|
2716
|
-
return;
|
|
2717
|
-
}
|
|
2718
|
-
const wasSelected = selected.size === 1 && selected.has(shape.id);
|
|
2719
|
-
const group = selected.has(shape.id) ? selected : /* @__PURE__ */ new Set([shape.id]);
|
|
2720
|
-
if (!selected.has(shape.id)) setSelectedIds(group);
|
|
2721
|
-
dragRef.current = {
|
|
2722
|
-
mode: "move",
|
|
2723
|
-
clickedId: shape.id,
|
|
2724
|
-
origin: point,
|
|
2725
|
-
origs: moveGroup(shapesRef.current, group, shape.id),
|
|
2726
|
-
wasSelected
|
|
2727
|
-
};
|
|
2728
|
-
},
|
|
2729
|
-
[isEditable, tool, editingText, getPoint]
|
|
2730
|
-
);
|
|
2731
|
-
const handleHandlePointerDown = useCallback(
|
|
2732
|
-
(e, drag) => {
|
|
2733
|
-
if (!isEditable || e.button !== 0) return;
|
|
2734
|
-
e.stopPropagation();
|
|
2735
|
-
capture(e);
|
|
2736
|
-
dragRef.current = drag;
|
|
2737
|
-
if (drag.mode === "endpoint") {
|
|
2738
|
-
const key = drag.end === "start" ? "startBinding" : "endBinding";
|
|
2739
|
-
updateShapes(
|
|
2740
|
-
(prev) => prev.map((s) => s.id === drag.id ? { ...s, [key]: void 0 } : s)
|
|
2741
|
-
);
|
|
2742
|
-
}
|
|
2743
|
-
},
|
|
2744
|
-
[isEditable, updateShapes]
|
|
2745
|
-
);
|
|
2746
|
-
const flushPointer = useCallback(() => {
|
|
2747
|
-
frameRef.current = null;
|
|
2748
|
-
const point = pendingPointRef.current;
|
|
2749
|
-
const drag = dragRef.current;
|
|
2750
|
-
if (!point || !drag) return;
|
|
2751
|
-
if (drag.mode === "marquee") {
|
|
2752
|
-
drag.current = point;
|
|
2753
|
-
setMarquee({ origin: drag.origin, current: point });
|
|
2754
|
-
return;
|
|
2755
|
-
}
|
|
2756
|
-
updateShapes((prev) => applyDrag(prev, drag, point));
|
|
2757
|
-
if (drag.mode === "endpoint" || drag.mode === "draw" && isConnectorType(shapesRef.current.find((s) => s.id === drag.id)?.type ?? "")) {
|
|
2758
|
-
const box = findBoxAt(shapesRef.current, point, drag.id);
|
|
2759
|
-
setHoverBoxId((prev) => prev === (box?.id ?? null) ? prev : box?.id ?? null);
|
|
2389
|
+
),
|
|
2390
|
+
/* @__PURE__ */ jsx4(
|
|
2391
|
+
"path",
|
|
2392
|
+
{
|
|
2393
|
+
...stroke,
|
|
2394
|
+
d: `M ${n(r - f)} ${n(btm)} V ${n(btm - f)} H ${n(r)}`,
|
|
2395
|
+
fill: "rgba(0,0,0,0.08)"
|
|
2396
|
+
}
|
|
2397
|
+
)
|
|
2398
|
+
] });
|
|
2760
2399
|
}
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
const handlePointerUp = useCallback(
|
|
2773
|
-
(e) => {
|
|
2774
|
-
const drag = dragRef.current;
|
|
2775
|
-
dragRef.current = null;
|
|
2776
|
-
if (frameRef.current !== null) {
|
|
2777
|
-
cancelAnimationFrame(frameRef.current);
|
|
2778
|
-
frameRef.current = null;
|
|
2779
|
-
}
|
|
2780
|
-
pendingPointRef.current = null;
|
|
2781
|
-
setHoverBoxId(null);
|
|
2782
|
-
if (pendingTextEditRef.current) {
|
|
2783
|
-
const id = pendingTextEditRef.current;
|
|
2784
|
-
pendingTextEditRef.current = null;
|
|
2785
|
-
startTextEditing(id, "text");
|
|
2786
|
-
return;
|
|
2787
|
-
}
|
|
2788
|
-
if (!drag) return;
|
|
2789
|
-
const point = getPoint(e);
|
|
2790
|
-
if (drag.mode === "marquee") {
|
|
2791
|
-
setMarquee(null);
|
|
2792
|
-
const rect = marqueeRect(drag.origin, point);
|
|
2793
|
-
if (rect.w < CLICK_TOLERANCE && rect.h < CLICK_TOLERANCE) return;
|
|
2794
|
-
const ids = shapesWithin(shapesRef.current, rect, paths);
|
|
2795
|
-
setSelectedIds((prev) => drag.additive ? /* @__PURE__ */ new Set([...prev, ...ids]) : new Set(ids));
|
|
2796
|
-
return;
|
|
2797
|
-
}
|
|
2798
|
-
if (drag.mode === "move" && drag.wasSelected) {
|
|
2799
|
-
const moved = Math.abs(point.x - drag.origin.x) > CLICK_TOLERANCE || Math.abs(point.y - drag.origin.y) > CLICK_TOLERANCE;
|
|
2800
|
-
const current = shapesRef.current.find((s) => s.id === drag.clickedId);
|
|
2801
|
-
if (!moved && current) {
|
|
2802
|
-
updateShapes((prev) => applyDrag(prev, drag, drag.origin));
|
|
2803
|
-
if (current.type === "text" || isConnectorType(current.type)) {
|
|
2804
|
-
startTextEditing(current.id, "text");
|
|
2805
|
-
} else {
|
|
2806
|
-
startTextEditing(current.id, slotAt(current, drag.origin.y));
|
|
2400
|
+
case "cylinder": {
|
|
2401
|
+
const ry = CYLINDER_RY(b);
|
|
2402
|
+
const rx = b.w / 2;
|
|
2403
|
+
const cx = b.x + rx;
|
|
2404
|
+
return /* @__PURE__ */ jsxs4("g", { children: [
|
|
2405
|
+
/* @__PURE__ */ jsx4(
|
|
2406
|
+
"path",
|
|
2407
|
+
{
|
|
2408
|
+
...stroke,
|
|
2409
|
+
d: `M ${n(b.x)} ${n(b.y + ry)} V ${n(b.y + b.h - ry)} A ${n(rx)} ${n(ry)} 0 0 0 ${n(b.x + b.w)} ${n(b.y + b.h - ry)} V ${n(b.y + ry)}`,
|
|
2410
|
+
fill
|
|
2807
2411
|
}
|
|
2808
|
-
|
|
2412
|
+
),
|
|
2413
|
+
/* @__PURE__ */ jsx4("ellipse", { ...stroke, cx, cy: b.y + ry, rx, ry, fill })
|
|
2414
|
+
] });
|
|
2415
|
+
}
|
|
2416
|
+
case "cloud": {
|
|
2417
|
+
const p = (u, v) => `${n(b.x + u * b.w)} ${n(b.y + v * b.h)}`;
|
|
2418
|
+
return /* @__PURE__ */ jsx4(
|
|
2419
|
+
"path",
|
|
2420
|
+
{
|
|
2421
|
+
...stroke,
|
|
2422
|
+
d: `M ${p(0.22, 0.86)} C ${p(0.04, 0.88)} ${p(0, 0.58)} ${p(0.16, 0.5)} C ${p(0.08, 0.26)} ${p(0.3, 0.12)} ${p(0.42, 0.28)} C ${p(0.5, 0.02)} ${p(0.76, 0.04)} ${p(0.78, 0.3)} C ${p(0.98, 0.26)} ${p(1.04, 0.56)} ${p(0.88, 0.64)} C ${p(1, 0.8)} ${p(0.9, 0.9)} ${p(0.76, 0.86)} Z`,
|
|
2423
|
+
fill
|
|
2809
2424
|
}
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
setTool("select");
|
|
2823
|
-
setSelectedIds(/* @__PURE__ */ new Set([drag.id]));
|
|
2824
|
-
} else {
|
|
2825
|
-
updateShapes((prev) => prev.filter((s) => s.id !== drag.id));
|
|
2425
|
+
);
|
|
2426
|
+
}
|
|
2427
|
+
case "queue": {
|
|
2428
|
+
const rx = QUEUE_RX(b);
|
|
2429
|
+
const ry = b.h / 2;
|
|
2430
|
+
return /* @__PURE__ */ jsxs4("g", { children: [
|
|
2431
|
+
/* @__PURE__ */ jsx4(
|
|
2432
|
+
"path",
|
|
2433
|
+
{
|
|
2434
|
+
...stroke,
|
|
2435
|
+
d: `M ${n(b.x + rx)} ${n(b.y)} H ${n(b.x + b.w - rx)} V ${n(b.y + b.h)} H ${n(b.x + rx)} A ${n(rx)} ${n(ry)} 0 0 1 ${n(b.x + rx)} ${n(b.y)} Z`,
|
|
2436
|
+
fill
|
|
2826
2437
|
}
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
(prev) => prev.map((s) => {
|
|
2838
|
-
if (s.id !== drag.id) return s;
|
|
2839
|
-
const end = drag.end === "start" ? { x: s.x, y: s.y } : { x: s.x + s.w, y: s.y + s.h };
|
|
2840
|
-
const box = findBoxAt(prev, end, s.id);
|
|
2841
|
-
const other = drag.end === "start" ? s.endBinding : s.startBinding;
|
|
2842
|
-
if (box && box.id === other?.id) return s;
|
|
2843
|
-
const key = drag.end === "start" ? "startBinding" : "endBinding";
|
|
2844
|
-
return { ...s, [key]: box ? makeBinding(box, end) : void 0 };
|
|
2845
|
-
})
|
|
2846
|
-
);
|
|
2847
|
-
}
|
|
2848
|
-
updateShapes((prev) => prev.map(normalize), { commit: true });
|
|
2849
|
-
},
|
|
2850
|
-
[getPoint, paths, updateShapes, startTextEditing]
|
|
2851
|
-
);
|
|
2852
|
-
const deleteSelection = useCallback(() => {
|
|
2853
|
-
const ids = selectedRef.current;
|
|
2854
|
-
if (!ids.size) return;
|
|
2855
|
-
setEditingText(null);
|
|
2856
|
-
setSelectedIds(EMPTY_SET);
|
|
2857
|
-
updateShapes(
|
|
2858
|
-
(prev) => prev.filter(
|
|
2859
|
-
(s) => !ids.has(s.id) && !(s.startBinding && ids.has(s.startBinding.id)) && !(s.endBinding && ids.has(s.endBinding.id))
|
|
2860
|
-
),
|
|
2861
|
-
{ commit: true }
|
|
2862
|
-
);
|
|
2863
|
-
}, [updateShapes]);
|
|
2864
|
-
const editingRef = useRef4(editingText);
|
|
2865
|
-
editingRef.current = editingText;
|
|
2866
|
-
useEffect7(() => {
|
|
2867
|
-
const root = rootRef.current;
|
|
2868
|
-
if (!root || !isEditable) return;
|
|
2869
|
-
const onKeyDown = (e) => {
|
|
2870
|
-
if (editingRef.current) return;
|
|
2871
|
-
const ids = selectedRef.current;
|
|
2872
|
-
if ((e.key === "Delete" || e.key === "Backspace") && ids.size) {
|
|
2873
|
-
e.preventDefault();
|
|
2874
|
-
e.stopPropagation();
|
|
2875
|
-
deleteSelection();
|
|
2876
|
-
} else if (e.key === "Enter" && ids.size === 1) {
|
|
2877
|
-
const [id] = ids;
|
|
2878
|
-
const shape = shapesRef.current.find((s) => s.id === id);
|
|
2879
|
-
if (shape) {
|
|
2880
|
-
e.preventDefault();
|
|
2881
|
-
e.stopPropagation();
|
|
2882
|
-
startTextEditing(shape.id, "text");
|
|
2883
|
-
}
|
|
2884
|
-
} else if (e.key === "a" && (e.metaKey || e.ctrlKey)) {
|
|
2885
|
-
e.preventDefault();
|
|
2886
|
-
e.stopPropagation();
|
|
2887
|
-
setSelectedIds(new Set(shapesRef.current.map((s) => s.id)));
|
|
2888
|
-
} else if (e.key === "Escape") {
|
|
2889
|
-
e.stopPropagation();
|
|
2890
|
-
setSelectedIds(EMPTY_SET);
|
|
2891
|
-
setTool("select");
|
|
2892
|
-
}
|
|
2893
|
-
};
|
|
2894
|
-
root.addEventListener("keydown", onKeyDown);
|
|
2895
|
-
return () => root.removeEventListener("keydown", onKeyDown);
|
|
2896
|
-
}, [isEditable, deleteSelection, startTextEditing]);
|
|
2897
|
-
const applyToSelection = useCallback(
|
|
2898
|
-
(patch) => {
|
|
2899
|
-
const ids = selectedRef.current;
|
|
2900
|
-
if (!ids.size) return;
|
|
2901
|
-
updateShapes((prev) => prev.map((s) => ids.has(s.id) ? patch(s) : s), { commit: true });
|
|
2902
|
-
},
|
|
2903
|
-
[updateShapes]
|
|
2904
|
-
);
|
|
2905
|
-
const applyStroke = useCallback(
|
|
2906
|
-
(color) => {
|
|
2907
|
-
setStroke(color);
|
|
2908
|
-
applyToSelection((s) => ({ ...s, stroke: color }));
|
|
2909
|
-
},
|
|
2910
|
-
[applyToSelection]
|
|
2911
|
-
);
|
|
2912
|
-
const applyFill = useCallback(
|
|
2913
|
-
(color) => {
|
|
2914
|
-
setFill(color);
|
|
2915
|
-
applyToSelection((s) => isBoxType(s.type) ? { ...s, fill: color } : s);
|
|
2916
|
-
},
|
|
2917
|
-
[applyToSelection]
|
|
2918
|
-
);
|
|
2919
|
-
const applyRouting = useCallback(
|
|
2920
|
-
(elbow) => applyToSelection(
|
|
2921
|
-
(s) => isConnectorType(s.type) ? { ...s, routing: elbow ? "elbow" : void 0, elbow: void 0, waypoints: void 0 } : s
|
|
2922
|
-
),
|
|
2923
|
-
[applyToSelection]
|
|
2924
|
-
);
|
|
2925
|
-
const applyDirection = useCallback(
|
|
2926
|
-
(bidirectional) => applyToSelection(
|
|
2927
|
-
(s) => s.type === "arrow" ? { ...s, bidirectional: bidirectional || void 0 } : s
|
|
2928
|
-
),
|
|
2929
|
-
[applyToSelection]
|
|
2930
|
-
);
|
|
2931
|
-
const applyWidth = useCallback(
|
|
2932
|
-
(next) => {
|
|
2933
|
-
setWidth(next);
|
|
2934
|
-
commit(shapesRef.current, { width: next });
|
|
2935
|
-
},
|
|
2936
|
-
[commit]
|
|
2937
|
-
);
|
|
2938
|
-
const addWaypoint = useCallback(
|
|
2939
|
-
(e, shapeId, segmentIndex, point) => {
|
|
2940
|
-
if (!isEditable || e.button !== 0) return;
|
|
2941
|
-
e.stopPropagation();
|
|
2942
|
-
capture(e);
|
|
2943
|
-
updateShapes(
|
|
2944
|
-
(prev) => prev.map((s) => {
|
|
2945
|
-
if (s.id !== shapeId || !isConnectorType(s.type)) return s;
|
|
2946
|
-
const interior = connectorPoints(s, prev).slice(1, -1);
|
|
2947
|
-
const waypoints = [...interior.slice(0, segmentIndex), point, ...interior.slice(segmentIndex)];
|
|
2948
|
-
return { ...s, waypoints, routing: void 0, elbow: void 0 };
|
|
2949
|
-
})
|
|
2950
|
-
);
|
|
2951
|
-
dragRef.current = { mode: "waypoint", id: shapeId, index: segmentIndex };
|
|
2952
|
-
},
|
|
2953
|
-
[isEditable, updateShapes]
|
|
2954
|
-
);
|
|
2955
|
-
const removeWaypoint = useCallback(
|
|
2956
|
-
(shapeId, index) => {
|
|
2957
|
-
updateShapes(
|
|
2958
|
-
(prev) => prev.map((s) => {
|
|
2959
|
-
if (s.id !== shapeId || !s.waypoints) return s;
|
|
2960
|
-
const waypoints = s.waypoints.filter((_, i) => i !== index);
|
|
2961
|
-
return { ...s, waypoints: waypoints.length ? waypoints : void 0 };
|
|
2962
|
-
}),
|
|
2963
|
-
{ commit: true }
|
|
2964
|
-
);
|
|
2965
|
-
},
|
|
2966
|
-
[updateShapes]
|
|
2967
|
-
);
|
|
2968
|
-
const commitText = useCallback(
|
|
2969
|
-
(id, field, value) => {
|
|
2970
|
-
setEditingText(null);
|
|
2971
|
-
rootRef.current?.focus({ preventScroll: true });
|
|
2972
|
-
updateShapes(
|
|
2973
|
-
(prev) => prev.flatMap((s) => {
|
|
2974
|
-
if (s.id !== id) return [s];
|
|
2975
|
-
if (s.type === "text") {
|
|
2976
|
-
return value.trim() === "" ? [] : [{ ...s, text: value, ...textBoxSize(value) }];
|
|
2438
|
+
),
|
|
2439
|
+
/* @__PURE__ */ jsx4(
|
|
2440
|
+
"ellipse",
|
|
2441
|
+
{
|
|
2442
|
+
...stroke,
|
|
2443
|
+
cx: b.x + b.w - rx,
|
|
2444
|
+
cy: b.y + ry,
|
|
2445
|
+
rx,
|
|
2446
|
+
ry,
|
|
2447
|
+
fill
|
|
2977
2448
|
}
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
{ commit: true }
|
|
2981
|
-
);
|
|
2982
|
-
},
|
|
2983
|
-
[updateShapes]
|
|
2984
|
-
);
|
|
2985
|
-
const copyMermaid = useCallback(async () => {
|
|
2986
|
-
const payload = { version: 2, canvasHeight: heightRef.current, shapes: shapesRef.current };
|
|
2987
|
-
try {
|
|
2988
|
-
await navigator.clipboard.writeText(drawingToMermaid(payload));
|
|
2989
|
-
return true;
|
|
2990
|
-
} catch {
|
|
2991
|
-
return false;
|
|
2449
|
+
)
|
|
2450
|
+
] });
|
|
2992
2451
|
}
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
2452
|
+
case "actor": {
|
|
2453
|
+
const figH = b.h * ACTOR_FIGURE_RATIO;
|
|
2454
|
+
const cx = b.x + b.w / 2;
|
|
2455
|
+
const r = Math.max(4, Math.min(figH * 0.16, b.w * 0.2));
|
|
2456
|
+
const neck = b.y + r * 2;
|
|
2457
|
+
const hip = b.y + figH * 0.62;
|
|
2458
|
+
const armY = neck + figH * 0.12;
|
|
2459
|
+
const reach = Math.min(b.w * 0.32, figH * 0.3);
|
|
2460
|
+
return /* @__PURE__ */ jsxs4("g", { children: [
|
|
2461
|
+
/* @__PURE__ */ jsx4("circle", { ...stroke, cx, cy: b.y + r, r, fill }),
|
|
2462
|
+
/* @__PURE__ */ jsx4(
|
|
2463
|
+
"path",
|
|
2464
|
+
{
|
|
2465
|
+
...stroke,
|
|
2466
|
+
fill: "none",
|
|
2467
|
+
d: `M ${n(cx)} ${n(neck)} V ${n(hip)} M ${n(cx - reach)} ${n(armY)} H ${n(cx + reach)} M ${n(cx)} ${n(hip)} L ${n(cx - reach)} ${n(b.y + figH)} M ${n(cx)} ${n(hip)} L ${n(cx + reach)} ${n(b.y + figH)}`
|
|
2468
|
+
}
|
|
2469
|
+
)
|
|
2470
|
+
] });
|
|
2471
|
+
}
|
|
2472
|
+
default:
|
|
2473
|
+
return null;
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
// src/drawing/canvas/ShapeView.tsx
|
|
2478
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2479
|
+
import { createElement } from "react";
|
|
2480
|
+
var CONNECTOR_FONT_SIZE = 12;
|
|
2481
|
+
var CONNECTOR_LABEL_MAX_WIDTH = 160;
|
|
2482
|
+
function slotLayout(shape) {
|
|
2483
|
+
const def = boxDefinition(shape);
|
|
2484
|
+
return {
|
|
2485
|
+
area: def ? def.textArea(shape) : bbox(shape),
|
|
2486
|
+
slots: def ? def.textSlots : ["label", "text", "footer"]
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
function slotAt(shape, y) {
|
|
2490
|
+
const { area, slots } = slotLayout(shape);
|
|
2491
|
+
const rel = (y - area.y) / Math.max(area.h, 1);
|
|
2492
|
+
if (rel < 0.3 && slots.includes("label")) return "label";
|
|
2493
|
+
if (rel > 0.7 && slots.includes("footer")) return "footer";
|
|
2494
|
+
return slots.includes("text") ? "text" : slots[0];
|
|
2495
|
+
}
|
|
2496
|
+
var textStyle = { userSelect: "none" };
|
|
2497
|
+
function luminance(color) {
|
|
2498
|
+
const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color);
|
|
2499
|
+
if (!m) return 1;
|
|
2500
|
+
const hex = m[1].length === 3 ? m[1].replace(/./g, (c) => c + c) : m[1];
|
|
2501
|
+
const [r, g, b] = [0, 2, 4].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
|
|
2502
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
2503
|
+
}
|
|
2504
|
+
function textColorFor(shape) {
|
|
2505
|
+
if (shape.stroke === "transparent" || shape.stroke === "none") return "#1e1e1e";
|
|
2506
|
+
if (luminance(shape.fill) < 0.35) return "#ffffff";
|
|
2507
|
+
return shape.stroke;
|
|
2508
|
+
}
|
|
2509
|
+
var hintStyle = { userSelect: "none", pointerEvents: "none" };
|
|
2510
|
+
function BoxTexts({
|
|
2511
|
+
shape,
|
|
2512
|
+
hideField,
|
|
2513
|
+
showHints
|
|
2514
|
+
}) {
|
|
2515
|
+
const { area, slots } = slotLayout(shape);
|
|
2516
|
+
const cx = area.x + area.w / 2;
|
|
2517
|
+
const wrapW = Math.max(area.w, 20);
|
|
2518
|
+
const hints = showHints && area.h >= 56 && area.w >= 44;
|
|
2519
|
+
const mainLines = wrapText(shape.text ?? "", wrapW, FONT_SIZE);
|
|
2520
|
+
const labelLines = wrapText(shape.label ?? "", wrapW, SMALL_FONT_SIZE);
|
|
2521
|
+
const footerLines = wrapText(shape.footer ?? "", wrapW, SMALL_FONT_SIZE);
|
|
2522
|
+
const mainLineH = FONT_SIZE * LINE_HEIGHT;
|
|
2523
|
+
const smallLineH = SMALL_FONT_SIZE * LINE_HEIGHT;
|
|
2524
|
+
const mainStart = area.y + area.h / 2 - (mainLines.length - 1) * mainLineH / 2 + FONT_SIZE * 0.35;
|
|
2525
|
+
const common = {
|
|
2526
|
+
fill: textColorFor(shape),
|
|
2527
|
+
textAnchor: "middle"
|
|
2528
|
+
};
|
|
2529
|
+
return /* @__PURE__ */ jsxs5("g", { children: [
|
|
2530
|
+
slots.includes("label") && hideField !== "label" && (shape.label ? labelLines.map((line, i) => /* @__PURE__ */ createElement(
|
|
2531
|
+
"text",
|
|
2532
|
+
{
|
|
2533
|
+
...common,
|
|
2534
|
+
key: i,
|
|
2535
|
+
x: cx,
|
|
2536
|
+
y: area.y + SMALL_FONT_SIZE + i * smallLineH,
|
|
2537
|
+
fontSize: SMALL_FONT_SIZE,
|
|
2538
|
+
fontWeight: 600,
|
|
2539
|
+
letterSpacing: 0.3,
|
|
2540
|
+
opacity: 0.85,
|
|
2541
|
+
style: textStyle
|
|
3009
2542
|
},
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
onStroke: applyStroke,
|
|
3033
|
-
onFill: applyFill,
|
|
3034
|
-
onRouting: applyRouting,
|
|
3035
|
-
onDirection: applyDirection,
|
|
3036
|
-
onDelete: deleteSelection
|
|
3037
|
-
}
|
|
3038
|
-
) })
|
|
3039
|
-
] }),
|
|
3040
|
-
/* @__PURE__ */ jsxs8("div", { className: "zui-drawing-stage", children: [
|
|
3041
|
-
/* @__PURE__ */ jsxs8(
|
|
3042
|
-
"svg",
|
|
3043
|
-
{
|
|
3044
|
-
ref: svgRef,
|
|
3045
|
-
className: "zui-drawing-surface",
|
|
3046
|
-
style: {
|
|
3047
|
-
height: logicalWidth ? void 0 : canvasHeight,
|
|
3048
|
-
width: logicalWidth ?? void 0,
|
|
3049
|
-
maxWidth: "100%",
|
|
3050
|
-
aspectRatio: logicalWidth ? `${logicalWidth} / ${canvasHeight}` : void 0,
|
|
3051
|
-
cursor: canvasCursor
|
|
3052
|
-
},
|
|
3053
|
-
viewBox: logicalWidth ? `0 0 ${logicalWidth} ${canvasHeight}` : void 0,
|
|
3054
|
-
preserveAspectRatio: "xMinYMin meet",
|
|
3055
|
-
onPointerDown: handleBackgroundPointerDown,
|
|
3056
|
-
onPointerMove: handlePointerMove,
|
|
3057
|
-
onPointerUp: handlePointerUp,
|
|
3058
|
-
onPointerCancel: handlePointerUp,
|
|
3059
|
-
onDoubleClick: (e) => {
|
|
3060
|
-
if (!isEditable) return;
|
|
3061
|
-
const target = e.target.closest("[data-shape-id]");
|
|
3062
|
-
const id = target?.getAttribute("data-shape-id");
|
|
3063
|
-
const shape = id ? shapes.find((s) => s.id === id) : null;
|
|
3064
|
-
if (!shape) return;
|
|
3065
|
-
if (shape.type === "text" || isConnectorType(shape.type)) {
|
|
3066
|
-
startTextEditing(shape.id, "text");
|
|
3067
|
-
} else {
|
|
3068
|
-
startTextEditing(shape.id, slotAt(shape, getPoint(e).y));
|
|
3069
|
-
}
|
|
3070
|
-
},
|
|
3071
|
-
children: [
|
|
3072
|
-
shapes.map((shape) => /* @__PURE__ */ jsxs8(
|
|
3073
|
-
"g",
|
|
3074
|
-
{
|
|
3075
|
-
"data-shape-id": shape.id,
|
|
3076
|
-
className: `zui-drawing-shape ${selectedIds.has(shape.id) ? "is-selected" : ""}`,
|
|
3077
|
-
style: { cursor: isEditable && tool === "select" ? "move" : void 0 },
|
|
3078
|
-
onPointerDown: (e) => handleShapePointerDown(e, shape),
|
|
3079
|
-
children: [
|
|
3080
|
-
/* @__PURE__ */ jsx9(HitArea, { shape, points: paths.get(shape.id) }),
|
|
3081
|
-
shape.id === editingText?.id && shape.type === "text" ? null : /* @__PURE__ */ jsx9(
|
|
3082
|
-
ShapeView,
|
|
3083
|
-
{
|
|
3084
|
-
shape,
|
|
3085
|
-
points: paths.get(shape.id),
|
|
3086
|
-
hideField: shape.id === editingText?.id ? editingText.field : null,
|
|
3087
|
-
showHints: isEditable && tool === "select" && single?.id === shape.id && shape.id !== editingText?.id
|
|
3088
|
-
}
|
|
3089
|
-
)
|
|
3090
|
-
]
|
|
3091
|
-
},
|
|
3092
|
-
shape.id
|
|
3093
|
-
)),
|
|
3094
|
-
hoverBox && /* @__PURE__ */ jsx9(
|
|
3095
|
-
"polygon",
|
|
3096
|
-
{
|
|
3097
|
-
className: "zui-drawing-selection zui-drawing-bind-target",
|
|
3098
|
-
points: boxOutline(hoverBox).map((p) => `${p.x},${p.y}`).join(" "),
|
|
3099
|
-
fill: "currentColor",
|
|
3100
|
-
fillOpacity: 0.06,
|
|
3101
|
-
stroke: "currentColor",
|
|
3102
|
-
strokeWidth: 2,
|
|
3103
|
-
strokeLinejoin: "round",
|
|
3104
|
-
pointerEvents: "none"
|
|
3105
|
-
}
|
|
3106
|
-
),
|
|
3107
|
-
single && isEditable && !editingText && /* @__PURE__ */ jsx9(
|
|
3108
|
-
SelectionOverlay,
|
|
3109
|
-
{
|
|
3110
|
-
shape: single,
|
|
3111
|
-
points: paths.get(single.id) ?? [],
|
|
3112
|
-
onHandlePointerDown: handleHandlePointerDown,
|
|
3113
|
-
onWaypointAdd: addWaypoint,
|
|
3114
|
-
onWaypointRemove: removeWaypoint
|
|
3115
|
-
}
|
|
3116
|
-
),
|
|
3117
|
-
selection.length > 1 && isEditable && /* @__PURE__ */ jsx9(GroupSelectionOverlay, { shapes: selection, paths }),
|
|
3118
|
-
marquee && /* @__PURE__ */ jsx9(MarqueeOverlay, { rect: marqueeRect(marquee.origin, marquee.current) })
|
|
3119
|
-
]
|
|
3120
|
-
}
|
|
3121
|
-
),
|
|
3122
|
-
isEditable && shapes.length === 0 && /* @__PURE__ */ jsx9("div", { className: "zui-drawing-empty", "aria-hidden": "true", children: "Pick a shape above, then click or drag on the canvas" }),
|
|
3123
|
-
editingShape && editingText && /* @__PURE__ */ jsx9(
|
|
3124
|
-
"div",
|
|
3125
|
-
{
|
|
3126
|
-
className: "zui-drawing-overlay",
|
|
3127
|
-
style: {
|
|
3128
|
-
transform: `scale(${scale})`,
|
|
3129
|
-
width: logicalWidth ?? void 0,
|
|
3130
|
-
height: logicalWidth ? canvasHeight : void 0
|
|
3131
|
-
},
|
|
3132
|
-
children: /* @__PURE__ */ jsx9(
|
|
3133
|
-
TextEditOverlay,
|
|
3134
|
-
{
|
|
3135
|
-
shape: editingShape,
|
|
3136
|
-
field: editingText.field,
|
|
3137
|
-
points: paths.get(editingShape.id),
|
|
3138
|
-
onCommit: commitText
|
|
3139
|
-
},
|
|
3140
|
-
`${editingShape.id}:${editingText.field}`
|
|
3141
|
-
)
|
|
3142
|
-
}
|
|
3143
|
-
),
|
|
3144
|
-
isEditable && /* @__PURE__ */ jsx9(
|
|
3145
|
-
HeightHandle,
|
|
3146
|
-
{
|
|
3147
|
-
height: canvasHeight,
|
|
3148
|
-
scale,
|
|
3149
|
-
onChange: setCanvasHeight,
|
|
3150
|
-
onCommit: (h) => commit(shapesRef.current, { height: h })
|
|
3151
|
-
}
|
|
3152
|
-
)
|
|
3153
|
-
] })
|
|
3154
|
-
]
|
|
3155
|
-
}
|
|
3156
|
-
);
|
|
3157
|
-
}
|
|
3158
|
-
function HeightHandle({
|
|
3159
|
-
height,
|
|
3160
|
-
scale,
|
|
3161
|
-
onChange,
|
|
3162
|
-
onCommit
|
|
3163
|
-
}) {
|
|
3164
|
-
const dragRef = useRef4(null);
|
|
3165
|
-
const latestRef = useRef4(height);
|
|
3166
|
-
latestRef.current = height;
|
|
3167
|
-
const clamp = (h) => Math.round(Math.min(MAX_HEIGHT, Math.max(MIN_HEIGHT, h)));
|
|
3168
|
-
return /* @__PURE__ */ jsx9(
|
|
3169
|
-
"div",
|
|
3170
|
-
{
|
|
3171
|
-
className: "zui-drawing-resize",
|
|
3172
|
-
title: "Drag to resize the canvas",
|
|
3173
|
-
role: "separator",
|
|
3174
|
-
"aria-orientation": "horizontal",
|
|
3175
|
-
onPointerDown: (e) => {
|
|
3176
|
-
e.preventDefault();
|
|
3177
|
-
e.stopPropagation();
|
|
3178
|
-
e.currentTarget.setPointerCapture(e.pointerId);
|
|
3179
|
-
dragRef.current = { startY: e.clientY, orig: height };
|
|
2543
|
+
line
|
|
2544
|
+
)) : hints && /* @__PURE__ */ jsx5(
|
|
2545
|
+
"text",
|
|
2546
|
+
{
|
|
2547
|
+
...common,
|
|
2548
|
+
x: cx,
|
|
2549
|
+
y: area.y + SMALL_FONT_SIZE,
|
|
2550
|
+
fontSize: SMALL_FONT_SIZE,
|
|
2551
|
+
opacity: 0.3,
|
|
2552
|
+
style: hintStyle,
|
|
2553
|
+
children: "Label"
|
|
2554
|
+
}
|
|
2555
|
+
)),
|
|
2556
|
+
slots.includes("text") && hideField !== "text" && (shape.text ? mainLines.map((line, i) => /* @__PURE__ */ createElement(
|
|
2557
|
+
"text",
|
|
2558
|
+
{
|
|
2559
|
+
...common,
|
|
2560
|
+
key: i,
|
|
2561
|
+
x: cx,
|
|
2562
|
+
y: mainStart + i * mainLineH,
|
|
2563
|
+
fontSize: FONT_SIZE,
|
|
2564
|
+
style: textStyle
|
|
3180
2565
|
},
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
2566
|
+
line
|
|
2567
|
+
)) : hints && /* @__PURE__ */ jsx5(
|
|
2568
|
+
"text",
|
|
2569
|
+
{
|
|
2570
|
+
...common,
|
|
2571
|
+
x: cx,
|
|
2572
|
+
y: mainStart,
|
|
2573
|
+
fontSize: FONT_SIZE,
|
|
2574
|
+
opacity: 0.3,
|
|
2575
|
+
style: hintStyle,
|
|
2576
|
+
children: "Text"
|
|
2577
|
+
}
|
|
2578
|
+
)),
|
|
2579
|
+
slots.includes("footer") && hideField !== "footer" && (shape.footer ? footerLines.map((line, i) => /* @__PURE__ */ createElement(
|
|
2580
|
+
"text",
|
|
2581
|
+
{
|
|
2582
|
+
...common,
|
|
2583
|
+
key: i,
|
|
2584
|
+
x: cx,
|
|
2585
|
+
y: area.y + area.h - 3 - (footerLines.length - 1 - i) * smallLineH,
|
|
2586
|
+
fontSize: SMALL_FONT_SIZE,
|
|
2587
|
+
opacity: 0.65,
|
|
2588
|
+
style: textStyle
|
|
3185
2589
|
},
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
2590
|
+
line
|
|
2591
|
+
)) : hints && /* @__PURE__ */ jsx5(
|
|
2592
|
+
"text",
|
|
2593
|
+
{
|
|
2594
|
+
...common,
|
|
2595
|
+
x: cx,
|
|
2596
|
+
y: area.y + area.h - 3,
|
|
2597
|
+
fontSize: SMALL_FONT_SIZE,
|
|
2598
|
+
opacity: 0.3,
|
|
2599
|
+
style: hintStyle,
|
|
2600
|
+
children: "Footer"
|
|
2601
|
+
}
|
|
2602
|
+
))
|
|
2603
|
+
] });
|
|
2604
|
+
}
|
|
2605
|
+
function ConnectorLabel({
|
|
2606
|
+
shape,
|
|
2607
|
+
points
|
|
2608
|
+
}) {
|
|
2609
|
+
const text = shape.text ?? "";
|
|
2610
|
+
const lines = wrapText(text, CONNECTOR_LABEL_MAX_WIDTH, CONNECTOR_FONT_SIZE);
|
|
2611
|
+
const size = textBoxSize(lines.join("\n"), CONNECTOR_FONT_SIZE);
|
|
2612
|
+
const { x: midX, y: midY } = connectorMidpoint(points);
|
|
2613
|
+
const lineH = CONNECTOR_FONT_SIZE * LINE_HEIGHT;
|
|
2614
|
+
const startY = midY - (lines.length - 1) * lineH / 2 + CONNECTOR_FONT_SIZE * 0.35;
|
|
2615
|
+
return /* @__PURE__ */ jsxs5("g", { children: [
|
|
2616
|
+
/* @__PURE__ */ jsx5(
|
|
2617
|
+
"rect",
|
|
2618
|
+
{
|
|
2619
|
+
x: midX - size.w / 2 - 5,
|
|
2620
|
+
y: midY - size.h / 2 - 3,
|
|
2621
|
+
width: size.w + 10,
|
|
2622
|
+
height: size.h + 6,
|
|
2623
|
+
rx: 5,
|
|
2624
|
+
className: "zui-drawing-label-plate",
|
|
2625
|
+
opacity: 0.94
|
|
2626
|
+
}
|
|
2627
|
+
),
|
|
2628
|
+
lines.map((line, i) => /* @__PURE__ */ jsx5(
|
|
2629
|
+
"text",
|
|
2630
|
+
{
|
|
2631
|
+
x: midX,
|
|
2632
|
+
y: startY + i * lineH,
|
|
2633
|
+
fill: shape.stroke,
|
|
2634
|
+
fontSize: CONNECTOR_FONT_SIZE,
|
|
2635
|
+
textAnchor: "middle",
|
|
2636
|
+
style: textStyle,
|
|
2637
|
+
children: line
|
|
3190
2638
|
},
|
|
3191
|
-
|
|
2639
|
+
i
|
|
2640
|
+
))
|
|
2641
|
+
] });
|
|
2642
|
+
}
|
|
2643
|
+
function ShapeView({
|
|
2644
|
+
shape,
|
|
2645
|
+
points,
|
|
2646
|
+
hideField,
|
|
2647
|
+
showHints
|
|
2648
|
+
}) {
|
|
2649
|
+
const stroke = {
|
|
2650
|
+
stroke: shape.stroke,
|
|
2651
|
+
strokeWidth: shape.strokeWidth,
|
|
2652
|
+
strokeLinecap: "round",
|
|
2653
|
+
strokeLinejoin: "round"
|
|
2654
|
+
};
|
|
2655
|
+
if (isBoxType(shape.type)) {
|
|
2656
|
+
return /* @__PURE__ */ jsxs5("g", { children: [
|
|
2657
|
+
/* @__PURE__ */ jsx5(BoxGeometry, { shape, stroke }),
|
|
2658
|
+
/* @__PURE__ */ jsx5(BoxTexts, { shape, hideField, showHints })
|
|
2659
|
+
] });
|
|
2660
|
+
}
|
|
2661
|
+
if (isConnectorType(shape.type)) {
|
|
2662
|
+
const pts = points ?? [
|
|
2663
|
+
{ x: shape.x, y: shape.y },
|
|
2664
|
+
{ x: shape.x + shape.w, y: shape.y + shape.h }
|
|
2665
|
+
];
|
|
2666
|
+
return /* @__PURE__ */ jsxs5("g", { children: [
|
|
2667
|
+
/* @__PURE__ */ jsx5("path", { ...stroke, d: polylinePath(pts), fill: "none" }),
|
|
2668
|
+
arrowHeads(shape, pts).map((d, i) => /* @__PURE__ */ jsx5("path", { ...stroke, d, fill: "none" }, i)),
|
|
2669
|
+
shape.text && hideField !== "text" && /* @__PURE__ */ jsx5(ConnectorLabel, { shape, points: pts })
|
|
2670
|
+
] });
|
|
2671
|
+
}
|
|
2672
|
+
return /* @__PURE__ */ jsx5(
|
|
2673
|
+
"text",
|
|
2674
|
+
{
|
|
2675
|
+
x: shape.x,
|
|
2676
|
+
y: shape.y + FONT_SIZE,
|
|
2677
|
+
fill: shape.stroke,
|
|
2678
|
+
fontSize: FONT_SIZE,
|
|
2679
|
+
style: { userSelect: "none", whiteSpace: "pre" },
|
|
2680
|
+
children: (shape.text ?? "").split("\n").map((line, i) => /* @__PURE__ */ jsx5("tspan", { x: shape.x, dy: i === 0 ? 0 : FONT_SIZE * LINE_HEIGHT, children: line }, i))
|
|
3192
2681
|
}
|
|
3193
2682
|
);
|
|
3194
2683
|
}
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
blue: { stroke: "#1971c2", fill: "#a5d8ff" },
|
|
3202
|
-
orange: { stroke: "#f08c00", fill: "#ffec99" },
|
|
3203
|
-
purple: { stroke: "#7048e8", fill: "#d0bfff" }
|
|
3204
|
-
};
|
|
3205
|
-
var COLOR_NAMES = Object.keys(COLOR_PRESETS);
|
|
3206
|
-
var TEXT_WRAP_WIDTH = 260;
|
|
3207
|
-
var SLOT_GAP = 6;
|
|
3208
|
-
var RANK_GAP = 90;
|
|
3209
|
-
var STACK_GAP = 40;
|
|
3210
|
-
var LAYOUT_ORIGIN = 32;
|
|
3211
|
-
var CANVAS_MARGIN = 32;
|
|
3212
|
-
var MIN_AUTO_CANVAS_HEIGHT = 120;
|
|
3213
|
-
var DEFAULT_STROKE2 = "#1e1e1e";
|
|
3214
|
-
var STROKE_WIDTH = 2;
|
|
3215
|
-
function isDrawingSkeleton(value) {
|
|
3216
|
-
return isRecord2(value) && Array.isArray(value.boxes);
|
|
3217
|
-
}
|
|
3218
|
-
function parseDrawingSkeleton(json) {
|
|
3219
|
-
try {
|
|
3220
|
-
const parsed = JSON.parse(json);
|
|
3221
|
-
if (!isDrawingSkeleton(parsed)) return EMPTY_DRAWING;
|
|
3222
|
-
return expandSkeleton(normalizeSkeleton(parsed));
|
|
3223
|
-
} catch {
|
|
3224
|
-
return EMPTY_DRAWING;
|
|
2684
|
+
function HitArea({
|
|
2685
|
+
shape,
|
|
2686
|
+
points
|
|
2687
|
+
}) {
|
|
2688
|
+
if (isConnectorType(shape.type) && points) {
|
|
2689
|
+
return /* @__PURE__ */ jsx5("path", { d: polylinePath(points), fill: "none", stroke: "transparent", strokeWidth: 16 });
|
|
3225
2690
|
}
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
(box) => expandBox(box, boxTypes.get(box.id), sizes.get(box.id), positions.get(box.id))
|
|
2691
|
+
const b = bbox(shape);
|
|
2692
|
+
return /* @__PURE__ */ jsx5(
|
|
2693
|
+
"rect",
|
|
2694
|
+
{
|
|
2695
|
+
x: b.x - 2,
|
|
2696
|
+
y: b.y - 2,
|
|
2697
|
+
width: Math.max(b.w, 8) + 4,
|
|
2698
|
+
height: Math.max(b.h, 8) + 4,
|
|
2699
|
+
fill: "transparent",
|
|
2700
|
+
stroke: "none"
|
|
2701
|
+
}
|
|
3238
2702
|
);
|
|
3239
|
-
const centers = new Map(boxShapes.map((shape) => [shape.id, shapeCenter(shape)]));
|
|
3240
|
-
const used = new Set(boxIds);
|
|
3241
|
-
const nextId = (prefix) => {
|
|
3242
|
-
let i = used.size + 1;
|
|
3243
|
-
while (used.has(`${prefix}${i}`)) i += 1;
|
|
3244
|
-
const id = `${prefix}${i}`;
|
|
3245
|
-
used.add(id);
|
|
3246
|
-
return id;
|
|
3247
|
-
};
|
|
3248
|
-
const connectorShapes = connectors.map((c) => expandConnector(c, centers, nextId("c")));
|
|
3249
|
-
const textShapes = (skeleton.texts ?? []).map((t) => expandText(t, nextId("t")));
|
|
3250
|
-
const shapes = resolveBindings([...boxShapes, ...connectorShapes, ...textShapes]);
|
|
3251
|
-
const bottom = shapes.reduce((max, shape) => {
|
|
3252
|
-
const b = bbox(shape);
|
|
3253
|
-
return Math.max(max, b.y + b.h);
|
|
3254
|
-
}, 0);
|
|
3255
|
-
return normalizeDrawingData({
|
|
3256
|
-
version: 2,
|
|
3257
|
-
...skeleton.canvasWidth !== void 0 ? { canvasWidth: skeleton.canvasWidth } : {},
|
|
3258
|
-
canvasHeight: skeleton.canvasHeight ?? Math.max(MIN_AUTO_CANVAS_HEIGHT, Math.ceil(bottom + CANVAS_MARGIN)),
|
|
3259
|
-
...skeleton.width !== void 0 ? { width: skeleton.width } : {},
|
|
3260
|
-
shapes
|
|
3261
|
-
});
|
|
3262
2703
|
}
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
const
|
|
3278
|
-
const
|
|
3279
|
-
const
|
|
3280
|
-
const
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
2704
|
+
|
|
2705
|
+
// src/drawing/canvas/TextEditOverlay.tsx
|
|
2706
|
+
import {
|
|
2707
|
+
useEffect as useEffect5,
|
|
2708
|
+
useRef as useRef2,
|
|
2709
|
+
useState
|
|
2710
|
+
} from "react";
|
|
2711
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
2712
|
+
function TextEditOverlay({
|
|
2713
|
+
shape,
|
|
2714
|
+
field,
|
|
2715
|
+
points,
|
|
2716
|
+
onCommit
|
|
2717
|
+
}) {
|
|
2718
|
+
const original = shape[field] ?? "";
|
|
2719
|
+
const [value, setValue] = useState(original);
|
|
2720
|
+
const ref = useRef2(null);
|
|
2721
|
+
const valueRef = useRef2(value);
|
|
2722
|
+
valueRef.current = value;
|
|
2723
|
+
useEffect5(() => {
|
|
2724
|
+
const el = ref.current;
|
|
2725
|
+
if (!el) return;
|
|
2726
|
+
el.focus();
|
|
2727
|
+
el.select();
|
|
2728
|
+
const onKeyDown = (e) => {
|
|
2729
|
+
e.stopPropagation();
|
|
2730
|
+
if ((e.key === "Home" || e.key === "End") && !e.shiftKey && !e.metaKey && !e.ctrlKey) {
|
|
2731
|
+
e.preventDefault();
|
|
2732
|
+
const text = el.value;
|
|
2733
|
+
const caret = e.key === "Home" ? el.selectionStart : el.selectionEnd;
|
|
2734
|
+
const pos = e.key === "Home" ? text.lastIndexOf("\n", caret - 1) + 1 : (text.indexOf("\n", caret) + 1 || text.length + 1) - 1;
|
|
2735
|
+
el.setSelectionRange(pos, pos);
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
2739
|
+
e.preventDefault();
|
|
2740
|
+
onCommit(shape.id, field, valueRef.current);
|
|
2741
|
+
} else if (e.key === "Escape") {
|
|
2742
|
+
e.preventDefault();
|
|
2743
|
+
onCommit(shape.id, field, original);
|
|
2744
|
+
}
|
|
2745
|
+
};
|
|
2746
|
+
const stop = (e) => e.stopPropagation();
|
|
2747
|
+
el.addEventListener("keydown", onKeyDown);
|
|
2748
|
+
el.addEventListener("keyup", stop);
|
|
2749
|
+
el.addEventListener("keypress", stop);
|
|
2750
|
+
return () => {
|
|
2751
|
+
el.removeEventListener("keydown", onKeyDown);
|
|
2752
|
+
el.removeEventListener("keyup", stop);
|
|
2753
|
+
el.removeEventListener("keypress", stop);
|
|
2754
|
+
};
|
|
2755
|
+
}, []);
|
|
2756
|
+
const style = {
|
|
2757
|
+
color: textColorFor(shape),
|
|
2758
|
+
fontFamily: "var(--zui-drawing-font)",
|
|
2759
|
+
lineHeight: LINE_HEIGHT
|
|
3289
2760
|
};
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
2761
|
+
if (shape.type === "text") {
|
|
2762
|
+
const size = textBoxSize(value || " ");
|
|
2763
|
+
Object.assign(style, {
|
|
2764
|
+
left: shape.x,
|
|
2765
|
+
top: shape.y,
|
|
2766
|
+
fontSize: FONT_SIZE,
|
|
2767
|
+
width: Math.max(80, size.w + 20),
|
|
2768
|
+
height: size.h + 2,
|
|
2769
|
+
// `pre` would keep the lines, but Chrome ignores Home/End in a
|
|
2770
|
+
// `white-space: pre` textarea; the width tracks the content anyway
|
|
2771
|
+
whiteSpace: "pre-wrap"
|
|
2772
|
+
});
|
|
2773
|
+
} else if (isConnectorType(shape.type)) {
|
|
2774
|
+
const lines = wrapText(value, CONNECTOR_LABEL_MAX_WIDTH, CONNECTOR_FONT_SIZE);
|
|
2775
|
+
const size = textBoxSize(lines.join("\n") || " ", CONNECTOR_FONT_SIZE);
|
|
2776
|
+
const width = Math.max(70, size.w + 16);
|
|
2777
|
+
const height = size.h + 4;
|
|
2778
|
+
const mid = connectorMidpoint(
|
|
2779
|
+
points ?? [
|
|
2780
|
+
{ x: shape.x, y: shape.y },
|
|
2781
|
+
{ x: shape.x + shape.w, y: shape.y + shape.h }
|
|
2782
|
+
]
|
|
2783
|
+
);
|
|
2784
|
+
Object.assign(style, {
|
|
2785
|
+
left: mid.x - width / 2,
|
|
2786
|
+
top: mid.y - height / 2,
|
|
2787
|
+
width,
|
|
2788
|
+
height,
|
|
2789
|
+
fontSize: CONNECTOR_FONT_SIZE,
|
|
2790
|
+
textAlign: "center",
|
|
2791
|
+
whiteSpace: "pre-wrap"
|
|
2792
|
+
});
|
|
2793
|
+
} else {
|
|
2794
|
+
const { area } = slotLayout(shape);
|
|
2795
|
+
const fontSize = field === "text" ? FONT_SIZE : SMALL_FONT_SIZE;
|
|
2796
|
+
const width = Math.max(area.w, 40);
|
|
2797
|
+
const lines = wrapText(value, width, fontSize).length;
|
|
2798
|
+
const boxH = lines * fontSize * LINE_HEIGHT + 2;
|
|
2799
|
+
const top = field === "label" ? area.y - 1 : field === "footer" ? area.y + area.h - boxH + 1 : area.y + area.h / 2 - boxH / 2;
|
|
2800
|
+
Object.assign(style, {
|
|
2801
|
+
left: area.x,
|
|
2802
|
+
top,
|
|
2803
|
+
width,
|
|
2804
|
+
height: boxH,
|
|
2805
|
+
fontSize,
|
|
2806
|
+
fontWeight: field === "label" ? 600 : void 0,
|
|
2807
|
+
letterSpacing: field === "label" ? 0.3 : void 0,
|
|
2808
|
+
opacity: field === "text" ? 1 : field === "label" ? 0.85 : 0.65,
|
|
2809
|
+
textAlign: "center",
|
|
2810
|
+
whiteSpace: "pre-wrap"
|
|
2811
|
+
});
|
|
3313
2812
|
}
|
|
3314
|
-
const
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
2813
|
+
const placeholder = field === "label" ? "Label" : field === "footer" ? "Footer" : "Text";
|
|
2814
|
+
return /* @__PURE__ */ jsx6(
|
|
2815
|
+
"textarea",
|
|
2816
|
+
{
|
|
2817
|
+
ref,
|
|
2818
|
+
className: "zui-drawing-text-input",
|
|
2819
|
+
style,
|
|
2820
|
+
value,
|
|
2821
|
+
placeholder,
|
|
2822
|
+
spellCheck: false,
|
|
2823
|
+
onChange: (e) => setValue(e.target.value),
|
|
2824
|
+
onBlur: () => onCommit(shape.id, field, value),
|
|
2825
|
+
onPointerDown: (e) => e.stopPropagation()
|
|
2826
|
+
}
|
|
3319
2827
|
);
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
// src/drawing/canvas/Toolbar.tsx
|
|
2831
|
+
import { useEffect as useEffect6, useRef as useRef3, useState as useState2 } from "react";
|
|
2832
|
+
|
|
2833
|
+
// src/components/blockWidthOptions.tsx
|
|
2834
|
+
import { Fragment, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2835
|
+
var LAYOUT_OPTIONS = [
|
|
2836
|
+
{
|
|
2837
|
+
preset: "compact",
|
|
2838
|
+
label: "Compact (shrink to content, tight rows)",
|
|
2839
|
+
width: "content",
|
|
2840
|
+
density: "compact",
|
|
2841
|
+
icon: /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
2842
|
+
/* @__PURE__ */ jsx7("path", { d: "M3 4v12M17 4v12" }),
|
|
2843
|
+
/* @__PURE__ */ jsx7("path", { d: "M5.5 10h3M11.5 10h3M6.5 7.5L9 10l-2.5 2.5M13.5 7.5L11 10l2.5 2.5" })
|
|
2844
|
+
] })
|
|
2845
|
+
},
|
|
2846
|
+
{
|
|
2847
|
+
preset: "comfortable",
|
|
2848
|
+
label: "Comfortable (text width)",
|
|
2849
|
+
width: "text",
|
|
2850
|
+
density: "comfortable",
|
|
2851
|
+
icon: /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
2852
|
+
/* @__PURE__ */ jsx7("path", { d: "M3 4v12M17 4v12" }),
|
|
2853
|
+
/* @__PURE__ */ jsx7("path", { d: "M6.5 7h7M6.5 10h7M6.5 13h4.5" })
|
|
2854
|
+
] })
|
|
2855
|
+
},
|
|
2856
|
+
{
|
|
2857
|
+
preset: "full",
|
|
2858
|
+
label: "Full width",
|
|
2859
|
+
width: "full",
|
|
2860
|
+
density: "comfortable",
|
|
2861
|
+
icon: /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
2862
|
+
/* @__PURE__ */ jsx7("path", { d: "M3 4v12M17 4v12" }),
|
|
2863
|
+
/* @__PURE__ */ jsx7("path", { d: "M6 10h8M8.5 7.5L6 10l2.5 2.5M11.5 7.5L14 10l-2.5 2.5" })
|
|
2864
|
+
] })
|
|
3339
2865
|
}
|
|
3340
|
-
|
|
2866
|
+
];
|
|
2867
|
+
function layoutPreset(width) {
|
|
2868
|
+
return width === "content" ? "compact" : width === "text" ? "comfortable" : "full";
|
|
3341
2869
|
}
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
2870
|
+
|
|
2871
|
+
// src/drawing/canvas/Toolbar.tsx
|
|
2872
|
+
import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2873
|
+
var PRIMARY_TOOLS = [
|
|
2874
|
+
{ tool: "select", label: "Select" },
|
|
2875
|
+
{ tool: "rect", label: "Rectangle" },
|
|
2876
|
+
{ tool: "ellipse", label: "Ellipse" },
|
|
2877
|
+
{ tool: "diamond", label: "Diamond" },
|
|
2878
|
+
{ tool: "arrow", label: "Arrow" },
|
|
2879
|
+
{ tool: "line", label: "Line" },
|
|
2880
|
+
{ tool: "text", label: "Text" }
|
|
2881
|
+
];
|
|
2882
|
+
var MORE_SHAPES = ["note", "cylinder", "cloud", "queue", "actor"];
|
|
2883
|
+
function ToolButton({
|
|
2884
|
+
label,
|
|
2885
|
+
active,
|
|
2886
|
+
onClick,
|
|
2887
|
+
children,
|
|
2888
|
+
className,
|
|
2889
|
+
pressed
|
|
2890
|
+
}) {
|
|
2891
|
+
return /* @__PURE__ */ jsx8(
|
|
2892
|
+
"button",
|
|
2893
|
+
{
|
|
2894
|
+
type: "button",
|
|
2895
|
+
title: label,
|
|
2896
|
+
"aria-label": label,
|
|
2897
|
+
"aria-pressed": pressed ?? active,
|
|
2898
|
+
className: `zui-drawing-tool ${active ? "is-active" : ""} ${className ?? ""}`,
|
|
2899
|
+
onClick,
|
|
2900
|
+
children: /* @__PURE__ */ jsx8(Icon, { children })
|
|
3354
2901
|
}
|
|
3355
|
-
|
|
3356
|
-
};
|
|
3357
|
-
for (const id of ids) if (!state.has(id)) visit(id);
|
|
3358
|
-
return kept;
|
|
3359
|
-
}
|
|
3360
|
-
function rankNodes(ids, edges) {
|
|
3361
|
-
const incoming = /* @__PURE__ */ new Map();
|
|
3362
|
-
for (const edge of edges) (incoming.get(edge.to) ?? incoming.set(edge.to, []).get(edge.to)).push(edge.from);
|
|
3363
|
-
const ranks = /* @__PURE__ */ new Map();
|
|
3364
|
-
const rankOf = (id) => {
|
|
3365
|
-
const known = ranks.get(id);
|
|
3366
|
-
if (known !== void 0) return known;
|
|
3367
|
-
const rank = (incoming.get(id) ?? []).reduce((max, from) => Math.max(max, rankOf(from) + 1), 0);
|
|
3368
|
-
ranks.set(id, rank);
|
|
3369
|
-
return rank;
|
|
3370
|
-
};
|
|
3371
|
-
for (const id of ids) rankOf(id);
|
|
3372
|
-
return ranks;
|
|
2902
|
+
);
|
|
3373
2903
|
}
|
|
3374
|
-
function
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
2904
|
+
function DrawingToolbar({
|
|
2905
|
+
tool,
|
|
2906
|
+
onToolChange,
|
|
2907
|
+
width,
|
|
2908
|
+
onWidthChange,
|
|
2909
|
+
onCopyMermaid,
|
|
2910
|
+
properties
|
|
2911
|
+
}) {
|
|
2912
|
+
const [moreOpen, setMoreOpen] = useState2(false);
|
|
2913
|
+
const [lastMore, setLastMore] = useState2("note");
|
|
2914
|
+
const [copied, setCopied] = useState2(false);
|
|
2915
|
+
const moreRef = useRef3(null);
|
|
2916
|
+
useEffect6(() => {
|
|
2917
|
+
if (!moreOpen) return;
|
|
2918
|
+
const close = (e) => {
|
|
2919
|
+
if (!moreRef.current?.contains(e.target)) setMoreOpen(false);
|
|
2920
|
+
};
|
|
2921
|
+
const key = (e) => {
|
|
2922
|
+
if (e.key === "Escape") setMoreOpen(false);
|
|
2923
|
+
};
|
|
2924
|
+
document.addEventListener("pointerdown", close);
|
|
2925
|
+
document.addEventListener("keydown", key);
|
|
2926
|
+
return () => {
|
|
2927
|
+
document.removeEventListener("pointerdown", close);
|
|
2928
|
+
document.removeEventListener("keydown", key);
|
|
2929
|
+
};
|
|
2930
|
+
}, [moreOpen]);
|
|
2931
|
+
const moreActive = MORE_SHAPES.includes(tool);
|
|
2932
|
+
const moreIcon = moreActive ? SHAPE_ICONS[tool] : SHAPE_ICONS[lastMore];
|
|
2933
|
+
return /* @__PURE__ */ jsxs7(Fragment2, { children: [
|
|
2934
|
+
/* @__PURE__ */ jsxs7("div", { className: "zui-drawing-toolbar-group", role: "toolbar", "aria-label": "Drawing tools", children: [
|
|
2935
|
+
PRIMARY_TOOLS.map(({ tool: t, label }) => /* @__PURE__ */ jsx8(ToolButton, { label, active: tool === t, onClick: () => onToolChange(t), children: SHAPE_ICONS[t] }, t)),
|
|
2936
|
+
/* @__PURE__ */ jsxs7("div", { className: "zui-drawing-more", ref: moreRef, children: [
|
|
2937
|
+
/* @__PURE__ */ jsxs7(
|
|
2938
|
+
"button",
|
|
2939
|
+
{
|
|
2940
|
+
type: "button",
|
|
2941
|
+
title: "More shapes",
|
|
2942
|
+
"aria-label": "More shapes",
|
|
2943
|
+
"aria-haspopup": "menu",
|
|
2944
|
+
"aria-expanded": moreOpen,
|
|
2945
|
+
className: `zui-drawing-tool zui-drawing-tool-more ${moreActive ? "is-active" : ""}`,
|
|
2946
|
+
onClick: () => setMoreOpen((open) => !open),
|
|
2947
|
+
children: [
|
|
2948
|
+
/* @__PURE__ */ jsx8(Icon, { children: moreIcon }),
|
|
2949
|
+
/* @__PURE__ */ jsx8("span", { className: "zui-drawing-caret", "aria-hidden": "true" })
|
|
2950
|
+
]
|
|
2951
|
+
}
|
|
2952
|
+
),
|
|
2953
|
+
moreOpen && /* @__PURE__ */ jsx8("div", { className: "zui-drawing-popover", role: "menu", children: MORE_SHAPES.map((type) => /* @__PURE__ */ jsxs7(
|
|
2954
|
+
"button",
|
|
2955
|
+
{
|
|
2956
|
+
type: "button",
|
|
2957
|
+
role: "menuitemradio",
|
|
2958
|
+
"aria-checked": tool === type,
|
|
2959
|
+
className: `zui-drawing-popover-item ${tool === type ? "is-active" : ""}`,
|
|
2960
|
+
onClick: () => {
|
|
2961
|
+
setLastMore(type);
|
|
2962
|
+
onToolChange(type);
|
|
2963
|
+
setMoreOpen(false);
|
|
2964
|
+
},
|
|
2965
|
+
children: [
|
|
2966
|
+
/* @__PURE__ */ jsx8(Icon, { size: 18, children: SHAPE_ICONS[type] }),
|
|
2967
|
+
/* @__PURE__ */ jsx8("span", { children: BOX_DEFINITIONS[type].label })
|
|
2968
|
+
]
|
|
2969
|
+
},
|
|
2970
|
+
type
|
|
2971
|
+
)) })
|
|
2972
|
+
] })
|
|
2973
|
+
] }),
|
|
2974
|
+
properties,
|
|
2975
|
+
/* @__PURE__ */ jsxs7("div", { className: "zui-drawing-toolbar-group zui-drawing-toolbar-group-end", children: [
|
|
2976
|
+
/* @__PURE__ */ jsx8(
|
|
2977
|
+
ToolButton,
|
|
2978
|
+
{
|
|
2979
|
+
label: copied ? "Copied Mermaid to clipboard" : "Copy as Mermaid",
|
|
2980
|
+
className: copied ? "is-success" : "",
|
|
2981
|
+
onClick: () => {
|
|
2982
|
+
void onCopyMermaid().then((ok) => {
|
|
2983
|
+
if (!ok) return;
|
|
2984
|
+
setCopied(true);
|
|
2985
|
+
setTimeout(() => setCopied(false), 1600);
|
|
2986
|
+
});
|
|
2987
|
+
},
|
|
2988
|
+
children: copied ? UI_ICONS.check : UI_ICONS.mermaid
|
|
2989
|
+
}
|
|
2990
|
+
),
|
|
2991
|
+
/* @__PURE__ */ jsx8("span", { className: "zui-drawing-toolbar-divider" }),
|
|
2992
|
+
LAYOUT_OPTIONS.map(({ preset, label, icon, width: presetWidth }) => /* @__PURE__ */ jsx8(
|
|
2993
|
+
ToolButton,
|
|
2994
|
+
{
|
|
2995
|
+
label,
|
|
2996
|
+
active: layoutPreset(width) === preset,
|
|
2997
|
+
onClick: () => onWidthChange(presetWidth),
|
|
2998
|
+
children: icon
|
|
2999
|
+
},
|
|
3000
|
+
preset
|
|
3001
|
+
))
|
|
3002
|
+
] })
|
|
3003
|
+
] });
|
|
3391
3004
|
}
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3005
|
+
|
|
3006
|
+
// src/drawing/canvas/DrawingCanvas.tsx
|
|
3007
|
+
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
3008
|
+
var MIN_HEIGHT = 120;
|
|
3009
|
+
var MAX_HEIGHT = 1200;
|
|
3010
|
+
var WIDTH_CLASS = {
|
|
3011
|
+
full: "",
|
|
3012
|
+
text: "is-text-width",
|
|
3013
|
+
content: "is-content-width"
|
|
3014
|
+
};
|
|
3015
|
+
var MIN_CONTENT_WIDTH = 240;
|
|
3016
|
+
var CONTENT_WIDTH_PADDING = 40;
|
|
3017
|
+
var EMPTY_SET = /* @__PURE__ */ new Set();
|
|
3018
|
+
function contentWidth(shapes, paths) {
|
|
3019
|
+
let right = 0;
|
|
3020
|
+
for (const shape of shapes) {
|
|
3021
|
+
if (isConnectorType(shape.type)) {
|
|
3022
|
+
for (const p of paths.get(shape.id) ?? []) right = Math.max(right, p.x);
|
|
3023
|
+
} else {
|
|
3024
|
+
const b = bbox(shape);
|
|
3025
|
+
right = Math.max(right, b.x + b.w);
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
return Math.max(MIN_CONTENT_WIDTH, Math.ceil(right + CONTENT_WIDTH_PADDING));
|
|
3412
3029
|
}
|
|
3413
|
-
function
|
|
3414
|
-
const
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
type: "text",
|
|
3418
|
-
x: text.x,
|
|
3419
|
-
y: text.y,
|
|
3420
|
-
w: size.w,
|
|
3421
|
-
h: size.h,
|
|
3422
|
-
stroke: text.stroke ?? (text.color ? COLOR_PRESETS[text.color].stroke : DEFAULT_STROKE2),
|
|
3423
|
-
fill: "transparent",
|
|
3424
|
-
strokeWidth: STROKE_WIDTH,
|
|
3425
|
-
text: text.text
|
|
3426
|
-
};
|
|
3427
|
-
}
|
|
3428
|
-
function endId(end) {
|
|
3429
|
-
return typeof end === "string" ? end : end.id;
|
|
3430
|
-
}
|
|
3431
|
-
function toBinding(end) {
|
|
3432
|
-
if (typeof end === "string" || !end.side) return { id: endId(end) };
|
|
3433
|
-
return { id: end.id, fixedPoint: SIDE_FIXED_POINTS[end.side] };
|
|
3434
|
-
}
|
|
3435
|
-
function shapeCenter(shape) {
|
|
3436
|
-
const b = bbox(shape);
|
|
3437
|
-
return { x: b.x + b.w / 2, y: b.y + b.h / 2 };
|
|
3438
|
-
}
|
|
3439
|
-
function dedupeById(items) {
|
|
3440
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3441
|
-
return items.filter((item) => seen.has(item.id) ? false : (seen.add(item.id), true));
|
|
3442
|
-
}
|
|
3443
|
-
function normalizeSkeleton(raw) {
|
|
3444
|
-
const value = raw;
|
|
3445
|
-
const boxes = value.boxes.flatMap((b) => {
|
|
3446
|
-
const box = normalizeBox(b);
|
|
3447
|
-
return box ? [box] : [];
|
|
3448
|
-
});
|
|
3449
|
-
const connectors = Array.isArray(value.connectors) ? value.connectors.flatMap((c) => {
|
|
3450
|
-
const connector = normalizeConnector(c);
|
|
3451
|
-
return connector ? [connector] : [];
|
|
3452
|
-
}) : [];
|
|
3453
|
-
const texts = Array.isArray(value.texts) ? value.texts.flatMap((t) => {
|
|
3454
|
-
const text = normalizeText(t);
|
|
3455
|
-
return text ? [text] : [];
|
|
3456
|
-
}) : [];
|
|
3457
|
-
const canvasWidth = finiteNumber(value.canvasWidth);
|
|
3458
|
-
const canvasHeight = finiteNumber(value.canvasHeight);
|
|
3459
|
-
return {
|
|
3460
|
-
direction: value.direction === "down" ? "down" : "right",
|
|
3461
|
-
...canvasWidth !== void 0 ? { canvasWidth } : {},
|
|
3462
|
-
...canvasHeight !== void 0 ? { canvasHeight } : {},
|
|
3463
|
-
...isBlockWidth(value.width) ? { width: value.width } : {},
|
|
3464
|
-
boxes,
|
|
3465
|
-
connectors,
|
|
3466
|
-
texts
|
|
3467
|
-
};
|
|
3468
|
-
}
|
|
3469
|
-
function normalizeBox(value) {
|
|
3470
|
-
if (!isRecord2(value) || !nonEmptyString(value.id)) return null;
|
|
3471
|
-
if (value.type !== void 0 && !(typeof value.type === "string" && isBoxType(value.type))) {
|
|
3472
|
-
return null;
|
|
3473
|
-
}
|
|
3474
|
-
return compact({
|
|
3475
|
-
id: value.id,
|
|
3476
|
-
type: value.type,
|
|
3477
|
-
label: nonEmptyString(value.label) ? value.label : void 0,
|
|
3478
|
-
text: nonEmptyString(value.text) ? value.text : void 0,
|
|
3479
|
-
footer: nonEmptyString(value.footer) ? value.footer : void 0,
|
|
3480
|
-
x: finiteNumber(value.x),
|
|
3481
|
-
y: finiteNumber(value.y),
|
|
3482
|
-
w: positiveNumber(value.w),
|
|
3483
|
-
h: positiveNumber(value.h),
|
|
3484
|
-
color: colorName(value.color),
|
|
3485
|
-
stroke: nonEmptyString(value.stroke) ? value.stroke : void 0,
|
|
3486
|
-
fill: nonEmptyString(value.fill) ? value.fill : void 0
|
|
3487
|
-
});
|
|
3488
|
-
}
|
|
3489
|
-
function normalizeConnector(value) {
|
|
3490
|
-
if (!isRecord2(value)) return null;
|
|
3491
|
-
const from = normalizeEnd(value.from);
|
|
3492
|
-
const to = normalizeEnd(value.to);
|
|
3493
|
-
if (!from || !to) return null;
|
|
3494
|
-
if (value.type !== void 0 && !(typeof value.type === "string" && isConnectorType(value.type))) {
|
|
3495
|
-
return null;
|
|
3030
|
+
function computePaths(shapes) {
|
|
3031
|
+
const paths = /* @__PURE__ */ new Map();
|
|
3032
|
+
for (const shape of shapes) {
|
|
3033
|
+
if (isConnectorType(shape.type)) paths.set(shape.id, connectorPoints(shape, shapes));
|
|
3496
3034
|
}
|
|
3497
|
-
return
|
|
3498
|
-
id: nonEmptyString(value.id) ? value.id : void 0,
|
|
3499
|
-
type: value.type,
|
|
3500
|
-
from,
|
|
3501
|
-
to,
|
|
3502
|
-
text: nonEmptyString(value.text) ? value.text : void 0,
|
|
3503
|
-
bidirectional: value.bidirectional === true ? true : void 0,
|
|
3504
|
-
routing: value.routing === "elbow" ? "elbow" : void 0,
|
|
3505
|
-
color: colorName(value.color),
|
|
3506
|
-
stroke: nonEmptyString(value.stroke) ? value.stroke : void 0
|
|
3507
|
-
});
|
|
3508
|
-
}
|
|
3509
|
-
function normalizeEnd(value) {
|
|
3510
|
-
if (nonEmptyString(value)) return value;
|
|
3511
|
-
if (!isRecord2(value) || !nonEmptyString(value.id)) return null;
|
|
3512
|
-
const side = value.side;
|
|
3513
|
-
return typeof side === "string" && side in SIDE_FIXED_POINTS ? { id: value.id, side } : { id: value.id };
|
|
3514
|
-
}
|
|
3515
|
-
function normalizeText(value) {
|
|
3516
|
-
if (!isRecord2(value) || !nonEmptyString(value.text)) return null;
|
|
3517
|
-
const x = finiteNumber(value.x);
|
|
3518
|
-
const y = finiteNumber(value.y);
|
|
3519
|
-
if (x === void 0 || y === void 0) return null;
|
|
3520
|
-
return compact({
|
|
3521
|
-
id: nonEmptyString(value.id) ? value.id : void 0,
|
|
3522
|
-
x,
|
|
3523
|
-
y,
|
|
3524
|
-
text: value.text,
|
|
3525
|
-
color: colorName(value.color),
|
|
3526
|
-
stroke: nonEmptyString(value.stroke) ? value.stroke : void 0
|
|
3527
|
-
});
|
|
3528
|
-
}
|
|
3529
|
-
function isRecord2(value) {
|
|
3530
|
-
return typeof value === "object" && value !== null;
|
|
3531
|
-
}
|
|
3532
|
-
function nonEmptyString(value) {
|
|
3533
|
-
return typeof value === "string" && value !== "";
|
|
3534
|
-
}
|
|
3535
|
-
function finiteNumber(value) {
|
|
3536
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
3537
|
-
}
|
|
3538
|
-
function positiveNumber(value) {
|
|
3539
|
-
const n2 = finiteNumber(value);
|
|
3540
|
-
return n2 !== void 0 && n2 > 0 ? n2 : void 0;
|
|
3541
|
-
}
|
|
3542
|
-
function colorName(value) {
|
|
3543
|
-
return typeof value === "string" && COLOR_NAMES.includes(value) ? value : void 0;
|
|
3035
|
+
return paths;
|
|
3544
3036
|
}
|
|
3545
|
-
function
|
|
3546
|
-
|
|
3037
|
+
function DrawingCanvas({ nodeKey, data }) {
|
|
3038
|
+
const [editor] = useLexicalComposerContext5();
|
|
3039
|
+
const isEditable = useLexicalEditable();
|
|
3040
|
+
const [shapes, setShapes] = useState3(data.shapes);
|
|
3041
|
+
const [canvasHeight, setCanvasHeight] = useState3(data.canvasHeight);
|
|
3042
|
+
const [width, setWidth] = useState3(data.width ?? "full");
|
|
3043
|
+
const [tool, setTool] = useState3("select");
|
|
3044
|
+
const [selectedIds, setSelectedIds] = useState3(EMPTY_SET);
|
|
3045
|
+
const [editingText, setEditingText] = useState3(null);
|
|
3046
|
+
const [stroke, setStroke] = useState3(STROKE_COLORS[0]);
|
|
3047
|
+
const [fill, setFill] = useState3(FILL_COLORS[0]);
|
|
3048
|
+
const [hoverBoxId, setHoverBoxId] = useState3(null);
|
|
3049
|
+
const [marquee, setMarquee] = useState3(null);
|
|
3050
|
+
const [scale, setScale] = useState3(1);
|
|
3051
|
+
const rootRef = useRef4(null);
|
|
3052
|
+
const svgRef = useRef4(null);
|
|
3053
|
+
const dragRef = useRef4(null);
|
|
3054
|
+
const pendingTextEditRef = useRef4(null);
|
|
3055
|
+
const pendingPointRef = useRef4(null);
|
|
3056
|
+
const frameRef = useRef4(null);
|
|
3057
|
+
const lastCommittedRef = useRef4(serializeDrawingData(data));
|
|
3058
|
+
const shapesRef = useRef4(shapes);
|
|
3059
|
+
shapesRef.current = shapes;
|
|
3060
|
+
const heightRef = useRef4(canvasHeight);
|
|
3061
|
+
heightRef.current = canvasHeight;
|
|
3062
|
+
const widthRef = useRef4(width);
|
|
3063
|
+
widthRef.current = width;
|
|
3064
|
+
const selectedRef = useRef4(selectedIds);
|
|
3065
|
+
selectedRef.current = selectedIds;
|
|
3066
|
+
const paths = useMemo(() => computePaths(shapes), [shapes]);
|
|
3067
|
+
const canvasWidth = data.canvasWidth;
|
|
3068
|
+
const logicalWidth = canvasWidth ?? (width === "content" ? contentWidth(shapes, paths) : null);
|
|
3069
|
+
useEffect7(() => {
|
|
3070
|
+
const incoming = serializeDrawingData(data);
|
|
3071
|
+
if (incoming !== lastCommittedRef.current) {
|
|
3072
|
+
lastCommittedRef.current = incoming;
|
|
3073
|
+
setShapes(data.shapes);
|
|
3074
|
+
setCanvasHeight(data.canvasHeight);
|
|
3075
|
+
setWidth(data.width ?? "full");
|
|
3076
|
+
setSelectedIds(EMPTY_SET);
|
|
3077
|
+
setEditingText(null);
|
|
3078
|
+
}
|
|
3079
|
+
}, [data]);
|
|
3080
|
+
useEffect7(() => {
|
|
3081
|
+
const svg = svgRef.current;
|
|
3082
|
+
if (!logicalWidth || !svg || typeof ResizeObserver === "undefined") {
|
|
3083
|
+
setScale(1);
|
|
3084
|
+
return;
|
|
3085
|
+
}
|
|
3086
|
+
const update = () => {
|
|
3087
|
+
const rect = svg.getBoundingClientRect();
|
|
3088
|
+
setScale(rect.width > 0 ? rect.width / logicalWidth : 1);
|
|
3089
|
+
};
|
|
3090
|
+
update();
|
|
3091
|
+
const observer = new ResizeObserver(update);
|
|
3092
|
+
observer.observe(svg);
|
|
3093
|
+
return () => observer.disconnect();
|
|
3094
|
+
}, [logicalWidth]);
|
|
3095
|
+
const commit = useCallback(
|
|
3096
|
+
(nextShapes, options) => {
|
|
3097
|
+
const nextWidth = options?.width ?? widthRef.current;
|
|
3098
|
+
const explicitFull = options?.width === void 0 && data.width !== void 0;
|
|
3099
|
+
const payload = {
|
|
3100
|
+
version: 2,
|
|
3101
|
+
...canvasWidth ? { canvasWidth } : {},
|
|
3102
|
+
canvasHeight: options?.height ?? heightRef.current,
|
|
3103
|
+
...nextWidth !== "full" || explicitFull ? { width: nextWidth } : {},
|
|
3104
|
+
shapes: nextShapes
|
|
3105
|
+
};
|
|
3106
|
+
const json = serializeDrawingData(payload);
|
|
3107
|
+
if (json === lastCommittedRef.current) return;
|
|
3108
|
+
lastCommittedRef.current = json;
|
|
3109
|
+
editor.update(() => {
|
|
3110
|
+
const node = $getNodeByKey2(nodeKey);
|
|
3111
|
+
if ($isDrawingNode(node)) node.setData(payload);
|
|
3112
|
+
});
|
|
3113
|
+
},
|
|
3114
|
+
[editor, nodeKey, canvasWidth, data.width]
|
|
3115
|
+
);
|
|
3116
|
+
const updateShapes = useCallback(
|
|
3117
|
+
(updater, options) => {
|
|
3118
|
+
setShapes((prev) => {
|
|
3119
|
+
const next = resolveBindings(updater(prev));
|
|
3120
|
+
if (options?.commit) commit(next);
|
|
3121
|
+
return next;
|
|
3122
|
+
});
|
|
3123
|
+
},
|
|
3124
|
+
[commit]
|
|
3125
|
+
);
|
|
3126
|
+
const getPoint = useCallback((e) => {
|
|
3127
|
+
const svg = svgRef.current;
|
|
3128
|
+
if (!svg) return { x: 0, y: 0 };
|
|
3129
|
+
const ctm = svg.getScreenCTM();
|
|
3130
|
+
if (!ctm) {
|
|
3131
|
+
const rect = svg.getBoundingClientRect();
|
|
3132
|
+
return { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
|
3133
|
+
}
|
|
3134
|
+
const inverse = ctm.inverse();
|
|
3135
|
+
return {
|
|
3136
|
+
x: inverse.a * e.clientX + inverse.c * e.clientY + inverse.e,
|
|
3137
|
+
y: inverse.b * e.clientX + inverse.d * e.clientY + inverse.f
|
|
3138
|
+
};
|
|
3139
|
+
}, []);
|
|
3140
|
+
const selection = useMemo(
|
|
3141
|
+
() => shapes.filter((s) => selectedIds.has(s.id)),
|
|
3142
|
+
[shapes, selectedIds]
|
|
3143
|
+
);
|
|
3144
|
+
const single = selection.length === 1 ? selection[0] : null;
|
|
3145
|
+
const startTextEditing = useCallback((id, field) => {
|
|
3146
|
+
setEditingText({ id, field });
|
|
3147
|
+
setSelectedIds(/* @__PURE__ */ new Set([id]));
|
|
3148
|
+
}, []);
|
|
3149
|
+
const capture = (e) => {
|
|
3150
|
+
svgRef.current?.setPointerCapture(e.pointerId);
|
|
3151
|
+
};
|
|
3152
|
+
const handleBackgroundPointerDown = useCallback(
|
|
3153
|
+
(e) => {
|
|
3154
|
+
if (!isEditable || editingText || e.button !== 0) return;
|
|
3155
|
+
const point = getPoint(e);
|
|
3156
|
+
capture(e);
|
|
3157
|
+
if (tool === "select") {
|
|
3158
|
+
if (!e.shiftKey) setSelectedIds(EMPTY_SET);
|
|
3159
|
+
dragRef.current = { mode: "marquee", origin: point, current: point, additive: e.shiftKey };
|
|
3160
|
+
return;
|
|
3161
|
+
}
|
|
3162
|
+
if (tool === "text") {
|
|
3163
|
+
const id2 = createShapeId();
|
|
3164
|
+
const size = textBoxSize("");
|
|
3165
|
+
updateShapes((prev) => [
|
|
3166
|
+
...prev,
|
|
3167
|
+
{
|
|
3168
|
+
id: id2,
|
|
3169
|
+
type: "text",
|
|
3170
|
+
x: point.x,
|
|
3171
|
+
y: point.y - FONT_SIZE / 2,
|
|
3172
|
+
w: size.w,
|
|
3173
|
+
h: size.h,
|
|
3174
|
+
stroke,
|
|
3175
|
+
fill: "transparent",
|
|
3176
|
+
strokeWidth: 2,
|
|
3177
|
+
text: ""
|
|
3178
|
+
}
|
|
3179
|
+
]);
|
|
3180
|
+
setTool("select");
|
|
3181
|
+
setSelectedIds(/* @__PURE__ */ new Set([id2]));
|
|
3182
|
+
pendingTextEditRef.current = id2;
|
|
3183
|
+
return;
|
|
3184
|
+
}
|
|
3185
|
+
const id = createShapeId();
|
|
3186
|
+
const def = isBoxType(tool) ? BOX_DEFINITIONS[tool] : null;
|
|
3187
|
+
const shapeFill = def ? fill === "transparent" && def.defaultFill ? def.defaultFill : fill : "transparent";
|
|
3188
|
+
dragRef.current = { mode: "draw", id, origin: point };
|
|
3189
|
+
setSelectedIds(EMPTY_SET);
|
|
3190
|
+
updateShapes((prev) => [
|
|
3191
|
+
...prev,
|
|
3192
|
+
{ id, type: tool, x: point.x, y: point.y, w: 0, h: 0, stroke, fill: shapeFill, strokeWidth: 2 }
|
|
3193
|
+
]);
|
|
3194
|
+
},
|
|
3195
|
+
[isEditable, editingText, getPoint, tool, stroke, fill, updateShapes, startTextEditing]
|
|
3196
|
+
);
|
|
3197
|
+
const handleShapePointerDown = useCallback(
|
|
3198
|
+
(e, shape) => {
|
|
3199
|
+
if (!isEditable || tool !== "select" || editingText || e.button !== 0) return;
|
|
3200
|
+
e.stopPropagation();
|
|
3201
|
+
const point = getPoint(e);
|
|
3202
|
+
capture(e);
|
|
3203
|
+
const selected = selectedRef.current;
|
|
3204
|
+
if (e.shiftKey) {
|
|
3205
|
+
if (selected.has(shape.id)) {
|
|
3206
|
+
const next2 = new Set(selected);
|
|
3207
|
+
next2.delete(shape.id);
|
|
3208
|
+
setSelectedIds(next2);
|
|
3209
|
+
return;
|
|
3210
|
+
}
|
|
3211
|
+
const next = new Set(selected).add(shape.id);
|
|
3212
|
+
setSelectedIds(next);
|
|
3213
|
+
dragRef.current = {
|
|
3214
|
+
mode: "move",
|
|
3215
|
+
clickedId: shape.id,
|
|
3216
|
+
origin: point,
|
|
3217
|
+
origs: moveGroup(shapesRef.current, next, shape.id),
|
|
3218
|
+
wasSelected: false
|
|
3219
|
+
};
|
|
3220
|
+
return;
|
|
3221
|
+
}
|
|
3222
|
+
const wasSelected = selected.size === 1 && selected.has(shape.id);
|
|
3223
|
+
const group = selected.has(shape.id) ? selected : /* @__PURE__ */ new Set([shape.id]);
|
|
3224
|
+
if (!selected.has(shape.id)) setSelectedIds(group);
|
|
3225
|
+
dragRef.current = {
|
|
3226
|
+
mode: "move",
|
|
3227
|
+
clickedId: shape.id,
|
|
3228
|
+
origin: point,
|
|
3229
|
+
origs: moveGroup(shapesRef.current, group, shape.id),
|
|
3230
|
+
wasSelected
|
|
3231
|
+
};
|
|
3232
|
+
},
|
|
3233
|
+
[isEditable, tool, editingText, getPoint]
|
|
3234
|
+
);
|
|
3235
|
+
const handleHandlePointerDown = useCallback(
|
|
3236
|
+
(e, drag) => {
|
|
3237
|
+
if (!isEditable || e.button !== 0) return;
|
|
3238
|
+
e.stopPropagation();
|
|
3239
|
+
capture(e);
|
|
3240
|
+
dragRef.current = drag;
|
|
3241
|
+
if (drag.mode === "endpoint") {
|
|
3242
|
+
const key = drag.end === "start" ? "startBinding" : "endBinding";
|
|
3243
|
+
updateShapes(
|
|
3244
|
+
(prev) => prev.map((s) => s.id === drag.id ? { ...s, [key]: void 0 } : s)
|
|
3245
|
+
);
|
|
3246
|
+
}
|
|
3247
|
+
},
|
|
3248
|
+
[isEditable, updateShapes]
|
|
3249
|
+
);
|
|
3250
|
+
const flushPointer = useCallback(() => {
|
|
3251
|
+
frameRef.current = null;
|
|
3252
|
+
const point = pendingPointRef.current;
|
|
3253
|
+
const drag = dragRef.current;
|
|
3254
|
+
if (!point || !drag) return;
|
|
3255
|
+
if (drag.mode === "marquee") {
|
|
3256
|
+
drag.current = point;
|
|
3257
|
+
setMarquee({ origin: drag.origin, current: point });
|
|
3258
|
+
return;
|
|
3259
|
+
}
|
|
3260
|
+
updateShapes((prev) => applyDrag(prev, drag, point));
|
|
3261
|
+
if (drag.mode === "endpoint" || drag.mode === "draw" && isConnectorType(shapesRef.current.find((s) => s.id === drag.id)?.type ?? "")) {
|
|
3262
|
+
const box = findBoxAt(shapesRef.current, point, drag.id);
|
|
3263
|
+
setHoverBoxId((prev) => prev === (box?.id ?? null) ? prev : box?.id ?? null);
|
|
3264
|
+
}
|
|
3265
|
+
}, [updateShapes]);
|
|
3266
|
+
const handlePointerMove = useCallback(
|
|
3267
|
+
(e) => {
|
|
3268
|
+
if (!dragRef.current) return;
|
|
3269
|
+
pendingPointRef.current = getPoint(e);
|
|
3270
|
+
if (frameRef.current === null) {
|
|
3271
|
+
frameRef.current = requestAnimationFrame(flushPointer);
|
|
3272
|
+
}
|
|
3273
|
+
},
|
|
3274
|
+
[getPoint, flushPointer]
|
|
3275
|
+
);
|
|
3276
|
+
const handlePointerUp = useCallback(
|
|
3277
|
+
(e) => {
|
|
3278
|
+
const drag = dragRef.current;
|
|
3279
|
+
dragRef.current = null;
|
|
3280
|
+
if (frameRef.current !== null) {
|
|
3281
|
+
cancelAnimationFrame(frameRef.current);
|
|
3282
|
+
frameRef.current = null;
|
|
3283
|
+
}
|
|
3284
|
+
pendingPointRef.current = null;
|
|
3285
|
+
setHoverBoxId(null);
|
|
3286
|
+
if (pendingTextEditRef.current) {
|
|
3287
|
+
const id = pendingTextEditRef.current;
|
|
3288
|
+
pendingTextEditRef.current = null;
|
|
3289
|
+
startTextEditing(id, "text");
|
|
3290
|
+
return;
|
|
3291
|
+
}
|
|
3292
|
+
if (!drag) return;
|
|
3293
|
+
const point = getPoint(e);
|
|
3294
|
+
if (drag.mode === "marquee") {
|
|
3295
|
+
setMarquee(null);
|
|
3296
|
+
const rect = marqueeRect(drag.origin, point);
|
|
3297
|
+
if (rect.w < CLICK_TOLERANCE && rect.h < CLICK_TOLERANCE) return;
|
|
3298
|
+
const ids = shapesWithin(shapesRef.current, rect, paths);
|
|
3299
|
+
setSelectedIds((prev) => drag.additive ? /* @__PURE__ */ new Set([...prev, ...ids]) : new Set(ids));
|
|
3300
|
+
return;
|
|
3301
|
+
}
|
|
3302
|
+
if (drag.mode === "move" && drag.wasSelected) {
|
|
3303
|
+
const moved = Math.abs(point.x - drag.origin.x) > CLICK_TOLERANCE || Math.abs(point.y - drag.origin.y) > CLICK_TOLERANCE;
|
|
3304
|
+
const current = shapesRef.current.find((s) => s.id === drag.clickedId);
|
|
3305
|
+
if (!moved && current) {
|
|
3306
|
+
updateShapes((prev) => applyDrag(prev, drag, drag.origin));
|
|
3307
|
+
if (current.type === "text" || isConnectorType(current.type)) {
|
|
3308
|
+
startTextEditing(current.id, "text");
|
|
3309
|
+
} else {
|
|
3310
|
+
startTextEditing(current.id, slotAt(current, drag.origin.y));
|
|
3311
|
+
}
|
|
3312
|
+
return;
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
if (drag.mode === "draw") {
|
|
3316
|
+
const drawn = shapesRef.current.find((s) => s.id === drag.id);
|
|
3317
|
+
if (drawn && Math.abs(drawn.w) < 4 && Math.abs(drawn.h) < 4) {
|
|
3318
|
+
if (drawn && isBoxType(drawn.type)) {
|
|
3319
|
+
const size = BOX_DEFINITIONS[drawn.type].defaultSize;
|
|
3320
|
+
updateShapes(
|
|
3321
|
+
(prev) => prev.map(
|
|
3322
|
+
(s) => s.id === drag.id ? { ...s, x: s.x - size.w / 2, y: s.y - size.h / 2, w: size.w, h: size.h } : s
|
|
3323
|
+
),
|
|
3324
|
+
{ commit: true }
|
|
3325
|
+
);
|
|
3326
|
+
setTool("select");
|
|
3327
|
+
setSelectedIds(/* @__PURE__ */ new Set([drag.id]));
|
|
3328
|
+
} else {
|
|
3329
|
+
updateShapes((prev) => prev.filter((s) => s.id !== drag.id));
|
|
3330
|
+
}
|
|
3331
|
+
return;
|
|
3332
|
+
}
|
|
3333
|
+
if (drawn && isConnectorType(drawn.type)) {
|
|
3334
|
+
updateShapes((prev) => prev.map((s) => s.id === drag.id ? bindEndpoints(s, prev) : s));
|
|
3335
|
+
}
|
|
3336
|
+
setTool("select");
|
|
3337
|
+
setSelectedIds(/* @__PURE__ */ new Set([drag.id]));
|
|
3338
|
+
}
|
|
3339
|
+
if (drag.mode === "endpoint") {
|
|
3340
|
+
updateShapes(
|
|
3341
|
+
(prev) => prev.map((s) => {
|
|
3342
|
+
if (s.id !== drag.id) return s;
|
|
3343
|
+
const end = drag.end === "start" ? { x: s.x, y: s.y } : { x: s.x + s.w, y: s.y + s.h };
|
|
3344
|
+
const box = findBoxAt(prev, end, s.id);
|
|
3345
|
+
const other = drag.end === "start" ? s.endBinding : s.startBinding;
|
|
3346
|
+
if (box && box.id === other?.id) return s;
|
|
3347
|
+
const key = drag.end === "start" ? "startBinding" : "endBinding";
|
|
3348
|
+
return { ...s, [key]: box ? makeBinding(box, end) : void 0 };
|
|
3349
|
+
})
|
|
3350
|
+
);
|
|
3351
|
+
}
|
|
3352
|
+
updateShapes((prev) => prev.map(normalize), { commit: true });
|
|
3353
|
+
},
|
|
3354
|
+
[getPoint, paths, updateShapes, startTextEditing]
|
|
3355
|
+
);
|
|
3356
|
+
const deleteSelection = useCallback(() => {
|
|
3357
|
+
const ids = selectedRef.current;
|
|
3358
|
+
if (!ids.size) return;
|
|
3359
|
+
setEditingText(null);
|
|
3360
|
+
setSelectedIds(EMPTY_SET);
|
|
3361
|
+
updateShapes(
|
|
3362
|
+
(prev) => prev.filter(
|
|
3363
|
+
(s) => !ids.has(s.id) && !(s.startBinding && ids.has(s.startBinding.id)) && !(s.endBinding && ids.has(s.endBinding.id))
|
|
3364
|
+
),
|
|
3365
|
+
{ commit: true }
|
|
3366
|
+
);
|
|
3367
|
+
}, [updateShapes]);
|
|
3368
|
+
const editingRef = useRef4(editingText);
|
|
3369
|
+
editingRef.current = editingText;
|
|
3370
|
+
useEffect7(() => {
|
|
3371
|
+
const root = rootRef.current;
|
|
3372
|
+
if (!root || !isEditable) return;
|
|
3373
|
+
const onKeyDown = (e) => {
|
|
3374
|
+
if (editingRef.current) return;
|
|
3375
|
+
const ids = selectedRef.current;
|
|
3376
|
+
if ((e.key === "Delete" || e.key === "Backspace") && ids.size) {
|
|
3377
|
+
e.preventDefault();
|
|
3378
|
+
e.stopPropagation();
|
|
3379
|
+
deleteSelection();
|
|
3380
|
+
} else if (e.key === "Enter" && ids.size === 1) {
|
|
3381
|
+
const [id] = ids;
|
|
3382
|
+
const shape = shapesRef.current.find((s) => s.id === id);
|
|
3383
|
+
if (shape) {
|
|
3384
|
+
e.preventDefault();
|
|
3385
|
+
e.stopPropagation();
|
|
3386
|
+
startTextEditing(shape.id, "text");
|
|
3387
|
+
}
|
|
3388
|
+
} else if (e.key === "a" && (e.metaKey || e.ctrlKey)) {
|
|
3389
|
+
e.preventDefault();
|
|
3390
|
+
e.stopPropagation();
|
|
3391
|
+
setSelectedIds(new Set(shapesRef.current.map((s) => s.id)));
|
|
3392
|
+
} else if (e.key === "Escape") {
|
|
3393
|
+
e.stopPropagation();
|
|
3394
|
+
setSelectedIds(EMPTY_SET);
|
|
3395
|
+
setTool("select");
|
|
3396
|
+
}
|
|
3397
|
+
};
|
|
3398
|
+
root.addEventListener("keydown", onKeyDown);
|
|
3399
|
+
return () => root.removeEventListener("keydown", onKeyDown);
|
|
3400
|
+
}, [isEditable, deleteSelection, startTextEditing]);
|
|
3401
|
+
const applyToSelection = useCallback(
|
|
3402
|
+
(patch) => {
|
|
3403
|
+
const ids = selectedRef.current;
|
|
3404
|
+
if (!ids.size) return;
|
|
3405
|
+
updateShapes((prev) => prev.map((s) => ids.has(s.id) ? patch(s) : s), { commit: true });
|
|
3406
|
+
},
|
|
3407
|
+
[updateShapes]
|
|
3408
|
+
);
|
|
3409
|
+
const applyColor = useCallback(
|
|
3410
|
+
(name) => {
|
|
3411
|
+
const preset = COLOR_PRESETS[name];
|
|
3412
|
+
setStroke(preset.stroke);
|
|
3413
|
+
setFill(preset.fill);
|
|
3414
|
+
applyToSelection(
|
|
3415
|
+
(s) => isBoxType(s.type) ? { ...s, stroke: preset.stroke, fill: preset.fill } : (
|
|
3416
|
+
// Connectors and text have nothing but their stroke to show
|
|
3417
|
+
{ ...s, stroke: preset.stroke === "transparent" ? STROKE_COLORS[0] : preset.stroke }
|
|
3418
|
+
)
|
|
3419
|
+
);
|
|
3420
|
+
},
|
|
3421
|
+
[applyToSelection]
|
|
3422
|
+
);
|
|
3423
|
+
const applyRouting = useCallback(
|
|
3424
|
+
(elbow) => applyToSelection(
|
|
3425
|
+
(s) => isConnectorType(s.type) ? { ...s, routing: elbow ? "elbow" : void 0, elbow: void 0, waypoints: void 0 } : s
|
|
3426
|
+
),
|
|
3427
|
+
[applyToSelection]
|
|
3428
|
+
);
|
|
3429
|
+
const applyDirection = useCallback(
|
|
3430
|
+
(bidirectional) => applyToSelection(
|
|
3431
|
+
(s) => s.type === "arrow" ? { ...s, bidirectional: bidirectional || void 0 } : s
|
|
3432
|
+
),
|
|
3433
|
+
[applyToSelection]
|
|
3434
|
+
);
|
|
3435
|
+
const applyWidth = useCallback(
|
|
3436
|
+
(next) => {
|
|
3437
|
+
setWidth(next);
|
|
3438
|
+
commit(shapesRef.current, { width: next });
|
|
3439
|
+
},
|
|
3440
|
+
[commit]
|
|
3441
|
+
);
|
|
3442
|
+
const addWaypoint = useCallback(
|
|
3443
|
+
(e, shapeId, segmentIndex, point) => {
|
|
3444
|
+
if (!isEditable || e.button !== 0) return;
|
|
3445
|
+
e.stopPropagation();
|
|
3446
|
+
capture(e);
|
|
3447
|
+
updateShapes(
|
|
3448
|
+
(prev) => prev.map((s) => {
|
|
3449
|
+
if (s.id !== shapeId || !isConnectorType(s.type)) return s;
|
|
3450
|
+
const interior = connectorPoints(s, prev).slice(1, -1);
|
|
3451
|
+
const waypoints = [...interior.slice(0, segmentIndex), point, ...interior.slice(segmentIndex)];
|
|
3452
|
+
return { ...s, waypoints, routing: void 0, elbow: void 0 };
|
|
3453
|
+
})
|
|
3454
|
+
);
|
|
3455
|
+
dragRef.current = { mode: "waypoint", id: shapeId, index: segmentIndex };
|
|
3456
|
+
},
|
|
3457
|
+
[isEditable, updateShapes]
|
|
3458
|
+
);
|
|
3459
|
+
const removeWaypoint = useCallback(
|
|
3460
|
+
(shapeId, index) => {
|
|
3461
|
+
updateShapes(
|
|
3462
|
+
(prev) => prev.map((s) => {
|
|
3463
|
+
if (s.id !== shapeId || !s.waypoints) return s;
|
|
3464
|
+
const waypoints = s.waypoints.filter((_, i) => i !== index);
|
|
3465
|
+
return { ...s, waypoints: waypoints.length ? waypoints : void 0 };
|
|
3466
|
+
}),
|
|
3467
|
+
{ commit: true }
|
|
3468
|
+
);
|
|
3469
|
+
},
|
|
3470
|
+
[updateShapes]
|
|
3471
|
+
);
|
|
3472
|
+
const commitText = useCallback(
|
|
3473
|
+
(id, field, value) => {
|
|
3474
|
+
setEditingText(null);
|
|
3475
|
+
rootRef.current?.focus({ preventScroll: true });
|
|
3476
|
+
updateShapes(
|
|
3477
|
+
(prev) => prev.flatMap((s) => {
|
|
3478
|
+
if (s.id !== id) return [s];
|
|
3479
|
+
if (s.type === "text") {
|
|
3480
|
+
return value.trim() === "" ? [] : [{ ...s, text: value, ...textBoxSize(value) }];
|
|
3481
|
+
}
|
|
3482
|
+
return [{ ...s, [field]: value.trim() === "" ? void 0 : value }];
|
|
3483
|
+
}),
|
|
3484
|
+
{ commit: true }
|
|
3485
|
+
);
|
|
3486
|
+
},
|
|
3487
|
+
[updateShapes]
|
|
3488
|
+
);
|
|
3489
|
+
const copyMermaid = useCallback(async () => {
|
|
3490
|
+
const payload = { version: 2, canvasHeight: heightRef.current, shapes: shapesRef.current };
|
|
3491
|
+
try {
|
|
3492
|
+
await navigator.clipboard.writeText(drawingToMermaid(payload));
|
|
3493
|
+
return true;
|
|
3494
|
+
} catch {
|
|
3495
|
+
return false;
|
|
3496
|
+
}
|
|
3497
|
+
}, []);
|
|
3498
|
+
const editingShape = shapes.find((s) => s.id === editingText?.id) ?? null;
|
|
3499
|
+
const hoverBox = hoverBoxId ? shapes.find((s) => s.id === hoverBoxId) : null;
|
|
3500
|
+
const propStroke = single?.stroke ?? selection[0]?.stroke ?? stroke;
|
|
3501
|
+
const propFill = single?.fill ?? selection[0]?.fill ?? fill;
|
|
3502
|
+
const canvasCursor = tool === "select" ? "default" : tool === "text" ? "text" : "crosshair";
|
|
3503
|
+
return /* @__PURE__ */ jsxs8(
|
|
3504
|
+
"div",
|
|
3505
|
+
{
|
|
3506
|
+
ref: rootRef,
|
|
3507
|
+
className: `zui-drawing-canvas ${WIDTH_CLASS[width]} ${isEditable ? "is-editable" : ""}`,
|
|
3508
|
+
tabIndex: isEditable ? 0 : void 0,
|
|
3509
|
+
onFocus: () => editor.dispatchCommand(DRAWING_FOCUS_COMMAND, nodeKey),
|
|
3510
|
+
onBlur: (e) => {
|
|
3511
|
+
if (rootRef.current?.contains(e.relatedTarget)) return;
|
|
3512
|
+
editor.dispatchCommand(DRAWING_FOCUS_COMMAND, null);
|
|
3513
|
+
},
|
|
3514
|
+
children: [
|
|
3515
|
+
isEditable && /* @__PURE__ */ jsx9("div", { className: "zui-drawing-toolbar", onPointerDown: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx9(
|
|
3516
|
+
DrawingToolbar,
|
|
3517
|
+
{
|
|
3518
|
+
tool,
|
|
3519
|
+
onToolChange: (t) => {
|
|
3520
|
+
setTool(t);
|
|
3521
|
+
if (t !== "select") setSelectedIds(EMPTY_SET);
|
|
3522
|
+
},
|
|
3523
|
+
width,
|
|
3524
|
+
onWidthChange: applyWidth,
|
|
3525
|
+
onCopyMermaid: copyMermaid,
|
|
3526
|
+
properties: (
|
|
3527
|
+
// Inline in the same row (so the stage never shifts); shown
|
|
3528
|
+
// only while the canvas has focus (see .zui-drawing-props-host)
|
|
3529
|
+
/* @__PURE__ */ jsx9("div", { className: "zui-drawing-props-host", children: /* @__PURE__ */ jsx9(
|
|
3530
|
+
PropertyBar,
|
|
3531
|
+
{
|
|
3532
|
+
selection,
|
|
3533
|
+
stroke: propStroke,
|
|
3534
|
+
fill: propFill,
|
|
3535
|
+
onColor: applyColor,
|
|
3536
|
+
onRouting: applyRouting,
|
|
3537
|
+
onDirection: applyDirection,
|
|
3538
|
+
onDelete: deleteSelection
|
|
3539
|
+
}
|
|
3540
|
+
) })
|
|
3541
|
+
)
|
|
3542
|
+
}
|
|
3543
|
+
) }),
|
|
3544
|
+
/* @__PURE__ */ jsxs8("div", { className: "zui-drawing-stage", children: [
|
|
3545
|
+
/* @__PURE__ */ jsxs8(
|
|
3546
|
+
"svg",
|
|
3547
|
+
{
|
|
3548
|
+
ref: svgRef,
|
|
3549
|
+
className: "zui-drawing-surface",
|
|
3550
|
+
style: {
|
|
3551
|
+
height: logicalWidth ? void 0 : canvasHeight,
|
|
3552
|
+
width: logicalWidth ?? void 0,
|
|
3553
|
+
maxWidth: "100%",
|
|
3554
|
+
aspectRatio: logicalWidth ? `${logicalWidth} / ${canvasHeight}` : void 0,
|
|
3555
|
+
cursor: canvasCursor
|
|
3556
|
+
},
|
|
3557
|
+
viewBox: logicalWidth ? `0 0 ${logicalWidth} ${canvasHeight}` : void 0,
|
|
3558
|
+
preserveAspectRatio: "xMinYMin meet",
|
|
3559
|
+
onPointerDown: handleBackgroundPointerDown,
|
|
3560
|
+
onPointerMove: handlePointerMove,
|
|
3561
|
+
onPointerUp: handlePointerUp,
|
|
3562
|
+
onPointerCancel: handlePointerUp,
|
|
3563
|
+
onDoubleClick: (e) => {
|
|
3564
|
+
if (!isEditable) return;
|
|
3565
|
+
const target = e.target.closest("[data-shape-id]");
|
|
3566
|
+
const id = target?.getAttribute("data-shape-id");
|
|
3567
|
+
const shape = id ? shapes.find((s) => s.id === id) : null;
|
|
3568
|
+
if (!shape) return;
|
|
3569
|
+
if (shape.type === "text" || isConnectorType(shape.type)) {
|
|
3570
|
+
startTextEditing(shape.id, "text");
|
|
3571
|
+
} else {
|
|
3572
|
+
startTextEditing(shape.id, slotAt(shape, getPoint(e).y));
|
|
3573
|
+
}
|
|
3574
|
+
},
|
|
3575
|
+
children: [
|
|
3576
|
+
shapes.map((shape) => /* @__PURE__ */ jsxs8(
|
|
3577
|
+
"g",
|
|
3578
|
+
{
|
|
3579
|
+
"data-shape-id": shape.id,
|
|
3580
|
+
className: `zui-drawing-shape ${selectedIds.has(shape.id) ? "is-selected" : ""}`,
|
|
3581
|
+
style: { cursor: isEditable && tool === "select" ? "move" : void 0 },
|
|
3582
|
+
onPointerDown: (e) => handleShapePointerDown(e, shape),
|
|
3583
|
+
children: [
|
|
3584
|
+
/* @__PURE__ */ jsx9(HitArea, { shape, points: paths.get(shape.id) }),
|
|
3585
|
+
shape.id === editingText?.id && shape.type === "text" ? null : /* @__PURE__ */ jsx9(
|
|
3586
|
+
ShapeView,
|
|
3587
|
+
{
|
|
3588
|
+
shape,
|
|
3589
|
+
points: paths.get(shape.id),
|
|
3590
|
+
hideField: shape.id === editingText?.id ? editingText.field : null,
|
|
3591
|
+
showHints: isEditable && tool === "select" && single?.id === shape.id && shape.id !== editingText?.id
|
|
3592
|
+
}
|
|
3593
|
+
)
|
|
3594
|
+
]
|
|
3595
|
+
},
|
|
3596
|
+
shape.id
|
|
3597
|
+
)),
|
|
3598
|
+
hoverBox && /* @__PURE__ */ jsx9(
|
|
3599
|
+
"polygon",
|
|
3600
|
+
{
|
|
3601
|
+
className: "zui-drawing-selection zui-drawing-bind-target",
|
|
3602
|
+
points: boxOutline(hoverBox).map((p) => `${p.x},${p.y}`).join(" "),
|
|
3603
|
+
fill: "currentColor",
|
|
3604
|
+
fillOpacity: 0.06,
|
|
3605
|
+
stroke: "currentColor",
|
|
3606
|
+
strokeWidth: 2,
|
|
3607
|
+
strokeLinejoin: "round",
|
|
3608
|
+
pointerEvents: "none"
|
|
3609
|
+
}
|
|
3610
|
+
),
|
|
3611
|
+
single && isEditable && !editingText && /* @__PURE__ */ jsx9(
|
|
3612
|
+
SelectionOverlay,
|
|
3613
|
+
{
|
|
3614
|
+
shape: single,
|
|
3615
|
+
points: paths.get(single.id) ?? [],
|
|
3616
|
+
onHandlePointerDown: handleHandlePointerDown,
|
|
3617
|
+
onWaypointAdd: addWaypoint,
|
|
3618
|
+
onWaypointRemove: removeWaypoint
|
|
3619
|
+
}
|
|
3620
|
+
),
|
|
3621
|
+
selection.length > 1 && isEditable && /* @__PURE__ */ jsx9(GroupSelectionOverlay, { shapes: selection, paths }),
|
|
3622
|
+
marquee && /* @__PURE__ */ jsx9(MarqueeOverlay, { rect: marqueeRect(marquee.origin, marquee.current) })
|
|
3623
|
+
]
|
|
3624
|
+
}
|
|
3625
|
+
),
|
|
3626
|
+
isEditable && shapes.length === 0 && /* @__PURE__ */ jsx9("div", { className: "zui-drawing-empty", "aria-hidden": "true", children: "Pick a shape above, then click or drag on the canvas" }),
|
|
3627
|
+
editingShape && editingText && /* @__PURE__ */ jsx9(
|
|
3628
|
+
"div",
|
|
3629
|
+
{
|
|
3630
|
+
className: "zui-drawing-overlay",
|
|
3631
|
+
style: {
|
|
3632
|
+
transform: `scale(${scale})`,
|
|
3633
|
+
width: logicalWidth ?? void 0,
|
|
3634
|
+
height: logicalWidth ? canvasHeight : void 0
|
|
3635
|
+
},
|
|
3636
|
+
children: /* @__PURE__ */ jsx9(
|
|
3637
|
+
TextEditOverlay,
|
|
3638
|
+
{
|
|
3639
|
+
shape: editingShape,
|
|
3640
|
+
field: editingText.field,
|
|
3641
|
+
points: paths.get(editingShape.id),
|
|
3642
|
+
onCommit: commitText
|
|
3643
|
+
},
|
|
3644
|
+
`${editingShape.id}:${editingText.field}`
|
|
3645
|
+
)
|
|
3646
|
+
}
|
|
3647
|
+
),
|
|
3648
|
+
isEditable && /* @__PURE__ */ jsx9(
|
|
3649
|
+
HeightHandle,
|
|
3650
|
+
{
|
|
3651
|
+
height: canvasHeight,
|
|
3652
|
+
scale,
|
|
3653
|
+
onChange: setCanvasHeight,
|
|
3654
|
+
onCommit: (h) => commit(shapesRef.current, { height: h })
|
|
3655
|
+
}
|
|
3656
|
+
)
|
|
3657
|
+
] })
|
|
3658
|
+
]
|
|
3659
|
+
}
|
|
3660
|
+
);
|
|
3547
3661
|
}
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3662
|
+
function HeightHandle({
|
|
3663
|
+
height,
|
|
3664
|
+
scale,
|
|
3665
|
+
onChange,
|
|
3666
|
+
onCommit
|
|
3667
|
+
}) {
|
|
3668
|
+
const dragRef = useRef4(null);
|
|
3669
|
+
const latestRef = useRef4(height);
|
|
3670
|
+
latestRef.current = height;
|
|
3671
|
+
const clamp = (h) => Math.round(Math.min(MAX_HEIGHT, Math.max(MIN_HEIGHT, h)));
|
|
3672
|
+
return /* @__PURE__ */ jsx9(
|
|
3673
|
+
"div",
|
|
3558
3674
|
{
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
}
|
|
3568
|
-
}
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
properties: {
|
|
3581
|
-
direction: {
|
|
3582
|
-
enum: ["right", "down"],
|
|
3583
|
-
description: 'Auto-layout flow direction. Boxes connected a\u2192b are placed in successive ranks along this axis ("right" = columns left to right, "down" = rows top to bottom). Default "right"'
|
|
3584
|
-
},
|
|
3585
|
-
canvasWidth: {
|
|
3586
|
-
type: "number",
|
|
3587
|
-
minimum: 120,
|
|
3588
|
-
description: "Logical canvas width in px. Omit for a fluid canvas in CSS pixels"
|
|
3589
|
-
},
|
|
3590
|
-
canvasHeight: {
|
|
3591
|
-
type: "number",
|
|
3592
|
-
minimum: 80,
|
|
3593
|
-
description: "Canvas height in px. Omit to fit the content"
|
|
3594
|
-
},
|
|
3595
|
-
width: {
|
|
3596
|
-
enum: ["full", "text", "content"],
|
|
3597
|
-
description: 'Block width: "text" aligns the drawing with the text column (same as "full" unless the app sets a text measure); "content" fits the drawing to its shapes and left-aligns it with the text; omit to span the editor'
|
|
3598
|
-
},
|
|
3599
|
-
boxes: {
|
|
3600
|
-
type: "array",
|
|
3601
|
-
items: { $ref: "#/definitions/box" },
|
|
3602
|
-
description: "Card-like shapes carrying up to three text slots. Order is the render order and the stacking order within a layout rank"
|
|
3603
|
-
},
|
|
3604
|
-
connectors: {
|
|
3605
|
-
type: "array",
|
|
3606
|
-
items: { $ref: "#/definitions/connector" },
|
|
3607
|
-
description: "Arrows and lines between boxes. Connectors whose from/to id does not match a box are dropped"
|
|
3608
|
-
},
|
|
3609
|
-
texts: {
|
|
3610
|
-
type: "array",
|
|
3611
|
-
items: { $ref: "#/definitions/text" },
|
|
3612
|
-
description: "Free-floating annotations not attached to any box. Prefer box slots (label/text/footer) when the text belongs to a box"
|
|
3613
|
-
}
|
|
3614
|
-
},
|
|
3615
|
-
definitions: {
|
|
3616
|
-
box: {
|
|
3617
|
-
type: "object",
|
|
3618
|
-
required: ["id"],
|
|
3619
|
-
additionalProperties: false,
|
|
3620
|
-
properties: {
|
|
3621
|
-
id: { type: "string", description: "Unique within the drawing; connectors reference it" },
|
|
3622
|
-
type: {
|
|
3623
|
-
enum: BOX_TYPES,
|
|
3624
|
-
description: 'Shape: rect (default), ellipse, diamond (decision), note (sticky note, yellow by default), cylinder (database), cloud (external system), queue (message queue), actor (stick figure; only the "text" slot renders, below the figure)'
|
|
3625
|
-
},
|
|
3626
|
-
label: { type: "string", description: "Small bold heading at the top of the box" },
|
|
3627
|
-
text: {
|
|
3628
|
-
type: "string",
|
|
3629
|
-
description: 'Main content, centered. Wraps automatically; "\\n" forces a line break'
|
|
3630
|
-
},
|
|
3631
|
-
footer: { type: "string", description: "Small dim line at the bottom of the box" },
|
|
3632
|
-
x: { type: "number", description: "Top-left x in px. Omit (with y) to let the layout place the box" },
|
|
3633
|
-
y: { type: "number", description: "Top-left y in px. Omit (with x) to let the layout place the box" },
|
|
3634
|
-
w: {
|
|
3635
|
-
type: "number",
|
|
3636
|
-
exclusiveMinimum: 0,
|
|
3637
|
-
description: "Width in px. Omit to size from the text (never smaller than the type default)"
|
|
3638
|
-
},
|
|
3639
|
-
h: {
|
|
3640
|
-
type: "number",
|
|
3641
|
-
exclusiveMinimum: 0,
|
|
3642
|
-
description: "Height in px. Omit to size from the text (never smaller than the type default)"
|
|
3643
|
-
},
|
|
3644
|
-
color: COLOR_SCHEMA,
|
|
3645
|
-
stroke: { type: "string", description: "CSS color of the outline and text; overrides the preset" },
|
|
3646
|
-
fill: {
|
|
3647
|
-
type: "string",
|
|
3648
|
-
description: 'CSS color of the interior ("transparent" for none); overrides the preset'
|
|
3649
|
-
}
|
|
3650
|
-
}
|
|
3651
|
-
},
|
|
3652
|
-
connector: {
|
|
3653
|
-
type: "object",
|
|
3654
|
-
required: ["from", "to"],
|
|
3655
|
-
additionalProperties: false,
|
|
3656
|
-
properties: {
|
|
3657
|
-
id: { type: "string", description: "Optional; generated when omitted" },
|
|
3658
|
-
type: {
|
|
3659
|
-
enum: CONNECTOR_TYPES,
|
|
3660
|
-
description: 'arrow (default; arrowhead at "to") or line (no arrowheads)'
|
|
3661
|
-
},
|
|
3662
|
-
from: END_SCHEMA,
|
|
3663
|
-
to: END_SCHEMA,
|
|
3664
|
-
text: { type: "string", description: "Label rendered at the middle of the connector" },
|
|
3665
|
-
bidirectional: {
|
|
3666
|
-
type: "boolean",
|
|
3667
|
-
description: "Arrow only: arrowheads on both ends"
|
|
3668
|
-
},
|
|
3669
|
-
routing: {
|
|
3670
|
-
enum: ["straight", "elbow"],
|
|
3671
|
-
description: "straight (default) draws a direct line; elbow draws a right-angled path routed around the boxes"
|
|
3672
|
-
},
|
|
3673
|
-
color: COLOR_SCHEMA,
|
|
3674
|
-
stroke: { type: "string", description: "CSS color of the line and label; overrides the preset" }
|
|
3675
|
-
}
|
|
3676
|
-
},
|
|
3677
|
-
text: {
|
|
3678
|
-
type: "object",
|
|
3679
|
-
required: ["x", "y", "text"],
|
|
3680
|
-
additionalProperties: false,
|
|
3681
|
-
properties: {
|
|
3682
|
-
id: { type: "string", description: "Optional; generated when omitted" },
|
|
3683
|
-
x: { type: "number", description: "Top-left x of the first line, in px" },
|
|
3684
|
-
y: { type: "number", description: "Top-left y of the first line, in px" },
|
|
3685
|
-
text: { type: "string", description: 'Content; "\\n" forces a line break' },
|
|
3686
|
-
color: COLOR_SCHEMA,
|
|
3687
|
-
stroke: { type: "string", description: "CSS color of the text; overrides the preset" }
|
|
3688
|
-
}
|
|
3675
|
+
className: "zui-drawing-resize",
|
|
3676
|
+
title: "Drag to resize the canvas",
|
|
3677
|
+
role: "separator",
|
|
3678
|
+
"aria-orientation": "horizontal",
|
|
3679
|
+
onPointerDown: (e) => {
|
|
3680
|
+
e.preventDefault();
|
|
3681
|
+
e.stopPropagation();
|
|
3682
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
3683
|
+
dragRef.current = { startY: e.clientY, orig: height };
|
|
3684
|
+
},
|
|
3685
|
+
onPointerMove: (e) => {
|
|
3686
|
+
const drag = dragRef.current;
|
|
3687
|
+
if (!drag) return;
|
|
3688
|
+
onChange(clamp(drag.orig + (e.clientY - drag.startY) / Math.max(scale, 0.05)));
|
|
3689
|
+
},
|
|
3690
|
+
onPointerUp: () => {
|
|
3691
|
+
if (!dragRef.current) return;
|
|
3692
|
+
dragRef.current = null;
|
|
3693
|
+
onCommit(latestRef.current);
|
|
3694
|
+
},
|
|
3695
|
+
children: /* @__PURE__ */ jsx9(Icon, { size: 14, children: UI_ICONS.grip })
|
|
3689
3696
|
}
|
|
3690
|
-
|
|
3691
|
-
}
|
|
3697
|
+
);
|
|
3698
|
+
}
|
|
3692
3699
|
|
|
3693
3700
|
// src/nodes/DrawingNode.tsx
|
|
3694
3701
|
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
@@ -4726,24 +4733,7 @@ function HistoryButtons() {
|
|
|
4726
4733
|
}
|
|
4727
4734
|
|
|
4728
4735
|
// src/plugins/TableSettingsPlugin.tsx
|
|
4729
|
-
import { jsx as jsx14
|
|
4730
|
-
var DENSITY_OPTIONS = [
|
|
4731
|
-
{
|
|
4732
|
-
density: "compact",
|
|
4733
|
-
label: "Compact rows",
|
|
4734
|
-
icon: /* @__PURE__ */ jsx14("path", { d: "M3 4.5h14M3 8.17h14M3 11.83h14M3 15.5h14" })
|
|
4735
|
-
},
|
|
4736
|
-
{
|
|
4737
|
-
density: "comfortable",
|
|
4738
|
-
label: "Comfortable rows",
|
|
4739
|
-
icon: /* @__PURE__ */ jsx14("path", { d: "M3 4.5h14M3 10h14M3 15.5h14" })
|
|
4740
|
-
},
|
|
4741
|
-
{
|
|
4742
|
-
density: "spacious",
|
|
4743
|
-
label: "Spacious rows",
|
|
4744
|
-
icon: /* @__PURE__ */ jsx14("path", { d: "M3 5h14M3 15h14" })
|
|
4745
|
-
}
|
|
4746
|
-
];
|
|
4736
|
+
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
4747
4737
|
function Icon3({ children }) {
|
|
4748
4738
|
return /* @__PURE__ */ jsx14(
|
|
4749
4739
|
"svg",
|
|
@@ -4806,42 +4796,29 @@ function TableSettingsPlugin() {
|
|
|
4806
4796
|
[editor]
|
|
4807
4797
|
);
|
|
4808
4798
|
if (!isEditable || !anchor || !settings) return null;
|
|
4809
|
-
return /* @__PURE__ */
|
|
4799
|
+
return /* @__PURE__ */ jsx14(
|
|
4810
4800
|
"div",
|
|
4811
4801
|
{
|
|
4812
4802
|
className: "zui-table-settings",
|
|
4813
4803
|
role: "toolbar",
|
|
4814
4804
|
"aria-label": "Table settings",
|
|
4815
4805
|
style: { top: anchor.top, right: anchor.right },
|
|
4816
|
-
children:
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
)),
|
|
4827
|
-
/* @__PURE__ */ jsx14(ToolbarDivider, {}),
|
|
4828
|
-
DENSITY_OPTIONS.map(({ density, label, icon }) => /* @__PURE__ */ jsx14(
|
|
4829
|
-
ToolbarButton,
|
|
4830
|
-
{
|
|
4831
|
-
label,
|
|
4832
|
-
active: settings.density === density,
|
|
4833
|
-
onClick: () => update({ density }),
|
|
4834
|
-
children: /* @__PURE__ */ jsx14(Icon3, { children: icon })
|
|
4835
|
-
},
|
|
4836
|
-
density
|
|
4837
|
-
))
|
|
4838
|
-
]
|
|
4806
|
+
children: LAYOUT_OPTIONS.map(({ preset, label, icon, width, density }) => /* @__PURE__ */ jsx14(
|
|
4807
|
+
ToolbarButton,
|
|
4808
|
+
{
|
|
4809
|
+
label,
|
|
4810
|
+
active: layoutPreset(settings.width) === preset,
|
|
4811
|
+
onClick: () => update({ width, density }),
|
|
4812
|
+
children: /* @__PURE__ */ jsx14(Icon3, { children: icon })
|
|
4813
|
+
},
|
|
4814
|
+
preset
|
|
4815
|
+
))
|
|
4839
4816
|
}
|
|
4840
4817
|
);
|
|
4841
4818
|
}
|
|
4842
4819
|
|
|
4843
4820
|
// src/EditorContent.tsx
|
|
4844
|
-
import { jsx as jsx15, jsxs as
|
|
4821
|
+
import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
4845
4822
|
function EditorContent({
|
|
4846
4823
|
placeholder = "Start writing...",
|
|
4847
4824
|
foldable = true,
|
|
@@ -4862,8 +4839,8 @@ function EditorContent({
|
|
|
4862
4839
|
}
|
|
4863
4840
|
);
|
|
4864
4841
|
}
|
|
4865
|
-
return /* @__PURE__ */
|
|
4866
|
-
/* @__PURE__ */
|
|
4842
|
+
return /* @__PURE__ */ jsxs11("div", { className: "zui-text-editor-body", children: [
|
|
4843
|
+
/* @__PURE__ */ jsxs11("div", { className: "zui-text-editor-main", children: [
|
|
4867
4844
|
/* @__PURE__ */ jsx15(
|
|
4868
4845
|
RichTextPlugin,
|
|
4869
4846
|
{
|
|
@@ -4891,7 +4868,7 @@ import { useLexicalComposerContext as useLexicalComposerContext10 } from "@lexic
|
|
|
4891
4868
|
import {
|
|
4892
4869
|
TableOfContentsPlugin
|
|
4893
4870
|
} from "@lexical/react/LexicalTableOfContentsPlugin";
|
|
4894
|
-
import { jsx as jsx16, jsxs as
|
|
4871
|
+
import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
4895
4872
|
var INDENT_PER_LEVEL = {
|
|
4896
4873
|
h1: 0,
|
|
4897
4874
|
h2: 1,
|
|
@@ -4947,7 +4924,7 @@ function OutlinePlugin() {
|
|
|
4947
4924
|
for (const [key] of entries) {
|
|
4948
4925
|
editor.getElementByKey(key)?.setAttribute("data-outline-key", key);
|
|
4949
4926
|
}
|
|
4950
|
-
return /* @__PURE__ */
|
|
4927
|
+
return /* @__PURE__ */ jsxs12(
|
|
4951
4928
|
"div",
|
|
4952
4929
|
{
|
|
4953
4930
|
className: `zui-text-editor-outline ${collapsed ? "is-collapsed" : ""}`,
|
|
@@ -4976,7 +4953,7 @@ function OutlinePlugin() {
|
|
|
4976
4953
|
)
|
|
4977
4954
|
}
|
|
4978
4955
|
),
|
|
4979
|
-
!collapsed && /* @__PURE__ */
|
|
4956
|
+
!collapsed && /* @__PURE__ */ jsxs12("nav", { className: "zui-text-editor-outline-list", "aria-label": "Table of contents", children: [
|
|
4980
4957
|
entries.length === 0 && /* @__PURE__ */ jsx16("div", { className: "zui-text-editor-outline-empty", children: "No headings" }),
|
|
4981
4958
|
entries.map(([key, text, tag]) => /* @__PURE__ */ jsx16(
|
|
4982
4959
|
"button",
|
|
@@ -4999,7 +4976,7 @@ function OutlinePlugin() {
|
|
|
4999
4976
|
}
|
|
5000
4977
|
|
|
5001
4978
|
// src/MarkdownEditor.tsx
|
|
5002
|
-
import { jsx as jsx17, jsxs as
|
|
4979
|
+
import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
5003
4980
|
var DEFAULT_ITEMS = {
|
|
5004
4981
|
format: /* @__PURE__ */ jsx17(FormatButtons, {}),
|
|
5005
4982
|
insert: /* @__PURE__ */ jsx17(InsertButtons, {}),
|
|
@@ -5020,7 +4997,7 @@ function MarkdownEditor({
|
|
|
5020
4997
|
foldable = true,
|
|
5021
4998
|
...rootProps
|
|
5022
4999
|
}) {
|
|
5023
|
-
return /* @__PURE__ */
|
|
5000
|
+
return /* @__PURE__ */ jsxs13(EditorRoot, { ...rootProps, children: [
|
|
5024
5001
|
toolbar === true && /* @__PURE__ */ jsx17(EditorToolbar, {}),
|
|
5025
5002
|
typeof toolbar === "function" && toolbar(DEFAULT_ITEMS),
|
|
5026
5003
|
/* @__PURE__ */ jsx17(EditorContent, { placeholder, foldable, children: outline && /* @__PURE__ */ jsx17(OutlinePlugin, {}) })
|