@heyramzi/ui 1.54.2 → 2.0.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/COMPONENTS.md +3 -2
- package/README.md +34 -9
- package/package.json +205 -13
- package/scripts/sync-shadcn.mjs +2 -0
- package/src/ai/generation-progress/GenerationProgress.svelte +12 -12
- package/src/ambient.d.ts +8 -0
- package/src/brand/brand.test.ts +67 -0
- package/src/brand/index.ts +137 -0
- package/src/canvas/README.md +236 -0
- package/src/canvas/card/card-frame.svelte +61 -0
- package/src/canvas/card/card.css +39 -0
- package/src/canvas/card/resize-handles.svelte +119 -0
- package/src/canvas/card/resize.ts +33 -0
- package/src/canvas/chrome/chrome-zoom.svelte +120 -0
- package/src/canvas/chrome/chrome.css +231 -0
- package/src/canvas/chrome/lock-badge.svelte +18 -0
- package/src/canvas/chrome/side-handles.svelte +30 -0
- package/src/canvas/chrome/size-chip.svelte +45 -0
- package/src/canvas/chrome/snap-guides.svelte +69 -0
- package/src/canvas/components/icons/ToolIcon.svelte +30 -0
- package/src/canvas/components/shared/svg/PreviewSvg.svelte +67 -0
- package/src/canvas/components/shared/svg/ShapeOutline.svelte +76 -0
- package/src/canvas/components/shared/svg/StrokeNode.svelte +69 -0
- package/src/canvas/components/shared/svg/TextRule.svelte +37 -0
- package/src/canvas/edges/bend-handle.svelte +76 -0
- package/src/canvas/edges/edge-gradient.svelte +30 -0
- package/src/canvas/edges/edge-path.test.ts +58 -0
- package/src/canvas/edges/edge-path.ts +293 -0
- package/src/canvas/edges/gradient-edge.svelte +43 -0
- package/src/canvas/editor/context-menu.test.ts +65 -0
- package/src/canvas/editor/context-menu.ts +48 -0
- package/src/canvas/editor/dispatch.svelte.test.ts +243 -0
- package/src/canvas/editor/dispatch.svelte.ts +205 -0
- package/src/canvas/editor/editor-context.ts +62 -0
- package/src/canvas/editor/editor-mode.ts +67 -0
- package/src/canvas/editor/frame.ts +98 -0
- package/src/canvas/editor/hand-context.ts +15 -0
- package/src/canvas/editor/history-core.test.ts +239 -0
- package/src/canvas/editor/history-core.ts +247 -0
- package/src/canvas/editor/keymap.test.ts +161 -0
- package/src/canvas/editor/keymap.ts +151 -0
- package/src/canvas/editor/layout-actions.test.ts +156 -0
- package/src/canvas/editor/layout-actions.ts +219 -0
- package/src/canvas/editor/lock.test.ts +68 -0
- package/src/canvas/editor/lock.ts +87 -0
- package/src/canvas/editor/rebase.test.ts +208 -0
- package/src/canvas/editor/rebase.ts +272 -0
- package/src/canvas/editor/style.ts +184 -0
- package/src/canvas/editor/text-field-context.ts +41 -0
- package/src/canvas/editor/z-order.test.ts +75 -0
- package/src/canvas/editor/z-order.ts +87 -0
- package/src/canvas/headless/alignment.test.ts +59 -0
- package/src/canvas/headless/alignment.ts +23 -0
- package/src/canvas/headless/break-apart-find.ts +418 -0
- package/src/canvas/headless/break-apart-pieces.ts +104 -0
- package/src/canvas/headless/break-apart-plan.ts +439 -0
- package/src/canvas/headless/break-apart-style.ts +106 -0
- package/src/canvas/headless/break-apart.ts +244 -0
- package/src/canvas/headless/canvas-cleanup.test.ts +181 -0
- package/src/canvas/headless/canvas-cleanup.ts +102 -0
- package/src/canvas/headless/edge-anchors.test.ts +70 -0
- package/src/canvas/headless/edge-anchors.ts +143 -0
- package/src/canvas/headless/edge-memo.test.ts +58 -0
- package/src/canvas/headless/edge-memo.ts +32 -0
- package/src/canvas/headless/edge-serialization.test.ts +55 -0
- package/src/canvas/headless/edge-serialization.ts +32 -0
- package/src/canvas/headless/figure-geometry.ts +293 -0
- package/src/canvas/headless/fit-viewport.test.ts +85 -0
- package/src/canvas/headless/fit-viewport.ts +137 -0
- package/src/canvas/headless/grid.test.ts +34 -0
- package/src/canvas/headless/grid.ts +94 -0
- package/src/canvas/headless/icon-node.test.ts +23 -0
- package/src/canvas/headless/icon-node.ts +66 -0
- package/src/canvas/headless/index.ts +9 -0
- package/src/canvas/headless/linkify.test.ts +23 -0
- package/src/canvas/headless/linkify.ts +25 -0
- package/src/canvas/headless/pen-marks.ts +133 -0
- package/src/canvas/headless/sketch.test.ts +99 -0
- package/src/canvas/headless/sketch.ts +302 -0
- package/src/canvas/headless/snap-guides.test.ts +61 -0
- package/src/canvas/headless/snap-guides.ts +146 -0
- package/src/canvas/headless/svg-figure.test.ts +49 -0
- package/src/canvas/headless/svg-figure.ts +176 -0
- package/src/canvas/headless/table-layout.test.ts +158 -0
- package/src/canvas/headless/table-layout.ts +309 -0
- package/src/canvas/index.ts +35 -0
- package/src/canvas/lod/lod-mode.svelte.ts +30 -0
- package/src/canvas/lod/lod-switch.svelte +140 -0
- package/src/canvas/lod/render-all.svelte.ts +52 -0
- package/src/canvas/ops/arrange.ts +108 -0
- package/src/canvas/ops/ops.test.ts +98 -0
- package/src/canvas/ops/ops.ts +347 -0
- package/src/canvas/pen/PenField.svelte +128 -0
- package/src/canvas/pen/PenField.test.ts +87 -0
- package/src/canvas/pen/PenOverlay.svelte +172 -0
- package/src/canvas/pen/ShapeNode.svelte +64 -0
- package/src/canvas/pen/TextNode.svelte +97 -0
- package/src/canvas/pen/colors.ts +28 -0
- package/src/canvas/pen/eraser.ts +46 -0
- package/src/canvas/pen/geometry.ts +148 -0
- package/src/canvas/pen/host-field.test.svelte +13 -0
- package/src/canvas/pen/index.ts +40 -0
- package/src/canvas/pen/ops.ts +16 -0
- package/src/canvas/pen/pen-session.ts +177 -0
- package/src/canvas/pen/pen.css +149 -0
- package/src/canvas/pen/plugin.ts +139 -0
- package/src/canvas/pen/shapes.ts +104 -0
- package/src/canvas/pen/stroke-path.ts +54 -0
- package/src/canvas/pen/types.ts +96 -0
- package/src/canvas/pen/ui.ts +26 -0
- package/src/canvas/plugin.test.ts +45 -0
- package/src/canvas/plugin.ts +159 -0
- package/src/canvas/screen/GlyphIcon.svelte +25 -0
- package/src/canvas/screen/PartView.svelte +176 -0
- package/src/canvas/screen/ScreenBody.svelte +142 -0
- package/src/canvas/screen/ScreenCard.svelte +32 -0
- package/src/canvas/screen/glyphs/button.svg +1 -0
- package/src/canvas/screen/glyphs/close.svg +1 -0
- package/src/canvas/screen/glyphs/down.svg +1 -0
- package/src/canvas/screen/glyphs/header.svg +1 -0
- package/src/canvas/screen/glyphs/hills.svg +1 -0
- package/src/canvas/screen/glyphs/image.svg +1 -0
- package/src/canvas/screen/glyphs/input.svg +1 -0
- package/src/canvas/screen/glyphs/list.svg +1 -0
- package/src/canvas/screen/glyphs/modal.svg +1 -0
- package/src/canvas/screen/glyphs/plus.svg +1 -0
- package/src/canvas/screen/glyphs/up.svg +1 -0
- package/src/canvas/screen/glyphs.ts +40 -0
- package/src/canvas/screen/labels.ts +41 -0
- package/src/canvas/screen/parts.ts +164 -0
- package/src/canvas/screen/plugin.ts +67 -0
- package/src/canvas/screen/screen.css +593 -0
- package/src/canvas/screen/types.ts +47 -0
- package/src/canvas/screen/ui.ts +13 -0
- package/src/canvas/test-utils.ts +18 -0
- package/src/canvas/types.ts +53 -0
- package/src/canvas/ui/CanvasToolbar.svelte +169 -0
- package/src/canvas/ui/FrameLayer.svelte +116 -0
- package/src/canvas/ui/PenLayer.svelte +39 -0
- package/src/canvas/ui/StylePanel.svelte +268 -0
- package/src/canvas/ui/canvas-grid.svelte +156 -0
- package/src/canvas/ui/canvas-ui.css +350 -0
- package/src/canvas/ui/forward-wheel.ts +29 -0
- package/src/canvas/ui/icons.ts +124 -0
- package/src/canvas/ui/place-panel.ts +80 -0
- package/src/canvas/ui/shortcut-tip.test.ts +16 -0
- package/src/canvas/ui/shortcut-tip.ts +17 -0
- package/src/canvas/ui/tools.ts +146 -0
- package/src/charts/README.md +104 -0
- package/src/charts/ambient.d.ts +4 -0
- package/src/charts/animated-value.css +7 -0
- package/src/charts/animated-value.svelte +110 -0
- package/src/charts/area-chart.css +29 -0
- package/src/charts/area-chart.svelte +204 -0
- package/src/charts/bar-chart.css +119 -0
- package/src/charts/bar-chart.svelte +275 -0
- package/src/charts/charts.css +201 -0
- package/src/charts/crosshair.svelte +35 -0
- package/src/charts/dashboard/chart.ts +5 -0
- package/src/charts/dashboard/dashboard-empty.svelte +55 -0
- package/src/charts/dashboard/dashboard-section.svelte +58 -0
- package/src/charts/dashboard/data-panel.svelte +81 -0
- package/src/charts/dashboard/data-sort-head.svelte +102 -0
- package/src/charts/dashboard/data-table.svelte +278 -0
- package/src/charts/dashboard/data-table.ts +26 -0
- package/src/charts/dashboard/index.ts +25 -0
- package/src/charts/dashboard/panel-grid.svelte +43 -0
- package/src/charts/dashboard/period-filter.svelte +53 -0
- package/src/charts/dashboard/rating-chip.svelte +35 -0
- package/src/charts/dashboard/stat-band.svelte +47 -0
- package/src/charts/dashboard/stat-tile.svelte +176 -0
- package/src/charts/dashboard/windows.ts +8 -0
- package/src/charts/donut-chart.css +56 -0
- package/src/charts/donut-chart.svelte +158 -0
- package/src/charts/edge-fade.svelte +43 -0
- package/src/charts/edge-fade.ts +16 -0
- package/src/charts/figure.css +100 -0
- package/src/charts/figure.svelte +122 -0
- package/src/charts/gauge-chart.css +94 -0
- package/src/charts/gauge-chart.svelte +302 -0
- package/src/charts/hover-band.test.ts +30 -0
- package/src/charts/hover-band.ts +21 -0
- package/src/charts/hover-focus.svelte +85 -0
- package/src/charts/index.ts +31 -0
- package/src/charts/legend.css +136 -0
- package/src/charts/legend.svelte +112 -0
- package/src/charts/line-chart.css +26 -0
- package/src/charts/line-chart.svelte +256 -0
- package/src/charts/motion.ts +19 -0
- package/src/charts/number-chart.css +179 -0
- package/src/charts/number-chart.svelte +187 -0
- package/src/charts/primitives/chart-container.svelte +57 -0
- package/src/charts/primitives/chart-style.svelte +36 -0
- package/src/charts/primitives/chart-tooltip.svelte +161 -0
- package/src/charts/primitives/chart-utils.ts +54 -0
- package/src/charts/primitives/index.ts +11 -0
- package/src/charts/reference-line.css +18 -0
- package/src/charts/reference-line.svelte +31 -0
- package/src/charts/reveal.svelte +61 -0
- package/src/charts/ring-chart.css +104 -0
- package/src/charts/ring-chart.svelte +166 -0
- package/src/charts/sparkline.css +45 -0
- package/src/charts/sparkline.svelte +141 -0
- package/src/charts/stat-card.css +105 -0
- package/src/charts/stat-card.svelte +267 -0
- package/src/charts/time-series-chart.css +28 -0
- package/src/charts/time-series-chart.svelte +179 -0
- package/src/charts/trend-pill.css +38 -0
- package/src/charts/trend-pill.svelte +48 -0
- package/src/components/copy/copy-button.svelte +1 -1
- package/src/components/docs/doc-page.svelte +1 -1
- package/src/components/docs/on-this-page.svelte +2 -2
- package/src/components/list-page/empty-state.svelte +1 -1
- package/src/components/list-page/filter-popover.svelte +1 -1
- package/src/components/list-page/section-label.svelte +1 -1
- package/src/components/segmented/segment.svelte +4 -4
- package/src/components/theme/theme-provider.svelte +6 -4
- package/src/components/toolbar/index.ts +5 -0
- package/src/components/toolbar/toolbar.svelte +168 -0
- package/src/components/toolbar/toolbar.test.ts +66 -0
- package/src/editor/README.md +58 -0
- package/src/editor/ambient.d.ts +1 -0
- package/src/editor/block-id.ts +77 -0
- package/src/editor/block-ops.test.ts +237 -0
- package/src/editor/block-ops.ts +157 -0
- package/src/editor/content.css +418 -0
- package/src/editor/editor-toolbar.svelte +303 -0
- package/src/editor/editor.svelte +469 -0
- package/src/editor/extensions.ts +145 -0
- package/src/editor/image-toolbar.svelte +123 -0
- package/src/editor/index.ts +17 -0
- package/src/editor/inline-editor.svelte +261 -0
- package/src/editor/markdown-escaping.test.ts +158 -0
- package/src/editor/markdown-marks.test.ts +89 -0
- package/src/editor/markdown-test-helpers.ts +13 -0
- package/src/editor/markdown-youtube.test.ts +93 -0
- package/src/editor/markdown.test.ts +250 -0
- package/src/editor/markdown.ts +511 -0
- package/src/editor/renderer.svelte +92 -0
- package/src/editor/serializer.test.ts +133 -0
- package/src/editor/serializer.ts +370 -0
- package/src/editor/slash-items.ts +115 -0
- package/src/editor/slash-menu.svelte +159 -0
- package/src/editor/youtube-url.ts +35 -0
- package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +13 -13
- package/src/gallery.test.ts +20 -1
- package/src/lib/result.ts +19 -0
- package/src/onboarding/README.md +120 -0
- package/src/onboarding/define-tours.test.ts +50 -0
- package/src/onboarding/define-tours.ts +31 -0
- package/src/onboarding/demo.svelte +86 -0
- package/src/onboarding/index.ts +12 -0
- package/src/onboarding/onboarding-provider.svelte +120 -0
- package/src/onboarding/onboarding.svelte.ts +24 -0
- package/src/onboarding/pick.test.ts +79 -0
- package/src/onboarding/pick.ts +36 -0
- package/src/onboarding/stores.test.ts +103 -0
- package/src/onboarding/stores.ts +58 -0
- package/src/onboarding/tour-card.svelte +70 -0
- package/src/onboarding/tour-runner.svelte +197 -0
- package/src/onboarding/types.ts +34 -0
- package/src/onboarding/wait.ts +29 -0
- package/src/shadcn/command/command-input.svelte +1 -1
- package/src/slides/Controls.svelte +138 -0
- package/src/slides/Deck.svelte +271 -0
- package/src/slides/Fragment.svelte +71 -0
- package/src/slides/Overview.svelte +113 -0
- package/src/slides/Presenter.svelte +292 -0
- package/src/slides/README.md +89 -0
- package/src/slides/Slide.svelte +179 -0
- package/src/slides/SlideList.svelte +19 -0
- package/src/slides/ambient.d.ts +1 -0
- package/src/slides/bridges.ts +69 -0
- package/src/slides/deck-controller.svelte.ts +292 -0
- package/src/slides/demo-slides.svelte +358 -0
- package/src/slides/demo.svelte +176 -0
- package/src/slides/export/capture.ts +113 -0
- package/src/slides/export/ffmpeg.ts +120 -0
- package/src/slides/export/index.ts +9 -0
- package/src/slides/export/launch.ts +10 -0
- package/src/slides/export/pdf.ts +152 -0
- package/src/slides/export/types.ts +48 -0
- package/src/slides/index.ts +19 -0
- package/src/slides/labels.ts +48 -0
- package/src/slides/layout.ts +51 -0
- package/src/slides/model.ts +47 -0
- package/src/slides/navigation.ts +100 -0
- package/src/slides/theme.css +205 -0
- package/src/slides/types.ts +86 -0
- package/src/tasks/README.md +312 -0
- package/src/tasks/ambient.d.ts +3 -0
- package/src/tasks/board-order.ts +137 -0
- package/src/tasks/dates.ts +31 -0
- package/src/tasks/define-statuses.ts +32 -0
- package/src/tasks/demo.svelte +389 -0
- package/src/tasks/detail/detail-demo.svelte +186 -0
- package/src/tasks/detail/detail-types.ts +54 -0
- package/src/tasks/detail/task-chip.svelte +28 -0
- package/src/tasks/detail/task-detail.svelte +315 -0
- package/src/tasks/detail/task-property.svelte +17 -0
- package/src/tasks/detail/task-thread.svelte +258 -0
- package/src/tasks/gantt/gantt-bar.svelte +243 -0
- package/src/tasks/gantt/gantt-grid.svelte +94 -0
- package/src/tasks/gantt/gantt-group-row.svelte +55 -0
- package/src/tasks/gantt/gantt-header.svelte +71 -0
- package/src/tasks/gantt/gantt-task-row.svelte +186 -0
- package/src/tasks/gantt/gantt.css +22 -0
- package/src/tasks/gantt/gantt.test.ts +77 -0
- package/src/tasks/gantt/logic.test.ts +163 -0
- package/src/tasks/gantt/logic.ts +248 -0
- package/src/tasks/inbox/inbox-card.svelte +283 -0
- package/src/tasks/inbox/inbox-draft.svelte +84 -0
- package/src/tasks/inbox/inbox-logic.ts +55 -0
- package/src/tasks/inbox/inbox-statuses.ts +48 -0
- package/src/tasks/inbox/inbox-trace.svelte +45 -0
- package/src/tasks/inbox/inbox-types.ts +108 -0
- package/src/tasks/inbox/inbox.svelte +203 -0
- package/src/tasks/inbox/index.ts +23 -0
- package/src/tasks/index.ts +85 -0
- package/src/tasks/kanban-board.svelte +561 -0
- package/src/tasks/kanban-column.svelte +117 -0
- package/src/tasks/kanban-drag.ts +38 -0
- package/src/tasks/kanban-types.ts +65 -0
- package/src/tasks/presets/sales.ts +16 -0
- package/src/tasks/record/demo-data.ts +286 -0
- package/src/tasks/record/field-section.svelte +53 -0
- package/src/tasks/record/fields.ts +142 -0
- package/src/tasks/record/format.ts +87 -0
- package/src/tasks/record/gutter.ts +2 -0
- package/src/tasks/record/index.ts +54 -0
- package/src/tasks/record/info-list.svelte +316 -0
- package/src/tasks/record/kinds.ts +42 -0
- package/src/tasks/record/logic.ts +324 -0
- package/src/tasks/record/next-up.svelte +70 -0
- package/src/tasks/record/project-list.svelte +133 -0
- package/src/tasks/record/record-demo.svelte +324 -0
- package/src/tasks/record/record-header.svelte +70 -0
- package/src/tasks/record/record-shell.svelte +138 -0
- package/src/tasks/record/record-stats.svelte +48 -0
- package/src/tasks/record/record-timeline.svelte +231 -0
- package/src/tasks/record/record.test.ts +350 -0
- package/src/tasks/record/steps-rail.svelte +474 -0
- package/src/tasks/record/types.ts +280 -0
- package/src/tasks/record/where-we-are.svelte +121 -0
- package/src/tasks/reorder.ts +19 -0
- package/src/tasks/side-panel-types.ts +16 -0
- package/src/tasks/side-panel.svelte +193 -0
- package/src/tasks/status-badge.svelte +63 -0
- package/src/tasks/status-dot.svelte +228 -0
- package/src/tasks/status-icon.svelte +30 -0
- package/src/tasks/status-types.ts +40 -0
- package/src/tasks/task-gantt.svelte +230 -0
- package/src/tasks/task-list.svelte +470 -0
- package/src/tasks/task-types.ts +33 -0
- package/src/tasks/time/duration.ts +69 -0
- package/src/tasks/time/time-chip.svelte +62 -0
- package/src/tasks/time/time-dot.svelte +58 -0
- package/src/tasks/time/time-ring.svelte +74 -0
- package/src/tasks/time/time-tracker.svelte +565 -0
- package/src/tasks/time/time-types.ts +74 -0
- package/src/tasks/use-kanban-dnd.svelte.ts +173 -0
- package/src/tokens/base.css +5 -5
- package/src/tokens/palette.css +115 -120
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
// Break apart, the writing half: the ops that swap a figure's pieces for real nodes and edges.
|
|
2
|
+
|
|
3
|
+
import type { CanvasOp } from "../ops/ops.js";
|
|
4
|
+
import type { PenColor } from "../pen/types.js";
|
|
5
|
+
import {
|
|
6
|
+
distanceToBox,
|
|
7
|
+
distanceToSegment,
|
|
8
|
+
areaOf,
|
|
9
|
+
contains,
|
|
10
|
+
num,
|
|
11
|
+
solid,
|
|
12
|
+
within,
|
|
13
|
+
type Box,
|
|
14
|
+
type FigureChild,
|
|
15
|
+
type FigureEl,
|
|
16
|
+
type Pt,
|
|
17
|
+
} from "./figure-geometry.js";
|
|
18
|
+
import {
|
|
19
|
+
LAND_TOLERANCE,
|
|
20
|
+
type Badge,
|
|
21
|
+
type Found,
|
|
22
|
+
type Loose,
|
|
23
|
+
} from "./break-apart-find.js";
|
|
24
|
+
import type { Arrow, Pieces } from "./break-apart-pieces.js";
|
|
25
|
+
import {
|
|
26
|
+
NOTE_TITLE_PX,
|
|
27
|
+
accentOf,
|
|
28
|
+
escapeMarkdown,
|
|
29
|
+
fitScale,
|
|
30
|
+
inked,
|
|
31
|
+
penColorOf,
|
|
32
|
+
} from "./break-apart-style.js";
|
|
33
|
+
|
|
34
|
+
/** The heading above the drawing; mirrors LABEL_H in svg-figure.ts. */
|
|
35
|
+
const LABEL_H = 27;
|
|
36
|
+
/** A step number sits this many drawing units from the card or arrow it numbers. */
|
|
37
|
+
const BADGE_REACH = 6;
|
|
38
|
+
const BADGE_LINK_REACH = 60;
|
|
39
|
+
const MARK_MIN = 24;
|
|
40
|
+
|
|
41
|
+
export type FigureNode = {
|
|
42
|
+
id: string;
|
|
43
|
+
position: { x: number; y: number };
|
|
44
|
+
width?: number;
|
|
45
|
+
parentId?: string;
|
|
46
|
+
data: Record<string, unknown>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** The figure node and what a caller measured about it. The parsed tree is read separately. */
|
|
50
|
+
export type PlanSource = {
|
|
51
|
+
/** The figure node as the board holds it. */
|
|
52
|
+
node: FigureNode;
|
|
53
|
+
/** Where the drawing sits inside the node box, in board px. Measured in a browser; headless callers omit it. */
|
|
54
|
+
art?: { left: number; top: number; width: number };
|
|
55
|
+
/** Ids already on the board, so a new card never reuses one. */
|
|
56
|
+
takenIds?: ReadonlySet<string>;
|
|
57
|
+
/** The look every new link starts with (the app's `NEW_EDGE_STYLE`). */
|
|
58
|
+
edgeData?: Record<string, unknown>;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Everything the emitters share while one figure breaks apart. */
|
|
62
|
+
export type Plan = {
|
|
63
|
+
node: FigureNode;
|
|
64
|
+
drawing: Box;
|
|
65
|
+
/** Board px per drawing unit. */
|
|
66
|
+
scale: number;
|
|
67
|
+
artLeft: number;
|
|
68
|
+
artTop: number;
|
|
69
|
+
artWidth: number;
|
|
70
|
+
cards: Found[];
|
|
71
|
+
loose: Loose;
|
|
72
|
+
arrows: Arrow[];
|
|
73
|
+
edgeData?: Record<string, unknown>;
|
|
74
|
+
taken: Set<string>;
|
|
75
|
+
removed: Set<FigureChild>;
|
|
76
|
+
ops: CanvasOp[];
|
|
77
|
+
cardIds: string[];
|
|
78
|
+
frameIds: string[];
|
|
79
|
+
/** Shapes and marks added so far, which numbers their ids. */
|
|
80
|
+
shapes: number;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
type Link = { arrow: Arrow; src: number; dst: number };
|
|
84
|
+
|
|
85
|
+
export function freshId(base: string, taken: Set<string>): string {
|
|
86
|
+
let id = base;
|
|
87
|
+
for (let n = 2; taken.has(id); n += 1) id = `${base}-${n}`;
|
|
88
|
+
taken.add(id);
|
|
89
|
+
return id;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function startPlan(
|
|
93
|
+
source: PlanSource,
|
|
94
|
+
pieces: Pieces,
|
|
95
|
+
drawing: Box,
|
|
96
|
+
artWidth: number,
|
|
97
|
+
): Plan {
|
|
98
|
+
const { node } = source;
|
|
99
|
+
const taken = new Set(source.takenIds ?? []);
|
|
100
|
+
taken.add(node.id);
|
|
101
|
+
const cardIds = pieces.cards.map((_, i) => freshId(`${node.id}-card-${i + 1}`, taken));
|
|
102
|
+
const frameIds = pieces.loose.frames.map((_, i) => freshId(`${node.id}-frame-${i + 1}`, taken));
|
|
103
|
+
return {
|
|
104
|
+
node,
|
|
105
|
+
drawing,
|
|
106
|
+
scale: artWidth / (drawing.x2 - drawing.x1),
|
|
107
|
+
artLeft: source.art?.left ?? 0,
|
|
108
|
+
artTop: source.art?.top ?? (node.data.label ? LABEL_H : 0),
|
|
109
|
+
artWidth,
|
|
110
|
+
cards: pieces.cards,
|
|
111
|
+
loose: pieces.loose,
|
|
112
|
+
arrows: pieces.arrows,
|
|
113
|
+
edgeData: source.edgeData,
|
|
114
|
+
taken,
|
|
115
|
+
removed: new Set(),
|
|
116
|
+
ops: [],
|
|
117
|
+
cardIds,
|
|
118
|
+
frameIds,
|
|
119
|
+
shapes: 0,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// =========================
|
|
124
|
+
// PLACEMENT
|
|
125
|
+
// =========================
|
|
126
|
+
|
|
127
|
+
export function toBoard(plan: Plan, p: Pt): Pt {
|
|
128
|
+
return {
|
|
129
|
+
x: Math.round(plan.node.position.x + plan.artLeft + (p.x - plan.drawing.x1) * plan.scale),
|
|
130
|
+
y: Math.round(plan.node.position.y + plan.artTop + (p.y - plan.drawing.y1) * plan.scale),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The smallest frame round a box, so a card inside two frames sits in the inner one. */
|
|
135
|
+
function frameOf(plan: Plan, box: Box, self = -1): number {
|
|
136
|
+
const { frames } = plan.loose;
|
|
137
|
+
let best = -1;
|
|
138
|
+
frames.forEach((frame, i) => {
|
|
139
|
+
if (i === self || !within(box, frame.box, 1) || areaOf(frame.box) <= areaOf(box)) return;
|
|
140
|
+
if (best < 0 || areaOf(frame.box) < areaOf(frames[best].box)) best = i;
|
|
141
|
+
});
|
|
142
|
+
return best;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Where a frame's own space starts on the board, or the figure's origin when there is no frame. */
|
|
146
|
+
function frameOrigin(plan: Plan, frame: number): Pt {
|
|
147
|
+
if (frame < 0) return { x: 0, y: 0 };
|
|
148
|
+
const { box } = plan.loose.frames[frame];
|
|
149
|
+
return toBoard(plan, { x: box.x1, y: box.y1 });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** A position in the frame's own space, or the figure's when there is no frame. */
|
|
153
|
+
function placed(plan: Plan, box: Box, frame: number): Pt {
|
|
154
|
+
const at = toBoard(plan, { x: box.x1, y: box.y1 });
|
|
155
|
+
const origin = frameOrigin(plan, frame);
|
|
156
|
+
return { x: at.x - origin.x, y: at.y - origin.y };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parentOf(plan: Plan, frame: number): { parentId?: string } {
|
|
160
|
+
if (frame >= 0) return { parentId: plan.frameIds[frame] };
|
|
161
|
+
return plan.node.parentId ? { parentId: plan.node.parentId } : {};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function sizeOf(plan: Plan, box: Box): { width: number; height: number } {
|
|
165
|
+
return {
|
|
166
|
+
width: Math.round((box.x2 - box.x1) * plan.scale),
|
|
167
|
+
height: Math.round((box.y2 - box.y1) * plan.scale),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** A shape drawn with fill alone gets the thinnest line the pen has. */
|
|
172
|
+
function strokeOf(plan: Plan, el: FigureEl): number {
|
|
173
|
+
if (!solid(el.attrs.stroke)) return 1;
|
|
174
|
+
return Math.max(1, Math.round(num(el.attrs["stroke-width"], 1.5) * plan.scale));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function colorOf(el: FigureEl): PenColor {
|
|
178
|
+
const stroked = solid(el.attrs.stroke) ? penColorOf(el.attrs.stroke) : undefined;
|
|
179
|
+
return stroked ?? penColorOf(el.attrs.fill) ?? "ink";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// =========================
|
|
183
|
+
// FRAMES AND SHAPES
|
|
184
|
+
// =========================
|
|
185
|
+
|
|
186
|
+
/** Frames come first so what sits in them draws above. */
|
|
187
|
+
export function addFrames(plan: Plan) {
|
|
188
|
+
plan.loose.frames.forEach((frame, i) => {
|
|
189
|
+
plan.removed.add(frame.el);
|
|
190
|
+
const parent = frameOf(plan, frame.box, i);
|
|
191
|
+
const { width, height } = sizeOf(plan, frame.box);
|
|
192
|
+
plan.ops.push({
|
|
193
|
+
kind: "addNode",
|
|
194
|
+
node: {
|
|
195
|
+
id: plan.frameIds[i],
|
|
196
|
+
type: "wrapper",
|
|
197
|
+
position: placed(plan, frame.box, parent),
|
|
198
|
+
data: {
|
|
199
|
+
nodeType: "wrapper",
|
|
200
|
+
label: "",
|
|
201
|
+
frame: true,
|
|
202
|
+
stroke: colorOf(frame.el),
|
|
203
|
+
strokeWidth: strokeOf(plan, frame.el),
|
|
204
|
+
fill: inked(frame.el.attrs.fill) ? (penColorOf(frame.el.attrs.fill) ?? "none") : "none",
|
|
205
|
+
},
|
|
206
|
+
style: { width, height },
|
|
207
|
+
width,
|
|
208
|
+
height,
|
|
209
|
+
...parentOf(plan, parent),
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function addShape(plan: Plan, shape: Record<string, unknown>, box: Box, frame: number) {
|
|
216
|
+
plan.shapes += 1;
|
|
217
|
+
const { width, height } = sizeOf(plan, box);
|
|
218
|
+
plan.ops.push({
|
|
219
|
+
kind: "addNode",
|
|
220
|
+
node: {
|
|
221
|
+
id: freshId(`${plan.node.id}-shape-${plan.shapes}`, plan.taken),
|
|
222
|
+
type: "shape",
|
|
223
|
+
position: placed(plan, box, frame),
|
|
224
|
+
data: { strokeWidth: 2, filled: false, label: "", ...shape },
|
|
225
|
+
width,
|
|
226
|
+
height,
|
|
227
|
+
...parentOf(plan, frame),
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function addShapes(plan: Plan) {
|
|
233
|
+
for (const shape of plan.loose.shapes) {
|
|
234
|
+
plan.removed.add(shape.el);
|
|
235
|
+
const fill = shape.el.attrs.fill;
|
|
236
|
+
const fillPen = inked(fill) ? penColorOf(fill) : undefined;
|
|
237
|
+
addShape(
|
|
238
|
+
plan,
|
|
239
|
+
{
|
|
240
|
+
shape: "rectangle",
|
|
241
|
+
color: colorOf(shape.el),
|
|
242
|
+
strokeWidth: strokeOf(plan, shape.el),
|
|
243
|
+
filled: inked(fill),
|
|
244
|
+
...(fillPen ? { fill: fillPen } : {}),
|
|
245
|
+
},
|
|
246
|
+
shape.box,
|
|
247
|
+
frameOf(plan, shape.box),
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// =========================
|
|
253
|
+
// LINKS AND STEP NUMBERS
|
|
254
|
+
// =========================
|
|
255
|
+
|
|
256
|
+
function cardAt(cards: Found[], point: Pt): number {
|
|
257
|
+
let best = -1;
|
|
258
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
259
|
+
cards.forEach((card, index) => {
|
|
260
|
+
if (!contains(card.box, point, LAND_TOLERANCE)) return;
|
|
261
|
+
const dx = Math.max(card.box.x1 - point.x, 0, point.x - card.box.x2);
|
|
262
|
+
const dy = Math.max(card.box.y1 - point.y, 0, point.y - card.box.y2);
|
|
263
|
+
const distance = Math.hypot(dx, dy);
|
|
264
|
+
if (distance < bestDistance) {
|
|
265
|
+
best = index;
|
|
266
|
+
bestDistance = distance;
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
return best;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function sideOf(box: Box, point: Pt): "top" | "bottom" | "left" | "right" {
|
|
273
|
+
const gaps = [
|
|
274
|
+
["left", Math.abs(point.x - box.x1)],
|
|
275
|
+
["right", Math.abs(point.x - box.x2)],
|
|
276
|
+
["top", Math.abs(point.y - box.y1)],
|
|
277
|
+
["bottom", Math.abs(point.y - box.y2)],
|
|
278
|
+
] as const;
|
|
279
|
+
return [...gaps].sort((a, b) => a[1] - b[1])[0][0];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** The arrows that run from one card to another. */
|
|
283
|
+
export function linksOf(plan: Plan): Link[] {
|
|
284
|
+
const links: Link[] = [];
|
|
285
|
+
for (const arrow of plan.arrows) {
|
|
286
|
+
const src = cardAt(plan.cards, arrow.from);
|
|
287
|
+
const dst = cardAt(plan.cards, arrow.to);
|
|
288
|
+
if (src >= 0 && dst >= 0 && src !== dst) links.push({ arrow, src, dst });
|
|
289
|
+
}
|
|
290
|
+
return links;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function nearestCard(cards: Found[], badge: Badge): number {
|
|
294
|
+
const reach = badge.r + BADGE_REACH;
|
|
295
|
+
let card = -1;
|
|
296
|
+
let cardGap = Number.POSITIVE_INFINITY;
|
|
297
|
+
cards.forEach((c, i) => {
|
|
298
|
+
const gap = distanceToBox(c.box, badge.center);
|
|
299
|
+
if (gap <= reach && gap < cardGap) {
|
|
300
|
+
card = i;
|
|
301
|
+
cardGap = gap;
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
return card;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function nearestLink(links: Link[], badge: Badge): Arrow | null {
|
|
308
|
+
let link: Arrow | null = null;
|
|
309
|
+
let linkGap = Math.max(BADGE_LINK_REACH, badge.r * 3);
|
|
310
|
+
for (const l of links) {
|
|
311
|
+
const gap = distanceToSegment(l.arrow.from, l.arrow.to, badge.center);
|
|
312
|
+
if (gap < linkGap) {
|
|
313
|
+
link = l.arrow;
|
|
314
|
+
linkGap = gap;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return link;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** A step number with no card or link to join becomes a small filled circle of its own. */
|
|
321
|
+
function addMark(plan: Plan, badge: Badge) {
|
|
322
|
+
const side = Math.max(Math.round(badge.r * 2 * plan.scale), MARK_MIN);
|
|
323
|
+
const at = toBoard(plan, badge.center);
|
|
324
|
+
const frame = frameOf(plan, {
|
|
325
|
+
x1: badge.center.x,
|
|
326
|
+
y1: badge.center.y,
|
|
327
|
+
x2: badge.center.x,
|
|
328
|
+
y2: badge.center.y,
|
|
329
|
+
});
|
|
330
|
+
const origin = frameOrigin(plan, frame);
|
|
331
|
+
plan.shapes += 1;
|
|
332
|
+
plan.ops.push({
|
|
333
|
+
kind: "addNode",
|
|
334
|
+
node: {
|
|
335
|
+
id: freshId(`${plan.node.id}-mark-${plan.shapes}`, plan.taken),
|
|
336
|
+
type: "shape",
|
|
337
|
+
position: {
|
|
338
|
+
x: at.x - origin.x - Math.round(side / 2),
|
|
339
|
+
y: at.y - origin.y - Math.round(side / 2),
|
|
340
|
+
},
|
|
341
|
+
data: {
|
|
342
|
+
shape: "ellipse",
|
|
343
|
+
color: colorOf(badge.circle),
|
|
344
|
+
strokeWidth: 2,
|
|
345
|
+
filled: true,
|
|
346
|
+
label: badge.label,
|
|
347
|
+
},
|
|
348
|
+
width: side,
|
|
349
|
+
height: side,
|
|
350
|
+
...parentOf(plan, frame),
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** A step number next to a card is its badge, next to a link its label, and otherwise a mark. */
|
|
356
|
+
export function placeBadges(plan: Plan, links: Link[]): Map<Arrow, string> {
|
|
357
|
+
const linkLabels = new Map<Arrow, string>();
|
|
358
|
+
for (const badge of plan.loose.badges) {
|
|
359
|
+
const card = nearestCard(plan.cards, badge);
|
|
360
|
+
const link = card < 0 ? nearestLink(links, badge) : null;
|
|
361
|
+
if (card >= 0) {
|
|
362
|
+
const kicker = plan.cards[card].kicker;
|
|
363
|
+
plan.cards[card].kicker = [badge.label, kicker].filter(Boolean).join(" · ");
|
|
364
|
+
} else if (link && !linkLabels.has(link)) linkLabels.set(link, badge.label);
|
|
365
|
+
else addMark(plan, badge);
|
|
366
|
+
plan.removed.add(badge.circle);
|
|
367
|
+
plan.removed.add(badge.text);
|
|
368
|
+
}
|
|
369
|
+
return linkLabels;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// =========================
|
|
373
|
+
// CARDS AND EDGES
|
|
374
|
+
// =========================
|
|
375
|
+
|
|
376
|
+
function noteData(plan: Plan, card: Found, width: number, height: number) {
|
|
377
|
+
const accent = accentOf(card.rect.attrs.stroke);
|
|
378
|
+
const rx = num(card.rect.attrs.rx);
|
|
379
|
+
const textScale = fitScale(card, (card.titleSize * plan.scale) / NOTE_TITLE_PX, width, height);
|
|
380
|
+
return {
|
|
381
|
+
nodeType: "note",
|
|
382
|
+
label: card.title,
|
|
383
|
+
...(card.body.length > 0 ? { content: card.body.map(escapeMarkdown).join("\n") } : {}),
|
|
384
|
+
...(card.kicker ? { kicker: card.kicker } : {}),
|
|
385
|
+
...(accent ? { accent } : {}),
|
|
386
|
+
...(rx < 3 ? { shape: "rect" } : {}),
|
|
387
|
+
userHeight: height,
|
|
388
|
+
...(Math.abs(textScale - 1) > 0.02 ? { textScale } : {}),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function addCards(plan: Plan) {
|
|
393
|
+
const cardFrames = plan.cards.map((card) => frameOf(plan, card.box));
|
|
394
|
+
plan.cards.forEach((card, i) => {
|
|
395
|
+
for (const part of card.claimed) plan.removed.add(part);
|
|
396
|
+
const frame = cardFrames[i];
|
|
397
|
+
const { width, height } = sizeOf(plan, card.box);
|
|
398
|
+
plan.ops.push({
|
|
399
|
+
kind: "addNode",
|
|
400
|
+
node: {
|
|
401
|
+
id: plan.cardIds[i],
|
|
402
|
+
type: "note",
|
|
403
|
+
position: placed(plan, card.box, frame),
|
|
404
|
+
data: noteData(plan, card, width, height),
|
|
405
|
+
style: { width },
|
|
406
|
+
width,
|
|
407
|
+
...parentOf(plan, frame),
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function addEdges(plan: Plan, links: Link[], linkLabels: Map<Arrow, string>) {
|
|
414
|
+
links.forEach(({ arrow, src: a, dst: b }, i) => {
|
|
415
|
+
plan.removed.add(arrow.el);
|
|
416
|
+
if (arrow.head) plan.removed.add(arrow.head);
|
|
417
|
+
// An arrow drawn tip-first reads backwards: the link runs from the tail to the tip.
|
|
418
|
+
const reversed = arrow.startArrow && !arrow.endArrow;
|
|
419
|
+
const [src, dst] = reversed ? [b, a] : [a, b];
|
|
420
|
+
const [srcPt, dstPt] = reversed ? [arrow.to, arrow.from] : [arrow.from, arrow.to];
|
|
421
|
+
const label = linkLabels.get(arrow);
|
|
422
|
+
plan.ops.push({
|
|
423
|
+
kind: "addEdge",
|
|
424
|
+
edge: {
|
|
425
|
+
id: freshId(`${plan.node.id}-link-${i + 1}`, plan.taken),
|
|
426
|
+
source: plan.cardIds[src],
|
|
427
|
+
target: plan.cardIds[dst],
|
|
428
|
+
sourceHandle: sideOf(plan.cards[src].box, srcPt),
|
|
429
|
+
targetHandle: sideOf(plan.cards[dst].box, dstPt),
|
|
430
|
+
...(label ? { label } : {}),
|
|
431
|
+
data: {
|
|
432
|
+
...plan.edgeData,
|
|
433
|
+
endArrow: arrow.startArrow || arrow.endArrow,
|
|
434
|
+
startArrow: arrow.startArrow && arrow.endArrow,
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Break apart, the look: which sketch accent or pen colour a drawn colour maps to, and how big a note sets.
|
|
2
|
+
|
|
3
|
+
import { SKETCH_ACCENTS, sketchColors } from "./sketch.js";
|
|
4
|
+
import { PEN_HEX } from "../pen/colors.js";
|
|
5
|
+
import type { PenColor } from "../pen/types.js";
|
|
6
|
+
import type { Found } from "./break-apart-find.js";
|
|
7
|
+
|
|
8
|
+
/** A note's type and spacing in px (sketch-note.svelte), for estimating how tall a card sets. */
|
|
9
|
+
export const NOTE_TITLE_PX = 19;
|
|
10
|
+
export const NOTE_BODY_PX = 15;
|
|
11
|
+
const NOTE_TITLE_LINE = 26;
|
|
12
|
+
const NOTE_BODY_LINE = 21.75;
|
|
13
|
+
const NOTE_PAD_X = 40;
|
|
14
|
+
const NOTE_PAD_Y = 32;
|
|
15
|
+
const NOTE_GAP = 10;
|
|
16
|
+
/** Average glyph width as a share of the type size. */
|
|
17
|
+
const NOTE_GLYPH = 0.55;
|
|
18
|
+
const NOTE_GLYPH_CAPS = 0.7;
|
|
19
|
+
/** Aim a little under the drawn height, since the estimate wraps by character count. */
|
|
20
|
+
const FIT_MARGIN = 0.92;
|
|
21
|
+
|
|
22
|
+
function channels(hex: string | undefined): [number, number, number] | null {
|
|
23
|
+
const m = hex?.trim().match(/^#([\da-f]{3}|[\da-f]{6})$/i);
|
|
24
|
+
if (!m) return null;
|
|
25
|
+
const full = m[1].length === 3 ? [...m[1]].map((c) => c + c).join("") : m[1];
|
|
26
|
+
return [0, 2, 4].map((i) => Number.parseInt(full.slice(i, i + 2), 16)) as [
|
|
27
|
+
number,
|
|
28
|
+
number,
|
|
29
|
+
number,
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The sketch accent nearest the rect's stroke. A grey or an unknown colour reads as ink, the default. */
|
|
34
|
+
export function accentOf(stroke: string | undefined): string | undefined {
|
|
35
|
+
const rgb = channels(stroke);
|
|
36
|
+
if (!rgb) return undefined;
|
|
37
|
+
const spread = Math.max(...rgb) - Math.min(...rgb);
|
|
38
|
+
if (spread < 40) return undefined;
|
|
39
|
+
let best: { name: string; distance: number } | null = null;
|
|
40
|
+
for (const name of SKETCH_ACCENTS) {
|
|
41
|
+
const target = channels(sketchColors(name).stroke);
|
|
42
|
+
if (!target) continue;
|
|
43
|
+
const distance = Math.hypot(...rgb.map((v, i) => v - target[i]));
|
|
44
|
+
if (!best || distance < best.distance) best = { name, distance };
|
|
45
|
+
}
|
|
46
|
+
return best && best.name !== "ink" && best.distance < 90 ? best.name : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Card text is markdown: a stray `*` or a leading `-` would turn into formatting. */
|
|
50
|
+
export function escapeMarkdown(line: string): string {
|
|
51
|
+
return line.replace(/([\\`*_~[\]])/g, "\\$1").replace(/^([#>+-]|\d+\.)/, "\\$1");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function hueOf([r, g, b]: [number, number, number]): number {
|
|
55
|
+
const max = Math.max(r, g, b);
|
|
56
|
+
const d = max - Math.min(r, g, b);
|
|
57
|
+
if (d === 0) return 0;
|
|
58
|
+
const h = max === r ? ((g - b) / d) % 6 : max === g ? (b - r) / d + 2 : (r - g) / d + 4;
|
|
59
|
+
return (h * 60 + 360) % 360;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The pen colour whose hue is nearest, or undefined for a grey, a white or a black. */
|
|
63
|
+
export function penColorOf(color: string | undefined): PenColor | undefined {
|
|
64
|
+
const rgb = channels(color);
|
|
65
|
+
if (!rgb || Math.max(...rgb) - Math.min(...rgb) < 40) return undefined;
|
|
66
|
+
const hue = hueOf(rgb);
|
|
67
|
+
let best: { name: PenColor; gap: number } | null = null;
|
|
68
|
+
for (const name of Object.keys(PEN_HEX) as PenColor[]) {
|
|
69
|
+
const target = channels(PEN_HEX[name].light);
|
|
70
|
+
if (!target || name === "ink") continue;
|
|
71
|
+
const diff = Math.abs(hue - hueOf(target));
|
|
72
|
+
const gap = Math.min(diff, 360 - diff);
|
|
73
|
+
if (!best || gap < best.gap) best = { name, gap };
|
|
74
|
+
}
|
|
75
|
+
return best?.name;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A solid colour, not white or paper. */
|
|
79
|
+
export function inked(color: string | undefined): boolean {
|
|
80
|
+
const rgb = channels(color);
|
|
81
|
+
return Boolean(rgb) && Math.min(...(rgb as number[])) < 240;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const round1 = (n: number) => Math.round(n * 10) / 10;
|
|
85
|
+
|
|
86
|
+
/** The share a note shows at: its title at the drawn size, or smaller until the copy fits the drawn box. */
|
|
87
|
+
export function fitScale(card: Found, matched: number, boxW: number, boxH: number): number {
|
|
88
|
+
// Capitals run wide, and a word wider than the card breaks mid-word, so that counts as too tall.
|
|
89
|
+
const glyph = (text: string) => (text === text.toUpperCase() ? NOTE_GLYPH_CAPS : NOTE_GLYPH);
|
|
90
|
+
const lines = (text: string, px: number, inner: number) => {
|
|
91
|
+
const widest = Math.max(...text.split(/\s+/).map((w) => w.length)) * px * glyph(text);
|
|
92
|
+
if (widest > inner) return 99;
|
|
93
|
+
return Math.max(1, Math.ceil((text.length * px * glyph(text)) / inner));
|
|
94
|
+
};
|
|
95
|
+
const heightAt = (z: number) => {
|
|
96
|
+
const inner = Math.max(boxW / z - NOTE_PAD_X, 40);
|
|
97
|
+
const title = lines([card.kicker, card.title].filter(Boolean).join(" "), NOTE_TITLE_PX, inner);
|
|
98
|
+
const body = card.body.reduce((n, line) => n + lines(line, NOTE_BODY_PX, inner), 0);
|
|
99
|
+
return (
|
|
100
|
+
(NOTE_PAD_Y + title * NOTE_TITLE_LINE + (body ? NOTE_GAP + body * NOTE_BODY_LINE : 0)) * z
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
let z = matched;
|
|
104
|
+
for (let i = 0; i < 16 && z > 0.2 && heightAt(z) > boxH * FIT_MARGIN; i += 1) z *= 0.92;
|
|
105
|
+
return Math.round(z * 1000) / 1000;
|
|
106
|
+
}
|