@heyramzi/ui 2.11.0 → 2.11.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.
Files changed (267) hide show
  1. package/dist/agents/helpers.js +38 -38
  2. package/dist/agents/page-state.js +49 -50
  3. package/dist/agents/vendor/color.js +146 -127
  4. package/dist/agents/vendor/draw.js +617 -508
  5. package/dist/agents/vendor/engine.js +698 -599
  6. package/dist/agents/vendor/mesh.js +653 -683
  7. package/dist/agents/vendor/plastic.js +3913 -3112
  8. package/dist/agents/vendor/presets.js +163 -164
  9. package/dist/agents/vendor/shapes.js +21 -20
  10. package/dist/agents/vendor/ticker.js +41 -42
  11. package/dist/agents/vendor/types.js +1 -0
  12. package/dist/agents/vendor/wear.js +1421 -1448
  13. package/dist/ai/confirmation/confirmation-context.svelte.js +6 -6
  14. package/dist/ai/confirmation/index.js +3 -1
  15. package/dist/ai/index.js +33 -0
  16. package/dist/ai/prompt-input/index.js +3 -1
  17. package/dist/ai/reasoning/index.js +3 -1
  18. package/dist/ai/reasoning/reasoning-context.svelte.js +6 -6
  19. package/dist/ai/suggestion/index.js +3 -1
  20. package/dist/ai/typing-dots/index.js +3 -1
  21. package/dist/animations/text-swap.js +24 -24
  22. package/dist/brand/index.js +54 -48
  23. package/dist/canvas/card/resize.js +5 -3
  24. package/dist/canvas/edges/edge-path.js +255 -242
  25. package/dist/canvas/editor/context-menu.js +39 -36
  26. package/dist/canvas/editor/dispatch.svelte.js +154 -151
  27. package/dist/canvas/editor/editor-context.js +7 -2
  28. package/dist/canvas/editor/editor-mode.js +1 -0
  29. package/dist/canvas/editor/frame.js +73 -76
  30. package/dist/canvas/editor/hand-context.js +5 -2
  31. package/dist/canvas/editor/history-core.js +163 -191
  32. package/dist/canvas/editor/keymap.js +95 -169
  33. package/dist/canvas/editor/layout-actions.js +143 -144
  34. package/dist/canvas/editor/lock.js +52 -51
  35. package/dist/canvas/editor/rebase.js +217 -240
  36. package/dist/canvas/editor/style.js +132 -137
  37. package/dist/canvas/editor/text-field-context.js +7 -2
  38. package/dist/canvas/editor/z-order.js +65 -70
  39. package/dist/canvas/headless/alignment.js +22 -16
  40. package/dist/canvas/headless/break-apart-find.js +286 -279
  41. package/dist/canvas/headless/break-apart-pieces.js +62 -58
  42. package/dist/canvas/headless/break-apart-plan.js +299 -325
  43. package/dist/canvas/headless/break-apart-style.js +75 -70
  44. package/dist/canvas/headless/break-apart.js +188 -181
  45. package/dist/canvas/headless/canvas-cleanup.js +78 -58
  46. package/dist/canvas/headless/edge-anchors.js +115 -116
  47. package/dist/canvas/headless/edge-memo.js +24 -22
  48. package/dist/canvas/headless/edge-serialization.js +17 -27
  49. package/dist/canvas/headless/figure-geometry.js +232 -264
  50. package/dist/canvas/headless/fit-viewport.js +84 -67
  51. package/dist/canvas/headless/grid.js +88 -86
  52. package/dist/canvas/headless/icon-node.js +44 -36
  53. package/dist/canvas/headless/index.js +1 -0
  54. package/dist/canvas/headless/linkify.js +17 -19
  55. package/dist/canvas/headless/pen-marks.js +101 -95
  56. package/dist/canvas/headless/sketch.js +231 -209
  57. package/dist/canvas/headless/snap-guides.js +99 -83
  58. package/dist/canvas/headless/svg-figure.js +133 -117
  59. package/dist/canvas/headless/table-layout.js +146 -107
  60. package/dist/canvas/index.js +2 -0
  61. package/dist/canvas/lod/lod-mode.svelte.js +20 -18
  62. package/dist/canvas/lod/render-all.svelte.js +36 -39
  63. package/dist/canvas/ops/arrange.js +78 -71
  64. package/dist/canvas/ops/ops.js +260 -255
  65. package/dist/canvas/pen/colors.js +37 -35
  66. package/dist/canvas/pen/eraser.js +28 -27
  67. package/dist/canvas/pen/geometry.js +120 -130
  68. package/dist/canvas/pen/index.js +1 -0
  69. package/dist/canvas/pen/ops.js +11 -11
  70. package/dist/canvas/pen/pen-session.js +150 -159
  71. package/dist/canvas/pen/plugin.js +107 -104
  72. package/dist/canvas/pen/shapes.js +73 -68
  73. package/dist/canvas/pen/stroke-path.js +32 -30
  74. package/dist/canvas/pen/types.js +21 -21
  75. package/dist/canvas/pen/ui.js +10 -8
  76. package/dist/canvas/plugin.js +46 -46
  77. package/dist/canvas/screen/glyphs.js +12 -11
  78. package/dist/canvas/screen/labels.js +22 -22
  79. package/dist/canvas/screen/parts.js +98 -111
  80. package/dist/canvas/screen/plugin.js +47 -56
  81. package/dist/canvas/screen/types.js +30 -35
  82. package/dist/canvas/screen/ui.js +7 -7
  83. package/dist/canvas/test-utils.js +14 -14
  84. package/dist/canvas/types.js +1 -0
  85. package/dist/canvas/ui/forward-wheel.js +24 -25
  86. package/dist/canvas/ui/icons.js +64 -131
  87. package/dist/canvas/ui/place-panel.js +58 -49
  88. package/dist/canvas/ui/shortcut-tip.js +14 -12
  89. package/dist/canvas/ui/tools.js +300 -385
  90. package/dist/charts/dashboard/chart.js +4 -0
  91. package/dist/charts/dashboard/data-table.js +11 -0
  92. package/dist/charts/dashboard/windows.js +8 -3
  93. package/dist/charts/edge-fade.js +10 -5
  94. package/dist/charts/hover-band.js +16 -11
  95. package/dist/charts/index.js +0 -1
  96. package/dist/charts/motion.js +11 -7
  97. package/dist/charts/primitives/chart-utils.js +12 -12
  98. package/dist/components/copy/clipboard.js +32 -32
  99. package/dist/components/copy/copied.svelte.js +31 -23
  100. package/dist/components/data-table/data-table.js +39 -39
  101. package/dist/components/docs/index.js +4 -0
  102. package/dist/components/docs/scroll-spy.svelte.js +73 -68
  103. package/dist/components/list-page/filter-section.js +4 -1
  104. package/dist/components/list-page/index.js +2 -0
  105. package/dist/components/list-page/list-table.js +7 -2
  106. package/dist/components/list-page/queue-rail.js +14 -0
  107. package/dist/components/list-page/section-host.js +4 -2
  108. package/dist/components/list-page/step.js +4 -3
  109. package/dist/components/list-page/types.js +1 -0
  110. package/dist/components/picker/trigger.js +48 -30
  111. package/dist/components/picker/types.js +1 -0
  112. package/dist/components/segmented/collapse.js +38 -27
  113. package/dist/components/segmented/index.js +8 -0
  114. package/dist/components/segmented/segmented.js +67 -42
  115. package/dist/components/star-rating-input/index.js +3 -1
  116. package/dist/components/table-view/types.js +1 -0
  117. package/dist/components/tags-input/tags.js +81 -69
  118. package/dist/components/theme/index.js +4 -0
  119. package/dist/components/toolbar/index.js +4 -0
  120. package/dist/components/toolbar/toolbar-trigger.js +14 -1
  121. package/dist/editor/anchors.js +17 -13
  122. package/dist/editor/block-id.js +52 -51
  123. package/dist/editor/block-ops.js +108 -80
  124. package/dist/editor/doc-blocks.js +155 -166
  125. package/dist/editor/extensions.js +139 -180
  126. package/dist/editor/index.js +4 -0
  127. package/dist/editor/markdown-test-helpers.js +4 -2
  128. package/dist/editor/markdown.js +420 -408
  129. package/dist/editor/serializer.js +204 -200
  130. package/dist/editor/slash-items.js +89 -101
  131. package/dist/editor/youtube-url.js +25 -20
  132. package/dist/effects/animated-beam/path-calculator.svelte.js +43 -38
  133. package/dist/effects/animated-beam/types.js +1 -0
  134. package/dist/effects/border-beam/css.js +92 -78
  135. package/dist/effects/border-beam/presets.js +1705 -1720
  136. package/dist/effects/border-beam/register-properties.js +43 -33
  137. package/dist/effects/border-beam/types.js +1 -0
  138. package/dist/effects/image-comparison/compare.js +3 -2
  139. package/dist/effects/image-reveal/types.js +1 -0
  140. package/dist/effects/magnetic/magnetic.js +58 -52
  141. package/dist/effects/reveal/observe.js +50 -40
  142. package/dist/effects/reveal/types.js +1 -0
  143. package/dist/effects/shader-gradient/color.js +36 -29
  144. package/dist/effects/shader-gradient/renderer.js +384 -295
  145. package/dist/effects/shader-gradient/shaders.js +31 -0
  146. package/dist/effects/shader-gradient/types.js +1 -0
  147. package/dist/icons/apple-symbol-exports.js +99 -124
  148. package/dist/icons/sf-symbols.js +357 -461
  149. package/dist/icons/trades.js +43 -28
  150. package/dist/inbox/index.js +1 -0
  151. package/dist/inbox/logic.js +126 -122
  152. package/dist/inbox/types.js +43 -43
  153. package/dist/lib/arrive.js +19 -12
  154. package/dist/lib/icon-types.js +1 -0
  155. package/dist/lib/initials.js +16 -10
  156. package/dist/lib/layout.js +5 -0
  157. package/dist/lib/panel.js +13 -0
  158. package/dist/lib/result.js +22 -22
  159. package/dist/lib/sliding-pill.js +59 -44
  160. package/dist/lib/theme-transition.js +43 -43
  161. package/dist/lib/tone.js +31 -24
  162. package/dist/lib/utils.js +3 -1
  163. package/dist/onboarding/define-tours.js +24 -23
  164. package/dist/onboarding/onboarding.svelte.js +16 -12
  165. package/dist/onboarding/pick.js +10 -6
  166. package/dist/onboarding/stores.js +40 -41
  167. package/dist/onboarding/wait.js +24 -23
  168. package/dist/panel/side-panel-types.js +1 -0
  169. package/dist/record/demo-data.js +459 -486
  170. package/dist/record/fields.js +129 -131
  171. package/dist/record/format.js +70 -82
  172. package/dist/record/gutter.js +1 -0
  173. package/dist/record/kinds.js +95 -105
  174. package/dist/record/logic.js +238 -219
  175. package/dist/record/types.js +49 -48
  176. package/dist/shadcn/accordion/index.js +3 -1
  177. package/dist/shadcn/alert/index.js +3 -1
  178. package/dist/shadcn/alert-dialog/index.js +3 -1
  179. package/dist/shadcn/attachment/index.js +3 -1
  180. package/dist/shadcn/avatar/index.js +3 -1
  181. package/dist/shadcn/breadcrumb/index.js +3 -1
  182. package/dist/shadcn/bubble/index.js +3 -1
  183. package/dist/shadcn/button/index.js +3 -1
  184. package/dist/shadcn/calendar/index.js +3 -1
  185. package/dist/shadcn/card/index.js +3 -1
  186. package/dist/shadcn/checkbox/index.js +3 -1
  187. package/dist/shadcn/code-block/index.js +3 -1
  188. package/dist/shadcn/collapsible/index.js +3 -1
  189. package/dist/shadcn/command/index.js +4 -1
  190. package/dist/shadcn/context-menu/index.js +3 -1
  191. package/dist/shadcn/dialog/index.js +3 -1
  192. package/dist/shadcn/dropdown-menu/menu-item.js +40 -20
  193. package/dist/shadcn/empty/index.js +3 -1
  194. package/dist/shadcn/field/index.js +3 -1
  195. package/dist/shadcn/hooks/highlighter.js +77 -75
  196. package/dist/shadcn/hooks/is-mobile.svelte.js +3 -3
  197. package/dist/shadcn/hooks/use-highlighted-code.svelte.js +62 -62
  198. package/dist/shadcn/input/index.js +3 -1
  199. package/dist/shadcn/kbd/index.js +3 -1
  200. package/dist/shadcn/label/index.js +3 -1
  201. package/dist/shadcn/marker/index.js +3 -1
  202. package/dist/shadcn/message/index.js +3 -1
  203. package/dist/shadcn/message-scroller/index.js +3 -1
  204. package/dist/shadcn/popover/index.js +3 -1
  205. package/dist/shadcn/progress/index.js +3 -1
  206. package/dist/shadcn/progress/tone.js +8 -7
  207. package/dist/shadcn/radio-group/index.js +3 -1
  208. package/dist/shadcn/scroll-area/index.js +3 -1
  209. package/dist/shadcn/select/index.js +3 -1
  210. package/dist/shadcn/separator/index.js +3 -1
  211. package/dist/shadcn/sheet/index.js +3 -1
  212. package/dist/shadcn/sidebar/context.svelte.js +42 -28
  213. package/dist/shadcn/sidebar/index.js +3 -1
  214. package/dist/shadcn/skeleton/index.js +3 -1
  215. package/dist/shadcn/slider/index.js +3 -1
  216. package/dist/shadcn/switch/index.js +3 -1
  217. package/dist/shadcn/table/index.js +3 -1
  218. package/dist/shadcn/tabs/index.js +3 -1
  219. package/dist/shadcn/textarea/index.js +3 -1
  220. package/dist/shadcn/toggle/index.js +3 -1
  221. package/dist/shadcn/toggle-group/index.js +3 -1
  222. package/dist/shadcn/tooltip/context.js +7 -2
  223. package/dist/shadcn/tooltip/index.js +3 -1
  224. package/dist/shadcn/trace/index.js +3 -1
  225. package/dist/slides/bridges.js +65 -59
  226. package/dist/slides/deck-controller.svelte.js +210 -207
  227. package/dist/slides/export/capture.js +66 -71
  228. package/dist/slides/export/ffmpeg.js +76 -72
  229. package/dist/slides/export/index.js +3 -0
  230. package/dist/slides/export/launch.js +9 -6
  231. package/dist/slides/export/pdf.js +60 -54
  232. package/dist/slides/export/types.js +1 -0
  233. package/dist/slides/labels.js +24 -24
  234. package/dist/slides/layout.js +39 -41
  235. package/dist/slides/model.js +9 -7
  236. package/dist/slides/navigation.js +78 -73
  237. package/dist/slides/types.js +2 -2
  238. package/dist/status/define-statuses.js +18 -14
  239. package/dist/status/status-types.js +1 -0
  240. package/dist/test-setup.js +24 -20
  241. package/dist/thread/logic.js +73 -77
  242. package/dist/thread/types.js +21 -21
  243. package/dist/work/board-order.js +94 -90
  244. package/dist/work/dates.js +16 -9
  245. package/dist/work/detail/detail-types.js +18 -18
  246. package/dist/work/docs/edited.js +32 -29
  247. package/dist/work/docs/index.js +4 -0
  248. package/dist/work/docs/labels.js +34 -34
  249. package/dist/work/docs/menu.js +113 -106
  250. package/dist/work/docs/outline.js +39 -29
  251. package/dist/work/gantt/logic.js +211 -235
  252. package/dist/work/inbox/inbox-logic.js +71 -75
  253. package/dist/work/inbox/inbox-statuses.js +75 -72
  254. package/dist/work/inbox/inbox-types.js +1 -0
  255. package/dist/work/kanban-drag.js +27 -22
  256. package/dist/work/kanban-types.js +1 -0
  257. package/dist/work/presets/sales.js +36 -32
  258. package/dist/work/reorder.js +9 -8
  259. package/dist/work/task-types.js +1 -0
  260. package/dist/work/time/duration.js +51 -46
  261. package/dist/work/time/time-types.js +23 -23
  262. package/dist/work/use-kanban-dnd.svelte.js +126 -125
  263. package/package.json +3 -1
  264. package/src/components/list-page/master-detail.svelte +3 -1
  265. package/src/components/list-page/rail-row.svelte +4 -4
  266. package/src/components/tags-input/tags-input.svelte +2 -0
  267. package/src/editor/markdown.ts +30 -35
