@orbat-mapper/tactical-draw 0.2.0-alpha.16 → 0.2.0-alpha.17

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 +66 -30
  2. package/package.json +2 -2
package/dist/index.mjs CHANGED
@@ -2024,6 +2024,31 @@ function boxOutlineFeature(box, fromPixel, featureId, style) {
2024
2024
  };
2025
2025
  }
2026
2026
  //#endregion
2027
+ //#region src/tactical-draw/transform-amplifier-placements.ts
2028
+ /**
2029
+ * Carry user-adjusted label anchors through a transform-box gesture. Anchors
2030
+ * are transformed in pixel space alongside the control points so the result
2031
+ * remains visually rigid across map projections.
2032
+ */
2033
+ function transformAmplifierPlacements(placements, from, to, projection) {
2034
+ if (!placements) return void 0;
2035
+ const scaleX = to.halfWidth / from.halfWidth;
2036
+ const scaleY = to.halfHeight / from.halfHeight;
2037
+ const transformed = {};
2038
+ for (const [field, position] of Object.entries(placements)) {
2039
+ if (position === void 0) continue;
2040
+ const pixel = projection.toPixel(position);
2041
+ if (!pixel) {
2042
+ transformed[field] = [...position];
2043
+ continue;
2044
+ }
2045
+ const [localX, localY] = rotate(pixel[0] - from.center[0], pixel[1] - from.center[1], -from.angle);
2046
+ const [outX, outY] = rotate(localX * scaleX, localY * scaleY, to.angle);
2047
+ transformed[field] = projection.fromPixel([to.center[0] + outX, to.center[1] + outY]);
2048
+ }
2049
+ return transformed;
2050
+ }
2051
+ //#endregion
2027
2052
  //#region src/tactical-draw/edit-controller.ts
2028
2053
  /**
2029
2054
  * Coordinate-space tolerance for `EditSession.dirty`. The working copy is
@@ -2704,24 +2729,6 @@ function runEdit(opts) {
2704
2729
  * `boxAngle`.
2705
2730
  */
2706
2731
  let activeBoxDrag = null;
