@orbat-mapper/tactical-draw 0.10.2 → 0.11.1

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
@@ -628,6 +628,12 @@ interface InteractionStyle {
628
628
  midpointHandle?: HandleStyle;
629
629
  /** Generator-option handle, such as a flared attack shaft's rear-width grip. */
630
630
  optionHandle?: HandleStyle;
631
+ /**
632
+ * Generator-option handle whose option has no stored value, so it shows a
633
+ * derived position (e.g. an interpolated vertex width). Falls back to
634
+ * `optionHandle` when unset.
635
+ */
636
+ unsetOptionHandle?: HandleStyle;
631
637
  /** Visible grab target for a draggable rendered text label. */
632
638
  labelHandle?: HandleStyle;
633
639
  scaleHandle?: HandleStyle;
@@ -762,6 +768,13 @@ interface EditSession {
762
768
  /** `true` after the first working-copy mutation; back to `false` within tolerance. */
763
769
  readonly dirty: boolean;
764
770
  readonly modes: readonly EditMode[];
771
+ /**
772
+ * Whether reshape mode shows the measure's detail handles: fine-adjustment
773
+ * option handles such as the per-vertex width grips on arrow shafts.
774
+ * Initialised from `EditOptions.detailHandles` and mutated by
775
+ * [[setDetailHandles]].
776
+ */
777
+ readonly detailHandles: boolean;
765
778
  /**
766
779
  * Live size anchor for the measure under edit (ADR-0020). `"ground"` bakes a
767
780
  * pixel size to meters on commit; `"screen"` keeps it zoom-aware. Initialised
@@ -790,6 +803,11 @@ interface EditSession {
790
803
  * is rejected with `TypeError`.
791
804
  */
792
805
  setModes(modes: readonly EditMode[]): void;
806
+ /**
807
+ * Show or hide the detail handles (see [[detailHandles]]). View state only:
808
+ * no change event, no history entry. A silent no-op after settlement.
809
+ */
810
+ setDetailHandles(visible: boolean): void;
793
811
  /** Atomically replace the live working graphic while preserving id and kind. */
794
812
  updateGraphic(updater: (graphic: ControlMeasure) => ControlMeasure): void;
795
813
  /** Shallow-merge per-measure generator options into the live working copy. */
@@ -1509,6 +1527,13 @@ interface EditOptions {
1509
1527
  * stays screen-anchored throughout the edit.
1510
1528
  */
1511
1529
  sizeAnchor?: SizeAnchor;
1530
+ /**
1531
+ * Show the measure's detail handles in reshape mode: fine-adjustment option
1532
+ * handles such as the left and right width grips beside each arrow-shaft
1533
+ * vertex. Default `false`, which keeps the handle set uncluttered; hosts
1534
+ * typically bind a toggle to [[EditSession.setDetailHandles]].
1535
+ */
1536
+ detailHandles?: boolean;
1512
1537
  }
1513
1538
  /**
1514
1539
  * Options for `TacticalDraw.editMany` — the multi-select group transform
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { DEFAULT_LABEL_HEIGHT_CSS_PIXELS, DEFAULT_SYMBOL_COLOR, FILL_PATTERN_DEFINITIONS, NON_SOLID_FILL_PATTERNS, applyBoxTransformOptions, canonicalTextAmplifierKey, cloneControlMeasure, controlMeasureIdFromFeature, dragControlMeasureOptionHandle, foldsBoxTransformOptions, freezeOrientationOptions, getControlMeasureMetadata, getControlMeasureOptionHandles, getDefaultOptions, normalizeTextAmplifiers, renderControlMeasure, resolveAmplifierPlacement, resolveSizePair, resolveStyleHints, resolveSymbolColor, roundToFixed, switchSizePairUnit } from "@orbat-mapper/control-measures";
1
+ import { DEFAULT_LABEL_HEIGHT_CSS_PIXELS, DEFAULT_SYMBOL_COLOR, FILL_PATTERN_DEFINITIONS, NON_SOLID_FILL_PATTERNS, applyBoxTransformOptions, canonicalTextAmplifierKey, cloneControlMeasure, controlMeasureIdFromFeature, dragControlMeasureOptionHandle, foldsBoxTransformOptions, freezeOrientationOptions, getControlMeasureMetadata, getControlMeasureOptionHandles, getDefaultOptions, normalizeTextAmplifiers, remapVertexAlignedOptions, renderControlMeasure, resetControlMeasureOptionHandle, resolveAmplifierPlacement, resolveSizePair, resolveStyleHints, resolveSymbolColor, 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";
@@ -3175,7 +3175,8 @@ function handleFeatureId(prefix, kind, index) {
3175
3175
  case "rotate": return `${prefix}:handle-rotate`;
3176
3176
  }
3177
3177
  }
3178
- function handleStyleFor(kind, interactionStyle) {
3178
+ function handleStyleFor(kind, interactionStyle, unset = false) {
3179
+ if (kind === "option" && unset && interactionStyle?.unsetOptionHandle) return interactionStyle.unsetOptionHandle;
3179
3180
  switch (kind) {
3180
3181
  case "vertex": return interactionStyle?.vertexHandle;
3181
3182
  case "midpoint": return interactionStyle?.midpointHandle;
@@ -3195,7 +3196,7 @@ function handleStyleFor(kind, interactionStyle) {
3195
3196
  */
3196
3197
  function toFeatures(handles, featureIdPrefix, interactionStyle) {
3197
3198
  return handles.map((handle) => {
3198
- const style = handleStyleFor(handle.kind, interactionStyle);
3199
+ const style = handleStyleFor(handle.kind, interactionStyle, handle.unset);
3199
3200
  return {
3200
3201
  type: "Feature",
3201
3202
  id: handleFeatureId(featureIdPrefix, handle.kind, handle.index),
@@ -4300,6 +4301,7 @@ function runEdit(opts) {
4300
4301
  symbolColors
4301
4302
  };
4302
4303
  let sizeAnchor = opts.sizeAnchor ?? defaultSizeAnchor(measure);
4304
+ let detailHandles = opts.detailHandles ?? false;
4303
4305
  const metadata = getControlMeasureMetadata(measure.kind);
4304
4306
  const rule = metadata.rule;
4305
4307
  const midpointHandlesEnabled = rule?.canonicalPointCount === void 0;
@@ -4561,14 +4563,15 @@ function runEdit(opts) {
4561
4563
  closedRing
4562
4564
  });
4563
4565
  if (modes.includes("reshape")) {
4564
- const optionHandles = getControlMeasureOptionHandles(measure.kind, workingPoints, workingOptions);
4566
+ const optionHandles = getControlMeasureOptionHandles(measure.kind, workingPoints, workingOptions, { detail: detailHandles });
4565
4567
  for (let index = 0; index < optionHandles.length; index++) {
4566
4568
  const handle = optionHandles[index];
4567
4569
  handles.push({
4568
4570
  kind: "option",
4569
4571
  index,
4570
4572
  optionId: handle.id,
4571
- position: [...handle.position]
4573
+ position: [...handle.position],
4574
+ ...handle.unset ? { unset: true } : {}
4572
4575
  });
4573
4576
  }
4574
4577
  }
@@ -5012,6 +5015,14 @@ function runEdit(opts) {
5012
5015
  if (ctx.settled) return;
5013
5016
  ctx.fail(new TacticalDrawAbortError("session"));
5014
5017
  },
5018
+ get detailHandles() {
5019
+ return detailHandles;
5020
+ },
5021
+ setDetailHandles(visible) {
5022
+ if (ctx.settled || visible === detailHandles) return;
5023
+ detailHandles = visible;
5024
+ renderHandlesLayer();
5025
+ },
5015
5026
  setModes(next) {
5016
5027
  if (ctx.settled) return;
5017
5028
  const normalised = normaliseModes(next);
@@ -5158,11 +5169,30 @@ function runEdit(opts) {
5158
5169
  if (ctx.settled) return;
5159
5170
  const previous = workingPoints.map((p) => [...p]);
5160
5171
  const spliced = previous.filter((_, i) => i !== index);
5161
- commitGesture(rule ? rule.transform({
5172
+ const next = rule ? rule.transform({
5162
5173
  previous,
5163
5174
  next: spliced,
5164
5175
  activePointIndex: index
5165
- }) : spliced);
5176
+ }) : spliced;
5177
+ const preGestureOptions = workingOptions;
5178
+ followVertexChange({
5179
+ type: "delete",
5180
+ index
5181
+ });
5182
+ commitGesture(next, preGestureOptions);
5183
+ }
5184
+ /**
5185
+ * Keep the measure's vertex-aligned options (e.g. `vertexLeftWidthRatios`) in
5186
+ * step with a vertex insert or delete, replacing `workingOptions`
5187
+ * with the patched copy. The caller commits with the pre-change options
5188
+ * snapshot so one change event and one undo step cover points and options.
5189
+ */
5190
+ function followVertexChange(change) {
5191
+ const patch = remapVertexAlignedOptions(measure.kind, workingOptions, change);
5192
+ if (Object.keys(patch).length > 0) workingOptions = {
5193
+ ...workingOptions ?? {},
5194
+ ...patch
5195
+ };
5166
5196
  }
5167
5197
  const onDriverDown = (pixel, info) => {
5168
5198
  if (ctx.settled || dragInFlight) return;
@@ -5186,10 +5216,18 @@ function runEdit(opts) {
5186
5216
  shell.suppressNextClickAway();
5187
5217
  return;
5188
5218
  }
5219
+ if (hit.kind === "option" && hit.optionId && info?.altKey) {
5220
+ const patch = resetControlMeasureOptionHandle(measure.kind, workingPoints, workingOptions, hit.optionId);
5221
+ if (patch) {
5222
+ setOptions(patch, "gesture", "Reset option handle");
5223
+ shell.suppressNextClickAway();
5224
+ return;
5225
+ }
5226
+ }
5189
5227
  if (hit.kind === "option" && hit.optionId) {
5190
5228
  activeOptionDrag = {
5191
5229
  handleId: hit.optionId,
5192
- preDragOptions: { ...workingOptions ?? {} },
5230
+ preDragOptions: workingOptions,
5193
5231
  preDragNonGeometryDirty: nonGeometryDirty
5194
5232
  };
5195
5233
  emitTransient("start", workingPoints, null);
@@ -5208,10 +5246,15 @@ function runEdit(opts) {
5208
5246
  activeDrag = {
5209
5247
  fsm,
5210
5248
  preDragPoints,
5249
+ preDragOptions: workingOptions,
5211
5250
  controlPointIndex: hit.kind === "midpoint" ? hit.index + 1 : hit.index
5212
5251
  };
5213
5252
  emitTransient("start", fsm.kind === "midpoint" ? initialPoints : preDragPoints, activeDrag.controlPointIndex);
5214
5253
  if (fsm.kind === "midpoint") {
5254
+ followVertexChange({
5255
+ type: "insert",
5256
+ index: hit.index + 1
5257
+ });
5215
5258
  workingPoints.splice(0, workingPoints.length, ...initialPoints);
5216
5259
  renderWorkingGraphic();
5217
5260
  renderHandlesLayer();
@@ -5386,13 +5429,13 @@ function runEdit(opts) {
5386
5429
  driver.setDragPanEnabled(true);
5387
5430
  return;
5388
5431
  }
5389
- const { fsm, preDragPoints, controlPointIndex } = activeDrag;
5432
+ const { fsm, preDragPoints, preDragOptions, controlPointIndex } = activeDrag;
5390
5433
  activeDrag = null;
5391
5434
  driver.setDragPanEnabled(true);
5392
5435
  const next = fsm.end(snappedPixel(pixel, info, "reshape", controlPointIndex), { constrainAngles: info?.shiftKey });
5393
5436
  emitTransient("end", next, controlPointIndex);
5394
5437
  replaceWorkingPoints(preDragPoints);
5395
- commitGesture(next);
5438
+ commitGesture(next, preDragOptions);
5396
5439
  return;
5397
5440
  }
5398
5441
  if (activeBoxDrag) {
@@ -8492,6 +8535,7 @@ var TacticalDraw = class {
8492
8535
  handleIdPrefix,
8493
8536
  guide: options?.guide,
8494
8537
  sizeAnchor: options?.sizeAnchor,
8538
+ detailHandles: options?.detailHandles,
8495
8539
  settleToGraphics,
8496
8540
  publishWorkingGraphic: (graphic, features) => this.publishWorkingGraphics([{
8497
8541
  id: graphic.id,
@@ -8973,6 +9017,12 @@ const BUILT_IN_INTERACTION_STYLE = {
8973
9017
  strokeColor: "#ffffff",
8974
9018
  strokeWidth: 2
8975
9019
  },
9020
+ unsetOptionHandle: {
9021
+ pointRadius: 5,
9022
+ fillColor: "#ffffff",
9023
+ strokeColor: "#f59e0b",
9024
+ strokeWidth: 2
9025
+ },
8976
9026
  labelHandle: {
8977
9027
  pointRadius: 6,
8978
9028
  fillColor: "#7c3aed",
@@ -8996,6 +9046,7 @@ const HANDLE_SLOTS = [
8996
9046
  "vertexHandle",
8997
9047
  "midpointHandle",
8998
9048
  "optionHandle",
9049
+ "unsetOptionHandle",
8999
9050
  "labelHandle",
9000
9051
  "scaleHandle",
9001
9052
  "rotateHandle"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orbat-mapper/tactical-draw",
3
- "version": "0.10.2",
3
+ "version": "0.11.1",
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.28.0"
42
+ "@orbat-mapper/control-measures": "^0.30.0"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "vitest": ">=4.0.0"