@orbat-mapper/tactical-draw 0.2.0-alpha.10 → 0.2.0-alpha.11
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 +113 -13
- package/dist/index.mjs +156 -73
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -490,6 +490,24 @@ interface InteractionStyle {
|
|
|
490
490
|
*/
|
|
491
491
|
type EditMode = "reshape" | "transform" | "delete";
|
|
492
492
|
//#endregion
|
|
493
|
+
//#region src/tactical-draw/session-events.d.ts
|
|
494
|
+
/**
|
|
495
|
+
* The reason a session (`TransformSession`, `EditSession`, or `DrawSession`)
|
|
496
|
+
* settled, delivered to that session's `onSettled` handler (#191 / #194 /
|
|
497
|
+
* #197). `"close"` covers every commit path of the owning session —
|
|
498
|
+
* `TransformSession.close()` / `EditSession.close()` / `DrawSession.commit()`,
|
|
499
|
+
* a click-away close, a gesture commit (dblclick, a second click/tap on the
|
|
500
|
+
* finishing vertex, a click on another graphics-layer measure), or
|
|
501
|
+
* `TransformSession.setMembers([])`. `"error"` is the commit-time snapshot
|
|
502
|
+
* build throwing (the outer promise rejects with that error, not a
|
|
503
|
+
* `TacticalDrawAbortError`). The remaining values replay
|
|
504
|
+
* `TacticalDrawAbortError.reason` verbatim: `"escape"`, `"signal"`,
|
|
505
|
+
* `"session"` (`session.abort()` / `event.abort()`), `"preempted"`,
|
|
506
|
+
* `"destroyed"`, and `"removed"` (edit / transform only — a measure dropped
|
|
507
|
+
* from `td.render()`'s measure set).
|
|
508
|
+
*/
|
|
509
|
+
type SettleReason = "close" | "error" | TacticalDrawAbortReason;
|
|
510
|
+
//#endregion
|
|
493
511
|
//#region src/tactical-draw/edit-session.d.ts
|
|
494
512
|
/**
|
|
495
513
|
* Surface delivered via `td.edit(measure, opts).onSession` and synchronously
|
|
@@ -497,7 +515,8 @@ type EditMode = "reshape" | "transform" | "delete";
|
|
|
497
515
|
*
|
|
498
516
|
* `measure` is the immutable edit-start input. `controlPoints` is live working
|
|
499
517
|
* state. After the session settles (`close` / `abort`), every method is a
|
|
500
|
-
* silent no-op
|
|
518
|
+
* silent no-op (except [[onSettled]] — late registration still delivers,
|
|
519
|
+
* asynchronously).
|
|
501
520
|
*/
|
|
502
521
|
interface EditSession {
|
|
503
522
|
/** Immutable edit-start input. */
|
|
@@ -557,6 +576,47 @@ interface EditSession {
|
|
|
557
576
|
* have run.
|
|
558
577
|
*/
|
|
559
578
|
onChange(listener: (event: EditChangeEvent) => void): () => void;
|
|
579
|
+
/**
|
|
580
|
+
* Subscribe to the committed snapshot — the single delivery channel for the
|
|
581
|
+
* `ControlMeasureSnapshot` this session ever commits (#197, mirroring
|
|
582
|
+
* `TransformSession.onCommit` #191). Fires exactly once, synchronously from
|
|
583
|
+
* within the [[close]] call that produced it (`session.close()`,
|
|
584
|
+
* `event.close()` from [[onChange]], a click-away close, or a click on
|
|
585
|
+
* another graphics-layer measure), with the same reference [[close]]
|
|
586
|
+
* returns and the outer `edit` promise resolves with. Never fires on abort.
|
|
587
|
+
*
|
|
588
|
+
* Listener exceptions are collected; after every listener has run, they are
|
|
589
|
+
* rethrown (one if single, `AggregateError` otherwise) — mirrors
|
|
590
|
+
* [[onChange]]. Returns an unsubscribe function; registering after the
|
|
591
|
+
* session has settled returns a no-op unsubscribe and the listener never
|
|
592
|
+
* fires.
|
|
593
|
+
*/
|
|
594
|
+
onCommit(handler: (snapshot: ControlMeasureSnapshot) => void): () => void;
|
|
595
|
+
/**
|
|
596
|
+
* Subscribe to the session's settlement — the lifecycle complement of
|
|
597
|
+
* [[onCommit]]: `onCommit` delivers *what* was committed, `onSettled` says
|
|
598
|
+
* the session is *over* and why (#197, mirroring
|
|
599
|
+
* `TransformSession.onSettled` #194). Fires exactly once, when the session
|
|
600
|
+
* settles by any path, commit or abort, with a `SettleReason`.
|
|
601
|
+
*
|
|
602
|
+
* - On the commit path it fires **after** [[onCommit]]'s emission, so a
|
|
603
|
+
* listener that clears selection state observes a document that already
|
|
604
|
+
* contains the committed snapshot.
|
|
605
|
+
* - On every path it fires **after** the facade has already cleared
|
|
606
|
+
* `td.activeSession` — so fold-then-render works from inside the handler.
|
|
607
|
+
* - `reason` distinguishes commit from abort but is deliberately coarse —
|
|
608
|
+
* no payload; the snapshot was already delivered through [[onCommit]].
|
|
609
|
+
*
|
|
610
|
+
* Registering after the session has already settled is **not** a no-op
|
|
611
|
+
* (unlike [[onChange]] / [[onCommit]]): the handler is invoked
|
|
612
|
+
* asynchronously (on a microtask) exactly once with the settle reason;
|
|
613
|
+
* unsubscribing before that microtask runs cancels the delivery.
|
|
614
|
+
*
|
|
615
|
+
* Listener exceptions are collected; after every listener has run, they are
|
|
616
|
+
* rethrown (one if single, `AggregateError` otherwise) — mirrors
|
|
617
|
+
* [[onChange]] / [[onCommit]]. Returns an unsubscribe function.
|
|
618
|
+
*/
|
|
619
|
+
onSettled(handler: (reason: SettleReason) => void): () => void;
|
|
560
620
|
}
|
|
561
621
|
/**
|
|
562
622
|
* Delivered to `EditSession.onChange` listeners once per completed gesture.
|
|
@@ -586,17 +646,6 @@ interface EditChangeEvent {
|
|
|
586
646
|
}
|
|
587
647
|
//#endregion
|
|
588
648
|
//#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
649
|
/**
|
|
601
650
|
* Surface delivered via `td.editMany(measures, opts).onSession` and
|
|
602
651
|
* synchronously available from `td.activeTransformSession` while the group
|
|
@@ -793,7 +842,8 @@ interface TransformChangeEvent {
|
|
|
793
842
|
* kinds. Also available from `td.activeSession` while the draw is live.
|
|
794
843
|
*
|
|
795
844
|
* After the draw has settled — either via `commit()` returning `true` or
|
|
796
|
-
* `abort()` resolving — every method is a silent no-op
|
|
845
|
+
* `abort()` resolving — every method is a silent no-op (except [[onSettled]]
|
|
846
|
+
* — late registration still delivers, asynchronously).
|
|
797
847
|
*/
|
|
798
848
|
interface DrawSession {
|
|
799
849
|
/** Live snapshot of the committed control points. Copy on read. */
|
|
@@ -815,6 +865,45 @@ interface DrawSession {
|
|
|
815
865
|
* are collected and rethrown after all listeners run.
|
|
816
866
|
*/
|
|
817
867
|
onChange(listener: (session: DrawSession) => void): () => void;
|
|
868
|
+
/**
|
|
869
|
+
* Subscribe to the committed snapshot — the single delivery channel for the
|
|
870
|
+
* `ControlMeasureSnapshot` this session ever commits (#197, mirroring
|
|
871
|
+
* `TransformSession.onCommit` #191). Fires exactly once, synchronously from
|
|
872
|
+
* within the commit that produced it (`session.commit()`, dblclick, or a
|
|
873
|
+
* second click/tap on the finishing vertex), with the same reference the
|
|
874
|
+
* outer `draw` promise resolves with — the committed snapshot, not just the
|
|
875
|
+
* measure. Never fires on abort.
|
|
876
|
+
*
|
|
877
|
+
* Listener exceptions are collected; after every listener has run, they are
|
|
878
|
+
* rethrown (one if single, `AggregateError` otherwise) — mirrors
|
|
879
|
+
* [[onChange]]. Returns an unsubscribe function; registering after the
|
|
880
|
+
* session has settled returns a no-op unsubscribe and the listener never
|
|
881
|
+
* fires.
|
|
882
|
+
*/
|
|
883
|
+
onCommit(handler: (snapshot: ControlMeasureSnapshot) => void): () => void;
|
|
884
|
+
/**
|
|
885
|
+
* Subscribe to the session's settlement — the lifecycle complement of
|
|
886
|
+
* [[onCommit]]: `onCommit` delivers *what* was committed, `onSettled` says
|
|
887
|
+
* the session is *over* and why (#197, mirroring
|
|
888
|
+
* `TransformSession.onSettled` #194). Fires exactly once, when the session
|
|
889
|
+
* settles by any path, commit or abort, with a `SettleReason`.
|
|
890
|
+
*
|
|
891
|
+
* - On the commit path it fires **after** [[onCommit]]'s emission.
|
|
892
|
+
* - On every path it fires **after** the facade has already cleared
|
|
893
|
+
* `td.activeSession` — so fold-then-render works from inside the handler.
|
|
894
|
+
* - `reason` distinguishes commit from abort but is deliberately coarse —
|
|
895
|
+
* no payload; the snapshot was already delivered through [[onCommit]].
|
|
896
|
+
*
|
|
897
|
+
* Registering after the session has already settled is **not** a no-op
|
|
898
|
+
* (unlike [[onChange]] / [[onCommit]]): the handler is invoked
|
|
899
|
+
* asynchronously (on a microtask) exactly once with the settle reason;
|
|
900
|
+
* unsubscribing before that microtask runs cancels the delivery.
|
|
901
|
+
*
|
|
902
|
+
* Listener exceptions are collected; after every listener has run, they are
|
|
903
|
+
* rethrown (one if single, `AggregateError` otherwise) — mirrors
|
|
904
|
+
* [[onChange]] / [[onCommit]]. Returns an unsubscribe function.
|
|
905
|
+
*/
|
|
906
|
+
onSettled(handler: (reason: SettleReason) => void): () => void;
|
|
818
907
|
}
|
|
819
908
|
//#endregion
|
|
820
909
|
//#region src/tactical-draw/tactical-draw.d.ts
|
|
@@ -1144,6 +1233,12 @@ declare class TacticalDraw {
|
|
|
1144
1233
|
* `opts.onSession` and commit on `session.commit()`, `dblclick`, or a second
|
|
1145
1234
|
* click on the last added control point. All paths reject with
|
|
1146
1235
|
* `TacticalDrawAbortError` on signal / Escape / destroy / `session.abort()`.
|
|
1236
|
+
*
|
|
1237
|
+
* For a variable-length draw, `DrawSession.onCommit` is the single delivery
|
|
1238
|
+
* channel for the committed snapshot and `DrawSession.onSettled` is the
|
|
1239
|
+
* lifecycle signal for when the session is over and why (#197) — together
|
|
1240
|
+
* they make the returned promise non-load-bearing for hosts that adopt them.
|
|
1241
|
+
* Fixed-length kinds have no session and settle only through the promise.
|
|
1147
1242
|
*/
|
|
1148
1243
|
draw<K extends ControlMeasureKind>(draft: DrawMeasureDraft<K>, options?: DrawOptions): Promise<ControlMeasureSnapshot<K>>;
|
|
1149
1244
|
/**
|
|
@@ -1155,6 +1250,11 @@ declare class TacticalDraw {
|
|
|
1155
1250
|
* Default click-away (`closeOnClickAway: true`) closes the edit on a click
|
|
1156
1251
|
* outside the preview / handle features. Hosts that own close policy pass
|
|
1157
1252
|
* `false`.
|
|
1253
|
+
*
|
|
1254
|
+
* `EditSession.onCommit` is the single delivery channel for the committed
|
|
1255
|
+
* snapshot and `EditSession.onSettled` is the lifecycle signal for when the
|
|
1256
|
+
* session is over and why (#197) — together they make the returned promise
|
|
1257
|
+
* non-load-bearing for hosts that adopt them.
|
|
1158
1258
|
*/
|
|
1159
1259
|
edit(measure: ControlMeasure, options?: EditOptions): Promise<ControlMeasureSnapshot>;
|
|
1160
1260
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -1007,6 +1007,117 @@ function outlinePreview$1(points) {
|
|
|
1007
1007
|
return features;
|
|
1008
1008
|
}
|
|
1009
1009
|
//#endregion
|
|
1010
|
+
//#region src/tactical-draw/session-events.ts
|
|
1011
|
+
/**
|
|
1012
|
+
* Map a settle-time error to its `SettleReason`: a `TacticalDrawAbortError`
|
|
1013
|
+
* replays its own `reason` verbatim; anything else (a `produce` throw from
|
|
1014
|
+
* `ctx.commit`) is the commit-time snapshot build failing, reported as
|
|
1015
|
+
* `"error"`.
|
|
1016
|
+
*/
|
|
1017
|
+
function settleReasonFromError(error) {
|
|
1018
|
+
return isTacticalDrawAbortError(error) ? error.reason : "error";
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Fan a `runInteraction` settlement out to a session's commit + settle
|
|
1022
|
+
* emitters — the shared body of the `onSettled` wrappers in the edit and draw
|
|
1023
|
+
* controllers (transform emits its close-path settle from `commitClose`
|
|
1024
|
+
* instead, so it does not use this). On the commit path, emit the snapshot
|
|
1025
|
+
* then the `"close"` settle in a try/finally so a throwing `onCommit` listener
|
|
1026
|
+
* cannot starve the settle emission; on the reject path, emit the settle with
|
|
1027
|
+
* the reason derived from the error. Callers run their own facade
|
|
1028
|
+
* slot-clearing (`onSettled()`) before this so a listener observes a cleared
|
|
1029
|
+
* `td.activeSession`.
|
|
1030
|
+
*/
|
|
1031
|
+
function settleFromInteraction(settlement, commitEmitter, settleEmitter) {
|
|
1032
|
+
if (settlement.status === "committed") try {
|
|
1033
|
+
commitEmitter.emit(settlement.value);
|
|
1034
|
+
} finally {
|
|
1035
|
+
settleEmitter.emit("close");
|
|
1036
|
+
}
|
|
1037
|
+
else settleEmitter.emit(settleReasonFromError(settlement.error));
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Subscribe/emit machinery shared by `TransformSession.onSettled`,
|
|
1041
|
+
* `EditSession.onSettled`, and `DrawSession.onSettled` (#191 / #194 / #197):
|
|
1042
|
+
* a settle-exactly-once latch, synchronous emission to every listener with
|
|
1043
|
+
* exception collection (single rethrow, `AggregateError` otherwise when more
|
|
1044
|
+
* than one listener throws), and a late-registration path that delivers the
|
|
1045
|
+
* latched reason on a microtask exactly once when a handler subscribes after
|
|
1046
|
+
* the session has already settled.
|
|
1047
|
+
*
|
|
1048
|
+
* `listenerErrorsMessage` becomes the `AggregateError` message — pass a
|
|
1049
|
+
* session-specific string (e.g. `"EditSession.onSettled listener errors"`).
|
|
1050
|
+
*/
|
|
1051
|
+
function createSettleEmitter(listenerErrorsMessage) {
|
|
1052
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1053
|
+
/** Set exactly once, by `emit` — also the "has settled" latch and the reason replayed to late registrations. */
|
|
1054
|
+
let settledReason = null;
|
|
1055
|
+
return {
|
|
1056
|
+
emit(reason) {
|
|
1057
|
+
if (settledReason !== null) return;
|
|
1058
|
+
settledReason = reason;
|
|
1059
|
+
const current = [...listeners];
|
|
1060
|
+
listeners.clear();
|
|
1061
|
+
const errors = [];
|
|
1062
|
+
for (const listener of current) try {
|
|
1063
|
+
listener(reason);
|
|
1064
|
+
} catch (err) {
|
|
1065
|
+
errors.push(err);
|
|
1066
|
+
}
|
|
1067
|
+
if (errors.length === 1) throw errors[0];
|
|
1068
|
+
if (errors.length > 1) throw new AggregateError(errors, listenerErrorsMessage);
|
|
1069
|
+
},
|
|
1070
|
+
register(handler) {
|
|
1071
|
+
if (settledReason !== null) {
|
|
1072
|
+
const reason = settledReason;
|
|
1073
|
+
let cancelled = false;
|
|
1074
|
+
queueMicrotask(() => {
|
|
1075
|
+
if (!cancelled) handler(reason);
|
|
1076
|
+
});
|
|
1077
|
+
return () => {
|
|
1078
|
+
cancelled = true;
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
listeners.add(handler);
|
|
1082
|
+
return () => {
|
|
1083
|
+
listeners.delete(handler);
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Subscribe/emit machinery shared by `TransformSession.onCommit`,
|
|
1090
|
+
* `EditSession.onCommit`, and `DrawSession.onCommit`: a listener set with
|
|
1091
|
+
* synchronous, exception-collecting emission (single rethrow, `AggregateError`
|
|
1092
|
+
* otherwise). Unlike `createSettleEmitter`, `emit` may be called more than
|
|
1093
|
+
* once — a transform's `setMembers` can commit several removal batches before
|
|
1094
|
+
* the close batch — and there is no latch or late-registration path:
|
|
1095
|
+
* registration-time `ctx.settled` guards stay in the controllers, mirroring
|
|
1096
|
+
* `onChange`.
|
|
1097
|
+
*/
|
|
1098
|
+
function createCommitEmitter(listenerErrorsMessage) {
|
|
1099
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1100
|
+
return {
|
|
1101
|
+
emit(value) {
|
|
1102
|
+
const current = [...listeners];
|
|
1103
|
+
const errors = [];
|
|
1104
|
+
for (const listener of current) try {
|
|
1105
|
+
listener(value);
|
|
1106
|
+
} catch (err) {
|
|
1107
|
+
errors.push(err);
|
|
1108
|
+
}
|
|
1109
|
+
if (errors.length === 1) throw errors[0];
|
|
1110
|
+
if (errors.length > 1) throw new AggregateError(errors, listenerErrorsMessage);
|
|
1111
|
+
},
|
|
1112
|
+
register(listener) {
|
|
1113
|
+
listeners.add(listener);
|
|
1114
|
+
return () => {
|
|
1115
|
+
listeners.delete(listener);
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
//#endregion
|
|
1010
1121
|
//#region src/tactical-draw/draw-controller-variable.ts
|
|
1011
1122
|
const PREVIEW_CM_ID = "__td_preview__";
|
|
1012
1123
|
const PREVIEW_CURSOR = "crosshair";
|
|
@@ -1064,6 +1175,8 @@ function runVariableLengthDraw(options) {
|
|
|
1064
1175
|
let ctx;
|
|
1065
1176
|
const savedCursor = "";
|
|
1066
1177
|
const changeListeners = /* @__PURE__ */ new Set();
|
|
1178
|
+
const commitEmitter = createCommitEmitter("DrawSession.onCommit listener errors");
|
|
1179
|
+
const settleEmitter = createSettleEmitter("DrawSession.onSettled listener errors");
|
|
1067
1180
|
function canCommitNow() {
|
|
1068
1181
|
const n = committed.length;
|
|
1069
1182
|
if (typeof max === "number" && n > max) return false;
|
|
@@ -1100,6 +1213,13 @@ function runVariableLengthDraw(options) {
|
|
|
1100
1213
|
return () => {
|
|
1101
1214
|
changeListeners.delete(listener);
|
|
1102
1215
|
};
|
|
1216
|
+
},
|
|
1217
|
+
onCommit(listener) {
|
|
1218
|
+
if (ctx.settled) return () => {};
|
|
1219
|
+
return commitEmitter.register(listener);
|
|
1220
|
+
},
|
|
1221
|
+
onSettled(handler) {
|
|
1222
|
+
return settleEmitter.register(handler);
|
|
1103
1223
|
}
|
|
1104
1224
|
};
|
|
1105
1225
|
function fireChange() {
|
|
@@ -1199,7 +1319,10 @@ function runVariableLengthDraw(options) {
|
|
|
1199
1319
|
signal,
|
|
1200
1320
|
layers: [previewLayer, guideLayer],
|
|
1201
1321
|
onViewChange: rescaleOnViewChange,
|
|
1202
|
-
onSettled
|
|
1322
|
+
onSettled: (settlement) => {
|
|
1323
|
+
if (onSettled) onSettled();
|
|
1324
|
+
settleFromInteraction(settlement, commitEmitter, settleEmitter);
|
|
1325
|
+
},
|
|
1203
1326
|
attach(c) {
|
|
1204
1327
|
ctx = c;
|
|
1205
1328
|
c.onTeardown(() => {
|
|
@@ -1897,6 +2020,8 @@ function runEdit(opts) {
|
|
|
1897
2020
|
*/
|
|
1898
2021
|
let pendingModes = null;
|
|
1899
2022
|
const changeListeners = /* @__PURE__ */ new Set();
|
|
2023
|
+
const commitEmitter = createCommitEmitter("EditSession.onCommit listener errors");
|
|
2024
|
+
const settleEmitter = createSettleEmitter("EditSession.onSettled listener errors");
|
|
1900
2025
|
/**
|
|
1901
2026
|
* The transform box's rotation, in radians, session-scoped (ADR-0017 decision
|
|
1902
2027
|
* #2): starts at `0` (axis-aligned) and accumulates every committed rotate
|
|
@@ -2300,6 +2425,13 @@ function runEdit(opts) {
|
|
|
2300
2425
|
changeListeners.delete(listener);
|
|
2301
2426
|
};
|
|
2302
2427
|
},
|
|
2428
|
+
onCommit(listener) {
|
|
2429
|
+
if (ctx.settled) return () => {};
|
|
2430
|
+
return commitEmitter.register(listener);
|
|
2431
|
+
},
|
|
2432
|
+
onSettled(handler) {
|
|
2433
|
+
return settleEmitter.register(handler);
|
|
2434
|
+
},
|
|
2303
2435
|
[COMMIT_GESTURE_FOR_TEST]: (nextPoints) => {
|
|
2304
2436
|
commitGesture(nextPoints);
|
|
2305
2437
|
},
|
|
@@ -2553,7 +2685,10 @@ function runEdit(opts) {
|
|
|
2553
2685
|
handlesLayer
|
|
2554
2686
|
],
|
|
2555
2687
|
onViewChange: rescaleOnViewChange,
|
|
2556
|
-
onSettled
|
|
2688
|
+
onSettled: (settlement) => {
|
|
2689
|
+
if (onSettled) onSettled();
|
|
2690
|
+
settleFromInteraction(settlement, commitEmitter, settleEmitter);
|
|
2691
|
+
},
|
|
2557
2692
|
attach(c) {
|
|
2558
2693
|
ctx = c;
|
|
2559
2694
|
driver = adapter.createEditPointerDriver();
|
|
@@ -2746,10 +2881,8 @@ function runGroupTransform(opts) {
|
|
|
2746
2881
|
/** Same click-away suppression role as `edit-controller.ts`'s flag of the same name. */
|
|
2747
2882
|
let suppressClickAway = false;
|
|
2748
2883
|
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;
|
|
2884
|
+
const commitEmitter = createCommitEmitter("TransformSession.onCommit listener errors");
|
|
2885
|
+
const settleEmitter = createSettleEmitter("TransformSession.onSettled listener errors");
|
|
2753
2886
|
/** Session-scoped box rotation (ADR-0017 decision #2), shared by the whole group. */
|
|
2754
2887
|
let boxAngle = 0;
|
|
2755
2888
|
/** The group box fitted over every member's rendered geometry, or `null` when nothing projects this frame. */
|
|
@@ -2971,51 +3104,6 @@ function runGroupTransform(opts) {
|
|
|
2971
3104
|
if (errors.length > 1) throw new AggregateError(errors, "TransformSession.onChange listener errors");
|
|
2972
3105
|
}
|
|
2973
3106
|
/**
|
|
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
3107
|
* Commit via `ctx.commit` and emit the close batch to `onCommit` listeners,
|
|
3020
3108
|
* then emit the settle to `onSettled` listeners (#194) — in a try/finally so
|
|
3021
3109
|
* a throwing `onCommit` listener cannot starve the settle emission. The
|
|
@@ -3031,9 +3119,9 @@ function runGroupTransform(opts) {
|
|
|
3031
3119
|
function commitClose() {
|
|
3032
3120
|
const snapshots = ctx.commit(rebuildRenderedAll);
|
|
3033
3121
|
if (snapshots !== void 0) try {
|
|
3034
|
-
|
|
3122
|
+
commitEmitter.emit(snapshots);
|
|
3035
3123
|
} finally {
|
|
3036
|
-
|
|
3124
|
+
settleEmitter.emit("close");
|
|
3037
3125
|
}
|
|
3038
3126
|
return snapshots;
|
|
3039
3127
|
}
|
|
@@ -3114,7 +3202,7 @@ function runGroupTransform(opts) {
|
|
|
3114
3202
|
renderAllPreviews();
|
|
3115
3203
|
renderHandlesLayer();
|
|
3116
3204
|
recomputeDirty();
|
|
3117
|
-
if (removedSnapshots.length > 0)
|
|
3205
|
+
if (removedSnapshots.length > 0) commitEmitter.emit(removedSnapshots);
|
|
3118
3206
|
return removedSnapshots;
|
|
3119
3207
|
}
|
|
3120
3208
|
function close() {
|
|
@@ -3153,26 +3241,10 @@ function runGroupTransform(opts) {
|
|
|
3153
3241
|
},
|
|
3154
3242
|
onCommit(listener) {
|
|
3155
3243
|
if (ctx.settled) return () => {};
|
|
3156
|
-
|
|
3157
|
-
return () => {
|
|
3158
|
-
commitListeners.delete(listener);
|
|
3159
|
-
};
|
|
3244
|
+
return commitEmitter.register(listener);
|
|
3160
3245
|
},
|
|
3161
3246
|
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
|
-
};
|
|
3247
|
+
return settleEmitter.register(handler);
|
|
3176
3248
|
}
|
|
3177
3249
|
};
|
|
3178
3250
|
let activeBoxDrag = null;
|
|
@@ -3290,7 +3362,7 @@ function runGroupTransform(opts) {
|
|
|
3290
3362
|
onViewChange: rescaleOnViewChange,
|
|
3291
3363
|
onSettled: (settlement) => {
|
|
3292
3364
|
if (onSettled) onSettled();
|
|
3293
|
-
if (settlement.status === "rejected")
|
|
3365
|
+
if (settlement.status === "rejected") settleEmitter.emit(settleReasonFromError(settlement.error));
|
|
3294
3366
|
},
|
|
3295
3367
|
attach(c) {
|
|
3296
3368
|
ctx = c;
|
|
@@ -3655,6 +3727,12 @@ var TacticalDraw = class {
|
|
|
3655
3727
|
* `opts.onSession` and commit on `session.commit()`, `dblclick`, or a second
|
|
3656
3728
|
* click on the last added control point. All paths reject with
|
|
3657
3729
|
* `TacticalDrawAbortError` on signal / Escape / destroy / `session.abort()`.
|
|
3730
|
+
*
|
|
3731
|
+
* For a variable-length draw, `DrawSession.onCommit` is the single delivery
|
|
3732
|
+
* channel for the committed snapshot and `DrawSession.onSettled` is the
|
|
3733
|
+
* lifecycle signal for when the session is over and why (#197) — together
|
|
3734
|
+
* they make the returned promise non-load-bearing for hosts that adopt them.
|
|
3735
|
+
* Fixed-length kinds have no session and settle only through the promise.
|
|
3658
3736
|
*/
|
|
3659
3737
|
draw(draft, options) {
|
|
3660
3738
|
this.assertAlive();
|
|
@@ -3713,6 +3791,11 @@ var TacticalDraw = class {
|
|
|
3713
3791
|
* Default click-away (`closeOnClickAway: true`) closes the edit on a click
|
|
3714
3792
|
* outside the preview / handle features. Hosts that own close policy pass
|
|
3715
3793
|
* `false`.
|
|
3794
|
+
*
|
|
3795
|
+
* `EditSession.onCommit` is the single delivery channel for the committed
|
|
3796
|
+
* snapshot and `EditSession.onSettled` is the lifecycle signal for when the
|
|
3797
|
+
* session is over and why (#197) — together they make the returned promise
|
|
3798
|
+
* non-load-bearing for hosts that adopt them.
|
|
3716
3799
|
*/
|
|
3717
3800
|
edit(measure, options) {
|
|
3718
3801
|
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.11",
|
|
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",
|