@orbat-mapper/tactical-draw 0.12.0 → 0.13.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.
Files changed (2) hide show
  1. package/dist/index.mjs +40 -12
  2. package/package.json +2 -2
package/dist/index.mjs CHANGED
@@ -3164,6 +3164,17 @@ function runEditInteraction(spec) {
3164
3164
  }
3165
3165
  //#endregion
3166
3166
  //#region src/tactical-draw/handles.ts
3167
+ /** Size of a `compact` option handle relative to its slot's style. */
3168
+ const COMPACT_HANDLE_SCALE = 2 / 3;
3169
+ /** Shrink a handle style's radius and stroke for a `compact` handle. */
3170
+ function compactHandleStyle(style) {
3171
+ const { pointRadius, strokeWidth } = style;
3172
+ return {
3173
+ ...style,
3174
+ ...pointRadius !== void 0 && { pointRadius: pointRadius * COMPACT_HANDLE_SCALE },
3175
+ ...strokeWidth !== void 0 && { strokeWidth: strokeWidth * COMPACT_HANDLE_SCALE }
3176
+ };
3177
+ }
3167
3178
  /**
3168
3179
  * Midpoint of the segment between two positions, in lonLat. The single source
3169
3180
  * for "where a midpoint handle sits": `buildHandleSet` places the handle here
@@ -3249,7 +3260,8 @@ function handleStyleFor(kind, interactionStyle, unset = false) {
3249
3260
  */