@@ -1,324 +1,331 @@
1
+ // Break apart, the reading half: which cards, shapes, frames and step numbers a figure holds.
1
2
  import { areaOf, boxOfEl, centerOf, contains, fontSize, hasText, isBold, isEl, num, plus, solid, strokeEnds, textLines, translateOf, visible, within } from "./figure-geometry.js";
3
+ /** A group with more text than this is a panel, not a card. */
2
4
  const MAX_CARD_TEXTS = 8;
3
- const MAX_CARD_AREA = 0.4;
5
+ /** Smaller than this share of the drawing, or it is a panel. */
6
+ const MAX_CARD_AREA = .4;
7
+ /** How far an arrow end may sit from a card edge and still land on it, in drawing units. */
4
8
  export const LAND_TOLERANCE = 8;
5
9
  export const ARROWHEAD_MAX = 20;
10
+ /** A dot's number is at most this long. */
6
11
  const BADGE_MAX_CHARS = 2;
7
12
  const BADGE_MAX_R = 30;
13
+ /** Nothing under this on a side is a card or a shape: a rule or a speck. */
8
14
  const MIN_SIDE = 4;
9
15
  const ORIGIN = {
10
- x: 0,
11
- y: 0
16
+ x: 0,
17
+ y: 0
12
18
  };
