@dou-ui/flow 0.0.0-stage → 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +10 -2
- package/dist/collaboration/index.js +360 -0
- package/dist/collaboration/index.js.map +1 -0
- package/dist/collaboration/peers.js +76 -0
- package/dist/collaboration/peers.js.map +1 -0
- package/dist/components/default-node.js +6 -0
- package/dist/components/default-node.js.map +1 -0
- package/dist/components/default-node.vue_vue_type_script_setup_true_lang.js +87 -0
- package/dist/components/default-node.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/default-node2.js +116 -0
- package/dist/components/default-node2.js.map +1 -0
- package/dist/components/flow-controls.js +6 -0
- package/dist/components/flow-controls.js.map +1 -0
- package/dist/components/flow-controls.vue_vue_type_script_setup_true_lang.js +148 -0
- package/dist/components/flow-controls.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/flow-menu.js +6 -0
- package/dist/components/flow-menu.js.map +1 -0
- package/dist/components/flow-menu.vue_vue_type_script_setup_true_lang.js +52 -0
- package/dist/components/flow-menu.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/flow-minimap.js +6 -0
- package/dist/components/flow-minimap.js.map +1 -0
- package/dist/components/flow-minimap.vue_vue_type_script_setup_true_lang.js +238 -0
- package/dist/components/flow-minimap.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/flow-port.js +6 -0
- package/dist/components/flow-port.js.map +1 -0
- package/dist/components/flow-port.vue_vue_type_script_setup_true_lang.js +57 -0
- package/dist/components/flow-port.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/flow-quick-add.js +6 -0
- package/dist/components/flow-quick-add.js.map +1 -0
- package/dist/components/flow-quick-add.vue_vue_type_script_setup_true_lang.js +126 -0
- package/dist/components/flow-quick-add.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/flow-toolbar.js +6 -0
- package/dist/components/flow-toolbar.js.map +1 -0
- package/dist/components/flow-toolbar.vue_vue_type_script_setup_true_lang.js +161 -0
- package/dist/components/flow-toolbar.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/group-node.js +6 -0
- package/dist/components/group-node.js.map +1 -0
- package/dist/components/group-node.vue_vue_type_script_setup_true_lang.js +84 -0
- package/dist/components/group-node.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/group-node2.js +42 -0
- package/dist/components/group-node2.js.map +1 -0
- package/dist/components/node-view.js +6 -0
- package/dist/components/node-view.js.map +1 -0
- package/dist/components/node-view.vue_vue_type_script_setup_true_lang.js +179 -0
- package/dist/components/node-view.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/context.js +13 -0
- package/dist/context.js.map +1 -0
- package/dist/core/clipboard.js +37 -0
- package/dist/core/clipboard.js.map +1 -0
- package/dist/core/connect.js +74 -0
- package/dist/core/connect.js.map +1 -0
- package/dist/core/edge-types.js +240 -0
- package/dist/core/edge-types.js.map +1 -0
- package/dist/core/extension.js +63 -0
- package/dist/core/extension.js.map +1 -0
- package/dist/core/geometry.js +232 -0
- package/dist/core/geometry.js.map +1 -0
- package/dist/core/groups.js +76 -0
- package/dist/core/groups.js.map +1 -0
- package/dist/core/history.js +79 -0
- package/dist/core/history.js.map +1 -0
- package/dist/core/model.js +21 -0
- package/dist/core/model.js.map +1 -0
- package/dist/core/node-types.js +32 -0
- package/dist/core/node-types.js.map +1 -0
- package/dist/core/order.js +42 -0
- package/dist/core/order.js.map +1 -0
- package/dist/core/transaction.js +288 -0
- package/dist/core/transaction.js.map +1 -0
- package/dist/document.js +220 -0
- package/dist/document.js.map +1 -0
- package/dist/export/color.js +38 -0
- package/dist/export/color.js.map +1 -0
- package/dist/export/context.js +223 -0
- package/dist/export/context.js.map +1 -0
- package/dist/export/draw.js +249 -0
- package/dist/export/draw.js.map +1 -0
- package/dist/flow-editor.js +6 -0
- package/dist/flow-editor.js.map +1 -0
- package/dist/flow-editor.vue_vue_type_script_setup_true_lang.js +3721 -0
- package/dist/flow-editor.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/kit.js +234 -0
- package/dist/kit.js.map +1 -0
- package/dist/layers.js +150 -0
- package/dist/layers.js.map +1 -0
- package/dist/locale.js +426 -0
- package/dist/locale.js.map +1 -0
- package/dist/style.css +1 -0
- package/dist/style.d.ts +1 -0
- package/dist/style.js +8 -0
- package/dist/types/collaboration/index.d.ts +42 -0
- package/dist/types/collaboration/peers.d.ts +27 -0
- package/dist/types/components/default-node.d.ts +18 -0
- package/dist/types/components/default-node.vue.d.ts +6 -0
- package/dist/types/components/flow-controls.vue.d.ts +8 -0
- package/dist/types/components/flow-menu.vue.d.ts +29 -0
- package/dist/types/components/flow-minimap.vue.d.ts +8 -0
- package/dist/types/components/flow-port.vue.d.ts +9 -0
- package/dist/types/components/flow-quick-add.vue.d.ts +31 -0
- package/dist/types/components/flow-toolbar.vue.d.ts +25 -0
- package/dist/types/components/group-node.d.ts +13 -0
- package/dist/types/components/group-node.vue.d.ts +6 -0
- package/dist/types/components/node-view.vue.d.ts +28 -0
- package/dist/types/context.d.ts +362 -0
- package/dist/types/core/clipboard.d.ts +9 -0
- package/dist/types/core/connect.d.ts +63 -0
- package/dist/types/core/edge-types.d.ts +50 -0
- package/dist/types/core/extension.d.ts +141 -0
- package/dist/types/core/geometry.d.ts +63 -0
- package/dist/types/core/groups.d.ts +23 -0
- package/dist/types/core/history.d.ts +24 -0
- package/dist/types/core/menu.d.ts +40 -0
- package/dist/types/core/model.d.ts +87 -0
- package/dist/types/core/node-types.d.ts +103 -0
- package/dist/types/core/order.d.ts +7 -0
- package/dist/types/core/transaction.d.ts +97 -0
- package/dist/types/document.d.ts +57 -0
- package/dist/types/export/color.d.ts +7 -0
- package/dist/types/export/context.d.ts +90 -0
- package/dist/types/export/draw.d.ts +72 -0
- package/dist/types/flow-editor.vue.d.ts +63 -0
- package/dist/types/index.d.ts +202 -0
- package/dist/types/kit.d.ts +22 -0
- package/dist/types/layers.d.ts +24 -0
- package/dist/types/locale.d.ts +172 -0
- package/dist/types/whiteboard/board-draft.vue.d.ts +3 -0
- package/dist/types/whiteboard/board-node.vue.d.ts +19 -0
- package/dist/types/whiteboard/connector.d.ts +12 -0
- package/dist/types/whiteboard/draw.d.ts +5 -0
- package/dist/types/whiteboard/image.d.ts +27 -0
- package/dist/types/whiteboard/index.d.ts +6 -0
- package/dist/types/whiteboard/ink.d.ts +30 -0
- package/dist/types/whiteboard/kit.d.ts +36 -0
- package/dist/types/whiteboard/model.d.ts +79 -0
- package/dist/types/whiteboard/pen.d.ts +7 -0
- package/dist/types/whiteboard/state.d.ts +37 -0
- package/dist/types/whiteboard/transform.d.ts +67 -0
- package/dist/types/whiteboard/whiteboard-toolbar.vue.d.ts +3 -0
- package/dist/whiteboard/board-draft.js +6 -0
- package/dist/whiteboard/board-draft.js.map +1 -0
- package/dist/whiteboard/board-draft.vue_vue_type_script_setup_true_lang.js +58 -0
- package/dist/whiteboard/board-draft.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/whiteboard/board-node.js +6 -0
- package/dist/whiteboard/board-node.js.map +1 -0
- package/dist/whiteboard/board-node.vue_vue_type_script_setup_true_lang.js +332 -0
- package/dist/whiteboard/board-node.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/whiteboard/connector.js +180 -0
- package/dist/whiteboard/connector.js.map +1 -0
- package/dist/whiteboard/draw.js +68 -0
- package/dist/whiteboard/draw.js.map +1 -0
- package/dist/whiteboard/image.js +104 -0
- package/dist/whiteboard/image.js.map +1 -0
- package/dist/whiteboard/index.js +5 -0
- package/dist/whiteboard/ink.js +168 -0
- package/dist/whiteboard/ink.js.map +1 -0
- package/dist/whiteboard/kit.js +406 -0
- package/dist/whiteboard/kit.js.map +1 -0
- package/dist/whiteboard/model.js +52 -0
- package/dist/whiteboard/model.js.map +1 -0
- package/dist/whiteboard/pen.js +88 -0
- package/dist/whiteboard/pen.js.map +1 -0
- package/dist/whiteboard/state.js +26 -0
- package/dist/whiteboard/state.js.map +1 -0
- package/dist/whiteboard/transform.js +256 -0
- package/dist/whiteboard/transform.js.map +1 -0
- package/dist/whiteboard/whiteboard-toolbar.js +6 -0
- package/dist/whiteboard/whiteboard-toolbar.js.map +1 -0
- package/dist/whiteboard/whiteboard-toolbar.vue_vue_type_script_setup_true_lang.js +171 -0
- package/dist/whiteboard/whiteboard-toolbar.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/package.json +74 -4
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { plainColor } from "./color.js";
|
|
2
|
+
import { h, render } from "vue";
|
|
3
|
+
import { ImageOffIcon } from "@dou-ui/icons";
|
|
4
|
+
// src/export/context.ts
|
|
5
|
+
var escapeXml = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, "");
|
|
6
|
+
var round = (value) => Math.round(value * 100) / 100;
|
|
7
|
+
/** Characters a line may break after, as CSS breaks Chinese, Japanese and Korean text. */
|
|
8
|
+
var WIDE = /[⺀-鿿가-豈-︰-﹏-]/;
|
|
9
|
+
var TOKENS = /[⺀-鿿가-豈-︰-﹏-]|[^\s⺀-鿿가-豈-︰-﹏-]+\s*|\s+/g;
|
|
10
|
+
/**
|
|
11
|
+
* Lines of text that fit `max`: breaks between words and between wide
|
|
12
|
+
* characters, inside a word only when it alone is too long, and at new lines.
|
|
13
|
+
*/
|
|
14
|
+
function wrapText(text, max, measure) {
|
|
15
|
+
const out = [];
|
|
16
|
+
const breakLong = (word) => {
|
|
17
|
+
let line = "";
|
|
18
|
+
for (const char of word) {
|
|
19
|
+
if (line && measure(line + char) > max) {
|
|
20
|
+
out.push(line);
|
|
21
|
+
line = "";
|
|
22
|
+
}
|
|
23
|
+
line += char;
|
|
24
|
+
}
|
|
25
|
+
return line;
|
|
26
|
+
};
|
|
27
|
+
for (const paragraph of text.split("\n")) {
|
|
28
|
+
let line = "";
|
|
29
|
+
for (const token of paragraph.match(TOKENS) ?? []) {
|
|
30
|
+
const next = line + token;
|
|
31
|
+
if (!line.trim() || measure(next.trimEnd()) <= max) {
|
|
32
|
+
line = next;
|
|
33
|
+
if (measure(line.trimEnd()) > max) line = breakLong(line.trimEnd());
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
out.push(line.trimEnd());
|
|
37
|
+
line = WIDE.test(token) ? token : token.trimStart();
|
|
38
|
+
if (measure(line.trimEnd()) > max) line = breakLong(line.trimEnd());
|
|
39
|
+
}
|
|
40
|
+
out.push(line.trimEnd());
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/** Cuts text to fit `max`, with an ellipsis. */
|
|
45
|
+
function cutText(text, max, measure, more = false) {
|
|
46
|
+
if (more) text = text.trimEnd();
|
|
47
|
+
if (!more && measure(text) <= max) return text;
|
|
48
|
+
let lo = 0;
|
|
49
|
+
let hi = text.length;
|
|
50
|
+
while (lo < hi) {
|
|
51
|
+
const mid = lo + hi + 1 >> 1;
|
|
52
|
+
if (measure(`${text.slice(0, mid)}…`) <= max) lo = mid;
|
|
53
|
+
else hi = mid - 1;
|
|
54
|
+
}
|
|
55
|
+
return lo ? `${text.slice(0, lo)}…` : "";
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The tools a drawing needs, reading the page inside `root`: styles from
|
|
59
|
+
* elements made in a hidden box there, text measured with the editor's font.
|
|
60
|
+
* `imageHref` turns an image address into the one written out.
|
|
61
|
+
*/
|
|
62
|
+
function drawTools(root, messages, imageHref = (src) => src) {
|
|
63
|
+
const box = document.createElement("div");
|
|
64
|
+
box.setAttribute("aria-hidden", "true");
|
|
65
|
+
box.style.cssText = "position:absolute;top:0;left:0;width:0;height:0;overflow:hidden;visibility:hidden;pointer-events:none";
|
|
66
|
+
root.append(box);
|
|
67
|
+
const rootStyle = getComputedStyle(root);
|
|
68
|
+
const fontFamily = rootStyle.fontFamily || "sans-serif";
|
|
69
|
+
const fontSize = Number.parseFloat(rootStyle.fontSize) || 14;
|
|
70
|
+
const styles = /* @__PURE__ */ new Map();
|
|
71
|
+
const style = (...classes) => {
|
|
72
|
+
const key = classes.join("\n");
|
|
73
|
+
let found = styles.get(key);
|
|
74
|
+
if (!found) {
|
|
75
|
+
let parent = box;
|
|
76
|
+
for (const name of classes) {
|
|
77
|
+
const el = document.createElement("div");
|
|
78
|
+
el.className = name;
|
|
79
|
+
parent.append(el);
|
|
80
|
+
parent = el;
|
|
81
|
+
}
|
|
82
|
+
found = getComputedStyle(parent);
|
|
83
|
+
styles.set(key, found);
|
|
84
|
+
}
|
|
85
|
+
return found;
|
|
86
|
+
};
|
|
87
|
+
const colors = /* @__PURE__ */ new Map();
|
|
88
|
+
const swatch = document.createElement("div");
|
|
89
|
+
box.append(swatch);
|
|
90
|
+
const color = (value) => {
|
|
91
|
+
let found = colors.get(value);
|
|
92
|
+
if (found === void 0) {
|
|
93
|
+
swatch.style.color = "";
|
|
94
|
+
swatch.style.color = value;
|
|
95
|
+
found = plainColor(getComputedStyle(swatch).color || value);
|
|
96
|
+
colors.set(value, found);
|
|
97
|
+
}
|
|
98
|
+
return found;
|
|
99
|
+
};
|
|
100
|
+
let pen;
|
|
101
|
+
const widths = /* @__PURE__ */ new Map();
|
|
102
|
+
const measure = (text, size, weight = 400) => {
|
|
103
|
+
const font = `${weight} ${size}px ${fontFamily}`;
|
|
104
|
+
const key = `${font}\n${text}`;
|
|
105
|
+
let width = widths.get(key);
|
|
106
|
+
if (width !== void 0) return width;
|
|
107
|
+
if (pen === void 0) try {
|
|
108
|
+
pen = document.createElement("canvas").getContext("2d");
|
|
109
|
+
} catch {
|
|
110
|
+
pen = null;
|
|
111
|
+
}
|
|
112
|
+
if (pen) {
|
|
113
|
+
pen.font = font;
|
|
114
|
+
width = pen.measureText(text).width;
|
|
115
|
+
} else {
|
|
116
|
+
width = 0;
|
|
117
|
+
for (const char of text) width += WIDE.test(char) ? size : size * .55;
|
|
118
|
+
}
|
|
119
|
+
widths.set(key, width);
|
|
120
|
+
return width;
|
|
121
|
+
};
|
|
122
|
+
/** Where the baseline goes to centre a line of `size` in its line box. */
|
|
123
|
+
const baselineOffset = (size) => {
|
|
124
|
+
if (pen) {
|
|
125
|
+
pen.font = `400 ${size}px ${fontFamily}`;
|
|
126
|
+
const m = pen.measureText("中Hg");
|
|
127
|
+
if (m.fontBoundingBoxAscent) return (m.fontBoundingBoxAscent - m.fontBoundingBoxDescent) / 2;
|
|
128
|
+
}
|
|
129
|
+
return size * .35;
|
|
130
|
+
};
|
|
131
|
+
const text = (value, area, options = {}) => {
|
|
132
|
+
if (!value) return "";
|
|
133
|
+
const size = options.size ?? fontSize;
|
|
134
|
+
const weight = options.weight ?? 400;
|
|
135
|
+
const lineHeight = options.lineHeight ?? size * 1.5;
|
|
136
|
+
const width = (line) => measure(line, size, weight);
|
|
137
|
+
let lines = options.nowrap ? [cutText(value.replace(/\s+/g, " "), area.width, width)] : wrapText(value, area.width, width);
|
|
138
|
+
const room = Math.max(1, Math.floor((area.height + .5) / lineHeight));
|
|
139
|
+
if (lines.length > room) {
|
|
140
|
+
lines = lines.slice(0, room);
|
|
141
|
+
lines[room - 1] = cutText(lines[room - 1], area.width, width, true);
|
|
142
|
+
}
|
|
143
|
+
const total = lines.length * lineHeight;
|
|
144
|
+
const top = options.valign === "middle" ? area.y + (area.height - total) / 2 : area.y;
|
|
145
|
+
const align = options.align ?? "start";
|
|
146
|
+
const x = align === "center" ? area.x + area.width / 2 : align === "end" ? area.x + area.width : area.x;
|
|
147
|
+
const anchor = align === "center" ? "middle" : align === "end" ? "end" : "start";
|
|
148
|
+
const shift = baselineOffset(size);
|
|
149
|
+
const fill = options.color ? ` fill="${options.color}"` : "";
|
|
150
|
+
const bold = weight === 400 || weight === "400" ? "" : ` font-weight="${weight}"`;
|
|
151
|
+
const spans = lines.map((line, i) => line ? `<tspan x="${round(x)}" y="${round(top + lineHeight * i + lineHeight / 2 + shift)}">${escapeXml(line)}</tspan>` : "").join("");
|
|
152
|
+
return spans ? `<text font-size="${round(size)}"${bold}${fill} text-anchor="${anchor}" xml:space="preserve">${spans}</text>` : "";
|
|
153
|
+
};
|
|
154
|
+
const holder = document.createElement("div");
|
|
155
|
+
const icons = /* @__PURE__ */ new Map();
|
|
156
|
+
const icon = (component, area, fill) => {
|
|
157
|
+
let markup = icons.get(component);
|
|
158
|
+
if (markup === void 0) {
|
|
159
|
+
try {
|
|
160
|
+
render(h(component), holder);
|
|
161
|
+
markup = holder.querySelector("svg")?.outerHTML ?? "";
|
|
162
|
+
} catch {
|
|
163
|
+
markup = "";
|
|
164
|
+
}
|
|
165
|
+
render(null, holder);
|
|
166
|
+
icons.set(component, markup);
|
|
167
|
+
}
|
|
168
|
+
if (!markup) return "";
|
|
169
|
+
return markup.replace(/^<svg\b([^>]*)>/, (_, attrs) => {
|
|
170
|
+
return `<svg${attrs.replace(/\s(width|height|x|y|class|style|aria-[\w-]+|role)="[^"]*"/g, "")} x="${round(area.x)}" y="${round(area.y)}" width="${round(area.width)}" height="${round(area.height)}">`;
|
|
171
|
+
}).replace(/currentColor/g, fill);
|
|
172
|
+
};
|
|
173
|
+
const images = /* @__PURE__ */ new Set();
|
|
174
|
+
let clips = 0;
|
|
175
|
+
const image = (src, area, options = {}) => {
|
|
176
|
+
images.add(src);
|
|
177
|
+
const href = imageHref(src);
|
|
178
|
+
const { x, y, width, height } = area;
|
|
179
|
+
if (href === null) {
|
|
180
|
+
const side = Math.min(24, width * .4, height * .4);
|
|
181
|
+
return `<rect x="${round(x)}" y="${round(y)}" width="${round(width)}" height="${round(height)}" rx="${options.radius ?? 0}" fill="${color("var(--dou-color-bg-muted)")}"/>` + icon(ImageOffIcon, {
|
|
182
|
+
x: x + (width - side) / 2,
|
|
183
|
+
y: y + (height - side) / 2,
|
|
184
|
+
width: side,
|
|
185
|
+
height: side
|
|
186
|
+
}, color("var(--dou-color-text-tertiary)"));
|
|
187
|
+
}
|
|
188
|
+
let clip = "";
|
|
189
|
+
let defs = "";
|
|
190
|
+
if (options.radius) {
|
|
191
|
+
const id = `dou-flow-clip-${++clips}`;
|
|
192
|
+
defs = `<clipPath id="${id}"><rect x="${round(x)}" y="${round(y)}" width="${round(width)}" height="${round(height)}" rx="${options.radius}"/></clipPath>`;
|
|
193
|
+
clip = ` clip-path="url(#${id})"`;
|
|
194
|
+
}
|
|
195
|
+
return `${defs}<image href="${escapeXml(href)}" x="${round(x)}" y="${round(y)}" width="${round(width)}" height="${round(height)}" preserveAspectRatio="xMidYMid meet"${clip}/>`;
|
|
196
|
+
};
|
|
197
|
+
return {
|
|
198
|
+
forNode: (size, ports, port) => ({
|
|
199
|
+
width: size.width,
|
|
200
|
+
height: size.height,
|
|
201
|
+
ports,
|
|
202
|
+
port,
|
|
203
|
+
messages,
|
|
204
|
+
style,
|
|
205
|
+
color,
|
|
206
|
+
text,
|
|
207
|
+
icon,
|
|
208
|
+
image,
|
|
209
|
+
escape: escapeXml
|
|
210
|
+
}),
|
|
211
|
+
style,
|
|
212
|
+
color,
|
|
213
|
+
text,
|
|
214
|
+
measure,
|
|
215
|
+
fontFamily,
|
|
216
|
+
fontSize,
|
|
217
|
+
images,
|
|
218
|
+
dispose: () => box.remove()
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
export { cutText, drawTools, escapeXml, round, wrapText };
|
|
222
|
+
|
|
223
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","names":[],"sources":["../../src/export/context.ts"],"sourcesContent":["import { ImageOffIcon } from '@dou-ui/icons'\nimport { h, render, type Component } from 'vue'\nimport type { PortAnchor } from '../context'\nimport type { Rect } from '../core/model'\nimport type { FlowMessages } from '../locale'\nimport { plainColor } from './color'\n\nexport interface FlowDrawTextOptions {\n /** In px; the editor's font size by default. */\n size?: number\n /** A CSS font weight, e.g. `500`. */\n weight?: string | number\n /** Line height in px; 1.5 times the size by default. */\n lineHeight?: number\n color?: string\n align?: 'start' | 'center' | 'end'\n /** Where the lines sit in the box when they take less than its height. */\n valign?: 'top' | 'middle'\n /** Keeps it to one line, cut with an ellipsis. */\n nowrap?: boolean\n}\n\n/**\n * What a node type's `draw` gets to draw a node as SVG, for `exportSVG` and\n * `exportPNG`. Coordinates are the node's own: `0, 0` is its top left.\n */\nexport interface FlowDrawContext {\n /** The node's size. */\n width: number\n height: number\n /** Its ports where they are on screen, relative to its top left. */\n ports: ReadonlyMap<string, PortAnchor>\n /**\n * A port as the editor draws it, where it is on screen: its ring or shape\n * in its type's color, filled once connected. Empty for a port not shown.\n */\n port: (id: string) => string\n /** The editor's current strings. */\n messages: FlowMessages\n /**\n * The computed style of an element made with these classes, outermost\n * first, inside the editor so its theme and variables apply. Pass colors\n * read from it through `color`.\n */\n style: (...classes: string[]) => CSSStyleDeclaration\n /**\n * A CSS color, also one with variables such as `var(--dou-color-text)`,\n * as `rgb(…)`, `rgba(…)` or `#rrggbb`, which every SVG reader understands.\n */\n color: (value: string) => string\n /** Text in a box, wrapped as on screen and cut where the box ends. */\n text: (value: string, box: Rect, options?: FlowDrawTextOptions) => string\n /** An icon component, e.g. from `@dou-ui/icons`, as SVG. */\n icon: (icon: Component, box: Rect, color: string) => string\n /** An image; in a PNG it is embedded when its address can be read. */\n image: (src: string, box: Rect, options?: { radius?: number }) => string\n /** Escapes text for SVG markup. */\n escape: (text: string) => string\n}\n\nexport const escapeXml = (text: string) =>\n text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n // control characters other than tab and new line aren't allowed in XML\n // oxlint-disable-next-line no-control-regex -- strips them\n .replace(/[\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F]/g, '')\n\nexport const round = (value: number) => Math.round(value * 100) / 100\n\n/** Characters a line may break after, as CSS breaks Chinese, Japanese and Korean text. */\nconst WIDE = /[⺀-鿿가-豈-︰-﹏-]/\nconst TOKENS = /[⺀-鿿가-豈-︰-﹏-]|[^\\s⺀-鿿가-豈-︰-﹏-]+\\s*|\\s+/g\n\n/**\n * Lines of text that fit `max`: breaks between words and between wide\n * characters, inside a word only when it alone is too long, and at new lines.\n */\nexport function wrapText(text: string, max: number, measure: (text: string) => number): string[] {\n const out: string[] = []\n const breakLong = (word: string) => {\n let line = ''\n for (const char of word) {\n if (line && measure(line + char) > max) {\n out.push(line)\n line = ''\n }\n line += char\n }\n return line\n }\n for (const paragraph of text.split('\\n')) {\n let line = ''\n for (const token of paragraph.match(TOKENS) ?? []) {\n const next = line + token\n if (!line.trim() || measure(next.trimEnd()) <= max) {\n line = next\n if (measure(line.trimEnd()) > max) line = breakLong(line.trimEnd())\n continue\n }\n out.push(line.trimEnd())\n line = WIDE.test(token) ? token : token.trimStart()\n if (measure(line.trimEnd()) > max) line = breakLong(line.trimEnd())\n }\n out.push(line.trimEnd())\n }\n return out\n}\n\n/** Cuts text to fit `max`, with an ellipsis. */\nexport function cutText(\n text: string,\n max: number,\n measure: (text: string) => number,\n /** Ends it with an ellipsis even when it fits: more text follows. */\n more = false,\n) {\n if (more) text = text.trimEnd()\n if (!more && measure(text) <= max) return text\n let lo = 0\n let hi = text.length\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1\n if (measure(`${text.slice(0, mid)}…`) <= max) lo = mid\n else hi = mid - 1\n }\n return lo ? `${text.slice(0, lo)}…` : ''\n}\n\nexport interface DrawTools {\n /** Makes the context for one node. */\n forNode: (\n size: { width: number; height: number },\n ports: ReadonlyMap<string, PortAnchor>,\n port: (id: string) => string,\n ) => FlowDrawContext\n style: FlowDrawContext['style']\n color: FlowDrawContext['color']\n text: FlowDrawContext['text']\n measure: (text: string, size: number, weight?: string | number) => number\n fontFamily: string\n fontSize: number\n /** Image addresses the drawing used. */\n images: Set<string>\n dispose: () => void\n}\n\n/**\n * The tools a drawing needs, reading the page inside `root`: styles from\n * elements made in a hidden box there, text measured with the editor's font.\n * `imageHref` turns an image address into the one written out.\n */\nexport function drawTools(\n root: HTMLElement,\n messages: FlowMessages,\n imageHref: (src: string) => string | null = (src) => src,\n): DrawTools {\n const box = document.createElement('div')\n box.setAttribute('aria-hidden', 'true')\n box.style.cssText =\n 'position:absolute;top:0;left:0;width:0;height:0;overflow:hidden;visibility:hidden;pointer-events:none'\n root.append(box)\n const rootStyle = getComputedStyle(root)\n const fontFamily = rootStyle.fontFamily || 'sans-serif'\n const fontSize = Number.parseFloat(rootStyle.fontSize) || 14\n\n const styles = new Map<string, CSSStyleDeclaration>()\n const style = (...classes: string[]) => {\n const key = classes.join('\\n')\n let found = styles.get(key)\n if (!found) {\n let parent: HTMLElement = box\n for (const name of classes) {\n const el = document.createElement('div')\n el.className = name\n parent.append(el)\n parent = el\n }\n found = getComputedStyle(parent)\n styles.set(key, found)\n }\n return found\n }\n\n const colors = new Map<string, string>()\n const swatch = document.createElement('div')\n box.append(swatch)\n const color = (value: string) => {\n let found = colors.get(value)\n if (found === undefined) {\n swatch.style.color = ''\n swatch.style.color = value\n found = plainColor(getComputedStyle(swatch).color || value)\n colors.set(value, found)\n }\n return found\n }\n\n let pen: CanvasRenderingContext2D | null | undefined\n const widths = new Map<string, number>()\n const measure = (text: string, size: number, weight: string | number = 400) => {\n const font = `${weight} ${size}px ${fontFamily}`\n const key = `${font}\\n${text}`\n let width = widths.get(key)\n if (width !== undefined) return width\n if (pen === undefined)\n try {\n pen = document.createElement('canvas').getContext('2d')\n } catch {\n pen = null\n }\n if (pen) {\n pen.font = font\n width = pen.measureText(text).width\n } else {\n // without a canvas, e.g. in tests: wide characters are square, others half\n width = 0\n for (const char of text) width += WIDE.test(char) ? size : size * 0.55\n }\n widths.set(key, width)\n return width\n }\n\n /** Where the baseline goes to centre a line of `size` in its line box. */\n const baselineOffset = (size: number) => {\n if (pen) {\n pen.font = `400 ${size}px ${fontFamily}`\n const m = pen.measureText('中Hg')\n if (m.fontBoundingBoxAscent) return (m.fontBoundingBoxAscent - m.fontBoundingBoxDescent) / 2\n }\n return size * 0.35\n }\n\n const text: FlowDrawContext['text'] = (value, area, options = {}) => {\n if (!value) return ''\n const size = options.size ?? fontSize\n const weight = options.weight ?? 400\n const lineHeight = options.lineHeight ?? size * 1.5\n const width = (line: string) => measure(line, size, weight)\n let lines = options.nowrap\n ? [cutText(value.replace(/\\s+/g, ' '), area.width, width)]\n : wrapText(value, area.width, width)\n // whole lines that fit, as the box clips them on screen\n const room = Math.max(1, Math.floor((area.height + 0.5) / lineHeight))\n if (lines.length > room) {\n lines = lines.slice(0, room)\n lines[room - 1] = cutText(lines[room - 1]!, area.width, width, true)\n }\n const total = lines.length * lineHeight\n const top = options.valign === 'middle' ? area.y + (area.height - total) / 2 : area.y\n const align = options.align ?? 'start'\n const x =\n align === 'center' ? area.x + area.width / 2 : align === 'end' ? area.x + area.width : area.x\n const anchor = align === 'center' ? 'middle' : align === 'end' ? 'end' : 'start'\n const shift = baselineOffset(size)\n const fill = options.color ? ` fill=\"${options.color}\"` : ''\n const bold = weight === 400 || weight === '400' ? '' : ` font-weight=\"${weight}\"`\n const spans = lines\n .map((line, i) =>\n line\n ? `<tspan x=\"${round(x)}\" y=\"${round(top + lineHeight * i + lineHeight / 2 + shift)}\">${escapeXml(line)}</tspan>`\n : '',\n )\n .join('')\n return spans\n ? `<text font-size=\"${round(size)}\"${bold}${fill} text-anchor=\"${anchor}\" xml:space=\"preserve\">${spans}</text>`\n : ''\n }\n\n const holder = document.createElement('div')\n const icons = new Map<Component, string>()\n const icon: FlowDrawContext['icon'] = (component, area, fill) => {\n let markup = icons.get(component)\n if (markup === undefined) {\n try {\n render(h(component), holder)\n markup = holder.querySelector('svg')?.outerHTML ?? ''\n } catch {\n markup = ''\n }\n render(null, holder)\n icons.set(component, markup)\n }\n if (!markup) return ''\n const svg = markup\n .replace(/^<svg\\b([^>]*)>/, (_, attrs: string) => {\n const kept = attrs.replace(/\\s(width|height|x|y|class|style|aria-[\\w-]+|role)=\"[^\"]*\"/g, '')\n return `<svg${kept} x=\"${round(area.x)}\" y=\"${round(area.y)}\" width=\"${round(area.width)}\" height=\"${round(area.height)}\">`\n })\n .replace(/currentColor/g, fill)\n return svg\n }\n\n const images = new Set<string>()\n let clips = 0\n const image: FlowDrawContext['image'] = (src, area, options = {}) => {\n images.add(src)\n const href = imageHref(src)\n const { x, y, width, height } = area\n if (href === null) {\n // not readable here: a plain box with a sign, as the canvas shows it\n const side = Math.min(24, width * 0.4, height * 0.4)\n return (\n `<rect x=\"${round(x)}\" y=\"${round(y)}\" width=\"${round(width)}\" height=\"${round(height)}\" rx=\"${options.radius ?? 0}\" fill=\"${color('var(--dou-color-bg-muted)')}\"/>` +\n icon(\n ImageOffIcon,\n { x: x + (width - side) / 2, y: y + (height - side) / 2, width: side, height: side },\n color('var(--dou-color-text-tertiary)'),\n )\n )\n }\n let clip = ''\n let defs = ''\n if (options.radius) {\n const id = `dou-flow-clip-${++clips}`\n defs = `<clipPath id=\"${id}\"><rect x=\"${round(x)}\" y=\"${round(y)}\" width=\"${round(width)}\" height=\"${round(height)}\" rx=\"${options.radius}\"/></clipPath>`\n clip = ` clip-path=\"url(#${id})\"`\n }\n return `${defs}<image href=\"${escapeXml(href)}\" x=\"${round(x)}\" y=\"${round(y)}\" width=\"${round(width)}\" height=\"${round(height)}\" preserveAspectRatio=\"xMidYMid meet\"${clip}/>`\n }\n\n return {\n forNode: (size, ports, port) => ({\n width: size.width,\n height: size.height,\n ports,\n port,\n messages,\n style,\n color,\n text,\n icon,\n image,\n escape: escapeXml,\n }),\n style,\n color,\n text,\n measure,\n fontFamily,\n fontSize,\n images,\n dispose: () => box.remove(),\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { nodeBounds } from "../core/node-types.js";
|
|
2
|
+
import { escapeXml, round } from "./context.js";
|
|
3
|
+
// src/export/draw.ts
|
|
4
|
+
/** The points a path passes through or bends towards; the curve stays inside them. */
|
|
5
|
+
function pathPoints(path) {
|
|
6
|
+
const numbers = path.match(/-?\d*\.?\d+(?:e[-+]?\d+)?/gi)?.map(Number) ?? [];
|
|
7
|
+
const out = [];
|
|
8
|
+
for (let i = 0; i + 1 < numbers.length; i += 2) out.push({
|
|
9
|
+
x: numbers[i],
|
|
10
|
+
y: numbers[i + 1]
|
|
11
|
+
});
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
/** A box that grows to hold what is added. */
|
|
15
|
+
function extent() {
|
|
16
|
+
let minX = Infinity;
|
|
17
|
+
let minY = Infinity;
|
|
18
|
+
let maxX = -Infinity;
|
|
19
|
+
let maxY = -Infinity;
|
|
20
|
+
return {
|
|
21
|
+
add(rect) {
|
|
22
|
+
minX = Math.min(minX, rect.x);
|
|
23
|
+
minY = Math.min(minY, rect.y);
|
|
24
|
+
maxX = Math.max(maxX, rect.x + rect.width);
|
|
25
|
+
maxY = Math.max(maxY, rect.y + rect.height);
|
|
26
|
+
},
|
|
27
|
+
get() {
|
|
28
|
+
return minX === Infinity ? null : {
|
|
29
|
+
x: minX,
|
|
30
|
+
y: minY,
|
|
31
|
+
width: maxX - minX,
|
|
32
|
+
height: maxY - minY
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
var px = (value, fallback = 0) => {
|
|
38
|
+
const n = Number.parseFloat(value);
|
|
39
|
+
return Number.isFinite(n) ? n : fallback;
|
|
40
|
+
};
|
|
41
|
+
/** A card like the editor's frame: border, background and radius. */
|
|
42
|
+
function drawCard(draw, classes = []) {
|
|
43
|
+
const s = draw.style(["dou-flow-editor__node", ...classes].join(" "));
|
|
44
|
+
const border = px(s.borderTopWidth, 1);
|
|
45
|
+
const radius = px(s.borderTopLeftRadius, 8);
|
|
46
|
+
const inset = border / 2;
|
|
47
|
+
return `<rect x="${inset}" y="${inset}" width="${round(Math.max(0, draw.width - border))}" height="${round(Math.max(0, draw.height - border))}" rx="${round(Math.max(0, radius - inset))}" fill="${draw.color(s.backgroundColor)}" stroke="${draw.color(s.borderTopColor)}" stroke-width="${border}"/>`;
|
|
48
|
+
}
|
|
49
|
+
/** What a node type without `draw` is drawn as: its card, its name and its ports. */
|
|
50
|
+
function drawFallback(draw, label, framed) {
|
|
51
|
+
const parts = framed ? [drawCard(draw)] : [];
|
|
52
|
+
for (const id of draw.ports.keys()) parts.push(draw.port(id));
|
|
53
|
+
parts.push(draw.text(label, {
|
|
54
|
+
x: 12,
|
|
55
|
+
y: 0,
|
|
56
|
+
width: Math.max(0, draw.width - 24),
|
|
57
|
+
height: draw.height
|
|
58
|
+
}, {
|
|
59
|
+
align: "center",
|
|
60
|
+
valign: "middle",
|
|
61
|
+
color: draw.color("var(--dou-color-text)")
|
|
62
|
+
}));
|
|
63
|
+
return parts.join("");
|
|
64
|
+
}
|
|
65
|
+
/** The edges' look, read from paths made like the editor's. */
|
|
66
|
+
function edgeLook(tools, root) {
|
|
67
|
+
const NS = "http://www.w3.org/2000/svg";
|
|
68
|
+
const svg = document.createElementNS(NS, "svg");
|
|
69
|
+
svg.setAttribute("class", "dou-flow-editor__edges");
|
|
70
|
+
svg.style.cssText = "position:absolute;visibility:hidden;pointer-events:none";
|
|
71
|
+
root.append(svg);
|
|
72
|
+
const looks = /* @__PURE__ */ new Map();
|
|
73
|
+
return {
|
|
74
|
+
of(item) {
|
|
75
|
+
const style = item.style;
|
|
76
|
+
const key = [
|
|
77
|
+
item.type,
|
|
78
|
+
item.stale,
|
|
79
|
+
item.edge.animated,
|
|
80
|
+
JSON.stringify(style ?? {})
|
|
81
|
+
].join("|");
|
|
82
|
+
let found = looks.get(key);
|
|
83
|
+
if (found === void 0) {
|
|
84
|
+
const g = document.createElementNS(NS, "g");
|
|
85
|
+
g.setAttribute("data-edge-type", item.type);
|
|
86
|
+
const path = document.createElementNS(NS, "path");
|
|
87
|
+
const classes = ["dou-flow-editor__edge"];
|
|
88
|
+
if (item.stale) classes.push("is-stale");
|
|
89
|
+
if (item.edge.animated) classes.push("is-animated");
|
|
90
|
+
path.setAttribute("class", classes.join(" "));
|
|
91
|
+
for (const [name, value] of Object.entries(style ?? {})) {
|
|
92
|
+
if (value === void 0) continue;
|
|
93
|
+
if (name.startsWith("--")) path.style.setProperty(name, value);
|
|
94
|
+
else path.style.setProperty(name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`), value);
|
|
95
|
+
}
|
|
96
|
+
g.append(path);
|
|
97
|
+
svg.append(g);
|
|
98
|
+
const s = getComputedStyle(path);
|
|
99
|
+
const dash = s.strokeDasharray && s.strokeDasharray !== "none" ? s.strokeDasharray : "";
|
|
100
|
+
const width = px(s.strokeWidth, 1);
|
|
101
|
+
found = `fill="none" stroke="${tools.color(s.stroke || "var(--dou-flow-editor-edge)")}" stroke-width="${round(width)}"` + (dash ? ` stroke-dasharray="${dash.replace(/px/g, "").replace(/,\s*/g, " ")}"` : "") + (s.strokeLinecap && s.strokeLinecap !== "butt" ? ` stroke-linecap="${s.strokeLinecap}"` : "") + (s.strokeLinejoin && s.strokeLinejoin !== "miter" ? ` stroke-linejoin="${s.strokeLinejoin}"` : "");
|
|
102
|
+
looks.set(key, found);
|
|
103
|
+
}
|
|
104
|
+
return found;
|
|
105
|
+
},
|
|
106
|
+
dispose: () => svg.remove()
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** An edge's label: a pill with its text, as on screen. */
|
|
110
|
+
function drawLabel(tools, text, at) {
|
|
111
|
+
const s = tools.style("dou-flow-editor__label");
|
|
112
|
+
const size = px(s.fontSize, 12);
|
|
113
|
+
const padding = px(s.paddingLeft, 6);
|
|
114
|
+
const border = px(s.borderTopWidth, 1);
|
|
115
|
+
const height = px(s.lineHeight, 20) + border * 2;
|
|
116
|
+
const max = px(s.maxWidth, 200);
|
|
117
|
+
const textWidth = Math.min(tools.measure(text, size), max - padding * 2 - border * 2);
|
|
118
|
+
const width = Math.ceil(textWidth + padding * 2 + border * 2);
|
|
119
|
+
const x = at.x - width / 2;
|
|
120
|
+
const y = at.y - height / 2;
|
|
121
|
+
const radius = px(s.borderTopLeftRadius, 6);
|
|
122
|
+
return {
|
|
123
|
+
markup: `<rect x="${round(x + border / 2)}" y="${round(y + border / 2)}" width="${round(width - border)}" height="${round(height - border)}" rx="${round(radius)}" fill="${tools.color(s.backgroundColor)}" stroke="${tools.color(s.borderTopColor)}" stroke-width="${border}"/>` + tools.text(text, {
|
|
124
|
+
x: x + border + padding,
|
|
125
|
+
y,
|
|
126
|
+
width: width - (border + padding) * 2,
|
|
127
|
+
height
|
|
128
|
+
}, {
|
|
129
|
+
size,
|
|
130
|
+
lineHeight: height,
|
|
131
|
+
color: tools.color(s.color),
|
|
132
|
+
align: "center",
|
|
133
|
+
nowrap: true
|
|
134
|
+
}),
|
|
135
|
+
box: {
|
|
136
|
+
x,
|
|
137
|
+
y,
|
|
138
|
+
width,
|
|
139
|
+
height
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The flow as an SVG document: containers, edges, edge labels, then the
|
|
145
|
+
* other nodes, as the editor stacks them.
|
|
146
|
+
*/
|
|
147
|
+
function drawFlow(input, tools, root) {
|
|
148
|
+
const bounds = extent();
|
|
149
|
+
const nodeParts = [];
|
|
150
|
+
const containerParts = [];
|
|
151
|
+
for (const item of input.nodes) {
|
|
152
|
+
const { node, spec, rect } = item;
|
|
153
|
+
bounds.add(nodeBounds(spec, node, rect));
|
|
154
|
+
const draw = tools.forNode({
|
|
155
|
+
width: rect.width,
|
|
156
|
+
height: rect.height
|
|
157
|
+
}, item.ports, item.port);
|
|
158
|
+
let body;
|
|
159
|
+
try {
|
|
160
|
+
body = spec.draw ? spec.draw(node, draw) : drawFallback(draw, item.label, spec.frame !== false);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error(`[@dou-ui/flow] drawing node "${node.id}" failed`, error);
|
|
163
|
+
body = drawFallback(draw, item.label, true);
|
|
164
|
+
}
|
|
165
|
+
if (!body) continue;
|
|
166
|
+
const g = `<g transform="translate(${round(rect.x)} ${round(rect.y)})">${body}</g>`;
|
|
167
|
+
(item.container ? containerParts : nodeParts).push(g);
|
|
168
|
+
}
|
|
169
|
+
const looks = edgeLook(tools, root);
|
|
170
|
+
const edgeParts = [];
|
|
171
|
+
const labelParts = [];
|
|
172
|
+
try {
|
|
173
|
+
for (const item of input.edges) {
|
|
174
|
+
for (const p of pathPoints(item.route.path)) bounds.add({
|
|
175
|
+
x: p.x,
|
|
176
|
+
y: p.y,
|
|
177
|
+
width: 0,
|
|
178
|
+
height: 0
|
|
179
|
+
});
|
|
180
|
+
const look = looks.of(item);
|
|
181
|
+
edgeParts.push(`<path d="${escapeXml(item.route.path)}" ${look}/>`);
|
|
182
|
+
if (item.route.marks) edgeParts.push(`<path d="${escapeXml(item.route.marks)}" ${look.replace(/ stroke-dasharray="[^"]*"/, "")}/>`);
|
|
183
|
+
for (const p of pathPoints(item.route.marks ?? "")) bounds.add({
|
|
184
|
+
x: p.x,
|
|
185
|
+
y: p.y,
|
|
186
|
+
width: 0,
|
|
187
|
+
height: 0
|
|
188
|
+
});
|
|
189
|
+
if (item.edge.label) {
|
|
190
|
+
const label = drawLabel(tools, item.edge.label, item.route.label);
|
|
191
|
+
labelParts.push(label.markup);
|
|
192
|
+
bounds.add(label.box);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} finally {
|
|
196
|
+
looks.dispose();
|
|
197
|
+
}
|
|
198
|
+
const box = input.frame ?? bounds.get() ?? {
|
|
199
|
+
x: 0,
|
|
200
|
+
y: 0,
|
|
201
|
+
width: 0,
|
|
202
|
+
height: 0
|
|
203
|
+
};
|
|
204
|
+
const pad = input.frame ? 0 : input.padding;
|
|
205
|
+
const x = Math.floor(box.x - pad);
|
|
206
|
+
const y = Math.floor(box.y - pad);
|
|
207
|
+
const width = Math.ceil(box.x + box.width + pad) - x;
|
|
208
|
+
const height = Math.ceil(box.y + box.height + pad) - y;
|
|
209
|
+
const layerParts = [];
|
|
210
|
+
if (input.layers?.length) {
|
|
211
|
+
const draw = tools.forNode({
|
|
212
|
+
width,
|
|
213
|
+
height
|
|
214
|
+
}, /* @__PURE__ */ new Map(), () => "");
|
|
215
|
+
for (const layer of input.layers) try {
|
|
216
|
+
layerParts.push(layer(draw));
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.error("[@dou-ui/flow] drawing a layer failed", error);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const rootStyle = getComputedStyle(root);
|
|
222
|
+
const background = input.background ? `<rect x="${x}" y="${y}" width="${width}" height="${height}" fill="${tools.color(rootStyle.backgroundColor || "var(--dou-color-bg-canvas)")}"/>` : "";
|
|
223
|
+
return {
|
|
224
|
+
svg: `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="${x} ${y} ${width} ${height}" font-family="${escapeXml(tools.fontFamily)}" font-size="${round(tools.fontSize)}" fill="${tools.color("var(--dou-color-text)")}">` + background + containerParts.join("") + edgeParts.join("") + labelParts.join("") + nodeParts.join("") + layerParts.join("") + "</svg>",
|
|
225
|
+
width,
|
|
226
|
+
height
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/** Reads an image into a data URL, or `null` when it can't be read from this page. */
|
|
230
|
+
async function readImage(src) {
|
|
231
|
+
if (src.startsWith("data:")) return src;
|
|
232
|
+
try {
|
|
233
|
+
const response = await fetch(src);
|
|
234
|
+
if (!response.ok) return null;
|
|
235
|
+
const blob = await response.blob();
|
|
236
|
+
if (!blob.type.startsWith("image/")) return null;
|
|
237
|
+
return await new Promise((resolve) => {
|
|
238
|
+
const reader = new FileReader();
|
|
239
|
+
reader.addEventListener("load", () => resolve(String(reader.result)));
|
|
240
|
+
reader.addEventListener("error", () => resolve(null));
|
|
241
|
+
reader.readAsDataURL(blob);
|
|
242
|
+
});
|
|
243
|
+
} catch {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
export { drawCard, drawFallback, drawFlow, pathPoints, readImage };
|
|
248
|
+
|
|
249
|
+
//# sourceMappingURL=draw.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"draw.js","names":[],"sources":["../../src/export/draw.ts"],"sourcesContent":["import type { CSSProperties } from 'vue'\nimport type { PortAnchor } from '../context'\nimport type { EdgeRoute } from '../core/edge-types'\nimport type { FlowEdge, FlowNode, Rect } from '../core/model'\nimport { nodeBounds, type NodeTypeSpec } from '../core/node-types'\nimport { escapeXml, round, type DrawTools, type FlowDrawContext } from './context'\n\nexport interface FlowExportOptions {\n /**\n * `all` (the default): every node and edge. `view`: what is in view now,\n * at the canvas's size, cut at its edges. `selection`: the selected nodes,\n * what is inside selected groups, and the edges between them; it throws\n * when nothing is selected.\n */\n range?: 'all' | 'view' | 'selection'\n /** Room around the drawing, in px. Defaults to `24`; not used by `view`. */\n padding?: number\n /** Fills the canvas color behind it. Defaults to `true`; `false` leaves it see-through. */\n background?: boolean\n}\n\nexport interface FlowImageOptions extends FlowExportOptions {\n /** Pixels per CSS pixel. Defaults to `2`, sharp on most screens and in print. */\n pixelRatio?: number\n}\n\nexport interface DrawNode {\n node: FlowNode\n spec: NodeTypeSpec\n rect: Rect\n ports: ReadonlyMap<string, PortAnchor>\n container: boolean\n label: string\n /** Draws one of its ports, as `FlowDrawContext.port`. */\n port: (id: string) => string\n}\n\nexport interface DrawEdge {\n edge: FlowEdge\n type: string\n route: EdgeRoute\n style?: CSSProperties\n stale: boolean\n}\n\nexport interface DrawInput {\n /** Containers outer first, then the other nodes lowest first. */\n nodes: DrawNode[]\n edges: DrawEdge[]\n padding: number\n background: boolean\n /** The area to draw, in place of what the drawing covers plus `padding`. */\n frame?: Rect\n /** Extensions' layers over the nodes, as `FlowExtension.drawLayer`. */\n layers?: ((draw: FlowDrawContext) => string)[]\n}\n\n/** The points a path passes through or bends towards; the curve stays inside them. */\nexport function pathPoints(path: string) {\n const numbers = path.match(/-?\\d*\\.?\\d+(?:e[-+]?\\d+)?/gi)?.map(Number) ?? []\n const out: { x: number; y: number }[] = []\n for (let i = 0; i + 1 < numbers.length; i += 2) out.push({ x: numbers[i]!, y: numbers[i + 1]! })\n return out\n}\n\n/** A box that grows to hold what is added. */\nfunction extent() {\n let minX = Infinity\n let minY = Infinity\n let maxX = -Infinity\n let maxY = -Infinity\n return {\n add(rect: Rect) {\n minX = Math.min(minX, rect.x)\n minY = Math.min(minY, rect.y)\n maxX = Math.max(maxX, rect.x + rect.width)\n maxY = Math.max(maxY, rect.y + rect.height)\n },\n get(): Rect | null {\n return minX === Infinity\n ? null\n : { x: minX, y: minY, width: maxX - minX, height: maxY - minY }\n },\n }\n}\n\nconst px = (value: string, fallback = 0) => {\n const n = Number.parseFloat(value)\n return Number.isFinite(n) ? n : fallback\n}\n\n/** A card like the editor's frame: border, background and radius. */\nexport function drawCard(draw: FlowDrawContext, classes: string[] = []): string {\n const s = draw.style(['dou-flow-editor__node', ...classes].join(' '))\n const border = px(s.borderTopWidth, 1)\n const radius = px(s.borderTopLeftRadius, 8)\n const inset = border / 2\n return `<rect x=\"${inset}\" y=\"${inset}\" width=\"${round(Math.max(0, draw.width - border))}\" height=\"${round(Math.max(0, draw.height - border))}\" rx=\"${round(Math.max(0, radius - inset))}\" fill=\"${draw.color(s.backgroundColor)}\" stroke=\"${draw.color(s.borderTopColor)}\" stroke-width=\"${border}\"/>`\n}\n\n/** What a node type without `draw` is drawn as: its card, its name and its ports. */\nexport function drawFallback(draw: FlowDrawContext, label: string, framed: boolean): string {\n const parts = framed ? [drawCard(draw)] : []\n for (const id of draw.ports.keys()) parts.push(draw.port(id))\n parts.push(\n draw.text(\n label,\n { x: 12, y: 0, width: Math.max(0, draw.width - 24), height: draw.height },\n { align: 'center', valign: 'middle', color: draw.color('var(--dou-color-text)') },\n ),\n )\n return parts.join('')\n}\n\n/** The edges' look, read from paths made like the editor's. */\nfunction edgeLook(tools: DrawTools, root: HTMLElement) {\n const NS = 'http://www.w3.org/2000/svg'\n const svg = document.createElementNS(NS, 'svg')\n svg.setAttribute('class', 'dou-flow-editor__edges')\n svg.style.cssText = 'position:absolute;visibility:hidden;pointer-events:none'\n root.append(svg)\n const looks = new Map<string, string>()\n return {\n of(item: DrawEdge) {\n const style = item.style as Record<string, string | undefined> | undefined\n const key = [item.type, item.stale, item.edge.animated, JSON.stringify(style ?? {})].join('|')\n let found = looks.get(key)\n if (found === undefined) {\n const g = document.createElementNS(NS, 'g')\n g.setAttribute('data-edge-type', item.type)\n const path = document.createElementNS(NS, 'path')\n const classes = ['dou-flow-editor__edge']\n if (item.stale) classes.push('is-stale')\n if (item.edge.animated) classes.push('is-animated')\n path.setAttribute('class', classes.join(' '))\n for (const [name, value] of Object.entries(style ?? {})) {\n if (value === undefined) continue\n if (name.startsWith('--')) path.style.setProperty(name, value)\n else\n path.style.setProperty(\n name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`),\n value,\n )\n }\n g.append(path)\n svg.append(g)\n const s = getComputedStyle(path)\n const dash = s.strokeDasharray && s.strokeDasharray !== 'none' ? s.strokeDasharray : ''\n const width = px(s.strokeWidth, 1)\n found =\n `fill=\"none\" stroke=\"${tools.color(s.stroke || 'var(--dou-flow-editor-edge)')}\" stroke-width=\"${round(width)}\"` +\n (dash ? ` stroke-dasharray=\"${dash.replace(/px/g, '').replace(/,\\s*/g, ' ')}\"` : '') +\n (s.strokeLinecap && s.strokeLinecap !== 'butt'\n ? ` stroke-linecap=\"${s.strokeLinecap}\"`\n : '') +\n (s.strokeLinejoin && s.strokeLinejoin !== 'miter'\n ? ` stroke-linejoin=\"${s.strokeLinejoin}\"`\n : '')\n looks.set(key, found)\n }\n return found\n },\n dispose: () => svg.remove(),\n }\n}\n\n/** An edge's label: a pill with its text, as on screen. */\nfunction drawLabel(tools: DrawTools, text: string, at: { x: number; y: number }) {\n const s = tools.style('dou-flow-editor__label')\n const size = px(s.fontSize, 12)\n const padding = px(s.paddingLeft, 6)\n const border = px(s.borderTopWidth, 1)\n const height = px(s.lineHeight, 20) + border * 2\n const max = px(s.maxWidth, 200)\n const textWidth = Math.min(tools.measure(text, size), max - padding * 2 - border * 2)\n const width = Math.ceil(textWidth + padding * 2 + border * 2)\n const x = at.x - width / 2\n const y = at.y - height / 2\n const radius = px(s.borderTopLeftRadius, 6)\n const markup =\n `<rect x=\"${round(x + border / 2)}\" y=\"${round(y + border / 2)}\" width=\"${round(width - border)}\" height=\"${round(height - border)}\" rx=\"${round(radius)}\" fill=\"${tools.color(s.backgroundColor)}\" stroke=\"${tools.color(s.borderTopColor)}\" stroke-width=\"${border}\"/>` +\n tools.text(\n text,\n { x: x + border + padding, y, width: width - (border + padding) * 2, height },\n { size, lineHeight: height, color: tools.color(s.color), align: 'center', nowrap: true },\n )\n return { markup, box: { x, y, width, height } }\n}\n\n/**\n * The flow as an SVG document: containers, edges, edge labels, then the\n * other nodes, as the editor stacks them.\n */\nexport function drawFlow(input: DrawInput, tools: DrawTools, root: HTMLElement) {\n const bounds = extent()\n const nodeParts: string[] = []\n const containerParts: string[] = []\n for (const item of input.nodes) {\n const { node, spec, rect } = item\n bounds.add(nodeBounds(spec, node, rect))\n const draw = tools.forNode({ width: rect.width, height: rect.height }, item.ports, item.port)\n let body: string\n try {\n body = spec.draw\n ? spec.draw(node as never, draw)\n : drawFallback(draw, item.label, spec.frame !== false)\n } catch (error) {\n console.error(`[@dou-ui/flow] drawing node \"${node.id}\" failed`, error)\n body = drawFallback(draw, item.label, true)\n }\n if (!body) continue\n const g = `<g transform=\"translate(${round(rect.x)} ${round(rect.y)})\">${body}</g>`\n ;(item.container ? containerParts : nodeParts).push(g)\n }\n const looks = edgeLook(tools, root)\n const edgeParts: string[] = []\n const labelParts: string[] = []\n try {\n for (const item of input.edges) {\n for (const p of pathPoints(item.route.path))\n bounds.add({ x: p.x, y: p.y, width: 0, height: 0 })\n const look = looks.of(item)\n edgeParts.push(`<path d=\"${escapeXml(item.route.path)}\" ${look}/>`)\n // arrowheads and the like: the same stroke, never dashed\n if (item.route.marks)\n edgeParts.push(\n `<path d=\"${escapeXml(item.route.marks)}\" ${look.replace(/ stroke-dasharray=\"[^\"]*\"/, '')}/>`,\n )\n for (const p of pathPoints(item.route.marks ?? ''))\n bounds.add({ x: p.x, y: p.y, width: 0, height: 0 })\n if (item.edge.label) {\n const label = drawLabel(tools, item.edge.label, item.route.label)\n labelParts.push(label.markup)\n bounds.add(label.box)\n }\n }\n } finally {\n looks.dispose()\n }\n const box = input.frame ?? bounds.get() ?? { x: 0, y: 0, width: 0, height: 0 }\n const pad = input.frame ? 0 : input.padding\n const x = Math.floor(box.x - pad)\n const y = Math.floor(box.y - pad)\n const width = Math.ceil(box.x + box.width + pad) - x\n const height = Math.ceil(box.y + box.height + pad) - y\n const layerParts: string[] = []\n if (input.layers?.length) {\n const draw = tools.forNode({ width, height }, new Map(), () => '')\n for (const layer of input.layers) {\n try {\n layerParts.push(layer(draw))\n } catch (error) {\n console.error('[@dou-ui/flow] drawing a layer failed', error)\n }\n }\n }\n const rootStyle = getComputedStyle(root)\n const background = input.background\n ? `<rect x=\"${x}\" y=\"${y}\" width=\"${width}\" height=\"${height}\" fill=\"${tools.color(rootStyle.backgroundColor || 'var(--dou-color-bg-canvas)')}\"/>`\n : ''\n const font = escapeXml(tools.fontFamily)\n const svg =\n `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\" viewBox=\"${x} ${y} ${width} ${height}\" font-family=\"${font}\" font-size=\"${round(tools.fontSize)}\" fill=\"${tools.color('var(--dou-color-text)')}\">` +\n background +\n containerParts.join('') +\n edgeParts.join('') +\n labelParts.join('') +\n nodeParts.join('') +\n layerParts.join('') +\n '</svg>'\n return { svg, width, height }\n}\n\n/** Reads an image into a data URL, or `null` when it can't be read from this page. */\nexport async function readImage(src: string): Promise<string | null> {\n if (src.startsWith('data:')) return src\n try {\n const response = await fetch(src)\n if (!response.ok) return null\n const blob = await response.blob()\n // e.g. a page in place of a missing file\n if (!blob.type.startsWith('image/')) return null\n return await new Promise((resolve) => {\n const reader = new FileReader()\n reader.addEventListener('load', () => resolve(String(reader.result)))\n reader.addEventListener('error', () => resolve(null))\n reader.readAsDataURL(blob)\n })\n } catch {\n return null\n }\n}\n\nexport type { FlowDrawContext }\n"],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import flow_editor_vue_vue_type_script_setup_true_lang_default from "./flow-editor.vue_vue_type_script_setup_true_lang.js";
|
|
2
|
+
// src/flow-editor.vue
|
|
3
|
+
var flow_editor_default = flow_editor_vue_vue_type_script_setup_true_lang_default;
|
|
4
|
+
export { flow_editor_default as default };
|
|
5
|
+
|
|
6
|
+
//# sourceMappingURL=flow-editor.js.map
|