@triiiceratops/plugin-annotation-editor 1.0.0-rc.7 → 1.0.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/README.md +332 -0
- package/dist/AnnotationStore.svelte.d.ts +25 -26
- package/dist/adapters/types.d.ts +1 -1
- package/dist/catalog.d.ts +3 -4
- package/dist/contextKey.d.ts +3 -3
- package/dist/drawingLayer.d.ts +22 -0
- package/dist/drawingSession.svelte.d.ts +101 -0
- package/dist/editableShape.d.ts +69 -0
- package/dist/geometry.d.ts +310 -0
- package/dist/icons.d.ts +4 -3
- package/dist/identity.d.ts +10 -0
- package/dist/iife.js +1 -1273
- package/dist/index.d.ts +1 -1
- package/dist/index.js +15 -6022
- package/dist/loader.svelte.d.ts +1 -1
- package/dist/plugin.d.ts +9 -5
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +1 -158
- package/dist/tools.d.ts +16 -0
- package/dist/types.d.ts +12 -15
- package/dist/viewerMirror.svelte.d.ts +47 -7
- package/package.json +10 -17
- package/dist/AnnotationManager.svelte.d.ts +0 -260
- package/dist/styles.d.ts +0 -11
package/README.md
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# @triiiceratops/plugin-annotation-editor
|
|
2
|
+
|
|
3
|
+
Annotation editing for the [Triiiceratops](https://triiiceratops.org/)
|
|
4
|
+
IIIF viewer.
|
|
5
|
+
|
|
6
|
+
Core renders and selects the annotations a manifest publishes; it writes none.
|
|
7
|
+
This plugin adds the writing half: a **drawing layer** over the image with
|
|
8
|
+
rectangle, ellipse, polygon, point and whole-canvas tools, a panel with a body
|
|
9
|
+
editor and persistence-aware undo/redo, and an `AnnotationStorageAdapter` seam so
|
|
10
|
+
annotations persist wherever your institution keeps them. Every tool is operable
|
|
11
|
+
from the keyboard, for creation as well as editing.
|
|
12
|
+
|
|
13
|
+
The drawing layer is built on core's own published primitives — an overlay layer
|
|
14
|
+
for its DOM, `canvasToScreen`/`screenToCanvas` for projection, `subscribeFrame`
|
|
15
|
+
for reprojection — so no third party's object model sits between the editor and
|
|
16
|
+
the viewer. Annotations are persisted as W3C Web Annotations targeting canvas
|
|
17
|
+
coordinates: `FragmentSelector` (`xywh=`) for a rectangle, `SvgSelector` with a
|
|
18
|
+
`<polygon>` for an ellipse or a polygon, `PointSelector` for a point, and no
|
|
19
|
+
selector at all for a whole-canvas note.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pnpm add @triiiceratops/plugin-annotation-editor
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`triiiceratops`, `@triiiceratops/plugin-sdk` and `svelte` are peers. The plugin
|
|
28
|
+
declares a core floor (`coreRange: '>=1.0.0-rc.36'`) — the first core that ships
|
|
29
|
+
overlay-layer registration — and refuses to activate against anything older,
|
|
30
|
+
loudly, on core's plugin-error channel rather than mounting a button that does
|
|
31
|
+
nothing.
|
|
32
|
+
|
|
33
|
+
## Registering it
|
|
34
|
+
|
|
35
|
+
### As a module (any bundler)
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import {
|
|
39
|
+
AnnotationEditorPlugin,
|
|
40
|
+
createAnnotationEditorPlugin,
|
|
41
|
+
LocalStorageAdapter,
|
|
42
|
+
} from '@triiiceratops/plugin-annotation-editor';
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`AnnotationEditorPlugin` is the preconfigured plugin: every tool, rectangle
|
|
46
|
+
armed by default, and the built-in `LocalStorageAdapter`. Use
|
|
47
|
+
`createAnnotationEditorPlugin(config)` for anything else. Hand either to the
|
|
48
|
+
viewer the way you hand it any other plugin — the `plugins` prop in Svelte,
|
|
49
|
+
React and Vue, or the `.plugins` **property** (never an attribute) on the
|
|
50
|
+
`<triiiceratops-viewer>` custom element:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const annotations = createAnnotationEditorPlugin({
|
|
54
|
+
adapter: new LocalStorageAdapter(),
|
|
55
|
+
user: { id: 'user-123', name: 'Jane Doe' },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
document.querySelector('triiiceratops-viewer').plugins = [annotations];
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### As a script tag (IIFE)
|
|
62
|
+
|
|
63
|
+
```html
|
|
64
|
+
<script src="/assets/triiiceratops-element.iife.js"></script>
|
|
65
|
+
<script src="/assets/plugin-annotation-editor/iife.js"></script>
|
|
66
|
+
|
|
67
|
+
<triiiceratops-viewer id="viewer"></triiiceratops-viewer>
|
|
68
|
+
<script>
|
|
69
|
+
// Loading the script only registers a factory; activation is per-viewer.
|
|
70
|
+
document.getElementById('viewer').plugins = [
|
|
71
|
+
window.Triiiceratops.plugins.get(
|
|
72
|
+
'@triiiceratops/plugin-annotation-editor',
|
|
73
|
+
),
|
|
74
|
+
];
|
|
75
|
+
</script>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Either script order works — the registry is bootstrapped order-independently —
|
|
79
|
+
and this bundle carries its own Svelte runtime, so it shares nothing private
|
|
80
|
+
with core. The IIFE path uses the built-in `LocalStorageAdapter`; a custom
|
|
81
|
+
adapter needs the module entry.
|
|
82
|
+
|
|
83
|
+
## Drawing
|
|
84
|
+
|
|
85
|
+
Drawing is **modal**: the reader arms a tool from the panel, and for as long as
|
|
86
|
+
it is armed the drawing layer takes pointer events across the whole image, so a
|
|
87
|
+
drag draws instead of panning. One tool, one gesture — nothing ever has to guess
|
|
88
|
+
whether a press was a click or a drag.
|
|
89
|
+
|
|
90
|
+
| Tool | Gesture | Persisted target |
|
|
91
|
+
| ------------ | ---------------------------------------------- | ------------------------- |
|
|
92
|
+
| Rectangle | drag a bounding box | `FragmentSelector` |
|
|
93
|
+
| Ellipse | drag a bounding box | `SvgSelector` `<polygon>` |
|
|
94
|
+
| Polygon | click per vertex; Enter or double-click closes | `SvgSelector` `<polygon>` |
|
|
95
|
+
| Point | single click | `PointSelector` |
|
|
96
|
+
| Whole canvas | activate the tool | no selector |
|
|
97
|
+
|
|
98
|
+
A region smaller than a few canvas pixels in either dimension is discarded
|
|
99
|
+
rather than committed, so hand jitter does not litter an annotation set with
|
|
100
|
+
two-pixel shapes.
|
|
101
|
+
|
|
102
|
+
An **ellipse is a creation affordance only**. It persists as a 64-point polygon
|
|
103
|
+
inscribed in the dragged box, and edits afterwards as the polygon it is —
|
|
104
|
+
nothing records that it was once an ellipse. This is deliberate; see
|
|
105
|
+
[ADR 0022](../../docs/adr/0022-an-ellipse-persists-as-a-polygon.md).
|
|
106
|
+
|
|
107
|
+
What still works while a tool is armed:
|
|
108
|
+
|
|
109
|
+
- **Wheel zoom**, unchanged — place a vertex precisely without disarming.
|
|
110
|
+
- **Keyboard zoom and arrow-key panning**, unchanged.
|
|
111
|
+
- **Hold Space to pan by dragging.** The layer drops its own pointer events for
|
|
112
|
+
as long as Space is held and the viewer's ordinary panning takes over
|
|
113
|
+
underneath; a Space-pan never commits a shape.
|
|
114
|
+
|
|
115
|
+
Pointer-drag panning is the one thing arming suppresses, and Space is its escape
|
|
116
|
+
hatch.
|
|
117
|
+
|
|
118
|
+
A tool is armed only while the panel that explains it is on screen. There are
|
|
119
|
+
two exits and both cancel rather than commit: Escape, and **closing the
|
|
120
|
+
panel** — which gives the image straight back.
|
|
121
|
+
|
|
122
|
+
## Keyboard
|
|
123
|
+
|
|
124
|
+
Creation from the keyboard is **place-then-shape**: arming a tool drops a
|
|
125
|
+
default-sized shape at the centre of the current view, which the ordinary
|
|
126
|
+
editing verbs then move and size. Creation and editing share one set of verbs.
|
|
127
|
+
|
|
128
|
+
| Key | Effect |
|
|
129
|
+
| --------------- | ---------------------------------------------------------------------------------------- |
|
|
130
|
+
| Arrows | nudge the focused handle, vertex or whole shape by 1 canvas pixel |
|
|
131
|
+
| Shift + arrows | the larger step, 10 canvas pixels |
|
|
132
|
+
| Tab / Shift+Tab | cycle the shape's handles and vertices |
|
|
133
|
+
| Enter | commit; for a polygon in progress, close the outline |
|
|
134
|
+
| Escape | cancel the keyboard shape or edit in progress, else discard any pointer draft and disarm |
|
|
135
|
+
| Delete | remove the selected annotation |
|
|
136
|
+
| `i` | insert a vertex after the focused polygon vertex |
|
|
137
|
+
| `x` | remove the focused polygon vertex |
|
|
138
|
+
|
|
139
|
+
Nudges are in **canvas** pixels, not screen pixels, so one press moves a vertex
|
|
140
|
+
the same distance across the folio at fit zoom as at 8×.
|
|
141
|
+
|
|
142
|
+
Every persisted annotation stays a focusable, labelled element while the editor
|
|
143
|
+
is open — those targets are core's, and the drawing layer draws only the one
|
|
144
|
+
annotation currently under edit, its handles and the in-progress preview.
|
|
145
|
+
|
|
146
|
+
## Storage: the adapter seam
|
|
147
|
+
|
|
148
|
+
An adapter is **pure storage**. It knows nothing about how a shape is displayed;
|
|
149
|
+
the plugin's store owns display sync, caching, id reconciliation, creator
|
|
150
|
+
stamping, hydration and error handling around it.
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import type {
|
|
154
|
+
AnnotationStorageAdapter,
|
|
155
|
+
W3CAnnotation,
|
|
156
|
+
} from '@triiiceratops/plugin-annotation-editor';
|
|
157
|
+
|
|
158
|
+
const adapter: AnnotationStorageAdapter = {
|
|
159
|
+
id: 'my-server',
|
|
160
|
+
name: 'Institutional annotation server',
|
|
161
|
+
|
|
162
|
+
async load(manifestId, canvasId) {
|
|
163
|
+
const res = await fetch(url(manifestId, canvasId));
|
|
164
|
+
return res.json();
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
// Return the canonical annotation (or just its id) when your server mints
|
|
168
|
+
// its own IRI — the plugin reconciles the id everywhere. `void` keeps the
|
|
169
|
+
// client-generated one.
|
|
170
|
+
async create(manifestId, canvasId, annotation) {
|
|
171
|
+
const res = await fetch(url(manifestId, canvasId), {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
body: JSON.stringify(annotation),
|
|
174
|
+
});
|
|
175
|
+
return (await res.json()) as W3CAnnotation;
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
async update(manifestId, canvasId, annotation) {
|
|
179
|
+
/* … */
|
|
180
|
+
},
|
|
181
|
+
async delete(manifestId, canvasId, annotationId) {
|
|
182
|
+
/* … */
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`load` may return **skeleton** entries — annotations whose bodies have not been
|
|
188
|
+
fetched — marked `__fullBodyLoaded: false`; implement the optional `hydrate` and
|
|
189
|
+
the plugin fetches a full body when one is opened. Optional `destroy` is called
|
|
190
|
+
on teardown.
|
|
191
|
+
|
|
192
|
+
A failed write **rolls back** the plugin's optimistic changes and then calls
|
|
193
|
+
`config.onPersistenceError` with the operation, the annotation id and a `retry()`
|
|
194
|
+
that re-runs the exact failed call. Omit the handler and the plugin logs and
|
|
195
|
+
shows a dismissible error line in the panel, so a failure is never invisible.
|
|
196
|
+
|
|
197
|
+
The conformance suite the built-in adapter passes is exported for yours to run
|
|
198
|
+
against:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import { runAdapterContractTests } from '@triiiceratops/plugin-annotation-editor/testing';
|
|
202
|
+
|
|
203
|
+
runAdapterContractTests(() => new MyAdapter(), {
|
|
204
|
+
supportsIdReconciliation: true,
|
|
205
|
+
supportsHydrate: true,
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`LocalStorageAdapter` is the built-in default and persists under a frozen
|
|
210
|
+
`@triiiceratops/plugin-annotation-editor:v1` namespace, keyed by manifest and
|
|
211
|
+
canvas.
|
|
212
|
+
|
|
213
|
+
## Replacing the body editor
|
|
214
|
+
|
|
215
|
+
The built-in body editor edits W3C bodies — a value, a format, a language and a
|
|
216
|
+
purpose from the W3C vocabulary — and leaves structured bodies it does not
|
|
217
|
+
understand untouched across a save. To edit your own metadata model in place,
|
|
218
|
+
pass `bodyEditor`: either a Svelte `component` taking an `api` prop, or a
|
|
219
|
+
framework-neutral `render(container, api)` returning its own cleanup.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
createAnnotationEditorPlugin({
|
|
223
|
+
bodyEditor: {
|
|
224
|
+
render(container, api) {
|
|
225
|
+
const input = document.createElement('textarea');
|
|
226
|
+
input.value = String(api.bodies[0]?.value ?? '');
|
|
227
|
+
input.onchange = () =>
|
|
228
|
+
api.save([{ type: 'TextualBody', value: input.value }]);
|
|
229
|
+
container.append(input);
|
|
230
|
+
return () => input.remove();
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`api` carries the full `annotation` in canvas space, its `bodies` normalised to
|
|
237
|
+
an array, the runtime `context` (manifest, canvas, user, host context),
|
|
238
|
+
`isHydrating`, and `save` / `cancel` / `requestDelete`. The plugin owns the
|
|
239
|
+
geometry and the persistence; the body editor owns only the bodies.
|
|
240
|
+
|
|
241
|
+
`extension` is the other seam, for host applications rather than for a different
|
|
242
|
+
body shape: gate creation (`canCreate`, `getCreateDisabledReason`), prefill a
|
|
243
|
+
draft (`prepareDraft`), transform on the way out (`beforeSave`), or observe
|
|
244
|
+
selection (`onSelectionChange`).
|
|
245
|
+
|
|
246
|
+
## Configuration
|
|
247
|
+
|
|
248
|
+
| Option | Default | Notes |
|
|
249
|
+
| --------------------------------------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------- |
|
|
250
|
+
| `adapter` | — | Storage. `LocalStorageAdapter` on the preconfigured plugin. |
|
|
251
|
+
| `user` | — | `{ id, name? }`, stamped onto new annotations as creator. |
|
|
252
|
+
| `tools` | all five | Which tools the panel offers. |
|
|
253
|
+
| `defaultTool` | first in `tools` | Honoured only if it is within `tools`. |
|
|
254
|
+
| `defaultMotivation` | `'commenting'` | Never overwrites a motivation the host already set. |
|
|
255
|
+
| `target` | `'panel'` | Where the plugin chrome renders. |
|
|
256
|
+
| `ui` | — | `showModeToggle`, `startInCreateMode`, `showUndoRedo`, `purposes`, `allowMultipleBodies`. |
|
|
257
|
+
| `bodyEditor`, `extension`, `onPersistenceError` | — | See above. |
|
|
258
|
+
| `prepareAnnotation`, `canCreateAnnotation`, `getCreateDisabledReason` | — | Flat equivalents of the matching `extension` hooks. |
|
|
259
|
+
|
|
260
|
+
## Styling
|
|
261
|
+
|
|
262
|
+
The drawing layer is DOM and SVG, so CSS styles it — theming follows the
|
|
263
|
+
viewer's `--tri-` custom properties like everything else. There is no styling
|
|
264
|
+
**option**; set these on the viewer or any ancestor.
|
|
265
|
+
|
|
266
|
+
| Property | Default |
|
|
267
|
+
| ------------------------------------ | ----------------------------------------------- |
|
|
268
|
+
| `--tri-annotation-draw-stroke` | `var(--tri-color-primary)` |
|
|
269
|
+
| `--tri-annotation-draw-stroke-width` | `2px` |
|
|
270
|
+
| `--tri-annotation-draw-fill` | 20% `--tri-color-primary`, mixed to transparent |
|
|
271
|
+
| `--tri-annotation-draw-cursor` | `crosshair` |
|
|
272
|
+
| `--tri-annotation-edit-move-cursor` | `move` |
|
|
273
|
+
| `--tri-annotation-handle-size` | `10px` |
|
|
274
|
+
| `--tri-annotation-handle-fill` | `var(--tri-color-primary)` |
|
|
275
|
+
| `--tri-annotation-handle-stroke` | `var(--tri-color-base-100)` |
|
|
276
|
+
| `--tri-annotation-point-fill` | `var(--tri-annotation-color)` |
|
|
277
|
+
| `--tri-annotation-point-stroke` | `var(--tri-annotation-color)` |
|
|
278
|
+
|
|
279
|
+
Point markers are the exception: they are core's, not this layer's. Their size is
|
|
280
|
+
`--tri-annotation-point-size` and their colour `--tri-annotation-color`, the same
|
|
281
|
+
two tokens core's read-only overlay draws and measures a marker from, so a point
|
|
282
|
+
looks the same open for editing as it does at rest and one declaration restyles
|
|
283
|
+
both. This plugin declares neither of its own, deliberately — a second setting
|
|
284
|
+
could only disagree with the first, and a point would change under the reader the
|
|
285
|
+
moment it was opened. `--tri-annotation-point-fill` and
|
|
286
|
+
`--tri-annotation-point-stroke` still override the colour for this layer alone,
|
|
287
|
+
which is the only way to make the two disagree on purpose.
|
|
288
|
+
|
|
289
|
+
## Upgrading from `1.0.0-rc.7`
|
|
290
|
+
|
|
291
|
+
`rc.7` was the last published version, and its editing surface was
|
|
292
|
+
[Annotorious](https://annotorious.dev/). Annotorious and OpenSeadragon are gone;
|
|
293
|
+
the drawing layer is first-party. Four configuration changes, all visible at the
|
|
294
|
+
call site:
|
|
295
|
+
|
|
296
|
+
- **`drawingStyle` is removed.** It was typed with an Annotorious type and only
|
|
297
|
+
ever read inside the deleted Annotorious binding. Style the drawing layer with
|
|
298
|
+
the CSS custom properties above instead.
|
|
299
|
+
- **`requiredCapabilities` is gone.** Compatibility is now the `coreRange` floor
|
|
300
|
+
alone — overlay layers are not optional in core, so there was nothing to
|
|
301
|
+
require.
|
|
302
|
+
- **`user` keeps its `{ id, name }` shape** but is a locally declared type
|
|
303
|
+
rather than an Annotorious one. No call-site change; replace an `import type
|
|
304
|
+
{ User } from '@annotorious/openseadragon'` with `AnnotationEditorUser` from
|
|
305
|
+
this package.
|
|
306
|
+
- **`pointStyle` is removed from this plugin's config.** It only ever styled the
|
|
307
|
+
Annotorious marker, which is why it goes the way `drawingStyle` does. Nothing
|
|
308
|
+
replaces it in configuration: a marker is theming now, and both core's
|
|
309
|
+
read-only marker and this editor read the same two tokens —
|
|
310
|
+
`--tri-annotation-point-size` and `--tri-annotation-color` — which is what
|
|
311
|
+
makes a point the same size and colour selected and not.
|
|
312
|
+
|
|
313
|
+
Nothing about persisted data changed. The v1 LocalStorage namespace and the W3C
|
|
314
|
+
annotation format are the same, so annotations written by `rc.7` load and edit
|
|
315
|
+
without migration — an ellipse simply was not drawable before.
|
|
316
|
+
|
|
317
|
+
Everything that was never the Annotorious binding is carried forward unchanged:
|
|
318
|
+
the store, the adapter seam and `LocalStorageAdapter`, display sync, stamping, id
|
|
319
|
+
reconciliation, hydration, persistence-aware undo/redo, the body editor and its
|
|
320
|
+
replacement hook, the extension hooks, the i18n catalog, and the adapter
|
|
321
|
+
conformance suite on the `/testing` subpath.
|
|
322
|
+
|
|
323
|
+
## Design records
|
|
324
|
+
|
|
325
|
+
- [ADR 0020](../../docs/adr/0020-modal-drawing-swallows-pointer-events-in-the-dom.md)
|
|
326
|
+
— why arming a tool takes pointer events in the DOM instead of claiming input
|
|
327
|
+
at the gesture arbiter.
|
|
328
|
+
- [ADR 0021](../../docs/adr/0021-the-editing-surface-is-first-party.md) — why the
|
|
329
|
+
editing surface is first-party, and why the editor draws one annotation while
|
|
330
|
+
core draws the set.
|
|
331
|
+
- [ADR 0022](../../docs/adr/0022-an-ellipse-persists-as-a-polygon.md) — why an
|
|
332
|
+
ellipse is stored as a polygon.
|
|
@@ -11,17 +11,14 @@ export interface AnnotationDisplayState {
|
|
|
11
11
|
clearUserAnnotations(manifestId: string, canvasId: string): void;
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
|
-
* Plugin-internal persistence core. Owns everything
|
|
15
|
-
*
|
|
14
|
+
* Plugin-internal persistence core. Owns everything "to storage": the
|
|
15
|
+
* annotation cache, per-annotation hydration state, create-vs-update
|
|
16
16
|
* resolution, the per-id save queue, the load-race token, and the raw adapter.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* This class is a refactor of code previously inlined in `AnnotationManager`
|
|
24
|
-
* (issues 01–04); behavior is intentionally unchanged (issue 05).
|
|
18
|
+
* The drawing layer talks only to this store for persistence and keeps the
|
|
19
|
+
* geometry (selection, tools, coordinate transforms). The store deals
|
|
20
|
+
* exclusively in **canvas-space** W3C annotations — transforms live at the
|
|
21
|
+
* drawing-layer/store boundary.
|
|
25
22
|
*/
|
|
26
23
|
export declare class AnnotationStore {
|
|
27
24
|
private static readonly W3C_CONTEXT;
|
|
@@ -30,16 +27,17 @@ export declare class AnnotationStore {
|
|
|
30
27
|
private config;
|
|
31
28
|
/**
|
|
32
29
|
* Notified when a `create` reconciles an annotation onto a server-assigned
|
|
33
|
-
* id (F5), so the
|
|
34
|
-
* id and
|
|
35
|
-
* leaves it
|
|
30
|
+
* id (F5), so the drawing layer can follow its in-flight create onto the
|
|
31
|
+
* canonical id and open the body editor on the id the annotation was
|
|
32
|
+
* actually stored under. Set by the drawing layer; the loader leaves it
|
|
33
|
+
* unset.
|
|
36
34
|
*/
|
|
37
35
|
onReconcileId?: (oldId: string, canonical: W3CAnnotation) => void;
|
|
38
36
|
/**
|
|
39
|
-
* Notified after an undo/redo replay so the
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
37
|
+
* Notified after an undo/redo replay so the drawing layer can reconcile the
|
|
38
|
+
* open editing session with the new storage state (F6): `annotation` is the
|
|
39
|
+
* annotation now in the cache under `affectedId`, or `null` when the replay
|
|
40
|
+
* removed it. Set by the drawing layer; the loader leaves it unset.
|
|
43
41
|
*/
|
|
44
42
|
onReplay?: (affectedId: string, annotation: W3CAnnotation | null) => void;
|
|
45
43
|
private manifestId;
|
|
@@ -61,8 +59,9 @@ export declare class AnnotationStore {
|
|
|
61
59
|
constructor(config: AnnotationEditorConfig);
|
|
62
60
|
/**
|
|
63
61
|
* Point display sync at the owning viewer's display state (ADR 0001,
|
|
64
|
-
* amended). Called by the
|
|
65
|
-
* Idempotent — re-attaching the
|
|
62
|
+
* amended). Called by the mount seam when the viewer is known, and by the
|
|
63
|
+
* loader for a store it drives on its own. Idempotent — re-attaching the
|
|
64
|
+
* same viewer is harmless.
|
|
66
65
|
*/
|
|
67
66
|
setDisplayState(displayState: AnnotationDisplayState | null): void;
|
|
68
67
|
get currentManifestId(): string | null;
|
|
@@ -88,8 +87,8 @@ export declare class AnnotationStore {
|
|
|
88
87
|
get canRedo(): boolean;
|
|
89
88
|
/**
|
|
90
89
|
* Point the store at a canvas and drop the previous canvas's cache. Does
|
|
91
|
-
* not load — the
|
|
92
|
-
*
|
|
90
|
+
* not load — the caller drives load timing and display sync around this
|
|
91
|
+
* call.
|
|
93
92
|
*/
|
|
94
93
|
setCanvas(manifestId: string | null, canvasId: string | null): void;
|
|
95
94
|
get(id: string): W3CAnnotation | null;
|
|
@@ -109,12 +108,12 @@ export declare class AnnotationStore {
|
|
|
109
108
|
*
|
|
110
109
|
* On create the store stamps a complete W3C/IIIF annotation (F18) and, if the
|
|
111
110
|
* adapter returns a canonical annotation or id, reconciles the cache/display
|
|
112
|
-
* onto the server-assigned id and notifies
|
|
111
|
+
* onto the server-assigned id and notifies its owner (F5). On update it
|
|
113
112
|
* refreshes `modified` and adopts a server-normalized copy when returned.
|
|
114
113
|
*
|
|
115
114
|
* Cache and display are only advanced *after* the adapter resolves, so a
|
|
116
115
|
* rejected write leaves both at their pre-operation state — the rollback the
|
|
117
|
-
*
|
|
116
|
+
* drawing layer relies on to re-signal selection (F20). Returns `true` on success,
|
|
118
117
|
* `false` when the adapter rejected (the failure has been reported).
|
|
119
118
|
*/
|
|
120
119
|
persist(annotation: W3CAnnotation): Promise<boolean>;
|
|
@@ -130,7 +129,7 @@ export declare class AnnotationStore {
|
|
|
130
129
|
* Fetch a skeleton annotation's full body from the adapter and cache it.
|
|
131
130
|
* Returns the full annotation, or null when there is nothing to do (no
|
|
132
131
|
* hydrate support), the fetch came back empty, the canvas changed while
|
|
133
|
-
* awaiting (F14), or `shouldApply` vetoes committing the result (the
|
|
132
|
+
* awaiting (F14), or `shouldApply` vetoes committing the result (the caller
|
|
134
133
|
* uses this to bail if the annotation is no longer being edited).
|
|
135
134
|
*/
|
|
136
135
|
hydrate(id: string, shouldApply?: () => boolean): Promise<W3CAnnotation | null>;
|
|
@@ -183,14 +182,14 @@ export declare class AnnotationStore {
|
|
|
183
182
|
/**
|
|
184
183
|
* Commit a created annotation to the cache under its canonical id. When the
|
|
185
184
|
* adapter returns a server-assigned annotation or id string, the cache key is
|
|
186
|
-
* swapped from the local id to the canonical one, and
|
|
187
|
-
* so
|
|
185
|
+
* swapped from the local id to the canonical one, and `onReconcileId` fires
|
|
186
|
+
* so the owner can follow the annotation onto the new id (F5).
|
|
188
187
|
*/
|
|
189
188
|
private reconcileCreate;
|
|
190
189
|
/**
|
|
191
190
|
* Stamp a complete, valid W3C/IIIF annotation before create without
|
|
192
191
|
* clobbering host-provided values (F18). `extension.beforeSave` has already
|
|
193
|
-
* run
|
|
192
|
+
* run by the caller and therefore still wins — stamping only fills gaps.
|
|
194
193
|
*/
|
|
195
194
|
private stampForCreate;
|
|
196
195
|
/** Refresh `modified` on an updated annotation (F18). */
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -66,7 +66,7 @@ export interface W3CAnnotation<TBody = W3CAnnotationBody> {
|
|
|
66
66
|
/**
|
|
67
67
|
* Shape an adapter's `load()`/`hydrate()` may return. Beyond a stored
|
|
68
68
|
* annotation it may carry the internal skeleton markers the plugin reads exactly
|
|
69
|
-
* once and strips before anything enters the cache
|
|
69
|
+
* once and strips before anything enters the cache:
|
|
70
70
|
* `__fullBodyLoaded: false` signals a skeleton whose body must be fetched via
|
|
71
71
|
* `hydrate()`. These markers are NOT part of the stored annotation contract —
|
|
72
72
|
* they never round-trip — so they live here rather than on {@link W3CAnnotation}.
|
package/dist/catalog.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { LocaleCatalog } from '@triiiceratops/plugin-sdk';
|
|
2
2
|
/**
|
|
3
3
|
* The plugin's package-owned localization catalog (CONTEXT.md **Active locale**).
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* required fallback; a missing key resolves to `en` and then to the key itself.
|
|
4
|
+
* The catalog ships with (and evolves with) the plugin, so core's catalogs carry
|
|
5
|
+
* no plugin keys. `en` is the required fallback; a missing key resolves to `en`
|
|
6
|
+
* and then to the key itself.
|
|
8
7
|
*/
|
|
9
8
|
export declare const catalog: LocaleCatalog;
|
package/dist/contextKey.d.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `VIEWER_STATE_KEY` carries the owning viewer's state (the live `ViewerState`,
|
|
5
5
|
* or the reactive mirror `view.mount` builds so the plugin's own Svelte runtime
|
|
6
|
-
* tracks cross-realm state changes) to the controller
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* tracks cross-realm state changes) to the controller, which reads it with
|
|
7
|
+
* `getContext(VIEWER_STATE_KEY)`. Package-local so the plugin never imports
|
|
8
|
+
* core internals.
|
|
9
9
|
*/
|
|
10
10
|
export declare const VIEWER_STATE_KEY: unique symbol;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { PluginContext } from '@triiiceratops/plugin-sdk';
|
|
2
|
+
import type { AnnotationStore } from './AnnotationStore.svelte';
|
|
3
|
+
import type { DrawingSession } from './drawingSession.svelte';
|
|
4
|
+
import type { TFn } from './i18n.svelte';
|
|
5
|
+
import type { MirroredViewerState } from './viewerMirror.svelte';
|
|
6
|
+
/** Marks the container so a spec can find it. */
|
|
7
|
+
export declare const DRAWING_LAYER_CLASS = "tri-annotation-drawing-layer";
|
|
8
|
+
/** The layer's name within this plugin's id namespace. */
|
|
9
|
+
export declare const DRAWING_LAYER_NAME = "drawing";
|
|
10
|
+
/**
|
|
11
|
+
* Register the drawing layer with the owning viewer. Returns the registry's
|
|
12
|
+
* idempotent dispose, which is a no-op if the registration was refused.
|
|
13
|
+
*
|
|
14
|
+
* One layer, not several: cross-plugin ordering cannot be coordinated, so
|
|
15
|
+
* internal stacking is `z-index` on this container's own children.
|
|
16
|
+
*/
|
|
17
|
+
export declare function registerDrawingLayer(context: PluginContext, surface: {
|
|
18
|
+
session: DrawingSession;
|
|
19
|
+
store: AnnotationStore;
|
|
20
|
+
viewerState: MirroredViewerState;
|
|
21
|
+
t: TFn;
|
|
22
|
+
}): () => void;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The arming state the panel and the drawing layer share.
|
|
3
|
+
*
|
|
4
|
+
* The two live in different component trees — the panel is mounted into core's
|
|
5
|
+
* plugin surface, the layer into the overlay-layer container — so the tool the
|
|
6
|
+
* reader armed reaches the layer through this object rather than through props.
|
|
7
|
+
* Reactive so the layer's `pointer-events` follow the panel's mode toggle
|
|
8
|
+
* without either side polling.
|
|
9
|
+
*/
|
|
10
|
+
import type { W3CAnnotation } from './adapters/types';
|
|
11
|
+
import type { DrawingTool } from './types';
|
|
12
|
+
export declare class DrawingSession {
|
|
13
|
+
/**
|
|
14
|
+
* The armed tool, or `null` while the reader is in edit mode — arming is
|
|
15
|
+
* modal, and this is what the layer's whole-surface `pointer-events: auto`
|
|
16
|
+
* is gated on.
|
|
17
|
+
*/
|
|
18
|
+
armedTool: DrawingTool | null;
|
|
19
|
+
/**
|
|
20
|
+
* The persisted annotation open for editing, or `null`. The panel decides
|
|
21
|
+
* it — a tap on a shape reaches core's edit bus, which the controller owns
|
|
22
|
+
* — and the drawing layer draws its shape and its handles, which is what
|
|
23
|
+
* makes core's own rendering of that one annotation stand down.
|
|
24
|
+
*/
|
|
25
|
+
editingAnnotationId: string | null;
|
|
26
|
+
/**
|
|
27
|
+
* Set by the controller when an edit was opened by TAPPING the shape on the
|
|
28
|
+
* image, and cleared by the drawing layer once focus is on it.
|
|
29
|
+
*
|
|
30
|
+
* A tap leaves focus wherever it was — core's own shape is removed the
|
|
31
|
+
* instant the editor takes the rendering over, so the document is left
|
|
32
|
+
* focused on nothing. Every keyboard verb the open shape has (nudge,
|
|
33
|
+
* commit, delete, the vertex keys) is bound on the shape, so until focus
|
|
34
|
+
* reaches it a reader who taps a shape has no keyboard at all.
|
|
35
|
+
*
|
|
36
|
+
* Only the tap. Selecting the annotation from the panel's LIST opens the
|
|
37
|
+
* same edit, and pulling focus out of the list onto the image would take
|
|
38
|
+
* the reader off the control they are actually working in.
|
|
39
|
+
*/
|
|
40
|
+
focusOnEdit: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Set by the controller so a committed shape opens its body editor. Not
|
|
43
|
+
* reactive state: it is wiring, replaced only when the controller mounts.
|
|
44
|
+
*/
|
|
45
|
+
onCreated: ((annotation: W3CAnnotation) => void) | null;
|
|
46
|
+
/**
|
|
47
|
+
* The host's `extension.prepareDraft` / `prepareAnnotation` hook, applied
|
|
48
|
+
* to a NEW annotation before its first save. Set by the controller, which
|
|
49
|
+
* is where the config and the runtime context the hook is handed live;
|
|
50
|
+
* called by the drawing layer, which is where a shape becomes an
|
|
51
|
+
* annotation.
|
|
52
|
+
*/
|
|
53
|
+
prepareDraft: ((annotation: W3CAnnotation) => W3CAnnotation) | null;
|
|
54
|
+
/**
|
|
55
|
+
* The host's `extension.beforeSave` hook, applied to every annotation on
|
|
56
|
+
* its way to the store — a create, a reshape and a body save alike, so a
|
|
57
|
+
* host that rewrites annotations sees all three.
|
|
58
|
+
*/
|
|
59
|
+
beforeSave: ((annotation: W3CAnnotation) => Promise<W3CAnnotation>) | null;
|
|
60
|
+
/**
|
|
61
|
+
* Create a whole-canvas annotation on the store's current canvas. Set by
|
|
62
|
+
* the drawing layer, called by the controller: the whole-canvas tool has no
|
|
63
|
+
* gesture, so the panel activating it IS the create, but the create itself
|
|
64
|
+
* belongs beside every other one so id reconciliation and the handoff to
|
|
65
|
+
* the body editor are not written twice.
|
|
66
|
+
*/
|
|
67
|
+
createWholeCanvasAnnotation: (() => void) | null;
|
|
68
|
+
/**
|
|
69
|
+
* Place a tool's default shape at the centre of the current view, ready for
|
|
70
|
+
* the ordinary editing verbs to move and size. Set by the drawing layer,
|
|
71
|
+
* called by the controller when a tool is activated from the KEYBOARD.
|
|
72
|
+
*
|
|
73
|
+
* Creation is place-then-shape: a keyboard user has no cursor to describe a
|
|
74
|
+
* region with, so the tool supplies a starting geometry rather than the
|
|
75
|
+
* layer growing a second, keyboard-only creation path.
|
|
76
|
+
*/
|
|
77
|
+
placeDefaultShape: ((tool: DrawingTool) => void) | null;
|
|
78
|
+
/**
|
|
79
|
+
* Delete the annotation open for editing — the Delete key's counterpart to
|
|
80
|
+
* the panel's own delete button, and the same confirmation with it. Set by
|
|
81
|
+
* the controller, called by the drawing layer.
|
|
82
|
+
*/
|
|
83
|
+
requestDelete: (() => void) | null;
|
|
84
|
+
/**
|
|
85
|
+
* Escape's way back out of an open edit — the only way out there is, since
|
|
86
|
+
* the body editor offers no cancel of its own. Set by the controller for the
|
|
87
|
+
* same reason as {@link requestDisarm}: the open edit is DERIVED from the
|
|
88
|
+
* panel's selection, so the layer clearing `editingAnnotationId` itself
|
|
89
|
+
* would be overwritten by the next change to that selection.
|
|
90
|
+
*/
|
|
91
|
+
requestCancelEdit: (() => void) | null;
|
|
92
|
+
/**
|
|
93
|
+
* Escape's way back out of the armed state. The layer cannot simply clear
|
|
94
|
+
* `armedTool`: arming is DERIVED in the controller from create mode and the
|
|
95
|
+
* selected tool, so a write here would be overwritten by the next change to
|
|
96
|
+
* either, and the panel would meanwhile still show the tool lit. Disarming
|
|
97
|
+
* has to happen at the state the panel renders from, which is the
|
|
98
|
+
* controller's.
|
|
99
|
+
*/
|
|
100
|
+
requestDisarm: (() => void) | null;
|
|
101
|
+
}
|