13
- const anchorOf = (el, offset)=>({
14
- x: num(el.attrs.x) + offset.x,
15
- y: num(el.attrs.y) + offset.y
16
- });
19
+ // =========================
20
+ // CARDS
21
+ // =========================
22
+ const anchorOf = (el, offset) => ({
23
+ x: num(el.attrs.x) + offset.x,
24
+ y: num(el.attrs.y) + offset.y
25
+ });
26
+ /** A solid rect that has text standing inside it. */
17
27
  function holdsText(rect, texts, offset) {
18
- if (rect.tag !== "rect" || !visible(rect) || rect.attrs.fill === "none") return null;
19
- const box = boxOfEl(rect, offset);
20
- if (!box) return null;
21
- return texts.some((t)=>contains(box, anchorOf(t, offset), 3)) ? box : null;
28
+ if (rect.tag !== "rect" || !visible(rect) || rect.attrs.fill === "none") return null;
29
+ const box = boxOfEl(rect, offset);
30
+ if (!box) return null;
31
+ return texts.some((t) => contains(box, anchorOf(t, offset), 3)) ? box : null;
22
32
  }
33
+ /** A number in a dot is a pin marker, not copy: the ring that stays in the figure carries it. */
23
34
  function splitBadge(inside, kids) {
24
- let badge;
25
- const words = [];
26
- const badgeParts = [];
27
- for (const t of inside){
28
- const line = textLines(t).join(" ");
29
- const dot = kids.find((k)=>k.tag === "circle" && Math.hypot(num(k.attrs.cx) - num(t.attrs.x), num(k.attrs.cy) - num(t.attrs.y)) <= num(k.attrs.r) + 2);
30
- if (dot && line.length <= 2) {
31
- badge = line;
32
- badgeParts.push(dot, t);
33
- } else words.push(t);
34
- }
35
- return {
36
- badge,
37
- words,
38
- badgeParts
39
- };
35
+ let badge;
36
+ const words = [];
37
+ const badgeParts = [];
38
+ for (const t of inside) {
39
+ const line = textLines(t).join(" ");
40
+ const dot = kids.find((k) => k.tag === "circle" && Math.hypot(num(k.attrs.cx) - num(t.attrs.x), num(k.attrs.cy) - num(t.attrs.y)) <= num(k.attrs.r) + 2);
41
+ if (dot && line.length <= 2) {
42
+ badge = line;
43
+ badgeParts.push(dot, t);
44
+ } else words.push(t);
45
+ }
46
+ return {
47
+ badge,
48
+ words,
49
+ badgeParts
50
+ };
40
51
  }
