@orbat-mapper/tactical-draw 0.2.0-alpha.0 → 0.2.0-alpha.10
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 +12 -12
- package/dist/index.d.mts +450 -32
- package/dist/index.mjs +1814 -505
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
# @orbat-mapper/tactical-draw
|
|
2
2
|
|
|
3
|
-
The engine-agnostic draw/edit core for
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
`EditPointerDriver` pointer contract, handle rendering, interaction styles, and
|
|
8
|
-
pick helpers.
|
|
3
|
+
The engine-agnostic draw/edit core for `@orbat-mapper` tactical graphics:
|
|
4
|
+
draw and edit controllers, sessions and draw rules, the `MapAdapter` contract
|
|
5
|
+
and base adapter, pixel-space hit-testing, handle rendering, interaction
|
|
6
|
+
styles, and pick helpers.
|
|
9
7
|
|
|
10
|
-
It depends
|
|
11
|
-
concrete map engine
|
|
12
|
-
|
|
8
|
+
It only depends on `@orbat-mapper/control-measures` and `geojson`, with no
|
|
9
|
+
concrete map engine in sight. Bring your own: pair this package with one of
|
|
10
|
+
the engine adapters, or implement `MapAdapter` against whatever you're
|
|
11
|
+
already using.
|
|
13
12
|
|
|
14
13
|
## Installation
|
|
15
14
|
|
|
@@ -86,9 +85,10 @@ if (session && "canCommit" in session) {
|
|
|
86
85
|
|
|
87
86
|
## Bring your own engine
|
|
88
87
|
|
|
89
|
-
Implement `MapAdapter` (or extend `BaseMapAdapter`) against your map engine
|
|
90
|
-
hand it to `TacticalDraw`.
|
|
91
|
-
|
|
88
|
+
Implement `MapAdapter` (or extend `BaseMapAdapter`) against your map engine
|
|
89
|
+
and hand it to `TacticalDraw`. The controllers, sessions, gestures, and
|
|
90
|
+
handle rendering above the adapter don't need to know or care which engine
|
|
91
|
+
is underneath.
|
|
92
92
|
|
|
93
93
|
## License
|
|
94
94
|
|
package/dist/index.d.mts
CHANGED
|
@@ -165,6 +165,15 @@ interface MapAdapter {
|
|
|
165
165
|
onPick(layerId: LayerId, handler: PickHandler, opts?: PickOptions): () => void;
|
|
166
166
|
/**
|
|
167
167
|
* Map Event Listeners
|
|
168
|
+
*
|
|
169
|
+
* Ordering contract: a `"click"` handler and the `onPick` handlers for the
|
|
170
|
+
* same user click MUST observe the pick first — adapters deliver picks and
|
|
171
|
+
* `"click"` from the same underlying engine event, in registration order.
|
|
172
|
+
* The draw/edit controllers register their pick subscriptions before their
|
|
173
|
+
* click handler and read the stashed pick inside it (click-away close /
|
|
174
|
+
* close-then-pick re-dispatch); an adapter that fires `"click"` from an
|
|
175
|
+
* earlier engine event than its picks breaks that contract (e.g. OL
|
|
176
|
+
* `click` vs `singleclick`).
|
|
168
177
|
*/
|
|
169
178
|
on(eventType: "click" | "pointermove" | "dblclick", handler: MapEventHandler): void;
|
|
170
179
|
off(eventType: "click" | "pointermove" | "dblclick", handler: MapEventHandler): void;
|
|
@@ -437,9 +446,10 @@ declare function getSizeAnchor(measure: Pick<ControlMeasure, "kind" | "options">
|
|
|
437
446
|
//#endregion
|
|
438
447
|
//#region src/tactical-draw/handles.d.ts
|
|
439
448
|
/**
|
|
440
|
-
* Line-only style surface for the rubber-band guide (ADR-0004)
|
|
441
|
-
*
|
|
442
|
-
*
|
|
449
|
+
* Line-only style surface for the rubber-band guide (ADR-0004) and the
|
|
450
|
+
* transform-box outline. Slot inside `InteractionStyle.guide` /
|
|
451
|
+
* `InteractionStyle.boxOutline`. Applied as a layer style for the guide, and
|
|
452
|
+
* per-feature (`properties.style`) for the box outline — see ADR-0017.
|
|
443
453
|
*/
|
|
444
454
|
interface GuideStyle {
|
|
445
455
|
strokeColor?: string;
|
|
@@ -447,25 +457,38 @@ interface GuideStyle {
|
|
|
447
457
|
strokeDash?: readonly number[];
|
|
448
458
|
}
|
|
449
459
|
/**
|
|
450
|
-
* Unified interaction-affordance style surface (ADR-0006
|
|
451
|
-
* for
|
|
452
|
-
*
|
|
460
|
+
* Unified interaction-affordance style surface (ADR-0006, extended by
|
|
461
|
+
* ADR-0017 for the transform box). One option object for the guide line,
|
|
462
|
+
* the box outline, and every handle slot. `guide` and `boxOutline` are line
|
|
463
|
+
* styles; handle slots ride per-feature on `properties.style`.
|
|
453
464
|
*/
|
|
454
465
|
interface InteractionStyle {
|
|
455
466
|
guide?: GuideStyle;
|
|
456
467
|
vertexHandle?: HandleStyle;
|
|
457
468
|
midpointHandle?: HandleStyle;
|
|
458
|
-
|
|
469
|
+
scaleHandle?: HandleStyle;
|
|
459
470
|
rotateHandle?: HandleStyle;
|
|
471
|
+
boxOutline?: GuideStyle;
|
|
472
|
+
/**
|
|
473
|
+
* Pixel padding added on every side of the fitted transform box
|
|
474
|
+
* (`fitTransformBox`, ADR-0017) so the box doesn't tightly hug the rendered
|
|
475
|
+
* geometry. Default `0`. A scalar, not a style object, but it participates
|
|
476
|
+
* in the same built-in → façade → per-call resolution slot system as every
|
|
477
|
+
* other `InteractionStyle` field (see `TacticalDraw.resolveInteractionStyle`).
|
|
478
|
+
*/
|
|
479
|
+
boxPadding?: number;
|
|
460
480
|
}
|
|
461
481
|
/**
|
|
462
|
-
* Reserved enum from PRD #40
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
482
|
+
* Reserved enum from PRD #40, replaced under ADR-0017 by the transform-box UI.
|
|
483
|
+
* `reshape` renders vertex/midpoint handles; `transform` renders the oriented
|
|
484
|
+
* selection box (4 corner scale handles + a rotate grip + drag-anywhere
|
|
485
|
+
* body translate) via `transform-box.ts`. The two can be combined freely —
|
|
486
|
+
* single-measure edits show both by default. `delete` is an explicit
|
|
487
|
+
* destructive mode: it renders the vertex handles (so they can be tapped to
|
|
488
|
+
* remove) but no midpoints or box, and the edit controller deletes the tapped
|
|
489
|
+
* vertex instead of dragging it.
|
|
467
490
|
*/
|
|
468
|
-
type EditMode = "reshape" | "
|
|
491
|
+
type EditMode = "reshape" | "transform" | "delete";
|
|
469
492
|
//#endregion
|
|
470
493
|
//#region src/tactical-draw/edit-session.d.ts
|
|
471
494
|
/**
|
|
@@ -495,16 +518,20 @@ interface EditSession {
|
|
|
495
518
|
*/
|
|
496
519
|
readonly sizeAnchor: SizeAnchor;
|
|
497
520
|
/**
|
|
498
|
-
*
|
|
499
|
-
*
|
|
521
|
+
* Commit: returns the `ControlMeasureSnapshot` of the current working state
|
|
522
|
+
* and resolves the outer `edit` promise with the same value (the same
|
|
523
|
+
* reference — no double-bake of ground-anchored sizes). Returns `undefined`
|
|
524
|
+
* after the session has settled (idempotent no-op).
|
|
500
525
|
*/
|
|
501
|
-
close():
|
|
526
|
+
close(): ControlMeasureSnapshot | undefined;
|
|
502
527
|
/** Reject the outer `edit` promise with reason `"session"`. */
|
|
503
528
|
abort(): void;
|
|
504
529
|
/**
|
|
505
|
-
* Switch the active edit-mode set. `"reshape"
|
|
506
|
-
*
|
|
507
|
-
*
|
|
530
|
+
* Switch the active edit-mode set. `"reshape"` produces vertex/midpoint
|
|
531
|
+
* handles; `"transform"` renders the oriented selection box (corner scale
|
|
532
|
+
* handles, rotate grip, body translate — ADR-0017); `"delete"` is exclusive
|
|
533
|
+
* (vertex-tap removal) and collapses the set to delete-only. An unknown mode
|
|
534
|
+
* is rejected with `TypeError`.
|
|
508
535
|
*/
|
|
509
536
|
setModes(modes: readonly EditMode[]): void;
|
|
510
537
|
/** Shallow-merge per-measure generator options into the live working copy. */
|
|
@@ -558,6 +585,208 @@ interface EditChangeEvent {
|
|
|
558
585
|
abort(): void;
|
|
559
586
|
}
|
|
560
587
|
//#endregion
|
|
588
|
+
//#region src/tactical-draw/transform-session.d.ts
|
|
589
|
+
/**
|
|
590
|
+
* The reason a `TransformSession` settled, delivered to [[TransformSession.onSettled]].
|
|
591
|
+
* `"close"` covers every commit path — `close()`, `setMembers([])`, a
|
|
592
|
+
* click-away close, or `TransformChangeEvent.close()`. `"error"` is the
|
|
593
|
+
* close-time snapshot build throwing (the `editMany` promise rejected with
|
|
594
|
+
* that error, not a `TacticalDrawAbortError`). The remaining values mirror
|
|
595
|
+
* `TacticalDrawAbortError.reason` exactly: `"escape"`, `"signal"`, `"session"`
|
|
596
|
+
* (`session.abort()` / `event.abort()`), `"preempted"`, `"destroyed"`, and
|
|
597
|
+
* `"removed"` (a member dropped from `td.render()`'s measure set).
|
|
598
|
+
*/
|
|
599
|
+
type SettleReason = "close" | "error" | TacticalDrawAbortReason;
|
|
600
|
+
/**
|
|
601
|
+
* Surface delivered via `td.editMany(measures, opts).onSession` and
|
|
602
|
+
* synchronously available from `td.activeTransformSession` while the group
|
|
603
|
+
* transform is live. The group sibling of `EditSession` (`edit-session.ts`):
|
|
604
|
+
* no reshape, no per-measure `setOptions`/`setStyle` — only the shared
|
|
605
|
+
* transform box's corner-scale / rotate / body-translate gestures, applied to
|
|
606
|
+
* every member at once. See the Phase 3 ("Multi-select") slice of ADR-0017.
|
|
607
|
+
*
|
|
608
|
+
* The member set is mutable via [[setMembers]] (#187): members can join or
|
|
609
|
+
* leave a live session without a session boundary. Because membership changes
|
|
610
|
+
* over the session's lifetime, snapshot arrays are keyed by **measure id**, not
|
|
611
|
+
* by index — a snapshot's identity is `snapshot.measure.id`. `measures` is a
|
|
612
|
+
* live view of the current members' join-time inputs; a `TransformChangeEvent`'s
|
|
613
|
+
* arrays are index-aligned with `session.measures` *as of that emission*. After
|
|
614
|
+
* the session settles (`close` / `abort` / an emptying `setMembers`), every
|
|
615
|
+
* method is a silent no-op (except [[onSettled]] — late registration still
|
|
616
|
+
* delivers, asynchronously).
|
|
617
|
+
*/
|
|
618
|
+
interface TransformSession {
|
|
619
|
+
/**
|
|
620
|
+
* Live view of the current members' join-time input measures — a fresh array
|
|
621
|
+
* on each read, in current membership order. Grows/shrinks as [[setMembers]]
|
|
622
|
+
* adds or removes members. Not stable across membership changes: key
|
|
623
|
+
* snapshots by `snapshot.measure.id` rather than relying on index alignment.
|
|
624
|
+
*/
|
|
625
|
+
readonly measures: readonly ControlMeasure[];
|
|
626
|
+
/**
|
|
627
|
+
* Live working control points for the member whose id is `measureId`, or
|
|
628
|
+
* `undefined` when no member carries that id. Copy on read.
|
|
629
|
+
*/
|
|
630
|
+
controlPointsFor(measureId: string): readonly Position[] | undefined;
|
|
631
|
+
/** `true` when any member's working points differ from its edit-start input beyond tolerance. */
|
|
632
|
+
readonly dirty: boolean;
|
|
633
|
+
/**
|
|
634
|
+
* Live size anchor, applied group-wide (ADR-0020). `"ground"` bakes each
|
|
635
|
+
* member's pixel size to meters on commit; `"screen"` keeps it zoom-aware.
|
|
636
|
+
* Initialised from `EditManyOptions.sizeAnchor` and mutated by [[setSizeAnchor]].
|
|
637
|
+
*/
|
|
638
|
+
readonly sizeAnchor: SizeAnchor;
|
|
639
|
+
/** Switch the group's size anchor (ADR-0020). Takes effect on the next emitted snapshot and on `close()`. */
|
|
640
|
+
setSizeAnchor(anchor: SizeAnchor): void;
|
|
641
|
+
/**
|
|
642
|
+
* Replace the member set in place — the primitive behind shift-click toggle,
|
|
643
|
+
* marquee replace, and single-edit → group folding, without tearing the
|
|
644
|
+
* session down (#187). Members are matched to the current set by measure id:
|
|
645
|
+
*
|
|
646
|
+
* - **Retained** members (id in both the current and next sets) carry their
|
|
647
|
+
* current working state forward; the passed measure object is ignored for
|
|
648
|
+
* them. Their `dirty` baseline stays their original join-time input, so a
|
|
649
|
+
* session that has already moved things stays dirty across the call.
|
|
650
|
+
* - **Removed** members (id in the current set, absent from the next) are
|
|
651
|
+
* committed at their current working state.
|
|
652
|
+
* - **Added** members (id only in the next set) join at their passed input
|
|
653
|
+
* state; if currently on the graphics layer they are lifted off it exactly
|
|
654
|
+
* like initial members.
|
|
655
|
+
*
|
|
656
|
+
* The transform box re-derives from the next member set at the session's
|
|
657
|
+
* current box angle. Session-scoped state (`sizeAnchor`, box angle,
|
|
658
|
+
* non-geometry dirty) is unchanged.
|
|
659
|
+
*
|
|
660
|
+
* **Return value** — the committed exit snapshots of the members removed *by
|
|
661
|
+
* this call*, built exactly like [[close]]'s snapshots (pixel size baked to
|
|
662
|
+
* ground unless `sizeAnchor` is `"screen"`, then cloned and rendered); an
|
|
663
|
+
* empty array when nothing was removed. The outer `editMany` promise resolves
|
|
664
|
+
* with the snapshots of the members in the set **at close time only**.
|
|
665
|
+
* Removed members are NOT written back to the graphics layer by the
|
|
666
|
+
* controller — the host folds the returned snapshots into its document and
|
|
667
|
+
* calls `td.render()`, the same contract as [[close]]. A removed member's
|
|
668
|
+
* exit snapshot is also delivered through [[onCommit]] — the recommended
|
|
669
|
+
* channel, since it covers this return value and the close-time promise
|
|
670
|
+
* resolution through a single subscription. Returns `undefined` (a silent
|
|
671
|
+
* no-op) after the session has settled.
|
|
672
|
+
*
|
|
673
|
+
* `setMembers([])` on a live session delegates to [[close]]: it commits every
|
|
674
|
+
* current member, resolves the promise with their snapshots, and returns that
|
|
675
|
+
* same array. Duplicate ids in `measures` throw `TypeError` synchronously,
|
|
676
|
+
* without mutating session state.
|
|
677
|
+
*/
|
|
678
|
+
setMembers(measures: readonly ControlMeasure[]): ControlMeasureSnapshot[] | undefined;
|
|
679
|
+
/**
|
|
680
|
+
* Commit: returns the members' `ControlMeasureSnapshot[]` (the members in the
|
|
681
|
+
* set at close time) and resolves the outer `editMany` promise with the same
|
|
682
|
+
* value (the same reference — no double-bake of ground-anchored sizes). A
|
|
683
|
+
* member removed mid-session via [[setMembers]] is not included here; its exit
|
|
684
|
+
* snapshot was returned from that `setMembers` call (and delivered to
|
|
685
|
+
* [[onCommit]] at that time). Returns `undefined` after the session has
|
|
686
|
+
* settled (idempotent no-op).
|
|
687
|
+
*/
|
|
688
|
+
close(): ControlMeasureSnapshot[] | undefined;
|
|
689
|
+
/** Reject the outer `editMany` promise with reason `"session"`. */
|
|
690
|
+
abort(): void;
|
|
691
|
+
/**
|
|
692
|
+
* Subscribe to working-copy changes. The listener receives a
|
|
693
|
+
* [[TransformChangeEvent]] for each completed group gesture (corner scale,
|
|
694
|
+
* rotate, or body-translate drag end) — never on rubber-band ticks. All
|
|
695
|
+
* currently-registered listeners receive the same event instance, in
|
|
696
|
+
* registration order.
|
|
697
|
+
*
|
|
698
|
+
* Listener exceptions are collected; after every listener has run, they are
|
|
699
|
+
* rethrown (one if single, aggregated otherwise). `event.reject()` /
|
|
700
|
+
* `event.close()` / `event.abort()` take effect only after all listeners
|
|
701
|
+
* have run — mirrors `EditSession.onChange` (issue #49).
|
|
702
|
+
*/
|
|
703
|
+
onChange(listener: (event: TransformChangeEvent) => void): () => void;
|
|
704
|
+
/**
|
|
705
|
+
* Subscribe to committed exit-snapshot batches — the single delivery channel
|
|
706
|
+
* for every `ControlMeasureSnapshot[]` this session ever commits (#191). Fires
|
|
707
|
+
* exactly once per batch:
|
|
708
|
+
*
|
|
709
|
+
* - The members [[setMembers]] removes, but only when that call actually
|
|
710
|
+
* removed at least one member.
|
|
711
|
+
* - The close-time members on any commit path: [[close]], `setMembers([])`,
|
|
712
|
+
* a click-away close, or `TransformChangeEvent.close()`.
|
|
713
|
+
*
|
|
714
|
+
* Never fires on an abort/reject path — pair it with [[onSettled]] to also
|
|
715
|
+
* observe abort settles. Fires synchronously from within the
|
|
716
|
+
* `setMembers`/`close` call that produced the batch, before that call
|
|
717
|
+
* returns, and the array delivered here is the same reference that call
|
|
718
|
+
* returns (and, for the close batch, the same reference the `editMany`
|
|
719
|
+
* promise resolves with). This is the recommended single fold path for exit
|
|
720
|
+
* state — subscribe once in `onSession` and every committed snapshot, from
|
|
721
|
+
* either delivery site, arrives here exactly once; the `setMembers` return
|
|
722
|
+
* value and the promise resolution are unchanged and remain valid for
|
|
723
|
+
* backward compatibility.
|
|
724
|
+
*
|
|
725
|
+
* Listener exceptions are collected; after every listener has run, they are
|
|
726
|
+
* rethrown (one if single, `AggregateError` otherwise) — mirrors
|
|
727
|
+
* [[onChange]]. Returns an unsubscribe function; registering after the
|
|
728
|
+
* session has settled returns a no-op unsubscribe and the listener never
|
|
729
|
+
* fires.
|
|
730
|
+
*/
|
|
731
|
+
onCommit(handler: (snapshots: ControlMeasureSnapshot[]) => void): () => void;
|
|
732
|
+
/**
|
|
733
|
+
* Subscribe to the session's settlement — the lifecycle complement of
|
|
734
|
+
* [[onCommit]]: `onCommit` delivers *what* was committed, `onSettled` says
|
|
735
|
+
* the session is *over* and why (#194). Fires exactly once, when the
|
|
736
|
+
* session settles by any path, commit or abort, with a [[SettleReason]].
|
|
737
|
+
*
|
|
738
|
+
* - On commit paths it fires **after** the close batch's `onCommit`
|
|
739
|
+
* emission, so a listener that clears selection state observes a document
|
|
740
|
+
* that already contains the exit snapshots.
|
|
741
|
+
* - On every path it fires **after** the facade has already cleared its
|
|
742
|
+
* slot — `td.activeTransformSession` is `null` and the members are no
|
|
743
|
+
* longer excluded from `td.render()` reconciliation, so fold-then-render
|
|
744
|
+
* works from inside the handler.
|
|
745
|
+
* - `reason` distinguishes commit from abort but is deliberately coarse —
|
|
746
|
+
* no payload; snapshots were already delivered through [[onCommit]].
|
|
747
|
+
*
|
|
748
|
+
* Registering after the session has already settled is **not** a no-op
|
|
749
|
+
* (unlike [[onChange]] / [[onCommit]]): the handler is invoked
|
|
750
|
+
* asynchronously (on a microtask) exactly once with the settle reason;
|
|
751
|
+
* unsubscribing before that microtask runs cancels the delivery.
|
|
752
|
+
*
|
|
753
|
+
* Listener exceptions are collected; after every listener has run, they are
|
|
754
|
+
* rethrown (one if single, `AggregateError` otherwise) — mirrors
|
|
755
|
+
* [[onChange]] / [[onCommit]]. Returns an unsubscribe function.
|
|
756
|
+
*/
|
|
757
|
+
onSettled(handler: (reason: SettleReason) => void): () => void;
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Delivered to `TransformSession.onChange` listeners once per completed group
|
|
761
|
+
* gesture. Both `measures` and `previous` are index-aligned with
|
|
762
|
+
* `session.measures` *as of this emission*. Key by `measure.id` rather than
|
|
763
|
+
* relying on index alignment across emissions. A `setMembers` call from within
|
|
764
|
+
* a listener takes effect immediately and supersedes this emission's
|
|
765
|
+
* `reject()` (the rollback baseline no longer matches the member set);
|
|
766
|
+
* `close()` and `abort()` still apply, against the new membership. See
|
|
767
|
+
* `EditChangeEvent` for the identical emission contract this mirrors.
|
|
768
|
+
*/
|
|
769
|
+
interface TransformChangeEvent {
|
|
770
|
+
/** Working state after the completed gesture, index-aligned with `session.measures` as of this emission. */
|
|
771
|
+
readonly measures: ControlMeasureSnapshot[];
|
|
772
|
+
/** State immediately before this gesture, index-aligned with `session.measures` as of this emission. */
|
|
773
|
+
readonly previous: ControlMeasureSnapshot[];
|
|
774
|
+
/** The session the event belongs to. */
|
|
775
|
+
readonly session: TransformSession;
|
|
776
|
+
/**
|
|
777
|
+
* Roll every member's working preview back to `previous` after all
|
|
778
|
+
* listeners have run. Multiple calls collapse to a single rollback.
|
|
779
|
+
* Overridden by `close()` or `abort()` if either is also called during this
|
|
780
|
+
* emission, and dropped if a listener called `session.setMembers` (the
|
|
781
|
+
* rollback baseline no longer matches the member set).
|
|
782
|
+
*/
|
|
783
|
+
reject(): void;
|
|
784
|
+
/** Shortcut for `event.session.close()`, deferred until the emission ends. */
|
|
785
|
+
close(): void;
|
|
786
|
+
/** Shortcut for `event.session.abort()`, deferred until the emission ends. */
|
|
787
|
+
abort(): void;
|
|
788
|
+
}
|
|
789
|
+
//#endregion
|
|
561
790
|
//#region src/tactical-draw/session.d.ts
|
|
562
791
|
/**
|
|
563
792
|
* Scoped surface delivered via `td.draw({ onSession })` for variable-length
|
|
@@ -637,11 +866,12 @@ interface EditOptions {
|
|
|
637
866
|
*/
|
|
638
867
|
closeOnClickAway?: boolean;
|
|
639
868
|
/**
|
|
640
|
-
* Active edit modes. Defaults to `["reshape"]`. `reshape`
|
|
641
|
-
* midpoint handles; `
|
|
642
|
-
*
|
|
643
|
-
*
|
|
644
|
-
* `"
|
|
869
|
+
* Active edit modes. Defaults to `["reshape", "transform"]`. `reshape`
|
|
870
|
+
* renders vertex / midpoint handles; `transform` renders the oriented
|
|
871
|
+
* selection box — 4 corner scale handles, a rotate grip above the top edge,
|
|
872
|
+
* and drag-anywhere-inside-the-box translate (ADR-0017). The two can be
|
|
873
|
+
* combined freely (the default does). `"delete"` is exclusive: it collapses
|
|
874
|
+
* the set to vertex-tap removal only. An unknown mode throws `TypeError`.
|
|
645
875
|
*/
|
|
646
876
|
modes?: readonly EditMode[];
|
|
647
877
|
/**
|
|
@@ -655,8 +885,10 @@ interface EditOptions {
|
|
|
655
885
|
* polyline sourced from the working control points alongside the working
|
|
656
886
|
* geometry preview. For `geometry === "area"` kinds, a separate
|
|
657
887
|
* closing-segment `LineString` is also emitted once `workingPoints.length >= 3`.
|
|
658
|
-
* The guide is suppressed mechanically when `geometry === "point"
|
|
659
|
-
* `minCoordinates === maxCoordinates
|
|
888
|
+
* The guide is suppressed mechanically when `geometry === "point"`, and for
|
|
889
|
+
* fixed-length kinds (`minCoordinates === maxCoordinates` or `canonicalPointCount`
|
|
890
|
+
* set) unless the kind's draw rule opts back in via `showGuide`, regardless of
|
|
891
|
+
* this flag. See ADR-0004.
|
|
660
892
|
*/
|
|
661
893
|
guide?: boolean;
|
|
662
894
|
/**
|
|
@@ -669,6 +901,32 @@ interface EditOptions {
|
|
|
669
901
|
*/
|
|
670
902
|
sizeAnchor?: SizeAnchor;
|
|
671
903
|
}
|
|
904
|
+
/**
|
|
905
|
+
* Options for `TacticalDraw.editMany` — the multi-select group transform
|
|
906
|
+
* (ADR-0017 "Multi-select" slice). Leaner than `EditOptions`: there is no
|
|
907
|
+
* `modes` (a group transform is always the oriented box — no reshape), no
|
|
908
|
+
* `guide` (no rubber-band content for a group), and no per-measure `setOptions`
|
|
909
|
+
* / `setStyle` surface on the resulting session.
|
|
910
|
+
*/
|
|
911
|
+
interface EditManyOptions {
|
|
912
|
+
signal?: AbortSignal;
|
|
913
|
+
/**
|
|
914
|
+
* Called synchronously once when the group transform starts, before any
|
|
915
|
+
* pointer event is processed. Lets a host wire Done/Cancel/Delete buttons to
|
|
916
|
+
* the active session.
|
|
917
|
+
*/
|
|
918
|
+
onSession?: (session: TransformSession) => void;
|
|
919
|
+
/**
|
|
920
|
+
* Default `true`. When `true`, a click on empty map (or on a graphics-layer
|
|
921
|
+
* measure outside the group) closes the active transform. Clicks on preview
|
|
922
|
+
* / handle features are consumed and keep it open.
|
|
923
|
+
*/
|
|
924
|
+
closeOnClickAway?: boolean;
|
|
925
|
+
/** Per-call interaction style override scoped to this transform. See ADR-0006. */
|
|
926
|
+
interactionStyle?: InteractionStyle;
|
|
927
|
+
/** Size anchor for the committed measures (ADR-0020), applied group-wide. Default `"ground"`. */
|
|
928
|
+
sizeAnchor?: SizeAnchor;
|
|
929
|
+
}
|
|
672
930
|
/**
|
|
673
931
|
* Draft control measure handed to `TacticalDraw.draw`. `kind` is required up
|
|
674
932
|
* front so the façade can resolve metadata, draw rule, and controller choice
|
|
@@ -699,8 +957,10 @@ interface DrawOptions {
|
|
|
699
957
|
* polyline alongside the kind's generator preview. For `geometry === "area"`
|
|
700
958
|
* kinds, a separate closing-segment `LineString` is also emitted once the
|
|
701
959
|
* total point count (cursor included) is ≥ 3. The guide is suppressed
|
|
702
|
-
* mechanically when `geometry === "point"
|
|
703
|
-
*
|
|
960
|
+
* mechanically when `geometry === "point"`, and for fixed-length kinds
|
|
961
|
+
* (`minCoordinates === maxCoordinates` or `canonicalPointCount` set) unless
|
|
962
|
+
* the kind's draw rule opts back in via `showGuide`, regardless of this
|
|
963
|
+
* flag. See ADR-0004.
|
|
704
964
|
*/
|
|
705
965
|
guide?: boolean;
|
|
706
966
|
/**
|
|
@@ -746,6 +1006,20 @@ declare class TacticalDraw {
|
|
|
746
1006
|
* `EditHandle` so the controller stays the single owner of edit state.
|
|
747
1007
|
*/
|
|
748
1008
|
private activeEdit;
|
|
1009
|
+
/**
|
|
1010
|
+
* Tracked alongside `activeInteraction` whenever the interaction is a group
|
|
1011
|
+
* transform (`editMany`). The `render()` / view-change paths need every
|
|
1012
|
+
* member's id (to exclude all of them from reconcile) — mirrors `activeEdit`
|
|
1013
|
+
* but for `TransformHandle`'s `measureIds` array.
|
|
1014
|
+
*/
|
|
1015
|
+
private activeTransform;
|
|
1016
|
+
/**
|
|
1017
|
+
* The live `TransformSession`, exposed via `activeTransformSession`. Set
|
|
1018
|
+
* directly by `editMany`'s `onSession` wrapper (not routed through
|
|
1019
|
+
* `ActiveInteractionTracker.session`, whose type is `DrawSession | EditSession`)
|
|
1020
|
+
* and cleared on settle.
|
|
1021
|
+
*/
|
|
1022
|
+
private activeTransformSessionRef;
|
|
749
1023
|
/**
|
|
750
1024
|
* Host pick handlers registered via `td.onPick` / `td.onMeasurePick`. The
|
|
751
1025
|
* façade subscribes once to `adapter.onPick` per layer and routes events
|
|
@@ -764,6 +1038,7 @@ declare class TacticalDraw {
|
|
|
764
1038
|
private deferredGraphicsPick;
|
|
765
1039
|
private idCounter;
|
|
766
1040
|
private editCounter;
|
|
1041
|
+
private transformCounter;
|
|
767
1042
|
/**
|
|
768
1043
|
* Snapshot of the host's last `render()` argument. The edit slice uses it
|
|
769
1044
|
* to recompute the graphics-layer state during a lift / restore — the
|
|
@@ -789,6 +1064,21 @@ declare class TacticalDraw {
|
|
|
789
1064
|
* as in `render()`; the preview's own view-change subscription rescales it.
|
|
790
1065
|
*/
|
|
791
1066
|
private readonly handleViewChange;
|
|
1067
|
+
/**
|
|
1068
|
+
* Every measure id currently lifted onto the preview layer by an active
|
|
1069
|
+
* edit or group transform — excluded from `render()` / view-change
|
|
1070
|
+
* reconciliation because the preview layer (not graphics) owns their
|
|
1071
|
+
* working copy. Empty while idle.
|
|
1072
|
+
*/
|
|
1073
|
+
private excludedInteractionIds;
|
|
1074
|
+
/**
|
|
1075
|
+
* Live member ids of the active group transform, or `undefined` when none.
|
|
1076
|
+
* Falls back to the session ref because `onSession` fires from inside the
|
|
1077
|
+
* controller's attach, before `runWithSlot` has assigned `activeTransform` —
|
|
1078
|
+
* the session ref is registered first (`trackTransformSession`), so a
|
|
1079
|
+
* `render()` from inside `onSession` still sees live membership.
|
|
1080
|
+
*/
|
|
1081
|
+
private transformMemberIds;
|
|
792
1082
|
/**
|
|
793
1083
|
* Reconcile the graphics layer against the authoritative list of measures.
|
|
794
1084
|
* Idempotent. Throws synchronously on duplicate measure ids *before*
|
|
@@ -841,6 +1131,13 @@ declare class TacticalDraw {
|
|
|
841
1131
|
* (exposing it via `activeSession`) before the host's own handler runs.
|
|
842
1132
|
*/
|
|
843
1133
|
private trackSession;
|
|
1134
|
+
/**
|
|
1135
|
+
* Wrap a host `onSession` callback for `editMany`, recording the session on
|
|
1136
|
+
* `activeTransformSessionRef` (exposed via `activeTransformSession`) before
|
|
1137
|
+
* the host's own handler runs. Separate from `trackSession` because
|
|
1138
|
+
* `ActiveInteractionTracker.session` is typed `DrawSession | EditSession`.
|
|
1139
|
+
*/
|
|
1140
|
+
private trackTransformSession;
|
|
844
1141
|
/**
|
|
845
1142
|
* Start an interactive draw. Fixed-length kinds commit automatically on the
|
|
846
1143
|
* last required click. Variable-length kinds surface a `DrawSession` via
|
|
@@ -860,6 +1157,62 @@ declare class TacticalDraw {
|
|
|
860
1157
|
* `false`.
|
|
861
1158
|
*/
|
|
862
1159
|
edit(measure: ControlMeasure, options?: EditOptions): Promise<ControlMeasureSnapshot>;
|
|
1160
|
+
/**
|
|
1161
|
+
* Start an interactive group transform (multi-select) over `measures`
|
|
1162
|
+
* (ADR-0017 "Multi-select" slice). Every member is lifted onto the shared
|
|
1163
|
+
* preview layer; the box (corner scale, rotate grip, drag-anywhere body
|
|
1164
|
+
* translate) applies rigidly to all of them at once. Returns a Promise that
|
|
1165
|
+
* resolves with `ControlMeasureSnapshot[]` on `session.close()` and rejects
|
|
1166
|
+
* with `TacticalDrawAbortError` on signal / Escape / destroy / preempt /
|
|
1167
|
+
* `session.abort()`. Key the resolved snapshots by `snapshot.measure.id`:
|
|
1168
|
+
* the member set is mutable (`session.setMembers`, #187) so snapshot arrays
|
|
1169
|
+
* are id-keyed, not index-aligned with this input. The promise resolves with
|
|
1170
|
+
* the members in the set at close time; a member removed mid-session had its
|
|
1171
|
+
* exit snapshot returned from the `setMembers` call that removed it.
|
|
1172
|
+
*
|
|
1173
|
+
* `measures.length` must be >= 1; a single-element array behaves like an
|
|
1174
|
+
* `edit` with only `transform` mode. Throws `TypeError` synchronously (before
|
|
1175
|
+
* any preemption) for an empty array or duplicate ids.
|
|
1176
|
+
*
|
|
1177
|
+
* For a selection that changes over time (shift-click toggle, marquee
|
|
1178
|
+
* replace), prefer [[syncTransformMembers]] over hand-rolling the
|
|
1179
|
+
* live-session-vs-fresh-`editMany` branch, and subscribe to
|
|
1180
|
+
* `TransformSession.onCommit` as the single fold path for exit snapshots
|
|
1181
|
+
* and `TransformSession.onSettled` as the lifecycle signal for when the
|
|
1182
|
+
* session is over and why.
|
|
1183
|
+
*/
|
|
1184
|
+
editMany(measures: ControlMeasure[], options?: EditManyOptions): Promise<ControlMeasureSnapshot[]>;
|
|
1185
|
+
/**
|
|
1186
|
+
* One-call selection-sync: mutate the live group transform's member set in
|
|
1187
|
+
* place, or start a fresh `editMany` when no transform is live (#191). This
|
|
1188
|
+
* owns the "live session → `setMembers`, else → `editMany`" branch that every
|
|
1189
|
+
* host otherwise hand-rolls against `activeTransformSession`.
|
|
1190
|
+
*
|
|
1191
|
+
* The facade knows session liveness synchronously — `activeTransformSessionRef`
|
|
1192
|
+
* is cleared inside the settle path, before control returns to host code — so
|
|
1193
|
+
* unlike a host's own `activeTransformSession ? setMembers : editMany` check,
|
|
1194
|
+
* this can never observe a stale ref pointing at an already-settled session.
|
|
1195
|
+
* The settled-race fall-through hosts had to hand-roll for that gap is owned
|
|
1196
|
+
* here instead: a `setMembers` call that (implausibly) still returns
|
|
1197
|
+
* `undefined` falls through to a fresh `editMany` rather than silently
|
|
1198
|
+
* dropping the selection.
|
|
1199
|
+
*
|
|
1200
|
+
* `measures: []` closes any live session (committing every member) and is a
|
|
1201
|
+
* no-op while idle. With a live session, `options` is ignored — session-scoped
|
|
1202
|
+
* state (`sizeAnchor`, box angle, close-on-click-away policy) is sticky per
|
|
1203
|
+
* `setMembers`'s contract; `options` applies only when this call starts a
|
|
1204
|
+
* fresh `editMany`. Exit snapshots are delivered via the session's
|
|
1205
|
+
* `onCommit` — subscribe once in `options.onSession`. Session lifecycle —
|
|
1206
|
+
* including an Escape / preempt / destroy abort the host did not itself
|
|
1207
|
+
* initiate — is observed via the session's `onSettled` (#194); together
|
|
1208
|
+
* `onCommit` and `onSettled` make the underlying `editMany` promise
|
|
1209
|
+
* irrelevant to hosts. That promise is owned by the facade; its abort
|
|
1210
|
+
* rejections are swallowed here (non-abort errors surface as unhandled
|
|
1211
|
+
* rejections). Throws `TypeError` synchronously on duplicate ids, from
|
|
1212
|
+
* either path. Starting a fresh transform preempts any other in-flight
|
|
1213
|
+
* draw/edit exactly like `editMany`.
|
|
1214
|
+
*/
|
|
1215
|
+
syncTransformMembers(measures: readonly ControlMeasure[], options?: EditManyOptions): void;
|
|
863
1216
|
/**
|
|
864
1217
|
* Subscribe to picks on committed control measures.
|
|
865
1218
|
*
|
|
@@ -895,6 +1248,13 @@ declare class TacticalDraw {
|
|
|
895
1248
|
* fixed-length draws. Safe to read after `destroy()`.
|
|
896
1249
|
*/
|
|
897
1250
|
get activeSession(): DrawSession | EditSession | null;
|
|
1251
|
+
/**
|
|
1252
|
+
* The live `editMany` group-transform session, or `null` while idle (and
|
|
1253
|
+
* during any other interaction). Separate from `activeSession` because
|
|
1254
|
+
* `TransformSession` isn't part of its `DrawSession | EditSession` union —
|
|
1255
|
+
* see `activeTransformSessionRef`. Safe to read after `destroy()`.
|
|
1256
|
+
*/
|
|
1257
|
+
get activeTransformSession(): TransformSession | null;
|
|
898
1258
|
/**
|
|
899
1259
|
* Tear down the façade. Releases only layer slots the façade created; host
|
|
900
1260
|
* supplied layer ids are left untouched. Safe to call multiple times. After
|
|
@@ -926,9 +1286,19 @@ declare class TacticalDraw {
|
|
|
926
1286
|
private applyGuideStyle;
|
|
927
1287
|
private baseGuideStyle;
|
|
928
1288
|
/**
|
|
929
|
-
* Resolve handle slots across built-in → façade →
|
|
930
|
-
* per-property shallow merge. Explicit `undefined`
|
|
931
|
-
* layer unsets the corresponding lower-priority field
|
|
1289
|
+
* Resolve handle slots (plus `boxOutline`) across built-in → façade →
|
|
1290
|
+
* per-call with per-slot, per-property shallow merge. Explicit `undefined`
|
|
1291
|
+
* at a higher-priority layer unsets the corresponding lower-priority field
|
|
1292
|
+
* (ADR-0006). `boxOutline` rides this same per-feature merge path (not the
|
|
1293
|
+
* `guide` layer-style path) — the edit controller reads it off
|
|
1294
|
+
* `properties.style` on the box outline `LineString` (ADR-0017).
|
|
1295
|
+
*
|
|
1296
|
+
* `boxPadding` is a scalar, not a mergeable style object, so it resolves
|
|
1297
|
+
* separately with the same façade → per-call precedence and the same
|
|
1298
|
+
* explicit-undefined-wins rule: a `boxPadding` key present at a
|
|
1299
|
+
* higher-priority layer (even set to `undefined`) wins over a lower layer's
|
|
1300
|
+
* value. There is no built-in `boxPadding` — an absent value at every layer
|
|
1301
|
+
* falls through to the controllers' own `?? 0` default.
|
|
932
1302
|
*
|
|
933
1303
|
* Returns `undefined` only when no slot would carry any value — keeps the
|
|
934
1304
|
* downstream "no style on the feature" path intact for tests that assert
|
|
@@ -947,4 +1317,52 @@ declare class TacticalDrawDestroyedError extends Error {
|
|
|
947
1317
|
constructor(message?: string);
|
|
948
1318
|
}
|
|
949
1319
|
//#endregion
|
|
950
|
-
|
|
1320
|
+
//#region src/tactical-draw/transform-box.d.ts
|
|
1321
|
+
/**
|
|
1322
|
+
* The Excalidraw/Felt-style transform box: the oriented bounding box drawn
|
|
1323
|
+
* around a selection, with 4 corner scale handles, a rotate grip above the
|
|
1324
|
+
* top edge, and drag-anywhere-inside-the-box translate. This module is the
|
|
1325
|
+
* single source of truth for box *placement* (`fitTransformBox`,
|
|
1326
|
+
* `boxCorners`, `rotateGripPixel`) AND box *picking* (`pointInBox`,
|
|
1327
|
+
* `pickBoxHandle`) — mirroring the ADR-0016 discipline that keeps
|
|
1328
|
+
* `handles.ts` owning vertex/midpoint placement/pick. Everything here is pure
|
|
1329
|
+
* pixel-space math: no projection, no rendering, no session state.
|
|
1330
|
+
*
|
|
1331
|
+
* **Angle convention**: `angle` is in radians, measured in screen pixel space
|
|
1332
|
+
* (y grows downward). A positive angle rotates the box clockwise as drawn on
|
|
1333
|
+
* screen — the same convention `rotatePointsAround` (`handle-drag.ts`) uses,
|
|
1334
|
+
* so box rotation and control-point rotation stay in lockstep.
|
|
1335
|
+
*
|
|
1336
|
+
* **Corner ordering**: `boxCorners` always returns
|
|
1337
|
+
* `[topLeft, topRight, bottomRight, bottomLeft]` in the box's own (unrotated)
|
|
1338
|
+
* local frame, then rotated into place. "Top" means the smaller-y edge before
|
|
1339
|
+
* rotation — i.e. the edge the rotate grip floats above. The order is stable
|
|
1340
|
+
* across calls so a scale handle's `index` (0–3) always means the same
|
|
1341
|
+
* corner, and its opposite corner is always `(index + 2) % 4`.
|
|
1342
|
+
*/
|
|
1343
|
+
interface TransformBox {
|
|
1344
|
+
/** Box center, pixel space. */
|
|
1345
|
+
center: PixelCoordinate;
|
|
1346
|
+
/** Half-width along the box's local x-axis (pre-rotation), in pixels. */
|
|
1347
|
+
halfWidth: number;
|
|
1348
|
+
/** Half-height along the box's local y-axis (pre-rotation), in pixels. */
|
|
1349
|
+
halfHeight: number;
|
|
1350
|
+
/** Radians, screen space, positive = clockwise on screen. */
|
|
1351
|
+
angle: number;
|
|
1352
|
+
}
|
|
1353
|
+
//#endregion
|
|
1354
|
+
//#region src/world-wrap.d.ts
|
|
1355
|
+
/**
|
|
1356
|
+
* Shift `lng` by whole world widths (360°) onto the copy nearest `refLng`.
|
|
1357
|
+
*
|
|
1358
|
+
* Map engines that wrap pointer longitudes into [-180, 180] (OpenLayers'
|
|
1359
|
+
* `toLonLat`, MapLibre's globe unproject) make positions jump ~360° when a
|
|
1360
|
+
* gesture crosses the antimeridian. Adapters use this to keep delivered
|
|
1361
|
+
* longitudes continuous with the view center, and to project coordinates on
|
|
1362
|
+
* the world copy the view is actually looking at.
|
|
1363
|
+
*/
|
|
1364
|
+
declare function nearestWorldCopyLng(lng: number, refLng: number): number;
|
|
1365
|
+
/** `nearestWorldCopyLng` applied to a lon/lat position. */
|
|
1366
|
+
declare function nearestWorldCopyPosition(position: Position, refLng: number): Position;
|
|
1367
|
+
//#endregion
|
|
1368
|
+
export { BaseMapAdapter, type CombinedAbort, type DrawMeasureDraft, type DrawOptions, type DrawSession, type EditChangeEvent, type EditManyOptions, type EditMode, type EditOptions, type EditPointerDriver, type EditSession, type FeatureOps, type GuideStyle, type HandleStyle, type InteractionStyle, type LayerId, type MapAdapter, type MapCursor, type MapEvent, type MapEventHandler, type NativeEventEntry, type PickEvent, type PickHandler, type PickOptions, type PixelCoordinate, type PointerCallback, PointerCallbackHub, type PointerEventInfo, type SettleReason, type SizeAnchor, type StyleOptions, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, type TacticalDrawAbortReason, TacticalDrawDestroyedError, type TacticalDrawLayerOptions, type TacticalDrawOptions, type TextStyleOptions, type ToPixel, type TransformBox, type TransformChangeEvent, type TransformSession, bindAbortable, coerceHandleStyle, combineWithHostSignal, getSizeAnchor, hitTestFeature, ignoreAbort, isTacticalDrawAbortError, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, tryControlMeasureIdFromFeature };
|