@orbat-mapper/tactical-draw 0.2.0-alpha.38 → 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
|
|
|
@@ -625,7 +625,15 @@ interface EditSession {
|
|
|
625
625
|
readonly history: SessionHistory;
|
|
626
626
|
/** Immutable edit-start input. */
|
|
627
627
|
readonly graphic: ControlMeasure;
|
|
628
|
-
/**
|
|
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
|
+
*/
|
|
629
637
|
readonly workingGraphic: ControlMeasure;
|
|
630
638
|
/** Live working state. Copy on read. */
|
|
631
639
|
readonly controlPoints: readonly Position[];
|
package/dist/index.mjs
CHANGED
|
@@ -2924,6 +2924,200 @@ function beginBoxDrag(args) {
|
|
|
2924
2924
|
};
|
|
2925
2925
|
}
|
|
2926
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
|
|
2927
3121
|
//#region src/tactical-draw/session-history.ts
|
|
2928
3122
|
/** @internal Controller-owned history stack. */
|
|
2929
3123
|
function createSessionHistory(options) {
|
|
@@ -3612,9 +3806,28 @@ function runEdit(opts) {
|
|
|
3612
3806
|
let workingOptions = { ...measure.options ?? {} };
|
|
3613
3807
|
let workingTextAmplifiers = measure.textAmplifiers && Object.keys(measure.textAmplifiers).length > 0 ? { ...measure.textAmplifiers } : void 0;
|
|
3614
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;
|
|
3615
3810
|
function snapshotAmplifierPlacements() {
|
|
3616
3811
|
return workingAmplifierPlacements ? structuredClone(workingAmplifierPlacements) : void 0;
|
|
3617
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
|
+
}
|
|
3618
3831
|
let modes = requestedModes;
|
|
3619
3832
|
let dirty = false;
|
|
3620
3833
|
let nonGeometryDirty = false;
|
|
@@ -3685,6 +3898,7 @@ function runEdit(opts) {
|
|
|
3685
3898
|
style: currentStyle ? { ...currentStyle } : void 0,
|
|
3686
3899
|
textAmplifiers: workingTextAmplifiers ? { ...workingTextAmplifiers } : void 0,
|
|
3687
3900
|
amplifierPlacements: snapshotAmplifierPlacements(),
|
|
3901
|
+
properties: snapshotProperties(),
|
|
3688
3902
|
sizeAnchor,
|
|
3689
3903
|
nonGeometryDirty,
|
|
3690
3904
|
boxAngle
|
|
@@ -3696,6 +3910,7 @@ function runEdit(opts) {
|
|
|
3696
3910
|
currentStyle = value.style ? { ...value.style } : void 0;
|
|
3697
3911
|
workingTextAmplifiers = value.textAmplifiers ? { ...value.textAmplifiers } : void 0;
|
|
3698
3912
|
workingAmplifierPlacements = value.amplifierPlacements ? structuredClone(value.amplifierPlacements) : void 0;
|
|
3913
|
+
workingProperties = value.properties ? structuredClone(value.properties) : void 0;
|
|
3699
3914
|
sizeAnchor = value.sizeAnchor;
|
|
3700
3915
|
nonGeometryDirty = value.nonGeometryDirty;
|
|
3701
3916
|
boxAngle = value.boxAngle;
|
|
@@ -3782,6 +3997,8 @@ function runEdit(opts) {
|
|
|
3782
3997
|
else delete working.textAmplifiers;
|
|
3783
3998
|
if (workingAmplifierPlacements) working.amplifierPlacements = workingAmplifierPlacements;
|
|
3784
3999
|
else delete working.amplifierPlacements;
|
|
4000
|
+
if (workingProperties) working.properties = workingProperties;
|
|
4001
|
+
else delete working.properties;
|
|
3785
4002
|
let features;
|
|
3786
4003
|
try {
|
|
3787
4004
|
features = renderMeasureForAdapter(adapter, working, renderOpts).features;
|
|
@@ -3859,6 +4076,8 @@ function runEdit(opts) {
|
|
|
3859
4076
|
else delete working.textAmplifiers;
|
|
3860
4077
|
if (workingAmplifierPlacements) working.amplifierPlacements = workingAmplifierPlacements;
|
|
3861
4078
|
else delete working.amplifierPlacements;
|
|
4079
|
+
if (workingProperties) working.properties = workingProperties;
|
|
4080
|
+
else delete working.properties;
|
|
3862
4081
|
const committed = sizeAnchor === "screen" ? working : bakePixelSizeToGround(working, adapter);
|
|
3863
4082
|
return {
|
|
3864
4083
|
graphic: cloneControlMeasure(committed),
|
|
@@ -3904,7 +4123,7 @@ function runEdit(opts) {
|
|
|
3904
4123
|
* the history record. The deferred reject/close/abort machinery, exception
|
|
3905
4124
|
* collection, and record-before-rethrow ordering live in the emitter.
|
|
3906
4125
|
*/
|
|
3907
|
-
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) {
|
|
3908
4127
|
changeEmitter.emit({
|
|
3909
4128
|
source,
|
|
3910
4129
|
graphic: currentRendered,
|
|
@@ -3918,6 +4137,7 @@ function runEdit(opts) {
|
|
|
3918
4137
|
sizeAnchor = previousSizeAnchor;
|
|
3919
4138
|
workingTextAmplifiers = previousTextAmplifiers;
|
|
3920
4139
|
workingAmplifierPlacements = previousAmplifierPlacements;
|
|
4140
|
+
workingProperties = previousProperties;
|
|
3921
4141
|
boxAngle = previousBoxAngle;
|
|
3922
4142
|
nonGeometryDirty = previousNonGeometryDirty;
|
|
3923
4143
|
renderWorkingGraphic();
|
|
@@ -3945,6 +4165,7 @@ function runEdit(opts) {
|
|
|
3945
4165
|
const previousNonGeometryDirty = nonGeometryDirty;
|
|
3946
4166
|
const previousTextAmplifiers = workingTextAmplifiers;
|
|
3947
4167
|
const previousAmplifierPlacements = snapshotAmplifierPlacements();
|
|
4168
|
+
const previousProperties = snapshotProperties();
|
|
3948
4169
|
const previousRendered = rebuildRenderedWith(previousOptions);
|
|
3949
4170
|
replaceWorkingPoints(nextPoints);
|
|
3950
4171
|
if (nextAmplifierPlacements !== void 0) workingAmplifierPlacements = structuredClone(nextAmplifierPlacements);
|
|
@@ -3964,7 +4185,7 @@ function runEdit(opts) {
|
|
|
3964
4185
|
recomputeDirty();
|
|
3965
4186
|
dragInFlight = false;
|
|
3966
4187
|
drainPendingModes();
|
|
3967
|
-
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);
|
|
3968
4189
|
}
|
|
3969
4190
|
function setOptions(partial) {
|
|
3970
4191
|
if (ctx.settled) return;
|
|
@@ -3974,12 +4195,13 @@ function runEdit(opts) {
|
|
|
3974
4195
|
const previousNonGeometryDirty = nonGeometryDirty;
|
|
3975
4196
|
const previousTextAmplifiers = workingTextAmplifiers;
|
|
3976
4197
|
const previousAmplifierPlacements = snapshotAmplifierPlacements();
|
|
4198
|
+
const previousProperties = snapshotProperties();
|
|
3977
4199
|
const previousRendered = rebuildRendered();
|
|
3978
4200
|
replaceWorkingOptions({
|
|
3979
4201
|
...workingOptions ?? {},
|
|
3980
4202
|
...partial ?? {}
|
|
3981
4203
|
});
|
|
3982
|
-
if (
|
|
4204
|
+
if (jsonEqual(workingOptions, previousOptions)) return;
|
|
3983
4205
|
let currentRendered;
|
|
3984
4206
|
try {
|
|
3985
4207
|
currentRendered = rebuildRendered();
|
|
@@ -3990,7 +4212,7 @@ function runEdit(opts) {
|
|
|
3990
4212
|
renderWorkingGraphicAndHandles();
|
|
3991
4213
|
nonGeometryDirty = true;
|
|
3992
4214
|
dirty = true;
|
|
3993
|
-
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");
|
|
3994
4216
|
}
|
|
3995
4217
|
function setStyle(partial) {
|
|
3996
4218
|
if (ctx.settled) return;
|
|
@@ -4000,12 +4222,13 @@ function runEdit(opts) {
|
|
|
4000
4222
|
const previousNonGeometryDirty = nonGeometryDirty;
|
|
4001
4223
|
const previousTextAmplifiers = workingTextAmplifiers;
|
|
4002
4224
|
const previousAmplifierPlacements = snapshotAmplifierPlacements();
|
|
4225
|
+
const previousProperties = snapshotProperties();
|
|
4003
4226
|
const previousRendered = rebuildRendered();
|
|
4004
4227
|
currentStyle = {
|
|
4005
4228
|
...currentStyle ?? {},
|
|
4006
4229
|
...partial
|
|
4007
4230
|
};
|
|
4008
|
-
if (
|
|
4231
|
+
if (jsonEqual(currentStyle, previousStyle)) return;
|
|
4009
4232
|
let currentRendered;
|
|
4010
4233
|
try {
|
|
4011
4234
|
currentRendered = rebuildRendered();
|
|
@@ -4016,7 +4239,7 @@ function runEdit(opts) {
|
|
|
4016
4239
|
renderWorkingGraphicAndHandles();
|
|
4017
4240
|
nonGeometryDirty = true;
|
|
4018
4241
|
dirty = true;
|
|
4019
|
-
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");
|
|
4020
4243
|
}
|
|
4021
4244
|
/**
|
|
4022
4245
|
* Shallow-merge into the live working text amplifiers. Mirrors
|
|
@@ -4041,6 +4264,7 @@ function runEdit(opts) {
|
|
|
4041
4264
|
const previousNonGeometryDirty = nonGeometryDirty;
|
|
4042
4265
|
const previousTextAmplifiers = workingTextAmplifiers;
|
|
4043
4266
|
const previousAmplifierPlacements = snapshotAmplifierPlacements();
|
|
4267
|
+
const previousProperties = snapshotProperties();
|
|
4044
4268
|
const previousRendered = rebuildRendered();
|
|
4045
4269
|
const merged = { ...workingTextAmplifiers ?? {} };
|
|
4046
4270
|
for (const [key, value] of Object.entries(partial)) {
|
|
@@ -4051,7 +4275,7 @@ function runEdit(opts) {
|
|
|
4051
4275
|
if (value !== void 0 && value.trim() !== "") merged[canonicalKey ?? key] = value;
|
|
4052
4276
|
}
|
|
4053
4277
|
workingTextAmplifiers = Object.keys(merged).length > 0 ? merged : void 0;
|
|
4054
|
-
if (
|
|
4278
|
+
if (jsonEqual(workingTextAmplifiers, previousTextAmplifiers)) return;
|
|
4055
4279
|
let currentRendered;
|
|
4056
4280
|
try {
|
|
4057
4281
|
currentRendered = rebuildRendered();
|
|
@@ -4062,7 +4286,7 @@ function runEdit(opts) {
|
|
|
4062
4286
|
renderWorkingGraphicAndHandles();
|
|
4063
4287
|
nonGeometryDirty = true;
|
|
4064
4288
|
dirty = true;
|
|
4065
|
-
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");
|
|
4066
4290
|
}
|
|
4067
4291
|
function setAmplifierPlacements(partial, source = "programmatic", historyLabel = "Move label") {
|
|
4068
4292
|
if (ctx.settled) return;
|
|
@@ -4072,6 +4296,7 @@ function runEdit(opts) {
|
|
|
4072
4296
|
const previousNonGeometryDirty = nonGeometryDirty;
|
|
4073
4297
|
const previousTextAmplifiers = workingTextAmplifiers;
|
|
4074
4298
|
const previousAmplifierPlacements = snapshotAmplifierPlacements();
|
|
4299
|
+
const previousProperties = snapshotProperties();
|
|
4075
4300
|
const previousRendered = rebuildRendered();
|
|
4076
4301
|
const next = { ...workingAmplifierPlacements ?? {} };
|
|
4077
4302
|
for (const [field, value] of Object.entries(partial)) {
|
|
@@ -4084,12 +4309,12 @@ function runEdit(opts) {
|
|
|
4084
4309
|
else next[field] = merged;
|
|
4085
4310
|
}
|
|
4086
4311
|
workingAmplifierPlacements = Object.keys(next).length > 0 ? next : void 0;
|
|
4087
|
-
if (
|
|
4312
|
+
if (jsonEqual(workingAmplifierPlacements, previousAmplifierPlacements)) return;
|
|
4088
4313
|
const currentRendered = rebuildRendered();
|
|
4089
4314
|
renderWorkingGraphicAndHandles();
|
|
4090
4315
|
nonGeometryDirty = true;
|
|
4091
4316
|
dirty = true;
|
|
4092
|
-
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);
|
|
4093
4318
|
}
|
|
4094
4319
|
/**
|
|
4095
4320
|
* Flip the size anchor for the measure under edit (ADR-0020). Re-renders the
|
|
@@ -4107,12 +4332,13 @@ function runEdit(opts) {
|
|
|
4107
4332
|
const previousSizeAnchor = sizeAnchor;
|
|
4108
4333
|
const previousTextAmplifiers = workingTextAmplifiers;
|
|
4109
4334
|
const previousAmplifierPlacements = snapshotAmplifierPlacements();
|
|
4335
|
+
const previousProperties = snapshotProperties();
|
|
4110
4336
|
const previousRendered = rebuildRendered();
|
|
4111
4337
|
sizeAnchor = next;
|
|
4112
4338
|
const currentRendered = rebuildRendered();
|
|
4113
4339
|
nonGeometryDirty = true;
|
|
4114
4340
|
dirty = true;
|
|
4115
|
-
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");
|
|
4116
4342
|
}
|
|
4117
4343
|
function modesEqual(a, b) {
|
|
4118
4344
|
if (a.length !== b.length) return false;
|
|
@@ -4139,34 +4365,38 @@ function runEdit(opts) {
|
|
|
4139
4365
|
style: value.style ? { ...value.style } : void 0,
|
|
4140
4366
|
textAmplifiers: value.textAmplifiers ? { ...value.textAmplifiers } : void 0,
|
|
4141
4367
|
amplifierPlacements: value.amplifierPlacements ? structuredClone(value.amplifierPlacements) : void 0,
|
|
4368
|
+
properties: value.properties ? structuredClone(value.properties) : void 0,
|
|
4142
4369
|
sizeAnchor: value.sizeAnchor,
|
|
4143
4370
|
nonGeometryDirty: value.nonGeometryDirty,
|
|
4144
4371
|
boxAngle: value.boxAngle
|
|
4145
4372
|
}),
|
|
4146
|
-
equals: (a, b) =>
|
|
4147
|
-
isCurrent: (value) =>
|
|
4373
|
+
equals: (a, b) => deepEqual(a, b),
|
|
4374
|
+
isCurrent: (value) => deepEqual(captureHistory(), value),
|
|
4148
4375
|
apply(from, to, source) {
|
|
4149
4376
|
const previousRendered = rebuildRendered();
|
|
4150
4377
|
restoreHistory(to);
|
|
4151
4378
|
renderWorkingGraphic();
|
|
4152
4379
|
renderHandlesLayer();
|
|
4153
4380
|
recomputeDirty();
|
|
4154
|
-
emitChange(previousRendered, from.points, from.options, from.style, from.nonGeometryDirty, from.sizeAnchor, rebuildRendered(), from.textAmplifiers, from.amplifierPlacements, source, "Edit", from.boxAngle);
|
|
4155
|
-
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);
|
|
4156
4383
|
}
|
|
4157
4384
|
});
|
|
4158
4385
|
const session = {
|
|
4159
4386
|
history,
|
|
4160
4387
|
graphic: measure,
|
|
4161
4388
|
get workingGraphic() {
|
|
4162
|
-
|
|
4389
|
+
const working = {
|
|
4163
4390
|
...measure,
|
|
4164
4391
|
controlPoints: workingPoints.map((point) => [...point]),
|
|
4165
4392
|
options: { ...workingOptions ?? {} },
|
|
4166
4393
|
style: currentStyle ? { ...currentStyle } : void 0,
|
|
4167
4394
|
textAmplifiers: workingTextAmplifiers ? { ...workingTextAmplifiers } : void 0,
|
|
4168
4395
|
amplifierPlacements: snapshotAmplifierPlacements()
|
|
4169
|
-
}
|
|
4396
|
+
};
|
|
4397
|
+
if (workingProperties) working.properties = workingProperties;
|
|
4398
|
+
else delete working.properties;
|
|
4399
|
+
return cloneControlMeasure(working);
|
|
4170
4400
|
},
|
|
4171
4401
|
get controlPoints() {
|
|
4172
4402
|
return workingPoints.map((p) => [...p]);
|
|
@@ -4213,21 +4443,23 @@ function runEdit(opts) {
|
|
|
4213
4443
|
const previous = rebuildRendered();
|
|
4214
4444
|
const previousNonGeometryDirty = nonGeometryDirty;
|
|
4215
4445
|
const previousSizeAnchor = sizeAnchor;
|
|
4446
|
+
const previousProperties = snapshotProperties();
|
|
4216
4447
|
const current = session.workingGraphic;
|
|
4217
4448
|
const next = updater(current);
|
|
4218
4449
|
if (next.id !== measure.id || next.kind !== measure.kind) throw new TypeError("updateGraphic must preserve control-measure id and kind");
|
|
4219
|
-
if (
|
|
4450
|
+
if (deepEqual(next, current)) return;
|
|
4220
4451
|
renderMeasureForAdapter(adapter, next, renderOpts);
|
|
4221
4452
|
workingPoints.splice(0, workingPoints.length, ...next.controlPoints.map((point) => [...point]));
|
|
4222
4453
|
workingOptions = { ...next.options ?? {} };
|
|
4223
4454
|
currentStyle = next.style ? { ...next.style } : void 0;
|
|
4224
4455
|
workingTextAmplifiers = next.textAmplifiers ? { ...next.textAmplifiers } : void 0;
|
|
4225
4456
|
workingAmplifierPlacements = next.amplifierPlacements ? structuredClone(next.amplifierPlacements) : void 0;
|
|
4457
|
+
workingProperties = next.properties ? structuredClone(next.properties) : void 0;
|
|
4226
4458
|
nonGeometryDirty = true;
|
|
4227
4459
|
renderWorkingGraphic();
|
|
4228
4460
|
renderHandlesLayer();
|
|
4229
4461
|
recomputeDirty();
|
|
4230
|
-
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");
|
|
4231
4463
|
},
|
|
4232
4464
|
setOptions(partial) {
|
|
4233
4465
|
setOptions(partial);
|
|
@@ -5756,7 +5988,7 @@ function runPointSymbolEdit(opts) {
|
|
|
5756
5988
|
}, {
|
|
5757
5989
|
rollback() {
|
|
5758
5990
|
working = clonePointSymbol(previousState.working);
|
|
5759
|
-
dirty =
|
|
5991
|
+
dirty = !deepEqual(working, symbol);
|
|
5760
5992
|
render();
|
|
5761
5993
|
},
|
|
5762
5994
|
onAccepted
|
|
@@ -5765,11 +5997,11 @@ function runPointSymbolEdit(opts) {
|
|
|
5765
5997
|
const history = createSessionHistory({
|
|
5766
5998
|
initial: captureHistory(),
|
|
5767
5999
|
clone: cloneHistory,
|
|
5768
|
-
equals: (a, b) =>
|
|
5769
|
-
isCurrent: (value) =>
|
|
6000
|
+
equals: (a, b) => deepEqual(a.working, b.working),
|
|
6001
|
+
isCurrent: (value) => deepEqual(working, value.working),
|
|
5770
6002
|
apply(from, to, source) {
|
|
5771
6003
|
working = clonePointSymbol(to.working);
|
|
5772
|
-
dirty =
|
|
6004
|
+
dirty = !deepEqual(working, symbol);
|
|
5773
6005
|
render();
|
|
5774
6006
|
return emitChange(from, source);
|
|
5775
6007
|
}
|
|
@@ -5777,7 +6009,7 @@ function runPointSymbolEdit(opts) {
|
|
|
5777
6009
|
const apply = (next, source = "programmatic", label = "Update graphic") => {
|
|
5778
6010
|
const previousState = captureHistory();
|
|
5779
6011
|
working = clonePointSymbol(next);
|
|
5780
|
-
dirty =
|
|
6012
|
+
dirty = !deepEqual(working, symbol);
|
|
5781
6013
|
render();
|
|
5782
6014
|
emitChange(previousState, source, () => history.record(captureHistory(), label));
|
|
5783
6015
|
};
|
|
@@ -7166,15 +7398,25 @@ function resolveSlot(adapter, supplied, owned) {
|
|
|
7166
7398
|
* expressions without re-spelling the pattern vocabulary.
|
|
7167
7399
|
*/
|
|
7168
7400
|
const HATCH_PATTERNS = [
|
|
7169
|
-
"
|
|
7170
|
-
"
|
|
7171
|
-
"cross"
|
|
7401
|
+
"hatch",
|
|
7402
|
+
"reverse-hatch",
|
|
7403
|
+
"cross-hatch",
|
|
7404
|
+
"horizontal",
|
|
7405
|
+
"vertical",
|
|
7406
|
+
"dots"
|
|
7172
7407
|
];
|
|
7173
7408
|
/** Tile edge length, CSS px, before `pixelRatio` scaling. Kept a power of two. */
|
|
7174
7409
|
const BASE_TILE_SIZE = 32;
|
|
7175
7410
|
/** Line spacing as a fraction of the tile edge — 4 repeats per tile. */
|
|
7176
7411
|
const SPACING_DIVISOR = 4;
|
|
7177
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
|
+
/**
|
|
7178
7420
|
* Stable id for a rasterized tile, keyed by pattern and color. Adapters use
|
|
7179
7421
|
* this as the image-registry key (MapLibre `addImage` id, OpenLayers pattern
|
|
7180
7422
|
* cache key) so the same color+pattern never gets rasterized twice. Color is
|
|
@@ -7254,8 +7496,8 @@ const SQRT2 = Math.SQRT2;
|
|
|
7254
7496
|
/**
|
|
7255
7497
|
* Coverage in `[0, 1]` of one diagonal line family (slope +1 or -1, both
|
|
7256
7498
|
* period `spacing`) at pixel center `(x, y)`, antialiased over a ~1 device-px
|
|
7257
|
-
* band. `axis` selects `y - x` (the "/"
|
|
7258
|
-
* "
|
|
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`
|
|
7259
7501
|
* divides the tile dimension evenly, shifting
|
|
7260
7502
|
* `x` or `y` by a whole tile is a whole number of `spacing` periods, so the
|
|
7261
7503
|
* mod-distance — and therefore the coverage — is identical at the tile edges
|
|
@@ -7263,30 +7505,61 @@ const SQRT2 = Math.SQRT2;
|
|
|
7263
7505
|
* seamless.
|
|
7264
7506
|
*/
|
|
7265
7507
|
function lineCoverage(x, y, spacing, lineWidth, axis) {
|
|
7266
|
-
const t = axis === "/" ? y - x : y + x;
|
|
7267
|
-
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;
|
|
7268
7511
|
const halfWidth = lineWidth / 2;
|
|
7269
7512
|
return Math.min(1, Math.max(0, halfWidth + .5 - perpDist));
|
|
7270
7513
|
}
|
|
7271
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
|
+
/**
|
|
7272
7525
|
* Rasterize one seamless hatch tile as a straight RGBA pixel buffer — no
|
|
7273
7526
|
* canvas involved, so this runs identically in a browser adapter and in
|
|
7274
7527
|
* jsdom/node tests. Diagonal lines are snapped to slope ±1 (45°) so the
|
|
7275
|
-
* mod-based coverage math in {@link lineCoverage} tiles exactly. `"
|
|
7276
|
-
* draws the "/" diagonal, `"
|
|
7277
|
-
* 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}).
|
|
7278
7533
|
*/
|
|
7279
7534
|
function makeFillPatternImage(color, pattern, pixelRatio = 1) {
|
|
7280
7535
|
const size = fillPatternImageDimension(pixelRatio);
|
|
7281
7536
|
const spacing = size / SPACING_DIVISOR;
|
|
7282
7537
|
const lineWidth = Math.max(1, Math.round(pixelRatio));
|
|
7538
|
+
const dotRadius = DOT_RADIUS * Math.max(1, Math.round(pixelRatio));
|
|
7283
7539
|
const { r, g, b, a } = parseColor(color);
|
|
7284
7540
|
const data = new Uint8ClampedArray(size * size * 4);
|
|
7285
7541
|
for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) {
|
|
7286
7542
|
const cx = x + .5;
|
|
7287
7543
|
const cy = y + .5;
|
|
7288
|
-
let coverage
|
|
7289
|
-
|
|
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
|
+
}
|
|
7290
7563
|
const i = (y * size + x) * 4;
|
|
7291
7564
|
data[i] = r;
|
|
7292
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.
|
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"
|