@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 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 commitListeners = /* @__PURE__ */ new Set();
2750
- const settleListeners = /* @__PURE__ */ new Set();
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
- emitCommit(snapshots);
3122
+ commitEmitter.emit(snapshots);
3035
3123
  } finally {
3036
- emitSettled("close");
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) emitCommit(removedSnapshots);
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
- commitListeners.add(listener);
3157
- return () => {
3158
- commitListeners.delete(listener);
3159
- };
3244
+ return commitEmitter.register(listener);
3160
3245
  },
3161
3246
  onSettled(handler) {
3162
- if (settledReason !== null) {
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") emitSettled(settleReasonFromError(settlement.error));
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.10",
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",