3250
3261
  function toFeatures(handles, featureIdPrefix, interactionStyle) {
3251
3262
  return handles.map((handle) => {
3252
- const style = handleStyleFor(handle.kind, interactionStyle, handle.unset);
3263
+ const slotStyle = handleStyleFor(handle.kind, interactionStyle, handle.unset);
3264
+ const style = slotStyle && handle.compact ? compactHandleStyle(slotStyle) : slotStyle;
3253
3265
  return {
3254
3266
  type: "Feature",
3255
3267
  id: handleFeatureId(featureIdPrefix, handle.kind, handle.index),
@@ -4365,6 +4377,7 @@ function runEdit(opts) {
4365
4377
  const isAreaKind = metadata.geometry === "area";
4366
4378
  const isTextGraphic = measure.kind === "text";
4367
4379
  const foldsTransformIntoOptions = foldsBoxTransformOptions(measure.kind);
4380
+ const holdsAnchorOnFold = foldsTransformIntoOptions && metadata.maxCoordinates === 1;
4368
4381
  const requestedModes = normaliseModes(initialModes);
4369
4382
  const editStartPoints = measure.controlPoints.map((p) => [...p]);
4370
4383
  const workingPoints = editStartPoints.map((p) => [...p]);
@@ -4523,7 +4536,7 @@ function runEdit(opts) {
4523
4536
  const px = driver.toPixel(p);
4524
4537
  if (px) pixels.push(px);
4525
4538
  }
4526
- return fitTransformBox(pixels, activeOptionTransformInFlight() ? activeBoxDrag.lastSnapshot.box.angle : boxAngle, interactionStyle?.boxPadding ?? 0);
4539
+ return fitTransformBox(pixels, anchoredOptionTransformInFlight() ? activeBoxDrag.lastSnapshot.box.angle : boxAngle, interactionStyle?.boxPadding ?? 0);
4527
4540
  }
4528
4541
  /**
4529
4542
  * The Text footprint box (rotated rectangle hugging the rendered text),
@@ -4542,12 +4555,12 @@ function runEdit(opts) {
4542
4555
  * Re-fit `box` from `lastWorkingFeatures`. A no-op while a box gesture is
4543
4556
  * in flight — that drag paints the box rigidly from its own snapshot
4544
4557
  * (`BoxDragSnapshot.box`) instead of a re-fit, so a corner/rotate/body drag
4545
- * doesn't fight its own paint. Option-backed transforms are the exception:
4546
- * their anchor points stay fixed while their rendered footprint changes, so
4558
+ * doesn't fight its own paint. Single-anchor option-backed transforms are the
4559
+ * exception: their anchor points stay fixed while their rendered footprint changes, so
4547
4560
  * the box must re-fit from that live render on every tick.
4548
4561
  */
4549
4562
  function refitBox() {
4550
- if (activeBoxDrag && !activeOptionTransformInFlight()) return;
4563
+ if (activeBoxDrag && !anchoredOptionTransformInFlight()) return;
4551
4564
  box = fitBoxFromFeatures(lastWorkingFeatures);
4552
4565
  }
4553
4566
  function renderWorkingGraphic() {
@@ -4596,7 +4609,7 @@ function runEdit(opts) {
4596
4609
  const features = toFeatures(buildCurrentHandleSet(), handleIdPrefix, interactionStyle);
4597
4610
  if (modes.includes("labeldrag")) features.push(...labels.handleFeatures());
4598
4611
  if (modes.includes("transform")) {
4599
- const liveBox = activeBoxDrag && !activeOptionTransformInFlight() ? activeBoxDrag.lastSnapshot.box : box;
4612
+ const liveBox = activeBoxDrag && !anchoredOptionTransformInFlight() ? activeBoxDrag.lastSnapshot.box : box;
4600
4613
  if (liveBox) {
4601
4614
  features.push(boxOutlineFeature(liveBox, dragProjection.fromPixel, `${handleIdPrefix}:box-outline`, interactionStyle?.boxOutline));
4602
4615
  features.push(...toFeatures(boxToHandles(liveBox, dragProjection.fromPixel), handleIdPrefix, interactionStyle));
@@ -4608,6 +4621,11 @@ function runEdit(opts) {
4608
4621
  iconResources: []
4609
4622
  }]);
4610
4623
  }
4624
+ /** Live view scale for option handles behind pixel-authored sizes. */
4625
+ function optionHandleContext() {
4626
+ const resolution = adapter.getResolution();
4627
+ return hasUsableResolution(resolution) ? { constructionMetersPerCssPixel: resolution } : {};
4628
+ }
4611
4629
  function buildCurrentHandleSet() {
4612
4630
  const handles = buildHandleSet(workingPoints, modes, {
4613
4631
  includeMidpoints: midpointHandlesEnabled,
@@ -4616,7 +4634,10 @@ function runEdit(opts) {
4616
4634
  closedRing
4617
4635
  });
4618
4636
  if (modes.includes("reshape")) {
4619
- const optionHandles = getControlMeasureOptionHandles(measure.kind, workingPoints, workingOptions, { detail: detailHandles });
4637
+ const optionHandles = getControlMeasureOptionHandles(measure.kind, workingPoints, workingOptions, {
4638
+ detail: detailHandles,
4639
+ ...optionHandleContext()
4640
+ });
4620
4641
  for (let index = 0; index < optionHandles.length; index++) {
4621
4642
  const handle = optionHandles[index];
4622
4643
  handles.push({
@@ -4624,7 +4645,8 @@ function runEdit(opts) {
4624
4645
  index,
4625
4646
  optionId: handle.id,
4626
4647
  position: [...handle.position],
4627
- ...handle.unset ? { unset: true } : {}
4648
+ ...handle.unset ? { unset: true } : {},
4649
+ ...handle.compact ? { compact: true } : {}
4628
4650
  });
4629
4651
  }
4630
4652
  }
@@ -5145,7 +5167,7 @@ function runEdit(opts) {
5145
5167
  let activeDrag = null;
5146
5168
  let activeOptionDrag = null;
5147
5169
  function optionsForOptionHandle(position, drag) {
5148
- const patch = dragControlMeasureOptionHandle(measure.kind, workingPoints, drag.preDragOptions, drag.handleId, position);
5170
+ const patch = dragControlMeasureOptionHandle(measure.kind, workingPoints, drag.preDragOptions, drag.handleId, position, optionHandleContext());
5149
5171
  return patch ? {
5150
5172
  ...drag.preDragOptions ?? {},
5151
5173
  ...patch
@@ -5270,7 +5292,7 @@ function runEdit(opts) {
5270
5292
  return;
5271
5293
  }
5272
5294
  if (hit.kind === "option" && hit.optionId && info?.altKey) {
5273
- const patch = resetControlMeasureOptionHandle(measure.kind, workingPoints, workingOptions, hit.optionId);
5295
+ const patch = resetControlMeasureOptionHandle(measure.kind, workingPoints, workingOptions, hit.optionId, optionHandleContext());
5274
5296
  if (patch) {
5275
5297
  setOptions(patch, "gesture", "Reset option handle");
5276
5298
  shell.suppressNextClickAway();
@@ -5396,6 +5418,10 @@ function runEdit(opts) {
5396
5418
  const kind = activeBoxDrag.fsm.kind;
5397
5419
  return kind === "rotate" || kind === "scale";
5398
5420
  }
5421
+ /** Whether the live option-backed gesture also holds a single anchor fixed. */
5422
+ function anchoredOptionTransformInFlight() {
5423
+ return holdsAnchorOnFold && activeOptionTransformInFlight();
5424
+ }
5399
5425
  function paintBoxDragSnapshot(pixel, info, constrainRotation = false) {
5400
5426
  if (!activeBoxDrag) return;
5401
5427
  const resolvedPixel = translatedBoxPixel(pixel, info, activeBoxDrag);
@@ -5403,9 +5429,11 @@ function runEdit(opts) {
5403
5429
  activeBoxDrag.lastSnapshot = snapshot;
5404
5430
  if (activeOptionTransformInFlight()) {
5405
5431
  const foldedOptions = foldOptionTransform(activeBoxDrag.preDragBox, snapshot.box, activeBoxDrag.fsm.kind, activeBoxDrag.preDragOptions);
5406
- workingPoints.splice(0, workingPoints.length, ...activeBoxDrag.preDragPoints);
5407
5432
  if (foldedOptions) workingOptions = foldedOptions;
5408
5433
  nonGeometryDirty = true;
5434
+ }
5435
+ if (anchoredOptionTransformInFlight()) {
5436
+ workingPoints.splice(0, workingPoints.length, ...activeBoxDrag.preDragPoints);
5409
5437
  renderWorkingGraphic();
5410
5438
  renderHandlesLayer();
5411
5439
  recomputeDirty();
@@ -5528,7 +5556,7 @@ function runEdit(opts) {
5528
5556
  if (foldedOptions) workingOptions = foldedOptions;
5529
5557
  nonGeometryDirty = true;
5530
5558
  }
5531
- const committedPoints = optionFold ? preDragPoints : finalSnapshot.points;
5559
+ const committedPoints = optionFold && holdsAnchorOnFold ? preDragPoints : finalSnapshot.points;
5532
5560
  emitTransient("end", committedPoints, null);
5533
5561
  commitGesture(committedPoints, preDragOptions, transformAmplifierPlacements(preDragAmplifierPlacements, preDragBox, finalSnapshot.box, driver), preDragBox.angle);
5534
5562
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orbat-mapper/tactical-draw",
3
- "version": "0.12.0",
3
+ "version": "0.13.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.30.1"
42
+ "@orbat-mapper/control-measures": "^0.31.0"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "vitest": ">=4.0.0"