2707
- function transformAmplifierPlacements(placements, from, to) {
2708
- if (!placements) return void 0;
2709
- const scaleX = to.halfWidth / from.halfWidth;
2710
- const scaleY = to.halfHeight / from.halfHeight;
2711
- const transformed = {};
2712
- for (const [field, position] of Object.entries(placements)) {
2713
- if (position === void 0) continue;
2714
- const pixel = driver.toPixel(position);
2715
- if (!pixel) {
2716
- transformed[field] = [...position];
2717
- continue;
2718
- }
2719
- const [localX, localY] = rotate(pixel[0] - from.center[0], pixel[1] - from.center[1], -from.angle);
2720
- const [outX, outY] = rotate(localX * scaleX, localY * scaleY, to.angle);
2721
- transformed[field] = driver.fromPixel([to.center[0] + outX, to.center[1] + outY]);
2722
- }
2723
- return transformed;
2724
- }
2725
2732
  /**
2726
2733
  * Whether the vertex at `index` may be deleted (via Alt+click or in the
2727
2734
  * exclusive `delete` mode). Deletion is
@@ -2859,7 +2866,7 @@ function runEdit(opts) {
2859
2866
  const snapshot = activeBoxDrag.fsm.move(pixel);
2860
2867
  activeBoxDrag.lastSnapshot = snapshot;
2861
2868
  workingPoints.splice(0, workingPoints.length, ...snapshot.points);
2862
- workingAmplifierPlacements = transformAmplifierPlacements(activeBoxDrag.preDragAmplifierPlacements, activeBoxDrag.preDragBox, snapshot.box);
2869
+ workingAmplifierPlacements = transformAmplifierPlacements(activeBoxDrag.preDragAmplifierPlacements, activeBoxDrag.preDragBox, snapshot.box, driver);
2863
2870
  renderPreview();
2864
2871
  renderHandlesLayer();
2865
2872
  recomputeDirty();
@@ -2938,7 +2945,7 @@ function runEdit(opts) {
2938
2945
  replaceWorkingPoints(preDragPoints);
2939
2946
  workingAmplifierPlacements = preDragAmplifierPlacements;
2940
2947
  if (fsm.kind === "rotate") boxAngle += finalSnapshot.box.angle - preDragBox.angle;
2941
- commitGesture(finalSnapshot.points, preDragOptions, transformAmplifierPlacements(preDragAmplifierPlacements, preDragBox, finalSnapshot.box));
2948
+ commitGesture(finalSnapshot.points, preDragOptions, transformAmplifierPlacements(preDragAmplifierPlacements, preDragBox, finalSnapshot.box, driver));
2942
2949
  return;
2943
2950
  }
2944
2951
  };
@@ -3126,6 +3133,7 @@ function runGroupTransform(opts) {
3126
3133
  measure,
3127
3134
  editStartPoints,
3128
3135
  workingPoints: editStartPoints.map((p) => [...p]),
3136
+ workingAmplifierPlacements: measure.amplifierPlacements ? structuredClone(measure.amplifierPlacements) : void 0,
3129
3137
  optionsOverride: null,
3130
3138
  box: null,
3131
3139
  lastPreviewFeatures: []
@@ -3157,7 +3165,8 @@ function runGroupTransform(opts) {
3157
3165
  const working = {
3158
3166
  ...member.measure,
3159
3167
  options: mergeOptions(member.measure.options, override),
3160
- controlPoints: member.workingPoints.map((p) => [...p])
3168
+ controlPoints: member.workingPoints.map((p) => [...p]),
3169
+ ...member.workingAmplifierPlacements ? { amplifierPlacements: member.workingAmplifierPlacements } : {}
3161
3170
  };
3162
3171
  const committed = sizeAnchor === "screen" ? working : bakePixelSizeToGround(working, adapter);
3163
3172
  return {
@@ -3171,7 +3180,8 @@ function runGroupTransform(opts) {
3171
3180
  return {
3172
3181
  ...member.measure,
3173
3182
  options: effectiveOptions(i),
3174
- controlPoints: member.workingPoints.map((p) => [...p])
3183
+ controlPoints: member.workingPoints.map((p) => [...p]),
3184
+ ...member.workingAmplifierPlacements ? { amplifierPlacements: member.workingAmplifierPlacements } : {}
3175
3185
  };
3176
3186
  }
3177
3187
  let dirty = false;
@@ -3338,6 +3348,21 @@ function runGroupTransform(opts) {
3338
3348
  member.workingPoints.splice(0, member.workingPoints.length, ...snapshot[i]);
3339
3349
  });
3340
3350
  }
3351
+ function cloneAmplifierPlacementsByMember() {
3352
+ return members.map((member) => member.workingAmplifierPlacements ? structuredClone(member.workingAmplifierPlacements) : void 0);
3353
+ }
3354
+ /**
3355
+ * Adopts `placements[i]` by reference — callers hand over ownership (the
3356
+ * sources are freshly built transforms or snapshot arrays that are dead
3357
+ * after the restore), and working placements are only ever replaced
3358
+ * wholesale, never mutated in place. This runs on every pointermove of a
3359
+ * box drag, so it must not deep-clone.
3360
+ */
3361
+ function applyAmplifierPlacementsByMember(placements) {
3362
+ members.forEach((member, i) => {
3363
+ member.workingAmplifierPlacements = placements[i];
3364
+ });
3365
+ }
3341
3366
  function cloneOptionsOverrideByMember() {
3342
3367
  return members.map((m) => m.optionsOverride ? { ...m.optionsOverride } : null);
3343
3368
  }
