@orbat-mapper/tactical-draw 0.2.0-alpha.27 → 0.2.0-alpha.29

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 { 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,52 @@ 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. `"hachure"`
80
+ * draws the "/" diagonal, `"backhachure"` its "\" mirror, and `"cross"` unions
81
+ * both via `Math.max` coverage.
82
+ */
83
+ declare function makeFillPatternImage(color: string, pattern: HatchPattern, pixelRatio?: number): {
84
+ width: number;
85
+ height: number;
86
+ data: Uint8ClampedArray;
87
+ };
88
+ //#endregion
43
89
  //#region src/map-adapter.d.ts
44
90
  type PixelCoordinate = [number, number];
45
91
  type MapCursor = "" | "default" | "crosshair" | "grab" | "grabbing" | "pointer";
@@ -102,6 +148,14 @@ interface HandleStyle {
102
148
  strokeWidth?: number;
103
149
  strokeDash?: readonly number[];
104
150
  fillColor?: string;
151
+ /**
152
+ * How `fillColor` paints a filled part's interior. Absent or `"solid"` is
153
+ * today's flat fill; `"hachure"`/`"backhachure"`/`"cross"` request the shared hatch texture
154
+ * ({@link makeFillPatternImage}). Alpha rides in `fillColor` — there is no
155
+ * separate opacity field. Adapters that cannot rasterize a pattern degrade
156
+ * to solid. Per-feature only: layer-level `StyleOptions` has no equivalent.
157
+ */
158
+ fillPattern?: "solid" | HatchPattern;
105
159
  pointRadius?: number;
106
160
  }
107
161
  type LayerId = string;
@@ -1910,4 +1964,4 @@ declare function nearestWorldCopyLng(lng: number, refLng: number): number;
1910
1964
  /** `nearestWorldCopyLng` applied to a lon/lat position. */
1911
1965
  declare function nearestWorldCopyPosition(position: Position, refLng: number): Position;
1912
1966
  //#endregion
1913
- 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 };
1967
+ 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
@@ -6093,6 +6093,147 @@ function resolveSlot(adapter, supplied, owned) {
6093
6093
  return id;
6094
6094
  }