52
+ /** The title is the top bold line, not an end-pinned tag or a big count further down. */
41
53
  function headlineOf(ordered) {
42
- const titleEl = ordered.find((t)=>isBold(t) && t.attrs["text-anchor"] !== "end") ?? [
43
- ...ordered
44
- ].sort((a, b)=>Number(isBold(b)) - Number(isBold(a)) || fontSize(b) - fontSize(a))[0];
45
- const tag = ordered.find((t)=>t !== titleEl && t.attrs["text-anchor"] === "end" && Math.abs(num(t.attrs.y) - num(titleEl.attrs.y)) < 8);
46
- const titleEls = [
47
- titleEl
48
- ];
49
- const sameFace = (t)=>fontSize(t) === fontSize(titleEl) && isBold(t) === isBold(titleEl) && t.attrs["text-anchor"] === titleEl.attrs["text-anchor"];
50
- for (const t of ordered.slice(ordered.indexOf(titleEl) + 1)){
51
- if (t === tag || !sameFace(t)) break;
52
- titleEls.push(t);
53
- }
54
- const bodyEls = ordered.filter((t)=>!titleEls.includes(t) && t !== tag);
55
- return {
56
- titleEls,
57
- tag,
58
- bodyEls
59
- };
54
+ const titleEl = ordered.find((t) => isBold(t) && t.attrs["text-anchor"] !== "end") ?? [...ordered].sort((a, b) => Number(isBold(b)) - Number(isBold(a)) || fontSize(b) - fontSize(a))[0];
55
+ const tag = ordered.find((t) => t !== titleEl && t.attrs["text-anchor"] === "end" && Math.abs(num(t.attrs.y) - num(titleEl.attrs.y)) < 8);
56
+ // A title set over two lines (CLAUDE, then CODE) is one title: the same face, directly after.
57
+ const titleEls = [titleEl];
58
+ const sameFace = (t) => fontSize(t) === fontSize(titleEl) && isBold(t) === isBold(titleEl) && t.attrs["text-anchor"] === titleEl.attrs["text-anchor"];
59
+ for (const t of ordered.slice(ordered.indexOf(titleEl) + 1)) {
60
+ if (t === tag || !sameFace(t)) break;
61
+ titleEls.push(t);
62
+ }
63
+ const bodyEls = ordered.filter((t) => !titleEls.includes(t) && t !== tag);
64
+ return {
65
+ titleEls,
66
+ tag,
67
+ bodyEls
68
+ };
60
69
  }
