@zuilib/text-editor 0.7.0 → 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 +28 -0
- package/DRAWING_FORMAT.md +3 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2087 -2092
- package/dist/styles.css +18 -37
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1543,2134 +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
|
-
|
|
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
|
+
}
|
|
2243
1922
|
}
|
|
2244
|
-
}
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
2309
2044
|
}
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
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",
|
|
2313
2058
|
{
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
onBlur: () => onCommit(shape.id, field, value),
|
|
2322
|
-
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
|
|
2323
2066
|
}
|
|
2324
2067
|
);
|
|
2325
2068
|
}
|
|
2326
|
-
|
|
2327
|
-
// src/drawing/canvas/Toolbar.tsx
|
|
2328
|
-
import { useEffect as useEffect6, useRef as useRef3, useState as useState2 } from "react";
|
|
2329
|
-
|
|
2330
|
-
// src/components/blockWidthOptions.tsx
|
|
2331
|
-
import { Fragment, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2332
|
-
var BLOCK_WIDTH_OPTIONS = [
|
|
2333
|
-
{
|
|
2334
|
-
width: "full",
|
|
2335
|
-
label: "Full width",
|
|
2336
|
-
icon: /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
2337
|
-
/* @__PURE__ */ jsx7("path", { d: "M3 4v12M17 4v12" }),
|
|
2338
|
-
/* @__PURE__ */ jsx7("path", { d: "M6 10h8M8.5 7.5L6 10l2.5 2.5M11.5 7.5L14 10l-2.5 2.5" })
|
|
2339
|
-
] })
|
|
2340
|
-
},
|
|
2341
|
-
{
|
|
2342
|
-
width: "text",
|
|
2343
|
-
label: "Text width",
|
|
2344
|
-
icon: /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
2345
|
-
/* @__PURE__ */ jsx7("path", { d: "M3 4v12M17 4v12" }),
|
|
2346
|
-
/* @__PURE__ */ jsx7("path", { d: "M6.5 7h7M6.5 10h7M6.5 13h4.5" })
|
|
2347
|
-
] })
|
|
2348
|
-
},
|
|
2349
|
-
{
|
|
2350
|
-
width: "content",
|
|
2351
|
-
label: "Content width",
|
|
2352
|
-
icon: /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
2353
|
-
/* @__PURE__ */ jsx7("path", { d: "M3 4v12M17 4v12" }),
|
|
2354
|
-
/* @__PURE__ */ jsx7("path", { d: "M5.5 10h3M11.5 10h3M6.5 7.5L9 10l-2.5 2.5M13.5 7.5L11 10l2.5 2.5" })
|
|
2355
|
-
] })
|
|
2356
|
-
}
|
|
2357
|
-
];
|
|
2358
|
-
|
|
2359
|
-
// src/drawing/canvas/Toolbar.tsx
|
|
2360
|
-
import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2361
|
-
var PRIMARY_TOOLS = [
|
|
2362
|
-
{ tool: "select", label: "Select" },
|
|
2363
|
-
{ tool: "rect", label: "Rectangle" },
|
|
2364
|
-
{ tool: "ellipse", label: "Ellipse" },
|
|
2365
|
-
{ tool: "diamond", label: "Diamond" },
|
|
2366
|
-
{ tool: "arrow", label: "Arrow" },
|
|
2367
|
-
{ tool: "line", label: "Line" },
|
|
2368
|
-
{ tool: "text", label: "Text" }
|
|
2369
|
-
];
|
|
2370
|
-
var MORE_SHAPES = ["note", "cylinder", "cloud", "queue", "actor"];
|
|
2371
|
-
function ToolButton({
|
|
2069
|
+
function OptionButton({
|
|
2372
2070
|
label,
|
|
2373
2071
|
active,
|
|
2374
2072
|
onClick,
|
|
2375
2073
|
children,
|
|
2376
|
-
className
|
|
2377
|
-
pressed
|
|
2074
|
+
className
|
|
2378
2075
|
}) {
|
|
2379
|
-
return /* @__PURE__ */
|
|
2076
|
+
return /* @__PURE__ */ jsx2(
|
|
2380
2077
|
"button",
|
|
2381
2078
|
{
|
|
2382
2079
|
type: "button",
|
|
2383
2080
|
title: label,
|
|
2384
2081
|
"aria-label": label,
|
|
2385
|
-
"aria-pressed":
|
|
2082
|
+
"aria-pressed": active,
|
|
2386
2083
|
className: `zui-drawing-tool ${active ? "is-active" : ""} ${className ?? ""}`,
|
|
2387
2084
|
onClick,
|
|
2388
|
-
children: /* @__PURE__ */
|
|
2085
|
+
children: /* @__PURE__ */ jsx2(Icon, { children })
|
|
2389
2086
|
}
|
|
2390
2087
|
);
|
|
2391
2088
|
}
|
|
2392
|
-
function
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2089
|
+
function PropertyBar({
|
|
2090
|
+
selection,
|
|
2091
|
+
stroke,
|
|
2092
|
+
fill,
|
|
2093
|
+
onColor,
|
|
2094
|
+
onRouting,
|
|
2095
|
+
onDirection,
|
|
2096
|
+
onDelete
|
|
2398
2097
|
}) {
|
|
2399
|
-
const
|
|
2400
|
-
const
|
|
2401
|
-
const
|
|
2402
|
-
const
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
const moreActive = MORE_SHAPES.includes(tool);
|
|
2419
|
-
const moreIcon = moreActive ? SHAPE_ICONS[tool] : SHAPE_ICONS[lastMore];
|
|
2420
|
-
return /* @__PURE__ */ jsxs7(Fragment2, { children: [
|
|
2421
|
-
/* @__PURE__ */ jsxs7("div", { className: "zui-drawing-toolbar-group", role: "toolbar", "aria-label": "Drawing tools", children: [
|
|
2422
|
-
PRIMARY_TOOLS.map(({ tool: t, label }) => /* @__PURE__ */ jsx8(ToolButton, { label, active: tool === t, onClick: () => onToolChange(t), children: SHAPE_ICONS[t] }, t)),
|
|
2423
|
-
/* @__PURE__ */ jsxs7("div", { className: "zui-drawing-more", ref: moreRef, children: [
|
|
2424
|
-
/* @__PURE__ */ jsxs7(
|
|
2425
|
-
"button",
|
|
2426
|
-
{
|
|
2427
|
-
type: "button",
|
|
2428
|
-
title: "More shapes",
|
|
2429
|
-
"aria-label": "More shapes",
|
|
2430
|
-
"aria-haspopup": "menu",
|
|
2431
|
-
"aria-expanded": moreOpen,
|
|
2432
|
-
className: `zui-drawing-tool zui-drawing-tool-more ${moreActive ? "is-active" : ""}`,
|
|
2433
|
-
onClick: () => setMoreOpen((open) => !open),
|
|
2434
|
-
children: [
|
|
2435
|
-
/* @__PURE__ */ jsx8(Icon, { children: moreIcon }),
|
|
2436
|
-
/* @__PURE__ */ jsx8("span", { className: "zui-drawing-caret", "aria-hidden": "true" })
|
|
2437
|
-
]
|
|
2438
|
-
}
|
|
2439
|
-
),
|
|
2440
|
-
moreOpen && /* @__PURE__ */ jsx8("div", { className: "zui-drawing-popover", role: "menu", children: MORE_SHAPES.map((type) => /* @__PURE__ */ jsxs7(
|
|
2441
|
-
"button",
|
|
2442
|
-
{
|
|
2443
|
-
type: "button",
|
|
2444
|
-
role: "menuitemradio",
|
|
2445
|
-
"aria-checked": tool === type,
|
|
2446
|
-
className: `zui-drawing-popover-item ${tool === type ? "is-active" : ""}`,
|
|
2447
|
-
onClick: () => {
|
|
2448
|
-
setLastMore(type);
|
|
2449
|
-
onToolChange(type);
|
|
2450
|
-
setMoreOpen(false);
|
|
2451
|
-
},
|
|
2452
|
-
children: [
|
|
2453
|
-
/* @__PURE__ */ jsx8(Icon, { size: 18, children: SHAPE_ICONS[type] }),
|
|
2454
|
-
/* @__PURE__ */ jsx8("span", { children: BOX_DEFINITIONS[type].label })
|
|
2455
|
-
]
|
|
2456
|
-
},
|
|
2457
|
-
type
|
|
2458
|
-
)) })
|
|
2459
|
-
] })
|
|
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 })
|
|
2460
2117
|
] }),
|
|
2461
|
-
/* @__PURE__ */
|
|
2462
|
-
/* @__PURE__ */
|
|
2463
|
-
|
|
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",
|
|
2464
2174
|
{
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
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);
|
|
2473
2193
|
},
|
|
2474
|
-
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
|
+
})
|
|
2475
2223
|
}
|
|
2476
2224
|
),
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
}
|
|
2491
|
-
|
|
2492
|
-
// src/drawing/canvas/DrawingCanvas.tsx
|
|
2493
|
-
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2494
|
-
var MIN_HEIGHT = 120;
|
|
2495
|
-
var MAX_HEIGHT = 1200;
|
|
2496
|
-
var WIDTH_CLASS = {
|
|
2497
|
-
full: "",
|
|
2498
|
-
text: "is-text-width",
|
|
2499
|
-
content: "is-content-width"
|
|
2500
|
-
};
|
|
2501
|
-
var MIN_CONTENT_WIDTH = 240;
|
|
2502
|
-
var CONTENT_WIDTH_PADDING = 40;
|
|
2503
|
-
var EMPTY_SET = /* @__PURE__ */ new Set();
|
|
2504
|
-
function contentWidth(shapes, paths) {
|
|
2505
|
-
let right = 0;
|
|
2506
|
-
for (const shape of shapes) {
|
|
2507
|
-
if (isConnectorType(shape.type)) {
|
|
2508
|
-
for (const p of paths.get(shape.id) ?? []) right = Math.max(right, p.x);
|
|
2509
|
-
} else {
|
|
2510
|
-
const b = bbox(shape);
|
|
2511
|
-
right = Math.max(right, b.x + b.w);
|
|
2512
|
-
}
|
|
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
|
+
] });
|
|
2513
2239
|
}
|
|
2514
|
-
|
|
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
|
+
))
|
|
2262
|
+
] });
|
|
2515
2263
|
}
|
|
2516
|
-
function
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
const [selectedIds, setSelectedIds] = useState3(EMPTY_SET);
|
|
2531
|
-
const [editingText, setEditingText] = useState3(null);
|
|
2532
|
-
const [stroke, setStroke] = useState3(STROKE_COLORS[0]);
|
|
2533
|
-
const [fill, setFill] = useState3(FILL_COLORS[0]);
|
|
2534
|
-
const [hoverBoxId, setHoverBoxId] = useState3(null);
|
|
2535
|
-
const [marquee, setMarquee] = useState3(null);
|
|
2536
|
-
const [scale, setScale] = useState3(1);
|
|
2537
|
-
const rootRef = useRef4(null);
|
|
2538
|
-
const svgRef = useRef4(null);
|
|
2539
|
-
const dragRef = useRef4(null);
|
|
2540
|
-
const pendingPointRef = useRef4(null);
|
|
2541
|
-
const frameRef = useRef4(null);
|
|
2542
|
-
const lastCommittedRef = useRef4(serializeDrawingData(data));
|
|
2543
|
-
const shapesRef = useRef4(shapes);
|
|
2544
|
-
shapesRef.current = shapes;
|
|
2545
|
-
const heightRef = useRef4(canvasHeight);
|
|
2546
|
-
heightRef.current = canvasHeight;
|
|
2547
|
-
const widthRef = useRef4(width);
|
|
2548
|
-
widthRef.current = width;
|
|
2549
|
-
const selectedRef = useRef4(selectedIds);
|
|
2550
|
-
selectedRef.current = selectedIds;
|
|
2551
|
-
const paths = useMemo(() => computePaths(shapes), [shapes]);
|
|
2552
|
-
const canvasWidth = data.canvasWidth;
|
|
2553
|
-
const logicalWidth = canvasWidth ?? (width === "content" ? contentWidth(shapes, paths) : null);
|
|
2554
|
-
useEffect7(() => {
|
|
2555
|
-
const incoming = serializeDrawingData(data);
|
|
2556
|
-
if (incoming !== lastCommittedRef.current) {
|
|
2557
|
-
lastCommittedRef.current = incoming;
|
|
2558
|
-
setShapes(data.shapes);
|
|
2559
|
-
setCanvasHeight(data.canvasHeight);
|
|
2560
|
-
setWidth(data.width ?? "full");
|
|
2561
|
-
setSelectedIds(EMPTY_SET);
|
|
2562
|
-
setEditingText(null);
|
|
2563
|
-
}
|
|
2564
|
-
}, [data]);
|
|
2565
|
-
useEffect7(() => {
|
|
2566
|
-
const svg = svgRef.current;
|
|
2567
|
-
if (!logicalWidth || !svg || typeof ResizeObserver === "undefined") {
|
|
2568
|
-
setScale(1);
|
|
2569
|
-
return;
|
|
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"
|
|
2570
2278
|
}
|
|
2571
|
-
const update = () => {
|
|
2572
|
-
const rect = svg.getBoundingClientRect();
|
|
2573
|
-
setScale(rect.width > 0 ? rect.width / logicalWidth : 1);
|
|
2574
|
-
};
|
|
2575
|
-
update();
|
|
2576
|
-
const observer = new ResizeObserver(update);
|
|
2577
|
-
observer.observe(svg);
|
|
2578
|
-
return () => observer.disconnect();
|
|
2579
|
-
}, [logicalWidth]);
|
|
2580
|
-
const commit = useCallback(
|
|
2581
|
-
(nextShapes, options) => {
|
|
2582
|
-
const nextWidth = options?.width ?? widthRef.current;
|
|
2583
|
-
const explicitFull = options?.width === void 0 && data.width !== void 0;
|
|
2584
|
-
const payload = {
|
|
2585
|
-
version: 2,
|
|
2586
|
-
...canvasWidth ? { canvasWidth } : {},
|
|
2587
|
-
canvasHeight: options?.height ?? heightRef.current,
|
|
2588
|
-
...nextWidth !== "full" || explicitFull ? { width: nextWidth } : {},
|
|
2589
|
-
shapes: nextShapes
|
|
2590
|
-
};
|
|
2591
|
-
const json = serializeDrawingData(payload);
|
|
2592
|
-
if (json === lastCommittedRef.current) return;
|
|
2593
|
-
lastCommittedRef.current = json;
|
|
2594
|
-
editor.update(() => {
|
|
2595
|
-
const node = $getNodeByKey2(nodeKey);
|
|
2596
|
-
if ($isDrawingNode(node)) node.setData(payload);
|
|
2597
|
-
});
|
|
2598
|
-
},
|
|
2599
|
-
[editor, nodeKey, canvasWidth, data.width]
|
|
2600
|
-
);
|
|
2601
|
-
const updateShapes = useCallback(
|
|
2602
|
-
(updater, options) => {
|
|
2603
|
-
setShapes((prev) => {
|
|
2604
|
-
const next = resolveBindings(updater(prev));
|
|
2605
|
-
if (options?.commit) commit(next);
|
|
2606
|
-
return next;
|
|
2607
|
-
});
|
|
2608
|
-
},
|
|
2609
|
-
[commit]
|
|
2610
2279
|
);
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2280
|
+
}
|
|
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
|
+
] });
|
|
2313
|
+
}
|
|
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"
|
|
2618
2329
|
}
|
|
2619
|
-
const inverse = ctm.inverse();
|
|
2620
|
-
return {
|
|
2621
|
-
x: inverse.a * e.clientX + inverse.c * e.clientY + inverse.e,
|
|
2622
|
-
y: inverse.b * e.clientX + inverse.d * e.clientY + inverse.f
|
|
2623
|
-
};
|
|
2624
|
-
}, []);
|
|
2625
|
-
const selection = useMemo(
|
|
2626
|
-
() => shapes.filter((s) => selectedIds.has(s.id)),
|
|
2627
|
-
[shapes, selectedIds]
|
|
2628
2330
|
);
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
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",
|
|
2652
2384
|
{
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
y: point.y - FONT_SIZE / 2,
|
|
2657
|
-
w: size.w,
|
|
2658
|
-
h: size.h,
|
|
2659
|
-
stroke,
|
|
2660
|
-
fill: "transparent",
|
|
2661
|
-
strokeWidth: 2,
|
|
2662
|
-
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
|
|
2663
2388
|
}
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
updateShapes((prev) => [
|
|
2675
|
-
...prev,
|
|
2676
|
-
{ id, type: tool, x: point.x, y: point.y, w: 0, h: 0, stroke, fill: shapeFill, strokeWidth: 2 }
|
|
2677
|
-
]);
|
|
2678
|
-
},
|
|
2679
|
-
[isEditable, editingText, getPoint, tool, stroke, fill, updateShapes, startTextEditing]
|
|
2680
|
-
);
|
|
2681
|
-
const handleShapePointerDown = useCallback(
|
|
2682
|
-
(e, shape) => {
|
|
2683
|
-
if (!isEditable || tool !== "select" || editingText || e.button !== 0) return;
|
|
2684
|
-
e.stopPropagation();
|
|
2685
|
-
const point = getPoint(e);
|
|
2686
|
-
capture(e);
|
|
2687
|
-
const selected = selectedRef.current;
|
|
2688
|
-
if (e.shiftKey) {
|
|
2689
|
-
if (selected.has(shape.id)) {
|
|
2690
|
-
const next2 = new Set(selected);
|
|
2691
|
-
next2.delete(shape.id);
|
|
2692
|
-
setSelectedIds(next2);
|
|
2693
|
-
return;
|
|
2694
|
-
}
|
|
2695
|
-
const next = new Set(selected).add(shape.id);
|
|
2696
|
-
setSelectedIds(next);
|
|
2697
|
-
dragRef.current = {
|
|
2698
|
-
mode: "move",
|
|
2699
|
-
clickedId: shape.id,
|
|
2700
|
-
origin: point,
|
|
2701
|
-
origs: moveGroup(shapesRef.current, next, shape.id),
|
|
2702
|
-
wasSelected: false
|
|
2703
|
-
};
|
|
2704
|
-
return;
|
|
2705
|
-
}
|
|
2706
|
-
const wasSelected = selected.size === 1 && selected.has(shape.id);
|
|
2707
|
-
const group = selected.has(shape.id) ? selected : /* @__PURE__ */ new Set([shape.id]);
|
|
2708
|
-
if (!selected.has(shape.id)) setSelectedIds(group);
|
|
2709
|
-
dragRef.current = {
|
|
2710
|
-
mode: "move",
|
|
2711
|
-
clickedId: shape.id,
|
|
2712
|
-
origin: point,
|
|
2713
|
-
origs: moveGroup(shapesRef.current, group, shape.id),
|
|
2714
|
-
wasSelected
|
|
2715
|
-
};
|
|
2716
|
-
},
|
|
2717
|
-
[isEditable, tool, editingText, getPoint]
|
|
2718
|
-
);
|
|
2719
|
-
const handleHandlePointerDown = useCallback(
|
|
2720
|
-
(e, drag) => {
|
|
2721
|
-
if (!isEditable || e.button !== 0) return;
|
|
2722
|
-
e.stopPropagation();
|
|
2723
|
-
capture(e);
|
|
2724
|
-
dragRef.current = drag;
|
|
2725
|
-
if (drag.mode === "endpoint") {
|
|
2726
|
-
const key = drag.end === "start" ? "startBinding" : "endBinding";
|
|
2727
|
-
updateShapes(
|
|
2728
|
-
(prev) => prev.map((s) => s.id === drag.id ? { ...s, [key]: void 0 } : s)
|
|
2729
|
-
);
|
|
2730
|
-
}
|
|
2731
|
-
},
|
|
2732
|
-
[isEditable, updateShapes]
|
|
2733
|
-
);
|
|
2734
|
-
const flushPointer = useCallback(() => {
|
|
2735
|
-
frameRef.current = null;
|
|
2736
|
-
const point = pendingPointRef.current;
|
|
2737
|
-
const drag = dragRef.current;
|
|
2738
|
-
if (!point || !drag) return;
|
|
2739
|
-
if (drag.mode === "marquee") {
|
|
2740
|
-
drag.current = point;
|
|
2741
|
-
setMarquee({ origin: drag.origin, current: point });
|
|
2742
|
-
return;
|
|
2743
|
-
}
|
|
2744
|
-
updateShapes((prev) => applyDrag(prev, drag, point));
|
|
2745
|
-
if (drag.mode === "endpoint" || drag.mode === "draw" && isConnectorType(shapesRef.current.find((s) => s.id === drag.id)?.type ?? "")) {
|
|
2746
|
-
const box = findBoxAt(shapesRef.current, point, drag.id);
|
|
2747
|
-
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
|
+
] });
|
|
2748
2399
|
}
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
const handlePointerUp = useCallback(
|
|
2761
|
-
(e) => {
|
|
2762
|
-
const drag = dragRef.current;
|
|
2763
|
-
dragRef.current = null;
|
|
2764
|
-
if (frameRef.current !== null) {
|
|
2765
|
-
cancelAnimationFrame(frameRef.current);
|
|
2766
|
-
frameRef.current = null;
|
|
2767
|
-
}
|
|
2768
|
-
pendingPointRef.current = null;
|
|
2769
|
-
setHoverBoxId(null);
|
|
2770
|
-
if (!drag) return;
|
|
2771
|
-
const point = getPoint(e);
|
|
2772
|
-
if (drag.mode === "marquee") {
|
|
2773
|
-
setMarquee(null);
|
|
2774
|
-
const rect = marqueeRect(drag.origin, point);
|
|
2775
|
-
if (rect.w < CLICK_TOLERANCE && rect.h < CLICK_TOLERANCE) return;
|
|
2776
|
-
const ids = shapesWithin(shapesRef.current, rect, paths);
|
|
2777
|
-
setSelectedIds((prev) => drag.additive ? /* @__PURE__ */ new Set([...prev, ...ids]) : new Set(ids));
|
|
2778
|
-
return;
|
|
2779
|
-
}
|
|
2780
|
-
if (drag.mode === "move" && drag.wasSelected) {
|
|
2781
|
-
const moved = Math.abs(point.x - drag.origin.x) > CLICK_TOLERANCE || Math.abs(point.y - drag.origin.y) > CLICK_TOLERANCE;
|
|
2782
|
-
const current = shapesRef.current.find((s) => s.id === drag.clickedId);
|
|
2783
|
-
if (!moved && current) {
|
|
2784
|
-
updateShapes((prev) => applyDrag(prev, drag, drag.origin));
|
|
2785
|
-
if (current.type === "text" || isConnectorType(current.type)) {
|
|
2786
|
-
startTextEditing(current.id, "text");
|
|
2787
|
-
} else {
|
|
2788
|
-
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
|
|
2789
2411
|
}
|
|
2790
|
-
|
|
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
|
|
2791
2424
|
}
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
setTool("select");
|
|
2805
|
-
setSelectedIds(/* @__PURE__ */ new Set([drag.id]));
|
|
2806
|
-
} else {
|
|
2807
|
-
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
|
|
2808
2437
|
}
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
(prev) => prev.map((s) => {
|
|
2820
|
-
if (s.id !== drag.id) return s;
|
|
2821
|
-
const end = drag.end === "start" ? { x: s.x, y: s.y } : { x: s.x + s.w, y: s.y + s.h };
|
|
2822
|
-
const box = findBoxAt(prev, end, s.id);
|
|
2823
|
-
const other = drag.end === "start" ? s.endBinding : s.startBinding;
|
|
2824
|
-
if (box && box.id === other?.id) return s;
|
|
2825
|
-
const key = drag.end === "start" ? "startBinding" : "endBinding";
|
|
2826
|
-
return { ...s, [key]: box ? makeBinding(box, end) : void 0 };
|
|
2827
|
-
})
|
|
2828
|
-
);
|
|
2829
|
-
}
|
|
2830
|
-
updateShapes((prev) => prev.map(normalize), { commit: true });
|
|
2831
|
-
},
|
|
2832
|
-
[getPoint, paths, updateShapes, startTextEditing]
|
|
2833
|
-
);
|
|
2834
|
-
const deleteSelection = useCallback(() => {
|
|
2835
|
-
const ids = selectedRef.current;
|
|
2836
|
-
if (!ids.size) return;
|
|
2837
|
-
setEditingText(null);
|
|
2838
|
-
setSelectedIds(EMPTY_SET);
|
|
2839
|
-
updateShapes(
|
|
2840
|
-
(prev) => prev.filter(
|
|
2841
|
-
(s) => !ids.has(s.id) && !(s.startBinding && ids.has(s.startBinding.id)) && !(s.endBinding && ids.has(s.endBinding.id))
|
|
2842
|
-
),
|
|
2843
|
-
{ commit: true }
|
|
2844
|
-
);
|
|
2845
|
-
}, [updateShapes]);
|
|
2846
|
-
const editingRef = useRef4(editingText);
|
|
2847
|
-
editingRef.current = editingText;
|
|
2848
|
-
useEffect7(() => {
|
|
2849
|
-
const root = rootRef.current;
|
|
2850
|
-
if (!root || !isEditable) return;
|
|
2851
|
-
const onKeyDown = (e) => {
|
|
2852
|
-
if (editingRef.current) return;
|
|
2853
|
-
const ids = selectedRef.current;
|
|
2854
|
-
if ((e.key === "Delete" || e.key === "Backspace") && ids.size) {
|
|
2855
|
-
e.preventDefault();
|
|
2856
|
-
e.stopPropagation();
|
|
2857
|
-
deleteSelection();
|
|
2858
|
-
} else if (e.key === "Enter" && ids.size === 1) {
|
|
2859
|
-
const [id] = ids;
|
|
2860
|
-
const shape = shapesRef.current.find((s) => s.id === id);
|
|
2861
|
-
if (shape) {
|
|
2862
|
-
e.preventDefault();
|
|
2863
|
-
e.stopPropagation();
|
|
2864
|
-
startTextEditing(shape.id, "text");
|
|
2865
|
-
}
|
|
2866
|
-
} else if (e.key === "a" && (e.metaKey || e.ctrlKey)) {
|
|
2867
|
-
e.preventDefault();
|
|
2868
|
-
e.stopPropagation();
|
|
2869
|
-
setSelectedIds(new Set(shapesRef.current.map((s) => s.id)));
|
|
2870
|
-
} else if (e.key === "Escape") {
|
|
2871
|
-
e.stopPropagation();
|
|
2872
|
-
setSelectedIds(EMPTY_SET);
|
|
2873
|
-
setTool("select");
|
|
2874
|
-
}
|
|
2875
|
-
};
|
|
2876
|
-
root.addEventListener("keydown", onKeyDown);
|
|
2877
|
-
return () => root.removeEventListener("keydown", onKeyDown);
|
|
2878
|
-
}, [isEditable, deleteSelection, startTextEditing]);
|
|
2879
|
-
const applyToSelection = useCallback(
|
|
2880
|
-
(patch) => {
|
|
2881
|
-
const ids = selectedRef.current;
|
|
2882
|
-
if (!ids.size) return;
|
|
2883
|
-
updateShapes((prev) => prev.map((s) => ids.has(s.id) ? patch(s) : s), { commit: true });
|
|
2884
|
-
},
|
|
2885
|
-
[updateShapes]
|
|
2886
|
-
);
|
|
2887
|
-
const applyStroke = useCallback(
|
|
2888
|
-
(color) => {
|
|
2889
|
-
setStroke(color);
|
|
2890
|
-
applyToSelection((s) => ({ ...s, stroke: color }));
|
|
2891
|
-
},
|
|
2892
|
-
[applyToSelection]
|
|
2893
|
-
);
|
|
2894
|
-
const applyFill = useCallback(
|
|
2895
|
-
(color) => {
|
|
2896
|
-
setFill(color);
|
|
2897
|
-
applyToSelection((s) => isBoxType(s.type) ? { ...s, fill: color } : s);
|
|
2898
|
-
},
|
|
2899
|
-
[applyToSelection]
|
|
2900
|
-
);
|
|
2901
|
-
const applyRouting = useCallback(
|
|
2902
|
-
(elbow) => applyToSelection(
|
|
2903
|
-
(s) => isConnectorType(s.type) ? { ...s, routing: elbow ? "elbow" : void 0, elbow: void 0, waypoints: void 0 } : s
|
|
2904
|
-
),
|
|
2905
|
-
[applyToSelection]
|
|
2906
|
-
);
|
|
2907
|
-
const applyDirection = useCallback(
|
|
2908
|
-
(bidirectional) => applyToSelection(
|
|
2909
|
-
(s) => s.type === "arrow" ? { ...s, bidirectional: bidirectional || void 0 } : s
|
|
2910
|
-
),
|
|
2911
|
-
[applyToSelection]
|
|
2912
|
-
);
|
|
2913
|
-
const applyWidth = useCallback(
|
|
2914
|
-
(next) => {
|
|
2915
|
-
setWidth(next);
|
|
2916
|
-
commit(shapesRef.current, { width: next });
|
|
2917
|
-
},
|
|
2918
|
-
[commit]
|
|
2919
|
-
);
|
|
2920
|
-
const addWaypoint = useCallback(
|
|
2921
|
-
(e, shapeId, segmentIndex, point) => {
|
|
2922
|
-
if (!isEditable || e.button !== 0) return;
|
|
2923
|
-
e.stopPropagation();
|
|
2924
|
-
capture(e);
|
|
2925
|
-
updateShapes(
|
|
2926
|
-
(prev) => prev.map((s) => {
|
|
2927
|
-
if (s.id !== shapeId || !isConnectorType(s.type)) return s;
|
|
2928
|
-
const interior = connectorPoints(s, prev).slice(1, -1);
|
|
2929
|
-
const waypoints = [...interior.slice(0, segmentIndex), point, ...interior.slice(segmentIndex)];
|
|
2930
|
-
return { ...s, waypoints, routing: void 0, elbow: void 0 };
|
|
2931
|
-
})
|
|
2932
|
-
);
|
|
2933
|
-
dragRef.current = { mode: "waypoint", id: shapeId, index: segmentIndex };
|
|
2934
|
-
},
|
|
2935
|
-
[isEditable, updateShapes]
|
|
2936
|
-
);
|
|
2937
|
-
const removeWaypoint = useCallback(
|
|
2938
|
-
(shapeId, index) => {
|
|
2939
|
-
updateShapes(
|
|
2940
|
-
(prev) => prev.map((s) => {
|
|
2941
|
-
if (s.id !== shapeId || !s.waypoints) return s;
|
|
2942
|
-
const waypoints = s.waypoints.filter((_, i) => i !== index);
|
|
2943
|
-
return { ...s, waypoints: waypoints.length ? waypoints : void 0 };
|
|
2944
|
-
}),
|
|
2945
|
-
{ commit: true }
|
|
2946
|
-
);
|
|
2947
|
-
},
|
|
2948
|
-
[updateShapes]
|
|
2949
|
-
);
|
|
2950
|
-
const commitText = useCallback(
|
|
2951
|
-
(id, field, value) => {
|
|
2952
|
-
setEditingText(null);
|
|
2953
|
-
updateShapes(
|
|
2954
|
-
(prev) => prev.flatMap((s) => {
|
|
2955
|
-
if (s.id !== id) return [s];
|
|
2956
|
-
if (s.type === "text") {
|
|
2957
|
-
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
|
|
2958
2448
|
}
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
{ commit: true }
|
|
2962
|
-
);
|
|
2963
|
-
},
|
|
2964
|
-
[updateShapes]
|
|
2965
|
-
);
|
|
2966
|
-
const copyMermaid = useCallback(async () => {
|
|
2967
|
-
const payload = { version: 2, canvasHeight: heightRef.current, shapes: shapesRef.current };
|
|
2968
|
-
try {
|
|
2969
|
-
await navigator.clipboard.writeText(drawingToMermaid(payload));
|
|
2970
|
-
return true;
|
|
2971
|
-
} catch {
|
|
2972
|
-
return false;
|
|
2449
|
+
)
|
|
2450
|
+
] });
|
|
2973
2451
|
}
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
isEditable && /* @__PURE__ */ jsxs8("div", { className: "zui-drawing-toolbar", onPointerDown: (e) => e.stopPropagation(), children: [
|
|
2994
|
-
/* @__PURE__ */ jsx9(
|
|
2995
|
-
DrawingToolbar,
|
|
2996
|
-
{
|
|
2997
|
-
tool,
|
|
2998
|
-
onToolChange: (t) => {
|
|
2999
|
-
setTool(t);
|
|
3000
|
-
if (t !== "select") setSelectedIds(EMPTY_SET);
|
|
3001
|
-
},
|
|
3002
|
-
width,
|
|
3003
|
-
onWidthChange: applyWidth,
|
|
3004
|
-
onCopyMermaid: copyMermaid
|
|
3005
|
-
}
|
|
3006
|
-
),
|
|
3007
|
-
/* @__PURE__ */ jsx9("div", { className: `zui-drawing-props-host ${showProps ? "is-visible" : ""}`, children: /* @__PURE__ */ jsx9(
|
|
3008
|
-
PropertyBar,
|
|
3009
|
-
{
|
|
3010
|
-
selection,
|
|
3011
|
-
showFill: tool !== "select" && isBoxType(tool),
|
|
3012
|
-
stroke: propStroke,
|
|
3013
|
-
fill: propFill,
|
|
3014
|
-
onStroke: applyStroke,
|
|
3015
|
-
onFill: applyFill,
|
|
3016
|
-
onRouting: applyRouting,
|
|
3017
|
-
onDirection: applyDirection,
|
|
3018
|
-
onDelete: deleteSelection
|
|
3019
|
-
}
|
|
3020
|
-
) })
|
|
3021
|
-
] }),
|
|
3022
|
-
/* @__PURE__ */ jsxs8("div", { className: "zui-drawing-stage", children: [
|
|
3023
|
-
/* @__PURE__ */ jsxs8(
|
|
3024
|
-
"svg",
|
|
3025
|
-
{
|
|
3026
|
-
ref: svgRef,
|
|
3027
|
-
className: "zui-drawing-surface",
|
|
3028
|
-
style: {
|
|
3029
|
-
height: logicalWidth ? void 0 : canvasHeight,
|
|
3030
|
-
width: logicalWidth ?? void 0,
|
|
3031
|
-
maxWidth: "100%",
|
|
3032
|
-
aspectRatio: logicalWidth ? `${logicalWidth} / ${canvasHeight}` : void 0,
|
|
3033
|
-
cursor: canvasCursor
|
|
3034
|
-
},
|
|
3035
|
-
viewBox: logicalWidth ? `0 0 ${logicalWidth} ${canvasHeight}` : void 0,
|
|
3036
|
-
preserveAspectRatio: "xMinYMin meet",
|
|
3037
|
-
onPointerDown: handleBackgroundPointerDown,
|
|
3038
|
-
onPointerMove: handlePointerMove,
|
|
3039
|
-
onPointerUp: handlePointerUp,
|
|
3040
|
-
onPointerCancel: handlePointerUp,
|
|
3041
|
-
onDoubleClick: (e) => {
|
|
3042
|
-
if (!isEditable) return;
|
|
3043
|
-
const target = e.target.closest("[data-shape-id]");
|
|
3044
|
-
const id = target?.getAttribute("data-shape-id");
|
|
3045
|
-
const shape = id ? shapes.find((s) => s.id === id) : null;
|
|
3046
|
-
if (!shape) return;
|
|
3047
|
-
if (shape.type === "text" || isConnectorType(shape.type)) {
|
|
3048
|
-
startTextEditing(shape.id, "text");
|
|
3049
|
-
} else {
|
|
3050
|
-
startTextEditing(shape.id, slotAt(shape, getPoint(e).y));
|
|
3051
|
-
}
|
|
3052
|
-
},
|
|
3053
|
-
children: [
|
|
3054
|
-
shapes.map((shape) => /* @__PURE__ */ jsxs8(
|
|
3055
|
-
"g",
|
|
3056
|
-
{
|
|
3057
|
-
"data-shape-id": shape.id,
|
|
3058
|
-
className: `zui-drawing-shape ${selectedIds.has(shape.id) ? "is-selected" : ""}`,
|
|
3059
|
-
style: { cursor: isEditable && tool === "select" ? "move" : void 0 },
|
|
3060
|
-
onPointerDown: (e) => handleShapePointerDown(e, shape),
|
|
3061
|
-
children: [
|
|
3062
|
-
/* @__PURE__ */ jsx9(HitArea, { shape, points: paths.get(shape.id) }),
|
|
3063
|
-
shape.id === editingText?.id && shape.type === "text" ? null : /* @__PURE__ */ jsx9(
|
|
3064
|
-
ShapeView,
|
|
3065
|
-
{
|
|
3066
|
-
shape,
|
|
3067
|
-
points: paths.get(shape.id),
|
|
3068
|
-
hideField: shape.id === editingText?.id ? editingText.field : null,
|
|
3069
|
-
showHints: isEditable && tool === "select" && single?.id === shape.id && shape.id !== editingText?.id
|
|
3070
|
-
}
|
|
3071
|
-
)
|
|
3072
|
-
]
|
|
3073
|
-
},
|
|
3074
|
-
shape.id
|
|
3075
|
-
)),
|
|
3076
|
-
hoverBox && /* @__PURE__ */ jsx9(
|
|
3077
|
-
"polygon",
|
|
3078
|
-
{
|
|
3079
|
-
className: "zui-drawing-selection zui-drawing-bind-target",
|
|
3080
|
-
points: boxOutline(hoverBox).map((p) => `${p.x},${p.y}`).join(" "),
|
|
3081
|
-
fill: "currentColor",
|
|
3082
|
-
fillOpacity: 0.06,
|
|
3083
|
-
stroke: "currentColor",
|
|
3084
|
-
strokeWidth: 2,
|
|
3085
|
-
strokeLinejoin: "round",
|
|
3086
|
-
pointerEvents: "none"
|
|
3087
|
-
}
|
|
3088
|
-
),
|
|
3089
|
-
single && isEditable && !editingText && /* @__PURE__ */ jsx9(
|
|
3090
|
-
SelectionOverlay,
|
|
3091
|
-
{
|
|
3092
|
-
shape: single,
|
|
3093
|
-
points: paths.get(single.id) ?? [],
|
|
3094
|
-
onHandlePointerDown: handleHandlePointerDown,
|
|
3095
|
-
onWaypointAdd: addWaypoint,
|
|
3096
|
-
onWaypointRemove: removeWaypoint
|
|
3097
|
-
}
|
|
3098
|
-
),
|
|
3099
|
-
selection.length > 1 && isEditable && /* @__PURE__ */ jsx9(GroupSelectionOverlay, { shapes: selection, paths }),
|
|
3100
|
-
marquee && /* @__PURE__ */ jsx9(MarqueeOverlay, { rect: marqueeRect(marquee.origin, marquee.current) })
|
|
3101
|
-
]
|
|
3102
|
-
}
|
|
3103
|
-
),
|
|
3104
|
-
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" }),
|
|
3105
|
-
editingShape && editingText && /* @__PURE__ */ jsx9(
|
|
3106
|
-
"div",
|
|
3107
|
-
{
|
|
3108
|
-
className: "zui-drawing-overlay",
|
|
3109
|
-
style: {
|
|
3110
|
-
transform: `scale(${scale})`,
|
|
3111
|
-
width: logicalWidth ?? void 0,
|
|
3112
|
-
height: logicalWidth ? canvasHeight : void 0
|
|
3113
|
-
},
|
|
3114
|
-
children: /* @__PURE__ */ jsx9(
|
|
3115
|
-
TextEditOverlay,
|
|
3116
|
-
{
|
|
3117
|
-
shape: editingShape,
|
|
3118
|
-
field: editingText.field,
|
|
3119
|
-
points: paths.get(editingShape.id),
|
|
3120
|
-
onCommit: commitText
|
|
3121
|
-
},
|
|
3122
|
-
`${editingShape.id}:${editingText.field}`
|
|
3123
|
-
)
|
|
3124
|
-
}
|
|
3125
|
-
),
|
|
3126
|
-
isEditable && /* @__PURE__ */ jsx9(
|
|
3127
|
-
HeightHandle,
|
|
3128
|
-
{
|
|
3129
|
-
height: canvasHeight,
|
|
3130
|
-
scale,
|
|
3131
|
-
onChange: setCanvasHeight,
|
|
3132
|
-
onCommit: (h) => commit(shapesRef.current, { height: h })
|
|
3133
|
-
}
|
|
3134
|
-
)
|
|
3135
|
-
] })
|
|
3136
|
-
]
|
|
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
|
+
] });
|
|
3137
2471
|
}
|
|
3138
|
-
|
|
2472
|
+
default:
|
|
2473
|
+
return null;
|
|
2474
|
+
}
|
|
3139
2475
|
}
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
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
|
|
3145
2514
|
}) {
|
|
3146
|
-
const
|
|
3147
|
-
const
|
|
3148
|
-
|
|
3149
|
-
const
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
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
|
|
3162
2542
|
},
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
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
|
|
3167
2565
|
},
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
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
|
|
3172
2589
|
},
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
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
|
+
] });
|
|
3176
2604
|
}
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
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
|
|
2638
|
+
},
|
|
2639
|
+
i
|
|
2640
|
+
))
|
|
2641
|
+
] });
|
|
3199
2642
|
}
|
|
3200
|
-
function
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
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
|
+
] });
|
|
3207
2660
|
}
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
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))
|
|
2681
|
+
}
|
|
3216
2682
|
);
|
|
3217
|
-
const positions = layoutBoxes(boxes, sizes, connectors, skeleton.direction ?? "right");
|
|
3218
|
-
const boxShapes = boxes.map(
|
|
3219
|
-
(box) => expandBox(box, boxTypes.get(box.id), sizes.get(box.id), positions.get(box.id))
|
|
3220
|
-
);
|
|
3221
|
-
const centers = new Map(boxShapes.map((shape) => [shape.id, shapeCenter(shape)]));
|
|
3222
|
-
const used = new Set(boxIds);
|
|
3223
|
-
const nextId = (prefix) => {
|
|
3224
|
-
let i = used.size + 1;
|
|
3225
|
-
while (used.has(`${prefix}${i}`)) i += 1;
|
|
3226
|
-
const id = `${prefix}${i}`;
|
|
3227
|
-
used.add(id);
|
|
3228
|
-
return id;
|
|
3229
|
-
};
|
|
3230
|
-
const connectorShapes = connectors.map((c) => expandConnector(c, centers, nextId("c")));
|
|
3231
|
-
const textShapes = (skeleton.texts ?? []).map((t) => expandText(t, nextId("t")));
|
|
3232
|
-
const shapes = resolveBindings([...boxShapes, ...connectorShapes, ...textShapes]);
|
|
3233
|
-
const bottom = shapes.reduce((max, shape) => {
|
|
3234
|
-
const b = bbox(shape);
|
|
3235
|
-
return Math.max(max, b.y + b.h);
|
|
3236
|
-
}, 0);
|
|
3237
|
-
return normalizeDrawingData({
|
|
3238
|
-
version: 2,
|
|
3239
|
-
...skeleton.canvasWidth !== void 0 ? { canvasWidth: skeleton.canvasWidth } : {},
|
|
3240
|
-
canvasHeight: skeleton.canvasHeight ?? Math.max(MIN_AUTO_CANVAS_HEIGHT, Math.ceil(bottom + CANVAS_MARGIN)),
|
|
3241
|
-
...skeleton.width !== void 0 ? { width: skeleton.width } : {},
|
|
3242
|
-
shapes
|
|
3243
|
-
});
|
|
3244
|
-
}
|
|
3245
|
-
function measureBox(box, type) {
|
|
3246
|
-
const def = BOX_DEFINITIONS[type];
|
|
3247
|
-
const probe = {
|
|
3248
|
-
id: "",
|
|
3249
|
-
type,
|
|
3250
|
-
x: 0,
|
|
3251
|
-
y: 0,
|
|
3252
|
-
w: def.defaultSize.w,
|
|
3253
|
-
h: def.defaultSize.h,
|
|
3254
|
-
stroke: "",
|
|
3255
|
-
fill: "",
|
|
3256
|
-
strokeWidth: 0
|
|
3257
|
-
};
|
|
3258
|
-
const area = def.textArea(probe);
|
|
3259
|
-
const ratioW = area.w / def.defaultSize.w;
|
|
3260
|
-
const ratioH = area.h / def.defaultSize.h;
|
|
3261
|
-
const wrapWidth = box.w !== void 0 ? Math.max(20, box.w * ratioW) : TEXT_WRAP_WIDTH;
|
|
3262
|
-
const slots = def.textSlots.flatMap((slot) => {
|
|
3263
|
-
const size = slotSize(box, slot, wrapWidth);
|
|
3264
|
-
return size ? [size] : [];
|
|
3265
|
-
});
|
|
3266
|
-
const innerW = slots.reduce((max, s) => Math.max(max, s.w), 0);
|
|
3267
|
-
const innerH = slots.reduce((sum, s) => sum + s.h, 0) + Math.max(0, slots.length - 1) * SLOT_GAP;
|
|
3268
|
-
return {
|
|
3269
|
-
w: box.w ?? Math.max(def.defaultSize.w, Math.ceil(innerW / ratioW)),
|
|
3270
|
-
h: box.h ?? Math.max(def.defaultSize.h, Math.ceil(innerH / ratioH))
|
|
3271
|
-
};
|
|
3272
|
-
}
|
|
3273
|
-
function slotSize(box, slot, wrapWidth) {
|
|
3274
|
-
const value = box[slot];
|
|
3275
|
-
if (!value) return null;
|
|
3276
|
-
const fontSize = slot === "text" ? FONT_SIZE : SMALL_FONT_SIZE;
|
|
3277
|
-
return textBoxSize(wrapText(value, wrapWidth, fontSize).join("\n"), fontSize);
|
|
3278
2683
|
}
|
|
3279
|
-
function
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
auto.map((box) => box.id),
|
|
3286
|
-
acyclicEdges(
|
|
3287
|
-
auto.map((box) => box.id),
|
|
3288
|
-
edges
|
|
3289
|
-
)
|
|
3290
|
-
);
|
|
3291
|
-
const rows = [];
|
|
3292
|
-
for (const box of auto) {
|
|
3293
|
-
const rank = ranks.get(box.id) ?? 0;
|
|
3294
|
-
(rows[rank] ?? (rows[rank] = [])).push(box.id);
|
|
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 });
|
|
3295
2690
|
}
|
|
3296
|
-
const
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
for (const id of row) {
|
|
3307
|
-
const size = sizes.get(id);
|
|
3308
|
-
const mainPos = Math.round(mainOffset + (rowMain[rank] - size[main]) / 2);
|
|
3309
|
-
const crossPos = Math.round(crossOffset);
|
|
3310
|
-
positions.set(
|
|
3311
|
-
id,
|
|
3312
|
-
direction === "right" ? { x: mainPos, y: crossPos } : { x: crossPos, y: mainPos }
|
|
3313
|
-
);
|
|
3314
|
-
crossOffset += size[cross] + STACK_GAP;
|
|
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"
|
|
3315
2701
|
}
|
|
3316
|
-
|
|
3317
|
-
});
|
|
3318
|
-
for (const box of boxes) {
|
|
3319
|
-
const computed = positions.get(box.id) ?? { x: LAYOUT_ORIGIN, y: LAYOUT_ORIGIN };
|
|
3320
|
-
positions.set(box.id, { x: box.x ?? computed.x, y: box.y ?? computed.y });
|
|
3321
|
-
}
|
|
3322
|
-
return positions;
|
|
2702
|
+
);
|
|
3323
2703
|
}
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
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
|
|
3338
2760
|
};
|
|
3339
|
-
|
|
3340
|
-
|
|
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
|
+
});
|
|
2812
|
+
}
|
|
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
|
+
}
|
|
2827
|
+
);
|
|
3341
2828
|
}
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
}
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
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
|
+
] })
|
|
2865
|
+
}
|
|
2866
|
+
];
|
|
2867
|
+
function layoutPreset(width) {
|
|
2868
|
+
return width === "content" ? "compact" : width === "text" ? "comfortable" : "full";
|
|
3373
2869
|
}
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
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 })
|
|
2901
|
+
}
|
|
2902
|
+
);
|
|
3394
2903
|
}
|
|
3395
|
-
function
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
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
|
+
] });
|
|
3409
3004
|
}
|
|
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
|
-
|
|
3415
|
-
|
|
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));
|
|
3034
|
+
}
|
|
3035
|
+
return paths;
|
|
3416
3036
|
}
|
|
3417
|
-
function
|
|
3418
|
-
const
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
const
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
const
|
|
3427
|
-
const
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
const
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
const
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
const
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
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);
|
|
3449
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
|
+
);
|
|
3450
3661
|
}
|
|
3451
|
-
function
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
y: finiteNumber(value.y),
|
|
3464
|
-
w: positiveNumber(value.w),
|
|
3465
|
-
h: positiveNumber(value.h),
|
|
3466
|
-
color: colorName(value.color),
|
|
3467
|
-
stroke: nonEmptyString(value.stroke) ? value.stroke : void 0,
|
|
3468
|
-
fill: nonEmptyString(value.fill) ? value.fill : void 0
|
|
3469
|
-
});
|
|
3470
|
-
}
|
|
3471
|
-
function normalizeConnector(value) {
|
|
3472
|
-
if (!isRecord2(value)) return null;
|
|
3473
|
-
const from = normalizeEnd(value.from);
|
|
3474
|
-
const to = normalizeEnd(value.to);
|
|
3475
|
-
if (!from || !to) return null;
|
|
3476
|
-
if (value.type !== void 0 && !(typeof value.type === "string" && isConnectorType(value.type))) {
|
|
3477
|
-
return null;
|
|
3478
|
-
}
|
|
3479
|
-
return compact({
|
|
3480
|
-
id: nonEmptyString(value.id) ? value.id : void 0,
|
|
3481
|
-
type: value.type,
|
|
3482
|
-
from,
|
|
3483
|
-
to,
|
|
3484
|
-
text: nonEmptyString(value.text) ? value.text : void 0,
|
|
3485
|
-
bidirectional: value.bidirectional === true ? true : void 0,
|
|
3486
|
-
routing: value.routing === "elbow" ? "elbow" : void 0,
|
|
3487
|
-
color: colorName(value.color),
|
|
3488
|
-
stroke: nonEmptyString(value.stroke) ? value.stroke : void 0
|
|
3489
|
-
});
|
|
3490
|
-
}
|
|
3491
|
-
function normalizeEnd(value) {
|
|
3492
|
-
if (nonEmptyString(value)) return value;
|
|
3493
|
-
if (!isRecord2(value) || !nonEmptyString(value.id)) return null;
|
|
3494
|
-
const side = value.side;
|
|
3495
|
-
return typeof side === "string" && side in SIDE_FIXED_POINTS ? { id: value.id, side } : { id: value.id };
|
|
3496
|
-
}
|
|
3497
|
-
function normalizeText(value) {
|
|
3498
|
-
if (!isRecord2(value) || !nonEmptyString(value.text)) return null;
|
|
3499
|
-
const x = finiteNumber(value.x);
|
|
3500
|
-
const y = finiteNumber(value.y);
|
|
3501
|
-
if (x === void 0 || y === void 0) return null;
|
|
3502
|
-
return compact({
|
|
3503
|
-
id: nonEmptyString(value.id) ? value.id : void 0,
|
|
3504
|
-
x,
|
|
3505
|
-
y,
|
|
3506
|
-
text: value.text,
|
|
3507
|
-
color: colorName(value.color),
|
|
3508
|
-
stroke: nonEmptyString(value.stroke) ? value.stroke : void 0
|
|
3509
|
-
});
|
|
3510
|
-
}
|
|
3511
|
-
function isRecord2(value) {
|
|
3512
|
-
return typeof value === "object" && value !== null;
|
|
3513
|
-
}
|
|
3514
|
-
function nonEmptyString(value) {
|
|
3515
|
-
return typeof value === "string" && value !== "";
|
|
3516
|
-
}
|
|
3517
|
-
function finiteNumber(value) {
|
|
3518
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
3519
|
-
}
|
|
3520
|
-
function positiveNumber(value) {
|
|
3521
|
-
const n2 = finiteNumber(value);
|
|
3522
|
-
return n2 !== void 0 && n2 > 0 ? n2 : void 0;
|
|
3523
|
-
}
|
|
3524
|
-
function colorName(value) {
|
|
3525
|
-
return typeof value === "string" && COLOR_NAMES.includes(value) ? value : void 0;
|
|
3526
|
-
}
|
|
3527
|
-
function compact(value) {
|
|
3528
|
-
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
|
|
3529
|
-
}
|
|
3530
|
-
var COLOR_SCHEMA = {
|
|
3531
|
-
enum: COLOR_NAMES,
|
|
3532
|
-
description: "Named color preset. Sets the stroke (outline + text) and, for boxes, a matching pastel fill. Omit for the default gray on transparent"
|
|
3533
|
-
};
|
|
3534
|
-
var END_SCHEMA = {
|
|
3535
|
-
oneOf: [
|
|
3536
|
-
{
|
|
3537
|
-
type: "string",
|
|
3538
|
-
description: "Id of a box. The connector attaches to its outline, aimed at the other end"
|
|
3539
|
-
},
|
|
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",
|
|
3540
3674
|
{
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
}
|
|
3550
|
-
}
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
properties: {
|
|
3563
|
-
direction: {
|
|
3564
|
-
enum: ["right", "down"],
|
|
3565
|
-
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"'
|
|
3566
|
-
},
|
|
3567
|
-
canvasWidth: {
|
|
3568
|
-
type: "number",
|
|
3569
|
-
minimum: 120,
|
|
3570
|
-
description: "Logical canvas width in px. Omit for a fluid canvas in CSS pixels"
|
|
3571
|
-
},
|
|
3572
|
-
canvasHeight: {
|
|
3573
|
-
type: "number",
|
|
3574
|
-
minimum: 80,
|
|
3575
|
-
description: "Canvas height in px. Omit to fit the content"
|
|
3576
|
-
},
|
|
3577
|
-
width: {
|
|
3578
|
-
enum: ["full", "text", "content"],
|
|
3579
|
-
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'
|
|
3580
|
-
},
|
|
3581
|
-
boxes: {
|
|
3582
|
-
type: "array",
|
|
3583
|
-
items: { $ref: "#/definitions/box" },
|
|
3584
|
-
description: "Card-like shapes carrying up to three text slots. Order is the render order and the stacking order within a layout rank"
|
|
3585
|
-
},
|
|
3586
|
-
connectors: {
|
|
3587
|
-
type: "array",
|
|
3588
|
-
items: { $ref: "#/definitions/connector" },
|
|
3589
|
-
description: "Arrows and lines between boxes. Connectors whose from/to id does not match a box are dropped"
|
|
3590
|
-
},
|
|
3591
|
-
texts: {
|
|
3592
|
-
type: "array",
|
|
3593
|
-
items: { $ref: "#/definitions/text" },
|
|
3594
|
-
description: "Free-floating annotations not attached to any box. Prefer box slots (label/text/footer) when the text belongs to a box"
|
|
3595
|
-
}
|
|
3596
|
-
},
|
|
3597
|
-
definitions: {
|
|
3598
|
-
box: {
|
|
3599
|
-
type: "object",
|
|
3600
|
-
required: ["id"],
|
|
3601
|
-
additionalProperties: false,
|
|
3602
|
-
properties: {
|
|
3603
|
-
id: { type: "string", description: "Unique within the drawing; connectors reference it" },
|
|
3604
|
-
type: {
|
|
3605
|
-
enum: BOX_TYPES,
|
|
3606
|
-
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)'
|
|
3607
|
-
},
|
|
3608
|
-
label: { type: "string", description: "Small bold heading at the top of the box" },
|
|
3609
|
-
text: {
|
|
3610
|
-
type: "string",
|
|
3611
|
-
description: 'Main content, centered. Wraps automatically; "\\n" forces a line break'
|
|
3612
|
-
},
|
|
3613
|
-
footer: { type: "string", description: "Small dim line at the bottom of the box" },
|
|
3614
|
-
x: { type: "number", description: "Top-left x in px. Omit (with y) to let the layout place the box" },
|
|
3615
|
-
y: { type: "number", description: "Top-left y in px. Omit (with x) to let the layout place the box" },
|
|
3616
|
-
w: {
|
|
3617
|
-
type: "number",
|
|
3618
|
-
exclusiveMinimum: 0,
|
|
3619
|
-
description: "Width in px. Omit to size from the text (never smaller than the type default)"
|
|
3620
|
-
},
|
|
3621
|
-
h: {
|
|
3622
|
-
type: "number",
|
|
3623
|
-
exclusiveMinimum: 0,
|
|
3624
|
-
description: "Height in px. Omit to size from the text (never smaller than the type default)"
|
|
3625
|
-
},
|
|
3626
|
-
color: COLOR_SCHEMA,
|
|
3627
|
-
stroke: { type: "string", description: "CSS color of the outline and text; overrides the preset" },
|
|
3628
|
-
fill: {
|
|
3629
|
-
type: "string",
|
|
3630
|
-
description: 'CSS color of the interior ("transparent" for none); overrides the preset'
|
|
3631
|
-
}
|
|
3632
|
-
}
|
|
3633
|
-
},
|
|
3634
|
-
connector: {
|
|
3635
|
-
type: "object",
|
|
3636
|
-
required: ["from", "to"],
|
|
3637
|
-
additionalProperties: false,
|
|
3638
|
-
properties: {
|
|
3639
|
-
id: { type: "string", description: "Optional; generated when omitted" },
|
|
3640
|
-
type: {
|
|
3641
|
-
enum: CONNECTOR_TYPES,
|
|
3642
|
-
description: 'arrow (default; arrowhead at "to") or line (no arrowheads)'
|
|
3643
|
-
},
|
|
3644
|
-
from: END_SCHEMA,
|
|
3645
|
-
to: END_SCHEMA,
|
|
3646
|
-
text: { type: "string", description: "Label rendered at the middle of the connector" },
|
|
3647
|
-
bidirectional: {
|
|
3648
|
-
type: "boolean",
|
|
3649
|
-
description: "Arrow only: arrowheads on both ends"
|
|
3650
|
-
},
|
|
3651
|
-
routing: {
|
|
3652
|
-
enum: ["straight", "elbow"],
|
|
3653
|
-
description: "straight (default) draws a direct line; elbow draws a right-angled path routed around the boxes"
|
|
3654
|
-
},
|
|
3655
|
-
color: COLOR_SCHEMA,
|
|
3656
|
-
stroke: { type: "string", description: "CSS color of the line and label; overrides the preset" }
|
|
3657
|
-
}
|
|
3658
|
-
},
|
|
3659
|
-
text: {
|
|
3660
|
-
type: "object",
|
|
3661
|
-
required: ["x", "y", "text"],
|
|
3662
|
-
additionalProperties: false,
|
|
3663
|
-
properties: {
|
|
3664
|
-
id: { type: "string", description: "Optional; generated when omitted" },
|
|
3665
|
-
x: { type: "number", description: "Top-left x of the first line, in px" },
|
|
3666
|
-
y: { type: "number", description: "Top-left y of the first line, in px" },
|
|
3667
|
-
text: { type: "string", description: 'Content; "\\n" forces a line break' },
|
|
3668
|
-
color: COLOR_SCHEMA,
|
|
3669
|
-
stroke: { type: "string", description: "CSS color of the text; overrides the preset" }
|
|
3670
|
-
}
|
|
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 })
|
|
3671
3696
|
}
|
|
3672
|
-
|
|
3673
|
-
}
|
|
3697
|
+
);
|
|
3698
|
+
}
|
|
3674
3699
|
|
|
3675
3700
|
// src/nodes/DrawingNode.tsx
|
|
3676
3701
|
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
@@ -4708,24 +4733,7 @@ function HistoryButtons() {
|
|
|
4708
4733
|
}
|
|
4709
4734
|
|
|
4710
4735
|
// src/plugins/TableSettingsPlugin.tsx
|
|
4711
|
-
import { jsx as jsx14
|
|
4712
|
-
var DENSITY_OPTIONS = [
|
|
4713
|
-
{
|
|
4714
|
-
density: "compact",
|
|
4715
|
-
label: "Compact rows",
|
|
4716
|
-
icon: /* @__PURE__ */ jsx14("path", { d: "M3 4.5h14M3 8.17h14M3 11.83h14M3 15.5h14" })
|
|
4717
|
-
},
|
|
4718
|
-
{
|
|
4719
|
-
density: "comfortable",
|
|
4720
|
-
label: "Comfortable rows",
|
|
4721
|
-
icon: /* @__PURE__ */ jsx14("path", { d: "M3 4.5h14M3 10h14M3 15.5h14" })
|
|
4722
|
-
},
|
|
4723
|
-
{
|
|
4724
|
-
density: "spacious",
|
|
4725
|
-
label: "Spacious rows",
|
|
4726
|
-
icon: /* @__PURE__ */ jsx14("path", { d: "M3 5h14M3 15h14" })
|
|
4727
|
-
}
|
|
4728
|
-
];
|
|
4736
|
+
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
4729
4737
|
function Icon3({ children }) {
|
|
4730
4738
|
return /* @__PURE__ */ jsx14(
|
|
4731
4739
|
"svg",
|
|
@@ -4788,42 +4796,29 @@ function TableSettingsPlugin() {
|
|
|
4788
4796
|
[editor]
|
|
4789
4797
|
);
|
|
4790
4798
|
if (!isEditable || !anchor || !settings) return null;
|
|
4791
|
-
return /* @__PURE__ */
|
|
4799
|
+
return /* @__PURE__ */ jsx14(
|
|
4792
4800
|
"div",
|
|
4793
4801
|
{
|
|
4794
4802
|
className: "zui-table-settings",
|
|
4795
4803
|
role: "toolbar",
|
|
4796
4804
|
"aria-label": "Table settings",
|
|
4797
4805
|
style: { top: anchor.top, right: anchor.right },
|
|
4798
|
-
children:
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
)),
|
|
4809
|
-
/* @__PURE__ */ jsx14(ToolbarDivider, {}),
|
|
4810
|
-
DENSITY_OPTIONS.map(({ density, label, icon }) => /* @__PURE__ */ jsx14(
|
|
4811
|
-
ToolbarButton,
|
|
4812
|
-
{
|
|
4813
|
-
label,
|
|
4814
|
-
active: settings.density === density,
|
|
4815
|
-
onClick: () => update({ density }),
|
|
4816
|
-
children: /* @__PURE__ */ jsx14(Icon3, { children: icon })
|
|
4817
|
-
},
|
|
4818
|
-
density
|
|
4819
|
-
))
|
|
4820
|
-
]
|
|
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
|
+
))
|
|
4821
4816
|
}
|
|
4822
4817
|
);
|
|
4823
4818
|
}
|
|
4824
4819
|
|
|
4825
4820
|
// src/EditorContent.tsx
|
|
4826
|
-
import { jsx as jsx15, jsxs as
|
|
4821
|
+
import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
4827
4822
|
function EditorContent({
|
|
4828
4823
|
placeholder = "Start writing...",
|
|
4829
4824
|
foldable = true,
|
|
@@ -4844,8 +4839,8 @@ function EditorContent({
|
|
|
4844
4839
|
}
|
|
4845
4840
|
);
|
|
4846
4841
|
}
|
|
4847
|
-
return /* @__PURE__ */
|
|
4848
|
-
/* @__PURE__ */
|
|
4842
|
+
return /* @__PURE__ */ jsxs11("div", { className: "zui-text-editor-body", children: [
|
|
4843
|
+
/* @__PURE__ */ jsxs11("div", { className: "zui-text-editor-main", children: [
|
|
4849
4844
|
/* @__PURE__ */ jsx15(
|
|
4850
4845
|
RichTextPlugin,
|
|
4851
4846
|
{
|
|
@@ -4873,7 +4868,7 @@ import { useLexicalComposerContext as useLexicalComposerContext10 } from "@lexic
|
|
|
4873
4868
|
import {
|
|
4874
4869
|
TableOfContentsPlugin
|
|
4875
4870
|
} from "@lexical/react/LexicalTableOfContentsPlugin";
|
|
4876
|
-
import { jsx as jsx16, jsxs as
|
|
4871
|
+
import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
4877
4872
|
var INDENT_PER_LEVEL = {
|
|
4878
4873
|
h1: 0,
|
|
4879
4874
|
h2: 1,
|
|
@@ -4929,7 +4924,7 @@ function OutlinePlugin() {
|
|
|
4929
4924
|
for (const [key] of entries) {
|
|
4930
4925
|
editor.getElementByKey(key)?.setAttribute("data-outline-key", key);
|
|
4931
4926
|
}
|
|
4932
|
-
return /* @__PURE__ */
|
|
4927
|
+
return /* @__PURE__ */ jsxs12(
|
|
4933
4928
|
"div",
|
|
4934
4929
|
{
|
|
4935
4930
|
className: `zui-text-editor-outline ${collapsed ? "is-collapsed" : ""}`,
|
|
@@ -4958,7 +4953,7 @@ function OutlinePlugin() {
|
|
|
4958
4953
|
)
|
|
4959
4954
|
}
|
|
4960
4955
|
),
|
|
4961
|
-
!collapsed && /* @__PURE__ */
|
|
4956
|
+
!collapsed && /* @__PURE__ */ jsxs12("nav", { className: "zui-text-editor-outline-list", "aria-label": "Table of contents", children: [
|
|
4962
4957
|
entries.length === 0 && /* @__PURE__ */ jsx16("div", { className: "zui-text-editor-outline-empty", children: "No headings" }),
|
|
4963
4958
|
entries.map(([key, text, tag]) => /* @__PURE__ */ jsx16(
|
|
4964
4959
|
"button",
|
|
@@ -4981,7 +4976,7 @@ function OutlinePlugin() {
|
|
|
4981
4976
|
}
|
|
4982
4977
|
|
|
4983
4978
|
// src/MarkdownEditor.tsx
|
|
4984
|
-
import { jsx as jsx17, jsxs as
|
|
4979
|
+
import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
4985
4980
|
var DEFAULT_ITEMS = {
|
|
4986
4981
|
format: /* @__PURE__ */ jsx17(FormatButtons, {}),
|
|
4987
4982
|
insert: /* @__PURE__ */ jsx17(InsertButtons, {}),
|
|
@@ -5002,7 +4997,7 @@ function MarkdownEditor({
|
|
|
5002
4997
|
foldable = true,
|
|
5003
4998
|
...rootProps
|
|
5004
4999
|
}) {
|
|
5005
|
-
return /* @__PURE__ */
|
|
5000
|
+
return /* @__PURE__ */ jsxs13(EditorRoot, { ...rootProps, children: [
|
|
5006
5001
|
toolbar === true && /* @__PURE__ */ jsx17(EditorToolbar, {}),
|
|
5007
5002
|
typeof toolbar === "function" && toolbar(DEFAULT_ITEMS),
|
|
5008
5003
|
/* @__PURE__ */ jsx17(EditorContent, { placeholder, foldable, children: outline && /* @__PURE__ */ jsx17(OutlinePlugin, {}) })
|