@orbat-mapper/tactical-draw 0.2.0-alpha.37 → 0.2.0-alpha.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as PointerEventInfo, _ as fillPatternImageDimension, a as MapAdapter, b as EditPointerDriver, c as MapEventHandler, d as PickOptions, f as PixelCoordinate, g as HatchPattern, h as HATCH_PATTERNS, i as LayerId, l as PickEvent, m as TextStyleOptions, n as HandleStyle, o as MapCursor, p as StyleOptions, r as IconResource, s as MapEvent, t as FeatureGroup, u as PickHandler, v as fillPatternImageId, x as PointerCallback, y as makeFillPatternImage } from "./map-adapter-
|
|
1
|
+
import { S as PointerEventInfo, _ as fillPatternImageDimension, a as MapAdapter, b as EditPointerDriver, c as MapEventHandler, d as PickOptions, f as PixelCoordinate, g as HatchPattern, h as HATCH_PATTERNS, i as LayerId, l as PickEvent, m as TextStyleOptions, n as HandleStyle, o as MapCursor, p as StyleOptions, r as IconResource, s as MapEvent, t as FeatureGroup, u as PickHandler, v as fillPatternImageId, x as PointerCallback, y as makeFillPatternImage } from "./map-adapter-84irgVJR.mjs";
|
|
2
2
|
import { AmplifierPlacement, AmplifierPlacements, ControlMeasure, ControlMeasureKind, ControlMeasureRender, ControlMeasureStyle, TextAmplifiers } from "@orbat-mapper/control-measures";
|
|
3
3
|
import { Feature, Position } from "geojson";
|
|
4
4
|
|
|
@@ -400,6 +400,15 @@ interface PointSymbolCapability {
|
|
|
400
400
|
readonly defaultRotation: number;
|
|
401
401
|
render(symbol: PointSymbol): PointSymbolRender;
|
|
402
402
|
invalidate(): void;
|
|
403
|
+
/**
|
|
404
|
+
* Monotonic counter that increments on each `invalidate()` call. Consumers
|
|
405
|
+
* (e.g. a render cache keyed on graphic snapshot identity) may read this to
|
|
406
|
+
* detect that previously rendered point-symbol output is stale even though
|
|
407
|
+
* the graphic snapshot itself is unchanged. Optional — when absent,
|
|
408
|
+
* invalidation cannot be detected by cache layers, so they must not attempt
|
|
409
|
+
* to key on it.
|
|
410
|
+
*/
|
|
411
|
+
readonly generation?: number;
|
|
403
412
|
}
|
|
404
413
|
declare function isPointSymbol(graphic: unknown): graphic is PointSymbol;
|
|
405
414
|
declare function clonePointSymbol(symbol: PointSymbol): PointSymbol;
|
|
@@ -616,7 +625,15 @@ interface EditSession {
|
|
|
616
625
|
readonly history: SessionHistory;
|
|
617
626
|
/** Immutable edit-start input. */
|
|
618
627
|
readonly graphic: ControlMeasure;
|
|
619
|
-
/**
|
|
628
|
+
/**
|
|
629
|
+
* Live complete working graphic. Copy on read. `properties` (host-owned
|
|
630
|
+
* metadata, e.g. `standardIdentity`/`colorMode`) is session-owned like every
|
|
631
|
+
* other working field: it starts at the edit-start value, is replaced
|
|
632
|
+
* wholesale by [[updateGraphic]], survives every subsequent gesture/setter
|
|
633
|
+
* call and in-session undo/redo, and is rolled back by `event.reject()`
|
|
634
|
+
* (issue #354). Absent when the edit-start measure carried none and
|
|
635
|
+
* [[updateGraphic]] has not set any since.
|
|
636
|
+
*/
|
|
620
637
|
readonly workingGraphic: ControlMeasure;
|
|
621
638
|
/** Live working state. Copy on read. */
|
|
622
639
|
readonly controlPoints: readonly Position[];
|
|
@@ -1443,6 +1460,23 @@ declare class TacticalDraw {
|
|
|
1443
1460
|
/** Snapshot of the host's latest authoritative Graphic membership and order. */
|
|
1444
1461
|
private lastRenderedMeasures;
|
|
1445
1462
|
private snapGeometryFeatures;
|
|
1463
|
+
/**
|
|
1464
|
+
* Rendered graphics-layer groups, keyed on Graphic snapshot identity. See
|
|
1465
|
+
* {@link renderGraphicGroup} for the invalidation contract; swapped for a
|
|
1466
|
+
* fresh map (not cleared entry-by-entry) so stale entries are collectable.
|
|
1467
|
+
*/
|
|
1468
|
+
private graphicGroupCache;
|
|
1469
|
+
/**
|
|
1470
|
+
* Last {@link PointSymbolCapability.generation} observed from `pointSymbols`,
|
|
1471
|
+
* initialised from the capability at construction. Compared at the top of
|
|
1472
|
+
* every {@link reconcileGraphicsStack} so a host calling `invalidate()` after
|
|
1473
|
+
* changing renderer-global config (e.g. milsymbol options) drops the render
|
|
1474
|
+
* cache even though the Graphic snapshots handed to `render()` are
|
|
1475
|
+
* unchanged — otherwise the cache would keep serving pre-invalidation
|
|
1476
|
+
* output and the capability would never be re-consulted. `undefined` when
|
|
1477
|
+
* there is no capability, or the capability doesn't expose `generation`.
|
|
1478
|
+
*/
|
|
1479
|
+
private lastSeenPointSymbolGeneration;
|
|
1446
1480
|
/**
|
|
1447
1481
|
* Ids of graphics currently carrying a passive selection-highlight outline
|
|
1448
1482
|
* ({@link setHighlightedGraphics}). Declarative set-replace, no session —
|
|
@@ -1462,8 +1496,34 @@ declare class TacticalDraw {
|
|
|
1462
1496
|
setSnappingOptions(options?: SnappingOptions): void;
|
|
1463
1497
|
/** Clear transient host affordances when snapping or its interaction ends. */
|
|
1464
1498
|
private clearSnapResult;
|
|
1465
|
-
/**
|
|
1499
|
+
/**
|
|
1500
|
+
* Render one atomic Graphic group for the engine-neutral graphics stack,
|
|
1501
|
+
* reusing the previous render when nothing that feeds it has changed.
|
|
1502
|
+
*
|
|
1503
|
+
* The cache is keyed on the Graphic snapshot's object identity: hosts hand
|
|
1504
|
+
* `render()` a fresh object whenever a graphic's data changes, so a changed
|
|
1505
|
+
* graphic misses by construction. (A host that mutates a Graphic in place and
|
|
1506
|
+
* re-renders the same reference is outside the contract — `render()` has
|
|
1507
|
+
* always been documented as taking snapshots.) Rendered output also depends
|
|
1508
|
+
* on live view state and on the resolved graphics style; the style is fixed
|
|
1509
|
+
* at construction, and every view change drops the whole cache. Crucially,
|
|
1510
|
+
* the view cannot change during a box drag (drag-pan is disabled), so every
|
|
1511
|
+
* graphic outside the selection stays a cache hit for the whole gesture and
|
|
1512
|
+
* per-frame reconcile cost becomes O(selection) rather than O(document).
|
|
1513
|
+
*/
|
|
1466
1514
|
private renderGraphicGroup;
|
|
1515
|
+
/**
|
|
1516
|
+
* Render a Graphic group from scratch and stamp it with a fresh revision,
|
|
1517
|
+
* drawn from the module-scoped {@link nextGraphicGroupRevision} counter (see
|
|
1518
|
+
* its doc comment for why it isn't a per-instance field).
|
|
1519
|
+
*/
|
|
1520
|
+
private buildGraphicGroup;
|
|
1521
|
+
/**
|
|
1522
|
+
* Drop every cached render. Called whenever an input shared by all graphic
|
|
1523
|
+
* renders changes — the live view state, or (via {@link reconcileGraphicsStack})
|
|
1524
|
+
* the point-symbol capability's `generation`.
|
|
1525
|
+
*/
|
|
1526
|
+
private invalidateGraphicGroupCache;
|
|
1467
1527
|
/** Submit one complete grouped desired state for the host stack plus overrides. */
|
|
1468
1528
|
private reconcileGraphicsStack;
|
|
1469
1529
|
/** Atomically publish several live renders into their host-owned Graphic slots. */
|
package/dist/index.mjs
CHANGED
|
@@ -486,7 +486,8 @@ var BaseMapAdapter = class {
|
|
|
486
486
|
const nextGroups = groups.map((group) => ({
|
|
487
487
|
id: group.id,
|
|
488
488
|
features: [...group.features],
|
|
489
|
-
iconResources: [...group.iconResources]
|
|
489
|
+
iconResources: [...group.iconResources],
|
|
490
|
+
revision: group.revision
|
|
490
491
|
}));
|
|
491
492
|
const nextFeatures = nextGroups.flatMap((group) => group.features);
|
|
492
493
|
const nextResourcesById = /* @__PURE__ */ new Map();
|
|
@@ -2923,6 +2924,200 @@ function beginBoxDrag(args) {
|
|
|
2923
2924
|
};
|
|
2924
2925
|
}
|
|
2925
2926
|
//#endregion
|
|
2927
|
+
//#region src/tactical-draw/deep-equal.ts
|
|
2928
|
+
/**
|
|
2929
|
+
* @internal
|
|
2930
|
+
*
|
|
2931
|
+
* Structural equality for structured-cloneable values. Not exported from
|
|
2932
|
+
* `src/index.ts` — this is a private helper for comparing session-owned host
|
|
2933
|
+
* data (e.g. `ControlMeasure.properties`, `PointSymbol`) where `JSON.stringify`
|
|
2934
|
+
* is unsafe: it throws on `bigint` and cyclic graphs, and it silently
|
|
2935
|
+
* collapses values it cannot represent (`{x: undefined}` and `{}` both
|
|
2936
|
+
* stringify to `"{}"`; a `Map` stringifies to `"{}"` regardless of contents;
|
|
2937
|
+
* distinct `Date`s can coerce to the same string). See issue #354 review
|
|
2938
|
+
* follow-up.
|
|
2939
|
+
*
|
|
2940
|
+
* Design notes / deliberate limitations:
|
|
2941
|
+
* - Uses `Object.is` as the base-case comparator, so `NaN` equals `NaN` but
|
|
2942
|
+
* `0` and `-0` are treated as different values. This mirrors the semantics
|
|
2943
|
+
* most callers actually want for "did this change" checks (a NaN-valued
|
|
2944
|
+
* field shouldn't perpetually look dirty), and is cheap/well-defined; it
|
|
2945
|
+
* is a deliberate choice, not an oversight.
|
|
2946
|
+
* - Object comparison is key-set sensitive: `{}` and `{ x: undefined }` are
|
|
2947
|
+
* NOT equal, because they have different keys, even though the values at
|
|
2948
|
+
* key `x` compare loosely-equal-ish. This is required for `updateGraphic`
|
|
2949
|
+
* dedupe correctness — a caller that explicitly adds an `undefined`-valued
|
|
2950
|
+
* key is making a distinguishable structural edit.
|
|
2951
|
+
* - Only "own enumerable string keys" are compared for plain objects
|
|
2952
|
+
* (`Object.keys`), matching what `structuredClone` round-trips and what
|
|
2953
|
+
* hosts can observe with normal iteration. Symbol-keyed properties are not
|
|
2954
|
+
* considered (functions and symbols aren't structured-cloneable anyway).
|
|
2955
|
+
* - `Set` members and `Map` keys/values are matched with a proper one-to-one
|
|
2956
|
+
* (bipartite) matching, not a greedy or order-dependent search: primitive
|
|
2957
|
+
* members/keys take an O(1) `has`/`get` fast path, and any remaining
|
|
2958
|
+
* non-primitive members are matched with an augmenting-path search over
|
|
2959
|
+
* the candidate deep-equal pairs (Kuhn's algorithm), which is O(n*m) in
|
|
2960
|
+
* the number of non-primitive members/keys but correct in adversarial
|
|
2961
|
+
* cases with multiple structurally-equal-but-distinct candidates.
|
|
2962
|
+
* - Functions compare by reference (the `Object.is` fast path) and are
|
|
2963
|
+
* otherwise unequal — `structuredClone` rejects functions outright, so any
|
|
2964
|
+
* function-valued field here has already failed to round-trip through the
|
|
2965
|
+
* session's clone boundary.
|
|
2966
|
+
* - Cycle-safe: a stack of visited pairs is tracked so a self-referential
|
|
2967
|
+
* graph terminates (returns a comparison result) instead of overflowing.
|
|
2968
|
+
* The pair association is a bijection — an object already paired with a
|
|
2969
|
+
* *different* counterpart along this comparison path is rejected, not
|
|
2970
|
+
* just re-accepted because the exact pair recurred.
|
|
2971
|
+
* - `Error` (and subclasses, including `AggregateError`) are special-cased:
|
|
2972
|
+
* `name`, `message`, and `cause` are compared (`cause` is key-set
|
|
2973
|
+
* sensitive too — an absent `cause` differs from `cause: undefined`),
|
|
2974
|
+
* `AggregateError.errors` is compared as an array, plus any own
|
|
2975
|
+
* enumerable properties as usual. `stack` is deliberately IGNORED: it's
|
|
2976
|
+
* environment-dependent noise (file/line info), and two structurally
|
|
2977
|
+
* identical errors constructed at different call sites shouldn't read as
|
|
2978
|
+
* a host edit.
|
|
2979
|
+
*/
|
|
2980
|
+
function tag(value) {
|
|
2981
|
+
return Object.prototype.toString.call(value);
|
|
2982
|
+
}
|
|
2983
|
+
function isTypedArrayOrDataView(value) {
|
|
2984
|
+
return ArrayBuffer.isView(value);
|
|
2985
|
+
}
|
|
2986
|
+
function bytesOf(value) {
|
|
2987
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
2988
|
+
}
|
|
2989
|
+
function bytesEqual(a, b) {
|
|
2990
|
+
if (a.length !== b.length) return false;
|
|
2991
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
2992
|
+
return true;
|
|
2993
|
+
}
|
|
2994
|
+
function isPrimitive(value) {
|
|
2995
|
+
return typeof value !== "object" || value === null;
|
|
2996
|
+
}
|
|
2997
|
+
/**
|
|
2998
|
+
* Does a perfect one-to-one matching exist between the `leftCount` left-side
|
|
2999
|
+
* indices and `rightCount` right-side indices, given `canMatch(i, j)`?
|
|
3000
|
+
* Kuhn's augmenting-path algorithm — O(leftCount * rightCount * edges), fine
|
|
3001
|
+
* for the small collections (`Set`/`Map` members in a host properties bag)
|
|
3002
|
+
* this is used for.
|
|
3003
|
+
*/
|
|
3004
|
+
function hasPerfectMatching(leftCount, rightCount, canMatch) {
|
|
3005
|
+
if (leftCount !== rightCount) return false;
|
|
3006
|
+
const matchOfRight = new Array(rightCount).fill(-1);
|
|
3007
|
+
function tryAugment(i, visited) {
|
|
3008
|
+
for (let j = 0; j < rightCount; j++) {
|
|
3009
|
+
if (visited[j] || !canMatch(i, j)) continue;
|
|
3010
|
+
visited[j] = true;
|
|
3011
|
+
if (matchOfRight[j] === -1 || tryAugment(matchOfRight[j], visited)) {
|
|
3012
|
+
matchOfRight[j] = i;
|
|
3013
|
+
return true;
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
return false;
|
|
3017
|
+
}
|
|
3018
|
+
for (let i = 0; i < leftCount; i++) if (!tryAugment(i, new Array(rightCount).fill(false))) return false;
|
|
3019
|
+
return true;
|
|
3020
|
+
}
|
|
3021
|
+
/**
|
|
3022
|
+
* Deep structural equality for structured-cloneable values (plus `bigint`,
|
|
3023
|
+
* `symbol`, and functions, which compare by identity). See the module
|
|
3024
|
+
* doc-comment above for the exact rules and deliberate limitations.
|
|
3025
|
+
*/
|
|
3026
|
+
function deepEqual(a, b) {
|
|
3027
|
+
return deepEqualInner(a, b, []);
|
|
3028
|
+
}
|
|
3029
|
+
function deepEqualInner(a, b, visited) {
|
|
3030
|
+
if (Object.is(a, b)) return true;
|
|
3031
|
+
if (typeof a !== typeof b) return false;
|
|
3032
|
+
if (a === null || b === null) return false;
|
|
3033
|
+
if (typeof a !== "object") return false;
|
|
3034
|
+
const objA = a;
|
|
3035
|
+
const objB = b;
|
|
3036
|
+
if (Object.getPrototypeOf(objA) !== Object.getPrototypeOf(objB)) return false;
|
|
3037
|
+
if (tag(objA) !== tag(objB)) return false;
|
|
3038
|
+
for (const [seenA, seenB] of visited) {
|
|
3039
|
+
if (seenA === objA && seenB === objB) return true;
|
|
3040
|
+
if (seenA === objA || seenB === objB) return false;
|
|
3041
|
+
}
|
|
3042
|
+
visited.push([objA, objB]);
|
|
3043
|
+
try {
|
|
3044
|
+
if (objA instanceof Date && objB instanceof Date) return Object.is(objA.getTime(), objB.getTime());
|
|
3045
|
+
if (objA instanceof RegExp && objB instanceof RegExp) return objA.source === objB.source && objA.flags === objB.flags;
|
|
3046
|
+
if (objA instanceof Error && objB instanceof Error) {
|
|
3047
|
+
if (objA.name !== objB.name) return false;
|
|
3048
|
+
if (objA.message !== objB.message) return false;
|
|
3049
|
+
const hasCauseA = Object.prototype.hasOwnProperty.call(objA, "cause");
|
|
3050
|
+
if (hasCauseA !== Object.prototype.hasOwnProperty.call(objB, "cause")) return false;
|
|
3051
|
+
if (hasCauseA && !deepEqualInner(objA.cause, objB.cause, visited)) return false;
|
|
3052
|
+
if (objA instanceof AggregateError) {
|
|
3053
|
+
if (!deepEqualInner(objA.errors, objB.errors, visited)) return false;
|
|
3054
|
+
}
|
|
3055
|
+
const keysA = Object.keys(objA);
|
|
3056
|
+
const keysB = Object.keys(objB);
|
|
3057
|
+
if (keysA.length !== keysB.length) return false;
|
|
3058
|
+
for (const key of keysA) if (!Object.prototype.hasOwnProperty.call(objB, key)) return false;
|
|
3059
|
+
for (const key of keysA) {
|
|
3060
|
+
const valueA = objA[key];
|
|
3061
|
+
const valueB = objB[key];
|
|
3062
|
+
if (!deepEqualInner(valueA, valueB, visited)) return false;
|
|
3063
|
+
}
|
|
3064
|
+
return true;
|
|
3065
|
+
}
|
|
3066
|
+
if (isTypedArrayOrDataView(objA) && isTypedArrayOrDataView(objB)) return bytesEqual(bytesOf(objA), bytesOf(objB));
|
|
3067
|
+
if (objA instanceof ArrayBuffer && objB instanceof ArrayBuffer) return bytesEqual(new Uint8Array(objA), new Uint8Array(objB));
|
|
3068
|
+
if (objA instanceof Set && objB instanceof Set) {
|
|
3069
|
+
if (objA.size !== objB.size) return false;
|
|
3070
|
+
const remainingA = [];
|
|
3071
|
+
const remainingB = new Set(objB);
|
|
3072
|
+
for (const itemA of objA) {
|
|
3073
|
+
if (isPrimitive(itemA) && remainingB.has(itemA)) {
|
|
3074
|
+
remainingB.delete(itemA);
|
|
3075
|
+
continue;
|
|
3076
|
+
}
|
|
3077
|
+
remainingA.push(itemA);
|
|
3078
|
+
}
|
|
3079
|
+
const remainingBArr = [...remainingB];
|
|
3080
|
+
return hasPerfectMatching(remainingA.length, remainingBArr.length, (i, j) => deepEqualInner(remainingA[i], remainingBArr[j], visited));
|
|
3081
|
+
}
|
|
3082
|
+
if (objA instanceof Map && objB instanceof Map) {
|
|
3083
|
+
if (objA.size !== objB.size) return false;
|
|
3084
|
+
const remainingA = [];
|
|
3085
|
+
const remainingB = new Map(objB);
|
|
3086
|
+
for (const [keyA, valueA] of objA) {
|
|
3087
|
+
if (isPrimitive(keyA) && remainingB.has(keyA)) {
|
|
3088
|
+
if (!deepEqualInner(valueA, remainingB.get(keyA), visited)) return false;
|
|
3089
|
+
remainingB.delete(keyA);
|
|
3090
|
+
continue;
|
|
3091
|
+
}
|
|
3092
|
+
remainingA.push([keyA, valueA]);
|
|
3093
|
+
}
|
|
3094
|
+
const remainingBArr = [...remainingB];
|
|
3095
|
+
return hasPerfectMatching(remainingA.length, remainingBArr.length, (i, j) => {
|
|
3096
|
+
const [keyA, valueA] = remainingA[i];
|
|
3097
|
+
const [keyB, valueB] = remainingBArr[j];
|
|
3098
|
+
return deepEqualInner(keyA, keyB, visited) && deepEqualInner(valueA, valueB, visited);
|
|
3099
|
+
});
|
|
3100
|
+
}
|
|
3101
|
+
if (Array.isArray(objA) && Array.isArray(objB)) {
|
|
3102
|
+
if (objA.length !== objB.length) return false;
|
|
3103
|
+
for (let i = 0; i < objA.length; i++) if (!deepEqualInner(objA[i], objB[i], visited)) return false;
|
|
3104
|
+
return true;
|
|
3105
|
+
}
|
|
3106
|
+
const keysA = Object.keys(objA);
|
|
3107
|
+
const keysB = Object.keys(objB);
|
|
3108
|
+
if (keysA.length !== keysB.length) return false;
|
|
3109
|
+
for (const key of keysA) if (!Object.prototype.hasOwnProperty.call(objB, key)) return false;
|
|
3110
|
+
for (const key of keysA) {
|
|
3111
|
+
const valueA = objA[key];
|
|
3112
|
+
const valueB = objB[key];
|
|
3113
|
+
if (!deepEqualInner(valueA, valueB, visited)) return false;
|
|
3114
|
+
}
|
|
3115
|
+
return true;
|
|
3116
|
+
} finally {
|
|
3117
|
+
visited.pop();
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
//#endregion
|
|
2926
3121
|
//#region src/tactical-draw/session-history.ts
|
|
2927
3122
|
/** @internal Controller-owned history stack. */
|
|
2928
3123
|
function createSessionHistory(options) {
|
|
@@ -3611,9 +3806,28 @@ function runEdit(opts) {
|
|
|
3611
3806
|
let workingOptions = { ...measure.options ?? {} };
|
|
3612
3807
|
let workingTextAmplifiers = measure.textAmplifiers && Object.keys(measure.textAmplifiers).length > 0 ? { ...measure.textAmplifiers } : void 0;
|
|
3613
3808
|
let workingAmplifierPlacements = measure.amplifierPlacements && Object.keys(measure.amplifierPlacements).length > 0 ? structuredClone(measure.amplifierPlacements) : void 0;
|
|
3809
|
+
let workingProperties = measure.properties ? structuredClone(measure.properties) : void 0;
|
|
3614
3810
|
function snapshotAmplifierPlacements() {
|
|
3615
3811
|
return workingAmplifierPlacements ? structuredClone(workingAmplifierPlacements) : void 0;
|
|
3616
3812
|
}
|
|
3813
|
+
function snapshotProperties() {
|
|
3814
|
+
return workingProperties ? structuredClone(workingProperties) : void 0;
|
|
3815
|
+
}
|
|
3816
|
+
/**
|
|
3817
|
+
* Dedupe comparison for the library's own typed session data — `options`,
|
|
3818
|
+
* `style`, `textAmplifiers`, `amplifierPlacements`. Those are declared as
|
|
3819
|
+
* JSON-representable values, so serialising them is safe and cheaper than a
|
|
3820
|
+
* structural walk.
|
|
3821
|
+
*
|
|
3822
|
+
* Deliberately NOT for anything that can carry `properties`: that bag is
|
|
3823
|
+
* `Record<string, unknown>` cloned with `structuredClone`, so a host may put
|
|
3824
|
+
* a bigint or a cyclic graph in it (throws here) or a `Map`/`undefined`-valued
|
|
3825
|
+
* key in it (silently collapses to the same string). Those comparisons use
|
|
3826
|
+
* `deepEqual` — see `./deep-equal` and issue #354.
|
|
3827
|
+
*/
|
|
3828
|
+
function jsonEqual(a, b) {
|
|
3829
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
3830
|
+
}
|
|
3617
3831
|
let modes = requestedModes;
|
|
3618
3832
|
let dirty = false;
|
|
3619
3833
|
let nonGeometryDirty = false;
|
|
@@ -3684,6 +3898,7 @@ function runEdit(opts) {
|
|
|
3684
3898
|
style: currentStyle ? { ...currentStyle } : void 0,
|
|
3685
3899
|
textAmplifiers: workingTextAmplifiers ? { ...workingTextAmplifiers } : void 0,
|
|
3686
3900
|
amplifierPlacements: snapshotAmplifierPlacements(),
|
|
3901
|
+
properties: snapshotProperties(),
|
|
3687
3902
|
sizeAnchor,
|
|
3688
3903
|
nonGeometryDirty,
|
|
3689
3904
|
boxAngle
|
|
@@ -3695,6 +3910,7 @@ function runEdit(opts) {
|
|
|
3695
3910
|
currentStyle = value.style ? { ...value.style } : void 0;
|
|
3696
3911
|
workingTextAmplifiers = value.textAmplifiers ? { ...value.textAmplifiers } : void 0;
|
|
3697
3912
|
workingAmplifierPlacements = value.amplifierPlacements ? structuredClone(value.amplifierPlacements) : void 0;
|
|
3913
|
+
workingProperties = value.properties ? structuredClone(value.properties) : void 0;
|
|
3698
3914
|
sizeAnchor = value.sizeAnchor;
|
|
3699
3915
|
nonGeometryDirty = value.nonGeometryDirty;
|
|
3700
3916
|
boxAngle = value.boxAngle;
|
|
@@ -3781,6 +3997,8 @@ function runEdit(opts) {
|
|
|
3781
3997
|
else delete working.textAmplifiers;
|
|
3782
3998
|
if (workingAmplifierPlacements) working.amplifierPlacements = workingAmplifierPlacements;
|
|
3783
3999
|
else delete working.amplifierPlacements;
|
|
4000
|
+
if (workingProperties) working.properties = workingProperties;
|
|
4001
|
+
else delete working.properties;
|
|
3784
4002
|
let features;
|
|
3785
4003
|
try {
|
|
3786
4004
|
features = renderMeasureForAdapter(adapter, working, renderOpts).features;
|
|
@@ -3858,6 +4076,8 @@ function runEdit(opts) {
|
|
|
3858
4076
|
else delete working.textAmplifiers;
|
|
3859
4077
|
if (workingAmplifierPlacements) working.amplifierPlacements = workingAmplifierPlacements;
|
|
3860
4078
|
else delete working.amplifierPlacements;
|
|
4079
|
+
if (workingProperties) working.properties = workingProperties;
|
|
4080
|
+
else delete working.properties;
|
|
3861
4081
|
const committed = sizeAnchor === "screen" ? working : bakePixelSizeToGround(working, adapter);
|
|
3862
4082
|
return {
|
|
3863
4083
|
graphic: cloneControlMeasure(committed),
|
|
@@ -3903,7 +4123,7 @@ function runEdit(opts) {
|
|
|
3903
4123
|
* the history record. The deferred reject/close/abort machinery, exception
|
|
3904
4124
|
* collection, and record-before-rethrow ordering live in the emitter.
|
|
3905
4125
|
*/
|
|
3906
|
-
function emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, source = "gesture", historyLabel = "Edit", previousBoxAngle = boxAngle) {
|
|
4126
|
+
function emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, previousProperties, source = "gesture", historyLabel = "Edit", previousBoxAngle = boxAngle) {
|
|
3907
4127
|
changeEmitter.emit({
|
|
3908
4128
|
source,
|
|
3909
4129
|
graphic: currentRendered,
|
|
@@ -3917,6 +4137,7 @@ function runEdit(opts) {
|
|
|
3917
4137
|
sizeAnchor = previousSizeAnchor;
|
|
3918
4138
|
workingTextAmplifiers = previousTextAmplifiers;
|
|
3919
4139
|
workingAmplifierPlacements = previousAmplifierPlacements;
|
|
4140
|
+
workingProperties = previousProperties;
|
|
3920
4141
|
boxAngle = previousBoxAngle;
|
|
3921
4142
|
nonGeometryDirty = previousNonGeometryDirty;
|
|
3922
4143
|
renderWorkingGraphic();
|
|
@@ -3944,6 +4165,7 @@ function runEdit(opts) {
|
|
|
3944
4165
|
const previousNonGeometryDirty = nonGeometryDirty;
|
|
3945
4166
|
const previousTextAmplifiers = workingTextAmplifiers;
|
|
3946
4167
|
const previousAmplifierPlacements = snapshotAmplifierPlacements();
|
|
4168
|
+
const previousProperties = snapshotProperties();
|
|
3947
4169
|
const previousRendered = rebuildRenderedWith(previousOptions);
|
|
3948
4170
|
replaceWorkingPoints(nextPoints);
|
|
3949
4171
|
if (nextAmplifierPlacements !== void 0) workingAmplifierPlacements = structuredClone(nextAmplifierPlacements);
|
|
@@ -3963,7 +4185,7 @@ function runEdit(opts) {
|
|
|
3963
4185
|
recomputeDirty();
|
|
3964
4186
|
dragInFlight = false;
|
|
3965
4187
|
drainPendingModes();
|
|
3966
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, "gesture", "Edit", preGestureBoxAngle);
|
|
4188
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, previousProperties, "gesture", "Edit", preGestureBoxAngle);
|
|
3967
4189
|
}
|
|
3968
4190
|
function setOptions(partial) {
|
|
3969
4191
|
if (ctx.settled) return;
|
|
@@ -3973,12 +4195,13 @@ function runEdit(opts) {
|
|
|
3973
4195
|
const previousNonGeometryDirty = nonGeometryDirty;
|
|
3974
4196
|
const previousTextAmplifiers = workingTextAmplifiers;
|
|
3975
4197
|
const previousAmplifierPlacements = snapshotAmplifierPlacements();
|
|
4198
|
+
const previousProperties = snapshotProperties();
|
|
3976
4199
|
const previousRendered = rebuildRendered();
|
|
3977
4200
|
replaceWorkingOptions({
|
|
3978
4201
|
...workingOptions ?? {},
|
|
3979
4202
|
...partial ?? {}
|
|
3980
4203
|
});
|
|
3981
|
-
if (
|
|
4204
|
+
if (jsonEqual(workingOptions, previousOptions)) return;
|
|
3982
4205
|
let currentRendered;
|
|
3983
4206
|
try {
|
|
3984
4207
|
currentRendered = rebuildRendered();
|
|
@@ -3989,7 +4212,7 @@ function runEdit(opts) {
|
|
|
3989
4212
|
renderWorkingGraphicAndHandles();
|
|
3990
4213
|
nonGeometryDirty = true;
|
|
3991
4214
|
dirty = true;
|
|
3992
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, "programmatic", "Change options");
|
|
4215
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, previousProperties, "programmatic", "Change options");
|
|
3993
4216
|
}
|
|
3994
4217
|
function setStyle(partial) {
|
|
3995
4218
|
if (ctx.settled) return;
|
|
@@ -3999,12 +4222,13 @@ function runEdit(opts) {
|
|
|
3999
4222
|
const previousNonGeometryDirty = nonGeometryDirty;
|
|
4000
4223
|
const previousTextAmplifiers = workingTextAmplifiers;
|
|
4001
4224
|
const previousAmplifierPlacements = snapshotAmplifierPlacements();
|
|
4225
|
+
const previousProperties = snapshotProperties();
|
|
4002
4226
|
const previousRendered = rebuildRendered();
|
|
4003
4227
|
currentStyle = {
|
|
4004
4228
|
...currentStyle ?? {},
|
|
4005
4229
|
...partial
|
|
4006
4230
|
};
|
|
4007
|
-
if (
|
|
4231
|
+
if (jsonEqual(currentStyle, previousStyle)) return;
|
|
4008
4232
|
let currentRendered;
|
|
4009
4233
|
try {
|
|
4010
4234
|
currentRendered = rebuildRendered();
|
|
@@ -4015,7 +4239,7 @@ function runEdit(opts) {
|
|
|
4015
4239
|
renderWorkingGraphicAndHandles();
|
|
4016
4240
|
nonGeometryDirty = true;
|
|
4017
4241
|
dirty = true;
|
|
4018
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, "programmatic", "Change style");
|
|
4242
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, previousProperties, "programmatic", "Change style");
|
|
4019
4243
|
}
|
|
4020
4244
|
/**
|
|
4021
4245
|
* Shallow-merge into the live working text amplifiers. Mirrors
|
|
@@ -4040,6 +4264,7 @@ function runEdit(opts) {
|
|
|
4040
4264
|
const previousNonGeometryDirty = nonGeometryDirty;
|
|
4041
4265
|
const previousTextAmplifiers = workingTextAmplifiers;
|
|
4042
4266
|
const previousAmplifierPlacements = snapshotAmplifierPlacements();
|
|
4267
|
+
const previousProperties = snapshotProperties();
|
|
4043
4268
|
const previousRendered = rebuildRendered();
|
|
4044
4269
|
const merged = { ...workingTextAmplifiers ?? {} };
|
|
4045
4270
|
for (const [key, value] of Object.entries(partial)) {
|
|
@@ -4050,7 +4275,7 @@ function runEdit(opts) {
|
|
|
4050
4275
|
if (value !== void 0 && value.trim() !== "") merged[canonicalKey ?? key] = value;
|
|
4051
4276
|
}
|
|
4052
4277
|
workingTextAmplifiers = Object.keys(merged).length > 0 ? merged : void 0;
|
|
4053
|
-
if (
|
|
4278
|
+
if (jsonEqual(workingTextAmplifiers, previousTextAmplifiers)) return;
|
|
4054
4279
|
let currentRendered;
|
|
4055
4280
|
try {
|
|
4056
4281
|
currentRendered = rebuildRendered();
|
|
@@ -4061,7 +4286,7 @@ function runEdit(opts) {
|
|
|
4061
4286
|
renderWorkingGraphicAndHandles();
|
|
4062
4287
|
nonGeometryDirty = true;
|
|
4063
4288
|
dirty = true;
|
|
4064
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, "programmatic", "Change text");
|
|
4289
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, previousProperties, "programmatic", "Change text");
|
|
4065
4290
|
}
|
|
4066
4291
|
function setAmplifierPlacements(partial, source = "programmatic", historyLabel = "Move label") {
|
|
4067
4292
|
if (ctx.settled) return;
|
|
@@ -4071,6 +4296,7 @@ function runEdit(opts) {
|
|
|
4071
4296
|
const previousNonGeometryDirty = nonGeometryDirty;
|
|
4072
4297
|
const previousTextAmplifiers = workingTextAmplifiers;
|
|
4073
4298
|
const previousAmplifierPlacements = snapshotAmplifierPlacements();
|
|
4299
|
+
const previousProperties = snapshotProperties();
|
|
4074
4300
|
const previousRendered = rebuildRendered();
|
|
4075
4301
|
const next = { ...workingAmplifierPlacements ?? {} };
|
|
4076
4302
|
for (const [field, value] of Object.entries(partial)) {
|
|
@@ -4083,12 +4309,12 @@ function runEdit(opts) {
|
|
|
4083
4309
|
else next[field] = merged;
|
|
4084
4310
|
}
|
|
4085
4311
|
workingAmplifierPlacements = Object.keys(next).length > 0 ? next : void 0;
|
|
4086
|
-
if (
|
|
4312
|
+
if (jsonEqual(workingAmplifierPlacements, previousAmplifierPlacements)) return;
|
|
4087
4313
|
const currentRendered = rebuildRendered();
|
|
4088
4314
|
renderWorkingGraphicAndHandles();
|
|
4089
4315
|
nonGeometryDirty = true;
|
|
4090
4316
|
dirty = true;
|
|
4091
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, source, historyLabel);
|
|
4317
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, previousProperties, source, historyLabel);
|
|
4092
4318
|
}
|
|
4093
4319
|
/**
|
|
4094
4320
|
* Flip the size anchor for the measure under edit (ADR-0020). Re-renders the
|
|
@@ -4106,12 +4332,13 @@ function runEdit(opts) {
|
|
|
4106
4332
|
const previousSizeAnchor = sizeAnchor;
|
|
4107
4333
|
const previousTextAmplifiers = workingTextAmplifiers;
|
|
4108
4334
|
const previousAmplifierPlacements = snapshotAmplifierPlacements();
|
|
4335
|
+
const previousProperties = snapshotProperties();
|
|
4109
4336
|
const previousRendered = rebuildRendered();
|
|
4110
4337
|
sizeAnchor = next;
|
|
4111
4338
|
const currentRendered = rebuildRendered();
|
|
4112
4339
|
nonGeometryDirty = true;
|
|
4113
4340
|
dirty = true;
|
|
4114
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, "programmatic", "Change size anchor");
|
|
4341
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, previousProperties, "programmatic", "Change size anchor");
|
|
4115
4342
|
}
|
|
4116
4343
|
function modesEqual(a, b) {
|
|
4117
4344
|
if (a.length !== b.length) return false;
|
|
@@ -4138,34 +4365,38 @@ function runEdit(opts) {
|
|
|
4138
4365
|
style: value.style ? { ...value.style } : void 0,
|
|
4139
4366
|
textAmplifiers: value.textAmplifiers ? { ...value.textAmplifiers } : void 0,
|
|
4140
4367
|
amplifierPlacements: value.amplifierPlacements ? structuredClone(value.amplifierPlacements) : void 0,
|
|
4368
|
+
properties: value.properties ? structuredClone(value.properties) : void 0,
|
|
4141
4369
|
sizeAnchor: value.sizeAnchor,
|
|
4142
4370
|
nonGeometryDirty: value.nonGeometryDirty,
|
|
4143
4371
|
boxAngle: value.boxAngle
|
|
4144
4372
|
}),
|
|
4145
|
-
equals: (a, b) =>
|
|
4146
|
-
isCurrent: (value) =>
|
|
4373
|
+
equals: (a, b) => deepEqual(a, b),
|
|
4374
|
+
isCurrent: (value) => deepEqual(captureHistory(), value),
|
|
4147
4375
|
apply(from, to, source) {
|
|
4148
4376
|
const previousRendered = rebuildRendered();
|
|
4149
4377
|
restoreHistory(to);
|
|
4150
4378
|
renderWorkingGraphic();
|
|
4151
4379
|
renderHandlesLayer();
|
|
4152
4380
|
recomputeDirty();
|
|
4153
|
-
emitChange(previousRendered, from.points, from.options, from.style, from.nonGeometryDirty, from.sizeAnchor, rebuildRendered(), from.textAmplifiers, from.amplifierPlacements, source, "Edit", from.boxAngle);
|
|
4154
|
-
return !ctx.settled &&
|
|
4381
|
+
emitChange(previousRendered, from.points, from.options, from.style, from.nonGeometryDirty, from.sizeAnchor, rebuildRendered(), from.textAmplifiers, from.amplifierPlacements, from.properties, source, "Edit", from.boxAngle);
|
|
4382
|
+
return !ctx.settled && deepEqual(captureHistory(), to);
|
|
4155
4383
|
}
|
|
4156
4384
|
});
|
|
4157
4385
|
const session = {
|
|
4158
4386
|
history,
|
|
4159
4387
|
graphic: measure,
|
|
4160
4388
|
get workingGraphic() {
|
|
4161
|
-
|
|
4389
|
+
const working = {
|
|
4162
4390
|
...measure,
|
|
4163
4391
|
controlPoints: workingPoints.map((point) => [...point]),
|
|
4164
4392
|
options: { ...workingOptions ?? {} },
|
|
4165
4393
|
style: currentStyle ? { ...currentStyle } : void 0,
|
|
4166
4394
|
textAmplifiers: workingTextAmplifiers ? { ...workingTextAmplifiers } : void 0,
|
|
4167
4395
|
amplifierPlacements: snapshotAmplifierPlacements()
|
|
4168
|
-
}
|
|
4396
|
+
};
|
|
4397
|
+
if (workingProperties) working.properties = workingProperties;
|
|
4398
|
+
else delete working.properties;
|
|
4399
|
+
return cloneControlMeasure(working);
|
|
4169
4400
|
},
|
|
4170
4401
|
get controlPoints() {
|
|
4171
4402
|
return workingPoints.map((p) => [...p]);
|
|
@@ -4212,21 +4443,23 @@ function runEdit(opts) {
|
|
|
4212
4443
|
const previous = rebuildRendered();
|
|
4213
4444
|
const previousNonGeometryDirty = nonGeometryDirty;
|
|
4214
4445
|
const previousSizeAnchor = sizeAnchor;
|
|
4446
|
+
const previousProperties = snapshotProperties();
|
|
4215
4447
|
const current = session.workingGraphic;
|
|
4216
4448
|
const next = updater(current);
|
|
4217
4449
|
if (next.id !== measure.id || next.kind !== measure.kind) throw new TypeError("updateGraphic must preserve control-measure id and kind");
|
|
4218
|
-
if (
|
|
4450
|
+
if (deepEqual(next, current)) return;
|
|
4219
4451
|
renderMeasureForAdapter(adapter, next, renderOpts);
|
|
4220
4452
|
workingPoints.splice(0, workingPoints.length, ...next.controlPoints.map((point) => [...point]));
|
|
4221
4453
|
workingOptions = { ...next.options ?? {} };
|
|
4222
4454
|
currentStyle = next.style ? { ...next.style } : void 0;
|
|
4223
4455
|
workingTextAmplifiers = next.textAmplifiers ? { ...next.textAmplifiers } : void 0;
|
|
4224
4456
|
workingAmplifierPlacements = next.amplifierPlacements ? structuredClone(next.amplifierPlacements) : void 0;
|
|
4457
|
+
workingProperties = next.properties ? structuredClone(next.properties) : void 0;
|
|
4225
4458
|
nonGeometryDirty = true;
|
|
4226
4459
|
renderWorkingGraphic();
|
|
4227
4460
|
renderHandlesLayer();
|
|
4228
4461
|
recomputeDirty();
|
|
4229
|
-
emitChange(previous, previous.graphic.controlPoints.map((point) => [...point]), previous.graphic.options, previous.graphic.style, previousNonGeometryDirty, previousSizeAnchor, rebuildRendered(), previous.graphic.textAmplifiers, previous.graphic.amplifierPlacements, "programmatic", "Update graphic");
|
|
4462
|
+
emitChange(previous, previous.graphic.controlPoints.map((point) => [...point]), previous.graphic.options, previous.graphic.style, previousNonGeometryDirty, previousSizeAnchor, rebuildRendered(), previous.graphic.textAmplifiers, previous.graphic.amplifierPlacements, previousProperties, "programmatic", "Update graphic");
|
|
4230
4463
|
},
|
|
4231
4464
|
setOptions(partial) {
|
|
4232
4465
|
setOptions(partial);
|
|
@@ -4768,6 +5001,42 @@ function boundProperty(key, value) {
|
|
|
4768
5001
|
return value !== void 0 && Number.isFinite(value) && value > 0 ? { [key]: value } : {};
|
|
4769
5002
|
}
|
|
4770
5003
|
//#endregion
|
|
5004
|
+
//#region src/tactical-draw/frame-coalescer.ts
|
|
5005
|
+
/**
|
|
5006
|
+
* @param run Callback invoked with the latest scheduled args, at most once per frame.
|
|
5007
|
+
*/
|
|
5008
|
+
function createFrameCoalescer(run) {
|
|
5009
|
+
const raf = globalThis.requestAnimationFrame?.bind(globalThis);
|
|
5010
|
+
const cancelRaf = globalThis.cancelAnimationFrame?.bind(globalThis);
|
|
5011
|
+
if (!raf || !cancelRaf) return {
|
|
5012
|
+
schedule(...args) {
|
|
5013
|
+
run(...args);
|
|
5014
|
+
},
|
|
5015
|
+
cancel() {}
|
|
5016
|
+
};
|
|
5017
|
+
let handle = null;
|
|
5018
|
+
let latestArgs = null;
|
|
5019
|
+
return {
|
|
5020
|
+
schedule(...args) {
|
|
5021
|
+
latestArgs = args;
|
|
5022
|
+
if (handle !== null) return;
|
|
5023
|
+
handle = raf(() => {
|
|
5024
|
+
handle = null;
|
|
5025
|
+
const pending = latestArgs;
|
|
5026
|
+
latestArgs = null;
|
|
5027
|
+
if (pending) run(...pending);
|
|
5028
|
+
});
|
|
5029
|
+
},
|
|
5030
|
+
cancel() {
|
|
5031
|
+
if (handle !== null) {
|
|
5032
|
+
cancelRaf(handle);
|
|
5033
|
+
handle = null;
|
|
5034
|
+
}
|
|
5035
|
+
latestArgs = null;
|
|
5036
|
+
}
|
|
5037
|
+
};
|
|
5038
|
+
}
|
|
5039
|
+
//#endregion
|
|
4771
5040
|
//#region src/tactical-draw/transform-controller.ts
|
|
4772
5041
|
/** No point symbol may scale below this factor of its own size — mirrors the box-drag floor. */
|
|
4773
5042
|
const MIN_POINT_SYMBOL_SIZE = .1;
|
|
@@ -5479,12 +5748,14 @@ function runGroupTransform(opts) {
|
|
|
5479
5748
|
renderHandlesLayer();
|
|
5480
5749
|
recomputeDirty();
|
|
5481
5750
|
}
|
|
5751
|
+
const boxDragPaintCoalescer = createFrameCoalescer(paintBoxDragSnapshot);
|
|
5482
5752
|
const onDriverMove = (pixel, info) => {
|
|
5483
5753
|
if (ctx.settled) return;
|
|
5484
|
-
if (activeBoxDrag)
|
|
5754
|
+
if (activeBoxDrag) boxDragPaintCoalescer.schedule(pixel, info?.shiftKey);
|
|
5485
5755
|
};
|
|
5486
5756
|
const onDriverUp = (pixel, info) => {
|
|
5487
5757
|
if (!activeBoxDrag) return;
|
|
5758
|
+
boxDragPaintCoalescer.cancel();
|
|
5488
5759
|
if (ctx.settled) {
|
|
5489
5760
|
activeBoxDrag = null;
|
|
5490
5761
|
dragInFlight = false;
|
|
@@ -5545,6 +5816,7 @@ function runGroupTransform(opts) {
|
|
|
5545
5816
|
if (settlement.status === "rejected") settleEmitter.emit(settleReasonFromError(settlement.error));
|
|
5546
5817
|
},
|
|
5547
5818
|
extraTeardown: () => {
|
|
5819
|
+
boxDragPaintCoalescer.cancel();
|
|
5548
5820
|
if (activeBoxDrag !== null) {
|
|
5549
5821
|
driver.setDragPanEnabled(true);
|
|
5550
5822
|
activeBoxDrag = null;
|
|
@@ -5716,7 +5988,7 @@ function runPointSymbolEdit(opts) {
|
|
|
5716
5988
|
}, {
|
|
5717
5989
|
rollback() {
|
|
5718
5990
|
working = clonePointSymbol(previousState.working);
|
|
5719
|
-
dirty =
|
|
5991
|
+
dirty = !deepEqual(working, symbol);
|
|
5720
5992
|
render();
|
|
5721
5993
|
},
|
|
5722
5994
|
onAccepted
|
|
@@ -5725,11 +5997,11 @@ function runPointSymbolEdit(opts) {
|
|
|
5725
5997
|
const history = createSessionHistory({
|
|
5726
5998
|
initial: captureHistory(),
|
|
5727
5999
|
clone: cloneHistory,
|
|
5728
|
-
equals: (a, b) =>
|
|
5729
|
-
isCurrent: (value) =>
|
|
6000
|
+
equals: (a, b) => deepEqual(a.working, b.working),
|
|
6001
|
+
isCurrent: (value) => deepEqual(working, value.working),
|
|
5730
6002
|
apply(from, to, source) {
|
|
5731
6003
|
working = clonePointSymbol(to.working);
|
|
5732
|
-
dirty =
|
|
6004
|
+
dirty = !deepEqual(working, symbol);
|
|
5733
6005
|
render();
|
|
5734
6006
|
return emitChange(from, source);
|
|
5735
6007
|
}
|
|
@@ -5737,7 +6009,7 @@ function runPointSymbolEdit(opts) {
|
|
|
5737
6009
|
const apply = (next, source = "programmatic", label = "Update graphic") => {
|
|
5738
6010
|
const previousState = captureHistory();
|
|
5739
6011
|
working = clonePointSymbol(next);
|
|
5740
|
-
dirty =
|
|
6012
|
+
dirty = !deepEqual(working, symbol);
|
|
5741
6013
|
render();
|
|
5742
6014
|
emitChange(previousState, source, () => history.record(captureHistory(), label));
|
|
5743
6015
|
};
|
|
@@ -5930,6 +6202,17 @@ var GraphicStackCoordinator = class {
|
|
|
5930
6202
|
//#endregion
|
|
5931
6203
|
//#region src/tactical-draw/tactical-draw.ts
|
|
5932
6204
|
/**
|
|
6205
|
+
* Source of {@link FeatureGroup.revision} stamps. Module-scoped rather than a
|
|
6206
|
+
* per-instance counter: adapters keep per-group bundles on host-supplied layer
|
|
6207
|
+
* handles, and `destroy()` intentionally leaves those layers untouched so a
|
|
6208
|
+
* host can attach a fresh façade to the same adapter/layer for reuse. An
|
|
6209
|
+
* instance counter would restart at 1 on the new façade, so a bundle the
|
|
6210
|
+
* previous façade stored at revision 1 — with different content — would
|
|
6211
|
+
* compare equal and the adapter would wrongly skip the repaint. A counter
|
|
6212
|
+
* shared across every façade in the JS realm guarantees revisions never repeat.
|
|
6213
|
+
*/
|
|
6214
|
+
let nextGraphicGroupRevision = 0;
|
|
6215
|
+
/**
|
|
5933
6216
|
* `TacticalDraw` façade — slice #6 (issue #45).
|
|
5934
6217
|
*
|
|
5935
6218
|
* Provides the constructor + lifecycle + `render(measures)` reconcile path.
|
|
@@ -5998,6 +6281,23 @@ var TacticalDraw = class {
|
|
|
5998
6281
|
lastRenderedMeasures = [];
|
|
5999
6282
|
snapGeometryFeatures = [];
|
|
6000
6283
|
/**
|
|
6284
|
+
* Rendered graphics-layer groups, keyed on Graphic snapshot identity. See
|
|
6285
|
+
* {@link renderGraphicGroup} for the invalidation contract; swapped for a
|
|
6286
|
+
* fresh map (not cleared entry-by-entry) so stale entries are collectable.
|
|
6287
|
+
*/
|
|
6288
|
+
graphicGroupCache = /* @__PURE__ */ new WeakMap();
|
|
6289
|
+
/**
|
|
6290
|
+
* Last {@link PointSymbolCapability.generation} observed from `pointSymbols`,
|
|
6291
|
+
* initialised from the capability at construction. Compared at the top of
|
|
6292
|
+
* every {@link reconcileGraphicsStack} so a host calling `invalidate()` after
|
|
6293
|
+
* changing renderer-global config (e.g. milsymbol options) drops the render
|
|
6294
|
+
* cache even though the Graphic snapshots handed to `render()` are
|
|
6295
|
+
* unchanged — otherwise the cache would keep serving pre-invalidation
|
|
6296
|
+
* output and the capability would never be re-consulted. `undefined` when
|
|
6297
|
+
* there is no capability, or the capability doesn't expose `generation`.
|
|
6298
|
+
*/
|
|
6299
|
+
lastSeenPointSymbolGeneration;
|
|
6300
|
+
/**
|
|
6001
6301
|
* Ids of graphics currently carrying a passive selection-highlight outline
|
|
6002
6302
|
* ({@link setHighlightedGraphics}). Declarative set-replace, no session —
|
|
6003
6303
|
* re-projected on every view change and re-evaluated at the end of every
|
|
@@ -6017,6 +6317,7 @@ var TacticalDraw = class {
|
|
|
6017
6317
|
this.defaultInteractionStyle = options?.interactionStyle;
|
|
6018
6318
|
this.generateId = options?.generateId ?? (() => `td-${++this.idCounter}`);
|
|
6019
6319
|
this.pointSymbols = options?.pointSymbols;
|
|
6320
|
+
this.lastSeenPointSymbolGeneration = this.pointSymbols?.generation;
|
|
6020
6321
|
this.snappingOptions = options?.snapping;
|
|
6021
6322
|
this.resolveSnap = createSnapResolver(adapter, () => this.lastRenderedMeasures, () => this.snapGeometryFeatures, () => this.snappingOptions);
|
|
6022
6323
|
const owned = /* @__PURE__ */ new Set();
|
|
@@ -6044,27 +6345,68 @@ var TacticalDraw = class {
|
|
|
6044
6345
|
clearSnapResult() {
|
|
6045
6346
|
this.snappingOptions?.onSnap?.(null);
|
|
6046
6347
|
}
|
|
6047
|
-
/**
|
|
6348
|
+
/**
|
|
6349
|
+
* Render one atomic Graphic group for the engine-neutral graphics stack,
|
|
6350
|
+
* reusing the previous render when nothing that feeds it has changed.
|
|
6351
|
+
*
|
|
6352
|
+
* The cache is keyed on the Graphic snapshot's object identity: hosts hand
|
|
6353
|
+
* `render()` a fresh object whenever a graphic's data changes, so a changed
|
|
6354
|
+
* graphic misses by construction. (A host that mutates a Graphic in place and
|
|
6355
|
+
* re-renders the same reference is outside the contract — `render()` has
|
|
6356
|
+
* always been documented as taking snapshots.) Rendered output also depends
|
|
6357
|
+
* on live view state and on the resolved graphics style; the style is fixed
|
|
6358
|
+
* at construction, and every view change drops the whole cache. Crucially,
|
|
6359
|
+
* the view cannot change during a box drag (drag-pan is disabled), so every
|
|
6360
|
+
* graphic outside the selection stays a cache hit for the whole gesture and
|
|
6361
|
+
* per-frame reconcile cost becomes O(selection) rather than O(document).
|
|
6362
|
+
*/
|
|
6048
6363
|
renderGraphicGroup(graphic) {
|
|
6049
|
-
const
|
|
6364
|
+
const cached = this.graphicGroupCache.get(graphic);
|
|
6365
|
+
if (cached) return cached;
|
|
6366
|
+
const group = this.buildGraphicGroup(graphic);
|
|
6367
|
+
this.graphicGroupCache.set(graphic, group);
|
|
6368
|
+
return group;
|
|
6369
|
+
}
|
|
6370
|
+
/**
|
|
6371
|
+
* Render a Graphic group from scratch and stamp it with a fresh revision,
|
|
6372
|
+
* drawn from the module-scoped {@link nextGraphicGroupRevision} counter (see
|
|
6373
|
+
* its doc comment for why it isn't a per-instance field).
|
|
6374
|
+
*/
|
|
6375
|
+
buildGraphicGroup(graphic) {
|
|
6376
|
+
const revision = ++nextGraphicGroupRevision;
|
|
6050
6377
|
if (isPointSymbol(graphic)) {
|
|
6051
6378
|
if (!this.pointSymbols) throw new PointSymbolCapabilityRequiredError();
|
|
6052
6379
|
const plan = renderPointSymbolForAdapter(this.adapter, this.pointSymbols, graphic);
|
|
6053
6380
|
return {
|
|
6054
6381
|
id: graphic.id,
|
|
6055
6382
|
features: [plan.feature],
|
|
6056
|
-
iconResources: [plan.resource]
|
|
6383
|
+
iconResources: [plan.resource],
|
|
6384
|
+
revision
|
|
6057
6385
|
};
|
|
6058
6386
|
}
|
|
6059
|
-
const render = renderMeasureForAdapter(this.adapter, graphic,
|
|
6387
|
+
const render = renderMeasureForAdapter(this.adapter, graphic, { graphicsStyle: this.graphicsStyle });
|
|
6060
6388
|
return {
|
|
6061
6389
|
id: graphic.id,
|
|
6062
6390
|
features: render.features,
|
|
6063
|
-
iconResources: []
|
|
6391
|
+
iconResources: [],
|
|
6392
|
+
revision
|
|
6064
6393
|
};
|
|
6065
6394
|
}
|
|
6395
|
+
/**
|
|
6396
|
+
* Drop every cached render. Called whenever an input shared by all graphic
|
|
6397
|
+
* renders changes — the live view state, or (via {@link reconcileGraphicsStack})
|
|
6398
|
+
* the point-symbol capability's `generation`.
|
|
6399
|
+
*/
|
|
6400
|
+
invalidateGraphicGroupCache() {
|
|
6401
|
+
this.graphicGroupCache = /* @__PURE__ */ new WeakMap();
|
|
6402
|
+
}
|
|
6066
6403
|
/** Submit one complete grouped desired state for the host stack plus overrides. */
|
|
6067
6404
|
reconcileGraphicsStack(graphics = this.lastRenderedMeasures) {
|
|
6405
|
+
const currentGeneration = this.pointSymbols?.generation;
|
|
6406
|
+
if (currentGeneration !== this.lastSeenPointSymbolGeneration) {
|
|
6407
|
+
this.invalidateGraphicGroupCache();
|
|
6408
|
+
this.lastSeenPointSymbolGeneration = currentGeneration;
|
|
6409
|
+
}
|
|
6068
6410
|
const groups = this.graphicStack.desiredGroups(graphics, (graphic) => this.renderGraphicGroup(graphic));
|
|
6069
6411
|
this.adapter.setLayerFeatureGroups(this.graphicsLayer, groups);
|
|
6070
6412
|
this.snapGeometryFeatures = groups.flatMap((group) => group.features);
|
|
@@ -6093,6 +6435,7 @@ var TacticalDraw = class {
|
|
|
6093
6435
|
*/
|
|
6094
6436
|
handleViewChange = () => {
|
|
6095
6437
|
if (this.destroyed || !this.hasRendered) return;
|
|
6438
|
+
this.invalidateGraphicGroupCache();
|
|
6096
6439
|
this.renderHighlights();
|
|
6097
6440
|
const excluded = this.graphicsStackExcludedIds();
|
|
6098
6441
|
const visible = excluded.size > 0 ? this.lastRenderedMeasures.filter((m) => !excluded.has(m.id)) : this.lastRenderedMeasures;
|
|
@@ -7055,15 +7398,25 @@ function resolveSlot(adapter, supplied, owned) {
|
|
|
7055
7398
|
* expressions without re-spelling the pattern vocabulary.
|
|
7056
7399
|
*/
|
|
7057
7400
|
const HATCH_PATTERNS = [
|
|
7058
|
-
"
|
|
7059
|
-
"
|
|
7060
|
-
"cross"
|
|
7401
|
+
"hatch",
|
|
7402
|
+
"reverse-hatch",
|
|
7403
|
+
"cross-hatch",
|
|
7404
|
+
"horizontal",
|
|
7405
|
+
"vertical",
|
|
7406
|
+
"dots"
|
|
7061
7407
|
];
|
|
7062
7408
|
/** Tile edge length, CSS px, before `pixelRatio` scaling. Kept a power of two. */
|
|
7063
7409
|
const BASE_TILE_SIZE = 32;
|
|
7064
7410
|
/** Line spacing as a fraction of the tile edge — 4 repeats per tile. */
|
|
7065
7411
|
const SPACING_DIVISOR = 4;
|
|
7066
7412
|
/**
|
|
7413
|
+
* Dot radius, CSS px, before `pixelRatio` scaling — the `"dots"` counterpart to
|
|
7414
|
+
* the 1px line width, sized so a dot reads at the same weight as a hatch line.
|
|
7415
|
+
* Must stay in lockstep with the vector tiles in the Leaflet adapter and the
|
|
7416
|
+
* static previews.
|
|
7417
|
+
*/
|
|
7418
|
+
const DOT_RADIUS = 1.25;
|
|
7419
|
+
/**
|
|
7067
7420
|
* Stable id for a rasterized tile, keyed by pattern and color. Adapters use
|
|
7068
7421
|
* this as the image-registry key (MapLibre `addImage` id, OpenLayers pattern
|
|
7069
7422
|
* cache key) so the same color+pattern never gets rasterized twice. Color is
|
|
@@ -7143,8 +7496,8 @@ const SQRT2 = Math.SQRT2;
|
|
|
7143
7496
|
/**
|
|
7144
7497
|
* Coverage in `[0, 1]` of one diagonal line family (slope +1 or -1, both
|
|
7145
7498
|
* period `spacing`) at pixel center `(x, y)`, antialiased over a ~1 device-px
|
|
7146
|
-
* band. `axis` selects `y - x` (the "/"
|
|
7147
|
-
* "
|
|
7499
|
+
* band. `axis` selects `y - x` (the "/" hatch) or `y + x` (the "\" mirror,
|
|
7500
|
+
* "reverse-hatch", which "cross-hatch" unions on top of the hatch). Because `spacing`
|
|
7148
7501
|
* divides the tile dimension evenly, shifting
|
|
7149
7502
|
* `x` or `y` by a whole tile is a whole number of `spacing` periods, so the
|
|
7150
7503
|
* mod-distance — and therefore the coverage — is identical at the tile edges
|
|
@@ -7152,30 +7505,61 @@ const SQRT2 = Math.SQRT2;
|
|
|
7152
7505
|
* seamless.
|
|
7153
7506
|
*/
|
|
7154
7507
|
function lineCoverage(x, y, spacing, lineWidth, axis) {
|
|
7155
|
-
const t = axis === "/" ? y - x : y + x;
|
|
7156
|
-
const
|
|
7508
|
+
const t = axis === "/" ? y - x : axis === "\\" ? y + x : (axis === "-" ? y : x) - spacing / 2;
|
|
7509
|
+
const raw = Math.abs(centeredMod(t, spacing));
|
|
7510
|
+
const perpDist = axis === "/" || axis === "\\" ? raw / SQRT2 : raw;
|
|
7157
7511
|
const halfWidth = lineWidth / 2;
|
|
7158
7512
|
return Math.min(1, Math.max(0, halfWidth + .5 - perpDist));
|
|
7159
7513
|
}
|
|
7160
7514
|
/**
|
|
7515
|
+
* Coverage in `[0, 1]` of a square lattice of dots — one per `spacing × spacing`
|
|
7516
|
+
* cell, centered in the cell so the tile stays seamless — antialiased over the
|
|
7517
|
+
* same ~1px band as {@link lineCoverage}.
|
|
7518
|
+
*/
|
|
7519
|
+
function dotCoverage(x, y, spacing, radius) {
|
|
7520
|
+
const dx = centeredMod(x - spacing / 2, spacing);
|
|
7521
|
+
const dy = centeredMod(y - spacing / 2, spacing);
|
|
7522
|
+
return Math.min(1, Math.max(0, radius + .5 - Math.hypot(dx, dy)));
|
|
7523
|
+
}
|
|
7524
|
+
/**
|
|
7161
7525
|
* Rasterize one seamless hatch tile as a straight RGBA pixel buffer — no
|
|
7162
7526
|
* canvas involved, so this runs identically in a browser adapter and in
|
|
7163
7527
|
* jsdom/node tests. Diagonal lines are snapped to slope ±1 (45°) so the
|
|
7164
|
-
* mod-based coverage math in {@link lineCoverage} tiles exactly. `"
|
|
7165
|
-
* draws the "/" diagonal, `"
|
|
7166
|
-
* both via `Math.max` coverage.
|
|
7528
|
+
* mod-based coverage math in {@link lineCoverage} tiles exactly. `"hatch"`
|
|
7529
|
+
* draws the "/" diagonal, `"reverse-hatch"` its "\" mirror, and `"cross-hatch"` unions
|
|
7530
|
+
* both via `Math.max` coverage. `"horizontal"`/`"vertical"` draw the
|
|
7531
|
+
* axis-aligned families at the same spacing, and `"dots"` a square lattice of
|
|
7532
|
+
* dots (see {@link dotCoverage}).
|
|
7167
7533
|
*/
|
|
7168
7534
|
function makeFillPatternImage(color, pattern, pixelRatio = 1) {
|
|
7169
7535
|
const size = fillPatternImageDimension(pixelRatio);
|
|
7170
7536
|
const spacing = size / SPACING_DIVISOR;
|
|
7171
7537
|
const lineWidth = Math.max(1, Math.round(pixelRatio));
|
|
7538
|
+
const dotRadius = DOT_RADIUS * Math.max(1, Math.round(pixelRatio));
|
|
7172
7539
|
const { r, g, b, a } = parseColor(color);
|
|
7173
7540
|
const data = new Uint8ClampedArray(size * size * 4);
|
|
7174
7541
|
for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) {
|
|
7175
7542
|
const cx = x + .5;
|
|
7176
7543
|
const cy = y + .5;
|
|
7177
|
-
let coverage
|
|
7178
|
-
|
|
7544
|
+
let coverage;
|
|
7545
|
+
switch (pattern) {
|
|
7546
|
+
case "reverse-hatch":
|
|
7547
|
+
coverage = lineCoverage(cx, cy, spacing, lineWidth, "\\");
|
|
7548
|
+
break;
|
|
7549
|
+
case "cross-hatch":
|
|
7550
|
+
coverage = Math.max(lineCoverage(cx, cy, spacing, lineWidth, "/"), lineCoverage(cx, cy, spacing, lineWidth, "\\"));
|
|
7551
|
+
break;
|
|
7552
|
+
case "horizontal":
|
|
7553
|
+
coverage = lineCoverage(cx, cy, spacing, lineWidth, "-");
|
|
7554
|
+
break;
|
|
7555
|
+
case "vertical":
|
|
7556
|
+
coverage = lineCoverage(cx, cy, spacing, lineWidth, "|");
|
|
7557
|
+
break;
|
|
7558
|
+
case "dots":
|
|
7559
|
+
coverage = dotCoverage(cx, cy, spacing, dotRadius);
|
|
7560
|
+
break;
|
|
7561
|
+
default: coverage = lineCoverage(cx, cy, spacing, lineWidth, "/");
|
|
7562
|
+
}
|
|
7179
7563
|
const i = (y * size + x) * 4;
|
|
7180
7564
|
data[i] = r;
|
|
7181
7565
|
data[i + 1] = g;
|
|
@@ -45,7 +45,7 @@ interface EditPointerDriver {
|
|
|
45
45
|
* Shared hatch fill-pattern tiles for area control-measure fills (ADR-0035
|
|
46
46
|
* `FillPattern`, excluding `"solid"` which every adapter already renders as a
|
|
47
47
|
* flat color). Both the MapLibre adapter (raster `addImage` pattern) and the
|
|
48
|
-
* OpenLayers adapter (canvas `Pattern` fill) need the *same* diagonal-
|
|
48
|
+
* OpenLayers adapter (canvas `Pattern` fill) need the *same* diagonal-hatch
|
|
49
49
|
* texture, so it is rasterized once here in pure math — no `HTMLCanvasElement`
|
|
50
50
|
* — so it works in adapter code and in jsdom/node tests alike.
|
|
51
51
|
*/
|
|
@@ -76,9 +76,11 @@ declare function fillPatternImageDimension(pixelRatio: number): number;
|
|
|
76
76
|
* Rasterize one seamless hatch tile as a straight RGBA pixel buffer — no
|
|
77
77
|
* canvas involved, so this runs identically in a browser adapter and in
|
|
78
78
|
* jsdom/node tests. Diagonal lines are snapped to slope ±1 (45°) so the
|
|
79
|
-
* mod-based coverage math in {@link lineCoverage} tiles exactly. `"
|
|
80
|
-
* draws the "/" diagonal, `"
|
|
81
|
-
* both via `Math.max` coverage.
|
|
79
|
+
* mod-based coverage math in {@link lineCoverage} tiles exactly. `"hatch"`
|
|
80
|
+
* draws the "/" diagonal, `"reverse-hatch"` its "\" mirror, and `"cross-hatch"` unions
|
|
81
|
+
* both via `Math.max` coverage. `"horizontal"`/`"vertical"` draw the
|
|
82
|
+
* axis-aligned families at the same spacing, and `"dots"` a square lattice of
|
|
83
|
+
* dots (see {@link dotCoverage}).
|
|
82
84
|
*/
|
|
83
85
|
declare function makeFillPatternImage(color: string, pattern: HatchPattern, pixelRatio?: number): {
|
|
84
86
|
width: number;
|
|
@@ -150,7 +152,7 @@ interface HandleStyle {
|
|
|
150
152
|
fillColor?: string;
|
|
151
153
|
/**
|
|
152
154
|
* How `fillColor` paints a filled part's interior. Absent or `"solid"` is
|
|
153
|
-
* today's flat fill; `"
|
|
155
|
+
* today's flat fill; `"hatch"`/`"reverse-hatch"`/`"cross-hatch"`/`"horizontal"`/`"vertical"`/`"dots"` request the shared pattern texture
|
|
154
156
|
* ({@link makeFillPatternImage}). Alpha rides in `fillColor` — there is no
|
|
155
157
|
* separate opacity field. Adapters that cannot rasterize a pattern degrade
|
|
156
158
|
* to solid. Per-feature only: layer-level `StyleOptions` has no equivalent.
|
|
@@ -188,6 +190,15 @@ interface FeatureGroup {
|
|
|
188
190
|
id: string;
|
|
189
191
|
features: readonly Feature[];
|
|
190
192
|
iconResources: readonly IconResource[];
|
|
193
|
+
/**
|
|
194
|
+
* Monotonic stamp identifying this group's rendered content, when the
|
|
195
|
+
* producer supplies one. Two groups with the same `id` and the same
|
|
196
|
+
* `revision` carry byte-identical `features` and `iconResources`, so an
|
|
197
|
+
* adapter may skip any diff or upload work for them. A missing `revision`
|
|
198
|
+
* means "unknown" — adapters must fall back to their own comparison and
|
|
199
|
+
* must never treat two absent stamps as equal.
|
|
200
|
+
*/
|
|
201
|
+
revision?: number;
|
|
191
202
|
}
|
|
192
203
|
/**
|
|
193
204
|
* Click-style hit event delivered by {@link MapAdapter.onPick}.
|
package/dist/testing.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as MapAdapter, f as PixelCoordinate, i as LayerId } from "./map-adapter-
|
|
1
|
+
import { a as MapAdapter, f as PixelCoordinate, i as LayerId } from "./map-adapter-84irgVJR.mjs";
|
|
2
2
|
import { Feature } from "geojson";
|
|
3
3
|
|
|
4
4
|
//#region src/testing/feature-group-conformance.d.ts
|
package/dist/testing.mjs
CHANGED
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.39",
|
|
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.2.0-alpha.
|
|
42
|
+
"@orbat-mapper/control-measures": "^0.2.0-alpha.28"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"vitest": ">=4.0.0"
|