70
+ /** Same-line fragments read as one line, left to right. */
61
71
  function bodyLines(bodyEls) {
62
- const body = [];
63
- let lastY = Number.NaN;
64
- for (const t of bodyEls){
65
- const text = textLines(t).join(" ");
66
- if (Math.abs(num(t.attrs.y) - lastY) < 3 && body.length > 0) body[body.length - 1] += ` ${text}`;
67
- else body.push(text);
68
- lastY = num(t.attrs.y);
69
- }
70
- return body;
72
+ const body = [];
73
+ let lastY = Number.NaN;
74
+ for (const t of bodyEls) {
75
+ const text = textLines(t).join(" ");
76
+ if (Math.abs(num(t.attrs.y) - lastY) < 3 && body.length > 0) body[body.length - 1] += ` ${text}`;
77
+ else body.push(text);
78
+ lastY = num(t.attrs.y);
79
+ }
80
+ return body;
71
81
  }
82
+ /** Anything else drawn inside the card's own rect is its chrome and goes with it. */
72
83
  function chromeOf(req, claimed) {
73
- const { kids, box, offset, taken } = req;
74
- return kids.filter((k)=>{
75
- if (claimed.has(k) || taken.has(k) || k.attrs.fill === "none") return false;
76
- const kb = boxOfEl(k, offset);
77
- return kb !== null && within(kb, box);
78
- });
84
+ const { kids, box, offset, taken } = req;
85
+ return kids.filter((k) => {
86
+ if (claimed.has(k) || taken.has(k) || k.attrs.fill === "none") return false;
87
+ const kb = boxOfEl(k, offset);
88
+ return kb !== null && within(kb, box);
89
+ });
79
90
  }
80
- const readingOrder = (a, b)=>num(a.attrs.y) - num(b.attrs.y) || num(a.attrs.x) - num(b.attrs.x);
91
+ const readingOrder = (a, b) => num(a.attrs.y) - num(b.attrs.y) || num(a.attrs.x) - num(b.attrs.x);
92
+ /** One card: `rect` and whatever of its group's other children stands inside it. */
81
93
  function cardFromRect(req) {
82
- const { group, kids, rect, box, offset, taken } = req;
83
- const inside = kids.filter((k)=>hasText(k) && !taken.has(k) && contains(box, anchorOf(k, offset), 3));
84
- if (inside.length === 0 || inside.length > MAX_CARD_TEXTS) return null;
85
- const { badge, words, badgeParts } = splitBadge(inside, kids);
86
- if (words.length === 0) return null;
87
- const { titleEls, tag, bodyEls } = headlineOf([
88
- ...words
89
- ].sort(readingOrder));
90
- const claimed = new Set([
91
- rect,
92
- ...badgeParts,
93
- ...titleEls,
94
- ...tag ? [
95
- tag
96
- ] : [],
97
- ...bodyEls
98
- ]);
99
- for (const k of chromeOf(req, claimed))claimed.add(k);
100
- return {
101
- group,
102
- offset,
103
- rect,
104
- box,
105
- claimed,
106
- title: titleEls.map((t)=>textLines(t).join(" ")).join(" "),
107
- titleSize: fontSize(titleEls[0]),
108
- kicker: [
109
- badge,
110
- tag ? textLines(tag).join(" ") : undefined
111
- ].filter(Boolean).join(" · ") || undefined,
112
- body: bodyLines(bodyEls)
113
- };
94
+ const { group, kids, rect, box, offset, taken } = req;
95
+ const inside = kids.filter((k) => hasText(k) && !taken.has(k) && contains(box, anchorOf(k, offset), 3));
96
+ if (inside.length === 0 || inside.length > MAX_CARD_TEXTS) return null;
97
+ const { badge, words, badgeParts } = splitBadge(inside, kids);
98
+ if (words.length === 0) return null;
99
+ const { titleEls, tag, bodyEls } = headlineOf([...words].sort(readingOrder));
100
+ const claimed = new Set([
101
+ rect,
102
+ ...badgeParts,
103
+ ...titleEls,
104
+ ...tag ? [tag] : [],
105
+ ...bodyEls
106
+ ]);
107
+ for (const k of chromeOf(req, claimed)) claimed.add(k);
108
+ return {
109
+ group,
110
+ offset,
111
+ rect,
112
+ box,
113
+ claimed,
114
+ title: titleEls.map((t) => textLines(t).join(" ")).join(" "),
115
+ titleSize: fontSize(titleEls[0]),
116
+ kicker: [badge, tag ? textLines(tag).join(" ") : undefined].filter(Boolean).join(" · ") || undefined,
117
+ body: bodyLines(bodyEls)
118
+ };
114
119
  }
