@orbat-mapper/tactical-draw 0.6.0 → 0.6.2

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
@@ -551,7 +551,7 @@ type SizeAnchor = "ground" | "screen";
551
551
  /**
552
552
  * The [[SizeAnchor]] a measure currently encodes (ADR-0020): `screen` when it
553
553
  * carries its kind's pixel-denominated size option, `ground` otherwise. Keys off
554
- * the same `PIXEL_SIZE_OPTIONS` table as the bake, so a consumer deciding how to
554
+ * the same metadata size pairs as the bake, so a consumer deciding how to
555
555
  * re-edit a graphic never has to re-derive the per-kind pixel-option knowledge
556
556
  * (e.g. via a fragile key-name heuristic). Accepts the minimal `{ kind, options }`
557
557
  * shape, mirroring [[measureUsesAdapterResolution]].
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { FILL_PATTERN_DEFINITIONS, NON_SOLID_FILL_PATTERNS, applyBoxTransformOptions, canonicalTextAmplifierKey, cloneControlMeasure, controlMeasureIdFromFeature, dragControlMeasureOptionHandle, foldsBoxTransformOptions, freezeOrientationOptions, getControlMeasureMetadata, getControlMeasureOptionHandles, getDefaultOptions, normalizeTextAmplifiers, renderControlMeasure, resolveAmplifierPlacement, resolveStyleHints, roundToFixed } from "@orbat-mapper/control-measures";
1
+ import { DEFAULT_LABEL_HEIGHT_CSS_PIXELS, FILL_PATTERN_DEFINITIONS, NON_SOLID_FILL_PATTERNS, applyBoxTransformOptions, canonicalTextAmplifierKey, cloneControlMeasure, controlMeasureIdFromFeature, dragControlMeasureOptionHandle, foldsBoxTransformOptions, freezeOrientationOptions, getControlMeasureMetadata, getControlMeasureOptionHandles, getDefaultOptions, normalizeTextAmplifiers, renderControlMeasure, resolveAmplifierPlacement, resolveSizePair, resolveStyleHints, roundToFixed, switchSizePairUnit } from "@orbat-mapper/control-measures";
2
2
  function pickPointerType(originalEvent) {
3
3
  if (typeof PointerEvent !== "undefined" && originalEvent instanceof PointerEvent) {
4
4
  if (originalEvent.pointerType === "touch") return "touch";
@@ -1046,114 +1046,23 @@ function combineWithHostSignal(hostSignal) {
1046
1046
  }
1047
1047
  //#endregion
1048
1048
  //#region src/tactical-draw/measure-options.ts
1049
- const DEFAULT_LABEL_SIZE_PIXELS = 14;
1050
- /**
1051
- * Per-kind pixel-denominated *geometry* size option and the meter option it
1052
- * bakes into — the single source of which geometry option makes a measure
1053
- * screen-anchored. Label sizing is not listed here: every `capturesLabelSize`
1054
- * kind carries the shared `labelSizePixels` → `labelSize` pair implicitly
1055
- * (ADR-0028) — see [[sizeOptionSpecs]]. Both the resolution gate
1056
- * ([[measureUsesAdapterResolution]]) and the bake ([[bakePixelSizeToGround]])
1057
- * derive from this table, so a new pixel-sized kind is one entry here
1058
- * (ADR-0020).
1059
- */
1060
- const PIXEL_SIZE_OPTIONS = {
1061
- boundary: {
1062
- pixels: "echelonSizePixels",
1063
- meters: "echelonSize"
1064
- },
1065
- "battle-position": {
1066
- pixels: "echelonSizePixels",
1067
- meters: "echelonSize"
1068
- },
1069
- "strong-point": {
1070
- pixels: "echelonSizePixels",
1071
- meters: "echelonSize"
1072
- },
1073
- "base-camp": {
1074
- pixels: "echelonSizePixels",
1075
- meters: "echelonSize"
1076
- },
1077
- "guerrilla-base": {
1078
- pixels: "echelonSizePixels",
1079
- meters: "echelonSize"
1080
- },
1081
- flot: {
1082
- pixels: "radiusPixels",
1083
- meters: "radius"
1084
- },
1085
- "fortified-line": {
1086
- pixels: "toothSizePixels",
1087
- meters: "toothSize"
1088
- },
1089
- "fortified-area": {
1090
- pixels: "toothSizePixels",
1091
- meters: "toothSize"
1092
- },
1093
- "antitank-ditch-under-construction": {
1094
- pixels: "toothHeightPixels",
1095
- meters: "toothHeight"
1096
- },
1097
- "antitank-ditch-completed": {
1098
- pixels: "toothHeightPixels",
1099
- meters: "toothHeight"
1100
- },
1101
- "antitank-wall": {
1102
- pixels: "toothHeightPixels",
1103
- meters: "toothHeight"
1104
- },
1105
- destroy: {
1106
- pixels: "sizePixels",
1107
- meters: "sizeMeters"
1108
- },
1109
- defeat: {
1110
- pixels: "sizePixels",
1111
- meters: "sizeMeters"
1112
- },
1113
- neutralize: {
1114
- pixels: "sizePixels",
1115
- meters: "sizeMeters"
1116
- },
1117
- suppress: {
1118
- pixels: "sizePixels",
1119
- meters: "sizeMeters"
1120
- },
1121
- minefield: {
1122
- pixels: "sizePixels",
1123
- meters: "sizeMeters"
1124
- },
1125
- text: {
1126
- pixels: "sizePixels",
1127
- meters: "sizeMeters"
1128
- }
1129
- };
1130
- /** The shared label-size pair every `capturesLabelSize` kind carries (ADR-0028). */
1131
- const LABEL_SIZE_SPEC = {
1132
- pixels: "labelSizePixels",
1133
- meters: "labelSize"
1134
- };
1135
- /**
1136
- * Every pixel/meter size-option pair `kind` can carry: its geometry entry from
1137
- * [[PIXEL_SIZE_OPTIONS]] (if any) plus the shared label pair when the kind
1138
- * captures a label size — a kind like Boundary can carry both anchor-bearing
1139
- * dimensions at once (echelon glyph + labels).
1140
- */
1049
+ const DEFAULT_LABEL_SIZE_PIXELS = DEFAULT_LABEL_HEIGHT_CSS_PIXELS;
1050
+ /** An adapter resolution usable for pixel<->meter conversion. */
1051
+ function hasUsableResolution(mpp) {
1052
+ return mpp !== void 0 && Number.isFinite(mpp) && mpp > 0;
1053
+ }
1054
+ /** Every supported dimension, including captured labels, comes from core metadata. */
1141
1055
  function sizeOptionSpecs(kind) {
1142
- const specs = [];
1143
- const geometry = PIXEL_SIZE_OPTIONS[kind];
1144
- if (geometry) specs.push(geometry);
1145
- if (getControlMeasureMetadata(kind).capturesLabelSize) specs.push(LABEL_SIZE_SPEC);
1146
- return specs;
1056
+ return getControlMeasureMetadata(kind).sizePairs ?? [];
1147
1057
  }