6095
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 = [
6103
+ "hachure",
6104
+ "backhachure",
6105
+ "cross"
6106
+ ];
6107
+ /** Tile edge length, CSS px, before `pixelRatio` scaling. Kept a power of two. */
6108
+ const BASE_TILE_SIZE = 32;
6109
+ /** Line spacing as a fraction of the tile edge — 4 repeats per tile. */
6110
+ const SPACING_DIVISOR = 4;
6111
+ /**
6112
+ * Stable id for a rasterized tile, keyed by pattern and color. Adapters use
6113
+ * this as the image-registry key (MapLibre `addImage` id, OpenLayers pattern
6114
+ * cache key) so the same color+pattern never gets rasterized twice. Color is
6115
+ * lowercased so `"#FF0000"` and `"#ff0000"` share one texture.
6116
+ */
6117
+ function fillPatternImageId(color, pattern) {
6118
+ return `td-hatch-${pattern}-${color.trim().toLowerCase()}`;
6119
+ }
6120
+ /**
6121
+ * Tile edge length in device pixels for a given `pixelRatio`. Must be a power
6122
+ * of two — MapLibre requires POT dimensions for seamless WebGL texture wrap —
6123
+ * and scales linearly with `pixelRatio` for the ratios that matter in
6124
+ * practice (1x/2x displays: 32 -> 64, both already POT). Non-POT products
6125
+ * (e.g. a 3x ratio, 32*3=96) round up to the next POT rather than losing the
6126
+ * power-of-two guarantee.
6127
+ */
6128
+ function fillPatternImageDimension(pixelRatio) {
6129
+ const target = BASE_TILE_SIZE * Math.max(1, Math.round(pixelRatio));
6130
+ return 2 ** Math.ceil(Math.log2(target));
6131
+ }
6132
+ /**
6133
+ * Parse a hex (`#rgb`, `#rrggbb`, `#rrggbbaa`) or `rgb()`/`rgba()` color
6134
+ * string into normalized channels, alpha in `[0, 1]`. Unparseable input falls
6135
+ * back to opaque black rather than throwing, since a mis-themed fill should
6136
+ * still render *something*.
6137
+ */
6138
+ function parseColor(color) {
6139
+ const s = color.trim();
6140
+ if (s.startsWith("#")) {
6141
+ const hex = s.slice(1);
6142
+ if (hex.length === 3) return {
6143
+ r: parseInt(hex[0] + hex[0], 16),
6144
+ g: parseInt(hex[1] + hex[1], 16),
6145
+ b: parseInt(hex[2] + hex[2], 16),
6146
+ a: 1
6147
+ };
6148
+ if (hex.length === 6) return {
6149
+ r: parseInt(hex.slice(0, 2), 16),
6150
+ g: parseInt(hex.slice(2, 4), 16),
6151
+ b: parseInt(hex.slice(4, 6), 16),
6152
+ a: 1
6153
+ };
6154
+ if (hex.length === 8) return {
6155
+ r: parseInt(hex.slice(0, 2), 16),
6156
+ g: parseInt(hex.slice(2, 4), 16),
6157
+ b: parseInt(hex.slice(4, 6), 16),
6158
+ a: parseInt(hex.slice(6, 8), 16) / 255
6159
+ };
6160
+ }
6161
+ const fn = s.match(/^rgba?\(([^)]+)\)$/i);
6162
+ if (fn) {
6163
+ const parts = fn[1].split(",").map((p) => p.trim());
6164
+ const r = Number.parseFloat(parts[0] ?? "0");
6165
+ const g = Number.parseFloat(parts[1] ?? "0");
6166
+ const b = Number.parseFloat(parts[2] ?? "0");
6167
+ const a = parts.length > 3 ? Number.parseFloat(parts[3]) : 1;
6168
+ return {
6169
+ r,
6170
+ g,
6171
+ b,
6172
+ a: Number.isFinite(a) ? a : 1
6173
+ };
6174
+ }
6175
+ return {
6176
+ r: 0,
6177
+ g: 0,
6178
+ b: 0,
6179
+ a: 1
6180
+ };
6181
+ }
6182
+ /** `v mod m`, folded into `[-m/2, m/2)`. Used to find distance to the nearest line in a periodic family. */
6183
+ function centeredMod(v, m) {
6184
+ const wrapped = (v % m + m) % m;
6185
+ return wrapped >= m / 2 ? wrapped - m : wrapped;
6186
+ }
6187
+ const SQRT2 = Math.SQRT2;
6188
+ /**
6189
+ * Coverage in `[0, 1]` of one diagonal line family (slope +1 or -1, both
6190
+ * period `spacing`) at pixel center `(x, y)`, antialiased over a ~1 device-px
6191
+ * band. `axis` selects `y - x` (the "/" hachure) or `y + x` (the "\" mirror,
6192
+ * "backhachure", which "cross" unions on top of the hachure). Because `spacing`
6193
+ * divides the tile dimension evenly, shifting
6194
+ * `x` or `y` by a whole tile is a whole number of `spacing` periods, so the
6195
+ * mod-distance — and therefore the coverage — is identical at the tile edges
6196
+ * as at their wrapped continuation. That periodicity is what makes the tile
6197
+ * seamless.
6198
+ */
6199
+ function lineCoverage(x, y, spacing, lineWidth, axis) {
6200
+ const t = axis === "/" ? y - x : y + x;
6201
+ const perpDist = Math.abs(centeredMod(t, spacing)) / SQRT2;
6202
+ const halfWidth = lineWidth / 2;
6203
+ return Math.min(1, Math.max(0, halfWidth + .5 - perpDist));
6204
+ }
6205
+ /**
6206
+ * Rasterize one seamless hatch tile as a straight RGBA pixel buffer — no
6207
+ * canvas involved, so this runs identically in a browser adapter and in
6208
+ * jsdom/node tests. Diagonal lines are snapped to slope ±1 (45°) so the
6209
+ * mod-based coverage math in {@link lineCoverage} tiles exactly. `"hachure"`
6210
+ * draws the "/" diagonal, `"backhachure"` its "\" mirror, and `"cross"` unions
6211
+ * both via `Math.max` coverage.
6212
+ */
6213
+ function makeFillPatternImage(color, pattern, pixelRatio = 1) {
6214
+ const size = fillPatternImageDimension(pixelRatio);
6215
+ const spacing = size / SPACING_DIVISOR;
6216
+ const lineWidth = Math.max(1, Math.round(pixelRatio));
6217
+ const { r, g, b, a } = parseColor(color);
6218
+ const data = new Uint8ClampedArray(size * size * 4);
6219
+ for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) {
6220
+ const cx = x + .5;
6221
+ const cy = y + .5;
6222
+ let coverage = pattern === "backhachure" ? lineCoverage(cx, cy, spacing, lineWidth, "\\") : lineCoverage(cx, cy, spacing, lineWidth, "/");
6223
+ if (pattern === "cross") coverage = Math.max(coverage, lineCoverage(cx, cy, spacing, lineWidth, "\\"));
6224
+ const i = (y * size + x) * 4;
6225
+ data[i] = r;
6226
+ data[i + 1] = g;
6227
+ data[i + 2] = b;
6228
+ data[i + 3] = Math.round(255 * a * coverage);
6229
+ }
6230
+ return {
6231
+ width: size,
6232
+ height: size,
6233
+ data
6234
+ };
6235
+ }
6236
+ //#endregion
6096
6237
  //#region src/world-wrap.ts
6097
6238
  /**
6098
6239
  * Shift `lng` by whole world widths (360°) onto the copy nearest `refLng`.
@@ -6111,4 +6252,4 @@ function nearestWorldCopyPosition(position, refLng) {
6111
6252
  return [nearestWorldCopyLng(position[0], refLng), ...position.slice(1)];
6112
6253
  }
6113
6254
  //#endregion
6114
- 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 };
6255
+ 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.27",
3
+ "version": "0.2.0-alpha.29",
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.16"
41
+ "@orbat-mapper/control-measures": "^0.2.0-alpha.21"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@arethetypeswrong/core": "^0.18.3",