@@ -3369,7 +3394,7 @@ function runGroupTransform(opts) {
3369
3394
  * instance to all listeners in order, deferred `reject`/`close`/`abort`
3370
3395
  * (close/abort override reject), exception collection with `AggregateError`.
3371
3396
  */
3372
- function emitChange(previousRendered, previousPointsByMember, previousSizeAnchor, previousNonGeometryDirty, previousOptionsOverrideByMember, currentRendered) {
3397
+ function emitChange(previousRendered, previousPointsByMember, previousAmplifierPlacementsByMember, previousSizeAnchor, previousNonGeometryDirty, previousOptionsOverrideByMember, currentRendered) {
3373
3398
  if (ctx.settled) return;
3374
3399
  let rejectRequested = false;
3375
3400
  let closeRequested = false;
@@ -3399,6 +3424,7 @@ function runGroupTransform(opts) {
3399
3424
  else if (closeRequested) commitClose();
3400
3425
  else if (rejectRequested && !ctx.settled && members === membersAtEmission) {
3401
3426
  restorePointsByMember(previousPointsByMember);
3427
+ applyAmplifierPlacementsByMember(previousAmplifierPlacementsByMember);
3402
3428
  sizeAnchor = previousSizeAnchor;
3403
3429
  nonGeometryDirty = previousNonGeometryDirty;
3404
3430
  applyOptionsOverrides(previousOptionsOverrideByMember);
@@ -3431,17 +3457,20 @@ function runGroupTransform(opts) {
3431
3457
  }
3432
3458
  return snapshots;
3433
3459
  }
3434
- function commitGesture(nextConcatPoints, preGestureOptionsOverrideByMember) {
3460
+ function commitGesture(nextConcatPoints, preGestureOptionsOverrideByMember, nextAmplifierPlacementsByMember) {
3435
3461
  if (ctx.settled) return;
3436
3462
  const previousPointsByMember = clonePointsByMember();
3463
+ const previousAmplifierPlacementsByMember = cloneAmplifierPlacementsByMember();
3437
3464
  const previousOptionsOverrideByMember = preGestureOptionsOverrideByMember ?? cloneOptionsOverrideByMember();
3438
3465
  const previousRendered = rebuildRenderedAllWith(previousOptionsOverrideByMember);
3439
3466
  applyConcatenatedPoints(nextConcatPoints);
3467
+ if (nextAmplifierPlacementsByMember) applyAmplifierPlacementsByMember(nextAmplifierPlacementsByMember);
3440
3468
  let currentRendered;
3441
3469
  try {
3442
3470
  currentRendered = rebuildRenderedAll();
3443
3471
  } catch (err) {
3444
3472
  restorePointsByMember(previousPointsByMember);
3473
+ applyAmplifierPlacementsByMember(previousAmplifierPlacementsByMember);
3445
3474
  dragInFlight = false;
3446
3475
  throw err;
3447
3476
  }
@@ -3449,7 +3478,7 @@ function runGroupTransform(opts) {
3449
3478
  renderHandlesLayer();
3450
3479
  recomputeDirty();
3451
3480
  dragInFlight = false;
3452
- emitChange(previousRendered, previousPointsByMember, sizeAnchor, nonGeometryDirty, previousOptionsOverrideByMember, currentRendered);
3481
+ emitChange(previousRendered, previousPointsByMember, previousAmplifierPlacementsByMember, sizeAnchor, nonGeometryDirty, previousOptionsOverrideByMember, currentRendered);
3453
3482
  }
3454
3483
  function setSizeAnchor(next) {
3455
3484
  if (ctx.settled || sizeAnchor === next) return;
@@ -3457,12 +3486,13 @@ function runGroupTransform(opts) {
3457
3486
  const previousSizeAnchor = sizeAnchor;
3458
3487
  const previousNonGeometryDirty = nonGeometryDirty;
3459
3488
  const previousOptionsOverrideByMember = cloneOptionsOverrideByMember();
3489
+ const previousAmplifierPlacementsByMember = cloneAmplifierPlacementsByMember();
3460
3490
  const previousRendered = rebuildRenderedAll();
3461
3491
  sizeAnchor = next;
3462
3492
  const currentRendered = rebuildRenderedAll();
3463
3493
  nonGeometryDirty = true;
3464
3494
  dirty = true;
3465
- emitChange(previousRendered, previousPointsByMember, previousSizeAnchor, previousNonGeometryDirty, previousOptionsOverrideByMember, currentRendered);
3495
+ emitChange(previousRendered, previousPointsByMember, previousAmplifierPlacementsByMember, previousSizeAnchor, previousNonGeometryDirty, previousOptionsOverrideByMember, currentRendered);
3466
3496
  }
3467
3497
  /**
3468
3498
  * Cancel an in-flight box drag without committing it: restore the pre-drag
@@ -3474,10 +3504,11 @@ function runGroupTransform(opts) {
3474
3504
  */
3475
3505
  function cancelActiveBoxDrag() {
3476
3506
  if (!activeBoxDrag) return;
3477
- const { preDragPoints, preDragOptionsOverrideByMember } = activeBoxDrag;
3507
+ const { preDragPoints, preDragAmplifierPlacementsByMember, preDragOptionsOverrideByMember } = activeBoxDrag;
3478
3508
  activeBoxDrag = null;
3479
3509
  driver.setDragPanEnabled(true);
3480
3510
  applyConcatenatedPoints(preDragPoints);
3511
+ applyAmplifierPlacementsByMember(preDragAmplifierPlacementsByMember);
3481
3512
  applyOptionsOverrides(preDragOptionsOverrideByMember);
3482
3513
  dragInFlight = false;
3483
3514
  }
@@ -3567,6 +3598,7 @@ function runGroupTransform(opts) {
3567
3598
  renderHandlesLayer();
3568
3599
  }
3569
3600
  const preDragPoints = concatWorkingPoints();
3601
+ const preDragAmplifierPlacementsByMember = cloneAmplifierPlacementsByMember();
3570
3602
  activeBoxDrag = {
3571
3603
  fsm: beginBoxDrag({
3572
3604
  kind: boxHit.kind,
@@ -3583,7 +3615,8 @@ function runGroupTransform(opts) {
3583
3615
  points: preDragPoints,
3584
3616
  box: groupBox
3585
3617
  },
3586
- preDragOptionsOverrideByMember
3618
+ preDragOptionsOverrideByMember,
3619
+ preDragAmplifierPlacementsByMember
3587
3620
  };
3588
3621
  dragInFlight = true;
3589
3622
  driver.setDragPanEnabled(false);
@@ -3593,6 +3626,7 @@ function runGroupTransform(opts) {
3593
3626
  const snapshot = activeBoxDrag.fsm.move(pixel);
3594
3627
  activeBoxDrag.lastSnapshot = snapshot;
3595
3628
  applyConcatenatedPoints(snapshot.points);
3629
+ applyAmplifierPlacementsByMember(activeBoxDrag.preDragAmplifierPlacementsByMember.map((placements) => transformAmplifierPlacements(placements, activeBoxDrag.preDragBox, snapshot.box, driver)));
3596
3630
  renderAllPreviews();
3597
3631
  renderHandlesLayer();
3598
3632
  recomputeDirty();
@@ -3609,12 +3643,13 @@ function runGroupTransform(opts) {
3609
3643
  driver.setDragPanEnabled(true);
3610
3644
  return;
3611
3645
  }
3612
- const { fsm, preDragPoints, preDragBox, downPixel, preDragOptionsOverrideByMember } = activeBoxDrag;
3646
+ const { fsm, preDragPoints, preDragBox, downPixel, preDragOptionsOverrideByMember, preDragAmplifierPlacementsByMember } = activeBoxDrag;
3613
3647
  activeBoxDrag = null;
3614
3648
  driver.setDragPanEnabled(true);
3615
3649
  const distance = Math.hypot(pixel[0] - downPixel[0], pixel[1] - downPixel[1]);
3616
3650
  if (fsm.kind === "body" && distance <= CLICK_SLOP_PX) {
3617
3651
  applyConcatenatedPoints(preDragPoints);
3652
+ applyAmplifierPlacementsByMember(preDragAmplifierPlacementsByMember);
3618
3653
  renderAllPreviews();
3619
3654
  renderHandlesLayer();
3620
3655
  recomputeDirty();
@@ -3624,8 +3659,9 @@ function runGroupTransform(opts) {
3624
3659
  }
3625
3660
  const finalSnapshot = fsm.move(pixel);
3626
3661
  applyConcatenatedPoints(preDragPoints);
3662
+ applyAmplifierPlacementsByMember(preDragAmplifierPlacementsByMember);
3627
3663
  if (fsm.kind === "rotate") boxAngle += finalSnapshot.box.angle - preDragBox.angle;
3628
- commitGesture(finalSnapshot.points, preDragOptionsOverrideByMember);
3664
+ commitGesture(finalSnapshot.points, preDragOptionsOverrideByMember, preDragAmplifierPlacementsByMember.map((placements) => transformAmplifierPlacements(placements, preDragBox, finalSnapshot.box, driver)));
3629
3665
  };
3630
3666
  let pickPreview;
3631
3667
  let pickHandles;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orbat-mapper/tactical-draw",
3
- "version": "0.2.0-alpha.16",
3
+ "version": "0.2.0-alpha.17",
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.12"
41
+ "@orbat-mapper/control-measures": "^0.2.0-alpha.13"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@arethetypeswrong/core": "^0.18.3",