1148
1058
  /**
1149
1059
  * Whether `kind` has at least one size that can be restored to its normal
1150
1060
  * pixel-authored appearance at the adapter's current resolution. Kept inside
1151
- * tactical-draw with the private size-pair catalog so hosts never need to
1061
+ * tactical-draw using the core metadata catalog so hosts never need to
1152
1062
  * infer option names or special-case captured labels.
1153
1063
  */
1154
1064
  function canResetGroundSizes(measure, adapter) {
1155
- const mpp = adapter.getResolution();
1156
- return mpp !== void 0 && mpp > 0 && sizeOptionSpecs(measure.kind).length > 0;
1065
+ return hasUsableResolution(adapter.getResolution()) && sizeOptionSpecs(measure.kind).length > 0;
1157
1066
  }
1158
1067
  /**
1159
1068
  * Return the complete option record produced by resetting every known size
@@ -1167,35 +1076,30 @@ function canResetGroundSizes(measure, adapter) {
1167
1076
  function resetGroundSizeOptions(measure, adapter) {
1168
1077
  const mpp = adapter.getResolution();
1169
1078
  const specs = sizeOptionSpecs(measure.kind);
1170
- if (mpp === void 0 || mpp <= 0 || specs.length === 0) return void 0;
1079
+ if (!hasUsableResolution(mpp) || specs.length === 0) return void 0;
1171
1080
  const defaults = getDefaultOptions(measure.kind);
1172
1081
  const next = { ...measure.options ?? {} };
1173
1082
  for (const spec of specs) {
1174
- const defaultPixels = spec === LABEL_SIZE_SPEC ? 14 : defaults[spec.pixels];
1083
+ const defaultPixels = spec.defaultUnit === "px" && spec.defaultValue !== void 0 ? spec.defaultValue : defaults[spec.pixels];
1175
1084
  if (typeof defaultPixels !== "number") continue;
1085
+ const result = switchSizePairUnit({
1086
+ kind: measure.kind,
1087
+ dimension: spec.id,
1088
+ options: {
1089
+ ...next,
1090
+ [spec.pixels]: defaultPixels
1091
+ },
1092
+ unit: "m",
1093
+ constructionMetersPerCssPixel: mpp
1094
+ });
1095
+ if (result.status !== "converted") return void 0;
1176
1096
  delete next[spec.pixels];
1177
- next[spec.meters] = defaultPixels * mpp;
1097
+ next[spec.meters] = result.value;
1178
1098
  }
1179
1099
  delete next.metersPerPixel;
1180
1100
  return next;
1181
1101
  }
1182
1102
  /**
1183
- * Whether `kind` authors `spec` in pixels — its defaults declare the pixel key
1184
- * with no meter counterpart. A static property of the definition, but
1185
- * `getDefaultOptions` deep-clones on every call, so it is memoised: the gate
1186
- * below runs per measure per view-change frame.
1187
- */
1188
- const pixelAuthoredCache = /* @__PURE__ */ new Map();
1189
- function isPixelAuthored(kind, spec) {
1190
- const cacheKey = `${kind}:${spec.pixels}`;
1191
- const cached = pixelAuthoredCache.get(cacheKey);
1192
- if (cached !== void 0) return cached;
1193
- const defaults = getDefaultOptions(kind);
1194
- const authored = defaults[spec.pixels] !== void 0 && defaults[spec.meters] === void 0;
1195
- pixelAuthoredCache.set(cacheKey, authored);
1196
- return authored;
1197
- }
1198
- /**
1199
1103
  * Whether `measure` carries a pixel-denominated size option whose resolution
1200
1104
  * to meters depends on the live map (`metersPerPixel`) — i.e. it is
1201
1105
  * *screen-anchored*. Used both to inject the override
@@ -1209,7 +1113,7 @@ function measureUsesAdapterResolution(measure) {
1209
1113
  return sizeOptionSpecs(measure.kind).some((spec) => {
1210
1114
  if (options?.[spec.pixels] !== void 0) return true;
1211
1115
  if (options?.[spec.meters] !== void 0) return false;
1212
- return isPixelAuthored(measure.kind, spec);
1116
+ return !spec.captureOnDraw && spec.defaultUnit === "px";
1213
1117
  });
1214
1118
  }
1215
1119
  /**
@@ -1241,7 +1145,7 @@ function measureRendersFromViewState(measure) {
1241
1145
  /**
1242
1146
  * The [[SizeAnchor]] a measure currently encodes (ADR-0020): `screen` when it
1243
1147
  * carries its kind's pixel-denominated size option, `ground` otherwise. Keys off
1244
- * the same `PIXEL_SIZE_OPTIONS` table as the bake, so a consumer deciding how to
1148
+ * the same metadata size pairs as the bake, so a consumer deciding how to
1245
1149
  * re-edit a graphic never has to re-derive the per-kind pixel-option knowledge
1246
1150
  * (e.g. via a fragile key-name heuristic). Accepts the minimal `{ kind, options }`
1247
1151
  * shape, mirroring [[measureUsesAdapterResolution]].
@@ -1254,32 +1158,32 @@ function defaultSizeAnchor(measure) {
1254
1158
  return getControlMeasureMetadata(measure.kind).uniformSizing ? getSizeAnchor(measure) : "ground";
1255
1159
  }
1256
1160
  /**
1257
- * Materialise a *pixel-authored* kind's default pixel size onto a measure that
1258
- * carries neither size key, so the ADR-0020 ground bake has a pixel value to
1259
- * resolve to meters at commit. A kind is pixel-authored when its default
1260
- * options declare the pixel-size option but no meter counterpart (e.g. Text's
1261
- * `sizePixels: 24` with no `sizeMeters`) — such a fresh draw is authored in
1262
- * pixels and must bake to meters, but a default draw commits with no options,
1263
- * so the pixel size would otherwise never land and the graphic would persist
1264
- * screen-anchored forever. No-op when the measure already carries a size key,
1265
- * the kind has no pixel-size option, or the kind's default is meter-denominated
1266
- * (e.g. FLOT, whose meter default governs a default draw without any bake) —
1267
- * so this never changes the committed size of the pre-existing pixel kinds.
1161
+ * Materialize uncaptured pixel authoring defaults before commit, so the ground
1162
+ * bake sees an explicit value. Meter-default dimensions and authored values
1163
+ * remain unchanged; captured labels have their separate render/commit seam.
1268
1164
  */
