@orbat-mapper/tactical-draw 0.2.0-alpha.26 → 0.2.0-alpha.28
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 +62 -3
- package/dist/index.mjs +203 -16
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AmplifierPlacement, AmplifierPlacements, ControlMeasure, ControlMeasureKind, ControlMeasureRender, ControlMeasureStyle, TextAmplifiers } from "@orbat-mapper/control-measures";
|
|
1
|
+
import { AmplifierPlacement, AmplifierPlacements, ControlMeasure, ControlMeasureKind, ControlMeasureRender, ControlMeasureStyle, FillPattern, TextAmplifiers } from "@orbat-mapper/control-measures";
|
|
2
2
|
import { Feature, Position } from "geojson";
|
|
3
3
|
|
|
4
4
|
//#region src/gestures/types.d.ts
|
|
@@ -40,6 +40,51 @@ interface EditPointerDriver {
|
|
|
40
40
|
dispose(): void;
|
|
41
41
|
}
|
|
42
42
|
//#endregion
|
|
43
|
+
//#region src/fill-pattern.d.ts
|
|
44
|
+
/**
|
|
45
|
+
* Shared hatch fill-pattern tiles for area control-measure fills (ADR-0035
|
|
46
|
+
* `FillPattern`, excluding `"solid"` which every adapter already renders as a
|
|
47
|
+
* flat color). Both the MapLibre adapter (raster `addImage` pattern) and the
|
|
48
|
+
* OpenLayers adapter (canvas `Pattern` fill) need the *same* diagonal-hachure
|
|
49
|
+
* texture, so it is rasterized once here in pure math — no `HTMLCanvasElement`
|
|
50
|
+
* — so it works in adapter code and in jsdom/node tests alike.
|
|
51
|
+
*/
|
|
52
|
+
type HatchPattern = Exclude<FillPattern, "solid">;
|
|
53
|
+
/**
|
|
54
|
+
* Every non-solid `FillPattern` as a runtime list — the value-level companion
|
|
55
|
+
* to {@link HatchPattern}, so adapters can test membership and build filter
|
|
56
|
+
* expressions without re-spelling the pattern vocabulary.
|
|
57
|
+
*/
|
|
58
|
+
declare const HATCH_PATTERNS: readonly HatchPattern[];
|
|
59
|
+
/**
|
|
60
|
+
* Stable id for a rasterized tile, keyed by pattern and color. Adapters use
|
|
61
|
+
* this as the image-registry key (MapLibre `addImage` id, OpenLayers pattern
|
|
62
|
+
* cache key) so the same color+pattern never gets rasterized twice. Color is
|
|
63
|
+
* lowercased so `"#FF0000"` and `"#ff0000"` share one texture.
|
|
64
|
+
*/
|
|
65
|
+
declare function fillPatternImageId(color: string, pattern: HatchPattern): string;
|
|
66
|
+
/**
|
|
67
|
+
* Tile edge length in device pixels for a given `pixelRatio`. Must be a power
|
|
68
|
+
* of two — MapLibre requires POT dimensions for seamless WebGL texture wrap —
|
|
69
|
+
* and scales linearly with `pixelRatio` for the ratios that matter in
|
|
70
|
+
* practice (1x/2x displays: 32 -> 64, both already POT). Non-POT products
|
|
71
|
+
* (e.g. a 3x ratio, 32*3=96) round up to the next POT rather than losing the
|
|
72
|
+
* power-of-two guarantee.
|
|
73
|
+
*/
|
|
74
|
+
declare function fillPatternImageDimension(pixelRatio: number): number;
|
|
75
|
+
/**
|
|
76
|
+
* Rasterize one seamless hatch tile as a straight RGBA pixel buffer — no
|
|
77
|
+
* canvas involved, so this runs identically in a browser adapter and in
|
|
78
|
+
* jsdom/node tests. Diagonal lines are snapped to slope ±1 (45°) so the
|
|
79
|
+
* mod-based coverage math in {@link lineCoverage} tiles exactly; `"cross"`
|
|
80
|
+
* unions both diagonals via `Math.max` coverage.
|
|
81
|
+
*/
|
|
82
|
+
declare function makeFillPatternImage(color: string, pattern: HatchPattern, pixelRatio?: number): {
|
|
83
|
+
width: number;
|
|
84
|
+
height: number;
|
|
85
|
+
data: Uint8ClampedArray;
|
|
86
|
+
};
|
|
87
|
+
//#endregion
|
|
43
88
|
//#region src/map-adapter.d.ts
|
|
44
89
|
type PixelCoordinate = [number, number];
|
|
45
90
|
type MapCursor = "" | "default" | "crosshair" | "grab" | "grabbing" | "pointer";
|
|
@@ -102,6 +147,14 @@ interface HandleStyle {
|
|
|
102
147
|
strokeWidth?: number;
|
|
103
148
|
strokeDash?: readonly number[];
|
|
104
149
|
fillColor?: string;
|
|
150
|
+
/**
|
|
151
|
+
* How `fillColor` paints a filled part's interior. Absent or `"solid"` is
|
|
152
|
+
* today's flat fill; `"hachure"`/`"cross"` request the shared hatch texture
|
|
153
|
+
* ({@link makeFillPatternImage}). Alpha rides in `fillColor` — there is no
|
|
154
|
+
* separate opacity field. Adapters that cannot rasterize a pattern degrade
|
|
155
|
+
* to solid. Per-feature only: layer-level `StyleOptions` has no equivalent.
|
|
156
|
+
*/
|
|
157
|
+
fillPattern?: "solid" | HatchPattern;
|
|
105
158
|
pointRadius?: number;
|
|
106
159
|
}
|
|
107
160
|
type LayerId = string;
|
|
@@ -180,6 +233,12 @@ interface MapAdapter {
|
|
|
180
233
|
* silently ignored.
|
|
181
234
|
*/
|
|
182
235
|
removeFeatures(layerId: LayerId, ids: readonly string[]): void;
|
|
236
|
+
/**
|
|
237
|
+
* Defer an upcoming removal from `layerId` until the most recent update to
|
|
238
|
+
* `updatedLayerId` has completed in the engine. Optional because synchronous
|
|
239
|
+
* adapters already make cross-layer write order visible atomically.
|
|
240
|
+
*/
|
|
241
|
+
deferLayerRemovalUntilLayerUpdate?(layerId: LayerId, updatedLayerId: LayerId): void;
|
|
183
242
|
/**
|
|
184
243
|
* Subscribe to click-style "pick" events for a specific layer. Basemap and
|
|
185
244
|
* other-layer clicks never fire the handler.
|
|
@@ -1518,7 +1577,7 @@ declare class TacticalDraw {
|
|
|
1518
1577
|
* Routes every measure through [[renderMeasureForAdapter]] so pixel sizing is
|
|
1519
1578
|
* resolved against the live map. Callers own any edit-exclusion policy — see
|
|
1520
1579
|
* `render()` (excludes the edited measure, owned by the preview layer) versus
|
|
1521
|
-
* `
|
|
1580
|
+
* `settleToGraphics` (no exclusion, the edit is ending).
|
|
1522
1581
|
*/
|
|
1523
1582
|
private renderMeasuresToGraphics;
|
|
1524
1583
|
/**
|
|
@@ -1904,4 +1963,4 @@ declare function nearestWorldCopyLng(lng: number, refLng: number): number;
|
|
|
1904
1963
|
/** `nearestWorldCopyLng` applied to a lon/lat position. */
|
|
1905
1964
|
declare function nearestWorldCopyPosition(position: Position, refLng: number): Position;
|
|
1906
1965
|
//#endregion
|
|
1907
|
-
export { BaseMapAdapter, type CombinedAbort, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, type DrawMeasureDraft, type DrawOptions, type DrawPointSymbolDraft, type DrawSession, type EditChangeEvent, type EditManyOptions, type EditMode, type EditOptions, type EditPointerDriver, type EditSession, type FeatureOps, type Graphic, type GraphicEditChangeEvent, type GraphicEditSession, type GraphicGeometrySnappingOptions, type GraphicRender, type GraphicSnapshot, type GuideStyle, type HandleStyle, type IconResource, type InteractionHit, type InteractionStyle, type LayerId, type MapAdapter, type MapCursor, type MapEvent, type MapEventHandler, type NativeEventEntry, type PickEvent, type PickHandler, type PickOptions, type PixelCoordinate, type PointSymbol, type PointSymbolAnchor, type PointSymbolCapability, PointSymbolCapabilityRequiredError, type PointSymbolColors, type PointSymbolFootprint, type PointSymbolGraphicModifiers, type PointSymbolRender, type PointSymbolSize, type PointSymbolStyle, type PointerCallback, PointerCallbackHub, type PointerEventInfo, type SessionChangeSource, type SessionHistory, type SessionHistoryState, type SettleReason, type SizeAnchor, type SnapCandidate, type SnapCandidateProvider, type SnapCandidateRequest, type SnapInteraction, type SnapResult, type SnappingOptions, type StyleOptions, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, type TacticalDrawAbortReason, TacticalDrawDestroyedError, type TacticalDrawLayerOptions, type TacticalDrawOptions, type TextStyleOptions, type ToPixel, type TransformBox, type TransformChangeEvent, type TransformSession, bindAbortable, clonePointSymbol, coerceHandleStyle, combineWithHostSignal, effectivePointSymbolPixelSize, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, isPointSymbol, isTacticalDrawAbortError, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, pointSymbolFootprint, resizePointSymbolSize, resolveGroundTextSizePixels, tryControlMeasureIdFromFeature };
|
|
1966
|
+
export { BaseMapAdapter, type CombinedAbort, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, type DrawMeasureDraft, type DrawOptions, type DrawPointSymbolDraft, type DrawSession, type EditChangeEvent, type EditManyOptions, type EditMode, type EditOptions, type EditPointerDriver, type EditSession, type FeatureOps, type Graphic, type GraphicEditChangeEvent, type GraphicEditSession, type GraphicGeometrySnappingOptions, type GraphicRender, type GraphicSnapshot, type GuideStyle, HATCH_PATTERNS, type HandleStyle, type HatchPattern, type IconResource, type InteractionHit, type InteractionStyle, type LayerId, type MapAdapter, type MapCursor, type MapEvent, type MapEventHandler, type NativeEventEntry, type PickEvent, type PickHandler, type PickOptions, type PixelCoordinate, type PointSymbol, type PointSymbolAnchor, type PointSymbolCapability, PointSymbolCapabilityRequiredError, type PointSymbolColors, type PointSymbolFootprint, type PointSymbolGraphicModifiers, type PointSymbolRender, type PointSymbolSize, type PointSymbolStyle, type PointerCallback, PointerCallbackHub, type PointerEventInfo, type SessionChangeSource, type SessionHistory, type SessionHistoryState, type SettleReason, type SizeAnchor, type SnapCandidate, type SnapCandidateProvider, type SnapCandidateRequest, type SnapInteraction, type SnapResult, type SnappingOptions, type StyleOptions, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, type TacticalDrawAbortReason, TacticalDrawDestroyedError, type TacticalDrawLayerOptions, type TacticalDrawOptions, type TextStyleOptions, type ToPixel, type TransformBox, type TransformChangeEvent, type TransformSession, bindAbortable, clonePointSymbol, coerceHandleStyle, combineWithHostSignal, effectivePointSymbolPixelSize, fillPatternImageDimension, fillPatternImageId, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, isPointSymbol, isTacticalDrawAbortError, makeFillPatternImage, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, pointSymbolFootprint, resizePointSymbolSize, resolveGroundTextSizePixels, tryControlMeasureIdFromFeature };
|
package/dist/index.mjs
CHANGED
|
@@ -1897,6 +1897,38 @@ function runEditInteraction(spec) {
|
|
|
1897
1897
|
attach(interaction) {
|
|
1898
1898
|
ctx = interaction;
|
|
1899
1899
|
driver = adapter.createEditPointerDriver();
|
|
1900
|
+
let beforeTeardownRan = false;
|
|
1901
|
+
const runBeforeTeardown = (settlement) => {
|
|
1902
|
+
if (beforeTeardownRan) return;
|
|
1903
|
+
beforeTeardownRan = true;
|
|
1904
|
+
try {
|
|
1905
|
+
spec.onBeforeTeardown?.(settlement);
|
|
1906
|
+
} catch {}
|
|
1907
|
+
};
|
|
1908
|
+
const controllerContext = {
|
|
1909
|
+
get settled() {
|
|
1910
|
+
return interaction.settled;
|
|
1911
|
+
},
|
|
1912
|
+
commit(produce) {
|
|
1913
|
+
return interaction.commit(() => {
|
|
1914
|
+
const value = produce();
|
|
1915
|
+
runBeforeTeardown({
|
|
1916
|
+
status: "committed",
|
|
1917
|
+
value
|
|
1918
|
+
});
|
|
1919
|
+
return value;
|
|
1920
|
+
});
|
|
1921
|
+
},
|
|
1922
|
+
fail(error) {
|
|
1923
|
+
if (!interaction.settled) runBeforeTeardown({
|
|
1924
|
+
status: "rejected",
|
|
1925
|
+
error
|
|
1926
|
+
});
|
|
1927
|
+
interaction.fail(error);
|
|
1928
|
+
},
|
|
1929
|
+
writeLayer: interaction.writeLayer,
|
|
1930
|
+
onTeardown: interaction.onTeardown
|
|
1931
|
+
};
|
|
1900
1932
|
const unsubDown = driver.onPointerDown(spec.onDown);
|
|
1901
1933
|
const unsubMove = driver.onPointerMove(spec.onMove);
|
|
1902
1934
|
const unsubUp = driver.onPointerUp(spec.onUp);
|
|
@@ -1905,6 +1937,10 @@ function runEditInteraction(spec) {
|
|
|
1905
1937
|
const unsubGraphics = adapter.onPick(graphicsLayer, onGraphicsPick);
|
|
1906
1938
|
adapter.on("click", onClick);
|
|
1907
1939
|
interaction.onTeardown(() => {
|
|
1940
|
+
runBeforeTeardown({
|
|
1941
|
+
status: "rejected",
|
|
1942
|
+
error: void 0
|
|
1943
|
+
});
|
|
1908
1944
|
adapter.off("click", onClick);
|
|
1909
1945
|
unsubPreview();
|
|
1910
1946
|
unsubHandles();
|
|
@@ -1915,7 +1951,7 @@ function runEditInteraction(spec) {
|
|
|
1915
1951
|
spec.extraTeardown?.();
|
|
1916
1952
|
driver.dispose();
|
|
1917
1953
|
}, spec.onAbort);
|
|
1918
|
-
spec.attach(
|
|
1954
|
+
spec.attach(controllerContext, driver, shell);
|
|
1919
1955
|
}
|
|
1920
1956
|
});
|
|
1921
1957
|
}
|
|
@@ -2983,7 +3019,7 @@ const ALLOWED_EDIT_MODES = new Set([
|
|
|
2983
3019
|
* - close / abort / signal / preempt / destroy paths
|
|
2984
3020
|
*/
|
|
2985
3021
|
function runEdit(opts) {
|
|
2986
|
-
const { adapter, measure, graphicsLayer, previewLayer, guideLayer, handlesLayer, graphicsStyle, signal, closeOnClickAway = true, modes: initialModes, interactionStyle, onSession, handleIdPrefix, guide: guideOpt,
|
|
3022
|
+
const { adapter, measure, graphicsLayer, previewLayer, guideLayer, handlesLayer, graphicsStyle, signal, closeOnClickAway = true, modes: initialModes, interactionStyle, onSession, handleIdPrefix, guide: guideOpt, settleToGraphics, wasOnGraphicsLayer, onSettled, onClickAwayGraphicsPick, snap } = opts;
|
|
2987
3023
|
const renderOpts = graphicsStyle ? { graphicsStyle } : void 0;
|
|
2988
3024
|
let sizeAnchor = opts.sizeAnchor ?? "ground";
|
|
2989
3025
|
const metadata = getControlMeasureMetadata(measure.kind);
|
|
@@ -3858,6 +3894,9 @@ function runEdit(opts) {
|
|
|
3858
3894
|
previewLayer,
|
|
3859
3895
|
handlesLayer,
|
|
3860
3896
|
closeOnClickAway,
|
|
3897
|
+
onBeforeTeardown: (settlement) => {
|
|
3898
|
+
settleToGraphics(settlement.status === "committed" ? settlement.value.graphic : void 0);
|
|
3899
|
+
},
|
|
3861
3900
|
onViewChange: rescaleOnViewChange,
|
|
3862
3901
|
onDown: onDriverDown,
|
|
3863
3902
|
onMove: onDriverMove,
|
|
@@ -3879,13 +3918,15 @@ function runEdit(opts) {
|
|
|
3879
3918
|
dragInFlight = false;
|
|
3880
3919
|
}
|
|
3881
3920
|
},
|
|
3882
|
-
onAbort: wasOnGraphicsLayer ? restoreOriginalToGraphics : void 0,
|
|
3883
3921
|
attach(interaction, createdDriver, createdShell) {
|
|
3884
3922
|
ctx = interaction;
|
|
3885
3923
|
driver = createdDriver;
|
|
3886
3924
|
shell = createdShell;
|
|
3887
|
-
liftOriginalFromGraphics();
|
|
3888
3925
|
renderPreview();
|
|
3926
|
+
if (wasOnGraphicsLayer) {
|
|
3927
|
+
adapter.deferLayerRemovalUntilLayerUpdate?.(graphicsLayer, previewLayer);
|
|
3928
|
+
liftOriginalFromGraphics();
|
|
3929
|
+
}
|
|
3889
3930
|
renderHandlesLayer();
|
|
3890
3931
|
recomputeDirty();
|
|
3891
3932
|
if (onSession) onSession(session);
|
|
@@ -4878,7 +4919,7 @@ function symbolBox(adapter, capability, symbol, padding = 0) {
|
|
|
4878
4919
|
};
|
|
4879
4920
|
}
|
|
4880
4921
|
function runPointSymbolEdit(opts) {
|
|
4881
|
-
const { adapter, capability, symbol, graphicsLayer, previewLayer, handlesLayer, signal, closeOnClickAway = true, interactionStyle, onSession, handleIdPrefix,
|
|
4922
|
+
const { adapter, capability, symbol, graphicsLayer, previewLayer, handlesLayer, signal, closeOnClickAway = true, interactionStyle, onSession, handleIdPrefix, settleToGraphics, wasOnGraphicsLayer, onSettled, onClickAwayGraphicsPick, snap } = opts;
|
|
4882
4923
|
let modes = normalisePointModes(opts.modes);
|
|
4883
4924
|
let working = clonePointSymbol(symbol);
|
|
4884
4925
|
let dirty = false;
|
|
@@ -5063,6 +5104,9 @@ function runPointSymbolEdit(opts) {
|
|
|
5063
5104
|
handlesLayer,
|
|
5064
5105
|
closeOnClickAway,
|
|
5065
5106
|
onViewChange: render,
|
|
5107
|
+
onBeforeTeardown: (settlement) => {
|
|
5108
|
+
settleToGraphics(settlement.status === "committed" ? settlement.value.graphic : void 0);
|
|
5109
|
+
},
|
|
5066
5110
|
onDown,
|
|
5067
5111
|
onMove,
|
|
5068
5112
|
onUp,
|
|
@@ -5075,15 +5119,15 @@ function runPointSymbolEdit(opts) {
|
|
|
5075
5119
|
facadeOnSettled: onSettled,
|
|
5076
5120
|
emitSettle: (settlement) => settleFromInteraction(settlement, commitEmitter, settleEmitter),
|
|
5077
5121
|
extraTeardown: () => adapter.reconcileLayerIconResources(previewLayer, []),
|
|
5078
|
-
onAbort: () => {
|
|
5079
|
-
if (wasOnGraphicsLayer) restoreOriginalToGraphics();
|
|
5080
|
-
},
|
|
5081
5122
|
attach(interaction, createdDriver, createdShell) {
|
|
5082
5123
|
ctx = interaction;
|
|
5083
5124
|
driver = createdDriver;
|
|
5084
5125
|
shell = createdShell;
|
|
5085
|
-
if (wasOnGraphicsLayer) adapter.removeFeatures(graphicsLayer, [`${symbol.id}:point-symbol`]);
|
|
5086
5126
|
render();
|
|
5127
|
+
if (wasOnGraphicsLayer) {
|
|
5128
|
+
adapter.deferLayerRemovalUntilLayerUpdate?.(graphicsLayer, previewLayer);
|
|
5129
|
+
adapter.removeFeatures(graphicsLayer, [`${symbol.id}:point-symbol`]);
|
|
5130
|
+
}
|
|
5087
5131
|
onSession?.(session);
|
|
5088
5132
|
}
|
|
5089
5133
|
});
|
|
@@ -5205,7 +5249,7 @@ var TacticalDraw = class {
|
|
|
5205
5249
|
* Routes every measure through [[renderMeasureForAdapter]] so pixel sizing is
|
|
5206
5250
|
* resolved against the live map. Callers own any edit-exclusion policy — see
|
|
5207
5251
|
* `render()` (excludes the edited measure, owned by the preview layer) versus
|
|
5208
|
-
* `
|
|
5252
|
+
* `settleToGraphics` (no exclusion, the edit is ending).
|
|
5209
5253
|
*/
|
|
5210
5254
|
renderMeasuresToGraphics(graphics) {
|
|
5211
5255
|
const features = [];
|
|
@@ -5501,7 +5545,12 @@ var TacticalDraw = class {
|
|
|
5501
5545
|
const preempt = this.guardAndPreempt();
|
|
5502
5546
|
if (preempt) return preempt;
|
|
5503
5547
|
const wasOnGraphicsLayer = this.lastRenderedMeasures.some((m) => m.id === measure.id);
|
|
5504
|
-
const
|
|
5548
|
+
const settleToGraphics = (settledSymbol) => {
|
|
5549
|
+
if (!wasOnGraphicsLayer) return;
|
|
5550
|
+
const graphics = settledSymbol ? this.lastRenderedMeasures.map((graphic) => graphic.id === measure.id ? settledSymbol : graphic) : this.lastRenderedMeasures;
|
|
5551
|
+
this.renderMeasuresToGraphics(graphics);
|
|
5552
|
+
this.adapter.deferLayerRemovalUntilLayerUpdate?.(this.previewLayer, this.graphicsLayer);
|
|
5553
|
+
};
|
|
5505
5554
|
const handleIdPrefix = `__td_edit_${++this.editCounter}__`;
|
|
5506
5555
|
return this.runWithSlot((onSettled) => runPointSymbolEdit({
|
|
5507
5556
|
adapter: this.adapter,
|
|
@@ -5516,7 +5565,7 @@ var TacticalDraw = class {
|
|
|
5516
5565
|
interactionStyle: this.resolveInteractionStyle(options?.interactionStyle),
|
|
5517
5566
|
onSession: options?.onSession,
|
|
5518
5567
|
handleIdPrefix,
|
|
5519
|
-
|
|
5568
|
+
settleToGraphics,
|
|
5520
5569
|
wasOnGraphicsLayer,
|
|
5521
5570
|
onSettled,
|
|
5522
5571
|
onClickAwayGraphicsPick: (event) => this.dispatchHostPick(this.graphicsLayer, event),
|
|
@@ -5532,8 +5581,11 @@ var TacticalDraw = class {
|
|
|
5532
5581
|
const preempt = this.guardAndPreempt();
|
|
5533
5582
|
if (preempt) return preempt;
|
|
5534
5583
|
const wasOnGraphicsLayer = this.lastRenderedMeasures.some((m) => m.id === measure.id);
|
|
5535
|
-
const
|
|
5536
|
-
|
|
5584
|
+
const settleToGraphics = (settledMeasure) => {
|
|
5585
|
+
if (!wasOnGraphicsLayer) return;
|
|
5586
|
+
const graphics = settledMeasure ? this.lastRenderedMeasures.map((graphic) => graphic.id === measure.id ? settledMeasure : graphic) : this.lastRenderedMeasures;
|
|
5587
|
+
this.renderMeasuresToGraphics(graphics);
|
|
5588
|
+
this.adapter.deferLayerRemovalUntilLayerUpdate?.(this.previewLayer, this.graphicsLayer);
|
|
5537
5589
|
};
|
|
5538
5590
|
const handleIdPrefix = `__td_edit_${++this.editCounter}__`;
|
|
5539
5591
|
const mergedInteractionStyle = this.resolveInteractionStyle(options?.interactionStyle);
|
|
@@ -5557,7 +5609,7 @@ var TacticalDraw = class {
|
|
|
5557
5609
|
handleIdPrefix,
|
|
5558
5610
|
guide: options?.guide,
|
|
5559
5611
|
sizeAnchor: options?.sizeAnchor,
|
|
5560
|
-
|
|
5612
|
+
settleToGraphics,
|
|
5561
5613
|
wasOnGraphicsLayer,
|
|
5562
5614
|
onSettled,
|
|
5563
5615
|
onClickAwayGraphicsPick,
|
|
@@ -6041,6 +6093,141 @@ function resolveSlot(adapter, supplied, owned) {
|
|
|
6041
6093
|
return id;
|
|
6042
6094
|
}
|
|
6043
6095
|
//#endregion
|
|
6096
|
+
//#region src/fill-pattern.ts
|
|
6097
|
+
/**
|
|
6098
|
+
* Every non-solid `FillPattern` as a runtime list — the value-level companion
|
|
6099
|
+
* to {@link HatchPattern}, so adapters can test membership and build filter
|
|
6100
|
+
* expressions without re-spelling the pattern vocabulary.
|
|
6101
|
+
*/
|
|
6102
|
+
const HATCH_PATTERNS = ["hachure", "cross"];
|
|
6103
|
+
/** Tile edge length, CSS px, before `pixelRatio` scaling. Kept a power of two. */
|
|
6104
|
+
const BASE_TILE_SIZE = 32;
|
|
6105
|
+
/** Line spacing as a fraction of the tile edge — 4 repeats per tile. */
|
|
6106
|
+
const SPACING_DIVISOR = 4;
|
|
6107
|
+
/**
|
|
6108
|
+
* Stable id for a rasterized tile, keyed by pattern and color. Adapters use
|
|
6109
|
+
* this as the image-registry key (MapLibre `addImage` id, OpenLayers pattern
|
|
6110
|
+
* cache key) so the same color+pattern never gets rasterized twice. Color is
|
|
6111
|
+
* lowercased so `"#FF0000"` and `"#ff0000"` share one texture.
|
|
6112
|
+
*/
|
|
6113
|
+
function fillPatternImageId(color, pattern) {
|
|
6114
|
+
return `td-hatch-${pattern}-${color.trim().toLowerCase()}`;
|
|
6115
|
+
}
|
|
6116
|
+
/**
|
|
6117
|
+
* Tile edge length in device pixels for a given `pixelRatio`. Must be a power
|
|
6118
|
+
* of two — MapLibre requires POT dimensions for seamless WebGL texture wrap —
|
|
6119
|
+
* and scales linearly with `pixelRatio` for the ratios that matter in
|
|
6120
|
+
* practice (1x/2x displays: 32 -> 64, both already POT). Non-POT products
|
|
6121
|
+
* (e.g. a 3x ratio, 32*3=96) round up to the next POT rather than losing the
|
|
6122
|
+
* power-of-two guarantee.
|
|
6123
|
+
*/
|
|
6124
|
+
function fillPatternImageDimension(pixelRatio) {
|
|
6125
|
+
const target = BASE_TILE_SIZE * Math.max(1, Math.round(pixelRatio));
|
|
6126
|
+
return 2 ** Math.ceil(Math.log2(target));
|
|
6127
|
+
}
|
|
6128
|
+
/**
|
|
6129
|
+
* Parse a hex (`#rgb`, `#rrggbb`, `#rrggbbaa`) or `rgb()`/`rgba()` color
|
|
6130
|
+
* string into normalized channels, alpha in `[0, 1]`. Unparseable input falls
|
|
6131
|
+
* back to opaque black rather than throwing, since a mis-themed fill should
|
|
6132
|
+
* still render *something*.
|
|
6133
|
+
*/
|
|
6134
|
+
function parseColor(color) {
|
|
6135
|
+
const s = color.trim();
|
|
6136
|
+
if (s.startsWith("#")) {
|
|
6137
|
+
const hex = s.slice(1);
|
|
6138
|
+
if (hex.length === 3) return {
|
|
6139
|
+
r: parseInt(hex[0] + hex[0], 16),
|
|
6140
|
+
g: parseInt(hex[1] + hex[1], 16),
|
|
6141
|
+
b: parseInt(hex[2] + hex[2], 16),
|
|
6142
|
+
a: 1
|
|
6143
|
+
};
|
|
6144
|
+
if (hex.length === 6) return {
|
|
6145
|
+
r: parseInt(hex.slice(0, 2), 16),
|
|
6146
|
+
g: parseInt(hex.slice(2, 4), 16),
|
|
6147
|
+
b: parseInt(hex.slice(4, 6), 16),
|
|
6148
|
+
a: 1
|
|
6149
|
+
};
|
|
6150
|
+
if (hex.length === 8) return {
|
|
6151
|
+
r: parseInt(hex.slice(0, 2), 16),
|
|
6152
|
+
g: parseInt(hex.slice(2, 4), 16),
|
|
6153
|
+
b: parseInt(hex.slice(4, 6), 16),
|
|
6154
|
+
a: parseInt(hex.slice(6, 8), 16) / 255
|
|
6155
|
+
};
|
|
6156
|
+
}
|
|
6157
|
+
const fn = s.match(/^rgba?\(([^)]+)\)$/i);
|
|
6158
|
+
if (fn) {
|
|
6159
|
+
const parts = fn[1].split(",").map((p) => p.trim());
|
|
6160
|
+
const r = Number.parseFloat(parts[0] ?? "0");
|
|
6161
|
+
const g = Number.parseFloat(parts[1] ?? "0");
|
|
6162
|
+
const b = Number.parseFloat(parts[2] ?? "0");
|
|
6163
|
+
const a = parts.length > 3 ? Number.parseFloat(parts[3]) : 1;
|
|
6164
|
+
return {
|
|
6165
|
+
r,
|
|
6166
|
+
g,
|
|
6167
|
+
b,
|
|
6168
|
+
a: Number.isFinite(a) ? a : 1
|
|
6169
|
+
};
|
|
6170
|
+
}
|
|
6171
|
+
return {
|
|
6172
|
+
r: 0,
|
|
6173
|
+
g: 0,
|
|
6174
|
+
b: 0,
|
|
6175
|
+
a: 1
|
|
6176
|
+
};
|
|
6177
|
+
}
|
|
6178
|
+
/** `v mod m`, folded into `[-m/2, m/2)`. Used to find distance to the nearest line in a periodic family. */
|
|
6179
|
+
function centeredMod(v, m) {
|
|
6180
|
+
const wrapped = (v % m + m) % m;
|
|
6181
|
+
return wrapped >= m / 2 ? wrapped - m : wrapped;
|
|
6182
|
+
}
|
|
6183
|
+
const SQRT2 = Math.SQRT2;
|
|
6184
|
+
/**
|
|
6185
|
+
* Coverage in `[0, 1]` of one diagonal line family (slope +1 or -1, both
|
|
6186
|
+
* period `spacing`) at pixel center `(x, y)`, antialiased over a ~1 device-px
|
|
6187
|
+
* band. `axis` selects `y - x` (the "/" hachure) or `y + x` (its mirror, added
|
|
6188
|
+
* for "cross"). Because `spacing` divides the tile dimension evenly, shifting
|
|
6189
|
+
* `x` or `y` by a whole tile is a whole number of `spacing` periods, so the
|
|
6190
|
+
* mod-distance — and therefore the coverage — is identical at the tile edges
|
|
6191
|
+
* as at their wrapped continuation. That periodicity is what makes the tile
|
|
6192
|
+
* seamless.
|
|
6193
|
+
*/
|
|
6194
|
+
function lineCoverage(x, y, spacing, lineWidth, axis) {
|
|
6195
|
+
const t = axis === "/" ? y - x : y + x;
|
|
6196
|
+
const perpDist = Math.abs(centeredMod(t, spacing)) / SQRT2;
|
|
6197
|
+
const halfWidth = lineWidth / 2;
|
|
6198
|
+
return Math.min(1, Math.max(0, halfWidth + .5 - perpDist));
|
|
6199
|
+
}
|
|
6200
|
+
/**
|
|
6201
|
+
* Rasterize one seamless hatch tile as a straight RGBA pixel buffer — no
|
|
6202
|
+
* canvas involved, so this runs identically in a browser adapter and in
|
|
6203
|
+
* jsdom/node tests. Diagonal lines are snapped to slope ±1 (45°) so the
|
|
6204
|
+
* mod-based coverage math in {@link lineCoverage} tiles exactly; `"cross"`
|
|
6205
|
+
* unions both diagonals via `Math.max` coverage.
|
|
6206
|
+
*/
|
|
6207
|
+
function makeFillPatternImage(color, pattern, pixelRatio = 1) {
|
|
6208
|
+
const size = fillPatternImageDimension(pixelRatio);
|
|
6209
|
+
const spacing = size / SPACING_DIVISOR;
|
|
6210
|
+
const lineWidth = Math.max(1, Math.round(pixelRatio));
|
|
6211
|
+
const { r, g, b, a } = parseColor(color);
|
|
6212
|
+
const data = new Uint8ClampedArray(size * size * 4);
|
|
6213
|
+
for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) {
|
|
6214
|
+
const cx = x + .5;
|
|
6215
|
+
const cy = y + .5;
|
|
6216
|
+
let coverage = lineCoverage(cx, cy, spacing, lineWidth, "/");
|
|
6217
|
+
if (pattern === "cross") coverage = Math.max(coverage, lineCoverage(cx, cy, spacing, lineWidth, "\\"));
|
|
6218
|
+
const i = (y * size + x) * 4;
|
|
6219
|
+
data[i] = r;
|
|
6220
|
+
data[i + 1] = g;
|
|
6221
|
+
data[i + 2] = b;
|
|
6222
|
+
data[i + 3] = Math.round(255 * a * coverage);
|
|
6223
|
+
}
|
|
6224
|
+
return {
|
|
6225
|
+
width: size,
|
|
6226
|
+
height: size,
|
|
6227
|
+
data
|
|
6228
|
+
};
|
|
6229
|
+
}
|
|
6230
|
+
//#endregion
|
|
6044
6231
|
//#region src/world-wrap.ts
|
|
6045
6232
|
/**
|
|
6046
6233
|
* Shift `lng` by whole world widths (360°) onto the copy nearest `refLng`.
|
|
@@ -6059,4 +6246,4 @@ function nearestWorldCopyPosition(position, refLng) {
|
|
|
6059
6246
|
return [nearestWorldCopyLng(position[0], refLng), ...position.slice(1)];
|
|
6060
6247
|
}
|
|
6061
6248
|
//#endregion
|
|
6062
|
-
export { BaseMapAdapter, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, PointSymbolCapabilityRequiredError, PointerCallbackHub, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, TacticalDrawDestroyedError, bindAbortable, clonePointSymbol, coerceHandleStyle, combineWithHostSignal, effectivePointSymbolPixelSize, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, isPointSymbol, isTacticalDrawAbortError, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, pointSymbolFootprint, resizePointSymbolSize, resolveGroundTextSizePixels, tryControlMeasureIdFromFeature };
|
|
6249
|
+
export { BaseMapAdapter, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, HATCH_PATTERNS, PointSymbolCapabilityRequiredError, PointerCallbackHub, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, TacticalDrawDestroyedError, bindAbortable, clonePointSymbol, coerceHandleStyle, combineWithHostSignal, effectivePointSymbolPixelSize, fillPatternImageDimension, fillPatternImageId, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, isPointSymbol, isTacticalDrawAbortError, makeFillPatternImage, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, pointSymbolFootprint, resizePointSymbolSize, resolveGroundTextSizePixels, tryControlMeasureIdFromFeature };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orbat-mapper/tactical-draw",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.28",
|
|
4
4
|
"description": "Engine-agnostic draw/edit core for tactical control measures (MIL-STD-2525 / APP-6). The MapAdapter ABI and bring-your-own-engine entry point for the tactical-draw package family.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Orbat Mapper",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"access": "public"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@orbat-mapper/control-measures": "^0.2.0-alpha.
|
|
41
|
+
"@orbat-mapper/control-measures": "^0.2.0-alpha.20"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@arethetypeswrong/core": "^0.18.3",
|