@orbat-mapper/tactical-draw 0.2.0-alpha.30 → 0.2.0-alpha.31
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 +33 -1
- package/dist/index.mjs +238 -35
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -474,6 +474,38 @@ declare const DEFAULT_TEXT_SIZE_MAX_PIXELS = 24;
|
|
|
474
474
|
* clamp inside its per-zoom-stop style expression.
|
|
475
475
|
*/
|
|
476
476
|
declare function resolveGroundTextSizePixels(textSizeMeters: number, resolution: number, text?: Pick<TextStyleOptions, "textSizeMinPixels" | "textSizeMaxPixels">): number;
|
|
477
|
+
/**
|
|
478
|
+
* The size properties a Text graphic's feature carries (see `text.ts` /
|
|
479
|
+
* `FeaturePartProps`). Mutually exclusive size anchors: `textSizePixels`
|
|
480
|
+
* (screen-anchored) or `textSizeMeters` (ground-anchored). `textMaxSizePixels`
|
|
481
|
+
* is the ADR-0037 hide threshold; the min/max band fields are the amplifier
|
|
482
|
+
* -label clamp bounds, applied only when no max-size threshold is present.
|
|
483
|
+
*/
|
|
484
|
+
interface TextSizeProps {
|
|
485
|
+
textSizeMeters?: number;
|
|
486
|
+
textSizePixels?: number;
|
|
487
|
+
textMaxSizePixels?: number;
|
|
488
|
+
textSizeMinPixels?: number;
|
|
489
|
+
textSizeMaxPixels?: number;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Resolve a Text graphic's on-screen size in pixels, honoring the
|
|
493
|
+
* `textMaxSizePixels` hide threshold (ADR-0037). Returns `null` — the "hidden"
|
|
494
|
+
* signal — when the graphic should render no text this frame; adapters emit no
|
|
495
|
+
* text style for a `null`. Distinct from [[resolveGroundTextSizePixels]], which
|
|
496
|
+
* keeps the 8–24 px clamp band for amplifier labels: this helper is the Text
|
|
497
|
+
* graphic's own sizing rule.
|
|
498
|
+
*
|
|
499
|
+
* - screen-anchored (`textSizePixels`): the fixed pixel size, never scaled or
|
|
500
|
+
* hidden by zoom.
|
|
501
|
+
* - ground-anchored (`textSizeMeters`) with a `textMaxSizePixels` threshold:
|
|
502
|
+
* *unclamped* `meters / resolution` (the 8–24 band does NOT apply), hidden
|
|
503
|
+
* (`null`) once that exceeds the threshold — text shrinks freely zooming out
|
|
504
|
+
* and vanishes rather than clamps zooming in.
|
|
505
|
+
* - ground-anchored without a threshold: falls back to the clamped band.
|
|
506
|
+
* - no usable size or resolution: `null`.
|
|
507
|
+
*/
|
|
508
|
+
declare function resolveTextSizePixels(props: TextSizeProps, resolution: number): number | null;
|
|
477
509
|
//#endregion
|
|
478
510
|
//#region src/gestures/hit-test.d.ts
|
|
479
511
|
type ToPixel = (lonLat: Position) => PixelCoordinate | null;
|
|
@@ -2031,4 +2063,4 @@ declare function nearestWorldCopyLng(lng: number, refLng: number): number;
|
|
|
2031
2063
|
/** `nearestWorldCopyLng` applied to a lon/lat position. */
|
|
2032
2064
|
declare function nearestWorldCopyPosition(position: Position, refLng: number): Position;
|
|
2033
2065
|
//#endregion
|
|
2034
|
-
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, type TransientChangeEvent, type TransientPhase, bindAbortable, clonePointSymbol, coerceHandleStyle, combineWithHostSignal, effectivePointSymbolPixelSize, fillPatternImageDimension, fillPatternImageId, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, isPointSymbol, isTacticalDrawAbortError, makeFillPatternImage, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, pointSymbolFootprint, resizePointSymbolSize, resolveGroundTextSizePixels, tryControlMeasureIdFromFeature };
|
|
2066
|
+
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 TextSizeProps, type TextStyleOptions, type ToPixel, type TransformBox, type TransformChangeEvent, type TransformSession, type TransientChangeEvent, type TransientPhase, bindAbortable, clonePointSymbol, coerceHandleStyle, combineWithHostSignal, effectivePointSymbolPixelSize, fillPatternImageDimension, fillPatternImageId, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, isPointSymbol, isTacticalDrawAbortError, makeFillPatternImage, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, pointSymbolFootprint, resizePointSymbolSize, resolveGroundTextSizePixels, resolveTextSizePixels, tryControlMeasureIdFromFeature };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { canonicalTextAmplifierKey, cloneControlMeasure, controlMeasureIdFromFeature, freezeOrientationOptions, getControlMeasureMetadata, renderControlMeasure, resolveAmplifierPlacement, resolveStyleHints, roundToFixed } from "@orbat-mapper/control-measures";
|
|
1
|
+
import { applyBoxTransformOptions, canonicalTextAmplifierKey, cloneControlMeasure, controlMeasureIdFromFeature, freezeOrientationOptions, getControlMeasureMetadata, getDefaultOptions, renderControlMeasure, resolveAmplifierPlacement, resolveStyleHints, roundToFixed } from "@orbat-mapper/control-measures";
|
|
2
2
|
//#region src/base-map-adapter.ts
|
|
3
3
|
/**
|
|
4
4
|
* Abstract base class shared by all `MapAdapter` implementations.
|
|
@@ -296,6 +296,33 @@ function resolveGroundTextSizePixels(textSizeMeters, resolution, text) {
|
|
|
296
296
|
const max = text?.textSizeMaxPixels ?? 24;
|
|
297
297
|
return Math.min(max, Math.max(min, textSizeMeters / resolution));
|
|
298
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Resolve a Text graphic's on-screen size in pixels, honoring the
|
|
301
|
+
* `textMaxSizePixels` hide threshold (ADR-0037). Returns `null` — the "hidden"
|
|
302
|
+
* signal — when the graphic should render no text this frame; adapters emit no
|
|
303
|
+
* text style for a `null`. Distinct from [[resolveGroundTextSizePixels]], which
|
|
304
|
+
* keeps the 8–24 px clamp band for amplifier labels: this helper is the Text
|
|
305
|
+
* graphic's own sizing rule.
|
|
306
|
+
*
|
|
307
|
+
* - screen-anchored (`textSizePixels`): the fixed pixel size, never scaled or
|
|
308
|
+
* hidden by zoom.
|
|
309
|
+
* - ground-anchored (`textSizeMeters`) with a `textMaxSizePixels` threshold:
|
|
310
|
+
* *unclamped* `meters / resolution` (the 8–24 band does NOT apply), hidden
|
|
311
|
+
* (`null`) once that exceeds the threshold — text shrinks freely zooming out
|
|
312
|
+
* and vanishes rather than clamps zooming in.
|
|
313
|
+
* - ground-anchored without a threshold: falls back to the clamped band.
|
|
314
|
+
* - no usable size or resolution: `null`.
|
|
315
|
+
*/
|
|
316
|
+
function resolveTextSizePixels(props, resolution) {
|
|
317
|
+
if (typeof props.textSizePixels === "number") return props.textSizePixels;
|
|
318
|
+
const meters = props.textSizeMeters;
|
|
319
|
+
if (typeof meters !== "number" || !(resolution > 0)) return null;
|
|
320
|
+
if (typeof props.textMaxSizePixels === "number") {
|
|
321
|
+
const px = meters / resolution;
|
|
322
|
+
return px > props.textMaxSizePixels ? null : px;
|
|
323
|
+
}
|
|
324
|
+
return resolveGroundTextSizePixels(meters, resolution, props);
|
|
325
|
+
}
|
|
299
326
|
//#endregion
|
|
300
327
|
//#region src/gestures/hit-test.ts
|
|
301
328
|
/** Clamped parameter t ∈ [0, 1] of `pixel` projected onto the segment a→b in pixel space, or null for a zero-length segment. */
|
|
@@ -684,6 +711,10 @@ const PIXEL_SIZE_OPTIONS = {
|
|
|
684
711
|
"antitank-wall": {
|
|
685
712
|
pixels: "toothHeightPixels",
|
|
686
713
|
meters: "toothHeight"
|
|
714
|
+
},
|
|
715
|
+
text: {
|
|
716
|
+
pixels: "sizePixels",
|
|
717
|
+
meters: "sizeMeters"
|
|
687
718
|
}
|
|
688
719
|
};
|
|
689
720
|
/** The shared label-size pair every `capturesLabelSize` kind carries (ADR-0028). */
|
|
@@ -745,6 +776,34 @@ function getSizeAnchor(measure) {
|
|
|
745
776
|
return measureUsesAdapterResolution(measure) ? "screen" : "ground";
|
|
746
777
|
}
|
|
747
778
|
/**
|
|
779
|
+
* Materialise a *pixel-authored* kind's default pixel size onto a measure that
|
|
780
|
+
* carries neither size key, so the ADR-0020 ground bake has a pixel value to
|
|
781
|
+
* resolve to meters at commit. A kind is pixel-authored when its default
|
|
782
|
+
* options declare the pixel-size option but no meter counterpart (e.g. Text's
|
|
783
|
+
* `sizePixels: 24` with no `sizeMeters`) — such a fresh draw is authored in
|
|
784
|
+
* pixels and must bake to meters, but a default draw commits with no options,
|
|
785
|
+
* so the pixel size would otherwise never land and the graphic would persist
|
|
786
|
+
* screen-anchored forever. No-op when the measure already carries a size key,
|
|
787
|
+
* the kind has no pixel-size option, or the kind's default is meter-denominated
|
|
788
|
+
* (e.g. FLOT, whose meter default governs a default draw without any bake) —
|
|
789
|
+
* so this never changes the committed size of the pre-existing pixel kinds.
|
|
790
|
+
*/
|
|
791
|
+
function withDefaultAuthoringSize(measure) {
|
|
792
|
+
const spec = PIXEL_SIZE_OPTIONS[measure.kind];
|
|
793
|
+
if (!spec) return measure;
|
|
794
|
+
const options = measure.options ?? {};
|
|
795
|
+
if (options[spec.pixels] !== void 0 || options[spec.meters] !== void 0) return measure;
|
|
796
|
+
const defaults = getDefaultOptions(measure.kind);
|
|
797
|
+
if (defaults[spec.meters] !== void 0 || defaults[spec.pixels] === void 0) return measure;
|
|
798
|
+
return {
|
|
799
|
+
...measure,
|
|
800
|
+
options: {
|
|
801
|
+
...options,
|
|
802
|
+
[spec.pixels]: defaults[spec.pixels]
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
748
807
|
* Bake a screen-anchored (pixel) size to a ground-anchored (meter) size at the
|
|
749
808
|
* adapter's current resolution, then strip the pixel + `metersPerPixel` keys so
|
|
750
809
|
* the result is an ordinary meters graphic carrying no pixel intent — afterward
|
|
@@ -1535,7 +1594,7 @@ function runFixedLengthDraw(options) {
|
|
|
1535
1594
|
}
|
|
1536
1595
|
function buildSnapshot() {
|
|
1537
1596
|
const finalPoints = roundControlPoints(rule ? rule.derive(committed) : committed);
|
|
1538
|
-
const
|
|
1597
|
+
const sized = withDefaultAuthoringSize(withCommittedAdapterMeasureOptions({
|
|
1539
1598
|
id: generateId(),
|
|
1540
1599
|
kind,
|
|
1541
1600
|
controlPoints: finalPoints,
|
|
@@ -1543,8 +1602,8 @@ function runFixedLengthDraw(options) {
|
|
|
1543
1602
|
...measureStyle !== void 0 ? { style: measureStyle } : {},
|
|
1544
1603
|
...measureProperties !== void 0 ? { properties: measureProperties } : {},
|
|
1545
1604
|
...measureTextAmplifiers !== void 0 ? { textAmplifiers: measureTextAmplifiers } : {}
|
|
1546
|
-
});
|
|
1547
|
-
const measure = sizeAnchor === "screen" ?
|
|
1605
|
+
}));
|
|
1606
|
+
const measure = sizeAnchor === "screen" ? sized : bakePixelSizeToGround(sized, adapter);
|
|
1548
1607
|
return {
|
|
1549
1608
|
graphic: cloneControlMeasure(measure),
|
|
1550
1609
|
render: renderMeasureForAdapter(adapter, measure, renderOpts)
|
|
@@ -1815,7 +1874,7 @@ function runVariableLengthDraw(options) {
|
|
|
1815
1874
|
renderPreview();
|
|
1816
1875
|
}
|
|
1817
1876
|
function buildSnapshot() {
|
|
1818
|
-
const
|
|
1877
|
+
const sized = withDefaultAuthoringSize(withCommittedAdapterMeasureOptions({
|
|
1819
1878
|
id: generateId(),
|
|
1820
1879
|
kind,
|
|
1821
1880
|
controlPoints: committed.map((p) => [...p]),
|
|
@@ -1823,8 +1882,8 @@ function runVariableLengthDraw(options) {
|
|
|
1823
1882
|
...measureStyle !== void 0 ? { style: measureStyle } : {},
|
|
1824
1883
|
...measureProperties !== void 0 ? { properties: measureProperties } : {},
|
|
1825
1884
|
...measureTextAmplifiers !== void 0 ? { textAmplifiers: measureTextAmplifiers } : {}
|
|
1826
|
-
});
|
|
1827
|
-
const measure = sizeAnchor === "screen" ?
|
|
1885
|
+
}));
|
|
1886
|
+
const measure = sizeAnchor === "screen" ? sized : bakePixelSizeToGround(sized, adapter);
|
|
1828
1887
|
return {
|
|
1829
1888
|
graphic: cloneControlMeasure(measure),
|
|
1830
1889
|
render: renderMeasureForAdapter(adapter, measure, renderOpts)
|
|
@@ -2182,6 +2241,21 @@ function normalizeRotation(angle) {
|
|
|
2182
2241
|
function constrainRotation(angle) {
|
|
2183
2242
|
return Math.round(angle / CONSTRAINED_ROTATION_STEP) * CONSTRAINED_ROTATION_STEP;
|
|
2184
2243
|
}
|
|
2244
|
+
//#endregion
|
|
2245
|
+
//#region src/tactical-draw/transform-box.ts
|
|
2246
|
+
/**
|
|
2247
|
+
* The uniform scale factor and rotation delta a box implies relative to a
|
|
2248
|
+
* pre-drag box, given the gesture kind. A `body` translate reports neither
|
|
2249
|
+
* (1, 0). Consumed both by point symbols (size/rotation) and by Text graphics
|
|
2250
|
+
* (folding the delta into rotation/size options); the delta is pivot-
|
|
2251
|
+
* independent so either can apply it without knowing the box center.
|
|
2252
|
+
*/
|
|
2253
|
+
function boxScaleRotation(kind, box, preDragBox) {
|
|
2254
|
+
return {
|
|
2255
|
+
scale: kind === "scale" ? Math.max(box.halfWidth / preDragBox.halfWidth, box.halfHeight / preDragBox.halfHeight) : 1,
|
|
2256
|
+
rotation: kind === "rotate" ? box.angle - preDragBox.angle : 0
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2185
2259
|
function rotate(x, y, angle) {
|
|
2186
2260
|
const cos = Math.cos(angle);
|
|
2187
2261
|
const sin = Math.sin(angle);
|
|
@@ -2720,12 +2794,16 @@ function normalizeAngle(angle) {
|
|
|
2720
2794
|
* grows away from the anchor) or local −x for `"end"` (text grows back
|
|
2721
2795
|
* toward the anchor) — `rotate()` (`transform-box.ts`) already uses the
|
|
2722
2796
|
* screen (y-down) convention this offset needs.
|
|
2797
|
+
* - **Hidden**: a ground-anchored size past `textMaxSizePixels` (ADR-0037)
|
|
2798
|
+
* returns `null` — the box tracks `resolveTextSizePixels`'s hide rule
|
|
2799
|
+
* rather than keep boxing text that no adapter is drawing.
|
|
2723
2800
|
*/
|
|
2724
2801
|
function fitLabelTransformBox(input) {
|
|
2725
|
-
const { text, rotation, textAnchor, textSizePixels, textSizeMeters, anchor, resolution, bearing } = input;
|
|
2802
|
+
const { text, rotation, textAnchor, textSizePixels, textSizeMeters, anchor, resolution, bearing, unclamped, textMaxSizePixels } = input;
|
|
2726
2803
|
if (!text) return null;
|
|
2804
|
+
if (typeof textMaxSizePixels === "number" && textSizePixels === void 0 && typeof textSizeMeters === "number" && typeof resolution === "number" && resolution > 0 && textSizeMeters / resolution > textMaxSizePixels) return null;
|
|
2727
2805
|
const angle = normalizeAngle(Math.PI - (rotation ?? 0) - (bearing ?? 0));
|
|
2728
|
-
const h = textSizePixels !== void 0 ? Math.max(1, textSizePixels) : textSizeMeters !== void 0 && resolution !== void 0 && resolution > 0 ? resolveGroundTextSizePixels(textSizeMeters, resolution) : 16;
|
|
2806
|
+
const h = textSizePixels !== void 0 ? Math.max(1, textSizePixels) : textSizeMeters !== void 0 && resolution !== void 0 && resolution > 0 ? unclamped ? Math.max(1, textSizeMeters / resolution) : resolveGroundTextSizePixels(textSizeMeters, resolution) : 16;
|
|
2729
2807
|
const lines = text.split("\n");
|
|
2730
2808
|
const width = lines.reduce((max, line) => Math.max(max, line.length), 0) * CHAR_WIDTH_FACTOR * h;
|
|
2731
2809
|
const height = lines.length * LINE_HEIGHT_FACTOR * h;
|
|
@@ -2760,12 +2838,47 @@ function fitLabelBoxFromFeature(feature, anchorPixel, view) {
|
|
|
2760
2838
|
textAnchor: props.textAnchor === "start" || props.textAnchor === "end" ? props.textAnchor : void 0,
|
|
2761
2839
|
textSizePixels: typeof props.textSizePixels === "number" ? props.textSizePixels : void 0,
|
|
2762
2840
|
textSizeMeters: typeof props.textSizeMeters === "number" ? props.textSizeMeters : void 0,
|
|
2841
|
+
textMaxSizePixels: typeof props.textMaxSizePixels === "number" ? props.textMaxSizePixels : void 0,
|
|
2763
2842
|
anchor: anchorPixel,
|
|
2764
2843
|
resolution: view.resolution,
|
|
2765
|
-
bearing: view.bearing
|
|
2844
|
+
bearing: view.bearing,
|
|
2845
|
+
unclamped: view.unclamped
|
|
2766
2846
|
});
|
|
2767
2847
|
}
|
|
2768
2848
|
/**
|
|
2849
|
+
* Fit a `TransformBox` around a rendered **Text generic graphic** feature — the
|
|
2850
|
+
* Text analogue of the point-symbol footprint (ADR-0031): a rotated logical
|
|
2851
|
+
* rectangle centered on / shifted from the anchor per `textAlign` (via the
|
|
2852
|
+
* feature's `textAnchor` prop), sized from the text's line count and longest
|
|
2853
|
+
* line at the actual rendered height (screen `textSizePixels` as-is; ground
|
|
2854
|
+
* `textSizeMeters / resolution` unclamped). Drives the Text edit/transform box
|
|
2855
|
+
* outline, its corner-scale handles, and the pick extent, and updates live as a
|
|
2856
|
+
* rotate/scale gesture folds new rotation/size into the feature. Returns `null`
|
|
2857
|
+
* when the feature carries no text. Rotation pivots at the anchor, so the box
|
|
2858
|
+
* orbits the anchor when the text rotates — matching how the adapters render a
|
|
2859
|
+
* `textAnchor`-shifted, rotated label.
|
|
2860
|
+
*/
|
|
2861
|
+
function fitTextGraphicFootprint(feature, anchorPixel, view) {
|
|
2862
|
+
return fitLabelBoxFromFeature(feature, anchorPixel, {
|
|
2863
|
+
...view,
|
|
2864
|
+
unclamped: true
|
|
2865
|
+
});
|
|
2866
|
+
}
|
|
2867
|
+
/**
|
|
2868
|
+
* Locate the single Text `Point` feature in `features`, project its anchor, and
|
|
2869
|
+
* return its {@link fitTextGraphicFootprint} box — the shared body behind the
|
|
2870
|
+
* edit controller's Text box and the group transform-controller's Text member
|
|
2871
|
+
* footprint. Returns `null` when there is no text feature or its anchor is
|
|
2872
|
+
* off-screen.
|
|
2873
|
+
*/
|
|
2874
|
+
function fitTextFootprintFromFeatures(features, toPixel, view) {
|
|
2875
|
+
const f = features.find((feat) => feat.geometry?.type === "Point" && typeof feat.properties?.text === "string");
|
|
2876
|
+
if (!f || f.geometry?.type !== "Point") return null;
|
|
2877
|
+
const anchorPixel = toPixel(f.geometry.coordinates);
|
|
2878
|
+
if (!anchorPixel) return null;
|
|
2879
|
+
return fitTextGraphicFootprint(f, anchorPixel, view);
|
|
2880
|
+
}
|
|
2881
|
+
/**
|
|
2769
2882
|
* Carry user-adjusted label anchors (and rotation overrides) through a
|
|
2770
2883
|
* transform-box gesture. Anchors are transformed in pixel space alongside the
|
|
2771
2884
|
* control points so the result remains visually rigid across map
|
|
@@ -3123,6 +3236,7 @@ function runEdit(opts) {
|
|
|
3123
3236
|
const closedRing = rule?.closedRing ?? false;
|
|
3124
3237
|
const guideEnabled = (guideOpt ?? true) && !shouldSuppressGuide(metadata);
|
|
3125
3238
|
const isAreaKind = metadata.geometry === "area";
|
|
3239
|
+
const isTextGraphic = measure.kind === "text";
|
|
3126
3240
|
const requestedModes = normaliseModes(initialModes);
|
|
3127
3241
|
const editStartPoints = measure.controlPoints.map((p) => [...p]);
|
|
3128
3242
|
const workingPoints = editStartPoints.map((p) => [...p]);
|
|
@@ -3252,6 +3366,7 @@ function runEdit(opts) {
|
|
|
3252
3366
|
* projects (e.g. the working copy renders no features this frame).
|
|
3253
3367
|
*/
|
|
3254
3368
|
function fitBoxFromFeatures(features) {
|
|
3369
|
+
if (isTextGraphic) return fitTextBox(features);
|
|
3255
3370
|
const positions = [];
|
|
3256
3371
|
for (const f of features) if (f.geometry) collectFeaturePositions(f.geometry, positions);
|
|
3257
3372
|
const pixels = [];
|
|
@@ -3262,13 +3377,28 @@ function runEdit(opts) {
|
|
|
3262
3377
|
return fitTransformBox(pixels, boxAngle, interactionStyle?.boxPadding ?? 0);
|
|
3263
3378
|
}
|
|
3264
3379
|
/**
|
|
3380
|
+
* The Text footprint box (rotated rectangle hugging the rendered text),
|
|
3381
|
+
* derived from the single Point feature's text props. Unlike geometry-fit
|
|
3382
|
+
* boxes it carries the text's own on-screen angle (so it tilts with the
|
|
3383
|
+
* rotation option) and honors `textAnchor` alignment; the session `boxAngle`
|
|
3384
|
+
* is not used for Text.
|
|
3385
|
+
*/
|
|
3386
|
+
function fitTextBox(features) {
|
|
3387
|
+
return fitTextFootprintFromFeatures(features, driver.toPixel, {
|
|
3388
|
+
resolution: adapter.getResolution(),
|
|
3389
|
+
bearing: adapter.getBearing()
|
|
3390
|
+
});
|
|
3391
|
+
}
|
|
3392
|
+
/**
|
|
3265
3393
|
* Re-fit `box` from `lastPreviewFeatures`. A no-op while a box gesture is
|
|
3266
3394
|
* in flight — that drag paints the box rigidly from its own snapshot
|
|
3267
3395
|
* (`BoxDragSnapshot.box`) instead of a re-fit, so a corner/rotate/body drag
|
|
3268
|
-
* doesn't fight its own paint.
|
|
3396
|
+
* doesn't fight its own paint. Text is the exception: its box follows the
|
|
3397
|
+
* rendered footprint live (a rotate/scale gesture changes the rendered text),
|
|
3398
|
+
* so it re-fits every frame, drag included.
|
|
3269
3399
|
*/
|
|
3270
3400
|
function refitBox() {
|
|
3271
|
-
if (activeBoxDrag) return;
|
|
3401
|
+
if (activeBoxDrag && !isTextGraphic) return;
|
|
3272
3402
|
box = fitBoxFromFeatures(lastPreviewFeatures);
|
|
3273
3403
|
}
|
|
3274
3404
|
function liftOriginalFromGraphics() {
|
|
@@ -3319,7 +3449,7 @@ function runEdit(opts) {
|
|
|
3319
3449
|
}), handleIdPrefix, interactionStyle);
|
|
3320
3450
|
if (modes.includes("labeldrag")) features.push(...labels.handleFeatures());
|
|
3321
3451
|
if (modes.includes("transform")) {
|
|
3322
|
-
const liveBox = activeBoxDrag ? activeBoxDrag.lastSnapshot.box : box;
|
|
3452
|
+
const liveBox = activeBoxDrag && !isTextGraphic ? activeBoxDrag.lastSnapshot.box : box;
|
|
3323
3453
|
if (liveBox) {
|
|
3324
3454
|
features.push(boxOutlineFeature(liveBox, dragProjection.fromPixel, `${handleIdPrefix}:box-outline`, interactionStyle?.boxOutline));
|
|
3325
3455
|
features.push(...toFeatures(boxToHandles(liveBox, dragProjection.fromPixel), handleIdPrefix, interactionStyle));
|
|
@@ -3328,6 +3458,19 @@ function runEdit(opts) {
|
|
|
3328
3458
|
ctx.writeLayer(handlesLayer, features);
|
|
3329
3459
|
}
|
|
3330
3460
|
/**
|
|
3461
|
+
* Repaint the preview and, in lockstep, the handles. `renderPreview` re-fits
|
|
3462
|
+
* `box` (for Text, the footprint depends on options — size/rotation/
|
|
3463
|
+
* alignment/text), so the transform-box outline and its corner/rotate handles
|
|
3464
|
+
* must repaint to match; otherwise they stay where the old options placed them
|
|
3465
|
+
* and the pick target (`box`) drifts out from under the visible handles
|
|
3466
|
+
* (#scale-handle desync). Used by the non-geometry option/style/amplifier
|
|
3467
|
+
* setters, which change what the preview renders without moving a point.
|
|
3468
|
+
*/
|
|
3469
|
+
function renderPreviewAndHandles() {
|
|
3470
|
+
renderPreview();
|
|
3471
|
+
renderHandlesLayer();
|
|
3472
|
+
}
|
|
3473
|
+
/**
|
|
3331
3474
|
* Builds the committed snapshot against explicit `options` rather than the
|
|
3332
3475
|
* live `workingOptions` — used to render a gesture's "previous" snapshot
|
|
3333
3476
|
* against pre-freeze options when a rotate has already merged its
|
|
@@ -3472,7 +3615,7 @@ function runEdit(opts) {
|
|
|
3472
3615
|
replaceWorkingOptions(previousOptions);
|
|
3473
3616
|
throw err;
|
|
3474
3617
|
}
|
|
3475
|
-
|
|
3618
|
+
renderPreviewAndHandles();
|
|
3476
3619
|
nonGeometryDirty = true;
|
|
3477
3620
|
dirty = true;
|
|
3478
3621
|
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, "programmatic", "Change options");
|
|
@@ -3498,7 +3641,7 @@ function runEdit(opts) {
|
|
|
3498
3641
|
currentStyle = previousStyle;
|
|
3499
3642
|
throw err;
|
|
3500
3643
|
}
|
|
3501
|
-
|
|
3644
|
+
renderPreviewAndHandles();
|
|
3502
3645
|
nonGeometryDirty = true;
|
|
3503
3646
|
dirty = true;
|
|
3504
3647
|
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, "programmatic", "Change style");
|
|
@@ -3544,7 +3687,7 @@ function runEdit(opts) {
|
|
|
3544
3687
|
workingTextAmplifiers = previousTextAmplifiers;
|
|
3545
3688
|
throw err;
|
|
3546
3689
|
}
|
|
3547
|
-
|
|
3690
|
+
renderPreviewAndHandles();
|
|
3548
3691
|
nonGeometryDirty = true;
|
|
3549
3692
|
dirty = true;
|
|
3550
3693
|
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, "programmatic", "Change text");
|
|
@@ -3571,7 +3714,7 @@ function runEdit(opts) {
|
|
|
3571
3714
|
workingAmplifierPlacements = Object.keys(next).length > 0 ? next : void 0;
|
|
3572
3715
|
if (JSON.stringify(workingAmplifierPlacements) === JSON.stringify(previousAmplifierPlacements)) return;
|
|
3573
3716
|
const currentRendered = rebuildRendered();
|
|
3574
|
-
|
|
3717
|
+
renderPreviewAndHandles();
|
|
3575
3718
|
nonGeometryDirty = true;
|
|
3576
3719
|
dirty = true;
|
|
3577
3720
|
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, source, historyLabel);
|
|
@@ -3917,11 +4060,40 @@ function runEdit(opts) {
|
|
|
3917
4060
|
recomputeDirty();
|
|
3918
4061
|
emitTransient("move", snapshot.points, activeDrag.controlPointIndex);
|
|
3919
4062
|
}
|
|
4063
|
+
/**
|
|
4064
|
+
* Fold a box gesture's delta (the box `to` relative to `from`, given the
|
|
4065
|
+
* gesture `kind`) into a Text graphic's `rotation`/size options, on top of
|
|
4066
|
+
* `base`. A `scale` grows the active size anchor; a `rotate` adds to the
|
|
4067
|
+
* rotation. Returns `base` unchanged when the kind produces no patch. The
|
|
4068
|
+
* delta (uniform scale ratio, angle difference) is pivot-independent, so
|
|
4069
|
+
* folding it keeps the anchor point fixed while the glyph turns/grows.
|
|
4070
|
+
*/
|
|
4071
|
+
function foldTextTransform(from, to, kind, base) {
|
|
4072
|
+
const { scale, rotation } = boxScaleRotation(kind, to, from);
|
|
4073
|
+
const patch = applyBoxTransformOptions(measure.kind, base ?? {}, {
|
|
4074
|
+
scale,
|
|
4075
|
+
rotationRadians: rotation
|
|
4076
|
+
});
|
|
4077
|
+
return patch ? {
|
|
4078
|
+
...base ?? {},
|
|
4079
|
+
...patch
|
|
4080
|
+
} : base;
|
|
4081
|
+
}
|
|
3920
4082
|
function paintBoxDragSnapshot(pixel, info, constrainRotation = false) {
|
|
3921
4083
|
if (!activeBoxDrag) return;
|
|
3922
4084
|
const resolvedPixel = translatedBoxPixel(pixel, info, activeBoxDrag);
|
|
3923
4085
|
const snapshot = activeBoxDrag.fsm.move(resolvedPixel, { constrainRotation });
|
|
3924
4086
|
activeBoxDrag.lastSnapshot = snapshot;
|
|
4087
|
+
if (isTextGraphic && (activeBoxDrag.fsm.kind === "rotate" || activeBoxDrag.fsm.kind === "scale")) {
|
|
4088
|
+
workingPoints.splice(0, workingPoints.length, ...activeBoxDrag.preDragPoints);
|
|
4089
|
+
workingOptions = foldTextTransform(activeBoxDrag.preDragBox, snapshot.box, activeBoxDrag.fsm.kind, activeBoxDrag.preDragOptions);
|
|
4090
|
+
nonGeometryDirty = true;
|
|
4091
|
+
renderPreview();
|
|
4092
|
+
renderHandlesLayer();
|
|
4093
|
+
recomputeDirty();
|
|
4094
|
+
emitTransient("move", activeBoxDrag.preDragPoints, null);
|
|
4095
|
+
return;
|
|
4096
|
+
}
|
|
3925
4097
|
workingPoints.splice(0, workingPoints.length, ...snapshot.points);
|
|
3926
4098
|
workingAmplifierPlacements = transformAmplifierPlacements(activeBoxDrag.preDragAmplifierPlacements, activeBoxDrag.preDragBox, snapshot.box, driver);
|
|
3927
4099
|
renderPreview();
|
|
@@ -4012,9 +4184,15 @@ function runEdit(opts) {
|
|
|
4012
4184
|
const finalSnapshot = fsm.move(finalPixel, { constrainRotation: info?.shiftKey });
|
|
4013
4185
|
replaceWorkingPoints(preDragPoints);
|
|
4014
4186
|
workingAmplifierPlacements = preDragAmplifierPlacements;
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4187
|
+
const textOptionFold = isTextGraphic && (fsm.kind === "rotate" || fsm.kind === "scale");
|
|
4188
|
+
if (fsm.kind === "rotate" && !isTextGraphic) boxAngle += finalSnapshot.box.angle - preDragBox.angle;
|
|
4189
|
+
if (textOptionFold) {
|
|
4190
|
+
workingOptions = foldTextTransform(preDragBox, finalSnapshot.box, fsm.kind, preDragOptions);
|
|
4191
|
+
nonGeometryDirty = true;
|
|
4192
|
+
}
|
|
4193
|
+
const committedPoints = textOptionFold ? preDragPoints : finalSnapshot.points;
|
|
4194
|
+
emitTransient("end", committedPoints, null);
|
|
4195
|
+
commitGesture(committedPoints, preDragOptions, transformAmplifierPlacements(preDragAmplifierPlacements, preDragBox, finalSnapshot.box, driver), preDragBox.angle);
|
|
4018
4196
|
return;
|
|
4019
4197
|
}
|
|
4020
4198
|
};
|
|
@@ -4223,18 +4401,6 @@ function boundProperty(key, value) {
|
|
|
4223
4401
|
//#region src/tactical-draw/transform-controller.ts
|
|
4224
4402
|
/** No point symbol may scale below this factor of its own size — mirrors the box-drag floor. */
|
|
4225
4403
|
const MIN_POINT_SYMBOL_SIZE = .1;
|
|
4226
|
-
/**
|
|
4227
|
-
* The uniform scale factor and rotation delta a box implies relative to a
|
|
4228
|
-
* pre-drag box, given the gesture kind. Body translate reports neither (1, 0);
|
|
4229
|
-
* measures ignore this and take their geometry straight from the transformed
|
|
4230
|
-
* anchor points — only point symbols consume it.
|
|
4231
|
-
*/
|
|
4232
|
-
function boxScaleRotation(kind, box, preDragBox) {
|
|
4233
|
-
return {
|
|
4234
|
-
scale: kind === "scale" ? Math.max(box.halfWidth / preDragBox.halfWidth, box.halfHeight / preDragBox.halfHeight) : 1,
|
|
4235
|
-
rotation: kind === "rotate" ? box.angle - preDragBox.angle : 0
|
|
4236
|
-
};
|
|
4237
|
-
}
|
|
4238
4404
|
/** Apply a box's scale/rotation deltas to each member's pre-drag point-symbol state. */
|
|
4239
4405
|
function scalePointSymbolStates(pre, scale, rotation) {
|
|
4240
4406
|
return pre.map((state) => state ? {
|
|
@@ -4444,6 +4610,40 @@ function runGroupTransform(opts) {
|
|
|
4444
4610
|
}
|
|
4445
4611
|
return pixels;
|
|
4446
4612
|
}
|
|
4613
|
+
/** The rotated-text footprint corners for a Text member's rendered feature, or `null`. */
|
|
4614
|
+
function textFootprintCorners(features) {
|
|
4615
|
+
const box = fitTextFootprintFromFeatures(features, driver.toPixel, {
|
|
4616
|
+
resolution: adapter.getResolution(),
|
|
4617
|
+
bearing: adapter.getBearing()
|
|
4618
|
+
});
|
|
4619
|
+
return box ? boxCorners(box) : null;
|
|
4620
|
+
}
|
|
4621
|
+
/**
|
|
4622
|
+
* Fold a box gesture's delta (`box` vs the pre-drag box) into each measure
|
|
4623
|
+
* member that persists orientation/size as options (e.g. Text), off the
|
|
4624
|
+
* per-member `baseOverrides` so live ticks recompute rather than compound.
|
|
4625
|
+
* Only members whose kind returns a patch (Text) are touched — members that
|
|
4626
|
+
* carry an orientation *freeze* (Battle Position's echelon anchor) return no
|
|
4627
|
+
* patch and keep their live override untouched. Point-symbol members take the
|
|
4628
|
+
* delta through their own working state, not here.
|
|
4629
|
+
*/
|
|
4630
|
+
function applyMeasureTransformForBox(box, fromBox, kind, baseOverrides) {
|
|
4631
|
+
const { scale, rotation } = boxScaleRotation(kind, box, fromBox);
|
|
4632
|
+
if (scale === 1 && rotation === 0) return;
|
|
4633
|
+
members.forEach((member, i) => {
|
|
4634
|
+
if (isPointSymbolMember(member)) return;
|
|
4635
|
+
const base = baseOverrides[i] ?? null;
|
|
4636
|
+
const effective = mergeOptions(member.graphic.options, base);
|
|
4637
|
+
const patch = applyBoxTransformOptions(member.graphic.kind, effective, {
|
|
4638
|
+
scale,
|
|
4639
|
+
rotationRadians: rotation
|
|
4640
|
+
});
|
|
4641
|
+
if (patch) member.optionsOverride = {
|
|
4642
|
+
...base ?? {},
|
|
4643
|
+
...patch
|
|
4644
|
+
};
|
|
4645
|
+
});
|
|
4646
|
+
}
|
|
4447
4647
|
/** The live box angle: the in-flight drag's rigidly-transformed angle, or the committed `boxAngle`. */
|
|
4448
4648
|
function currentAngle() {
|
|
4449
4649
|
return activeBoxDrag ? activeBoxDrag.lastSnapshot.box.angle : boxAngle;
|
|
@@ -4483,8 +4683,9 @@ function runGroupTransform(opts) {
|
|
|
4483
4683
|
const footprint = pointSymbolFootprint(symbol, plan.render, adapter);
|
|
4484
4684
|
if (footprint) boxPixels = footprint.corners;
|
|
4485
4685
|
} else {
|
|
4486
|
-
|
|
4487
|
-
|
|
4686
|
+
const working = workingGraphicFor(i);
|
|
4687
|
+
features = renderMeasureForAdapter(adapter, working, renderOpts).features;
|
|
4688
|
+
boxPixels = (working.kind === "text" ? textFootprintCorners(features) : null) ?? pixelsFromFeatures(features);
|
|
4488
4689
|
}
|
|
4489
4690
|
} catch {
|
|
4490
4691
|
features = [];
|
|
@@ -4883,6 +5084,7 @@ function runGroupTransform(opts) {
|
|
|
4883
5084
|
activeBoxDrag.lastSnapshot = snapshot;
|
|
4884
5085
|
applyConcatenatedPoints(snapshot.points);
|
|
4885
5086
|
applyPointSymbolStateByMember(pointSymbolStateForBox(snapshot.box));
|
|
5087
|
+
applyMeasureTransformForBox(snapshot.box, activeBoxDrag.preDragBox, activeBoxDrag.fsm.kind, activeBoxDrag.preDragOptionsOverrideByMember);
|
|
4886
5088
|
applyAmplifierPlacementsByMember(activeBoxDrag.preDragAmplifierPlacementsByMember.map((placements) => transformAmplifierPlacements(placements, activeBoxDrag.preDragBox, snapshot.box, driver)));
|
|
4887
5089
|
renderAllPreviews();
|
|
4888
5090
|
renderHandlesLayer();
|
|
@@ -4922,6 +5124,7 @@ function runGroupTransform(opts) {
|
|
|
4922
5124
|
applyAmplifierPlacementsByMember(preDragAmplifierPlacementsByMember);
|
|
4923
5125
|
if (members.some(isPointSymbolMember)) shell.suppressNextClickAway();
|
|
4924
5126
|
if (fsm.kind === "rotate") boxAngle += finalSnapshot.box.angle - preDragBox.angle;
|
|
5127
|
+
applyMeasureTransformForBox(finalSnapshot.box, preDragBox, fsm.kind, preDragOptionsOverrideByMember);
|
|
4925
5128
|
commitGesture(finalSnapshot.points, preDragOptionsOverrideByMember, preDragAmplifierPlacementsByMember.map((placements) => transformAmplifierPlacements(placements, preDragBox, finalSnapshot.box, driver)), preDragBox.angle, nextPointSymbolStateByMember);
|
|
4926
5129
|
};
|
|
4927
5130
|
const core = runEditInteraction({
|
|
@@ -6390,4 +6593,4 @@ function nearestWorldCopyPosition(position, refLng) {
|
|
|
6390
6593
|
return [nearestWorldCopyLng(position[0], refLng), ...position.slice(1)];
|
|
6391
6594
|
}
|
|
6392
6595
|
//#endregion
|
|
6393
|
-
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 };
|
|
6596
|
+
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, resolveTextSizePixels, 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.31",
|
|
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.24"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@arethetypeswrong/core": "^0.18.3",
|