1269
1165
  function withDefaultAuthoringSize(measure) {
1270
- const spec = PIXEL_SIZE_OPTIONS[measure.kind];
1271
- if (!spec) return measure;
1272
- const options = measure.options ?? {};
1273
- if (options[spec.pixels] !== void 0 || options[spec.meters] !== void 0) return measure;
1274
- const defaults = getDefaultOptions(measure.kind);
1275
- if (defaults[spec.meters] !== void 0 || defaults[spec.pixels] === void 0) return measure;
1276
- return {
1277
- ...measure,
1278
- options: {
1166
+ let options = measure.options ?? {};
1167
+ let changed = false;
1168
+ for (const spec of sizeOptionSpecs(measure.kind)) {
1169
+ if (spec.captureOnDraw || spec.defaultUnit !== "px") continue;
1170
+ if (options[spec.pixels] !== void 0 || options[spec.meters] !== void 0) continue;
1171
+ const resolved = resolveSizePair({
1172
+ kind: measure.kind,
1173
+ dimension: spec.id,
1174
+ options
1175
+ });
1176
+ if (resolved.status !== "resolved") continue;
1177
+ options = {
1279
1178
  ...options,
1280
- [spec.pixels]: defaults[spec.pixels]
1281
- }
1282
- };
1179
+ [spec.pixels]: resolved.value
1180
+ };
1181
+ changed = true;
1182
+ }
1183
+ return changed ? {
1184
+ ...measure,
1185
+ options
1186
+ } : measure;
1283
1187
  }