120
+ /** A `data-pins` group holds a pin per card rect. */
115
121
  function readPinCards({ group, kids, texts, offset }) {
116
- const taken = new Set();
117
- const out = [];
118
- for (const rect of kids){
119
- const box = holdsText(rect, texts, offset);
120
- const found = box && cardFromRect({
121
- group,
122
- kids,
123
- rect,
124
- box,
125
- offset,
126
- taken
127
- });
128
- if (!found) continue;
129
- for (const part of found.claimed)taken.add(part);
130
- out.push(found);
131
- }
132
- return out;
122
+ const taken = new Set();
123
+ const out = [];
124
+ for (const rect of kids) {
125
+ const box = holdsText(rect, texts, offset);
126
+ const found = box && cardFromRect({
127
+ group,
128
+ kids,
129
+ rect,
130
+ box,
131
+ offset,
132
+ taken
133
+ });
134
+ if (!found) continue;
135
+ for (const part of found.claimed) taken.add(part);
136
+ out.push(found);
137
+ }
138
+ return out;
133
139
  }
140
+ /** Any other group is one card, unless it is a panel: too much text, a subgroup or too much of the drawing. */
134
141
  function readOneCard({ group, kids, texts, offset }, drawing) {
135
- if (texts.length > MAX_CARD_TEXTS || kids.some((k)=>k.tag === "g")) return [];
136
- for (const rect of kids){
137
- const box = holdsText(rect, texts, offset);
138
- if (!box) continue;
139
- if (areaOf(box) > areaOf(drawing) * MAX_CARD_AREA) return [];
140
- const found = cardFromRect({
141
- group,
142
- kids,
143
- rect,
144
- box,
145
- offset,
146
- taken: new Set()
147
- });
148
- return found ? [
149
- found
150
- ] : [];
151
- }
152
- return [];
142
+ if (texts.length > MAX_CARD_TEXTS || kids.some((k) => k.tag === "g")) return [];
143
+ for (const rect of kids) {
144
+ const box = holdsText(rect, texts, offset);
145
+ if (!box) continue;
146
+ if (areaOf(box) > areaOf(drawing) * MAX_CARD_AREA) return [];
147
+ const found = cardFromRect({
148
+ group,
149
+ kids,
150
+ rect,
151
+ box,
152
+ offset,
153
+ taken: new Set()
154
+ });
155
+ return found ? [found] : [];
156
+ }
157
+ return [];
153
158
  }
159
+ /** Cards in one group. */
154
160
  function readCards(group, offset, drawing) {
155
- const kids = group.children.filter(isEl);
156
- const texts = kids.filter(hasText);
157
- if (texts.length === 0) return [];
158
- const read = {
159
- group,
160
- kids,
161
- texts,
162
- offset
163
- };
164
- return "data-pins" in group.attrs ? readPinCards(read) : readOneCard(read, drawing);
161
+ const kids = group.children.filter(isEl);
162
+ const texts = kids.filter(hasText);
163
+ if (texts.length === 0) return [];
164
+ const read = {
165
+ group,
166
+ kids,
167
+ texts,
168
+ offset
169
+ };
170
+ return "data-pins" in group.attrs ? readPinCards(read) : readOneCard(read, drawing);
165
171
  }
166
172
  export function findCards(el, offset, drawing, out) {
167
- for (const child of el.children){
168
- if (!isEl(child) || child.tag !== "g") continue;
169
- const shift = translateOf(child);
170
- if (!shift) continue;
171
- const at = plus(offset, shift);
172
- const found = readCards(child, at, drawing);
173
- if (found.length > 0) out.push(...found);
174
- else findCards(child, at, drawing, out);
175
- }
173
+ for (const child of el.children) {
174
+ if (!isEl(child) || child.tag !== "g") continue;
175
+ const shift = translateOf(child);
176
+ if (!shift) continue;
177
+ const at = plus(offset, shift);
178
+ const found = readCards(child, at, drawing);
179
+ if (found.length > 0) out.push(...found);
180
+ else findCards(child, at, drawing, out);
181
+ }
176
182
  }
183
+ // =========================
184
+ // LOOSE SHAPES
185
+ // =========================
177
186
  function flatten(container, offset, pins, out) {
178
- const kids = container.children.filter(isEl);
179
- for (const el of kids){
180
- if (el.tag === "g") {
181
- const shift = translateOf(el);
182
- if (shift) flatten(el, plus(offset, shift), pins || "data-pins" in el.attrs, out);
183
- continue;
184
- }
185
- if (el.tag === "defs" || el.tag === "title" || el.tag === "desc") continue;
186
- out.push({
187
- el,
188
- box: boxOfEl(el, offset),
189
- offset,
190
- kids,
191
- container,
192
- pins
193
- });
194
- }
187
+ const kids = container.children.filter(isEl);
188
+ for (const el of kids) {
189
+ if (el.tag === "g") {
190
+ const shift = translateOf(el);
191
+ if (shift) flatten(el, plus(offset, shift), pins || "data-pins" in el.attrs, out);
192
+ continue;
193
+ }
194
+ if (el.tag === "defs" || el.tag === "title" || el.tag === "desc") continue;
195
+ out.push({
196
+ el,
197
+ box: boxOfEl(el, offset),
198
+ offset,
199
+ kids,
200
+ container,
201
+ pins
202
+ });
203
+ }
195
204
  }
205
+ /** Whether a text sits on a dot and is short enough to be its number. */
196
206
  function numbersDot(a, center, r) {
197
- const at = anchorOf(a.el, a.offset);
198
- return textLines(a.el).join(" ").length <= BADGE_MAX_CHARS && Math.hypot(at.x - center.x, at.y - center.y) <= r + 2;
207
+ const at = anchorOf(a.el, a.offset);
208
+ return textLines(a.el).join(" ").length <= BADGE_MAX_CHARS && Math.hypot(at.x - center.x, at.y - center.y) <= r + 2;
199
209
  }
