@orbat-mapper/tactical-draw 0.2.0-alpha.14 → 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 +201 -19
- 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,18 +2034,23 @@ 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
|
|
2037
2042
|
* click, not a gesture: it is not committed (geometry restored, no
|
|
2038
|
-
* `EditChangeEvent`, `dirty` untouched)
|
|
2039
|
-
* suppressed so the click doesn't
|
|
2040
|
-
*
|
|
2043
|
+
* `EditChangeEvent`, `dirty` untouched). The click-away's *empty-map* close is
|
|
2044
|
+
* suppressed so the click doesn't end the edit, but a pick on another
|
|
2045
|
+
* graphics-layer measure inside the box still closes-then-selects it.
|
|
2046
|
+
* Corner/rotate drags commit even at zero movement (matches the pre-ADR-0017
|
|
2047
|
+
* translate/rotate seams).
|
|
2041
2048
|
*/
|
|
2042
2049
|
const CLICK_SLOP_PX$1 = 3;
|
|
2043
2050
|
const ALLOWED_EDIT_MODES = new Set([
|
|
2044
2051
|
"reshape",
|
|
2045
2052
|
"transform",
|
|
2053
|
+
"labeldrag",
|
|
2046
2054
|
"delete"
|
|
2047
2055
|
]);
|
|
2048
2056
|
/**
|
|
@@ -2072,6 +2080,10 @@ function runEdit(opts) {
|
|
|
2072
2080
|
const workingPoints = editStartPoints.map((p) => [...p]);
|
|
2073
2081
|
let workingOptions = { ...measure.options ?? {} };
|
|
2074
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
|
+
}
|
|
2075
2087
|
let modes = requestedModes;
|
|
2076
2088
|
let dirty = false;
|
|
2077
2089
|
let nonGeometryDirty = false;
|
|
@@ -2083,6 +2095,15 @@ function runEdit(opts) {
|
|
|
2083
2095
|
*/
|
|
2084
2096
|
let suppressClickAway = false;
|
|
2085
2097
|
/**
|
|
2098
|
+
* Set when a zero-movement body drag (a plain click inside the transform
|
|
2099
|
+
* box) released this pointer cycle. Unlike {@link suppressClickAway} it only
|
|
2100
|
+
* suppresses the *empty-map* close in `handleClick` — a stashed pick on
|
|
2101
|
+
* another graphics-layer measure still closes-then-re-dispatches, so
|
|
2102
|
+
* measures sitting inside a large box stay selectable. Consumed by the next
|
|
2103
|
+
* `handleClick`.
|
|
2104
|
+
*/
|
|
2105
|
+
let bodyClickPending = false;
|
|
2106
|
+
/**
|
|
2086
2107
|
* Mutable style snapshot. Initialised from the edit-start input; replaced
|
|
2087
2108
|
* when the façade routes an external style change to us via
|
|
2088
2109
|
* `notifyExternalMeasure`. `EditSession.measure` stays the immutable
|
|
@@ -2162,6 +2183,8 @@ function runEdit(opts) {
|
|
|
2162
2183
|
};
|
|
2163
2184
|
if (workingTextAmplifiers) working.textAmplifiers = workingTextAmplifiers;
|
|
2164
2185
|
else delete working.textAmplifiers;
|
|
2186
|
+
if (workingAmplifierPlacements) working.amplifierPlacements = workingAmplifierPlacements;
|
|
2187
|
+
else delete working.amplifierPlacements;
|
|
2165
2188
|
let features;
|
|
2166
2189
|
try {
|
|
2167
2190
|
features = renderMeasureForAdapter(adapter, working, renderOpts).features;
|
|
@@ -2191,6 +2214,31 @@ function runEdit(opts) {
|
|
|
2191
2214
|
trailingFixedSlots,
|
|
2192
2215
|
closedRing
|
|
2193
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
|
+
}
|
|
2194
2242
|
if (modes.includes("transform")) {
|
|
2195
2243
|
const liveBox = activeBoxDrag ? activeBoxDrag.lastSnapshot.box : box;
|
|
2196
2244
|
if (liveBox) {
|
|
@@ -2215,6 +2263,8 @@ function runEdit(opts) {
|
|
|
2215
2263
|
};
|
|
2216
2264
|
if (workingTextAmplifiers) working.textAmplifiers = workingTextAmplifiers;
|
|
2217
2265
|
else delete working.textAmplifiers;
|
|
2266
|
+
if (workingAmplifierPlacements) working.amplifierPlacements = workingAmplifierPlacements;
|
|
2267
|
+
else delete working.amplifierPlacements;
|
|
2218
2268
|
const committed = sizeAnchor === "screen" ? working : bakePixelSizeToGround(working, adapter);
|
|
2219
2269
|
return {
|
|
2220
2270
|
measure: cloneControlMeasure(committed),
|
|
@@ -2266,7 +2316,7 @@ function runEdit(opts) {
|
|
|
2266
2316
|
* - Reject restores the working state to the pre-gesture snapshot and
|
|
2267
2317
|
* recomputes `dirty`. Close/abort settle the session as normal.
|
|
2268
2318
|
*/
|
|
2269
|
-
function emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered, previousTextAmplifiers) {
|
|
2319
|
+
function emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements) {
|
|
2270
2320
|
if (ctx.settled) return;
|
|
2271
2321
|
let rejectRequested = false;
|
|
2272
2322
|
let closeRequested = false;
|
|
@@ -2299,6 +2349,7 @@ function runEdit(opts) {
|
|
|
2299
2349
|
currentStyle = previousStyle;
|
|
2300
2350
|
sizeAnchor = previousSizeAnchor;
|
|
2301
2351
|
workingTextAmplifiers = previousTextAmplifiers;
|
|
2352
|
+
workingAmplifierPlacements = previousAmplifierPlacements;
|
|
2302
2353
|
nonGeometryDirty = previousNonGeometryDirty;
|
|
2303
2354
|
renderPreview();
|
|
2304
2355
|
renderHandlesLayer();
|
|
@@ -2317,20 +2368,23 @@ function runEdit(opts) {
|
|
|
2317
2368
|
* queued during the drag are applied after the listener emission (so the
|
|
2318
2369
|
* post-gesture render reflects the requested mode set).
|
|
2319
2370
|
*/
|
|
2320
|
-
function commitGesture(nextPoints, preGestureOptions) {
|
|
2371
|
+
function commitGesture(nextPoints, preGestureOptions, nextAmplifierPlacements) {
|
|
2321
2372
|
if (ctx.settled) return;
|
|
2322
2373
|
const previousPoints = workingPoints.map((p) => [...p]);
|
|
2323
2374
|
const previousOptions = { ...preGestureOptions ?? workingOptions ?? {} };
|
|
2324
2375
|
const previousStyle = currentStyle;
|
|
2325
2376
|
const previousNonGeometryDirty = nonGeometryDirty;
|
|
2326
2377
|
const previousTextAmplifiers = workingTextAmplifiers;
|
|
2378
|
+
const previousAmplifierPlacements = snapshotAmplifierPlacements();
|
|
2327
2379
|
const previousRendered = rebuildRenderedWith(previousOptions);
|
|
2328
2380
|
replaceWorkingPoints(nextPoints);
|
|
2381
|
+
if (nextAmplifierPlacements !== void 0) workingAmplifierPlacements = structuredClone(nextAmplifierPlacements);
|
|
2329
2382
|
let currentRendered;
|
|
2330
2383
|
try {
|
|
2331
2384
|
currentRendered = rebuildRendered();
|
|
2332
2385
|
} catch (err) {
|
|
2333
2386
|
replaceWorkingPoints(previousPoints);
|
|
2387
|
+
workingAmplifierPlacements = previousAmplifierPlacements;
|
|
2334
2388
|
if (preGestureOptions !== void 0) replaceWorkingOptions(previousOptions);
|
|
2335
2389
|
dragInFlight = false;
|
|
2336
2390
|
drainPendingModes();
|
|
@@ -2341,7 +2395,7 @@ function runEdit(opts) {
|
|
|
2341
2395
|
recomputeDirty();
|
|
2342
2396
|
dragInFlight = false;
|
|
2343
2397
|
drainPendingModes();
|
|
2344
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers);
|
|
2398
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements);
|
|
2345
2399
|
}
|
|
2346
2400
|
function setOptions(partial) {
|
|
2347
2401
|
if (ctx.settled) return;
|
|
@@ -2350,6 +2404,7 @@ function runEdit(opts) {
|
|
|
2350
2404
|
const previousStyle = currentStyle;
|
|
2351
2405
|
const previousNonGeometryDirty = nonGeometryDirty;
|
|
2352
2406
|
const previousTextAmplifiers = workingTextAmplifiers;
|
|
2407
|
+
const previousAmplifierPlacements = snapshotAmplifierPlacements();
|
|
2353
2408
|
const previousRendered = rebuildRendered();
|
|
2354
2409
|
replaceWorkingOptions({
|
|
2355
2410
|
...workingOptions ?? {},
|
|
@@ -2365,7 +2420,7 @@ function runEdit(opts) {
|
|
|
2365
2420
|
renderPreview();
|
|
2366
2421
|
nonGeometryDirty = true;
|
|
2367
2422
|
dirty = true;
|
|
2368
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers);
|
|
2423
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements);
|
|
2369
2424
|
}
|
|
2370
2425
|
function setStyle(partial) {
|
|
2371
2426
|
if (ctx.settled) return;
|
|
@@ -2374,6 +2429,7 @@ function runEdit(opts) {
|
|
|
2374
2429
|
const previousStyle = currentStyle;
|
|
2375
2430
|
const previousNonGeometryDirty = nonGeometryDirty;
|
|
2376
2431
|
const previousTextAmplifiers = workingTextAmplifiers;
|
|
2432
|
+
const previousAmplifierPlacements = snapshotAmplifierPlacements();
|
|
2377
2433
|
const previousRendered = rebuildRendered();
|
|
2378
2434
|
currentStyle = {
|
|
2379
2435
|
...currentStyle ?? {},
|
|
@@ -2389,7 +2445,7 @@ function runEdit(opts) {
|
|
|
2389
2445
|
renderPreview();
|
|
2390
2446
|
nonGeometryDirty = true;
|
|
2391
2447
|
dirty = true;
|
|
2392
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers);
|
|
2448
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements);
|
|
2393
2449
|
}
|
|
2394
2450
|
/**
|
|
2395
2451
|
* Shallow-merge into the live working text amplifiers. Mirrors
|
|
@@ -2413,6 +2469,7 @@ function runEdit(opts) {
|
|
|
2413
2469
|
const previousStyle = currentStyle;
|
|
2414
2470
|
const previousNonGeometryDirty = nonGeometryDirty;
|
|
2415
2471
|
const previousTextAmplifiers = workingTextAmplifiers;
|
|
2472
|
+
const previousAmplifierPlacements = snapshotAmplifierPlacements();
|
|
2416
2473
|
const previousRendered = rebuildRendered();
|
|
2417
2474
|
const merged = { ...workingTextAmplifiers ?? {} };
|
|
2418
2475
|
for (const [key, value] of Object.entries(partial)) {
|
|
@@ -2433,7 +2490,26 @@ function runEdit(opts) {
|
|
|
2433
2490
|
renderPreview();
|
|
2434
2491
|
nonGeometryDirty = true;
|
|
2435
2492
|
dirty = true;
|
|
2436
|
-
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);
|
|
2437
2513
|
}
|
|
2438
2514
|
/**
|
|
2439
2515
|
* Flip the size anchor for the measure under edit (ADR-0020). Re-renders the
|
|
@@ -2450,12 +2526,13 @@ function runEdit(opts) {
|
|
|
2450
2526
|
const previousNonGeometryDirty = nonGeometryDirty;
|
|
2451
2527
|
const previousSizeAnchor = sizeAnchor;
|
|
2452
2528
|
const previousTextAmplifiers = workingTextAmplifiers;
|
|
2529
|
+
const previousAmplifierPlacements = snapshotAmplifierPlacements();
|
|
2453
2530
|
const previousRendered = rebuildRendered();
|
|
2454
2531
|
sizeAnchor = next;
|
|
2455
2532
|
const currentRendered = rebuildRendered();
|
|
2456
2533
|
nonGeometryDirty = true;
|
|
2457
2534
|
dirty = true;
|
|
2458
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered, previousTextAmplifiers);
|
|
2535
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements);
|
|
2459
2536
|
}
|
|
2460
2537
|
/** Apply a translate gesture by shifting every working point by `delta`. */
|
|
2461
2538
|
function commitTranslate(delta) {
|
|
@@ -2516,6 +2593,9 @@ function runEdit(opts) {
|
|
|
2516
2593
|
get textAmplifiers() {
|
|
2517
2594
|
return workingTextAmplifiers ? { ...workingTextAmplifiers } : void 0;
|
|
2518
2595
|
},
|
|
2596
|
+
get amplifierPlacements() {
|
|
2597
|
+
return snapshotAmplifierPlacements();
|
|
2598
|
+
},
|
|
2519
2599
|
get dirty() {
|
|
2520
2600
|
return dirty;
|
|
2521
2601
|
},
|
|
@@ -2550,6 +2630,9 @@ function runEdit(opts) {
|
|
|
2550
2630
|
setTextAmplifiers(partial) {
|
|
2551
2631
|
setTextAmplifiers(partial);
|
|
2552
2632
|
},
|
|
2633
|
+
setAmplifierPlacements(partial) {
|
|
2634
|
+
setAmplifierPlacements(partial);
|
|
2635
|
+
},
|
|
2553
2636
|
setSizeAnchor(next) {
|
|
2554
2637
|
setSizeAnchor(next);
|
|
2555
2638
|
},
|
|
@@ -2587,6 +2670,31 @@ function runEdit(opts) {
|
|
|
2587
2670
|
}
|
|
2588
2671
|
};
|
|
2589
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
|
+
}
|
|
2590
2698
|
/**
|
|
2591
2699
|
* A live transform-box gesture (corner scale, box rotate, or body
|
|
2592
2700
|
* translate — ADR-0017). `lastSnapshot` starts as the pre-drag points/box
|
|
@@ -2596,6 +2704,24 @@ function runEdit(opts) {
|
|
|
2596
2704
|
* `boxAngle`.
|
|
2597
2705
|
*/
|
|
2598
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
|
+
}
|
|
2599
2725
|
/**
|
|
2600
2726
|
* Whether the vertex at `index` may be deleted (via Alt+click or in the
|
|
2601
2727
|
* exclusive `delete` mode). Deletion is
|
|
@@ -2635,6 +2761,18 @@ function runEdit(opts) {
|
|
|
2635
2761
|
const onDriverDown = (pixel, info) => {
|
|
2636
2762
|
if (ctx.settled || dragInFlight) return;
|
|
2637
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
|
+
}
|
|
2638
2776
|
const hit = pickHandle(buildHandleSet(workingPoints, modes, {
|
|
2639
2777
|
includeMidpoints: midpointHandlesEnabled,
|
|
2640
2778
|
trailingFixedSlots,
|
|
@@ -2694,6 +2832,7 @@ function runEdit(opts) {
|
|
|
2694
2832
|
cornerIndex: boxHit.kind === "scale" ? boxHit.index : void 0
|
|
2695
2833
|
}),
|
|
2696
2834
|
preDragPoints,
|
|
2835
|
+
preDragAmplifierPlacements: snapshotAmplifierPlacements(),
|
|
2697
2836
|
preDragBox: box,
|
|
2698
2837
|
downPixel: pixel,
|
|
2699
2838
|
lastSnapshot: {
|
|
@@ -2720,13 +2859,23 @@ function runEdit(opts) {
|
|
|
2720
2859
|
const snapshot = activeBoxDrag.fsm.move(pixel);
|
|
2721
2860
|
activeBoxDrag.lastSnapshot = snapshot;
|
|
2722
2861
|
workingPoints.splice(0, workingPoints.length, ...snapshot.points);
|
|
2862
|
+
workingAmplifierPlacements = transformAmplifierPlacements(activeBoxDrag.preDragAmplifierPlacements, activeBoxDrag.preDragBox, snapshot.box);
|
|
2723
2863
|
renderPreview();
|
|
2724
2864
|
renderHandlesLayer();
|
|
2725
2865
|
recomputeDirty();
|
|
2726
2866
|
}
|
|
2727
2867
|
const onDriverMove = (pixel) => {
|
|
2728
2868
|
if (ctx.settled) return;
|
|
2729
|
-
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);
|
|
2730
2879
|
else if (activeBoxDrag) paintBoxDragSnapshot(pixel);
|
|
2731
2880
|
};
|
|
2732
2881
|
/** Reset drag-in-flight bookkeeping shared by every "gesture ended" path. */
|
|
@@ -2736,6 +2885,19 @@ function runEdit(opts) {
|
|
|
2736
2885
|
drainPendingModes();
|
|
2737
2886
|
}
|
|
2738
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
|
+
}
|
|
2739
2901
|
if (activeDrag) {
|
|
2740
2902
|
if (ctx.settled) {
|
|
2741
2903
|
activeDrag = null;
|
|
@@ -2758,23 +2920,25 @@ function runEdit(opts) {
|
|
|
2758
2920
|
driver.setDragPanEnabled(true);
|
|
2759
2921
|
return;
|
|
2760
2922
|
}
|
|
2761
|
-
const { fsm, preDragPoints, preDragBox, downPixel, preDragOptions } = activeBoxDrag;
|
|
2923
|
+
const { fsm, preDragPoints, preDragAmplifierPlacements, preDragBox, downPixel, preDragOptions } = activeBoxDrag;
|
|
2762
2924
|
activeBoxDrag = null;
|
|
2763
2925
|
driver.setDragPanEnabled(true);
|
|
2764
2926
|
const distance = Math.hypot(pixel[0] - downPixel[0], pixel[1] - downPixel[1]);
|
|
2765
2927
|
if (fsm.kind === "body" && distance <= CLICK_SLOP_PX$1) {
|
|
2766
2928
|
replaceWorkingPoints(preDragPoints);
|
|
2929
|
+
workingAmplifierPlacements = preDragAmplifierPlacements;
|
|
2767
2930
|
renderPreview();
|
|
2768
2931
|
renderHandlesLayer();
|
|
2769
2932
|
recomputeDirty();
|
|
2770
2933
|
endDragBookkeeping();
|
|
2771
|
-
|
|
2934
|
+
bodyClickPending = true;
|
|
2772
2935
|
return;
|
|
2773
2936
|
}
|
|
2774
2937
|
const finalSnapshot = fsm.move(pixel);
|
|
2775
2938
|
replaceWorkingPoints(preDragPoints);
|
|
2939
|
+
workingAmplifierPlacements = preDragAmplifierPlacements;
|
|
2776
2940
|
if (fsm.kind === "rotate") boxAngle += finalSnapshot.box.angle - preDragBox.angle;
|
|
2777
|
-
commitGesture(finalSnapshot.points, preDragOptions);
|
|
2941
|
+
commitGesture(finalSnapshot.points, preDragOptions, transformAmplifierPlacements(preDragAmplifierPlacements, preDragBox, finalSnapshot.box));
|
|
2778
2942
|
return;
|
|
2779
2943
|
}
|
|
2780
2944
|
};
|
|
@@ -2794,21 +2958,25 @@ function runEdit(opts) {
|
|
|
2794
2958
|
const localPreview = pickPreview;
|
|
2795
2959
|
const localHandles = pickHandles;
|
|
2796
2960
|
const localGraphics = pickGraphics;
|
|
2961
|
+
const wasBodyClick = bodyClickPending;
|
|
2797
2962
|
pickPreview = void 0;
|
|
2798
2963
|
pickHandles = void 0;
|
|
2799
2964
|
pickGraphics = void 0;
|
|
2965
|
+
bodyClickPending = false;
|
|
2800
2966
|
if (ctx.settled) return;
|
|
2801
2967
|
if (!closeOnClickAway) return;
|
|
2802
2968
|
if (suppressClickAway) {
|
|
2803
2969
|
suppressClickAway = false;
|
|
2804
2970
|
return;
|
|
2805
2971
|
}
|
|
2806
|
-
if (
|
|
2972
|
+
if (localHandles) return;
|
|
2807
2973
|
if (localGraphics && localGraphics.id !== measure.id) {
|
|
2808
2974
|
ctx.commit(rebuildRendered);
|
|
2809
2975
|
if (onClickAwayGraphicsPick) onClickAwayGraphicsPick(localGraphics);
|
|
2810
2976
|
return;
|
|
2811
2977
|
}
|
|
2978
|
+
if (localPreview) return;
|
|
2979
|
+
if (wasBodyClick) return;
|
|
2812
2980
|
ctx.commit(rebuildRendered);
|
|
2813
2981
|
};
|
|
2814
2982
|
const core = runInteraction({
|
|
@@ -2843,10 +3011,11 @@ function runEdit(opts) {
|
|
|
2843
3011
|
unsubDown();
|
|
2844
3012
|
unsubMove();
|
|
2845
3013
|
unsubUp();
|
|
2846
|
-
if (activeDrag !== null || activeBoxDrag !== null) {
|
|
3014
|
+
if (activeDrag !== null || activeBoxDrag !== null || activeAmplifierDrag !== null) {
|
|
2847
3015
|
driver.setDragPanEnabled(true);
|
|
2848
3016
|
activeDrag = null;
|
|
2849
3017
|
activeBoxDrag = null;
|
|
3018
|
+
activeAmplifierDrag = null;
|
|
2850
3019
|
dragInFlight = false;
|
|
2851
3020
|
}
|
|
2852
3021
|
driver.dispose();
|
|
@@ -3015,6 +3184,8 @@ function runGroupTransform(opts) {
|
|
|
3015
3184
|
let dragInFlight = false;
|
|
3016
3185
|
/** Same click-away suppression role as `edit-controller.ts`'s flag of the same name. */
|
|
3017
3186
|
let suppressClickAway = false;
|
|
3187
|
+
/** Same body-click role as `edit-controller.ts`'s flag of the same name. */
|
|
3188
|
+
let bodyClickPending = false;
|
|
3018
3189
|
const changeListeners = /* @__PURE__ */ new Set();
|
|
3019
3190
|
const commitEmitter = createCommitEmitter("TransformSession.onCommit listener errors");
|
|
3020
3191
|
const settleEmitter = createSettleEmitter("TransformSession.onSettled listener errors");
|
|
@@ -3448,7 +3619,7 @@ function runGroupTransform(opts) {
|
|
|
3448
3619
|
renderHandlesLayer();
|
|
3449
3620
|
recomputeDirty();
|
|
3450
3621
|
dragInFlight = false;
|
|
3451
|
-
|
|
3622
|
+
bodyClickPending = true;
|
|
3452
3623
|
return;
|
|
3453
3624
|
}
|
|
3454
3625
|
const finalSnapshot = fsm.move(pixel);
|
|
@@ -3472,22 +3643,26 @@ function runGroupTransform(opts) {
|
|
|
3472
3643
|
const localPreview = pickPreview;
|
|
3473
3644
|
const localHandles = pickHandles;
|
|
3474
3645
|
const localGraphics = pickGraphics;
|
|
3646
|
+
const wasBodyClick = bodyClickPending;
|
|
3475
3647
|
pickPreview = void 0;
|
|
3476
3648
|
pickHandles = void 0;
|
|
3477
3649
|
pickGraphics = void 0;
|
|
3650
|
+
bodyClickPending = false;
|
|
3478
3651
|
if (ctx.settled) return;
|
|
3479
3652
|
if (!closeOnClickAway) return;
|
|
3480
3653
|
if (suppressClickAway) {
|
|
3481
3654
|
suppressClickAway = false;
|
|
3482
3655
|
return;
|
|
3483
3656
|
}
|
|
3484
|
-
if (
|
|
3657
|
+
if (localHandles) return;
|
|
3485
3658
|
const isMember = localGraphics ? members.some((m) => m.measure.id === localGraphics.id) : false;
|
|
3486
3659
|
if (localGraphics && !isMember) {
|
|
3487
3660
|
commitClose();
|
|
3488
3661
|
if (onClickAwayGraphicsPick) onClickAwayGraphicsPick(localGraphics);
|
|
3489
3662
|
return;
|
|
3490
3663
|
}
|
|
3664
|
+
if (localPreview) return;
|
|
3665
|
+
if (wasBodyClick) return;
|
|
3491
3666
|
commitClose();
|
|
3492
3667
|
};
|
|
3493
3668
|
const core = runInteraction({
|
|
@@ -4303,6 +4478,12 @@ const BUILT_IN_INTERACTION_STYLE = {
|
|
|
4303
4478
|
strokeColor: "#6b7280",
|
|
4304
4479
|
strokeWidth: 1.5
|
|
4305
4480
|
},
|
|
4481
|
+
labelHandle: {
|
|
4482
|
+
pointRadius: 6,
|
|
4483
|
+
fillColor: "#7c3aed",
|
|
4484
|
+
strokeColor: "#ffffff",
|
|
4485
|
+
strokeWidth: 2
|
|
4486
|
+
},
|
|
4306
4487
|
scaleHandle: {
|
|
4307
4488
|
pointRadius: 5,
|
|
4308
4489
|
fillColor: "#ffffff",
|
|
@@ -4319,6 +4500,7 @@ const BUILT_IN_INTERACTION_STYLE = {
|
|
|
4319
4500
|
const HANDLE_SLOTS = [
|
|
4320
4501
|
"vertexHandle",
|
|
4321
4502
|
"midpointHandle",
|
|
4503
|
+
"labelHandle",
|
|
4322
4504
|
"scaleHandle",
|
|
4323
4505
|
"rotateHandle"
|
|
4324
4506
|
];
|
|
@@ -4382,4 +4564,4 @@ function nearestWorldCopyPosition(position, refLng) {
|
|
|
4382
4564
|
return [nearestWorldCopyLng(position[0], refLng), ...position.slice(1)];
|
|
4383
4565
|
}
|
|
4384
4566
|
//#endregion
|
|
4385
|
-
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",
|