1284
1188
  /**
1285
1189
  * Bake a screen-anchored (pixel) size to a ground-anchored (meter) size at the
@@ -1291,17 +1195,26 @@ function withDefaultAuthoringSize(measure) {
1291
1195
  */
1292
1196
  function bakePixelSizeToGround(measure, adapter) {
1293
1197
  const options = measure.options ?? {};
1294
- const specs = sizeOptionSpecs(measure.kind).filter((spec) => typeof options[spec.pixels] === "number");
1198
+ const specs = sizeOptionSpecs(measure.kind).filter((spec) => options[spec.pixels] !== void 0);
1295
1199
  if (specs.length === 0) return measure;
1296
1200
  const mpp = adapter.getResolution();
1297
- if (mpp === void 0 || mpp <= 0) return measure;
1298
- const pixelKeys = new Set(specs.map((spec) => spec.pixels));
1299
- const rest = {};
1300
- for (const [key, value] of Object.entries(options)) if (!pixelKeys.has(key) && key !== "metersPerPixel") rest[key] = value;
1301
- for (const spec of specs) rest[spec.meters] = options[spec.pixels] * mpp;
1201
+ if (!hasUsableResolution(mpp)) return measure;
1202
+ let next = options;
1203
+ for (const spec of specs) {
1204
+ const result = switchSizePairUnit({
1205
+ kind: measure.kind,
1206
+ dimension: spec.id,
1207
+ options: next,
1208
+ unit: "m",
1209
+ constructionMetersPerCssPixel: mpp
1210
+ });
1211
+ if (result.status !== "converted") return measure;
1212
+ next = result.options;
1213
+ }
1214
+ delete next.metersPerPixel;
1302
1215
  return {
1303
1216
  ...measure,
1304
- options: rest
1217
+ options: next
1305
1218
  };
1306
1219
  }
1307
1220
  /**
@@ -1322,7 +1235,7 @@ function withCommittedAdapterMeasureOptions(measure) {
1322
1235
  ...measure,
1323
1236
  options: {
1324
1237
  ...measure.options,
1325
- labelSizePixels: 14
1238
+ labelSizePixels: DEFAULT_LABEL_SIZE_PIXELS
1326
1239
  }
1327
1240
  };
1328
1241
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orbat-mapper/tactical-draw",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
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",
@@ -39,7 +39,7 @@
39
39
  "access": "public"
40
40
  },
41
41
  "dependencies": {
42
- "@orbat-mapper/control-measures": "^0.20.0"
42
+ "@orbat-mapper/control-measures": "^0.22.0"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "vitest": ">=4.0.0"