210
+ /** A number in a dot is a step number. */
200
211
  function findBadges(free, texts) {
201
- const parts = new Set();
202
- const badges = [];
203
- for (const c of free.filter((a)=>a.el.tag === "circle")){
204
- const r = num(c.el.attrs.r);
205
- if (!c.box || r <= 0 || r > BADGE_MAX_R) continue;
206
- const center = centerOf(c.box);
207
- const t = texts.find((a)=>!parts.has(a.el) && numbersDot(a, center, r));
208
- if (!t) continue;
209
- parts.add(c.el);
210
- parts.add(t.el);
211
- badges.push({
212
- circle: c.el,
213
- text: t.el,
214
- center,
215
- r,
216
- label: textLines(t.el).join(" ")
217
- });
218
- }
219
- return {
220
- badges,
221
- parts
222
- };
212
+ const parts = new Set();
213
+ const badges = [];
214
+ for (const c of free.filter((a) => a.el.tag === "circle")) {
215
+ const r = num(c.el.attrs.r);
216
+ if (!c.box || r <= 0 || r > BADGE_MAX_R) continue;
217
+ const center = centerOf(c.box);
218
+ const t = texts.find((a) => !parts.has(a.el) && numbersDot(a, center, r));
219
+ if (!t) continue;
220
+ parts.add(c.el);
221
+ parts.add(t.el);
222
+ badges.push({
223
+ circle: c.el,
224
+ text: t.el,
225
+ center,
226
+ r,
227
+ label: textLines(t.el).join(" ")
228
+ });
229
+ }
230
+ return {
231
+ badges,
232
+ parts
233
+ };
223
234
  }
224
- const isTip = (box)=>box !== null && box.x2 - box.x1 <= ARROWHEAD_MAX && box.y2 - box.y1 <= ARROWHEAD_MAX;
235
+ const isTip = (box) => box !== null && box.x2 - box.x1 <= ARROWHEAD_MAX && box.y2 - box.y1 <= ARROWHEAD_MAX;
236
+ /** A line that ends on two items joins them, and a small filled tip beside it is its arrowhead. */
225
237
  function isConnector(a, itemBoxes) {
226
- if (![
227
- "line",
228
- "polyline",
229
- "path"
230
- ].includes(a.el.tag)) return false;
231
- if (solid(a.el.attrs.fill)) return isTip(a.box);
232
- const ends = strokeEnds(a.el, a.offset);
233
- return ends !== null && ends.every((p)=>itemBoxes.some((b)=>contains(b, p, LAND_TOLERANCE)));
238
+ if (![
239
+ "line",
240
+ "polyline",
241
+ "path"
242
+ ].includes(a.el.tag)) return false;
243
+ if (solid(a.el.attrs.fill)) return isTip(a.box);
244
+ const ends = strokeEnds(a.el, a.offset);
245
+ return ends !== null && ends.every((p) => itemBoxes.some((b) => contains(b, p, LAND_TOLERANCE)));
234
246
  }
247
+ /** A rect with no rect inside it: a bare shape, or a card when it holds its own text. */
235
248
  function leafRect(sweep, x, hasWords, out) {
236
- if (!hasWords) {
237
- out.shapes.push({
238
- el: x.el,
239
- box: x.box
240
- });
241
- return;
242
- }
243
- if (areaOf(x.box) > areaOf(sweep.drawing) * MAX_CARD_AREA) return;
244
- const found = cardFromRect({
245
- group: x.container,
246
- kids: x.kids,
247
- rect: x.el,
248
- box: x.box,
249
- offset: x.offset,
250
- taken: sweep.taken
251
- });
252
- if (!found) return;
253
- for (const part of found.claimed)sweep.taken.add(part);
254
- out.cards.push(found);
249
+ if (!hasWords) {
250
+ out.shapes.push({
251
+ el: x.el,
252
+ box: x.box
253
+ });
254
+ return;
255
+ }
256
+ if (areaOf(x.box) > areaOf(sweep.drawing) * MAX_CARD_AREA) return;
257
+ const found = cardFromRect({
258
+ group: x.container,
259
+ kids: x.kids,
260
+ rect: x.el,
261
+ box: x.box,
262
+ offset: x.offset,
263
+ taken: sweep.taken
264
+ });
265
+ if (!found) return;
266
+ for (const part of found.claimed) sweep.taken.add(part);
267
+ out.cards.push(found);
255
268
  }
