@orbat-mapper/tactical-draw 0.2.0-alpha.11 → 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 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;
@@ -527,6 +534,13 @@ interface EditSession {
527
534
  readonly options: ControlMeasure["options"];
528
535
  /** Live working style. Copy on read. */
529
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;
530
544
  /** `true` after the first working-copy mutation; back to `false` within tolerance. */
531
545
  readonly dirty: boolean;
532
546
  readonly modes: readonly EditMode[];
@@ -564,6 +578,13 @@ interface EditSession {
564
578
  setSizeAnchor(anchor: SizeAnchor): void;
565
579
  /** Shallow-merge per-measure style into the live working copy. */
566
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;
567
588
  /**
568
589
  * Subscribe to working-copy changes. The listener receives an
569
590
  * [[EditChangeEvent]] for each completed gesture (drag end, insert/delete
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, {
@@ -1129,7 +1131,7 @@ const PREVIEW_CURSOR = "crosshair";
1129
1131
  * added point again" as additional gesture-level commit triggers.
1130
1132
  */
1131
1133
  function runVariableLengthDraw(options) {
1132
- 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;
1133
1135
  const metadata = getControlMeasureMetadata(kind);
1134
1136
  if (typeof metadata.minCoordinates !== "number") throw new Error(`runVariableLengthDraw: kind "${kind}" has no minCoordinates metadata`);
1135
1137
  const min = metadata.minCoordinates;
@@ -1164,7 +1166,8 @@ function runVariableLengthDraw(options) {
1164
1166
  kind,
1165
1167
  controlPoints: initialPoints.map((p) => [...p]),
1166
1168
  ...measureOptions !== void 0 ? { options: measureOptions } : {},
1167
- ...measureStyle !== void 0 ? { style: measureStyle } : {}
1169
+ ...measureStyle !== void 0 ? { style: measureStyle } : {},
1170
+ ...measureTextAmplifiers !== void 0 ? { textAmplifiers: measureTextAmplifiers } : {}
1168
1171
  }, {
1169
1172
  ...renderOpts,
1170
1173
  validationMode: "throw"
@@ -1287,7 +1290,7 @@ function runVariableLengthDraw(options) {
1287
1290
  points.splice(idx, 0, rubberBand);
1288
1291
  } else points = [...committed];
1289
1292
  if (rule) points = rule.derive(points);
1290
- 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);
1291
1294
  ctx.writeLayer(previewLayer, previewFeatures);
1292
1295
  const guideFeatures = points.length === 0 || !guideEnabled ? [] : buildGuideFeatures(points, isAreaKind, trailingFixed);
1293
1296
  ctx.writeLayer(guideLayer, guideFeatures);
@@ -1306,7 +1309,8 @@ function runVariableLengthDraw(options) {
1306
1309
  controlPoints: committed.map((p) => [...p]),
1307
1310
  ...measureOptions !== void 0 ? { options: measureOptions } : {},
1308
1311
  ...measureStyle !== void 0 ? { style: measureStyle } : {},
1309
- ...measureProperties !== void 0 ? { properties: measureProperties } : {}
1312
+ ...measureProperties !== void 0 ? { properties: measureProperties } : {},
1313
+ ...measureTextAmplifiers !== void 0 ? { textAmplifiers: measureTextAmplifiers } : {}
1310
1314
  }, adapter);
1311
1315
  const measure = sizeAnchor === "screen" ? built : bakePixelSizeToGround(built, adapter);
1312
1316
  return {
@@ -1357,13 +1361,14 @@ function isValidPosition(value) {
1357
1361
  }
1358
1362
  return true;
1359
1363
  }
1360
- function buildPreviewFeatures(adapter, kind, points, measureOptions, measureStyle, renderOpts) {
1364
+ function buildPreviewFeatures(adapter, kind, points, measureOptions, measureStyle, measureTextAmplifiers, renderOpts) {
1361
1365
  const measure = {
1362
1366
  id: PREVIEW_CM_ID,
1363
1367
  kind,
1364
1368
  controlPoints: points,
1365
1369
  ...measureOptions !== void 0 ? { options: measureOptions } : {},
1366
- ...measureStyle !== void 0 ? { style: measureStyle } : {}
1370
+ ...measureStyle !== void 0 ? { style: measureStyle } : {},
1371
+ ...measureTextAmplifiers !== void 0 ? { textAmplifiers: measureTextAmplifiers } : {}
1367
1372
  };
1368
1373
  try {
1369
1374
  return renderMeasureForAdapter(adapter, measure, renderOpts).features;
@@ -1996,6 +2001,7 @@ function runEdit(opts) {
1996
2001
  const editStartPoints = measure.controlPoints.map((p) => [...p]);
1997
2002
  const workingPoints = editStartPoints.map((p) => [...p]);
1998
2003
  let workingOptions = { ...measure.options ?? {} };
2004
+ let workingTextAmplifiers = measure.textAmplifiers && Object.keys(measure.textAmplifiers).length > 0 ? { ...measure.textAmplifiers } : void 0;
1999
2005
  let modes = requestedModes;
2000
2006
  let dirty = false;
2001
2007
  let nonGeometryDirty = false;
@@ -2084,6 +2090,8 @@ function runEdit(opts) {
2084
2090
  options: workingOptions,
2085
2091
  controlPoints: workingPoints.map((p) => [...p])
2086
2092
  };
2093
+ if (workingTextAmplifiers) working.textAmplifiers = workingTextAmplifiers;
2094
+ else delete working.textAmplifiers;
2087
2095
  let features;
2088
2096
  try {
2089
2097
  features = renderMeasureForAdapter(adapter, working, renderOpts).features;
@@ -2135,6 +2143,8 @@ function runEdit(opts) {
2135
2143
  options,
2136
2144
  controlPoints: workingPoints.map((p) => [...p])
2137
2145
  };
2146
+ if (workingTextAmplifiers) working.textAmplifiers = workingTextAmplifiers;
2147
+ else delete working.textAmplifiers;
2138
2148
  const committed = sizeAnchor === "screen" ? working : bakePixelSizeToGround(working, adapter);
2139
2149
  return {
2140
2150
  measure: cloneControlMeasure(committed),
@@ -2186,7 +2196,7 @@ function runEdit(opts) {
2186
2196
  * - Reject restores the working state to the pre-gesture snapshot and
2187
2197
  * recomputes `dirty`. Close/abort settle the session as normal.
2188
2198
  */
2189
- function emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered) {
2199
+ function emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered, previousTextAmplifiers) {
2190
2200
  if (ctx.settled) return;
2191
2201
  let rejectRequested = false;
2192
2202
  let closeRequested = false;
@@ -2218,6 +2228,7 @@ function runEdit(opts) {
2218
2228
  replaceWorkingOptions(previousOptions);
2219
2229
  currentStyle = previousStyle;
2220
2230
  sizeAnchor = previousSizeAnchor;
2231
+ workingTextAmplifiers = previousTextAmplifiers;
2221
2232
  nonGeometryDirty = previousNonGeometryDirty;
2222
2233
  renderPreview();
2223
2234
  renderHandlesLayer();
@@ -2242,6 +2253,7 @@ function runEdit(opts) {
2242
2253
  const previousOptions = { ...preGestureOptions ?? workingOptions ?? {} };
2243
2254
  const previousStyle = currentStyle;
2244
2255
  const previousNonGeometryDirty = nonGeometryDirty;
2256
+ const previousTextAmplifiers = workingTextAmplifiers;
2245
2257
  const previousRendered = rebuildRenderedWith(previousOptions);
2246
2258
  replaceWorkingPoints(nextPoints);
2247
2259
  let currentRendered;
@@ -2259,7 +2271,7 @@ function runEdit(opts) {
2259
2271
  recomputeDirty();
2260
2272
  dragInFlight = false;
2261
2273
  drainPendingModes();
2262
- emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered);
2274
+ emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers);
2263
2275
  }
2264
2276
  function setOptions(partial) {
2265
2277
  if (ctx.settled) return;
@@ -2267,6 +2279,7 @@ function runEdit(opts) {
2267
2279
  const previousOptions = { ...workingOptions ?? {} };
2268
2280
  const previousStyle = currentStyle;
2269
2281
  const previousNonGeometryDirty = nonGeometryDirty;
2282
+ const previousTextAmplifiers = workingTextAmplifiers;
2270
2283
  const previousRendered = rebuildRendered();
2271
2284
  replaceWorkingOptions({
2272
2285
  ...workingOptions ?? {},
@@ -2282,7 +2295,7 @@ function runEdit(opts) {
2282
2295
  renderPreview();
2283
2296
  nonGeometryDirty = true;
2284
2297
  dirty = true;
2285
- emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered);
2298
+ emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers);
2286
2299
  }
2287
2300
  function setStyle(partial) {
2288
2301
  if (ctx.settled) return;
@@ -2290,6 +2303,7 @@ function runEdit(opts) {
2290
2303
  const previousOptions = { ...workingOptions ?? {} };
2291
2304
  const previousStyle = currentStyle;
2292
2305
  const previousNonGeometryDirty = nonGeometryDirty;
2306
+ const previousTextAmplifiers = workingTextAmplifiers;
2293
2307
  const previousRendered = rebuildRendered();
2294
2308
  currentStyle = {
2295
2309
  ...currentStyle ?? {},
@@ -2305,7 +2319,51 @@ function runEdit(opts) {
2305
2319
  renderPreview();
2306
2320
  nonGeometryDirty = true;
2307
2321
  dirty = true;
2308
- 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);
2309
2367
  }
2310
2368
  /**
2311
2369
  * Flip the size anchor for the measure under edit (ADR-0020). Re-renders the
@@ -2321,12 +2379,13 @@ function runEdit(opts) {
2321
2379
  const previousStyle = currentStyle;
2322
2380
  const previousNonGeometryDirty = nonGeometryDirty;
2323
2381
  const previousSizeAnchor = sizeAnchor;
2382
+ const previousTextAmplifiers = workingTextAmplifiers;
2324
2383
  const previousRendered = rebuildRendered();
2325
2384
  sizeAnchor = next;
2326
2385
  const currentRendered = rebuildRendered();
2327
2386
  nonGeometryDirty = true;
2328
2387
  dirty = true;
2329
- emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered);
2388
+ emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered, previousTextAmplifiers);
2330
2389
  }
2331
2390
  /** Apply a translate gesture by shifting every working point by `delta`. */
2332
2391
  function commitTranslate(delta) {
@@ -2384,6 +2443,9 @@ function runEdit(opts) {
2384
2443
  get style() {
2385
2444
  return currentStyle ? { ...currentStyle } : void 0;
2386
2445
  },
2446
+ get textAmplifiers() {
2447
+ return workingTextAmplifiers ? { ...workingTextAmplifiers } : void 0;
2448
+ },
2387
2449
  get dirty() {
2388
2450
  return dirty;
2389
2451
  },
@@ -2415,6 +2477,9 @@ function runEdit(opts) {
2415
2477
  setStyle(partial) {
2416
2478
  setStyle(partial);
2417
2479
  },
2480
+ setTextAmplifiers(partial) {
2481
+ setTextAmplifiers(partial);
2482
+ },
2418
2483
  setSizeAnchor(next) {
2419
2484
  setSizeAnchor(next);
2420
2485
  },
@@ -3761,6 +3826,7 @@ var TacticalDraw = class {
3761
3826
  measureOptions: draft.options,
3762
3827
  measureStyle: draft.style,
3763
3828
  measureProperties: draft.properties,
3829
+ measureTextAmplifiers: draft.textAmplifiers,
3764
3830
  guide: options?.guide,
3765
3831
  sizeAnchor: options?.sizeAnchor,
3766
3832
  onSettled
@@ -3777,6 +3843,7 @@ var TacticalDraw = class {
3777
3843
  measureOptions: draft.options,
3778
3844
  measureStyle: draft.style,
3779
3845
  measureProperties: draft.properties,
3846
+ measureTextAmplifiers: draft.textAmplifiers,
3780
3847
  guide: options?.guide,
3781
3848
  sizeAnchor: options?.sizeAnchor,
3782
3849
  onSettled
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orbat-mapper/tactical-draw",
3
- "version": "0.2.0-alpha.11",
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.8"
41
+ "@orbat-mapper/control-measures": "^0.2.0-alpha.9"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@arethetypeswrong/core": "^0.18.3",