@thomasfosterau/components 0.0.0-stage → 0.1.0
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 +817 -2
- package/dist/canvas/VectorCanvas.svelte +239 -0
- package/dist/canvas/VectorCanvas.svelte.d.ts +87 -0
- package/dist/canvas/history.d.ts +105 -0
- package/dist/canvas/history.js +147 -0
- package/dist/canvas/index.d.ts +15 -0
- package/dist/canvas/index.js +14 -0
- package/dist/canvas/types.d.ts +91 -0
- package/dist/canvas/types.js +13 -0
- package/dist/canvas/utils.d.ts +66 -0
- package/dist/canvas/utils.js +114 -0
- package/dist/charts/Chart.svelte +300 -0
- package/dist/charts/Chart.svelte.d.ts +50 -0
- package/dist/charts/index.d.ts +11 -0
- package/dist/charts/index.js +10 -0
- package/dist/charts/types.d.ts +28 -0
- package/dist/charts/types.js +6 -0
- package/dist/citations/BookCitation.svelte +53 -0
- package/dist/citations/BookCitation.svelte.d.ts +41 -0
- package/dist/citations/ChapterCitation.svelte +38 -0
- package/dist/citations/ChapterCitation.svelte.d.ts +50 -0
- package/dist/citations/Citation.svelte +47 -0
- package/dist/citations/Citation.svelte.d.ts +24 -0
- package/dist/citations/CreditList.svelte +69 -0
- package/dist/citations/CreditList.svelte.d.ts +47 -0
- package/dist/citations/DOILink.svelte +28 -0
- package/dist/citations/DOILink.svelte.d.ts +19 -0
- package/dist/citations/Footnote.svelte +39 -0
- package/dist/citations/Footnote.svelte.d.ts +17 -0
- package/dist/citations/FootnoteList.svelte +36 -0
- package/dist/citations/FootnoteList.svelte.d.ts +21 -0
- package/dist/citations/ISBNLink.svelte +29 -0
- package/dist/citations/ISBNLink.svelte.d.ts +19 -0
- package/dist/citations/ISSNLink.svelte +29 -0
- package/dist/citations/ISSNLink.svelte.d.ts +19 -0
- package/dist/citations/Publisher.svelte +34 -0
- package/dist/citations/Publisher.svelte.d.ts +14 -0
- package/dist/citations/ReferenceList.svelte +53 -0
- package/dist/citations/ReferenceList.svelte.d.ts +21 -0
- package/dist/citations/ReportCitation.svelte +41 -0
- package/dist/citations/ReportCitation.svelte.d.ts +36 -0
- package/dist/citations/ScholarlyArticleCitation.svelte +64 -0
- package/dist/citations/ScholarlyArticleCitation.svelte.d.ts +41 -0
- package/dist/citations/ThesisCitation.svelte +35 -0
- package/dist/citations/ThesisCitation.svelte.d.ts +29 -0
- package/dist/citations/bibliography.svelte.d.ts +55 -0
- package/dist/citations/bibliography.svelte.js +78 -0
- package/dist/citations/citation.d.ts +71 -0
- package/dist/citations/citation.js +121 -0
- package/dist/citations/csl.d.ts +102 -0
- package/dist/citations/csl.js +20 -0
- package/dist/citations/index.d.ts +31 -0
- package/dist/citations/index.js +36 -0
- package/dist/citations/list-format.d.ts +34 -0
- package/dist/citations/list-format.js +38 -0
- package/dist/context.svelte.d.ts +46 -0
- package/dist/context.svelte.js +51 -0
- package/dist/data/DataGrid.svelte +150 -0
- package/dist/data/DataGrid.svelte.d.ts +118 -0
- package/dist/data/DescriptionList.svelte +47 -0
- package/dist/data/DescriptionList.svelte.d.ts +26 -0
- package/dist/data/DescriptionRow.svelte +55 -0
- package/dist/data/DescriptionRow.svelte.d.ts +21 -0
- package/dist/data/DiffText.svelte +29 -0
- package/dist/data/DiffText.svelte.d.ts +19 -0
- package/dist/data/ItemList.svelte +80 -0
- package/dist/data/ItemList.svelte.d.ts +52 -0
- package/dist/data/LinkListItem.svelte +46 -0
- package/dist/data/LinkListItem.svelte.d.ts +21 -0
- package/dist/data/Pagination.svelte +136 -0
- package/dist/data/Pagination.svelte.d.ts +71 -0
- package/dist/data/PaginationFooter.svelte +41 -0
- package/dist/data/PaginationFooter.svelte.d.ts +59 -0
- package/dist/data/ResultCount.svelte +27 -0
- package/dist/data/ResultCount.svelte.d.ts +18 -0
- package/dist/data/Table.svelte +145 -0
- package/dist/data/Table.svelte.d.ts +75 -0
- package/dist/data/TreeList.svelte +41 -0
- package/dist/data/TreeList.svelte.d.ts +17 -0
- package/dist/data/TreeListItem.svelte +29 -0
- package/dist/data/TreeListItem.svelte.d.ts +10 -0
- package/dist/data/description-context.d.ts +33 -0
- package/dist/data/description-context.js +35 -0
- package/dist/data/diff.d.ts +14 -0
- package/dist/data/diff.js +10 -0
- package/dist/data/index.d.ts +35 -0
- package/dist/data/index.js +35 -0
- package/dist/data/list-item.d.ts +30 -0
- package/dist/data/list-item.js +19 -0
- package/dist/data/pagination.d.ts +81 -0
- package/dist/data/pagination.js +79 -0
- package/dist/data/table-types.d.ts +64 -0
- package/dist/data/table-types.js +35 -0
- package/dist/feedback/ConfirmDialog.svelte +79 -0
- package/dist/feedback/ConfirmDialog.svelte.d.ts +82 -0
- package/dist/feedback/Dialog.svelte +77 -0
- package/dist/feedback/Dialog.svelte.d.ts +37 -0
- package/dist/feedback/LoadingBoundary.svelte +61 -0
- package/dist/feedback/LoadingBoundary.svelte.d.ts +34 -0
- package/dist/feedback/ProgressBar.svelte +46 -0
- package/dist/feedback/ProgressBar.svelte.d.ts +28 -0
- package/dist/feedback/SegmentBar.svelte +83 -0
- package/dist/feedback/SegmentBar.svelte.d.ts +36 -0
- package/dist/feedback/StatTile.svelte +77 -0
- package/dist/feedback/StatTile.svelte.d.ts +49 -0
- package/dist/feedback/Stats.svelte +18 -0
- package/dist/feedback/Stats.svelte.d.ts +16 -0
- package/dist/feedback/Timeline.svelte +86 -0
- package/dist/feedback/Timeline.svelte.d.ts +52 -0
- package/dist/feedback/Toaster.svelte +78 -0
- package/dist/feedback/Toaster.svelte.d.ts +32 -0
- package/dist/feedback/index.d.ts +31 -0
- package/dist/feedback/index.js +27 -0
- package/dist/feedback/sizes.d.ts +27 -0
- package/dist/feedback/sizes.js +41 -0
- package/dist/feedback/toaster.d.ts +48 -0
- package/dist/feedback/toaster.js +29 -0
- package/dist/fields/Checkbox.svelte +33 -0
- package/dist/fields/Checkbox.svelte.d.ts +24 -0
- package/dist/fields/CheckboxGroup.svelte +41 -0
- package/dist/fields/CheckboxGroup.svelte.d.ts +40 -0
- package/dist/fields/Combobox.svelte +222 -0
- package/dist/fields/Combobox.svelte.d.ts +98 -0
- package/dist/fields/DateInput.svelte +31 -0
- package/dist/fields/DateInput.svelte.d.ts +29 -0
- package/dist/fields/FieldErrors.svelte +30 -0
- package/dist/fields/FieldErrors.svelte.d.ts +11 -0
- package/dist/fields/Fieldset.svelte +48 -0
- package/dist/fields/Fieldset.svelte.d.ts +26 -0
- package/dist/fields/FileInput.svelte +67 -0
- package/dist/fields/FileInput.svelte.d.ts +33 -0
- package/dist/fields/FormField.svelte +54 -0
- package/dist/fields/FormField.svelte.d.ts +29 -0
- package/dist/fields/ImageInput.svelte +87 -0
- package/dist/fields/ImageInput.svelte.d.ts +39 -0
- package/dist/fields/PushToggle.svelte +93 -0
- package/dist/fields/PushToggle.svelte.d.ts +55 -0
- package/dist/fields/RadioGroup.svelte +85 -0
- package/dist/fields/RadioGroup.svelte.d.ts +41 -0
- package/dist/fields/SearchInput.svelte +57 -0
- package/dist/fields/SearchInput.svelte.d.ts +33 -0
- package/dist/fields/Select.svelte +36 -0
- package/dist/fields/Select.svelte.d.ts +29 -0
- package/dist/fields/TextInput.svelte +26 -0
- package/dist/fields/TextInput.svelte.d.ts +30 -0
- package/dist/fields/Textarea.svelte +22 -0
- package/dist/fields/Textarea.svelte.d.ts +25 -0
- package/dist/fields/Toggle.svelte +72 -0
- package/dist/fields/Toggle.svelte.d.ts +32 -0
- package/dist/fields/VideoInput.svelte +35 -0
- package/dist/fields/VideoInput.svelte.d.ts +40 -0
- package/dist/fields/field-types.d.ts +51 -0
- package/dist/fields/field-types.js +60 -0
- package/dist/fields/index.d.ts +34 -0
- package/dist/fields/index.js +34 -0
- package/dist/formatting/DateTime.svelte +35 -0
- package/dist/formatting/DateTime.svelte.d.ts +26 -0
- package/dist/formatting/FormatList.svelte +31 -0
- package/dist/formatting/FormatList.svelte.d.ts +46 -0
- package/dist/formatting/Highlight.svelte +47 -0
- package/dist/formatting/Highlight.svelte.d.ts +15 -0
- package/dist/formatting/Number.svelte +35 -0
- package/dist/formatting/Number.svelte.d.ts +43 -0
- package/dist/formatting/RelativeTime.svelte +69 -0
- package/dist/formatting/RelativeTime.svelte.d.ts +26 -0
- package/dist/formatting/Year.svelte +39 -0
- package/dist/formatting/Year.svelte.d.ts +21 -0
- package/dist/formatting/index.d.ts +12 -0
- package/dist/formatting/index.js +14 -0
- package/dist/forms/EntityForm.svelte +177 -0
- package/dist/forms/EntityForm.svelte.d.ts +57 -0
- package/dist/forms/FormActions.svelte +29 -0
- package/dist/forms/FormActions.svelte.d.ts +37 -0
- package/dist/forms/FormErrors.svelte +21 -0
- package/dist/forms/FormErrors.svelte.d.ts +16 -0
- package/dist/forms/HiddenField.svelte +22 -0
- package/dist/forms/HiddenField.svelte.d.ts +11 -0
- package/dist/forms/SearchForm.svelte +43 -0
- package/dist/forms/SearchForm.svelte.d.ts +36 -0
- package/dist/forms/entity-form.d.ts +12 -0
- package/dist/forms/entity-form.js +12 -0
- package/dist/forms/index.d.ts +14 -0
- package/dist/forms/index.js +14 -0
- package/dist/hotkeys/HotkeyBadge.svelte +39 -0
- package/dist/hotkeys/HotkeyBadge.svelte.d.ts +25 -0
- package/dist/hotkeys/HotkeyHelpDialog.svelte +68 -0
- package/dist/hotkeys/HotkeyHelpDialog.svelte.d.ts +35 -0
- package/dist/hotkeys/HotkeyProvider.svelte +92 -0
- package/dist/hotkeys/HotkeyProvider.svelte.d.ts +4 -0
- package/dist/hotkeys/context.svelte.d.ts +47 -0
- package/dist/hotkeys/context.svelte.js +72 -0
- package/dist/hotkeys/index.d.ts +23 -0
- package/dist/hotkeys/index.js +22 -0
- package/dist/hotkeys/match.d.ts +65 -0
- package/dist/hotkeys/match.js +133 -0
- package/dist/hotkeys/registry.svelte.d.ts +55 -0
- package/dist/hotkeys/registry.svelte.js +110 -0
- package/dist/hotkeys/types.d.ts +72 -0
- package/dist/hotkeys/types.js +18 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +36 -0
- package/dist/infobox/Infobox.svelte +79 -0
- package/dist/infobox/Infobox.svelte.d.ts +26 -0
- package/dist/infobox/InfoboxHeader.svelte +27 -0
- package/dist/infobox/InfoboxHeader.svelte.d.ts +11 -0
- package/dist/infobox/InfoboxImage.svelte +38 -0
- package/dist/infobox/InfoboxImage.svelte.d.ts +21 -0
- package/dist/infobox/InfoboxRow.svelte +50 -0
- package/dist/infobox/InfoboxRow.svelte.d.ts +25 -0
- package/dist/infobox/InfoboxSection.svelte +24 -0
- package/dist/infobox/InfoboxSection.svelte.d.ts +12 -0
- package/dist/infobox/index.d.ts +21 -0
- package/dist/infobox/index.js +20 -0
- package/dist/infobox/types.d.ts +86 -0
- package/dist/infobox/types.js +27 -0
- package/dist/layout/Breadcrumbs.svelte +52 -0
- package/dist/layout/Breadcrumbs.svelte.d.ts +18 -0
- package/dist/layout/DetailLayout.svelte +50 -0
- package/dist/layout/DetailLayout.svelte.d.ts +19 -0
- package/dist/layout/FilterBar.svelte +40 -0
- package/dist/layout/FilterBar.svelte.d.ts +12 -0
- package/dist/layout/Footer.svelte +32 -0
- package/dist/layout/Footer.svelte.d.ts +12 -0
- package/dist/layout/Main.svelte +30 -0
- package/dist/layout/Main.svelte.d.ts +15 -0
- package/dist/layout/Navbar.svelte +68 -0
- package/dist/layout/Navbar.svelte.d.ts +31 -0
- package/dist/layout/PageHeader.svelte +77 -0
- package/dist/layout/PageHeader.svelte.d.ts +33 -0
- package/dist/layout/Section.svelte +72 -0
- package/dist/layout/Section.svelte.d.ts +40 -0
- package/dist/layout/Sidebar.svelte +54 -0
- package/dist/layout/Sidebar.svelte.d.ts +27 -0
- package/dist/layout/Tabs.svelte +85 -0
- package/dist/layout/Tabs.svelte.d.ts +42 -0
- package/dist/layout/index.d.ts +29 -0
- package/dist/layout/index.js +29 -0
- package/dist/layout/types.d.ts +52 -0
- package/dist/layout/types.js +27 -0
- package/dist/links/ContextLinks.svelte +28 -0
- package/dist/links/ContextLinks.svelte.d.ts +20 -0
- package/dist/links/EmailLink.svelte +26 -0
- package/dist/links/EmailLink.svelte.d.ts +20 -0
- package/dist/links/index.d.ts +7 -0
- package/dist/links/index.js +7 -0
- package/dist/media/Figure.svelte +26 -0
- package/dist/media/Figure.svelte.d.ts +12 -0
- package/dist/media/Image.svelte +76 -0
- package/dist/media/Image.svelte.d.ts +41 -0
- package/dist/media/Video.svelte +65 -0
- package/dist/media/Video.svelte.d.ts +29 -0
- package/dist/media/image-dimensions.d.ts +28 -0
- package/dist/media/image-dimensions.js +34 -0
- package/dist/media/index.d.ts +10 -0
- package/dist/media/index.js +10 -0
- package/dist/search/CommandPalette.svelte +257 -0
- package/dist/search/CommandPalette.svelte.d.ts +59 -0
- package/dist/search/index.d.ts +13 -0
- package/dist/search/index.js +13 -0
- package/dist/search/types.d.ts +37 -0
- package/dist/search/types.js +20 -0
- package/dist/seo/Head.svelte +190 -0
- package/dist/seo/Head.svelte.d.ts +86 -0
- package/dist/seo/JsonLd.svelte +30 -0
- package/dist/seo/JsonLd.svelte.d.ts +20 -0
- package/dist/seo/index.d.ts +18 -0
- package/dist/seo/index.js +17 -0
- package/dist/seo/json-ld.d.ts +33 -0
- package/dist/seo/json-ld.js +29 -0
- package/dist/seo/og-image.d.ts +71 -0
- package/dist/seo/og-image.js +81 -0
- package/dist/seo/types.d.ts +43 -0
- package/dist/seo/types.js +6 -0
- package/dist/theme.css +161 -0
- package/dist/tips/PopoverTip.svelte +80 -0
- package/dist/tips/PopoverTip.svelte.d.ts +33 -0
- package/dist/tips/Tip.svelte +91 -0
- package/dist/tips/Tip.svelte.d.ts +25 -0
- package/dist/tips/index.d.ts +15 -0
- package/dist/tips/index.js +14 -0
- package/dist/tips/tip-stack.d.ts +25 -0
- package/dist/tips/tip-stack.js +37 -0
- package/dist/tips/tip-store.svelte.d.ts +74 -0
- package/dist/tips/tip-store.svelte.js +158 -0
- package/dist/tips/types.d.ts +46 -0
- package/dist/tips/types.js +6 -0
- package/dist/ui/Alert.svelte +61 -0
- package/dist/ui/Alert.svelte.d.ts +37 -0
- package/dist/ui/Badge.svelte +39 -0
- package/dist/ui/Badge.svelte.d.ts +32 -0
- package/dist/ui/Button.svelte +43 -0
- package/dist/ui/Button.svelte.d.ts +29 -0
- package/dist/ui/Card.svelte +49 -0
- package/dist/ui/Card.svelte.d.ts +44 -0
- package/dist/ui/Chip.svelte +58 -0
- package/dist/ui/Chip.svelte.d.ts +21 -0
- package/dist/ui/ConfirmButton.svelte +75 -0
- package/dist/ui/ConfirmButton.svelte.d.ts +48 -0
- package/dist/ui/Disclosure.svelte +39 -0
- package/dist/ui/Disclosure.svelte.d.ts +22 -0
- package/dist/ui/EmptyState.svelte +37 -0
- package/dist/ui/EmptyState.svelte.d.ts +22 -0
- package/dist/ui/ExternalLink.svelte +38 -0
- package/dist/ui/ExternalLink.svelte.d.ts +34 -0
- package/dist/ui/Heading.svelte +58 -0
- package/dist/ui/Heading.svelte.d.ts +22 -0
- package/dist/ui/Link.svelte +34 -0
- package/dist/ui/Link.svelte.d.ts +20 -0
- package/dist/ui/LinkButton.svelte +51 -0
- package/dist/ui/LinkButton.svelte.d.ts +33 -0
- package/dist/ui/Menu.svelte +54 -0
- package/dist/ui/Menu.svelte.d.ts +34 -0
- package/dist/ui/MenuItem.svelte +75 -0
- package/dist/ui/MenuItem.svelte.d.ts +21 -0
- package/dist/ui/MenuSeparator.svelte +26 -0
- package/dist/ui/MenuSeparator.svelte.d.ts +8 -0
- package/dist/ui/Popover.svelte +63 -0
- package/dist/ui/Popover.svelte.d.ts +40 -0
- package/dist/ui/ShareButton.svelte +64 -0
- package/dist/ui/ShareButton.svelte.d.ts +39 -0
- package/dist/ui/Skeleton.svelte +43 -0
- package/dist/ui/Skeleton.svelte.d.ts +22 -0
- package/dist/ui/ThemeToggle.svelte +65 -0
- package/dist/ui/ThemeToggle.svelte.d.ts +24 -0
- package/dist/ui/Tooltip.svelte +54 -0
- package/dist/ui/Tooltip.svelte.d.ts +35 -0
- package/dist/ui/VisuallyHidden.svelte +23 -0
- package/dist/ui/VisuallyHidden.svelte.d.ts +10 -0
- package/dist/ui/colour-token.d.ts +41 -0
- package/dist/ui/colour-token.js +49 -0
- package/dist/ui/index.d.ts +44 -0
- package/dist/ui/index.js +44 -0
- package/dist/ui/menu-context.d.ts +22 -0
- package/dist/ui/menu-context.js +26 -0
- package/dist/ui/slug.d.ts +19 -0
- package/dist/ui/slug.js +25 -0
- package/dist/ui/tones.d.ts +68 -0
- package/dist/ui/tones.js +150 -0
- package/package.json +101 -3
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
A pan-and-zoom SVG surface over a `VectorDocument`, with each element drawn by a renderer snippet the application supplies.
|
|
3
|
+
-->
|
|
4
|
+
<!--
|
|
5
|
+
VectorCanvas — a pan-and-zoom SVG surface over a {@link VectorDocument}, with
|
|
6
|
+
the elements drawn by a renderer the application supplies.
|
|
7
|
+
|
|
8
|
+
It owns the viewport and the pointer plumbing; it owns nothing about what the
|
|
9
|
+
elements MEAN. Selection, creation and dragging are the application's, reached
|
|
10
|
+
through `oninteract` — which is why a floor-plan editor and a planting plan
|
|
11
|
+
can share this without either one's vocabulary leaking into it.
|
|
12
|
+
|
|
13
|
+
## ARIA contract
|
|
14
|
+
|
|
15
|
+
The `<svg>` is `role="application"` named by `label` (default "Vector editing
|
|
16
|
+
canvas"). `application` is the honest role for a surface whose whole input
|
|
17
|
+
vocabulary is pointer gestures the page interprets — it tells a screen reader
|
|
18
|
+
to pass keys through rather than to read the SVG as a picture. It is also a
|
|
19
|
+
promise this component alone cannot keep: an application that makes a canvas
|
|
20
|
+
its only way to do something owes the reader a second, non-pointer path to the
|
|
21
|
+
same edit, and that path is the page's, not this component's.
|
|
22
|
+
|
|
23
|
+
## Two decisions worth stating
|
|
24
|
+
|
|
25
|
+
- The document prop is `doc`, not `document`: a prop called `document` shadows
|
|
26
|
+
the global inside a component that also reaches for the DOM, which is a bug
|
|
27
|
+
waiting for the next line of code.
|
|
28
|
+
- The viewport is REPORTED through `onviewportchange` rather than mutated
|
|
29
|
+
through `$bindable`, so the document stays the caller's to own and an undo
|
|
30
|
+
stack has something to undo.
|
|
31
|
+
|
|
32
|
+
Pointer events rather than mouse events: one code path covers mouse, touch and
|
|
33
|
+
pen, and `setPointerCapture` keeps a drag alive past the edge of the canvas.
|
|
34
|
+
|
|
35
|
+
Hand-rolled rather than built on a bits-ui primitive because there is no
|
|
36
|
+
primitive for this: bits-ui owns focus, keyboard and ARIA relationships
|
|
37
|
+
between named widgets, and a pan/zoom surface is none of those — it is raw
|
|
38
|
+
pointer geometry.
|
|
39
|
+
-->
|
|
40
|
+
<script lang="ts" generics="T extends CanvasElement">import { panBy, screenToCanvas, snapToGrid, sortByZIndex, zoomAbout } from "./utils.js";
|
|
41
|
+
let { doc, selected = new Set(), tool = "select", width = 800, height = 600, element, interactive = true, label = "Vector editing canvas", onviewportchange, oninteract, class: className, ...rest } = $props();
|
|
42
|
+
// Scoped to this instance: two canvases on one page would otherwise share a
|
|
43
|
+
// `<pattern id>`, and the second would paint the first's grid.
|
|
44
|
+
const uid = $props.id();
|
|
45
|
+
const gridPatternId = `vector-canvas-grid-${uid}`;
|
|
46
|
+
let surface = $state();
|
|
47
|
+
/** -1 is "no pointer down"; the id is what `setPointerCapture` released. */
|
|
48
|
+
let dragging = $state(-1);
|
|
49
|
+
let panning = $state(false);
|
|
50
|
+
/**
|
|
51
|
+
* The pointer's last SCREEN position, so a pan is the difference between two
|
|
52
|
+
* positions we recorded. `MouseEvent.movementX` would say the same thing in
|
|
53
|
+
* one property, but its value is scaled by the device pixel ratio in some
|
|
54
|
+
* browsers and zeroed after a coalesced move in others — a pan that drifts
|
|
55
|
+
* on one machine and not another is not worth the line it saves.
|
|
56
|
+
*/
|
|
57
|
+
let lastScreen = $state({
|
|
58
|
+
x: 0,
|
|
59
|
+
y: 0
|
|
60
|
+
});
|
|
61
|
+
/**
|
|
62
|
+
* Chromium's own double-click interval and its slop, in milliseconds and CSS
|
|
63
|
+
* pixels.
|
|
64
|
+
*/
|
|
65
|
+
const MULTI_PRESS_MS = 500;
|
|
66
|
+
const MULTI_PRESS_SLOP = 4;
|
|
67
|
+
/** The run of rapid presses at one spot: 1, then 2, then 3… */
|
|
68
|
+
let pressCount = 0;
|
|
69
|
+
let lastPress = {
|
|
70
|
+
time: 0,
|
|
71
|
+
x: 0,
|
|
72
|
+
y: 0
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* The press count, counted HERE rather than read off `event.detail`.
|
|
76
|
+
*
|
|
77
|
+
* `MouseEvent.detail` carries the click count; `PointerEvent.detail` is 0,
|
|
78
|
+
* always, in every browser — and a component that listens on pointer events
|
|
79
|
+
* to cover mouse, touch and pen alike therefore never sees it. That is easy
|
|
80
|
+
* to miss, because a synthetic `pointerdown` dispatched by a test carries
|
|
81
|
+
* whatever `detail` the test puts on it: the check reads as working and does
|
|
82
|
+
* nothing at all in a browser. Counting it here also makes a double-TAP
|
|
83
|
+
* count, which `detail` would not have done on any input.
|
|
84
|
+
*/
|
|
85
|
+
const countPress = (event) => {
|
|
86
|
+
const rapid = event.timeStamp - lastPress.time < MULTI_PRESS_MS && Math.abs(event.clientX - lastPress.x) <= MULTI_PRESS_SLOP && Math.abs(event.clientY - lastPress.y) <= MULTI_PRESS_SLOP;
|
|
87
|
+
pressCount = rapid ? pressCount + 1 : 1;
|
|
88
|
+
lastPress = {
|
|
89
|
+
time: event.timeStamp,
|
|
90
|
+
x: event.clientX,
|
|
91
|
+
y: event.clientY
|
|
92
|
+
};
|
|
93
|
+
return pressCount;
|
|
94
|
+
};
|
|
95
|
+
const ordered = $derived(sortByZIndex(doc.elements));
|
|
96
|
+
const at = (event) => {
|
|
97
|
+
if (surface === undefined) return {
|
|
98
|
+
x: 0,
|
|
99
|
+
y: 0
|
|
100
|
+
};
|
|
101
|
+
const box = surface.getBoundingClientRect();
|
|
102
|
+
const point = screenToCanvas({
|
|
103
|
+
x: event.clientX - box.left,
|
|
104
|
+
y: event.clientY - box.top
|
|
105
|
+
}, doc.viewport);
|
|
106
|
+
return doc.grid.snap ? snapToGrid(point, doc.grid.size) : point;
|
|
107
|
+
};
|
|
108
|
+
const down = (event) => {
|
|
109
|
+
if (!interactive) return;
|
|
110
|
+
// Capture on the surface so a drag that leaves the element keeps reporting.
|
|
111
|
+
// Guarded rather than called bare: a test DOM (and an old browser) can
|
|
112
|
+
// have the element without the method, and a TypeError here would take
|
|
113
|
+
// the whole gesture with it.
|
|
114
|
+
if (typeof surface?.setPointerCapture === "function") surface.setPointerCapture(event.pointerId);
|
|
115
|
+
dragging = event.pointerId;
|
|
116
|
+
panning = tool === "pan";
|
|
117
|
+
lastScreen = {
|
|
118
|
+
x: event.clientX,
|
|
119
|
+
y: event.clientY
|
|
120
|
+
};
|
|
121
|
+
const detail = countPress(event);
|
|
122
|
+
if (!panning) oninteract?.({
|
|
123
|
+
phase: "down",
|
|
124
|
+
tool,
|
|
125
|
+
point: at(event),
|
|
126
|
+
detail,
|
|
127
|
+
native: event
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
const move = (event) => {
|
|
131
|
+
if (!interactive || dragging !== event.pointerId) return;
|
|
132
|
+
if (panning) {
|
|
133
|
+
onviewportchange?.(panBy(doc.viewport, event.clientX - lastScreen.x, event.clientY - lastScreen.y));
|
|
134
|
+
lastScreen = {
|
|
135
|
+
x: event.clientX,
|
|
136
|
+
y: event.clientY
|
|
137
|
+
};
|
|
138
|
+
} else oninteract?.({
|
|
139
|
+
phase: "move",
|
|
140
|
+
tool,
|
|
141
|
+
point: at(event),
|
|
142
|
+
detail: 1,
|
|
143
|
+
native: event
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
const up = (event) => {
|
|
147
|
+
if (!interactive || dragging !== event.pointerId) return;
|
|
148
|
+
if (typeof surface?.releasePointerCapture === "function") {
|
|
149
|
+
surface.releasePointerCapture(event.pointerId);
|
|
150
|
+
}
|
|
151
|
+
if (!panning) oninteract?.({
|
|
152
|
+
phase: "up",
|
|
153
|
+
tool,
|
|
154
|
+
point: at(event),
|
|
155
|
+
detail: 1,
|
|
156
|
+
native: event
|
|
157
|
+
});
|
|
158
|
+
dragging = -1;
|
|
159
|
+
panning = false;
|
|
160
|
+
};
|
|
161
|
+
const wheel = (event) => {
|
|
162
|
+
if (!interactive || surface === undefined) return;
|
|
163
|
+
event.preventDefault();
|
|
164
|
+
const box = surface.getBoundingClientRect();
|
|
165
|
+
const pointer = {
|
|
166
|
+
x: event.clientX - box.left,
|
|
167
|
+
y: event.clientY - box.top
|
|
168
|
+
};
|
|
169
|
+
onviewportchange?.(zoomAbout(doc.viewport, pointer, event.deltaY > 0 ? .9 : 1.1));
|
|
170
|
+
};
|
|
171
|
+
</script>
|
|
172
|
+
|
|
173
|
+
<div
|
|
174
|
+
class={['bg-base-100 relative overflow-hidden select-none', className]}
|
|
175
|
+
style:width="{width}px"
|
|
176
|
+
style:height="{height}px"
|
|
177
|
+
style:touch-action="none"
|
|
178
|
+
>
|
|
179
|
+
<svg
|
|
180
|
+
{...rest}
|
|
181
|
+
bind:this={surface}
|
|
182
|
+
{width}
|
|
183
|
+
{height}
|
|
184
|
+
role="application"
|
|
185
|
+
aria-label={label}
|
|
186
|
+
class="cursor-crosshair"
|
|
187
|
+
onpointerdown={down}
|
|
188
|
+
onpointermove={move}
|
|
189
|
+
onpointerup={up}
|
|
190
|
+
onpointercancel={up}
|
|
191
|
+
onwheel={wheel}
|
|
192
|
+
>
|
|
193
|
+
<defs>
|
|
194
|
+
<pattern
|
|
195
|
+
id={gridPatternId}
|
|
196
|
+
width={doc.grid.size}
|
|
197
|
+
height={doc.grid.size}
|
|
198
|
+
patternUnits="userSpaceOnUse"
|
|
199
|
+
>
|
|
200
|
+
<path
|
|
201
|
+
d="M {doc.grid.size} 0 L 0 0 0 {doc.grid.size}"
|
|
202
|
+
fill="none"
|
|
203
|
+
stroke={doc.grid.color}
|
|
204
|
+
stroke-width="0.5"
|
|
205
|
+
/>
|
|
206
|
+
</pattern>
|
|
207
|
+
</defs>
|
|
208
|
+
|
|
209
|
+
<g
|
|
210
|
+
transform="translate({doc.viewport.offsetX},{doc.viewport.offsetY}) scale({doc.viewport.zoom})"
|
|
211
|
+
>
|
|
212
|
+
{#if doc.grid.visible}
|
|
213
|
+
<!-- Sized to swamp any reachable pan rather than tracked to the
|
|
214
|
+
viewport: a rect that has to be recomputed on every pan is a
|
|
215
|
+
reactive dependency for a decoration. -->
|
|
216
|
+
<rect x={-10000} y={-10000} width={20000} height={20000} fill="url(#{gridPatternId})" />
|
|
217
|
+
{/if}
|
|
218
|
+
|
|
219
|
+
{#each ordered as node (node.id)}
|
|
220
|
+
<g opacity={node.visible === false ? 0 : node.opacity}>
|
|
221
|
+
{#if element}
|
|
222
|
+
{@render element(node, selected.has(node.id))}
|
|
223
|
+
{:else}
|
|
224
|
+
<!-- The fallback is a marker, not a drawing: an application that
|
|
225
|
+
has not supplied a renderer should SEE that it has not. -->
|
|
226
|
+
<circle
|
|
227
|
+
cx={node.position.x}
|
|
228
|
+
cy={node.position.y}
|
|
229
|
+
r="5"
|
|
230
|
+
fill={node.fillColor}
|
|
231
|
+
stroke={selected.has(node.id) ? 'currentColor' : node.strokeColor}
|
|
232
|
+
stroke-width={node.strokeWidth}
|
|
233
|
+
/>
|
|
234
|
+
{/if}
|
|
235
|
+
</g>
|
|
236
|
+
{/each}
|
|
237
|
+
</g>
|
|
238
|
+
</svg>
|
|
239
|
+
</div>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { SVGAttributes } from 'svelte/elements';
|
|
3
|
+
import type { CanvasElement, Point, VectorDocument, Viewport } from './types.ts';
|
|
4
|
+
declare function $$render<T extends CanvasElement>(): {
|
|
5
|
+
props: Omit<SVGAttributes<SVGSVGElement>, "class" | "height" | "width"> & {
|
|
6
|
+
/** The document to draw: its elements, viewport and grid. */
|
|
7
|
+
doc: VectorDocument<T>;
|
|
8
|
+
/**
|
|
9
|
+
* Which elements the application considers selected.
|
|
10
|
+
* @defaultValue an empty set
|
|
11
|
+
*/
|
|
12
|
+
selected?: ReadonlySet<string> | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* The application's active tool; `pan` is the one this component handles.
|
|
15
|
+
* @defaultValue `'select'`
|
|
16
|
+
*/
|
|
17
|
+
tool?: string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Viewport size in CSS pixels.
|
|
20
|
+
* @defaultValue `800`
|
|
21
|
+
*/
|
|
22
|
+
width?: number | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Viewport height in CSS pixels.
|
|
25
|
+
* @defaultValue `600`
|
|
26
|
+
*/
|
|
27
|
+
height?: number | undefined;
|
|
28
|
+
/** Draws one element, told whether it is selected. */
|
|
29
|
+
element?: Snippet<[T, boolean]> | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Whether pointer input is accepted at all.
|
|
32
|
+
* @defaultValue `true`
|
|
33
|
+
*/
|
|
34
|
+
interactive?: boolean | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Names the surface for assistive tech.
|
|
37
|
+
* @defaultValue `'Vector editing canvas'`
|
|
38
|
+
*/
|
|
39
|
+
label?: string | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* The viewport after a pan or a zoom.
|
|
42
|
+
*
|
|
43
|
+
* `| undefined` on every callback here and below is the `$props()` seam
|
|
44
|
+
* under `exactOptionalPropertyTypes`: a caller holding an optional handler
|
|
45
|
+
* spreads it straight through, and `{...maybe}` hands over an explicit
|
|
46
|
+
* `undefined` that a bare `?:` would refuse.
|
|
47
|
+
*/
|
|
48
|
+
onviewportchange?: ((viewport: Viewport) => void) | undefined;
|
|
49
|
+
/** A pointer gesture, in canvas coordinates, snapped when the grid says so. */
|
|
50
|
+
oninteract?: ((event: {
|
|
51
|
+
readonly phase: "down" | "move" | "up";
|
|
52
|
+
readonly tool: string;
|
|
53
|
+
readonly point: Point;
|
|
54
|
+
/**
|
|
55
|
+
* How many presses this one is in a run of rapid presses at the same
|
|
56
|
+
* spot: 1 for a single press, 2 for the second of a double-press.
|
|
57
|
+
* Always 1 on `move` and `up`.
|
|
58
|
+
*/
|
|
59
|
+
readonly detail: number;
|
|
60
|
+
readonly native: PointerEvent;
|
|
61
|
+
}) => void) | undefined;
|
|
62
|
+
/** Merged onto the sized box that clips the surface, caller's last. */
|
|
63
|
+
class?: string | undefined;
|
|
64
|
+
};
|
|
65
|
+
exports: {};
|
|
66
|
+
bindings: "";
|
|
67
|
+
slots: {};
|
|
68
|
+
events: {};
|
|
69
|
+
};
|
|
70
|
+
declare class __sveltets_Render<T extends CanvasElement> {
|
|
71
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
72
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
73
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
74
|
+
bindings(): "";
|
|
75
|
+
exports(): {};
|
|
76
|
+
}
|
|
77
|
+
interface $$IsomorphicComponent {
|
|
78
|
+
new <T extends CanvasElement>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
79
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
80
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
81
|
+
<T extends CanvasElement>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
82
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
83
|
+
}
|
|
84
|
+
/** A pan-and-zoom SVG surface over a `VectorDocument`, with each element drawn by a renderer snippet the application supplies. */
|
|
85
|
+
declare const VectorCanvas: $$IsomorphicComponent;
|
|
86
|
+
type VectorCanvas<T extends CanvasElement> = InstanceType<typeof VectorCanvas<T>>;
|
|
87
|
+
export default VectorCanvas;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Undo and redo over a {@link VectorDocument}, as pure functions on the
|
|
3
|
+
* {@link Transform} and {@link HistoryEntry} types `./types.ts` already
|
|
4
|
+
* declares.
|
|
5
|
+
*
|
|
6
|
+
* Those two types were written for exactly this; `Transform`'s own comment is
|
|
7
|
+
* the specification — "carrying the previous elements alongside the new ones is
|
|
8
|
+
* what makes a transform invertible — undo is the same structure read
|
|
9
|
+
* backwards, rather than a second code path". This module takes it at its word,
|
|
10
|
+
* so there is no second structure to keep in step with the first.
|
|
11
|
+
*
|
|
12
|
+
* **Transforms, not snapshots.** A history of whole documents is bounded by the
|
|
13
|
+
* size of the PLAN; a history of transforms is bounded by the size of the
|
|
14
|
+
* EDITS. A plan with four hundred elements would otherwise cost four hundred
|
|
15
|
+
* elements per keystroke.
|
|
16
|
+
*
|
|
17
|
+
* @module @thomasfosterau/components/canvas/history
|
|
18
|
+
*/
|
|
19
|
+
import type { CanvasElement, HistoryEntry, Transform, VectorDocument } from './types.ts';
|
|
20
|
+
/**
|
|
21
|
+
* How many steps back an editing session can walk.
|
|
22
|
+
*
|
|
23
|
+
* Capped rather than unbounded: a plan editor is a long-lived page, and an
|
|
24
|
+
* unbounded stack is a leak that only shows up in the session that matters. A
|
|
25
|
+
* hundred is well past what a reader walks back by hand.
|
|
26
|
+
*/
|
|
27
|
+
export declare const HISTORY_LIMIT = 100;
|
|
28
|
+
/** The undo and redo stacks; the caller owns them, as it owns the document. */
|
|
29
|
+
export interface History<T extends CanvasElement = CanvasElement> {
|
|
30
|
+
/** Oldest first; the last entry is the one `undo` takes back. */
|
|
31
|
+
readonly past: readonly HistoryEntry<T>[];
|
|
32
|
+
/** Nearest first; the first entry is the one `redo` puts back. */
|
|
33
|
+
readonly future: readonly HistoryEntry<T>[];
|
|
34
|
+
}
|
|
35
|
+
/** A session with nothing to undo. */
|
|
36
|
+
export declare const emptyHistory: <T extends CanvasElement>() => History<T>;
|
|
37
|
+
/**
|
|
38
|
+
* A transform's meaning, for every one of the four `type`s:
|
|
39
|
+
* **every id in `elementIds` ends as its entry in `elements`, or absent when
|
|
40
|
+
* `elements` has none for it.**
|
|
41
|
+
*
|
|
42
|
+
* `add`, `update` and `delete` are the narrow cases of that one rule (absent
|
|
43
|
+
* before, present both, present before) and `batch` is the mixed one. Naming
|
|
44
|
+
* them separately is for the reader and for a history entry's `description`;
|
|
45
|
+
* the arithmetic below does not branch on `type` at all, which is why a
|
|
46
|
+
* mixed `batch` cannot be the case somebody forgot to handle.
|
|
47
|
+
*
|
|
48
|
+
* `previousElements` is not read here — it is what {@link invertTransform}
|
|
49
|
+
* needs, and reading it would make applying a transform depend on the document
|
|
50
|
+
* it was recorded against still being the current one.
|
|
51
|
+
*
|
|
52
|
+
* A re-added element is APPENDED rather than restored to its old index. Paint
|
|
53
|
+
* order is `zIndex`'s job (`sortByZIndex`), so the array's order carries no
|
|
54
|
+
* meaning to preserve.
|
|
55
|
+
*/
|
|
56
|
+
export declare const applyTransform: <T extends CanvasElement>(doc: VectorDocument<T>, transform: Transform<T>) => VectorDocument<T>;
|
|
57
|
+
/**
|
|
58
|
+
* The transform that undoes this one: the two element lists swapped, and
|
|
59
|
+
* `add` and `delete` exchanged so a history entry still reads as what it did.
|
|
60
|
+
*
|
|
61
|
+
* Both lists are written as arrays even when the original omitted one, so the
|
|
62
|
+
* result is a transform whose own inverse is the original — `invert(invert(t))`
|
|
63
|
+
* differs from `t` only in carrying `[]` where `t` carried nothing.
|
|
64
|
+
*/
|
|
65
|
+
export declare const invertTransform: <T extends CanvasElement>(transform: Transform<T>) => Transform<T>;
|
|
66
|
+
/**
|
|
67
|
+
* The history with one edit pushed on.
|
|
68
|
+
*
|
|
69
|
+
* Recording CLEARS the redo stack, which is the standard rule and the only
|
|
70
|
+
* honest one: once the document has diverged, the futures recorded against the
|
|
71
|
+
* old one describe a document that no longer exists.
|
|
72
|
+
*
|
|
73
|
+
* `timestamp` is a parameter rather than a `Date.now()` call inside, so this
|
|
74
|
+
* stays a pure function a test can pin.
|
|
75
|
+
*/
|
|
76
|
+
export declare const recordEdit: <T extends CanvasElement>(history: History<T>, transform: Transform<T>, description?: string, timestamp?: number) => History<T>;
|
|
77
|
+
/**
|
|
78
|
+
* The transform from one set of elements to another, both keyed by id.
|
|
79
|
+
*
|
|
80
|
+
* This is how a component that has just changed a document reports WHAT it
|
|
81
|
+
* changed without keeping a parallel log: snapshot the elements a gesture is
|
|
82
|
+
* about when it starts, read them again when it ends, and the difference is the
|
|
83
|
+
* history entry. A gesture that touched one element costs one element, however
|
|
84
|
+
* many pointer moves it took.
|
|
85
|
+
*
|
|
86
|
+
* The `type` is named from the two sets — absent before is an `add`, absent
|
|
87
|
+
* after a `delete`, a mix a `batch` — so an entry's `type` still reads as what
|
|
88
|
+
* the reader did even though {@link applyTransform} does not branch on it.
|
|
89
|
+
*/
|
|
90
|
+
export declare const diffTransform: <T extends CanvasElement>(before: readonly T[], after: readonly T[]) => Transform<T>;
|
|
91
|
+
/** A document and the history that goes with it, after a step in either direction. */
|
|
92
|
+
export interface HistoryStep<T extends CanvasElement = CanvasElement> {
|
|
93
|
+
readonly doc: VectorDocument<T>;
|
|
94
|
+
readonly history: History<T>;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The document one step back, or nothing when there is nothing to undo.
|
|
98
|
+
*
|
|
99
|
+
* None-or-one rather than a nullable result, for `boundingBox`'s reason: the
|
|
100
|
+
* caller writes `const [step] = undo(...)` and the empty case needs no
|
|
101
|
+
* sentinel to remember to check.
|
|
102
|
+
*/
|
|
103
|
+
export declare const undo: <T extends CanvasElement>(doc: VectorDocument<T>, history: History<T>) => readonly HistoryStep<T>[];
|
|
104
|
+
/** The document one step forward, or nothing when there is nothing to redo. */
|
|
105
|
+
export declare const redo: <T extends CanvasElement>(doc: VectorDocument<T>, history: History<T>) => readonly HistoryStep<T>[];
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Undo and redo over a {@link VectorDocument}, as pure functions on the
|
|
3
|
+
* {@link Transform} and {@link HistoryEntry} types `./types.ts` already
|
|
4
|
+
* declares.
|
|
5
|
+
*
|
|
6
|
+
* Those two types were written for exactly this; `Transform`'s own comment is
|
|
7
|
+
* the specification — "carrying the previous elements alongside the new ones is
|
|
8
|
+
* what makes a transform invertible — undo is the same structure read
|
|
9
|
+
* backwards, rather than a second code path". This module takes it at its word,
|
|
10
|
+
* so there is no second structure to keep in step with the first.
|
|
11
|
+
*
|
|
12
|
+
* **Transforms, not snapshots.** A history of whole documents is bounded by the
|
|
13
|
+
* size of the PLAN; a history of transforms is bounded by the size of the
|
|
14
|
+
* EDITS. A plan with four hundred elements would otherwise cost four hundred
|
|
15
|
+
* elements per keystroke.
|
|
16
|
+
*
|
|
17
|
+
* @module @thomasfosterau/components/canvas/history
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* How many steps back an editing session can walk.
|
|
21
|
+
*
|
|
22
|
+
* Capped rather than unbounded: a plan editor is a long-lived page, and an
|
|
23
|
+
* unbounded stack is a leak that only shows up in the session that matters. A
|
|
24
|
+
* hundred is well past what a reader walks back by hand.
|
|
25
|
+
*/
|
|
26
|
+
export const HISTORY_LIMIT = 100;
|
|
27
|
+
/** A session with nothing to undo. */
|
|
28
|
+
export const emptyHistory = () => ({ past: [], future: [] });
|
|
29
|
+
/**
|
|
30
|
+
* A transform's meaning, for every one of the four `type`s:
|
|
31
|
+
* **every id in `elementIds` ends as its entry in `elements`, or absent when
|
|
32
|
+
* `elements` has none for it.**
|
|
33
|
+
*
|
|
34
|
+
* `add`, `update` and `delete` are the narrow cases of that one rule (absent
|
|
35
|
+
* before, present both, present before) and `batch` is the mixed one. Naming
|
|
36
|
+
* them separately is for the reader and for a history entry's `description`;
|
|
37
|
+
* the arithmetic below does not branch on `type` at all, which is why a
|
|
38
|
+
* mixed `batch` cannot be the case somebody forgot to handle.
|
|
39
|
+
*
|
|
40
|
+
* `previousElements` is not read here — it is what {@link invertTransform}
|
|
41
|
+
* needs, and reading it would make applying a transform depend on the document
|
|
42
|
+
* it was recorded against still being the current one.
|
|
43
|
+
*
|
|
44
|
+
* A re-added element is APPENDED rather than restored to its old index. Paint
|
|
45
|
+
* order is `zIndex`'s job (`sortByZIndex`), so the array's order carries no
|
|
46
|
+
* meaning to preserve.
|
|
47
|
+
*/
|
|
48
|
+
export const applyTransform = (doc, transform) => {
|
|
49
|
+
const touched = new Set(transform.elementIds);
|
|
50
|
+
const next = new Map((transform.elements ?? []).map((element) => [element.id, element]));
|
|
51
|
+
const present = new Set(doc.elements.map((element) => element.id));
|
|
52
|
+
const kept = doc.elements.flatMap((element) => {
|
|
53
|
+
if (!touched.has(element.id))
|
|
54
|
+
return [element];
|
|
55
|
+
const replacement = next.get(element.id);
|
|
56
|
+
return replacement === undefined ? [] : [replacement];
|
|
57
|
+
});
|
|
58
|
+
const added = (transform.elements ?? []).filter((element) => !present.has(element.id));
|
|
59
|
+
return { ...doc, elements: [...kept, ...added] };
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* The transform that undoes this one: the two element lists swapped, and
|
|
63
|
+
* `add` and `delete` exchanged so a history entry still reads as what it did.
|
|
64
|
+
*
|
|
65
|
+
* Both lists are written as arrays even when the original omitted one, so the
|
|
66
|
+
* result is a transform whose own inverse is the original — `invert(invert(t))`
|
|
67
|
+
* differs from `t` only in carrying `[]` where `t` carried nothing.
|
|
68
|
+
*/
|
|
69
|
+
export const invertTransform = (transform) => ({
|
|
70
|
+
type: transform.type === 'add' ? 'delete' : transform.type === 'delete' ? 'add' : transform.type,
|
|
71
|
+
elementIds: transform.elementIds,
|
|
72
|
+
elements: transform.previousElements ?? [],
|
|
73
|
+
previousElements: transform.elements ?? [],
|
|
74
|
+
});
|
|
75
|
+
/**
|
|
76
|
+
* The history with one edit pushed on.
|
|
77
|
+
*
|
|
78
|
+
* Recording CLEARS the redo stack, which is the standard rule and the only
|
|
79
|
+
* honest one: once the document has diverged, the futures recorded against the
|
|
80
|
+
* old one describe a document that no longer exists.
|
|
81
|
+
*
|
|
82
|
+
* `timestamp` is a parameter rather than a `Date.now()` call inside, so this
|
|
83
|
+
* stays a pure function a test can pin.
|
|
84
|
+
*/
|
|
85
|
+
export const recordEdit = (history, transform, description = '', timestamp = Date.now()) => {
|
|
86
|
+
const entry = {
|
|
87
|
+
transform,
|
|
88
|
+
timestamp,
|
|
89
|
+
...(description === '' ? {} : { description }),
|
|
90
|
+
};
|
|
91
|
+
const past = [...history.past, entry];
|
|
92
|
+
return { past: past.slice(Math.max(0, past.length - HISTORY_LIMIT)), future: [] };
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* The transform from one set of elements to another, both keyed by id.
|
|
96
|
+
*
|
|
97
|
+
* This is how a component that has just changed a document reports WHAT it
|
|
98
|
+
* changed without keeping a parallel log: snapshot the elements a gesture is
|
|
99
|
+
* about when it starts, read them again when it ends, and the difference is the
|
|
100
|
+
* history entry. A gesture that touched one element costs one element, however
|
|
101
|
+
* many pointer moves it took.
|
|
102
|
+
*
|
|
103
|
+
* The `type` is named from the two sets — absent before is an `add`, absent
|
|
104
|
+
* after a `delete`, a mix a `batch` — so an entry's `type` still reads as what
|
|
105
|
+
* the reader did even though {@link applyTransform} does not branch on it.
|
|
106
|
+
*/
|
|
107
|
+
export const diffTransform = (before, after) => {
|
|
108
|
+
const gone = before.some((element) => !after.some((other) => other.id === element.id));
|
|
109
|
+
const fresh = after.some((element) => !before.some((other) => other.id === element.id));
|
|
110
|
+
const ids = [...new Set([...before, ...after].map((element) => element.id))];
|
|
111
|
+
return {
|
|
112
|
+
type: gone && fresh ? 'batch' : gone ? 'delete' : fresh ? 'add' : 'update',
|
|
113
|
+
elementIds: ids,
|
|
114
|
+
elements: after,
|
|
115
|
+
previousElements: before,
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* The document one step back, or nothing when there is nothing to undo.
|
|
120
|
+
*
|
|
121
|
+
* None-or-one rather than a nullable result, for `boundingBox`'s reason: the
|
|
122
|
+
* caller writes `const [step] = undo(...)` and the empty case needs no
|
|
123
|
+
* sentinel to remember to check.
|
|
124
|
+
*/
|
|
125
|
+
export const undo = (doc, history) => {
|
|
126
|
+
const entry = history.past.at(-1);
|
|
127
|
+
if (entry === undefined)
|
|
128
|
+
return [];
|
|
129
|
+
return [
|
|
130
|
+
{
|
|
131
|
+
doc: applyTransform(doc, invertTransform(entry.transform)),
|
|
132
|
+
history: { past: history.past.slice(0, -1), future: [entry, ...history.future] },
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
};
|
|
136
|
+
/** The document one step forward, or nothing when there is nothing to redo. */
|
|
137
|
+
export const redo = (doc, history) => {
|
|
138
|
+
const [entry, ...rest] = history.future;
|
|
139
|
+
if (entry === undefined)
|
|
140
|
+
return [];
|
|
141
|
+
return [
|
|
142
|
+
{
|
|
143
|
+
doc: applyTransform(doc, entry.transform),
|
|
144
|
+
history: { past: [...history.past, entry], future: rest },
|
|
145
|
+
},
|
|
146
|
+
];
|
|
147
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@thomasfosterau/components/canvas` — a low-level toolkit for vector-editing
|
|
3
|
+
* surfaces: pan, zoom, a grid, and pointer gestures reported in canvas
|
|
4
|
+
* coordinates. The application defines its element types, its tools and its
|
|
5
|
+
* renderer.
|
|
6
|
+
*
|
|
7
|
+
* A separate entry point so it never sits on the always-loaded path of a
|
|
8
|
+
* consumer that does not use it.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
export { default as VectorCanvas } from './VectorCanvas.svelte';
|
|
13
|
+
export { applyTransform, diffTransform, emptyHistory, HISTORY_LIMIT, invertTransform, recordEdit, redo, undo, type History, type HistoryStep, } from './history.ts';
|
|
14
|
+
export type { CanvasElement, ElementId, GridConfig, HistoryEntry, Point, Rect, Transform, VectorDocument, Viewport, } from './types.ts';
|
|
15
|
+
export { boundingBox, canvasToScreen, distance, generateId, panBy, pointInRect, pointNearLine, rotatePoint, screenToCanvas, snapToGrid, sortByZIndex, zoomAbout, } from './utils.ts';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@thomasfosterau/components/canvas` — a low-level toolkit for vector-editing
|
|
3
|
+
* surfaces: pan, zoom, a grid, and pointer gestures reported in canvas
|
|
4
|
+
* coordinates. The application defines its element types, its tools and its
|
|
5
|
+
* renderer.
|
|
6
|
+
*
|
|
7
|
+
* A separate entry point so it never sits on the always-loaded path of a
|
|
8
|
+
* consumer that does not use it.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
export { default as VectorCanvas } from './VectorCanvas.svelte';
|
|
13
|
+
export { applyTransform, diffTransform, emptyHistory, HISTORY_LIMIT, invertTransform, recordEdit, redo, undo, } from "./history.js";
|
|
14
|
+
export { boundingBox, canvasToScreen, distance, generateId, panBy, pointInRect, pointNearLine, rotatePoint, screenToCanvas, snapToGrid, sortByZIndex, zoomAbout, } from "./utils.js";
|