269
+ /** A rect round other items is a panel, a frame or a shape, by what it holds. */
256
270
  function classifyRect(sweep, x, dropped, out) {
257
- const { box } = x;
258
- const inner = sweep.rects.filter((o)=>o !== x && !dropped.has(o) && within(o.box, box, 1) && areaOf(o.box) < areaOf(box));
259
- const innerBoxes = [
260
- ...inner.map((o)=>o.box),
261
- ...sweep.groupCards.filter((c)=>within(c.box, box, 1)).map((c)=>c.box)
262
- ];
263
- const held = (point)=>innerBoxes.some((b)=>contains(b, point, 3)) ? false : contains(box, point, 3);
264
- const own = sweep.words.filter((w)=>held(anchorOf(w.el, w.offset)));
265
- if (innerBoxes.length === 0) {
266
- leafRect(sweep, x, own.length > 0, out);
267
- return;
268
- }
269
- const stray = sweep.strays.some(({ box: sb })=>sb !== null && within(sb, box, 1) && !innerBoxes.some((b)=>within(sb, b, 1)));
270
- if (own.length > 0 || stray) {
271
- out.panels.push(box);
272
- for (const o of inner)dropped.add(o);
273
- return;
274
- }
275
- if (innerBoxes.length >= 2) out.frames.push({
276
- el: x.el,
277
- box
278
- });
279
- else out.shapes.push({
280
- el: x.el,
281
- box
282
- });
271
+ const { box } = x;
272
+ const inner = sweep.rects.filter((o) => o !== x && !dropped.has(o) && within(o.box, box, 1) && areaOf(o.box) < areaOf(box));
273
+ const innerBoxes = [...inner.map((o) => o.box), ...sweep.groupCards.filter((c) => within(c.box, box, 1)).map((c) => c.box)];
274
+ const held = (point) => innerBoxes.some((b) => contains(b, point, 3)) ? false : contains(box, point, 3);
275
+ const own = sweep.words.filter((w) => held(anchorOf(w.el, w.offset)));
276
+ if (innerBoxes.length === 0) {
277
+ leafRect(sweep, x, own.length > 0, out);
278
+ return;
279
+ }
280
+ const stray = sweep.strays.some(({ box: sb }) => sb !== null && within(sb, box, 1) && !innerBoxes.some((b) => within(sb, b, 1)));
281
+ if (own.length > 0 || stray) {
282
+ out.panels.push(box);
283
+ for (const o of inner) dropped.add(o);
284
+ return;
285
+ }
286
+ if (innerBoxes.length >= 2) out.frames.push({
287
+ el: x.el,
288
+ box
289
+ });
290
+ else out.shapes.push({
291
+ el: x.el,
292
+ box
293
+ });
283
294
  }
284
295
  function sweepRects(sweep, out) {
285
- const dropped = new Set();
286
- for (const x of sweep.rects){
287
- if (!dropped.has(x)) classifyRect(sweep, x, dropped, out);
288
- }
296
+ const dropped = new Set();
297
+ for (const x of sweep.rects) {
298
+ if (!dropped.has(x)) classifyRect(sweep, x, dropped, out);
299
+ }
289
300
  }
301
+ /** Loose cards, bare rectangles, frames and step numbers. A rectangle with its own text and shapes inside is a panel, so its chrome stays. */
290
302
  export function findLoose(tree, drawing, taken, groupCards) {
291
- const atoms = [];
292
- flatten(tree, ORIGIN, false, atoms);
293
- const free = atoms.filter((a)=>!a.pins && a.box && !taken.has(a.el));
294
- const texts = free.filter((a)=>hasText(a.el));
295
- const { badges, parts } = findBadges(free, texts);
296
- const rects = free.filter((a)=>a.el.tag === "rect" && visible(a.el) && a.box !== null).filter((a)=>a.box.x2 - a.box.x1 >= MIN_SIDE && a.box.y2 - a.box.y1 >= MIN_SIDE).sort((a, b)=>areaOf(b.box) - areaOf(a.box));
297
- const itemBoxes = [
298
- ...rects.map((a)=>a.box),
299
- ...groupCards.map((c)=>c.box)
300
- ];
301
- const strays = free.filter((a)=>![
302
- "rect",
303
- "text"
304
- ].includes(a.el.tag) && !parts.has(a.el) && !isConnector(a, itemBoxes));
305
- const out = {
306
- cards: [],
307
- shapes: [],
308
- frames: [],
309
- badges,
310
- panels: []
311
- };
312
- const words = texts.filter((a)=>!parts.has(a.el));
313
- sweepRects({
314
- rects,
315
- words,
316
- strays,
317
- groupCards,
318
- drawing,
319
- taken
320
- }, out);
321
- out.badges = out.badges.filter((b)=>!taken.has(b.circle) && !taken.has(b.text));
322
- out.badges = out.badges.filter((b)=>!out.panels.some((p)=>contains(p, b.center, 0)));
323
- return out;
303
+ const atoms = [];
304
+ flatten(tree, ORIGIN, false, atoms);
305
+ const free = atoms.filter((a) => !a.pins && a.box && !taken.has(a.el));
306
+ const texts = free.filter((a) => hasText(a.el));
307
+ const { badges, parts } = findBadges(free, texts);
308
+ const rects = free.filter((a) => a.el.tag === "rect" && visible(a.el) && a.box !== null).filter((a) => a.box.x2 - a.box.x1 >= MIN_SIDE && a.box.y2 - a.box.y1 >= MIN_SIDE).sort((a, b) => areaOf(b.box) - areaOf(a.box));
309
+ const itemBoxes = [...rects.map((a) => a.box), ...groupCards.map((c) => c.box)];
310
+ const strays = free.filter((a) => !["rect", "text"].includes(a.el.tag) && !parts.has(a.el) && !isConnector(a, itemBoxes));
311
+ const out = {
312
+ cards: [],
313
+ shapes: [],
314
+ frames: [],
315
+ badges,
316
+ panels: []
317
+ };
318
+ const words = texts.filter((a) => !parts.has(a.el));
319
+ sweepRects({
320
+ rects,
321
+ words,
322
+ strays,
323
+ groupCards,
324
+ drawing,
325
+ taken
326
+ }, out);
327
+ // A number a card already took is not a mark of its own.
328
+ out.badges = out.badges.filter((b) => !taken.has(b.circle) && !taken.has(b.text));
329
+ out.badges = out.badges.filter((b) => !out.panels.some((p) => contains(p, b.center, 0)));
330
+ return out;
324
331
  }