@orbat-mapper/tactical-draw 0.2.0-alpha.10 → 0.2.0-alpha.12
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/dist/index.d.mts +135 -14
- package/dist/index.mjs +240 -90
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ControlMeasure, ControlMeasureKind, ControlMeasureSnapshot, ControlMeasureStyle } from "@orbat-mapper/control-measures";
|
|
1
|
+
import { ControlMeasure, ControlMeasureKind, ControlMeasureSnapshot, ControlMeasureStyle, TextAmplifiers } from "@orbat-mapper/control-measures";
|
|
2
2
|
import { Feature, Position } from "geojson";
|
|
3
3
|
|
|
4
4
|
//#region src/gestures/types.d.ts
|
|
@@ -54,6 +54,13 @@ interface StyleOptions {
|
|
|
54
54
|
pointRadius?: number;
|
|
55
55
|
text?: TextStyleOptions;
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Text (label) style for a layer. When `labelProperty` is set, adapters also
|
|
59
|
+
* read per-feature label properties: `rotation` (radians, rendered as
|
|
60
|
+
* `π − rotation`), `textSizePixels`/`textSizeZoom`/`textSizeResolution`
|
|
61
|
+
* (ADR-0020), and `textAnchor` (`"start"` = text begins at the label point,
|
|
62
|
+
* `"end"` = text ends there; absent = centered).
|
|
63
|
+
*/
|
|
57
64
|
interface TextStyleOptions {
|
|
58
65
|
text?: string;
|
|
59
66
|
labelProperty?: string;
|
|
@@ -490,6 +497,24 @@ interface InteractionStyle {
|
|
|
490
497
|
*/
|
|
491
498
|
type EditMode = "reshape" | "transform" | "delete";
|
|
492
499
|
//#endregion
|
|
500
|
+
//#region src/tactical-draw/session-events.d.ts
|
|
501
|
+
/**
|
|
502
|
+
* The reason a session (`TransformSession`, `EditSession`, or `DrawSession`)
|
|
503
|
+
* settled, delivered to that session's `onSettled` handler (#191 / #194 /
|
|
504
|
+
* #197). `"close"` covers every commit path of the owning session —
|
|
505
|
+
* `TransformSession.close()` / `EditSession.close()` / `DrawSession.commit()`,
|
|
506
|
+
* a click-away close, a gesture commit (dblclick, a second click/tap on the
|
|
507
|
+
* finishing vertex, a click on another graphics-layer measure), or
|
|
508
|
+
* `TransformSession.setMembers([])`. `"error"` is the commit-time snapshot
|
|
509
|
+
* build throwing (the outer promise rejects with that error, not a
|
|
510
|
+
* `TacticalDrawAbortError`). The remaining values replay
|
|
511
|
+
* `TacticalDrawAbortError.reason` verbatim: `"escape"`, `"signal"`,
|
|
512
|
+
* `"session"` (`session.abort()` / `event.abort()`), `"preempted"`,
|
|
513
|
+
* `"destroyed"`, and `"removed"` (edit / transform only — a measure dropped
|
|
514
|
+
* from `td.render()`'s measure set).
|
|
515
|
+
*/
|
|
516
|
+
type SettleReason = "close" | "error" | TacticalDrawAbortReason;
|
|
517
|
+
//#endregion
|
|
493
518
|
//#region src/tactical-draw/edit-session.d.ts
|
|
494
519
|
/**
|
|
495
520
|
* Surface delivered via `td.edit(measure, opts).onSession` and synchronously
|
|
@@ -497,7 +522,8 @@ type EditMode = "reshape" | "transform" | "delete";
|
|
|
497
522
|
*
|
|
498
523
|
* `measure` is the immutable edit-start input. `controlPoints` is live working
|
|
499
524
|
* state. After the session settles (`close` / `abort`), every method is a
|
|
500
|
-
* silent no-op
|
|
525
|
+
* silent no-op (except [[onSettled]] — late registration still delivers,
|
|
526
|
+
* asynchronously).
|
|
501
527
|
*/
|
|
502
528
|
interface EditSession {
|
|
503
529
|
/** Immutable edit-start input. */
|
|
@@ -508,6 +534,13 @@ interface EditSession {
|
|
|
508
534
|
readonly options: ControlMeasure["options"];
|
|
509
535
|
/** Live working style. Copy on read. */
|
|
510
536
|
readonly style: ControlMeasureStyle | undefined;
|
|
537
|
+
/**
|
|
538
|
+
* Live working text amplifiers (MIL-STD-2525E Appendix L Table L-II). Copy
|
|
539
|
+
* on read. `undefined` when the edit-start measure carried none and none
|
|
540
|
+
* have been set since. Keys are stored as given — canonicalisation to
|
|
541
|
+
* doctrinal field codes happens in the core render seam, not here.
|
|
542
|
+
*/
|
|
543
|
+
readonly textAmplifiers: TextAmplifiers | undefined;
|
|
511
544
|
/** `true` after the first working-copy mutation; back to `false` within tolerance. */
|
|
512
545
|
readonly dirty: boolean;
|
|
513
546
|
readonly modes: readonly EditMode[];
|
|
@@ -545,6 +578,13 @@ interface EditSession {
|
|
|
545
578
|
setSizeAnchor(anchor: SizeAnchor): void;
|
|
546
579
|
/** Shallow-merge per-measure style into the live working copy. */
|
|
547
580
|
setStyle(partial: ControlMeasureStyle): void;
|
|
581
|
+
/**
|
|
582
|
+
* Shallow-merge into the live working text amplifiers. A value of
|
|
583
|
+
* `undefined` or an empty/whitespace-only string deletes that key; the
|
|
584
|
+
* working record collapses back to `undefined` once it holds no keys. A
|
|
585
|
+
* silent no-op after the session has settled (like [[setOptions]]).
|
|
586
|
+
*/
|
|
587
|
+
setTextAmplifiers(partial: Record<string, string | undefined>): void;
|
|
548
588
|
/**
|
|
549
589
|
* Subscribe to working-copy changes. The listener receives an
|
|
550
590
|
* [[EditChangeEvent]] for each completed gesture (drag end, insert/delete
|
|
@@ -557,6 +597,47 @@ interface EditSession {
|
|
|
557
597
|
* have run.
|
|
558
598
|
*/
|
|
559
599
|
onChange(listener: (event: EditChangeEvent) => void): () => void;
|
|
600
|
+
/**
|
|
601
|
+
* Subscribe to the committed snapshot — the single delivery channel for the
|
|
602
|
+
* `ControlMeasureSnapshot` this session ever commits (#197, mirroring
|
|
603
|
+
* `TransformSession.onCommit` #191). Fires exactly once, synchronously from
|
|
604
|
+
* within the [[close]] call that produced it (`session.close()`,
|
|
605
|
+
* `event.close()` from [[onChange]], a click-away close, or a click on
|
|
606
|
+
* another graphics-layer measure), with the same reference [[close]]
|
|
607
|
+
* returns and the outer `edit` promise resolves with. Never fires on abort.
|
|
608
|
+
*
|
|
609
|
+
* Listener exceptions are collected; after every listener has run, they are
|
|
610
|
+
* rethrown (one if single, `AggregateError` otherwise) — mirrors
|
|
611
|
+
* [[onChange]]. Returns an unsubscribe function; registering after the
|
|
612
|
+
* session has settled returns a no-op unsubscribe and the listener never
|
|
613
|
+
* fires.
|
|
614
|
+
*/
|
|
615
|
+
onCommit(handler: (snapshot: ControlMeasureSnapshot) => void): () => void;
|
|
616
|
+
/**
|
|
617
|
+
* Subscribe to the session's settlement — the lifecycle complement of
|
|
618
|
+
* [[onCommit]]: `onCommit` delivers *what* was committed, `onSettled` says
|
|
619
|
+
* the session is *over* and why (#197, mirroring
|
|
620
|
+
* `TransformSession.onSettled` #194). Fires exactly once, when the session
|
|
621
|
+
* settles by any path, commit or abort, with a `SettleReason`.
|
|
622
|
+
*
|
|
623
|
+
* - On the commit path it fires **after** [[onCommit]]'s emission, so a
|
|
624
|
+
* listener that clears selection state observes a document that already
|
|
625
|
+
* contains the committed snapshot.
|
|
626
|
+
* - On every path it fires **after** the facade has already cleared
|
|
627
|
+
* `td.activeSession` — so fold-then-render works from inside the handler.
|
|
628
|
+
* - `reason` distinguishes commit from abort but is deliberately coarse —
|
|
629
|
+
* no payload; the snapshot was already delivered through [[onCommit]].
|
|
630
|
+
*
|
|
631
|
+
* Registering after the session has already settled is **not** a no-op
|
|
632
|
+
* (unlike [[onChange]] / [[onCommit]]): the handler is invoked
|
|
633
|
+
* asynchronously (on a microtask) exactly once with the settle reason;
|
|
634
|
+
* unsubscribing before that microtask runs cancels the delivery.
|
|
635
|
+
*
|
|
636
|
+
* Listener exceptions are collected; after every listener has run, they are
|
|
637
|
+
* rethrown (one if single, `AggregateError` otherwise) — mirrors
|
|
638
|
+
* [[onChange]] / [[onCommit]]. Returns an unsubscribe function.
|
|
639
|
+
*/
|
|
640
|
+
onSettled(handler: (reason: SettleReason) => void): () => void;
|
|
560
641
|
}
|
|
561
642
|
/**
|
|
562
643
|
* Delivered to `EditSession.onChange` listeners once per completed gesture.
|
|
@@ -586,17 +667,6 @@ interface EditChangeEvent {
|
|
|
586
667
|
}
|
|
587
668
|
//#endregion
|
|
588
669
|
//#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
670
|
/**
|
|
601
671
|
* Surface delivered via `td.editMany(measures, opts).onSession` and
|
|
602
672
|
* synchronously available from `td.activeTransformSession` while the group
|
|
@@ -793,7 +863,8 @@ interface TransformChangeEvent {
|
|
|
793
863
|
* kinds. Also available from `td.activeSession` while the draw is live.
|
|
794
864
|
*
|
|
795
865
|
* After the draw has settled — either via `commit()` returning `true` or
|
|
796
|
-
* `abort()` resolving — every method is a silent no-op
|
|
866
|
+
* `abort()` resolving — every method is a silent no-op (except [[onSettled]]
|
|
867
|
+
* — late registration still delivers, asynchronously).
|
|
797
868
|
*/
|
|
798
869
|
interface DrawSession {
|
|
799
870
|
/** Live snapshot of the committed control points. Copy on read. */
|
|
@@ -815,6 +886,45 @@ interface DrawSession {
|
|
|
815
886
|
* are collected and rethrown after all listeners run.
|
|
816
887
|
*/
|
|
817
888
|
onChange(listener: (session: DrawSession) => void): () => void;
|
|
889
|
+
/**
|
|
890
|
+
* Subscribe to the committed snapshot — the single delivery channel for the
|
|
891
|
+
* `ControlMeasureSnapshot` this session ever commits (#197, mirroring
|
|
892
|
+
* `TransformSession.onCommit` #191). Fires exactly once, synchronously from
|
|
893
|
+
* within the commit that produced it (`session.commit()`, dblclick, or a
|
|
894
|
+
* second click/tap on the finishing vertex), with the same reference the
|
|
895
|
+
* outer `draw` promise resolves with — the committed snapshot, not just the
|
|
896
|
+
* measure. Never fires on abort.
|
|
897
|
+
*
|
|
898
|
+
* Listener exceptions are collected; after every listener has run, they are
|
|
899
|
+
* rethrown (one if single, `AggregateError` otherwise) — mirrors
|
|
900
|
+
* [[onChange]]. Returns an unsubscribe function; registering after the
|
|
901
|
+
* session has settled returns a no-op unsubscribe and the listener never
|
|
902
|
+
* fires.
|
|
903
|
+
*/
|
|
904
|
+
onCommit(handler: (snapshot: ControlMeasureSnapshot) => void): () => void;
|
|
905
|
+
/**
|
|
906
|
+
* Subscribe to the session's settlement — the lifecycle complement of
|
|
907
|
+
* [[onCommit]]: `onCommit` delivers *what* was committed, `onSettled` says
|
|
908
|
+
* the session is *over* and why (#197, mirroring
|
|
909
|
+
* `TransformSession.onSettled` #194). Fires exactly once, when the session
|
|
910
|
+
* settles by any path, commit or abort, with a `SettleReason`.
|
|
911
|
+
*
|
|
912
|
+
* - On the commit path it fires **after** [[onCommit]]'s emission.
|
|
913
|
+
* - On every path it fires **after** the facade has already cleared
|
|
914
|
+
* `td.activeSession` — so fold-then-render works from inside the handler.
|
|
915
|
+
* - `reason` distinguishes commit from abort but is deliberately coarse —
|
|
916
|
+
* no payload; the snapshot was already delivered through [[onCommit]].
|
|
917
|
+
*
|
|
918
|
+
* Registering after the session has already settled is **not** a no-op
|
|
919
|
+
* (unlike [[onChange]] / [[onCommit]]): the handler is invoked
|
|
920
|
+
* asynchronously (on a microtask) exactly once with the settle reason;
|
|
921
|
+
* unsubscribing before that microtask runs cancels the delivery.
|
|
922
|
+
*
|
|
923
|
+
* Listener exceptions are collected; after every listener has run, they are
|
|
924
|
+
* rethrown (one if single, `AggregateError` otherwise) — mirrors
|
|
925
|
+
* [[onChange]] / [[onCommit]]. Returns an unsubscribe function.
|
|
926
|
+
*/
|
|
927
|
+
onSettled(handler: (reason: SettleReason) => void): () => void;
|
|
818
928
|
}
|
|
819
929
|
//#endregion
|
|
820
930
|
//#region src/tactical-draw/tactical-draw.d.ts
|
|
@@ -1144,6 +1254,12 @@ declare class TacticalDraw {
|
|
|
1144
1254
|
* `opts.onSession` and commit on `session.commit()`, `dblclick`, or a second
|
|
1145
1255
|
* click on the last added control point. All paths reject with
|
|
1146
1256
|
* `TacticalDrawAbortError` on signal / Escape / destroy / `session.abort()`.
|
|
1257
|
+
*
|
|
1258
|
+
* For a variable-length draw, `DrawSession.onCommit` is the single delivery
|
|
1259
|
+
* channel for the committed snapshot and `DrawSession.onSettled` is the
|
|
1260
|
+
* lifecycle signal for when the session is over and why (#197) — together
|
|
1261
|
+
* they make the returned promise non-load-bearing for hosts that adopt them.
|
|
1262
|
+
* Fixed-length kinds have no session and settle only through the promise.
|
|
1147
1263
|
*/
|
|
1148
1264
|
draw<K extends ControlMeasureKind>(draft: DrawMeasureDraft<K>, options?: DrawOptions): Promise<ControlMeasureSnapshot<K>>;
|
|
1149
1265
|
/**
|
|
@@ -1155,6 +1271,11 @@ declare class TacticalDraw {
|
|
|
1155
1271
|
* Default click-away (`closeOnClickAway: true`) closes the edit on a click
|
|
1156
1272
|
* outside the preview / handle features. Hosts that own close policy pass
|
|
1157
1273
|
* `false`.
|
|
1274
|
+
*
|
|
1275
|
+
* `EditSession.onCommit` is the single delivery channel for the committed
|
|
1276
|
+
* snapshot and `EditSession.onSettled` is the lifecycle signal for when the
|
|
1277
|
+
* session is over and why (#197) — together they make the returned promise
|
|
1278
|
+
* non-load-bearing for hosts that adopt them.
|
|
1158
1279
|
*/
|
|
1159
1280
|
edit(measure: ControlMeasure, options?: EditOptions): Promise<ControlMeasureSnapshot>;
|
|
1160
1281
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cloneControlMeasure, controlMeasureIdFromFeature, freezeOrientationOptions, getControlMeasureMetadata, renderControlMeasure, resolveStyleHints, roundToFixed } from "@orbat-mapper/control-measures";
|
|
1
|
+
import { canonicalTextAmplifierKey, cloneControlMeasure, controlMeasureIdFromFeature, freezeOrientationOptions, getControlMeasureMetadata, renderControlMeasure, resolveStyleHints, roundToFixed } from "@orbat-mapper/control-measures";
|
|
2
2
|
//#region src/base-map-adapter.ts
|
|
3
3
|
/**
|
|
4
4
|
* Abstract base class shared by all `MapAdapter` implementations.
|
|
@@ -876,7 +876,7 @@ const PREVIEW_CURSOR$1 = "crosshair";
|
|
|
876
876
|
* kinds need no host UI.
|
|
877
877
|
*/
|
|
878
878
|
function runFixedLengthDraw(options) {
|
|
879
|
-
const { adapter, kind, previewLayer, guideLayer, graphicsStyle, generateId, signal, measureOptions, measureStyle, measureProperties, guide: guideOpt, sizeAnchor, onSettled } = options;
|
|
879
|
+
const { adapter, kind, previewLayer, guideLayer, graphicsStyle, generateId, signal, measureOptions, measureStyle, measureProperties, measureTextAmplifiers, guide: guideOpt, sizeAnchor, onSettled } = options;
|
|
880
880
|
const metadata = getControlMeasureMetadata(kind);
|
|
881
881
|
const rule = metadata.rule;
|
|
882
882
|
const min = metadata.minCoordinates;
|
|
@@ -916,7 +916,7 @@ function runFixedLengthDraw(options) {
|
|
|
916
916
|
return;
|
|
917
917
|
}
|
|
918
918
|
const previewThreshold = rule?.minimumPreviewPoints ?? required;
|
|
919
|
-
const features = buildPreviewFeatures$1(adapter, kind, rule && raw.length >= previewThreshold ? rule.derive(raw) : raw, measureOptions, measureStyle, renderOpts);
|
|
919
|
+
const features = buildPreviewFeatures$1(adapter, kind, rule && raw.length >= previewThreshold ? rule.derive(raw) : raw, measureOptions, measureStyle, measureTextAmplifiers, renderOpts);
|
|
920
920
|
ctx.writeLayer(previewLayer, features);
|
|
921
921
|
const guidePoints = rule?.guidePoints ? rule.guidePoints(raw) : raw;
|
|
922
922
|
const guideFeatures = guideEnabled ? buildGuideFeatures(guidePoints, isAreaKind) : [];
|
|
@@ -937,7 +937,8 @@ function runFixedLengthDraw(options) {
|
|
|
937
937
|
controlPoints: finalPoints,
|
|
938
938
|
...measureOptions !== void 0 ? { options: measureOptions } : {},
|
|
939
939
|
...measureStyle !== void 0 ? { style: measureStyle } : {},
|
|
940
|
-
...measureProperties !== void 0 ? { properties: measureProperties } : {}
|
|
940
|
+
...measureProperties !== void 0 ? { properties: measureProperties } : {},
|
|
941
|
+
...measureTextAmplifiers !== void 0 ? { textAmplifiers: measureTextAmplifiers } : {}
|
|
941
942
|
}, adapter);
|
|
942
943
|
const measure = sizeAnchor === "screen" ? captured : bakePixelSizeToGround(captured, adapter);
|
|
943
944
|
return {
|
|
@@ -970,13 +971,14 @@ function runFixedLengthDraw(options) {
|
|
|
970
971
|
* falls back to a plain Point/LineString outline when the kind's generator
|
|
971
972
|
* cannot produce a shape yet (under-min count).
|
|
972
973
|
*/
|
|
973
|
-
function buildPreviewFeatures$1(adapter, kind, points, measureOptions, measureStyle, renderOpts) {
|
|
974
|
+
function buildPreviewFeatures$1(adapter, kind, points, measureOptions, measureStyle, measureTextAmplifiers, renderOpts) {
|
|
974
975
|
const measure = {
|
|
975
976
|
id: PREVIEW_CM_ID$1,
|
|
976
977
|
kind,
|
|
977
978
|
controlPoints: points,
|
|
978
979
|
...measureOptions !== void 0 ? { options: measureOptions } : {},
|
|
979
|
-
...measureStyle !== void 0 ? { style: measureStyle } : {}
|
|
980
|
+
...measureStyle !== void 0 ? { style: measureStyle } : {},
|
|
981
|
+
...measureTextAmplifiers !== void 0 ? { textAmplifiers: measureTextAmplifiers } : {}
|
|
980
982
|
};
|
|
981
983
|
try {
|
|
982
984
|
return renderMeasureForAdapter(adapter, measure, {
|
|
@@ -1007,6 +1009,117 @@ function outlinePreview$1(points) {
|
|
|
1007
1009
|
return features;
|
|
1008
1010
|
}
|
|
1009
1011
|
//#endregion
|
|
1012
|
+
//#region src/tactical-draw/session-events.ts
|
|
1013
|
+
/**
|
|
1014
|
+
* Map a settle-time error to its `SettleReason`: a `TacticalDrawAbortError`
|
|
1015
|
+
* replays its own `reason` verbatim; anything else (a `produce` throw from
|
|
1016
|
+
* `ctx.commit`) is the commit-time snapshot build failing, reported as
|
|
1017
|
+
* `"error"`.
|
|
1018
|
+
*/
|
|
1019
|
+
function settleReasonFromError(error) {
|
|
1020
|
+
return isTacticalDrawAbortError(error) ? error.reason : "error";
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Fan a `runInteraction` settlement out to a session's commit + settle
|
|
1024
|
+
* emitters — the shared body of the `onSettled` wrappers in the edit and draw
|
|
1025
|
+
* controllers (transform emits its close-path settle from `commitClose`
|
|
1026
|
+
* instead, so it does not use this). On the commit path, emit the snapshot
|
|
1027
|
+
* then the `"close"` settle in a try/finally so a throwing `onCommit` listener
|
|
1028
|
+
* cannot starve the settle emission; on the reject path, emit the settle with
|
|
1029
|
+
* the reason derived from the error. Callers run their own facade
|
|
1030
|
+
* slot-clearing (`onSettled()`) before this so a listener observes a cleared
|
|
1031
|
+
* `td.activeSession`.
|
|
1032
|
+
*/
|
|
1033
|
+
function settleFromInteraction(settlement, commitEmitter, settleEmitter) {
|
|
1034
|
+
if (settlement.status === "committed") try {
|
|
1035
|
+
commitEmitter.emit(settlement.value);
|
|
1036
|
+
} finally {
|
|
1037
|
+
settleEmitter.emit("close");
|
|
1038
|
+
}
|
|
1039
|
+
else settleEmitter.emit(settleReasonFromError(settlement.error));
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Subscribe/emit machinery shared by `TransformSession.onSettled`,
|
|
1043
|
+
* `EditSession.onSettled`, and `DrawSession.onSettled` (#191 / #194 / #197):
|
|
1044
|
+
* a settle-exactly-once latch, synchronous emission to every listener with
|
|
1045
|
+
* exception collection (single rethrow, `AggregateError` otherwise when more
|
|
1046
|
+
* than one listener throws), and a late-registration path that delivers the
|
|
1047
|
+
* latched reason on a microtask exactly once when a handler subscribes after
|
|
1048
|
+
* the session has already settled.
|
|
1049
|
+
*
|
|
1050
|
+
* `listenerErrorsMessage` becomes the `AggregateError` message — pass a
|
|
1051
|
+
* session-specific string (e.g. `"EditSession.onSettled listener errors"`).
|
|
1052
|
+
*/
|
|
1053
|
+
function createSettleEmitter(listenerErrorsMessage) {
|
|
1054
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1055
|
+
/** Set exactly once, by `emit` — also the "has settled" latch and the reason replayed to late registrations. */
|
|
1056
|
+
let settledReason = null;
|
|
1057
|
+
return {
|
|
1058
|
+
emit(reason) {
|
|
1059
|
+
if (settledReason !== null) return;
|
|
1060
|
+
settledReason = reason;
|
|
1061
|
+
const current = [...listeners];
|
|
1062
|
+
listeners.clear();
|
|
1063
|
+
const errors = [];
|
|
1064
|
+
for (const listener of current) try {
|
|
1065
|
+
listener(reason);
|
|
1066
|
+
} catch (err) {
|
|
1067
|
+
errors.push(err);
|
|
1068
|
+
}
|
|
1069
|
+
if (errors.length === 1) throw errors[0];
|
|
1070
|
+
if (errors.length > 1) throw new AggregateError(errors, listenerErrorsMessage);
|
|
1071
|
+
},
|
|
1072
|
+
register(handler) {
|
|
1073
|
+
if (settledReason !== null) {
|
|
1074
|
+
const reason = settledReason;
|
|
1075
|
+
let cancelled = false;
|
|
1076
|
+
queueMicrotask(() => {
|
|
1077
|
+
if (!cancelled) handler(reason);
|
|
1078
|
+
});
|
|
1079
|
+
return () => {
|
|
1080
|
+
cancelled = true;
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
listeners.add(handler);
|
|
1084
|
+
return () => {
|
|
1085
|
+
listeners.delete(handler);
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Subscribe/emit machinery shared by `TransformSession.onCommit`,
|
|
1092
|
+
* `EditSession.onCommit`, and `DrawSession.onCommit`: a listener set with
|
|
1093
|
+
* synchronous, exception-collecting emission (single rethrow, `AggregateError`
|
|
1094
|
+
* otherwise). Unlike `createSettleEmitter`, `emit` may be called more than
|
|
1095
|
+
* once — a transform's `setMembers` can commit several removal batches before
|
|
1096
|
+
* the close batch — and there is no latch or late-registration path:
|
|
1097
|
+
* registration-time `ctx.settled` guards stay in the controllers, mirroring
|
|
1098
|
+
* `onChange`.
|
|
1099
|
+
*/
|
|
1100
|
+
function createCommitEmitter(listenerErrorsMessage) {
|
|
1101
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1102
|
+
return {
|
|
1103
|
+
emit(value) {
|
|
1104
|
+
const current = [...listeners];
|
|
1105
|
+
const errors = [];
|
|
1106
|
+
for (const listener of current) try {
|
|
1107
|
+
listener(value);
|
|
1108
|
+
} catch (err) {
|
|
1109
|
+
errors.push(err);
|
|
1110
|
+
}
|
|
1111
|
+
if (errors.length === 1) throw errors[0];
|
|
1112
|
+
if (errors.length > 1) throw new AggregateError(errors, listenerErrorsMessage);
|
|
1113
|
+
},
|
|
1114
|
+
register(listener) {
|
|
1115
|
+
listeners.add(listener);
|
|
1116
|
+
return () => {
|
|
1117
|
+
listeners.delete(listener);
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
//#endregion
|
|
1010
1123
|
//#region src/tactical-draw/draw-controller-variable.ts
|
|
1011
1124
|
const PREVIEW_CM_ID = "__td_preview__";
|
|
1012
1125
|
const PREVIEW_CURSOR = "crosshair";
|
|
@@ -1018,7 +1131,7 @@ const PREVIEW_CURSOR = "crosshair";
|
|
|
1018
1131
|
* added point again" as additional gesture-level commit triggers.
|
|
1019
1132
|
*/
|
|
1020
1133
|
function runVariableLengthDraw(options) {
|
|
1021
|
-
const { adapter, kind, previewLayer, guideLayer, graphicsStyle, generateId, signal, onSession, seed, measureOptions, measureStyle, measureProperties, guide: guideOpt, sizeAnchor, onSettled } = options;
|
|
1134
|
+
const { adapter, kind, previewLayer, guideLayer, graphicsStyle, generateId, signal, onSession, seed, measureOptions, measureStyle, measureProperties, measureTextAmplifiers, guide: guideOpt, sizeAnchor, onSettled } = options;
|
|
1022
1135
|
const metadata = getControlMeasureMetadata(kind);
|
|
1023
1136
|
if (typeof metadata.minCoordinates !== "number") throw new Error(`runVariableLengthDraw: kind "${kind}" has no minCoordinates metadata`);
|
|
1024
1137
|
const min = metadata.minCoordinates;
|
|
@@ -1053,7 +1166,8 @@ function runVariableLengthDraw(options) {
|
|
|
1053
1166
|
kind,
|
|
1054
1167
|
controlPoints: initialPoints.map((p) => [...p]),
|
|
1055
1168
|
...measureOptions !== void 0 ? { options: measureOptions } : {},
|
|
1056
|
-
...measureStyle !== void 0 ? { style: measureStyle } : {}
|
|
1169
|
+
...measureStyle !== void 0 ? { style: measureStyle } : {},
|
|
1170
|
+
...measureTextAmplifiers !== void 0 ? { textAmplifiers: measureTextAmplifiers } : {}
|
|
1057
1171
|
}, {
|
|
1058
1172
|
...renderOpts,
|
|
1059
1173
|
validationMode: "throw"
|
|
@@ -1064,6 +1178,8 @@ function runVariableLengthDraw(options) {
|
|
|
1064
1178
|
let ctx;
|
|
1065
1179
|
const savedCursor = "";
|
|
1066
1180
|
const changeListeners = /* @__PURE__ */ new Set();
|
|
1181
|
+
const commitEmitter = createCommitEmitter("DrawSession.onCommit listener errors");
|
|
1182
|
+
const settleEmitter = createSettleEmitter("DrawSession.onSettled listener errors");
|
|
1067
1183
|
function canCommitNow() {
|
|
1068
1184
|
const n = committed.length;
|
|
1069
1185
|
if (typeof max === "number" && n > max) return false;
|
|
@@ -1100,6 +1216,13 @@ function runVariableLengthDraw(options) {
|
|
|
1100
1216
|
return () => {
|
|
1101
1217
|
changeListeners.delete(listener);
|
|
1102
1218
|
};
|
|
1219
|
+
},
|
|
1220
|
+
onCommit(listener) {
|
|
1221
|
+
if (ctx.settled) return () => {};
|
|
1222
|
+
return commitEmitter.register(listener);
|
|
1223
|
+
},
|
|
1224
|
+
onSettled(handler) {
|
|
1225
|
+
return settleEmitter.register(handler);
|
|
1103
1226
|
}
|
|
1104
1227
|
};
|
|
1105
1228
|
function fireChange() {
|
|
@@ -1167,7 +1290,7 @@ function runVariableLengthDraw(options) {
|
|
|
1167
1290
|
points.splice(idx, 0, rubberBand);
|
|
1168
1291
|
} else points = [...committed];
|
|
1169
1292
|
if (rule) points = rule.derive(points);
|
|
1170
|
-
const previewFeatures = points.length === 0 ? [] : buildPreviewFeatures(adapter, kind, points, measureOptions, measureStyle, renderOpts);
|
|
1293
|
+
const previewFeatures = points.length === 0 ? [] : buildPreviewFeatures(adapter, kind, points, measureOptions, measureStyle, measureTextAmplifiers, renderOpts);
|
|
1171
1294
|
ctx.writeLayer(previewLayer, previewFeatures);
|
|
1172
1295
|
const guideFeatures = points.length === 0 || !guideEnabled ? [] : buildGuideFeatures(points, isAreaKind, trailingFixed);
|
|
1173
1296
|
ctx.writeLayer(guideLayer, guideFeatures);
|
|
@@ -1186,7 +1309,8 @@ function runVariableLengthDraw(options) {
|
|
|
1186
1309
|
controlPoints: committed.map((p) => [...p]),
|
|
1187
1310
|
...measureOptions !== void 0 ? { options: measureOptions } : {},
|
|
1188
1311
|
...measureStyle !== void 0 ? { style: measureStyle } : {},
|
|
1189
|
-
...measureProperties !== void 0 ? { properties: measureProperties } : {}
|
|
1312
|
+
...measureProperties !== void 0 ? { properties: measureProperties } : {},
|
|
1313
|
+
...measureTextAmplifiers !== void 0 ? { textAmplifiers: measureTextAmplifiers } : {}
|
|
1190
1314
|
}, adapter);
|
|
1191
1315
|
const measure = sizeAnchor === "screen" ? built : bakePixelSizeToGround(built, adapter);
|
|
1192
1316
|
return {
|
|
@@ -1199,7 +1323,10 @@ function runVariableLengthDraw(options) {
|
|
|
1199
1323
|
signal,
|
|
1200
1324
|
layers: [previewLayer, guideLayer],
|
|
1201
1325
|
onViewChange: rescaleOnViewChange,
|
|
1202
|
-
onSettled
|
|
1326
|
+
onSettled: (settlement) => {
|
|
1327
|
+
if (onSettled) onSettled();
|
|
1328
|
+
settleFromInteraction(settlement, commitEmitter, settleEmitter);
|
|
1329
|
+
},
|
|
1203
1330
|
attach(c) {
|
|
1204
1331
|
ctx = c;
|
|
1205
1332
|
c.onTeardown(() => {
|
|
@@ -1234,13 +1361,14 @@ function isValidPosition(value) {
|
|
|
1234
1361
|
}
|
|
1235
1362
|
return true;
|
|
1236
1363
|
}
|
|
1237
|
-
function buildPreviewFeatures(adapter, kind, points, measureOptions, measureStyle, renderOpts) {
|
|
1364
|
+
function buildPreviewFeatures(adapter, kind, points, measureOptions, measureStyle, measureTextAmplifiers, renderOpts) {
|
|
1238
1365
|
const measure = {
|
|
1239
1366
|
id: PREVIEW_CM_ID,
|
|
1240
1367
|
kind,
|
|
1241
1368
|
controlPoints: points,
|
|
1242
1369
|
...measureOptions !== void 0 ? { options: measureOptions } : {},
|
|
1243
|
-
...measureStyle !== void 0 ? { style: measureStyle } : {}
|
|
1370
|
+
...measureStyle !== void 0 ? { style: measureStyle } : {},
|
|
1371
|
+
...measureTextAmplifiers !== void 0 ? { textAmplifiers: measureTextAmplifiers } : {}
|
|
1244
1372
|
};
|
|
1245
1373
|
try {
|
|
1246
1374
|
return renderMeasureForAdapter(adapter, measure, renderOpts).features;
|
|
@@ -1873,6 +2001,7 @@ function runEdit(opts) {
|
|
|
1873
2001
|
const editStartPoints = measure.controlPoints.map((p) => [...p]);
|
|
1874
2002
|
const workingPoints = editStartPoints.map((p) => [...p]);
|
|
1875
2003
|
let workingOptions = { ...measure.options ?? {} };
|
|
2004
|
+
let workingTextAmplifiers = measure.textAmplifiers && Object.keys(measure.textAmplifiers).length > 0 ? { ...measure.textAmplifiers } : void 0;
|
|
1876
2005
|
let modes = requestedModes;
|
|
1877
2006
|
let dirty = false;
|
|
1878
2007
|
let nonGeometryDirty = false;
|
|
@@ -1897,6 +2026,8 @@ function runEdit(opts) {
|
|
|
1897
2026
|
*/
|
|
1898
2027
|
let pendingModes = null;
|
|
1899
2028
|
const changeListeners = /* @__PURE__ */ new Set();
|
|
2029
|
+
const commitEmitter = createCommitEmitter("EditSession.onCommit listener errors");
|
|
2030
|
+
const settleEmitter = createSettleEmitter("EditSession.onSettled listener errors");
|
|
1900
2031
|
/**
|
|
1901
2032
|
* The transform box's rotation, in radians, session-scoped (ADR-0017 decision
|
|
1902
2033
|
* #2): starts at `0` (axis-aligned) and accumulates every committed rotate
|
|
@@ -1959,6 +2090,8 @@ function runEdit(opts) {
|
|
|
1959
2090
|
options: workingOptions,
|
|
1960
2091
|
controlPoints: workingPoints.map((p) => [...p])
|
|
1961
2092
|
};
|
|
2093
|
+
if (workingTextAmplifiers) working.textAmplifiers = workingTextAmplifiers;
|
|
2094
|
+
else delete working.textAmplifiers;
|
|
1962
2095
|
let features;
|
|
1963
2096
|
try {
|
|
1964
2097
|
features = renderMeasureForAdapter(adapter, working, renderOpts).features;
|
|
@@ -2010,6 +2143,8 @@ function runEdit(opts) {
|
|
|
2010
2143
|
options,
|
|
2011
2144
|
controlPoints: workingPoints.map((p) => [...p])
|
|
2012
2145
|
};
|
|
2146
|
+
if (workingTextAmplifiers) working.textAmplifiers = workingTextAmplifiers;
|
|
2147
|
+
else delete working.textAmplifiers;
|
|
2013
2148
|
const committed = sizeAnchor === "screen" ? working : bakePixelSizeToGround(working, adapter);
|
|
2014
2149
|
return {
|
|
2015
2150
|
measure: cloneControlMeasure(committed),
|
|
@@ -2061,7 +2196,7 @@ function runEdit(opts) {
|
|
|
2061
2196
|
* - Reject restores the working state to the pre-gesture snapshot and
|
|
2062
2197
|
* recomputes `dirty`. Close/abort settle the session as normal.
|
|
2063
2198
|
*/
|
|
2064
|
-
function emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered) {
|
|
2199
|
+
function emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered, previousTextAmplifiers) {
|
|
2065
2200
|
if (ctx.settled) return;
|
|
2066
2201
|
let rejectRequested = false;
|
|
2067
2202
|
let closeRequested = false;
|
|
@@ -2093,6 +2228,7 @@ function runEdit(opts) {
|
|
|
2093
2228
|
replaceWorkingOptions(previousOptions);
|
|
2094
2229
|
currentStyle = previousStyle;
|
|
2095
2230
|
sizeAnchor = previousSizeAnchor;
|
|
2231
|
+
workingTextAmplifiers = previousTextAmplifiers;
|
|
2096
2232
|
nonGeometryDirty = previousNonGeometryDirty;
|
|
2097
2233
|
renderPreview();
|
|
2098
2234
|
renderHandlesLayer();
|
|
@@ -2117,6 +2253,7 @@ function runEdit(opts) {
|
|
|
2117
2253
|
const previousOptions = { ...preGestureOptions ?? workingOptions ?? {} };
|
|
2118
2254
|
const previousStyle = currentStyle;
|
|
2119
2255
|
const previousNonGeometryDirty = nonGeometryDirty;
|
|
2256
|
+
const previousTextAmplifiers = workingTextAmplifiers;
|
|
2120
2257
|
const previousRendered = rebuildRenderedWith(previousOptions);
|
|
2121
2258
|
replaceWorkingPoints(nextPoints);
|
|
2122
2259
|
let currentRendered;
|
|
@@ -2134,7 +2271,7 @@ function runEdit(opts) {
|
|
|
2134
2271
|
recomputeDirty();
|
|
2135
2272
|
dragInFlight = false;
|
|
2136
2273
|
drainPendingModes();
|
|
2137
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered);
|
|
2274
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers);
|
|
2138
2275
|
}
|
|
2139
2276
|
function setOptions(partial) {
|
|
2140
2277
|
if (ctx.settled) return;
|
|
@@ -2142,6 +2279,7 @@ function runEdit(opts) {
|
|
|
2142
2279
|
const previousOptions = { ...workingOptions ?? {} };
|
|
2143
2280
|
const previousStyle = currentStyle;
|
|
2144
2281
|
const previousNonGeometryDirty = nonGeometryDirty;
|
|
2282
|
+
const previousTextAmplifiers = workingTextAmplifiers;
|
|
2145
2283
|
const previousRendered = rebuildRendered();
|
|
2146
2284
|
replaceWorkingOptions({
|
|
2147
2285
|
...workingOptions ?? {},
|
|
@@ -2157,7 +2295,7 @@ function runEdit(opts) {
|
|
|
2157
2295
|
renderPreview();
|
|
2158
2296
|
nonGeometryDirty = true;
|
|
2159
2297
|
dirty = true;
|
|
2160
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered);
|
|
2298
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers);
|
|
2161
2299
|
}
|
|
2162
2300
|
function setStyle(partial) {
|
|
2163
2301
|
if (ctx.settled) return;
|
|
@@ -2165,6 +2303,7 @@ function runEdit(opts) {
|
|
|
2165
2303
|
const previousOptions = { ...workingOptions ?? {} };
|
|
2166
2304
|
const previousStyle = currentStyle;
|
|
2167
2305
|
const previousNonGeometryDirty = nonGeometryDirty;
|
|
2306
|
+
const previousTextAmplifiers = workingTextAmplifiers;
|
|
2168
2307
|
const previousRendered = rebuildRendered();
|
|
2169
2308
|
currentStyle = {
|
|
2170
2309
|
...currentStyle ?? {},
|
|
@@ -2180,7 +2319,51 @@ function runEdit(opts) {
|
|
|
2180
2319
|
renderPreview();
|
|
2181
2320
|
nonGeometryDirty = true;
|
|
2182
2321
|
dirty = true;
|
|
2183
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered);
|
|
2322
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers);
|
|
2323
|
+
}
|
|
2324
|
+
/**
|
|
2325
|
+
* Shallow-merge into the live working text amplifiers. Mirrors
|
|
2326
|
+
* [[setOptions]] exactly (dirty tracking, re-render, change emission) —
|
|
2327
|
+
* see that function for the shared shape. A merge value of `undefined` or
|
|
2328
|
+
* an empty/whitespace-only string deletes the key; the record collapses to
|
|
2329
|
+
* `undefined` once empty so `renderPreview`/`rebuildRenderedWith` omit the
|
|
2330
|
+
* key rather than persist `{}`.
|
|
2331
|
+
*
|
|
2332
|
+
* Each patch key resolves to its canonical field key and evicts every
|
|
2333
|
+
* working entry for the same field + suffix — including friendly-alias
|
|
2334
|
+
* twins (`uniqueDesignation` for `T`) — so clearing or replacing via a
|
|
2335
|
+
* canonical key cannot be undone by an alias-backed value surviving in the
|
|
2336
|
+
* working record. Non-empty values are stored under the canonical key;
|
|
2337
|
+
* unknown keys keep literal behavior.
|
|
2338
|
+
*/
|
|
2339
|
+
function setTextAmplifiers(partial) {
|
|
2340
|
+
if (ctx.settled) return;
|
|
2341
|
+
const previousPoints = workingPoints.map((p) => [...p]);
|
|
2342
|
+
const previousOptions = { ...workingOptions ?? {} };
|
|
2343
|
+
const previousStyle = currentStyle;
|
|
2344
|
+
const previousNonGeometryDirty = nonGeometryDirty;
|
|
2345
|
+
const previousTextAmplifiers = workingTextAmplifiers;
|
|
2346
|
+
const previousRendered = rebuildRendered();
|
|
2347
|
+
const merged = { ...workingTextAmplifiers ?? {} };
|
|
2348
|
+
for (const [key, value] of Object.entries(partial)) {
|
|
2349
|
+
const canonicalKey = canonicalTextAmplifierKey(key);
|
|
2350
|
+
if (canonicalKey) {
|
|
2351
|
+
for (const existing of Object.keys(merged)) if (canonicalTextAmplifierKey(existing) === canonicalKey) delete merged[existing];
|
|
2352
|
+
} else delete merged[key];
|
|
2353
|
+
if (value !== void 0 && value.trim() !== "") merged[canonicalKey ?? key] = value;
|
|
2354
|
+
}
|
|
2355
|
+
workingTextAmplifiers = Object.keys(merged).length > 0 ? merged : void 0;
|
|
2356
|
+
let currentRendered;
|
|
2357
|
+
try {
|
|
2358
|
+
currentRendered = rebuildRendered();
|
|
2359
|
+
} catch (err) {
|
|
2360
|
+
workingTextAmplifiers = previousTextAmplifiers;
|
|
2361
|
+
throw err;
|
|
2362
|
+
}
|
|
2363
|
+
renderPreview();
|
|
2364
|
+
nonGeometryDirty = true;
|
|
2365
|
+
dirty = true;
|
|
2366
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers);
|
|
2184
2367
|
}
|
|
2185
2368
|
/**
|
|
2186
2369
|
* Flip the size anchor for the measure under edit (ADR-0020). Re-renders the
|
|
@@ -2196,12 +2379,13 @@ function runEdit(opts) {
|
|
|
2196
2379
|
const previousStyle = currentStyle;
|
|
2197
2380
|
const previousNonGeometryDirty = nonGeometryDirty;
|
|
2198
2381
|
const previousSizeAnchor = sizeAnchor;
|
|
2382
|
+
const previousTextAmplifiers = workingTextAmplifiers;
|
|
2199
2383
|
const previousRendered = rebuildRendered();
|
|
2200
2384
|
sizeAnchor = next;
|
|
2201
2385
|
const currentRendered = rebuildRendered();
|
|
2202
2386
|
nonGeometryDirty = true;
|
|
2203
2387
|
dirty = true;
|
|
2204
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered);
|
|
2388
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered, previousTextAmplifiers);
|
|
2205
2389
|
}
|
|
2206
2390
|
/** Apply a translate gesture by shifting every working point by `delta`. */
|
|
2207
2391
|
function commitTranslate(delta) {
|
|
@@ -2259,6 +2443,9 @@ function runEdit(opts) {
|
|
|
2259
2443
|
get style() {
|
|
2260
2444
|
return currentStyle ? { ...currentStyle } : void 0;
|
|
2261
2445
|
},
|
|
2446
|
+
get textAmplifiers() {
|
|
2447
|
+
return workingTextAmplifiers ? { ...workingTextAmplifiers } : void 0;
|
|
2448
|
+
},
|
|
2262
2449
|
get dirty() {
|
|
2263
2450
|
return dirty;
|
|
2264
2451
|
},
|
|
@@ -2290,6 +2477,9 @@ function runEdit(opts) {
|
|
|
2290
2477
|
setStyle(partial) {
|
|
2291
2478
|
setStyle(partial);
|
|
2292
2479
|
},
|
|
2480
|
+
setTextAmplifiers(partial) {
|
|
2481
|
+
setTextAmplifiers(partial);
|
|
2482
|
+
},
|
|
2293
2483
|
setSizeAnchor(next) {
|
|
2294
2484
|
setSizeAnchor(next);
|
|
2295
2485
|
},
|
|
@@ -2300,6 +2490,13 @@ function runEdit(opts) {
|
|
|
2300
2490
|
changeListeners.delete(listener);
|
|
2301
2491
|
};
|
|
2302
2492
|
},
|
|
2493
|
+
onCommit(listener) {
|
|
2494
|
+
if (ctx.settled) return () => {};
|
|
2495
|
+
return commitEmitter.register(listener);
|
|
2496
|
+
},
|
|
2497
|
+
onSettled(handler) {
|
|
2498
|
+
return settleEmitter.register(handler);
|
|
2499
|
+
},
|
|
2303
2500
|
[COMMIT_GESTURE_FOR_TEST]: (nextPoints) => {
|
|
2304
2501
|
commitGesture(nextPoints);
|
|
2305
2502
|
},
|
|
@@ -2553,7 +2750,10 @@ function runEdit(opts) {
|
|
|
2553
2750
|
handlesLayer
|
|
2554
2751
|
],
|
|
2555
2752
|
onViewChange: rescaleOnViewChange,
|
|
2556
|
-
onSettled
|
|
2753
|
+
onSettled: (settlement) => {
|
|
2754
|
+
if (onSettled) onSettled();
|
|
2755
|
+
settleFromInteraction(settlement, commitEmitter, settleEmitter);
|
|
2756
|
+
},
|
|
2557
2757
|
attach(c) {
|
|
2558
2758
|
ctx = c;
|
|
2559
2759
|
driver = adapter.createEditPointerDriver();
|
|
@@ -2746,10 +2946,8 @@ function runGroupTransform(opts) {
|
|
|
2746
2946
|
/** Same click-away suppression role as `edit-controller.ts`'s flag of the same name. */
|
|
2747
2947
|
let suppressClickAway = false;
|
|
2748
2948
|
const changeListeners = /* @__PURE__ */ new Set();
|
|
2749
|
-
const
|
|
2750
|
-
const
|
|
2751
|
-
/** Set exactly once, by `emitSettled` — also the "has settled been emitted" latch and the reason replayed to late registrations. */
|
|
2752
|
-
let settledReason = null;
|
|
2949
|
+
const commitEmitter = createCommitEmitter("TransformSession.onCommit listener errors");
|
|
2950
|
+
const settleEmitter = createSettleEmitter("TransformSession.onSettled listener errors");
|
|
2753
2951
|
/** Session-scoped box rotation (ADR-0017 decision #2), shared by the whole group. */
|
|
2754
2952
|
let boxAngle = 0;
|
|
2755
2953
|
/** The group box fitted over every member's rendered geometry, or `null` when nothing projects this frame. */
|
|
@@ -2971,51 +3169,6 @@ function runGroupTransform(opts) {
|
|
|
2971
3169
|
if (errors.length > 1) throw new AggregateError(errors, "TransformSession.onChange listener errors");
|
|
2972
3170
|
}
|
|
2973
3171
|
/**
|
|
2974
|
-
* Deliver a committed exit-snapshot batch to every `onCommit` listener — the
|
|
2975
|
-
* single delivery channel for both the `setMembers`-removed batch and the
|
|
2976
|
-
* close-time batch (#191). Same exception-collection contract as `emitChange`.
|
|
2977
|
-
*/
|
|
2978
|
-
function emitCommit(snapshots) {
|
|
2979
|
-
const errors = [];
|
|
2980
|
-
for (const listener of [...commitListeners]) try {
|
|
2981
|
-
listener(snapshots);
|
|
2982
|
-
} catch (err) {
|
|
2983
|
-
errors.push(err);
|
|
2984
|
-
}
|
|
2985
|
-
if (errors.length === 1) throw errors[0];
|
|
2986
|
-
if (errors.length > 1) throw new AggregateError(errors, "TransformSession.onCommit listener errors");
|
|
2987
|
-
}
|
|
2988
|
-
/**
|
|
2989
|
-
* Map a settle-time error to its `SettleReason`: a `TacticalDrawAbortError`
|
|
2990
|
-
* replays its own `reason` verbatim; anything else (a `produce` throw from
|
|
2991
|
-
* `ctx.commit`) is the close-time snapshot build failing, reported as
|
|
2992
|
-
* `"error"`.
|
|
2993
|
-
*/
|
|
2994
|
-
function settleReasonFromError(error) {
|
|
2995
|
-
return isTacticalDrawAbortError(error) ? error.reason : "error";
|
|
2996
|
-
}
|
|
2997
|
-
/**
|
|
2998
|
-
* Deliver the session's settlement to every `onSettled` listener — fires
|
|
2999
|
-
* exactly once, whichever path settles first wins the reason. Mirrors
|
|
3000
|
-
* `emitCommit`'s exception-collection contract (one throw if single,
|
|
3001
|
-
* `AggregateError` otherwise), so a throwing listener cannot suppress a
|
|
3002
|
-
* later one nor leave `settledReason` unset.
|
|
3003
|
-
*/
|
|
3004
|
-
function emitSettled(reason) {
|
|
3005
|
-
if (settledReason !== null) return;
|
|
3006
|
-
settledReason = reason;
|
|
3007
|
-
const listeners = [...settleListeners];
|
|
3008
|
-
settleListeners.clear();
|
|
3009
|
-
const errors = [];
|
|
3010
|
-
for (const listener of listeners) try {
|
|
3011
|
-
listener(reason);
|
|
3012
|
-
} catch (err) {
|
|
3013
|
-
errors.push(err);
|
|
3014
|
-
}
|
|
3015
|
-
if (errors.length === 1) throw errors[0];
|
|
3016
|
-
if (errors.length > 1) throw new AggregateError(errors, "TransformSession.onSettled listener errors");
|
|
3017
|
-
}
|
|
3018
|
-
/**
|
|
3019
3172
|
* Commit via `ctx.commit` and emit the close batch to `onCommit` listeners,
|
|
3020
3173
|
* then emit the settle to `onSettled` listeners (#194) — in a try/finally so
|
|
3021
3174
|
* a throwing `onCommit` listener cannot starve the settle emission. The
|
|
@@ -3031,9 +3184,9 @@ function runGroupTransform(opts) {
|
|
|
3031
3184
|
function commitClose() {
|
|
3032
3185
|
const snapshots = ctx.commit(rebuildRenderedAll);
|
|
3033
3186
|
if (snapshots !== void 0) try {
|
|
3034
|
-
|
|
3187
|
+
commitEmitter.emit(snapshots);
|
|
3035
3188
|
} finally {
|
|
3036
|
-
|
|
3189
|
+
settleEmitter.emit("close");
|
|
3037
3190
|
}
|
|
3038
3191
|
return snapshots;
|
|
3039
3192
|
}
|
|
@@ -3114,7 +3267,7 @@ function runGroupTransform(opts) {
|
|
|
3114
3267
|
renderAllPreviews();
|
|
3115
3268
|
renderHandlesLayer();
|
|
3116
3269
|
recomputeDirty();
|
|
3117
|
-
if (removedSnapshots.length > 0)
|
|
3270
|
+
if (removedSnapshots.length > 0) commitEmitter.emit(removedSnapshots);
|
|
3118
3271
|
return removedSnapshots;
|
|
3119
3272
|
}
|
|
3120
3273
|
function close() {
|
|
@@ -3153,26 +3306,10 @@ function runGroupTransform(opts) {
|
|
|
3153
3306
|
},
|
|
3154
3307
|
onCommit(listener) {
|
|
3155
3308
|
if (ctx.settled) return () => {};
|
|
3156
|
-
|
|
3157
|
-
return () => {
|
|
3158
|
-
commitListeners.delete(listener);
|
|
3159
|
-
};
|
|
3309
|
+
return commitEmitter.register(listener);
|
|
3160
3310
|
},
|
|
3161
3311
|
onSettled(handler) {
|
|
3162
|
-
|
|
3163
|
-
const reason = settledReason;
|
|
3164
|
-
let cancelled = false;
|
|
3165
|
-
queueMicrotask(() => {
|
|
3166
|
-
if (!cancelled) handler(reason);
|
|
3167
|
-
});
|
|
3168
|
-
return () => {
|
|
3169
|
-
cancelled = true;
|
|
3170
|
-
};
|
|
3171
|
-
}
|
|
3172
|
-
settleListeners.add(handler);
|
|
3173
|
-
return () => {
|
|
3174
|
-
settleListeners.delete(handler);
|
|
3175
|
-
};
|
|
3312
|
+
return settleEmitter.register(handler);
|
|
3176
3313
|
}
|
|
3177
3314
|
};
|
|
3178
3315
|
let activeBoxDrag = null;
|
|
@@ -3290,7 +3427,7 @@ function runGroupTransform(opts) {
|
|
|
3290
3427
|
onViewChange: rescaleOnViewChange,
|
|
3291
3428
|
onSettled: (settlement) => {
|
|
3292
3429
|
if (onSettled) onSettled();
|
|
3293
|
-
if (settlement.status === "rejected")
|
|
3430
|
+
if (settlement.status === "rejected") settleEmitter.emit(settleReasonFromError(settlement.error));
|
|
3294
3431
|
},
|
|
3295
3432
|
attach(c) {
|
|
3296
3433
|
ctx = c;
|
|
@@ -3655,6 +3792,12 @@ var TacticalDraw = class {
|
|
|
3655
3792
|
* `opts.onSession` and commit on `session.commit()`, `dblclick`, or a second
|
|
3656
3793
|
* click on the last added control point. All paths reject with
|
|
3657
3794
|
* `TacticalDrawAbortError` on signal / Escape / destroy / `session.abort()`.
|
|
3795
|
+
*
|
|
3796
|
+
* For a variable-length draw, `DrawSession.onCommit` is the single delivery
|
|
3797
|
+
* channel for the committed snapshot and `DrawSession.onSettled` is the
|
|
3798
|
+
* lifecycle signal for when the session is over and why (#197) — together
|
|
3799
|
+
* they make the returned promise non-load-bearing for hosts that adopt them.
|
|
3800
|
+
* Fixed-length kinds have no session and settle only through the promise.
|
|
3658
3801
|
*/
|
|
3659
3802
|
draw(draft, options) {
|
|
3660
3803
|
this.assertAlive();
|
|
@@ -3683,6 +3826,7 @@ var TacticalDraw = class {
|
|
|
3683
3826
|
measureOptions: draft.options,
|
|
3684
3827
|
measureStyle: draft.style,
|
|
3685
3828
|
measureProperties: draft.properties,
|
|
3829
|
+
measureTextAmplifiers: draft.textAmplifiers,
|
|
3686
3830
|
guide: options?.guide,
|
|
3687
3831
|
sizeAnchor: options?.sizeAnchor,
|
|
3688
3832
|
onSettled
|
|
@@ -3699,6 +3843,7 @@ var TacticalDraw = class {
|
|
|
3699
3843
|
measureOptions: draft.options,
|
|
3700
3844
|
measureStyle: draft.style,
|
|
3701
3845
|
measureProperties: draft.properties,
|
|
3846
|
+
measureTextAmplifiers: draft.textAmplifiers,
|
|
3702
3847
|
guide: options?.guide,
|
|
3703
3848
|
sizeAnchor: options?.sizeAnchor,
|
|
3704
3849
|
onSettled
|
|
@@ -3713,6 +3858,11 @@ var TacticalDraw = class {
|
|
|
3713
3858
|
* Default click-away (`closeOnClickAway: true`) closes the edit on a click
|
|
3714
3859
|
* outside the preview / handle features. Hosts that own close policy pass
|
|
3715
3860
|
* `false`.
|
|
3861
|
+
*
|
|
3862
|
+
* `EditSession.onCommit` is the single delivery channel for the committed
|
|
3863
|
+
* snapshot and `EditSession.onSettled` is the lifecycle signal for when the
|
|
3864
|
+
* session is over and why (#197) — together they make the returned promise
|
|
3865
|
+
* non-load-bearing for hosts that adopt them.
|
|
3716
3866
|
*/
|
|
3717
3867
|
edit(measure, options) {
|
|
3718
3868
|
this.assertAlive();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orbat-mapper/tactical-draw",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.12",
|
|
4
4
|
"description": "Engine-agnostic draw/edit core for tactical control measures (MIL-STD-2525 / APP-6). The MapAdapter ABI and bring-your-own-engine entry point for the tactical-draw package family.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Orbat Mapper",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"access": "public"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@orbat-mapper/control-measures": "^0.2.0-alpha.
|
|
41
|
+
"@orbat-mapper/control-measures": "^0.2.0-alpha.9"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@arethetypeswrong/core": "^0.18.3",
|