@orbat-mapper/tactical-draw 0.2.0-alpha.15 → 0.2.0-alpha.16
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 +14 -5
- package/dist/index.mjs +173 -12
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ControlMeasure, ControlMeasureKind, ControlMeasureSnapshot, ControlMeasureStyle, TextAmplifiers } from "@orbat-mapper/control-measures";
|
|
1
|
+
import { AmplifierPlacements, 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
|
|
@@ -461,6 +461,7 @@ interface CombinedAbort {
|
|
|
461
461
|
declare function combineWithHostSignal(hostSignal?: AbortSignal): CombinedAbort;
|
|
462
462
|
//#endregion
|
|
463
463
|
//#region src/tactical-draw/measure-options.d.ts
|
|
464
|
+
declare const DEFAULT_LABEL_SIZE_PIXELS = 14;
|
|
464
465
|
/**
|
|
465
466
|
* Where a control measure's size is anchored (ADR-0020). `ground` (the
|
|
466
467
|
* default) means a meter size fixed to the terrain — static, never re-rendered
|
|
@@ -502,6 +503,8 @@ interface InteractionStyle {
|
|
|
502
503
|
guide?: GuideStyle;
|
|
503
504
|
vertexHandle?: HandleStyle;
|
|
504
505
|
midpointHandle?: HandleStyle;
|
|
506
|
+
/** Visible grab target for a draggable rendered text label. */
|
|
507
|
+
labelHandle?: HandleStyle;
|
|
505
508
|
scaleHandle?: HandleStyle;
|
|
506
509
|
rotateHandle?: HandleStyle;
|
|
507
510
|
boxOutline?: GuideStyle;
|
|
@@ -519,12 +522,13 @@ interface InteractionStyle {
|
|
|
519
522
|
* `reshape` renders vertex/midpoint handles; `transform` renders the oriented
|
|
520
523
|
* selection box (4 corner scale handles + a rotate grip + drag-anywhere
|
|
521
524
|
* body translate) via `transform-box.ts`. The two can be combined freely —
|
|
522
|
-
* single-measure edits show both by default. `
|
|
525
|
+
* single-measure edits show both by default. `labeldrag` enables dragging
|
|
526
|
+
* rendered text labels. `delete` is an explicit
|
|
523
527
|
* destructive mode: it renders the vertex handles (so they can be tapped to
|
|
524
528
|
* remove) but no midpoints or box, and the edit controller deletes the tapped
|
|
525
529
|
* vertex instead of dragging it.
|
|
526
530
|
*/
|
|
527
|
-
type EditMode = "reshape" | "transform" | "delete";
|
|
531
|
+
type EditMode = "reshape" | "transform" | "labeldrag" | "delete";
|
|
528
532
|
//#endregion
|
|
529
533
|
//#region src/tactical-draw/session-events.d.ts
|
|
530
534
|
/**
|
|
@@ -570,6 +574,8 @@ interface EditSession {
|
|
|
570
574
|
* doctrinal field codes happens in the core render seam, not here.
|
|
571
575
|
*/
|
|
572
576
|
readonly textAmplifiers: TextAmplifiers | undefined;
|
|
577
|
+
/** Live user-adjusted anchors for individually rendered text amplifiers. */
|
|
578
|
+
readonly amplifierPlacements: AmplifierPlacements | undefined;
|
|
573
579
|
/** `true` after the first working-copy mutation; back to `false` within tolerance. */
|
|
574
580
|
readonly dirty: boolean;
|
|
575
581
|
readonly modes: readonly EditMode[];
|
|
@@ -614,6 +620,8 @@ interface EditSession {
|
|
|
614
620
|
* silent no-op after the session has settled (like [[setOptions]]).
|
|
615
621
|
*/
|
|
616
622
|
setTextAmplifiers(partial: Record<string, string | undefined>): void;
|
|
623
|
+
/** Merge field-keyed label anchors; `undefined` resets a field to its doctrinal position. */
|
|
624
|
+
setAmplifierPlacements(partial: Record<string, Position | undefined>): void;
|
|
617
625
|
/**
|
|
618
626
|
* Subscribe to working-copy changes. The listener receives an
|
|
619
627
|
* [[EditChangeEvent]] for each completed gesture (drag end, insert/delete
|
|
@@ -1009,7 +1017,8 @@ interface EditOptions {
|
|
|
1009
1017
|
* renders vertex / midpoint handles; `transform` renders the oriented
|
|
1010
1018
|
* selection box — 4 corner scale handles, a rotate grip above the top edge,
|
|
1011
1019
|
* and drag-anywhere-inside-the-box translate (ADR-0017). The two can be
|
|
1012
|
-
* combined freely (the default does). `"
|
|
1020
|
+
* combined freely (the default does). `"labeldrag"` opt-in enables repositioning
|
|
1021
|
+
* rendered text labels. `"delete"` is exclusive: it collapses
|
|
1013
1022
|
* the set to vertex-tap removal only. An unknown mode throws `TypeError`.
|
|
1014
1023
|
*/
|
|
1015
1024
|
modes?: readonly EditMode[];
|
|
@@ -1516,4 +1525,4 @@ declare function nearestWorldCopyLng(lng: number, refLng: number): number;
|
|
|
1516
1525
|
/** `nearestWorldCopyLng` applied to a lon/lat position. */
|
|
1517
1526
|
declare function nearestWorldCopyPosition(position: Position, refLng: number): Position;
|
|
1518
1527
|
//#endregion
|
|
1519
|
-
export { BaseMapAdapter, type CombinedAbort, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, type DrawMeasureDraft, type DrawOptions, type DrawSession, type EditChangeEvent, type EditManyOptions, type EditMode, type EditOptions, type EditPointerDriver, type EditSession, type FeatureOps, type GuideStyle, type HandleStyle, type InteractionStyle, type LayerId, type MapAdapter, type MapCursor, type MapEvent, type MapEventHandler, type NativeEventEntry, type PickEvent, type PickHandler, type PickOptions, type PixelCoordinate, type PointerCallback, PointerCallbackHub, type PointerEventInfo, type SettleReason, type SizeAnchor, type StyleOptions, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, type TacticalDrawAbortReason, TacticalDrawDestroyedError, type TacticalDrawLayerOptions, type TacticalDrawOptions, type TextStyleOptions, type ToPixel, type TransformBox, type TransformChangeEvent, type TransformSession, bindAbortable, coerceHandleStyle, combineWithHostSignal, getSizeAnchor, hitTestFeature, ignoreAbort, isTacticalDrawAbortError, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, resolveGroundTextSizePixels, tryControlMeasureIdFromFeature };
|
|
1528
|
+
export { BaseMapAdapter, type CombinedAbort, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, type DrawMeasureDraft, type DrawOptions, type DrawSession, type EditChangeEvent, type EditManyOptions, type EditMode, type EditOptions, type EditPointerDriver, type EditSession, type FeatureOps, type GuideStyle, type HandleStyle, type InteractionStyle, type LayerId, type MapAdapter, type MapCursor, type MapEvent, type MapEventHandler, type NativeEventEntry, type PickEvent, type PickHandler, type PickOptions, type PixelCoordinate, type PointerCallback, PointerCallbackHub, type PointerEventInfo, type SettleReason, type SizeAnchor, type StyleOptions, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, type TacticalDrawAbortReason, TacticalDrawDestroyedError, type TacticalDrawLayerOptions, type TacticalDrawOptions, type TextStyleOptions, type ToPixel, type TransformBox, type TransformChangeEvent, type TransformSession, bindAbortable, coerceHandleStyle, combineWithHostSignal, getSizeAnchor, hitTestFeature, ignoreAbort, isTacticalDrawAbortError, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, resolveGroundTextSizePixels, tryControlMeasureIdFromFeature };
|
package/dist/index.mjs
CHANGED
|
@@ -521,6 +521,9 @@ function combineWithHostSignal(hostSignal) {
|
|
|
521
521
|
dispose
|
|
522
522
|
};
|
|
523
523
|
}
|
|
524
|
+
//#endregion
|
|
525
|
+
//#region src/tactical-draw/measure-options.ts
|
|
526
|
+
const DEFAULT_LABEL_SIZE_PIXELS = 14;
|
|
524
527
|
/**
|
|
525
528
|
* Per-kind pixel-denominated *geometry* size option and the meter option it
|
|
526
529
|
* bakes into — the single source of which geometry option makes a measure
|
|
@@ -2031,6 +2034,8 @@ function boxOutlineFeature(box, fromPixel, featureId, style) {
|
|
|
2031
2034
|
*/
|
|
2032
2035
|
const DIRTY_EPSILON$1 = 1e-9;
|
|
2033
2036
|
const HANDLE_HIT_TOLERANCE_PX$1 = 12;
|
|
2037
|
+
/** Screen-space clearance between a label's anchor and its visible drag grip. */
|
|
2038
|
+
const LABEL_HANDLE_OFFSET_PX = 14;
|
|
2034
2039
|
/**
|
|
2035
2040
|
* A body drag (drag-anywhere-inside-the-box translate) whose release pixel
|
|
2036
2041
|
* lands within this many pixels of the down pixel is treated as a plain
|
|
@@ -2045,6 +2050,7 @@ const CLICK_SLOP_PX$1 = 3;
|
|
|
2045
2050
|
const ALLOWED_EDIT_MODES = new Set([
|
|
2046
2051
|
"reshape",
|
|
2047
2052
|
"transform",
|
|
2053
|
+
"labeldrag",
|
|
2048
2054
|
"delete"
|
|
2049
2055
|
]);
|
|
2050
2056
|
/**
|
|
@@ -2074,6 +2080,10 @@ function runEdit(opts) {
|
|
|
2074
2080
|
const workingPoints = editStartPoints.map((p) => [...p]);
|
|
2075
2081
|
let workingOptions = { ...measure.options ?? {} };
|
|
2076
2082
|
let workingTextAmplifiers = measure.textAmplifiers && Object.keys(measure.textAmplifiers).length > 0 ? { ...measure.textAmplifiers } : void 0;
|
|
2083
|
+
let workingAmplifierPlacements = measure.amplifierPlacements && Object.keys(measure.amplifierPlacements).length > 0 ? structuredClone(measure.amplifierPlacements) : void 0;
|
|
2084
|
+
function snapshotAmplifierPlacements() {
|
|
2085
|
+
return workingAmplifierPlacements ? structuredClone(workingAmplifierPlacements) : void 0;
|
|
2086
|
+
}
|
|
2077
2087
|
let modes = requestedModes;
|
|
2078
2088
|
let dirty = false;
|
|
2079
2089
|
let nonGeometryDirty = false;
|
|
@@ -2173,6 +2183,8 @@ function runEdit(opts) {
|
|
|
2173
2183
|
};
|
|
2174
2184
|
if (workingTextAmplifiers) working.textAmplifiers = workingTextAmplifiers;
|
|
2175
2185
|
else delete working.textAmplifiers;
|
|
2186
|
+
if (workingAmplifierPlacements) working.amplifierPlacements = workingAmplifierPlacements;
|
|
2187
|
+
else delete working.amplifierPlacements;
|
|
2176
2188
|
let features;
|
|
2177
2189
|
try {
|
|
2178
2190
|
features = renderMeasureForAdapter(adapter, working, renderOpts).features;
|
|
@@ -2202,6 +2214,31 @@ function runEdit(opts) {
|
|
|
2202
2214
|
trailingFixedSlots,
|
|
2203
2215
|
closedRing
|
|
2204
2216
|
}), handleIdPrefix, interactionStyle);
|
|
2217
|
+
if (modes.includes("labeldrag")) {
|
|
2218
|
+
const style = interactionStyle?.labelHandle;
|
|
2219
|
+
let index = 0;
|
|
2220
|
+
for (const feature of lastPreviewFeatures) {
|
|
2221
|
+
const key = feature.properties?.labelPlacementKey;
|
|
2222
|
+
if (typeof key !== "string" || feature.geometry?.type !== "Point") continue;
|
|
2223
|
+
const pixel = driver.toPixel(feature.geometry.coordinates);
|
|
2224
|
+
if (!pixel) continue;
|
|
2225
|
+
features.push({
|
|
2226
|
+
type: "Feature",
|
|
2227
|
+
id: `${handleIdPrefix}:label-handle:${index}`,
|
|
2228
|
+
geometry: {
|
|
2229
|
+
type: "Point",
|
|
2230
|
+
coordinates: driver.fromPixel([pixel[0], pixel[1] - LABEL_HANDLE_OFFSET_PX])
|
|
2231
|
+
},
|
|
2232
|
+
properties: {
|
|
2233
|
+
handle: "label",
|
|
2234
|
+
index,
|
|
2235
|
+
labelPlacementKey: key,
|
|
2236
|
+
...style ? { style: { ...style } } : {}
|
|
2237
|
+
}
|
|
2238
|
+
});
|
|
2239
|
+
index += 1;
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2205
2242
|
if (modes.includes("transform")) {
|
|
2206
2243
|
const liveBox = activeBoxDrag ? activeBoxDrag.lastSnapshot.box : box;
|
|
2207
2244
|
if (liveBox) {
|
|
@@ -2226,6 +2263,8 @@ function runEdit(opts) {
|
|
|
2226
2263
|
};
|
|
2227
2264
|
if (workingTextAmplifiers) working.textAmplifiers = workingTextAmplifiers;
|
|
2228
2265
|
else delete working.textAmplifiers;
|
|
2266
|
+
if (workingAmplifierPlacements) working.amplifierPlacements = workingAmplifierPlacements;
|
|
2267
|
+
else delete working.amplifierPlacements;
|
|
2229
2268
|
const committed = sizeAnchor === "screen" ? working : bakePixelSizeToGround(working, adapter);
|
|
2230
2269
|
return {
|
|
2231
2270
|
measure: cloneControlMeasure(committed),
|
|
@@ -2277,7 +2316,7 @@ function runEdit(opts) {
|
|
|
2277
2316
|
* - Reject restores the working state to the pre-gesture snapshot and
|
|
2278
2317
|
* recomputes `dirty`. Close/abort settle the session as normal.
|
|
2279
2318
|
*/
|
|
2280
|
-
function emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered, previousTextAmplifiers) {
|
|
2319
|
+
function emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements) {
|
|
2281
2320
|
if (ctx.settled) return;
|
|
2282
2321
|
let rejectRequested = false;
|
|
2283
2322
|
let closeRequested = false;
|
|
@@ -2310,6 +2349,7 @@ function runEdit(opts) {
|
|
|
2310
2349
|
currentStyle = previousStyle;
|
|
2311
2350
|
sizeAnchor = previousSizeAnchor;
|
|
2312
2351
|
workingTextAmplifiers = previousTextAmplifiers;
|
|
2352
|
+
workingAmplifierPlacements = previousAmplifierPlacements;
|
|
2313
2353
|
nonGeometryDirty = previousNonGeometryDirty;
|
|
2314
2354
|
renderPreview();
|
|
2315
2355
|
renderHandlesLayer();
|
|
@@ -2328,20 +2368,23 @@ function runEdit(opts) {
|
|
|
2328
2368
|
* queued during the drag are applied after the listener emission (so the
|
|
2329
2369
|
* post-gesture render reflects the requested mode set).
|
|
2330
2370
|
*/
|
|
2331
|
-
function commitGesture(nextPoints, preGestureOptions) {
|
|
2371
|
+
function commitGesture(nextPoints, preGestureOptions, nextAmplifierPlacements) {
|
|
2332
2372
|
if (ctx.settled) return;
|
|
2333
2373
|
const previousPoints = workingPoints.map((p) => [...p]);
|
|
2334
2374
|
const previousOptions = { ...preGestureOptions ?? workingOptions ?? {} };
|
|
2335
2375
|
const previousStyle = currentStyle;
|
|
2336
2376
|
const previousNonGeometryDirty = nonGeometryDirty;
|
|
2337
2377
|
const previousTextAmplifiers = workingTextAmplifiers;
|
|
2378
|
+
const previousAmplifierPlacements = snapshotAmplifierPlacements();
|
|
2338
2379
|
const previousRendered = rebuildRenderedWith(previousOptions);
|
|
2339
2380
|
replaceWorkingPoints(nextPoints);
|
|
2381
|
+
if (nextAmplifierPlacements !== void 0) workingAmplifierPlacements = structuredClone(nextAmplifierPlacements);
|
|
2340
2382
|
let currentRendered;
|
|
2341
2383
|
try {
|
|
2342
2384
|
currentRendered = rebuildRendered();
|
|
2343
2385
|
} catch (err) {
|
|
2344
2386
|
replaceWorkingPoints(previousPoints);
|
|
2387
|
+
workingAmplifierPlacements = previousAmplifierPlacements;
|
|
2345
2388
|
if (preGestureOptions !== void 0) replaceWorkingOptions(previousOptions);
|
|
2346
2389
|
dragInFlight = false;
|
|
2347
2390
|
drainPendingModes();
|
|
@@ -2352,7 +2395,7 @@ function runEdit(opts) {
|
|
|
2352
2395
|
recomputeDirty();
|
|
2353
2396
|
dragInFlight = false;
|
|
2354
2397
|
drainPendingModes();
|
|
2355
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers);
|
|
2398
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements);
|
|
2356
2399
|
}
|
|
2357
2400
|
function setOptions(partial) {
|
|
2358
2401
|
if (ctx.settled) return;
|
|
@@ -2361,6 +2404,7 @@ function runEdit(opts) {
|
|
|
2361
2404
|
const previousStyle = currentStyle;
|
|
2362
2405
|
const previousNonGeometryDirty = nonGeometryDirty;
|
|
2363
2406
|
const previousTextAmplifiers = workingTextAmplifiers;
|
|
2407
|
+
const previousAmplifierPlacements = snapshotAmplifierPlacements();
|
|
2364
2408
|
const previousRendered = rebuildRendered();
|
|
2365
2409
|
replaceWorkingOptions({
|
|
2366
2410
|
...workingOptions ?? {},
|
|
@@ -2376,7 +2420,7 @@ function runEdit(opts) {
|
|
|
2376
2420
|
renderPreview();
|
|
2377
2421
|
nonGeometryDirty = true;
|
|
2378
2422
|
dirty = true;
|
|
2379
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers);
|
|
2423
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements);
|
|
2380
2424
|
}
|
|
2381
2425
|
function setStyle(partial) {
|
|
2382
2426
|
if (ctx.settled) return;
|
|
@@ -2385,6 +2429,7 @@ function runEdit(opts) {
|
|
|
2385
2429
|
const previousStyle = currentStyle;
|
|
2386
2430
|
const previousNonGeometryDirty = nonGeometryDirty;
|
|
2387
2431
|
const previousTextAmplifiers = workingTextAmplifiers;
|
|
2432
|
+
const previousAmplifierPlacements = snapshotAmplifierPlacements();
|
|
2388
2433
|
const previousRendered = rebuildRendered();
|
|
2389
2434
|
currentStyle = {
|
|
2390
2435
|
...currentStyle ?? {},
|
|
@@ -2400,7 +2445,7 @@ function runEdit(opts) {
|
|
|
2400
2445
|
renderPreview();
|
|
2401
2446
|
nonGeometryDirty = true;
|
|
2402
2447
|
dirty = true;
|
|
2403
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers);
|
|
2448
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements);
|
|
2404
2449
|
}
|
|
2405
2450
|
/**
|
|
2406
2451
|
* Shallow-merge into the live working text amplifiers. Mirrors
|
|
@@ -2424,6 +2469,7 @@ function runEdit(opts) {
|
|
|
2424
2469
|
const previousStyle = currentStyle;
|
|
2425
2470
|
const previousNonGeometryDirty = nonGeometryDirty;
|
|
2426
2471
|
const previousTextAmplifiers = workingTextAmplifiers;
|
|
2472
|
+
const previousAmplifierPlacements = snapshotAmplifierPlacements();
|
|
2427
2473
|
const previousRendered = rebuildRendered();
|
|
2428
2474
|
const merged = { ...workingTextAmplifiers ?? {} };
|
|
2429
2475
|
for (const [key, value] of Object.entries(partial)) {
|
|
@@ -2444,7 +2490,26 @@ function runEdit(opts) {
|
|
|
2444
2490
|
renderPreview();
|
|
2445
2491
|
nonGeometryDirty = true;
|
|
2446
2492
|
dirty = true;
|
|
2447
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers);
|
|
2493
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements);
|
|
2494
|
+
}
|
|
2495
|
+
function setAmplifierPlacements(partial) {
|
|
2496
|
+
if (ctx.settled) return;
|
|
2497
|
+
const previousPoints = workingPoints.map((p) => [...p]);
|
|
2498
|
+
const previousOptions = { ...workingOptions ?? {} };
|
|
2499
|
+
const previousStyle = currentStyle;
|
|
2500
|
+
const previousNonGeometryDirty = nonGeometryDirty;
|
|
2501
|
+
const previousTextAmplifiers = workingTextAmplifiers;
|
|
2502
|
+
const previousAmplifierPlacements = snapshotAmplifierPlacements();
|
|
2503
|
+
const previousRendered = rebuildRendered();
|
|
2504
|
+
const next = { ...workingAmplifierPlacements ?? {} };
|
|
2505
|
+
for (const [field, position] of Object.entries(partial)) if (position === void 0) delete next[field];
|
|
2506
|
+
else next[field] = [...position];
|
|
2507
|
+
workingAmplifierPlacements = Object.keys(next).length > 0 ? next : void 0;
|
|
2508
|
+
const currentRendered = rebuildRendered();
|
|
2509
|
+
renderPreview();
|
|
2510
|
+
nonGeometryDirty = true;
|
|
2511
|
+
dirty = true;
|
|
2512
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements);
|
|
2448
2513
|
}
|
|
2449
2514
|
/**
|
|
2450
2515
|
* Flip the size anchor for the measure under edit (ADR-0020). Re-renders the
|
|
@@ -2461,12 +2526,13 @@ function runEdit(opts) {
|
|
|
2461
2526
|
const previousNonGeometryDirty = nonGeometryDirty;
|
|
2462
2527
|
const previousSizeAnchor = sizeAnchor;
|
|
2463
2528
|
const previousTextAmplifiers = workingTextAmplifiers;
|
|
2529
|
+
const previousAmplifierPlacements = snapshotAmplifierPlacements();
|
|
2464
2530
|
const previousRendered = rebuildRendered();
|
|
2465
2531
|
sizeAnchor = next;
|
|
2466
2532
|
const currentRendered = rebuildRendered();
|
|
2467
2533
|
nonGeometryDirty = true;
|
|
2468
2534
|
dirty = true;
|
|
2469
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered, previousTextAmplifiers);
|
|
2535
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements);
|
|
2470
2536
|
}
|
|
2471
2537
|
/** Apply a translate gesture by shifting every working point by `delta`. */
|
|
2472
2538
|
function commitTranslate(delta) {
|
|
@@ -2527,6 +2593,9 @@ function runEdit(opts) {
|
|
|
2527
2593
|
get textAmplifiers() {
|
|
2528
2594
|
return workingTextAmplifiers ? { ...workingTextAmplifiers } : void 0;
|
|
2529
2595
|
},
|
|
2596
|
+
get amplifierPlacements() {
|
|
2597
|
+
return snapshotAmplifierPlacements();
|
|
2598
|
+
},
|
|
2530
2599
|
get dirty() {
|
|
2531
2600
|
return dirty;
|
|
2532
2601
|
},
|
|
@@ -2561,6 +2630,9 @@ function runEdit(opts) {
|
|
|
2561
2630
|
setTextAmplifiers(partial) {
|
|
2562
2631
|
setTextAmplifiers(partial);
|
|
2563
2632
|
},
|
|
2633
|
+
setAmplifierPlacements(partial) {
|
|
2634
|
+
setAmplifierPlacements(partial);
|
|
2635
|
+
},
|
|
2564
2636
|
setSizeAnchor(next) {
|
|
2565
2637
|
setSizeAnchor(next);
|
|
2566
2638
|
},
|
|
@@ -2598,6 +2670,31 @@ function runEdit(opts) {
|
|
|
2598
2670
|
}
|
|
2599
2671
|
};
|
|
2600
2672
|
let activeDrag = null;
|
|
2673
|
+
let activeAmplifierDrag = null;
|
|
2674
|
+
function pickLabel(pixel, tolerance) {
|
|
2675
|
+
for (const feature of lastPreviewFeatures) {
|
|
2676
|
+
const key = feature.properties?.labelPlacementKey;
|
|
2677
|
+
if (typeof key !== "string" || feature.geometry?.type !== "Point") continue;
|
|
2678
|
+
const projected = driver.toPixel(feature.geometry.coordinates);
|
|
2679
|
+
if (!projected) continue;
|
|
2680
|
+
for (const handle of [false, true]) {
|
|
2681
|
+
const targetY = projected[1] - (handle ? LABEL_HANDLE_OFFSET_PX : 0);
|
|
2682
|
+
if (Math.hypot(projected[0] - pixel[0], targetY - pixel[1]) <= tolerance) return {
|
|
2683
|
+
key,
|
|
2684
|
+
handle
|
|
2685
|
+
};
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
return null;
|
|
2689
|
+
}
|
|
2690
|
+
/**
|
|
2691
|
+
* The label anchor a pointer pixel maps to: 1:1 for a grab on the label
|
|
2692
|
+
* itself, offset back down by the grip clearance when the drag rides the
|
|
2693
|
+
* visible handle, so the label lands where the anchor (not the grip) points.
|
|
2694
|
+
*/
|
|
2695
|
+
function labelAnchorFromPointer(pixel, handle) {
|
|
2696
|
+
return driver.fromPixel([pixel[0], pixel[1] + (handle ? LABEL_HANDLE_OFFSET_PX : 0)]);
|
|
2697
|
+
}
|
|
2601
2698
|
/**
|
|
2602
2699
|
* A live transform-box gesture (corner scale, box rotate, or body
|
|
2603
2700
|
* translate — ADR-0017). `lastSnapshot` starts as the pre-drag points/box
|
|
@@ -2607,6 +2704,24 @@ function runEdit(opts) {
|
|
|
2607
2704
|
* `boxAngle`.
|
|
2608
2705
|
*/
|
|
2609
2706
|
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
|
+
}
|
|
2610
2725
|
/**
|
|
2611
2726
|
* Whether the vertex at `index` may be deleted (via Alt+click or in the
|
|
2612
2727
|
* exclusive `delete` mode). Deletion is
|
|
@@ -2646,6 +2761,18 @@ function runEdit(opts) {
|
|
|
2646
2761
|
const onDriverDown = (pixel, info) => {
|
|
2647
2762
|
if (ctx.settled || dragInFlight) return;
|
|
2648
2763
|
const tol = info?.hitTolerance ?? HANDLE_HIT_TOLERANCE_PX$1;
|
|
2764
|
+
const labelHit = modes.includes("labeldrag") ? pickLabel(pixel, tol) : null;
|
|
2765
|
+
if (labelHit) {
|
|
2766
|
+
activeAmplifierDrag = {
|
|
2767
|
+
field: labelHit.key,
|
|
2768
|
+
handle: labelHit.handle,
|
|
2769
|
+
previous: snapshotAmplifierPlacements(),
|
|
2770
|
+
downPixel: pixel
|
|
2771
|
+
};
|
|
2772
|
+
dragInFlight = true;
|
|
2773
|
+
driver.setDragPanEnabled(false);
|
|
2774
|
+
return;
|
|
2775
|
+
}
|
|
2649
2776
|
const hit = pickHandle(buildHandleSet(workingPoints, modes, {
|
|
2650
2777
|
includeMidpoints: midpointHandlesEnabled,
|
|
2651
2778
|
trailingFixedSlots,
|
|
@@ -2705,6 +2832,7 @@ function runEdit(opts) {
|
|
|
2705
2832
|
cornerIndex: boxHit.kind === "scale" ? boxHit.index : void 0
|
|
2706
2833
|
}),
|
|
2707
2834
|
preDragPoints,
|
|
2835
|
+
preDragAmplifierPlacements: snapshotAmplifierPlacements(),
|
|
2708
2836
|
preDragBox: box,
|
|
2709
2837
|
downPixel: pixel,
|
|
2710
2838
|
lastSnapshot: {
|
|
@@ -2731,13 +2859,23 @@ function runEdit(opts) {
|
|
|
2731
2859
|
const snapshot = activeBoxDrag.fsm.move(pixel);
|
|
2732
2860
|
activeBoxDrag.lastSnapshot = snapshot;
|
|
2733
2861
|
workingPoints.splice(0, workingPoints.length, ...snapshot.points);
|
|
2862
|
+
workingAmplifierPlacements = transformAmplifierPlacements(activeBoxDrag.preDragAmplifierPlacements, activeBoxDrag.preDragBox, snapshot.box);
|
|
2734
2863
|
renderPreview();
|
|
2735
2864
|
renderHandlesLayer();
|
|
2736
2865
|
recomputeDirty();
|
|
2737
2866
|
}
|
|
2738
2867
|
const onDriverMove = (pixel) => {
|
|
2739
2868
|
if (ctx.settled) return;
|
|
2740
|
-
if (
|
|
2869
|
+
if (activeAmplifierDrag) {
|
|
2870
|
+
const position = labelAnchorFromPointer(pixel, activeAmplifierDrag.handle);
|
|
2871
|
+
if (!position) return;
|
|
2872
|
+
workingAmplifierPlacements = {
|
|
2873
|
+
...workingAmplifierPlacements ?? {},
|
|
2874
|
+
[activeAmplifierDrag.field]: position
|
|
2875
|
+
};
|
|
2876
|
+
renderPreview();
|
|
2877
|
+
renderHandlesLayer();
|
|
2878
|
+
} else if (activeDrag) paintDragSnapshot(pixel);
|
|
2741
2879
|
else if (activeBoxDrag) paintBoxDragSnapshot(pixel);
|
|
2742
2880
|
};
|
|
2743
2881
|
/** Reset drag-in-flight bookkeeping shared by every "gesture ended" path. */
|
|
@@ -2747,6 +2885,19 @@ function runEdit(opts) {
|
|
|
2747
2885
|
drainPendingModes();
|
|
2748
2886
|
}
|
|
2749
2887
|
const onDriverUp = (pixel) => {
|
|
2888
|
+
if (activeAmplifierDrag) {
|
|
2889
|
+
const { field, handle, previous, downPixel } = activeAmplifierDrag;
|
|
2890
|
+
activeAmplifierDrag = null;
|
|
2891
|
+
const position = labelAnchorFromPointer(pixel, handle);
|
|
2892
|
+
workingAmplifierPlacements = previous;
|
|
2893
|
+
endDragBookkeeping();
|
|
2894
|
+
if (position && Math.hypot(pixel[0] - downPixel[0], pixel[1] - downPixel[1]) > CLICK_SLOP_PX$1) setAmplifierPlacements({ [field]: position });
|
|
2895
|
+
else {
|
|
2896
|
+
renderPreview();
|
|
2897
|
+
renderHandlesLayer();
|
|
2898
|
+
}
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2750
2901
|
if (activeDrag) {
|
|
2751
2902
|
if (ctx.settled) {
|
|
2752
2903
|
activeDrag = null;
|
|
@@ -2769,12 +2920,13 @@ function runEdit(opts) {
|
|
|
2769
2920
|
driver.setDragPanEnabled(true);
|
|
2770
2921
|
return;
|
|
2771
2922
|
}
|
|
2772
|
-
const { fsm, preDragPoints, preDragBox, downPixel, preDragOptions } = activeBoxDrag;
|
|
2923
|
+
const { fsm, preDragPoints, preDragAmplifierPlacements, preDragBox, downPixel, preDragOptions } = activeBoxDrag;
|
|
2773
2924
|
activeBoxDrag = null;
|
|
2774
2925
|
driver.setDragPanEnabled(true);
|
|
2775
2926
|
const distance = Math.hypot(pixel[0] - downPixel[0], pixel[1] - downPixel[1]);
|
|
2776
2927
|
if (fsm.kind === "body" && distance <= CLICK_SLOP_PX$1) {
|
|
2777
2928
|
replaceWorkingPoints(preDragPoints);
|
|
2929
|
+
workingAmplifierPlacements = preDragAmplifierPlacements;
|
|
2778
2930
|
renderPreview();
|
|
2779
2931
|
renderHandlesLayer();
|
|
2780
2932
|
recomputeDirty();
|
|
@@ -2784,8 +2936,9 @@ function runEdit(opts) {
|
|
|
2784
2936
|
}
|
|
2785
2937
|
const finalSnapshot = fsm.move(pixel);
|
|
2786
2938
|
replaceWorkingPoints(preDragPoints);
|
|
2939
|
+
workingAmplifierPlacements = preDragAmplifierPlacements;
|
|
2787
2940
|
if (fsm.kind === "rotate") boxAngle += finalSnapshot.box.angle - preDragBox.angle;
|
|
2788
|
-
commitGesture(finalSnapshot.points, preDragOptions);
|
|
2941
|
+
commitGesture(finalSnapshot.points, preDragOptions, transformAmplifierPlacements(preDragAmplifierPlacements, preDragBox, finalSnapshot.box));
|
|
2789
2942
|
return;
|
|
2790
2943
|
}
|
|
2791
2944
|
};
|
|
@@ -2858,10 +3011,11 @@ function runEdit(opts) {
|
|
|
2858
3011
|
unsubDown();
|
|
2859
3012
|
unsubMove();
|
|
2860
3013
|
unsubUp();
|
|
2861
|
-
if (activeDrag !== null || activeBoxDrag !== null) {
|
|
3014
|
+
if (activeDrag !== null || activeBoxDrag !== null || activeAmplifierDrag !== null) {
|
|
2862
3015
|
driver.setDragPanEnabled(true);
|
|
2863
3016
|
activeDrag = null;
|
|
2864
3017
|
activeBoxDrag = null;
|
|
3018
|
+
activeAmplifierDrag = null;
|
|
2865
3019
|
dragInFlight = false;
|
|
2866
3020
|
}
|
|
2867
3021
|
driver.dispose();
|
|
@@ -4324,6 +4478,12 @@ const BUILT_IN_INTERACTION_STYLE = {
|
|
|
4324
4478
|
strokeColor: "#6b7280",
|
|
4325
4479
|
strokeWidth: 1.5
|
|
4326
4480
|
},
|
|
4481
|
+
labelHandle: {
|
|
4482
|
+
pointRadius: 6,
|
|
4483
|
+
fillColor: "#7c3aed",
|
|
4484
|
+
strokeColor: "#ffffff",
|
|
4485
|
+
strokeWidth: 2
|
|
4486
|
+
},
|
|
4327
4487
|
scaleHandle: {
|
|
4328
4488
|
pointRadius: 5,
|
|
4329
4489
|
fillColor: "#ffffff",
|
|
@@ -4340,6 +4500,7 @@ const BUILT_IN_INTERACTION_STYLE = {
|
|
|
4340
4500
|
const HANDLE_SLOTS = [
|
|
4341
4501
|
"vertexHandle",
|
|
4342
4502
|
"midpointHandle",
|
|
4503
|
+
"labelHandle",
|
|
4343
4504
|
"scaleHandle",
|
|
4344
4505
|
"rotateHandle"
|
|
4345
4506
|
];
|
|
@@ -4403,4 +4564,4 @@ function nearestWorldCopyPosition(position, refLng) {
|
|
|
4403
4564
|
return [nearestWorldCopyLng(position[0], refLng), ...position.slice(1)];
|
|
4404
4565
|
}
|
|
4405
4566
|
//#endregion
|
|
4406
|
-
export { BaseMapAdapter, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, PointerCallbackHub, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, TacticalDrawDestroyedError, bindAbortable, coerceHandleStyle, combineWithHostSignal, getSizeAnchor, hitTestFeature, ignoreAbort, isTacticalDrawAbortError, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, resolveGroundTextSizePixels, tryControlMeasureIdFromFeature };
|
|
4567
|
+
export { BaseMapAdapter, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, PointerCallbackHub, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, TacticalDrawDestroyedError, bindAbortable, coerceHandleStyle, combineWithHostSignal, getSizeAnchor, hitTestFeature, ignoreAbort, isTacticalDrawAbortError, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, resolveGroundTextSizePixels, tryControlMeasureIdFromFeature };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orbat-mapper/tactical-draw",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.16",
|
|
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.
|
|
41
|
+
"@orbat-mapper/control-measures": "^0.2.0-alpha.12"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@arethetypeswrong/core": "^0.18.3",
|