@orbat-mapper/tactical-draw 0.6.2 → 0.8.0
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.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_LABEL_HEIGHT_CSS_PIXELS, FILL_PATTERN_DEFINITIONS, NON_SOLID_FILL_PATTERNS, applyBoxTransformOptions, canonicalTextAmplifierKey, cloneControlMeasure, controlMeasureIdFromFeature, dragControlMeasureOptionHandle, foldsBoxTransformOptions, freezeOrientationOptions, getControlMeasureMetadata, getControlMeasureOptionHandles, getDefaultOptions, normalizeTextAmplifiers, renderControlMeasure, resolveAmplifierPlacement, resolveSizePair, resolveStyleHints, roundToFixed, switchSizePairUnit } from "@orbat-mapper/control-measures";
|
|
1
|
+
import { DEFAULT_LABEL_HEIGHT_CSS_PIXELS, DEFAULT_SYMBOL_COLOR, FILL_PATTERN_DEFINITIONS, NON_SOLID_FILL_PATTERNS, applyBoxTransformOptions, canonicalTextAmplifierKey, cloneControlMeasure, controlMeasureIdFromFeature, dragControlMeasureOptionHandle, foldsBoxTransformOptions, freezeOrientationOptions, getControlMeasureMetadata, getControlMeasureOptionHandles, getDefaultOptions, normalizeTextAmplifiers, renderControlMeasure, resolveAmplifierPlacement, resolveSizePair, resolveStyleHints, resolveSymbolColor, roundToFixed, switchSizePairUnit } from "@orbat-mapper/control-measures";
|
|
2
2
|
function pickPointerType(originalEvent) {
|
|
3
3
|
if (typeof PointerEvent !== "undefined" && originalEvent instanceof PointerEvent) {
|
|
4
4
|
if (originalEvent.pointerType === "touch") return "touch";
|
|
@@ -1878,7 +1878,7 @@ const PREVIEW_CURSOR$1 = "crosshair";
|
|
|
1878
1878
|
* kinds need no host UI.
|
|
1879
1879
|
*/
|
|
1880
1880
|
function runFixedLengthDraw(options) {
|
|
1881
|
-
const { adapter, kind, previewLayer, guideLayer, graphicsStyle, generateId, signal, measureOptions, measureStyle, measureProperties, measureTextAmplifiers, guide: guideOpt, sizeAnchor, onSettled, onSession, seed, snap } = options;
|
|
1881
|
+
const { adapter, kind, previewLayer, guideLayer, graphicsStyle, symbolColors, generateId, signal, measureOptions, measureStyle, measureProperties, measureTextAmplifiers, guide: guideOpt, sizeAnchor, onSettled, onSession, seed, snap } = options;
|
|
1882
1882
|
const metadata = getControlMeasureMetadata(kind);
|
|
1883
1883
|
const rule = metadata.rule;
|
|
1884
1884
|
const min = metadata.minCoordinates;
|
|
@@ -1887,7 +1887,10 @@ function runFixedLengthDraw(options) {
|
|
|
1887
1887
|
if (rule) required = rule.minimumUserPoints;
|
|
1888
1888
|
else if (typeof min === "number" && typeof max === "number" && min === max) required = min;
|
|
1889
1889
|
else throw new Error(`runFixedLengthDraw: kind "${kind}" is not fixed-length (min=${min}, max=${max})`);
|
|
1890
|
-
const renderOpts =
|
|
1890
|
+
const renderOpts = {
|
|
1891
|
+
graphicsStyle,
|
|
1892
|
+
symbolColors
|
|
1893
|
+
};
|
|
1891
1894
|
const guideEnabled = (guideOpt ?? true) && !shouldSuppressGuide(metadata);
|
|
1892
1895
|
const isAreaKind = metadata.geometry === "area";
|
|
1893
1896
|
const initialPoints = [];
|
|
@@ -2155,7 +2158,7 @@ const PREVIEW_CURSOR = "crosshair";
|
|
|
2155
2158
|
* added point again" as additional gesture-level commit triggers.
|
|
2156
2159
|
*/
|
|
2157
2160
|
function runVariableLengthDraw(options) {
|
|
2158
|
-
const { adapter, kind, previewLayer, guideLayer, graphicsStyle, generateId, signal, onSession, seed, measureOptions, measureStyle, measureProperties, measureTextAmplifiers, guide: guideOpt, sizeAnchor, onSettled, snap } = options;
|
|
2161
|
+
const { adapter, kind, previewLayer, guideLayer, graphicsStyle, symbolColors, generateId, signal, onSession, seed, measureOptions, measureStyle, measureProperties, measureTextAmplifiers, guide: guideOpt, sizeAnchor, onSettled, snap } = options;
|
|
2159
2162
|
const metadata = getControlMeasureMetadata(kind);
|
|
2160
2163
|
if (typeof metadata.minCoordinates !== "number") throw new Error(`runVariableLengthDraw: kind "${kind}" has no minCoordinates metadata`);
|
|
2161
2164
|
const min = metadata.minCoordinates;
|
|
@@ -2173,7 +2176,10 @@ function runVariableLengthDraw(options) {
|
|
|
2173
2176
|
function insertionIndex(len) {
|
|
2174
2177
|
return canonicalFormed(len) ? len - trailingFixed : len;
|
|
2175
2178
|
}
|
|
2176
|
-
const renderOpts =
|
|
2179
|
+
const renderOpts = {
|
|
2180
|
+
graphicsStyle,
|
|
2181
|
+
symbolColors
|
|
2182
|
+
};
|
|
2177
2183
|
const initialPoints = [];
|
|
2178
2184
|
if (seed) {
|
|
2179
2185
|
for (const pos of seed) {
|
|
@@ -2594,6 +2600,140 @@ function runEditInteraction(spec) {
|
|
|
2594
2600
|
});
|
|
2595
2601
|
}
|
|
2596
2602
|
//#endregion
|
|
2603
|
+
//#region src/tactical-draw/transform-box.ts
|
|
2604
|
+
/**
|
|
2605
|
+
* The uniform scale factor and rotation delta a box implies relative to a
|
|
2606
|
+
* pre-drag box, given the gesture kind. A `body` translate reports neither
|
|
2607
|
+
* (1, 0). Consumed both by point symbols (size/rotation) and by Text graphics
|
|
2608
|
+
* (folding the delta into rotation/size options); the delta is pivot-
|
|
2609
|
+
* independent so either can apply it without knowing the box center.
|
|
2610
|
+
*/
|
|
2611
|
+
function boxScaleRotation(kind, box, preDragBox) {
|
|
2612
|
+
return {
|
|
2613
|
+
scale: kind === "scale" ? Math.max(box.halfWidth / preDragBox.halfWidth, box.halfHeight / preDragBox.halfHeight) : 1,
|
|
2614
|
+
rotation: kind === "rotate" ? box.angle - preDragBox.angle : 0
|
|
2615
|
+
};
|
|
2616
|
+
}
|
|
2617
|
+
function rotate(x, y, angle) {
|
|
2618
|
+
const cos = Math.cos(angle);
|
|
2619
|
+
const sin = Math.sin(angle);
|
|
2620
|
+
return [x * cos - y * sin, x * sin + y * cos];
|
|
2621
|
+
}
|
|
2622
|
+
/**
|
|
2623
|
+
* Fit an oriented `TransformBox` around `pixels` at a caller-chosen `angle`.
|
|
2624
|
+
* Rotates the points by `-angle` about their centroid, takes the axis-aligned
|
|
2625
|
+
* bounding box in that de-rotated frame, then re-expresses the result as an
|
|
2626
|
+
* oriented box at `angle` — so the box always has exactly the requested tilt,
|
|
2627
|
+
* sized to just contain the (de-rotated) points. Half-dimensions are floored
|
|
2628
|
+
* at `MIN_HALF_SIZE_PX` so a degenerate selection still yields a drawable,
|
|
2629
|
+
* pickable box. Returns `null` for an empty list — there is nothing to fit.
|
|
2630
|
+
*
|
|
2631
|
+
* `padding` (pixels, default `0`) is added to `halfWidth`/`halfHeight` *after*
|
|
2632
|
+
* the `MIN_HALF_SIZE_PX` floor, on every side, without moving `center` — the
|
|
2633
|
+
* `InteractionStyle.boxPadding` knob (ADR-0017) so the box doesn't tightly hug
|
|
2634
|
+
* the rendered geometry. Everything downstream that reads a fitted box
|
|
2635
|
+
* (`boxCorners`, `rotateGripPixel`, `pointInBox`, `pickBoxHandle`) derives from
|
|
2636
|
+
* `halfWidth`/`halfHeight` alone, so the padding flows through to corner
|
|
2637
|
+
* placement, the rotate grip, and picking automatically — no other function
|
|
2638
|
+
* needs to know about it.
|
|
2639
|
+
*/
|
|
2640
|
+
function fitTransformBox(pixels, angle, padding = 0) {
|
|
2641
|
+
if (pixels.length === 0) return null;
|
|
2642
|
+
let cx = 0;
|
|
2643
|
+
let cy = 0;
|
|
2644
|
+
for (const p of pixels) {
|
|
2645
|
+
cx += p[0];
|
|
2646
|
+
cy += p[1];
|
|
2647
|
+
}
|
|
2648
|
+
cx /= pixels.length;
|
|
2649
|
+
cy /= pixels.length;
|
|
2650
|
+
let minX = Infinity;
|
|
2651
|
+
let minY = Infinity;
|
|
2652
|
+
let maxX = -Infinity;
|
|
2653
|
+
let maxY = -Infinity;
|
|
2654
|
+
for (const p of pixels) {
|
|
2655
|
+
const [rx, ry] = rotate(p[0] - cx, p[1] - cy, -angle);
|
|
2656
|
+
if (rx < minX) minX = rx;
|
|
2657
|
+
if (rx > maxX) maxX = rx;
|
|
2658
|
+
if (ry < minY) minY = ry;
|
|
2659
|
+
if (ry > maxY) maxY = ry;
|
|
2660
|
+
}
|
|
2661
|
+
const localCenterX = (minX + maxX) / 2;
|
|
2662
|
+
const localCenterY = (minY + maxY) / 2;
|
|
2663
|
+
const halfWidth = Math.max((maxX - minX) / 2, 1) + padding;
|
|
2664
|
+
const halfHeight = Math.max((maxY - minY) / 2, 1) + padding;
|
|
2665
|
+
const [ox, oy] = rotate(localCenterX, localCenterY, angle);
|
|
2666
|
+
return {
|
|
2667
|
+
center: [cx + ox, cy + oy],
|
|
2668
|
+
halfWidth,
|
|
2669
|
+
halfHeight,
|
|
2670
|
+
angle
|
|
2671
|
+
};
|
|
2672
|
+
}
|
|
2673
|
+
/**
|
|
2674
|
+
* The box's 4 corners in screen pixels: `[topLeft, topRight, bottomRight,
|
|
2675
|
+
* bottomLeft]` in the box's local (pre-rotation) frame, then rotated by
|
|
2676
|
+
* `box.angle` about `box.center`. Corner index is stable — `pickBoxHandle`
|
|
2677
|
+
* and the scale-drag transition (`handle-drag.ts`) key off it, and the scale
|
|
2678
|
+
* anchor is always the opposite corner, `(index + 2) % 4`.
|
|
2679
|
+
*/
|
|
2680
|
+
function boxCorners(box) {
|
|
2681
|
+
const { center, halfWidth, halfHeight, angle } = box;
|
|
2682
|
+
return [
|
|
2683
|
+
[-halfWidth, -halfHeight],
|
|
2684
|
+
[halfWidth, -halfHeight],
|
|
2685
|
+
[halfWidth, halfHeight],
|
|
2686
|
+
[-halfWidth, halfHeight]
|
|
2687
|
+
].map(([x, y]) => {
|
|
2688
|
+
const [rx, ry] = rotate(x, y, angle);
|
|
2689
|
+
return [center[0] + rx, center[1] + ry];
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
/**
|
|
2693
|
+
* Where the rotate grip sits: `offset` pixels (default `ROTATE_GRIP_OFFSET_PX`)
|
|
2694
|
+
* above the top-edge midpoint, along the box's local "up" direction, so the
|
|
2695
|
+
* grip stays glued to the box (rotates with it) rather than floating at a
|
|
2696
|
+
* fixed screen offset. The label transform box passes its own tighter
|
|
2697
|
+
* clearance (`LABEL_ROTATE_GRIP_OFFSET_PX`).
|
|
2698
|
+
*/
|
|
2699
|
+
function rotateGripPixel(box, offset = 28) {
|
|
2700
|
+
const { center, halfHeight, angle } = box;
|
|
2701
|
+
const [rx, ry] = rotate(0, -(halfHeight + offset), angle);
|
|
2702
|
+
return [center[0] + rx, center[1] + ry];
|
|
2703
|
+
}
|
|
2704
|
+
/**
|
|
2705
|
+
* Point-in-oriented-rectangle test, used to start a body (translate) drag
|
|
2706
|
+
* when the pointer goes down inside the box but not on a corner/rotate grip.
|
|
2707
|
+
* De-rotates `pixel` into the box's local frame about `box.center` and
|
|
2708
|
+
* compares against the half-extents.
|
|
2709
|
+
*/
|
|
2710
|
+
function pointInBox(box, pixel) {
|
|
2711
|
+
const { center, halfWidth, halfHeight, angle } = box;
|
|
2712
|
+
const [lx, ly] = rotate(pixel[0] - center[0], pixel[1] - center[1], -angle);
|
|
2713
|
+
return Math.abs(lx) <= halfWidth && Math.abs(ly) <= halfHeight;
|
|
2714
|
+
}
|
|
2715
|
+
/** Plain 2D distance between two pixel coordinates. */
|
|
2716
|
+
function pixelDistance$1(a, b) {
|
|
2717
|
+
return Math.hypot(a[0] - b[0], a[1] - b[1]);
|
|
2718
|
+
}
|
|
2719
|
+
/**
|
|
2720
|
+
* Pick what part of the box, if any, `pixel` hits — corners first, then the
|
|
2721
|
+
* rotate grip, then plain containment (body drag). Corners and the rotate
|
|
2722
|
+
* grip use `tolerance` pixels; body containment is exact (`pointInBox`), so a
|
|
2723
|
+
* click just outside the box never starts a body drag. Returns `null` when
|
|
2724
|
+
* nothing matches.
|
|
2725
|
+
*/
|
|
2726
|
+
function pickBoxHandle(box, pixel, tolerance) {
|
|
2727
|
+
const corners = boxCorners(box);
|
|
2728
|
+
for (let i = 0; i < corners.length; i++) if (pixelDistance$1(corners[i], pixel) <= tolerance) return {
|
|
2729
|
+
kind: "scale",
|
|
2730
|
+
index: i
|
|
2731
|
+
};
|
|
2732
|
+
if (pixelDistance$1(rotateGripPixel(box), pixel) <= tolerance) return { kind: "rotate" };
|
|
2733
|
+
if (pointInBox(box, pixel)) return { kind: "body" };
|
|
2734
|
+
return null;
|
|
2735
|
+
}
|
|
2736
|
+
//#endregion
|
|
2597
2737
|
//#region src/tactical-draw/handles.ts
|
|
2598
2738
|
/**
|
|
2599
2739
|
* Midpoint of the segment between two positions, in lonLat. The single source
|
|
@@ -2698,9 +2838,6 @@ function toFeatures(handles, featureIdPrefix, interactionStyle) {
|
|
|
2698
2838
|
};
|
|
2699
2839
|
});
|
|
2700
2840
|
}
|
|
2701
|
-
function pixelDistance$2(a, b) {
|
|
2702
|
-
return Math.hypot(a[0] - b[0], a[1] - b[1]);
|
|
2703
|
-
}
|
|
2704
2841
|
/**
|
|
2705
2842
|
* The pick projection of `buildHandleSet` (ADR-0016): the first handle within
|
|
2706
2843
|
* `tolerance` pixels of `pixel`, tested in the set's priority order. Because
|
|
@@ -2714,7 +2851,7 @@ function pixelDistance$2(a, b) {
|
|
|
2714
2851
|
function pickHandle(handles, pixel, tolerance, toPixel) {
|
|
2715
2852
|
for (const handle of handles) {
|
|
2716
2853
|
const projected = toPixel(handle.position);
|
|
2717
|
-
if (projected && pixelDistance$
|
|
2854
|
+
if (projected && pixelDistance$1(projected, pixel) <= tolerance) return handle;
|
|
2718
2855
|
}
|
|
2719
2856
|
return null;
|
|
2720
2857
|
}
|
|
@@ -2732,139 +2869,6 @@ function constrainRotation(angle) {
|
|
|
2732
2869
|
return Math.round(angle / CONSTRAINED_ROTATION_STEP) * CONSTRAINED_ROTATION_STEP;
|
|
2733
2870
|
}
|
|
2734
2871
|
//#endregion
|
|
2735
|
-
//#region src/tactical-draw/transform-box.ts
|
|
2736
|
-
/**
|
|
2737
|
-
* The uniform scale factor and rotation delta a box implies relative to a
|
|
2738
|
-
* pre-drag box, given the gesture kind. A `body` translate reports neither
|
|
2739
|
-
* (1, 0). Consumed both by point symbols (size/rotation) and by Text graphics
|
|
2740
|
-
* (folding the delta into rotation/size options); the delta is pivot-
|
|
2741
|
-
* independent so either can apply it without knowing the box center.
|
|
2742
|
-
*/
|
|
2743
|
-
function boxScaleRotation(kind, box, preDragBox) {
|
|
2744
|
-
return {
|
|
2745
|
-
scale: kind === "scale" ? Math.max(box.halfWidth / preDragBox.halfWidth, box.halfHeight / preDragBox.halfHeight) : 1,
|
|
2746
|
-
rotation: kind === "rotate" ? box.angle - preDragBox.angle : 0
|
|
2747
|
-
};
|
|
2748
|
-
}
|
|
2749
|
-
function rotate(x, y, angle) {
|
|
2750
|
-
const cos = Math.cos(angle);
|
|
2751
|
-
const sin = Math.sin(angle);
|
|
2752
|
-
return [x * cos - y * sin, x * sin + y * cos];
|
|
2753
|
-
}
|
|
2754
|
-
/**
|
|
2755
|
-
* Fit an oriented `TransformBox` around `pixels` at a caller-chosen `angle`.
|
|
2756
|
-
* Rotates the points by `-angle` about their centroid, takes the axis-aligned
|
|
2757
|
-
* bounding box in that de-rotated frame, then re-expresses the result as an
|
|
2758
|
-
* oriented box at `angle` — so the box always has exactly the requested tilt,
|
|
2759
|
-
* sized to just contain the (de-rotated) points. Half-dimensions are floored
|
|
2760
|
-
* at `MIN_HALF_SIZE_PX` so a degenerate selection still yields a drawable,
|
|
2761
|
-
* pickable box. Returns `null` for an empty list — there is nothing to fit.
|
|
2762
|
-
*
|
|
2763
|
-
* `padding` (pixels, default `0`) is added to `halfWidth`/`halfHeight` *after*
|
|
2764
|
-
* the `MIN_HALF_SIZE_PX` floor, on every side, without moving `center` — the
|
|
2765
|
-
* `InteractionStyle.boxPadding` knob (ADR-0017) so the box doesn't tightly hug
|
|
2766
|
-
* the rendered geometry. Everything downstream that reads a fitted box
|
|
2767
|
-
* (`boxCorners`, `rotateGripPixel`, `pointInBox`, `pickBoxHandle`) derives from
|
|
2768
|
-
* `halfWidth`/`halfHeight` alone, so the padding flows through to corner
|
|
2769
|
-
* placement, the rotate grip, and picking automatically — no other function
|
|
2770
|
-
* needs to know about it.
|
|
2771
|
-
*/
|
|
2772
|
-
function fitTransformBox(pixels, angle, padding = 0) {
|
|
2773
|
-
if (pixels.length === 0) return null;
|
|
2774
|
-
let cx = 0;
|
|
2775
|
-
let cy = 0;
|
|
2776
|
-
for (const p of pixels) {
|
|
2777
|
-
cx += p[0];
|
|
2778
|
-
cy += p[1];
|
|
2779
|
-
}
|
|
2780
|
-
cx /= pixels.length;
|
|
2781
|
-
cy /= pixels.length;
|
|
2782
|
-
let minX = Infinity;
|
|
2783
|
-
let minY = Infinity;
|
|
2784
|
-
let maxX = -Infinity;
|
|
2785
|
-
let maxY = -Infinity;
|
|
2786
|
-
for (const p of pixels) {
|
|
2787
|
-
const [rx, ry] = rotate(p[0] - cx, p[1] - cy, -angle);
|
|
2788
|
-
if (rx < minX) minX = rx;
|
|
2789
|
-
if (rx > maxX) maxX = rx;
|
|
2790
|
-
if (ry < minY) minY = ry;
|
|
2791
|
-
if (ry > maxY) maxY = ry;
|
|
2792
|
-
}
|
|
2793
|
-
const localCenterX = (minX + maxX) / 2;
|
|
2794
|
-
const localCenterY = (minY + maxY) / 2;
|
|
2795
|
-
const halfWidth = Math.max((maxX - minX) / 2, 1) + padding;
|
|
2796
|
-
const halfHeight = Math.max((maxY - minY) / 2, 1) + padding;
|
|
2797
|
-
const [ox, oy] = rotate(localCenterX, localCenterY, angle);
|
|
2798
|
-
return {
|
|
2799
|
-
center: [cx + ox, cy + oy],
|
|
2800
|
-
halfWidth,
|
|
2801
|
-
halfHeight,
|
|
2802
|
-
angle
|
|
2803
|
-
};
|
|
2804
|
-
}
|
|
2805
|
-
/**
|
|
2806
|
-
* The box's 4 corners in screen pixels: `[topLeft, topRight, bottomRight,
|
|
2807
|
-
* bottomLeft]` in the box's local (pre-rotation) frame, then rotated by
|
|
2808
|
-
* `box.angle` about `box.center`. Corner index is stable — `pickBoxHandle`
|
|
2809
|
-
* and the scale-drag transition (`handle-drag.ts`) key off it, and the scale
|
|
2810
|
-
* anchor is always the opposite corner, `(index + 2) % 4`.
|
|
2811
|
-
*/
|
|
2812
|
-
function boxCorners(box) {
|
|
2813
|
-
const { center, halfWidth, halfHeight, angle } = box;
|
|
2814
|
-
return [
|
|
2815
|
-
[-halfWidth, -halfHeight],
|
|
2816
|
-
[halfWidth, -halfHeight],
|
|
2817
|
-
[halfWidth, halfHeight],
|
|
2818
|
-
[-halfWidth, halfHeight]
|
|
2819
|
-
].map(([x, y]) => {
|
|
2820
|
-
const [rx, ry] = rotate(x, y, angle);
|
|
2821
|
-
return [center[0] + rx, center[1] + ry];
|
|
2822
|
-
});
|
|
2823
|
-
}
|
|
2824
|
-
/**
|
|
2825
|
-
* Where the rotate grip sits: `offset` pixels (default `ROTATE_GRIP_OFFSET_PX`)
|
|
2826
|
-
* above the top-edge midpoint, along the box's local "up" direction, so the
|
|
2827
|
-
* grip stays glued to the box (rotates with it) rather than floating at a
|
|
2828
|
-
* fixed screen offset. The label transform box passes its own tighter
|
|
2829
|
-
* clearance (`LABEL_ROTATE_GRIP_OFFSET_PX`).
|
|
2830
|
-
*/
|
|
2831
|
-
function rotateGripPixel(box, offset = 28) {
|
|
2832
|
-
const { center, halfHeight, angle } = box;
|
|
2833
|
-
const [rx, ry] = rotate(0, -(halfHeight + offset), angle);
|
|
2834
|
-
return [center[0] + rx, center[1] + ry];
|
|
2835
|
-
}
|
|
2836
|
-
/**
|
|
2837
|
-
* Point-in-oriented-rectangle test, used to start a body (translate) drag
|
|
2838
|
-
* when the pointer goes down inside the box but not on a corner/rotate grip.
|
|
2839
|
-
* De-rotates `pixel` into the box's local frame about `box.center` and
|
|
2840
|
-
* compares against the half-extents.
|
|
2841
|
-
*/
|
|
2842
|
-
function pointInBox(box, pixel) {
|
|
2843
|
-
const { center, halfWidth, halfHeight, angle } = box;
|
|
2844
|
-
const [lx, ly] = rotate(pixel[0] - center[0], pixel[1] - center[1], -angle);
|
|
2845
|
-
return Math.abs(lx) <= halfWidth && Math.abs(ly) <= halfHeight;
|
|
2846
|
-
}
|
|
2847
|
-
function pixelDistance$1(a, b) {
|
|
2848
|
-
return Math.hypot(a[0] - b[0], a[1] - b[1]);
|
|
2849
|
-
}
|
|
2850
|
-
/**
|
|
2851
|
-
* Pick what part of the box, if any, `pixel` hits — corners first, then the
|
|
2852
|
-
* rotate grip, then plain containment (body drag). Corners and the rotate
|
|
2853
|
-
* grip use `tolerance` pixels; body containment is exact (`pointInBox`), so a
|
|
2854
|
-
* click just outside the box never starts a body drag. Returns `null` when
|
|
2855
|
-
* nothing matches.
|
|
2856
|
-
*/
|
|
2857
|
-
function pickBoxHandle(box, pixel, tolerance) {
|
|
2858
|
-
const corners = boxCorners(box);
|
|
2859
|
-
for (let i = 0; i < corners.length; i++) if (pixelDistance$1(corners[i], pixel) <= tolerance) return {
|
|
2860
|
-
kind: "scale",
|
|
2861
|
-
index: i
|
|
2862
|
-
};
|
|
2863
|
-
if (pixelDistance$1(rotateGripPixel(box), pixel) <= tolerance) return { kind: "rotate" };
|
|
2864
|
-
if (pointInBox(box, pixel)) return { kind: "body" };
|
|
2865
|
-
return null;
|
|
2866
|
-
}
|
|
2867
|
-
//#endregion
|
|
2868
2872
|
//#region src/tactical-draw/handle-drag.ts
|
|
2869
2873
|
function clonePoints(points) {
|
|
2870
2874
|
return points.map((p) => [...p]);
|
|
@@ -3914,8 +3918,11 @@ const ALLOWED_EDIT_MODES = new Set([
|
|
|
3914
3918
|
* - close / abort / signal / preempt / destroy paths
|
|
3915
3919
|
*/
|
|
3916
3920
|
function runEdit(opts) {
|
|
3917
|
-
const { adapter, measure, graphicsLayer, previewLayer, guideLayer, handlesLayer, graphicsStyle, signal, closeOnClickAway = true, modes: initialModes, interactionStyle, onSession, handleIdPrefix, guide: guideOpt, settleToGraphics, publishWorkingGraphic, onSettled, onClickAwayGraphicsPick, snap } = opts;
|
|
3918
|
-
const renderOpts =
|
|
3921
|
+
const { adapter, measure, graphicsLayer, previewLayer, guideLayer, handlesLayer, graphicsStyle, symbolColors, signal, closeOnClickAway = true, modes: initialModes, interactionStyle, onSession, handleIdPrefix, guide: guideOpt, settleToGraphics, publishWorkingGraphic, onSettled, onClickAwayGraphicsPick, snap } = opts;
|
|
3922
|
+
const renderOpts = {
|
|
3923
|
+
graphicsStyle,
|
|
3924
|
+
symbolColors
|
|
3925
|
+
};
|
|
3919
3926
|
let sizeAnchor = opts.sizeAnchor ?? defaultSizeAnchor(measure);
|
|
3920
3927
|
const metadata = getControlMeasureMetadata(measure.kind);
|
|
3921
3928
|
const rule = metadata.rule;
|
|
@@ -5181,7 +5188,7 @@ var ImageCapabilityRequiredError = class ImageCapabilityRequiredError extends Er
|
|
|
5181
5188
|
}
|
|
5182
5189
|
};
|
|
5183
5190
|
//#endregion
|
|
5184
|
-
//#region src/
|
|
5191
|
+
//#region src/point-symbol-size.ts
|
|
5185
5192
|
function effectivePointSymbolPixelSize(symbol, resolution) {
|
|
5186
5193
|
if (symbol.size.unit === "pixels") return symbol.size.value;
|
|
5187
5194
|
if (resolution === void 0 || resolution <= 0) return symbol.size.value;
|
|
@@ -5193,10 +5200,221 @@ function pointSymbolPixelScale(symbol, render, resolution) {
|
|
|
5193
5200
|
if (!Number.isFinite(render.octagonSize) || render.octagonSize <= 0) throw new TypeError("PointSymbolRender.octagonSize must be a positive finite number");
|
|
5194
5201
|
return effectivePointSymbolPixelSize(symbol, resolution) / render.octagonSize;
|
|
5195
5202
|
}
|
|
5203
|
+
//#endregion
|
|
5204
|
+
//#region src/point-symbol-offset.ts
|
|
5205
|
+
const EPSILON = 1e-7;
|
|
5206
|
+
const distance = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]);
|
|
5207
|
+
/** Trim from the symbol attachment end, including across a junction when necessary. */
|
|
5208
|
+
function padAnchor(path, padding) {
|
|
5209
|
+
if (!Number.isFinite(padding) || padding <= 0) return path;
|
|
5210
|
+
const trimmed = [...path];
|
|
5211
|
+
while (trimmed.length > 1) {
|
|
5212
|
+
const start = trimmed.shift();
|
|
5213
|
+
const end = trimmed[0];
|
|
5214
|
+
const length = distance(start, end);
|
|
5215
|
+
if (length > padding) {
|
|
5216
|
+
const fraction = padding / length;
|
|
5217
|
+
trimmed.unshift([start[0] + (end[0] - start[0]) * fraction, start[1] + (end[1] - start[1]) * fraction]);
|
|
5218
|
+
return trimmed;
|
|
5219
|
+
}
|
|
5220
|
+
padding -= length;
|
|
5221
|
+
}
|
|
5222
|
+
return [];
|
|
5223
|
+
}
|
|
5224
|
+
/** Pure layout: projection input/output is in CSS pixels, including document contexts. */
|
|
5225
|
+
function resolvePointSymbolOffset(symbol, render, view) {
|
|
5226
|
+
const actual = view.getPixelFromCoordinate(symbol.position);
|
|
5227
|
+
if (!actual) return null;
|
|
5228
|
+
const scale = pointSymbolPixelScale(symbol, render, view.getResolution());
|
|
5229
|
+
const angle = symbol.rotation - view.getBearing();
|
|
5230
|
+
const metadata = render.offset;
|
|
5231
|
+
const registration = metadata?.ordinaryRegistration ?? render.octagonAnchor;
|
|
5232
|
+
const shift = rotate((registration.x - render.anchor.x) * scale, (registration.y - render.anchor.y) * scale, angle);
|
|
5233
|
+
const ordinaryDisplay = [actual[0] + shift[0], actual[1] + shift[1]];
|
|
5234
|
+
const layout = {
|
|
5235
|
+
render,
|
|
5236
|
+
resource: pointSymbolResource(render),
|
|
5237
|
+
bodyOpacity: (symbol.style?.opacity ?? 1) * (symbol.style?.overallOpacity ?? 1),
|
|
5238
|
+
rotation: angle,
|
|
5239
|
+
actual,
|
|
5240
|
+
display: ordinaryDisplay,
|
|
5241
|
+
imageAnchor: actual,
|
|
5242
|
+
ordinaryDisplay,
|
|
5243
|
+
path: [],
|
|
5244
|
+
kind: "ordinary",
|
|
5245
|
+
status: "ok",
|
|
5246
|
+
scale,
|
|
5247
|
+
connectorStyle: {
|
|
5248
|
+
strokeColor: symbol.offset?.style?.strokeColor ?? metadata?.strokeColor ?? "#000000",
|
|
5249
|
+
opacity: (symbol.offset?.style?.opacity ?? symbol.style?.opacity ?? 1) * (symbol.style?.overallOpacity ?? 1),
|
|
5250
|
+
strokeWidth: symbol.offset?.style?.strokeWidth ?? (metadata?.strokeWidth ?? 4) * scale,
|
|
5251
|
+
strokeDash: [...symbol.offset?.style?.strokeDash ?? []]
|
|
5252
|
+
}
|
|
5253
|
+
};
|
|
5254
|
+
if (!symbol.offset) return layout;
|
|
5255
|
+
const { placement, connector, anchorPadding = 0 } = symbol.offset;
|
|
5256
|
+
if (!metadata || connector !== "direct" && !metadata.indicator) return {
|
|
5257
|
+
...layout,
|
|
5258
|
+
status: "unsupported-presentation"
|
|
5259
|
+
};
|
|
5260
|
+
const display = placement.mode === "ground" ? view.getPixelFromCoordinate(placement.position) : [ordinaryDisplay[0] + placement.offset[0], ordinaryDisplay[1] + placement.offset[1]];
|
|
5261
|
+
if (!display) return {
|
|
5262
|
+
...layout,
|
|
5263
|
+
status: "unsupported-presentation"
|
|
5264
|
+
};
|
|
5265
|
+
if (distance(display, ordinaryDisplay) < EPSILON) return layout;
|
|
5266
|
+
const body = metadata.body;
|
|
5267
|
+
const offset = rotate((metadata.attachment.x - body.anchor.x) * scale, (metadata.attachment.y - body.anchor.y) * scale, angle);
|
|
5268
|
+
const attachment = [display[0] + offset[0], display[1] + offset[1]];
|
|
5269
|
+
const resolved = {
|
|
5270
|
+
...layout,
|
|
5271
|
+
render: body,
|
|
5272
|
+
resource: pointSymbolResource(body),
|
|
5273
|
+
display,
|
|
5274
|
+
imageAnchor: display,
|
|
5275
|
+
attachment
|
|
5276
|
+
};
|
|
5277
|
+
if (connector === "direct") return {
|
|
5278
|
+
...resolved,
|
|
5279
|
+
kind: "direct",
|
|
5280
|
+
path: padAnchor(distance(attachment, actual) < EPSILON ? [] : [attachment, actual], anchorPadding)
|
|
5281
|
+
};
|
|
5282
|
+
const indicator = metadata.indicator;
|
|
5283
|
+
const rawDirection = rotate(indicator.direction.x, indicator.direction.y, angle);
|
|
5284
|
+
const norm = Math.hypot(...rawDirection);
|
|
5285
|
+
const direction = [rawDirection[0] / norm, rawDirection[1] / norm];
|
|
5286
|
+
const dx = actual[0] - attachment[0], dy = actual[1] - attachment[1];
|
|
5287
|
+
const forward = dx * direction[0] + dy * direction[1];
|
|
5288
|
+
if (Math.abs(dx * direction[1] - dy * direction[0]) < EPSILON && forward >= (indicator.minLength ?? 0) * scale) return {
|
|
5289
|
+
...resolved,
|
|
5290
|
+
direction,
|
|
5291
|
+
kind: "indicator-only",
|
|
5292
|
+
path: padAnchor([attachment, actual], anchorPadding)
|
|
5293
|
+
};
|
|
5294
|
+
const length = symbol.offset.indicatorLength !== void 0 ? symbol.offset.indicatorLength * render.octagonSize : indicator.length;
|
|
5295
|
+
const junction = [attachment[0] + direction[0] * length * scale, attachment[1] + direction[1] * length * scale];
|
|
5296
|
+
return {
|
|
5297
|
+
...resolved,
|
|
5298
|
+
direction,
|
|
5299
|
+
junction,
|
|
5300
|
+
kind: "with-extension",
|
|
5301
|
+
status: "ok",
|
|
5302
|
+
path: padAnchor(distance(junction, actual) < EPSILON ? [attachment, junction] : [
|
|
5303
|
+
attachment,
|
|
5304
|
+
junction,
|
|
5305
|
+
actual
|
|
5306
|
+
], anchorPadding)
|
|
5307
|
+
};
|
|
5308
|
+
}
|
|
5309
|
+
/**
|
|
5310
|
+
* Authored indicator length that puts the junction at `point`: the forward
|
|
5311
|
+
* projection onto the attachment ray, expressed as a multiple of the displayed
|
|
5312
|
+
* octagon size and clamped to a non-negative length.
|
|
5313
|
+
*/
|
|
5314
|
+
function indicatorLengthAt(point, attachment, direction, scale, octagonSize) {
|
|
5315
|
+
const forward = (point[0] - attachment[0]) * direction[0] + (point[1] - attachment[1]) * direction[1];
|
|
5316
|
+
return Math.max(0, forward / (scale * octagonSize));
|
|
5317
|
+
}
|
|
5318
|
+
/** Set body placement without changing the authoritative coordinate. */
|
|
5319
|
+
function placePointSymbol(symbol, render, view, display, mode = symbol.offset?.placement.mode ?? "screen-relative") {
|
|
5320
|
+
if (!render.offset) throw new TypeError("This renderer does not support offset placement");
|
|
5321
|
+
const ordinary = resolvePointSymbolOffset({
|
|
5322
|
+
...symbol,
|
|
5323
|
+
offset: void 0
|
|
5324
|
+
}, render, view);
|
|
5325
|
+
if (!ordinary) throw new RangeError("Point symbol cannot be projected in this view");
|
|
5326
|
+
const next = clonePointSymbol(symbol);
|
|
5327
|
+
const coordinate = mode === "ground" ? view.getCoordinateFromPixel?.(display) : void 0;
|
|
5328
|
+
if (mode === "ground" && !coordinate) throw new RangeError("Ground placement requires inverse projection");
|
|
5329
|
+
next.offset = {
|
|
5330
|
+
...next.offset,
|
|
5331
|
+
connector: next.offset?.connector ?? (render.offset.indicator ? "auto" : "direct"),
|
|
5332
|
+
placement: mode === "ground" ? {
|
|
5333
|
+
mode,
|
|
5334
|
+
position: [...coordinate]
|
|
5335
|
+
} : {
|
|
5336
|
+
mode,
|
|
5337
|
+
offset: [display[0] - ordinary.display[0], display[1] - ordinary.display[1]]
|
|
5338
|
+
}
|
|
5339
|
+
};
|
|
5340
|
+
return next;
|
|
5341
|
+
}
|
|
5342
|
+
function convertPointSymbolPlacement(symbol, render, view, mode) {
|
|
5343
|
+
const layout = resolvePointSymbolOffset(symbol, render, view);
|
|
5344
|
+
if (!layout || layout.status === "unsupported-presentation") throw new TypeError("Offset presentation is unavailable");
|
|
5345
|
+
return placePointSymbol(symbol, render, view, layout.display, mode);
|
|
5346
|
+
}
|
|
5347
|
+
/** Explicit alignment, also used with hysteresis by pointer editing. */
|
|
5348
|
+
function alignPointSymbolIndicator(symbol, render, view) {
|
|
5349
|
+
const layout = resolvePointSymbolOffset(symbol, render, view);
|
|
5350
|
+
if (!layout?.attachment || !layout.direction || !render.offset?.indicator) return symbol;
|
|
5351
|
+
const d = layout.direction;
|
|
5352
|
+
const delta = [layout.actual[0] - layout.attachment[0], layout.actual[1] - layout.attachment[1]];
|
|
5353
|
+
const forward = Math.max((render.offset.indicator.minLength ?? 0) * layout.scale, delta[0] * d[0] + delta[1] * d[1]);
|
|
5354
|
+
return placePointSymbol(symbol, render, view, [layout.display[0] + delta[0] - forward * d[0], layout.display[1] + delta[1] - forward * d[1]]);
|
|
5355
|
+
}
|
|
5356
|
+
function movePointSymbolLocation(symbol, render, view, position) {
|
|
5357
|
+
const before = resolvePointSymbolOffset(symbol, render, view);
|
|
5358
|
+
let next = {
|
|
5359
|
+
...clonePointSymbol(symbol),
|
|
5360
|
+
position: [...position]
|
|
5361
|
+
};
|
|
5362
|
+
if (!symbol.offset || !before || before.status === "unsupported-presentation") return next;
|
|
5363
|
+
next = placePointSymbol(next, render, view, before.display);
|
|
5364
|
+
const after = resolvePointSymbolOffset(next, render, view);
|
|
5365
|
+
if (before.junction && after?.attachment && after.direction && symbol.offset.indicatorLength !== void 0) next.offset.indicatorLength = indicatorLengthAt(before.junction, after.attachment, after.direction, after.scale, render.octagonSize);
|
|
5366
|
+
return next;
|
|
5367
|
+
}
|
|
5368
|
+
/** Canonical resource metadata shared by native adapters and external exporters. */
|
|
5369
|
+
function pointSymbolResource(render) {
|
|
5370
|
+
return {
|
|
5371
|
+
id: render.resourceId,
|
|
5372
|
+
svg: render.svg,
|
|
5373
|
+
width: render.width,
|
|
5374
|
+
height: render.height,
|
|
5375
|
+
anchor: [render.anchor.x, render.anchor.y],
|
|
5376
|
+
coreRect: {
|
|
5377
|
+
x: render.octagonAnchor.x - render.octagonSize / 2,
|
|
5378
|
+
y: render.octagonAnchor.y - render.octagonSize / 2,
|
|
5379
|
+
width: render.octagonSize,
|
|
5380
|
+
height: render.octagonSize
|
|
5381
|
+
}
|
|
5382
|
+
};
|
|
5383
|
+
}
|
|
5384
|
+
/**
|
|
5385
|
+
* Convert a resolved CSS-pixel layout to output units (e.g. 25.4 / 96 for mm).
|
|
5386
|
+
* Scales positions, body scale, line width and dashes together, including the
|
|
5387
|
+
* already-resolved offset and padding. Canonical resources, direction, rotation
|
|
5388
|
+
* and opacity stay unchanged. Does not mutate the input or authored symbol.
|
|
5389
|
+
*/
|
|
5390
|
+
function scalePointSymbolLayout(layout, factor) {
|
|
5391
|
+
if (!Number.isFinite(factor) || factor <= 0) throw new RangeError("Layout scale factor must be positive and finite");
|
|
5392
|
+
const point = ([x, y]) => [x * factor, y * factor];
|
|
5393
|
+
return {
|
|
5394
|
+
...layout,
|
|
5395
|
+
actual: point(layout.actual),
|
|
5396
|
+
display: point(layout.display),
|
|
5397
|
+
imageAnchor: point(layout.imageAnchor),
|
|
5398
|
+
ordinaryDisplay: point(layout.ordinaryDisplay),
|
|
5399
|
+
attachment: layout.attachment && point(layout.attachment),
|
|
5400
|
+
junction: layout.junction && point(layout.junction),
|
|
5401
|
+
path: layout.path.map(point),
|
|
5402
|
+
scale: layout.scale * factor,
|
|
5403
|
+
connectorStyle: {
|
|
5404
|
+
...layout.connectorStyle,
|
|
5405
|
+
strokeWidth: layout.connectorStyle.strokeWidth * factor,
|
|
5406
|
+
strokeDash: layout.connectorStyle.strokeDash.map((length) => length * factor)
|
|
5407
|
+
}
|
|
5408
|
+
};
|
|
5409
|
+
}
|
|
5410
|
+
//#endregion
|
|
5411
|
+
//#region src/tactical-draw/point-symbol-rendering.ts
|
|
5196
5412
|
/** Complete rotated logical footprint for selection and transform chrome. */
|
|
5197
5413
|
function pointSymbolFootprint(symbol, render, adapter) {
|
|
5198
|
-
const
|
|
5199
|
-
if (!
|
|
5414
|
+
const layout = resolvePointSymbolOffset(symbol, render, adapter);
|
|
5415
|
+
if (!layout) return null;
|
|
5416
|
+
const anchor = layout.imageAnchor;
|
|
5417
|
+
render = layout.render;
|
|
5200
5418
|
const scale = pointSymbolPixelScale(symbol, render, adapter.getResolution());
|
|
5201
5419
|
const angle = symbol.rotation - adapter.getBearing();
|
|
5202
5420
|
const centerOffsetX = (render.width / 2 - render.anchor.x) * scale;
|
|
@@ -5211,27 +5429,64 @@ function pointSymbolFootprint(symbol, render, adapter) {
|
|
|
5211
5429
|
};
|
|
5212
5430
|
return {
|
|
5213
5431
|
box,
|
|
5214
|
-
corners: boxCorners(box)
|
|
5432
|
+
corners: boxCorners(box),
|
|
5433
|
+
connector: layout.path
|
|
5215
5434
|
};
|
|
5216
5435
|
}
|
|
5436
|
+
function resolveAdapterLayout(symbol, render, adapter) {
|
|
5437
|
+
let layout = resolvePointSymbolOffset(symbol, render, adapter);
|
|
5438
|
+
if (layout && layout.kind !== "ordinary" && (!adapter.getCoordinateFromPixel?.(layout.imageAnchor) || layout.path.some((p) => !adapter.getCoordinateFromPixel?.(p)))) {
|
|
5439
|
+
const ordinary = resolvePointSymbolOffset({
|
|
5440
|
+
...symbol,
|
|
5441
|
+
offset: void 0
|
|
5442
|
+
}, render, adapter);
|
|
5443
|
+
layout = ordinary ? {
|
|
5444
|
+
...ordinary,
|
|
5445
|
+
status: "unsupported-presentation"
|
|
5446
|
+
} : null;
|
|
5447
|
+
}
|
|
5448
|
+
return layout;
|
|
5449
|
+
}
|
|
5217
5450
|
function renderPointSymbolForAdapter(adapter, capability, symbol) {
|
|
5218
|
-
const
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5451
|
+
const intrinsic = capability.render(symbol);
|
|
5452
|
+
const layout = resolveAdapterLayout(symbol, intrinsic, adapter);
|
|
5453
|
+
const render = layout?.render ?? intrinsic;
|
|
5454
|
+
const feature = pointSymbolFeature(symbol, render, adapter.getResolution());
|
|
5455
|
+
const coordinate = layout && adapter.getCoordinateFromPixel?.(layout.imageAnchor);
|
|
5456
|
+
if (coordinate) feature.geometry = {
|
|
5457
|
+
type: "Point",
|
|
5458
|
+
coordinates: coordinate
|
|
5459
|
+
};
|
|
5460
|
+
feature.properties.offsetStatus = layout?.status ?? "unsupported-presentation";
|
|
5461
|
+
feature.properties.connectorKind = layout?.kind ?? "ordinary";
|
|
5462
|
+
if (symbol.offset) {
|
|
5463
|
+
feature.properties.iconScreenRotation = symbol.rotation - adapter.getBearing();
|
|
5464
|
+
delete feature.properties.iconSizeMeters;
|
|
5465
|
+
delete feature.properties.iconOctagonSize;
|
|
5466
|
+
}
|
|
5467
|
+
const features = [];
|
|
5468
|
+
if (layout && layout.path.length > 1 && adapter.getCoordinateFromPixel) {
|
|
5469
|
+
const coordinates = layout.path.map((p) => adapter.getCoordinateFromPixel(p));
|
|
5470
|
+
if (coordinates.every((p) => p !== null)) features.push({
|
|
5471
|
+
type: "Feature",
|
|
5472
|
+
id: `${symbol.id}:connector`,
|
|
5473
|
+
geometry: {
|
|
5474
|
+
type: "LineString",
|
|
5475
|
+
coordinates
|
|
5476
|
+
},
|
|
5477
|
+
properties: {
|
|
5478
|
+
graphicId: symbol.id,
|
|
5479
|
+
graphicKind: "point-symbol",
|
|
5480
|
+
style: resolveSymbolColor(layout.connectorStyle, false)
|
|
5232
5481
|
}
|
|
5233
|
-
}
|
|
5234
|
-
|
|
5482
|
+
});
|
|
5483
|
+
}
|
|
5484
|
+
features.push(feature);
|
|
5485
|
+
return {
|
|
5486
|
+
render: intrinsic,
|
|
5487
|
+
layout,
|
|
5488
|
+
features,
|
|
5489
|
+
resource: layout?.resource ?? pointSymbolResource(render)
|
|
5235
5490
|
};
|
|
5236
5491
|
}
|
|
5237
5492
|
function pointSymbolFeature(symbol, render, resolution) {
|
|
@@ -5255,7 +5510,7 @@ function pointSymbolFeature(symbol, render, resolution) {
|
|
|
5255
5510
|
iconId: render.resourceId,
|
|
5256
5511
|
iconScale: pointSymbolPixelScale(symbol, render, resolution),
|
|
5257
5512
|
iconRotation: symbol.rotation,
|
|
5258
|
-
iconOpacity: symbol.style?.opacity ?? 1,
|
|
5513
|
+
iconOpacity: (symbol.style?.opacity ?? 1) * (symbol.style?.overallOpacity ?? 1),
|
|
5259
5514
|
...groundSizeProperties
|
|
5260
5515
|
}
|
|
5261
5516
|
};
|
|
@@ -5455,10 +5710,13 @@ function defaultGroupSizeAnchor(graphics) {
|
|
|
5455
5710
|
* a live session without a session boundary
|
|
5456
5711
|
*/
|
|
5457
5712
|
function runGroupTransform(opts) {
|
|
5458
|
-
const { adapter, capability, imageCapability, graphics, graphicsLayer, previewLayer, handlesLayer, graphicsStyle, signal, closeOnClickAway = true, interactionStyle, onSession, handleIdPrefix, publishWorkingGraphics, settleWorkingGraphics, isOnGraphicsLayer, onSettled, onClickAwayGraphicsPick } = opts;
|
|
5713
|
+
const { adapter, capability, imageCapability, graphics, graphicsLayer, previewLayer, handlesLayer, graphicsStyle, symbolColors, signal, closeOnClickAway = true, interactionStyle, onSession, handleIdPrefix, publishWorkingGraphics, settleWorkingGraphics, isOnGraphicsLayer, onSettled, onClickAwayGraphicsPick } = opts;
|
|
5459
5714
|
assertCapabilityFor(graphics);
|
|
5460
5715
|
validateImageResources(graphics);
|
|
5461
|
-
const renderOpts =
|
|
5716
|
+
const renderOpts = {
|
|
5717
|
+
graphicsStyle,
|
|
5718
|
+
symbolColors
|
|
5719
|
+
};
|
|
5462
5720
|
let sizeAnchor = opts.sizeAnchor ?? defaultGroupSizeAnchor(graphics);
|
|
5463
5721
|
/** Throw synchronously if any graphic in `list` is a point symbol and no capability was supplied. */
|
|
5464
5722
|
function assertCapabilityFor(list) {
|
|
@@ -5482,6 +5740,7 @@ function runGroupTransform(opts) {
|
|
|
5482
5740
|
function createMemberState(graphic) {
|
|
5483
5741
|
if (isAnchoredGraphic(graphic)) {
|
|
5484
5742
|
const editStartPoints = [[...graphic.position]];
|
|
5743
|
+
if (isPointSymbol(graphic) && graphic.offset?.placement.mode === "ground") editStartPoints.push([...graphic.offset.placement.position]);
|
|
5485
5744
|
return {
|
|
5486
5745
|
graphic,
|
|
5487
5746
|
editStartPoints,
|
|
@@ -5534,6 +5793,14 @@ function runGroupTransform(opts) {
|
|
|
5534
5793
|
symbol.position = [...member.workingPoints[0]];
|
|
5535
5794
|
symbol.size = resizePointSymbolSize(symbol.size, member.workingSize);
|
|
5536
5795
|
symbol.rotation = member.workingRotation;
|
|
5796
|
+
if (symbol.offset?.placement.mode === "ground") symbol.offset.placement.position = [...member.workingPoints[1]];
|
|
5797
|
+
else if (symbol.offset?.placement.mode === "screen-relative") {
|
|
5798
|
+
const [x, y] = symbol.offset.placement.offset;
|
|
5799
|
+
const angle = member.workingRotation - member.editStartRotation;
|
|
5800
|
+
const scale = member.workingSize / member.editStartSize;
|
|
5801
|
+
const [rx, ry] = rotate(x, y, angle);
|
|
5802
|
+
symbol.offset.placement.offset = [rx * scale, ry * scale];
|
|
5803
|
+
}
|
|
5537
5804
|
return symbol;
|
|
5538
5805
|
}
|
|
5539
5806
|
function buildWorkingImage(member) {
|
|
@@ -5724,7 +5991,7 @@ function runGroupTransform(opts) {
|
|
|
5724
5991
|
const symbol = buildWorkingPointSymbol(member);
|
|
5725
5992
|
const plan = renderPointSymbolForAdapter(adapter, capability, symbol);
|
|
5726
5993
|
iconResources = [plan.resource];
|
|
5727
|
-
features =
|
|
5994
|
+
features = plan.features;
|
|
5728
5995
|
const footprint = pointSymbolFootprint(symbol, plan.render, adapter);
|
|
5729
5996
|
if (footprint) boxPixels = footprint.corners;
|
|
5730
5997
|
} else {
|
|
@@ -6291,7 +6558,7 @@ function runPointSymbolDraw(options) {
|
|
|
6291
6558
|
const plan = renderPointSymbolForAdapter(adapter, capability, build(PREVIEW_ID$1, latestPosition));
|
|
6292
6559
|
ctx.writeLayer(previewLayer, [{
|
|
6293
6560
|
id: PREVIEW_ID$1,
|
|
6294
|
-
features:
|
|
6561
|
+
features: plan.features,
|
|
6295
6562
|
iconResources: [plan.resource]
|
|
6296
6563
|
}]);
|
|
6297
6564
|
};
|
|
@@ -6331,9 +6598,19 @@ function runPointSymbolDraw(options) {
|
|
|
6331
6598
|
}
|
|
6332
6599
|
//#endregion
|
|
6333
6600
|
//#region src/tactical-draw/point-symbol-edit-controller.ts
|
|
6601
|
+
/**
|
|
6602
|
+
* Rotating or resizing a displaced symbol keeps its body where the user sees
|
|
6603
|
+
* it (ADR-0054 rules 7/8). Both the pointer path and `updateGraphic` route
|
|
6604
|
+
* through here so the invariant is enforced in one place.
|
|
6605
|
+
*/
|
|
6606
|
+
function preserveDisplayPlacement(next, render, view, layout) {
|
|
6607
|
+
if (!next.offset || !render.offset) return next;
|
|
6608
|
+
if (!layout || layout.status === "unsupported-presentation") return next;
|
|
6609
|
+
return placePointSymbol(next, render, view, layout.display);
|
|
6610
|
+
}
|
|
6334
6611
|
function normalisePointModes(input) {
|
|
6335
6612
|
const modes = input ?? ["transform"];
|
|
6336
|
-
for (const mode of modes) if (mode !== "transform" && mode !== "delete") throw new TypeError(`Point-symbol editing does not support mode "${mode}"`);
|
|
6613
|
+
for (const mode of modes) if (mode !== "transform" && mode !== "delete" && mode !== "offset") throw new TypeError(`Point-symbol editing does not support mode "${mode}"`);
|
|
6337
6614
|
return modes.includes("delete") ? ["delete"] : [...new Set(modes)];
|
|
6338
6615
|
}
|
|
6339
6616
|
function symbolBox(adapter, symbol, rendered, padding = 0) {
|
|
@@ -6347,11 +6624,12 @@ function symbolBox(adapter, symbol, rendered, padding = 0) {
|
|
|
6347
6624
|
}
|
|
6348
6625
|
function runPointSymbolEdit(opts) {
|
|
6349
6626
|
const { adapter, capability, symbol, graphicsLayer, previewLayer, handlesLayer, signal, closeOnClickAway = true, interactionStyle, onSession, handleIdPrefix, settleToGraphics, publishWorkingGraphic, wasOnGraphicsLayer, onSettled, onClickAwayGraphicsPick, snap } = opts;
|
|
6350
|
-
let modes = normalisePointModes(opts.modes);
|
|
6627
|
+
let modes = normalisePointModes(opts.modes ?? (symbol.offset ? ["transform", "offset"] : ["transform"]));
|
|
6351
6628
|
let usesGraphicStack = wasOnGraphicsLayer;
|
|
6352
6629
|
let working = clonePointSymbol(symbol);
|
|
6353
6630
|
let dirty = false;
|
|
6354
6631
|
let box = null;
|
|
6632
|
+
let currentLayout = null;
|
|
6355
6633
|
let drag = null;
|
|
6356
6634
|
let ctx;
|
|
6357
6635
|
let driver;
|
|
@@ -6373,21 +6651,48 @@ function runPointSymbolEdit(opts) {
|
|
|
6373
6651
|
const render = () => {
|
|
6374
6652
|
if (ctx.settled) return;
|
|
6375
6653
|
const plan = renderPointSymbolForAdapter(adapter, capability, working);
|
|
6654
|
+
currentLayout = plan.layout;
|
|
6376
6655
|
const nextBox = symbolBox(adapter, working, plan.render, interactionStyle?.boxPadding);
|
|
6377
6656
|
const handles = [];
|
|
6378
6657
|
if (modes.includes("transform") && nextBox) {
|
|
6379
6658
|
handles.push(boxOutlineFeature(nextBox, (pixel) => driver.fromPixel(pixel), `${handleIdPrefix}:box-outline`, interactionStyle?.boxOutline));
|
|
6380
6659
|
handles.push(...toFeatures(boxToHandles(nextBox, (pixel) => driver.fromPixel(pixel)), handleIdPrefix, interactionStyle));
|
|
6381
6660
|
}
|
|
6661
|
+
if (modes.includes("offset") && working.offset && plan.layout?.status !== "unsupported-presentation") {
|
|
6662
|
+
[plan.layout?.actual, plan.layout?.junction].forEach((pixel, index) => {
|
|
6663
|
+
if (!pixel) return;
|
|
6664
|
+
handles.push(...toFeatures([{
|
|
6665
|
+
kind: index === 0 ? "vertex" : "option",
|
|
6666
|
+
index,
|
|
6667
|
+
position: driver.fromPixel(pixel)
|
|
6668
|
+
}], `${handleIdPrefix}:offset`, interactionStyle));
|
|
6669
|
+
});
|
|
6670
|
+
if (drag?.snapped && plan.layout?.attachment && plan.layout.direction) {
|
|
6671
|
+
const a = plan.layout.attachment, d = plan.layout.direction;
|
|
6672
|
+
handles.push({
|
|
6673
|
+
type: "Feature",
|
|
6674
|
+
id: `${handleIdPrefix}:alignment`,
|
|
6675
|
+
geometry: {
|
|
6676
|
+
type: "LineString",
|
|
6677
|
+
coordinates: [driver.fromPixel([a[0] - 200 * d[0], a[1] - 200 * d[1]]), driver.fromPixel([a[0] + 200 * d[0], a[1] + 200 * d[1]])]
|
|
6678
|
+
},
|
|
6679
|
+
properties: { style: {
|
|
6680
|
+
strokeColor: "#0284c7",
|
|
6681
|
+
strokeWidth: 1,
|
|
6682
|
+
strokeDash: [4, 4]
|
|
6683
|
+
} }
|
|
6684
|
+
});
|
|
6685
|
+
}
|
|
6686
|
+
}
|
|
6382
6687
|
publishWorkingGraphic({
|
|
6383
6688
|
id: working.id,
|
|
6384
|
-
features:
|
|
6689
|
+
features: plan.features,
|
|
6385
6690
|
iconResources: [plan.resource]
|
|
6386
6691
|
});
|
|
6387
6692
|
if (usesGraphicStack) ctx.writeLayer(previewLayer, []);
|
|
6388
6693
|
else ctx.writeLayer(previewLayer, [{
|
|
6389
6694
|
id: `__td_point_symbol_edit_preview__:${working.id}`,
|
|
6390
|
-
features:
|
|
6695
|
+
features: plan.features,
|
|
6391
6696
|
iconResources: [plan.resource]
|
|
6392
6697
|
}]);
|
|
6393
6698
|
box = nextBox;
|
|
@@ -6437,6 +6742,37 @@ function runPointSymbolEdit(opts) {
|
|
|
6437
6742
|
};
|
|
6438
6743
|
const session = {
|
|
6439
6744
|
history,
|
|
6745
|
+
offset: {
|
|
6746
|
+
get layout() {
|
|
6747
|
+
return currentLayout ? structuredClone(currentLayout) : null;
|
|
6748
|
+
},
|
|
6749
|
+
get connectors() {
|
|
6750
|
+
const metadata = capability.render(working).offset;
|
|
6751
|
+
return !metadata ? [] : metadata.indicator ? ["auto", "direct"] : ["direct"];
|
|
6752
|
+
},
|
|
6753
|
+
setPlacement(mode) {
|
|
6754
|
+
if (ctx.settled) return;
|
|
6755
|
+
apply(convertPointSymbolPlacement(working, capability.render(working), adapter, mode), "programmatic", "Placement mode");
|
|
6756
|
+
},
|
|
6757
|
+
setConnector(connector) {
|
|
6758
|
+
if (ctx.settled) return;
|
|
6759
|
+
const intrinsic = capability.render(working);
|
|
6760
|
+
if (!intrinsic.offset || connector !== "direct" && !intrinsic.offset.indicator) throw new TypeError("Unsupported connector");
|
|
6761
|
+
const next = working.offset ? clonePointSymbol(working) : convertPointSymbolPlacement(working, intrinsic, adapter, "screen-relative");
|
|
6762
|
+
next.offset.connector = connector;
|
|
6763
|
+
apply(next, "programmatic", "Connector preference");
|
|
6764
|
+
},
|
|
6765
|
+
align() {
|
|
6766
|
+
if (!ctx.settled) apply(alignPointSymbolIndicator(working, capability.render(working), adapter), "programmatic", "Align indicator");
|
|
6767
|
+
},
|
|
6768
|
+
reset() {
|
|
6769
|
+
if (!ctx.settled) {
|
|
6770
|
+
const next = clonePointSymbol(working);
|
|
6771
|
+
delete next.offset;
|
|
6772
|
+
apply(next, "programmatic", "Reset offset");
|
|
6773
|
+
}
|
|
6774
|
+
}
|
|
6775
|
+
},
|
|
6440
6776
|
graphic: clonePointSymbol(symbol),
|
|
6441
6777
|
get workingGraphic() {
|
|
6442
6778
|
return clonePointSymbol(working);
|
|
@@ -6462,8 +6798,8 @@ function runPointSymbolEdit(opts) {
|
|
|
6462
6798
|
if (ctx.settled) return;
|
|
6463
6799
|
const next = updater(clonePointSymbol(working));
|
|
6464
6800
|
if (next.kind !== "point-symbol" || next.id !== symbol.id) throw new TypeError("updateGraphic must preserve point-symbol kind and id");
|
|
6465
|
-
capability.render(next);
|
|
6466
|
-
apply(next, "programmatic", "Update graphic");
|
|
6801
|
+
const nextRender = capability.render(next);
|
|
6802
|
+
apply(working.offset !== void 0 && next.offset !== void 0 && deepEqual(working.offset, next.offset) && deepEqual(working.position, next.position) && (working.rotation !== next.rotation || !deepEqual(working.size, next.size)) ? preserveDisplayPlacement(next, nextRender, adapter, currentLayout) : next, "programmatic", "Update graphic");
|
|
6467
6803
|
},
|
|
6468
6804
|
onChange(listener) {
|
|
6469
6805
|
if (ctx.settled) return () => {};
|
|
@@ -6479,41 +6815,87 @@ function runPointSymbolEdit(opts) {
|
|
|
6479
6815
|
};
|
|
6480
6816
|
const pointerAngle = (pixel, anchor) => Math.atan2(pixel[1] - anchor[1], pixel[0] - anchor[0]);
|
|
6481
6817
|
const onDown = (pixel, info) => {
|
|
6482
|
-
if (ctx.settled || !box || !modes.includes("
|
|
6483
|
-
|
|
6484
|
-
if (!
|
|
6485
|
-
const
|
|
6486
|
-
|
|
6818
|
+
if (ctx.settled || !box || !currentLayout || modes.includes("delete")) return;
|
|
6819
|
+
let offsetMode = modes.includes("offset");
|
|
6820
|
+
if (!offsetMode && !modes.includes("transform")) return;
|
|
6821
|
+
const tolerance = info?.hitTolerance ?? 12;
|
|
6822
|
+
const near = (point) => point && pixelDistance$1(point, pixel) <= tolerance;
|
|
6823
|
+
let kind;
|
|
6824
|
+
if (offsetMode && working.offset && currentLayout.status !== "unsupported-presentation") {
|
|
6825
|
+
if (near(currentLayout.actual)) kind = "location";
|
|
6826
|
+
else if (near(currentLayout.junction)) kind = "junction";
|
|
6827
|
+
}
|
|
6828
|
+
if (!kind) {
|
|
6829
|
+
const hit = pickBoxHandle(box, pixel, tolerance);
|
|
6830
|
+
if (hit?.kind === "body" || hit && modes.includes("transform")) kind = hit?.kind;
|
|
6831
|
+
}
|
|
6832
|
+
const render = capability.render(working);
|
|
6833
|
+
const modifier = opts.offsetDragModifier?.();
|
|
6834
|
+
if (kind === "body" && modifier && info?.[`${modifier}Key`] && currentLayout.status !== "unsupported-presentation" && render.offset) offsetMode = true;
|
|
6835
|
+
if (!kind || offsetMode && !render.offset) return;
|
|
6836
|
+
const anchor = kind === "body" || kind === "location" || !working.offset ? currentLayout.actual : currentLayout.display;
|
|
6487
6837
|
drag = {
|
|
6488
|
-
kind
|
|
6838
|
+
kind,
|
|
6839
|
+
offsetMode,
|
|
6840
|
+
render,
|
|
6841
|
+
layout: structuredClone(currentLayout),
|
|
6842
|
+
snapped: false,
|
|
6489
6843
|
down: [...pixel],
|
|
6490
6844
|
start: clonePointSymbol(working),
|
|
6491
6845
|
anchor,
|
|
6492
6846
|
startPointerAngle: pointerAngle(pixel, anchor),
|
|
6493
|
-
startDistance: Math.max(1,
|
|
6847
|
+
startDistance: Math.max(1, pixelDistance$1(pixel, anchor))
|
|
6494
6848
|
};
|
|
6495
6849
|
driver.setDragPanEnabled(false);
|
|
6496
6850
|
};
|
|
6497
6851
|
const transformed = (pixel, info) => {
|
|
6498
6852
|
if (!drag) return null;
|
|
6499
|
-
|
|
6500
|
-
|
|
6853
|
+
let next = clonePointSymbol(drag.start);
|
|
6854
|
+
const intrinsic = drag.render;
|
|
6855
|
+
if (drag.kind === "body" && drag.offsetMode) {
|
|
6856
|
+
const display = [drag.layout.display[0] + pixel[0] - drag.down[0], drag.layout.display[1] + pixel[1] - drag.down[1]];
|
|
6857
|
+
next = placePointSymbol(next, intrinsic, adapter, display);
|
|
6858
|
+
const aligned = alignPointSymbolIndicator(next, intrinsic, adapter);
|
|
6859
|
+
const alignedLayout = resolvePointSymbolOffset(aligned, intrinsic, adapter);
|
|
6860
|
+
const gap = alignedLayout ? pixelDistance$1(alignedLayout.display, display) : Infinity;
|
|
6861
|
+
if (next.offset.connector === "auto" && gap <= (drag.snapped ? 10 : 6)) {
|
|
6862
|
+
next = aligned;
|
|
6863
|
+
drag.snapped = true;
|
|
6864
|
+
} else drag.snapped = false;
|
|
6865
|
+
return next;
|
|
6866
|
+
}
|
|
6867
|
+
if (drag.kind === "junction") {
|
|
6868
|
+
const layout = drag.layout;
|
|
6869
|
+
if (!layout.attachment || !layout.direction || !layout.junction || !next.offset) return null;
|
|
6870
|
+
const desired = [layout.junction[0] + pixel[0] - drag.down[0], layout.junction[1] + pixel[1] - drag.down[1]];
|
|
6871
|
+
next.offset.connector = "auto";
|
|
6872
|
+
next.offset.indicatorLength = indicatorLengthAt(desired, layout.attachment, layout.direction, layout.scale, intrinsic.octagonSize);
|
|
6873
|
+
return next;
|
|
6874
|
+
}
|
|
6875
|
+
if (drag.kind === "body" || drag.kind === "location") {
|
|
6501
6876
|
const unsnappedPosition = driver.fromPixel([drag.anchor[0] + pixel[0] - drag.down[0], drag.anchor[1] + pixel[1] - drag.down[1]]);
|
|
6502
6877
|
const anchorPixel = driver.toPixel(unsnappedPosition) ?? pixel;
|
|
6503
|
-
|
|
6878
|
+
const position = (snap?.({
|
|
6504
6879
|
coordinate: unsnappedPosition,
|
|
6505
6880
|
pixel: anchorPixel,
|
|
6506
6881
|
interaction: "translate",
|
|
6507
6882
|
graphicId: symbol.id,
|
|
6508
6883
|
originalEvent: info?.native
|
|
6509
6884
|
}))?.coordinate ?? unsnappedPosition;
|
|
6885
|
+
if (drag.kind === "location") return movePointSymbolLocation(next, intrinsic, adapter, position);
|
|
6886
|
+
next.position = position;
|
|
6887
|
+
if (next.offset?.placement.mode === "ground") {
|
|
6888
|
+
const actual = driver.toPixel(position);
|
|
6889
|
+
next = placePointSymbol(next, intrinsic, adapter, [drag.layout.display[0] + actual[0] - drag.layout.actual[0], drag.layout.display[1] + actual[1] - drag.layout.actual[1]]);
|
|
6890
|
+
}
|
|
6510
6891
|
} else if (drag.kind === "rotate") {
|
|
6511
6892
|
const rotation = drag.start.rotation + pointerAngle(pixel, drag.anchor) - drag.startPointerAngle;
|
|
6512
6893
|
next.rotation = normalizeRotation(info?.shiftKey ? constrainRotation(rotation) : rotation);
|
|
6513
6894
|
} else {
|
|
6514
|
-
const distance =
|
|
6895
|
+
const distance = pixelDistance$1(pixel, drag.anchor);
|
|
6515
6896
|
next.size = resizePointSymbolSize(drag.start.size, Math.max(.1, drag.start.size.value * (distance / drag.startDistance)));
|
|
6516
6897
|
}
|
|
6898
|
+
if (drag.kind === "rotate" || drag.kind === "scale") next = preserveDisplayPlacement(next, capability.render(next), adapter, drag.layout);
|
|
6517
6899
|
return next;
|
|
6518
6900
|
};
|
|
6519
6901
|
const onMove = (pixel, info) => {
|
|
@@ -6982,8 +7364,10 @@ var TacticalDraw = class {
|
|
|
6982
7364
|
handlesLayer;
|
|
6983
7365
|
ownedLayers;
|
|
6984
7366
|
graphicsStyle;
|
|
7367
|
+
symbolColors;
|
|
6985
7368
|
defaultInteractionStyle;
|
|
6986
7369
|
generateId;
|
|
7370
|
+
offsetDragModifier;
|
|
6987
7371
|
pointSymbols;
|
|
6988
7372
|
images;
|
|
6989
7373
|
snappingOptions;
|
|
@@ -7034,6 +7418,8 @@ var TacticalDraw = class {
|
|
|
7034
7418
|
transformCounter = 0;
|
|
7035
7419
|
/** Snapshot of the host's latest authoritative Graphic membership and order. */
|
|
7036
7420
|
lastRenderedMeasures = [];
|
|
7421
|
+
/** Whether any last-rendered graphic reads the whole camera, not just scale. */
|
|
7422
|
+
lastRenderedHasViewDependent = false;
|
|
7037
7423
|
snapGeometryFeatures = [];
|
|
7038
7424
|
/**
|
|
7039
7425
|
* Rendered graphics-layer groups, keyed on Graphic snapshot identity. Each
|
|
@@ -7072,6 +7458,8 @@ var TacticalDraw = class {
|
|
|
7072
7458
|
hasHighlightOutlines = false;
|
|
7073
7459
|
constructor(adapter, options) {
|
|
7074
7460
|
this.adapter = adapter;
|
|
7461
|
+
this.offsetDragModifier = options?.offsetDragModifier ?? false;
|
|
7462
|
+
this.symbolColors = options?.symbolColors ? { ...options.symbolColors } : void 0;
|
|
7075
7463
|
this.lastViewResolution = adapter.getResolution();
|
|
7076
7464
|
this.graphicsStyle = resolveStyleHints(BUILT_IN_GRAPHICS_STYLE, options?.graphicsStyle, void 0);
|
|
7077
7465
|
this.defaultInteractionStyle = options?.interactionStyle;
|
|
@@ -7097,6 +7485,11 @@ var TacticalDraw = class {
|
|
|
7097
7485
|
this.adapter.setLayerStyle(this.highlightLayer, guideToStyleOptions(this.baseHighlightStyle()));
|
|
7098
7486
|
this.adapter.onViewChange(this.handleViewChange);
|
|
7099
7487
|
}
|
|
7488
|
+
/** Change the default shortcut for subsequent drags, including in an active edit. */
|
|
7489
|
+
setOffsetDragModifier(modifier) {
|
|
7490
|
+
this.assertAlive();
|
|
7491
|
+
this.offsetDragModifier = modifier;
|
|
7492
|
+
}
|
|
7100
7493
|
/** Replace snapping configuration without recreating the map adapter or façade. */
|
|
7101
7494
|
setSnappingOptions(options) {
|
|
7102
7495
|
this.assertAlive();
|
|
@@ -7127,15 +7520,15 @@ var TacticalDraw = class {
|
|
|
7127
7520
|
renderGraphicGroup(graphic) {
|
|
7128
7521
|
const cached = this.graphicGroupCache.get(graphic);
|
|
7129
7522
|
if (cached) {
|
|
7130
|
-
if (
|
|
7131
|
-
if (Object.is(cached.resolution, this.adapter.getResolution())) return cached.group;
|
|
7523
|
+
if (cached.dependence === "none") return cached.group;
|
|
7524
|
+
if (cached.dependence === "resolution" && Object.is(cached.resolution, this.adapter.getResolution())) return cached.group;
|
|
7132
7525
|
}
|
|
7133
|
-
const
|
|
7134
|
-
const resolution =
|
|
7526
|
+
const dependence = graphicViewDependence(graphic, this.adapter.scalesGroundPointSymbolsNatively === true);
|
|
7527
|
+
const resolution = dependence === "none" ? void 0 : this.adapter.getResolution();
|
|
7135
7528
|
const group = this.buildGraphicGroup(graphic);
|
|
7136
7529
|
this.graphicGroupCache.set(graphic, {
|
|
7137
7530
|
group,
|
|
7138
|
-
|
|
7531
|
+
dependence,
|
|
7139
7532
|
resolution
|
|
7140
7533
|
});
|
|
7141
7534
|
return group;
|
|
@@ -7152,7 +7545,7 @@ var TacticalDraw = class {
|
|
|
7152
7545
|
const plan = renderPointSymbolForAdapter(this.adapter, this.pointSymbols, graphic);
|
|
7153
7546
|
return {
|
|
7154
7547
|
id: graphic.id,
|
|
7155
|
-
features:
|
|
7548
|
+
features: plan.features,
|
|
7156
7549
|
iconResources: [plan.resource],
|
|
7157
7550
|
revision
|
|
7158
7551
|
};
|
|
@@ -7168,7 +7561,10 @@ var TacticalDraw = class {
|
|
|
7168
7561
|
revision
|
|
7169
7562
|
};
|
|
7170
7563
|
}
|
|
7171
|
-
const render = renderMeasureForAdapter(this.adapter, graphic, {
|
|
7564
|
+
const render = renderMeasureForAdapter(this.adapter, graphic, {
|
|
7565
|
+
graphicsStyle: this.graphicsStyle,
|
|
7566
|
+
symbolColors: this.symbolColors
|
|
7567
|
+
});
|
|
7172
7568
|
return {
|
|
7173
7569
|
id: graphic.id,
|
|
7174
7570
|
features: render.features,
|
|
@@ -7230,11 +7626,11 @@ var TacticalDraw = class {
|
|
|
7230
7626
|
this.lastViewResolution = resolution;
|
|
7231
7627
|
if (!this.hasRendered) return;
|
|
7232
7628
|
this.renderHighlights();
|
|
7233
|
-
|
|
7629
|
+
const nativePointSymbolScaling = this.adapter.scalesGroundPointSymbolsNatively === true;
|
|
7630
|
+
if (!resolutionChanged && !this.lastRenderedHasViewDependent) return;
|
|
7234
7631
|
const excluded = this.graphicsStackExcludedIds();
|
|
7235
7632
|
const visible = excluded.size > 0 ? this.lastRenderedMeasures.filter((m) => !excluded.has(m.id)) : this.lastRenderedMeasures;
|
|
7236
|
-
|
|
7237
|
-
if (!visible.some((g) => graphicRendersFromViewState(g, nativePointSymbolScaling))) return;
|
|
7633
|
+
if (!visible.some((g) => graphicViewDependence(g, nativePointSymbolScaling) !== "none")) return;
|
|
7238
7634
|
this.reconcileGraphicsStack(visible);
|
|
7239
7635
|
};
|
|
7240
7636
|
/**
|
|
@@ -7318,6 +7714,7 @@ var TacticalDraw = class {
|
|
|
7318
7714
|
}
|
|
7319
7715
|
}
|
|
7320
7716
|
this.lastRenderedMeasures = measures.slice();
|
|
7717
|
+
this.lastRenderedHasViewDependent = this.lastRenderedMeasures.some((g) => graphicViewDependence(g, this.adapter.scalesGroundPointSymbolsNatively === true) === "view");
|
|
7321
7718
|
this.graphicStack.acknowledgeHostRender(measures);
|
|
7322
7719
|
if (activeEdit && "syncHostMembership" in activeEdit && measures.some((graphic) => graphic.id === activeEdit.measureId && graphic.kind === activeEdit.graphicKind)) activeEdit.syncHostMembership(true);
|
|
7323
7720
|
(this.activeTransform ?? this.activeTransformSessionRef)?.syncHostGraphics(measures);
|
|
@@ -7412,7 +7809,10 @@ var TacticalDraw = class {
|
|
|
7412
7809
|
if (!this.images) return null;
|
|
7413
7810
|
return imageFootprint(graphic, renderImageForAdapter(this.adapter, this.images, graphic).render, this.adapter)?.box ?? null;
|
|
7414
7811
|
}
|
|
7415
|
-
const render = renderMeasureForAdapter(this.adapter, graphic, {
|
|
7812
|
+
const render = renderMeasureForAdapter(this.adapter, graphic, {
|
|
7813
|
+
graphicsStyle: this.graphicsStyle,
|
|
7814
|
+
symbolColors: this.symbolColors
|
|
7815
|
+
});
|
|
7416
7816
|
if (graphic.kind === "text") return fitTextFootprintFromFeatures(render.features, (lonLat) => this.adapter.getPixelFromCoordinate(lonLat), {
|
|
7417
7817
|
resolution: this.adapter.getResolution(),
|
|
7418
7818
|
bearing
|
|
@@ -7576,6 +7976,7 @@ var TacticalDraw = class {
|
|
|
7576
7976
|
previewLayer: this.previewLayer,
|
|
7577
7977
|
guideLayer: this.guideLayer,
|
|
7578
7978
|
graphicsStyle: this.graphicsStyle,
|
|
7979
|
+
symbolColors: this.symbolColors,
|
|
7579
7980
|
generateId: () => measureDraft.id ?? this.generateId(),
|
|
7580
7981
|
signal: options?.signal,
|
|
7581
7982
|
measureOptions: measureDraft.options,
|
|
@@ -7594,6 +7995,7 @@ var TacticalDraw = class {
|
|
|
7594
7995
|
previewLayer: this.previewLayer,
|
|
7595
7996
|
guideLayer: this.guideLayer,
|
|
7596
7997
|
graphicsStyle: this.graphicsStyle,
|
|
7998
|
+
symbolColors: this.symbolColors,
|
|
7597
7999
|
generateId: () => measureDraft.id ?? this.generateId(),
|
|
7598
8000
|
signal: options?.signal,
|
|
7599
8001
|
onSession: this.trackSession(options?.onSession),
|
|
@@ -7664,6 +8066,7 @@ var TacticalDraw = class {
|
|
|
7664
8066
|
adapter: this.adapter,
|
|
7665
8067
|
capability: this.pointSymbols,
|
|
7666
8068
|
symbol: measure,
|
|
8069
|
+
offsetDragModifier: () => options?.offsetDragModifier ?? this.offsetDragModifier,
|
|
7667
8070
|
graphicsLayer: this.graphicsLayer,
|
|
7668
8071
|
previewLayer: this.previewLayer,
|
|
7669
8072
|
handlesLayer: this.handlesLayer,
|
|
@@ -7710,6 +8113,7 @@ var TacticalDraw = class {
|
|
|
7710
8113
|
guideLayer: this.guideLayer,
|
|
7711
8114
|
handlesLayer: this.handlesLayer,
|
|
7712
8115
|
graphicsStyle: this.graphicsStyle,
|
|
8116
|
+
symbolColors: this.symbolColors,
|
|
7713
8117
|
signal: options?.signal,
|
|
7714
8118
|
closeOnClickAway: options?.closeOnClickAway,
|
|
7715
8119
|
modes: options?.modes,
|
|
@@ -7764,6 +8168,7 @@ var TacticalDraw = class {
|
|
|
7764
8168
|
previewLayer: this.previewLayer,
|
|
7765
8169
|
handlesLayer: this.handlesLayer,
|
|
7766
8170
|
graphicsStyle: this.graphicsStyle,
|
|
8171
|
+
symbolColors: this.symbolColors,
|
|
7767
8172
|
signal: options?.signal,
|
|
7768
8173
|
closeOnClickAway: options?.closeOnClickAway,
|
|
7769
8174
|
interactionStyle: mergedInteractionStyle,
|
|
@@ -8133,23 +8538,28 @@ var TacticalDraw = class {
|
|
|
8133
8538
|
if (this.destroyed) throw new TacticalDrawDestroyedError();
|
|
8134
8539
|
}
|
|
8135
8540
|
};
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8541
|
+
/**
|
|
8542
|
+
* Classify what live view state a graphic's render reads. Offset point symbols
|
|
8543
|
+
* resolve their body and connector in pixel space, so they depend on the whole
|
|
8544
|
+
* camera rather than on scale alone.
|
|
8545
|
+
*/
|
|
8546
|
+
function graphicViewDependence(graphic, nativePointSymbolScaling) {
|
|
8547
|
+
if (isPointSymbol(graphic)) {
|
|
8548
|
+
if (graphic.offset) return "view";
|
|
8549
|
+
return graphic.size.unit === "meters" && !nativePointSymbolScaling ? "resolution" : "none";
|
|
8550
|
+
}
|
|
8551
|
+
if (isImageGraphic(graphic)) return "none";
|
|
8552
|
+
return measureRendersFromViewState(graphic) ? "resolution" : "none";
|
|
8140
8553
|
}
|
|
8141
8554
|
function normalisePointEditModes(modes) {
|
|
8142
|
-
for (const mode of modes ?? ["transform"]) if (mode !== "transform" && mode !== "delete") throw new TypeError(`Point-symbol editing does not support mode "${mode}"`);
|
|
8555
|
+
for (const mode of modes ?? ["transform"]) if (mode !== "transform" && mode !== "delete" && mode !== "offset") throw new TypeError(`Point-symbol editing does not support mode "${mode}"`);
|
|
8143
8556
|
}
|
|
8144
8557
|
/**
|
|
8145
8558
|
* Built-in default graphics style. Ensures OL / MapLibre / Leaflet render
|
|
8146
8559
|
* identically when neither host nor measure supplies a style — without it,
|
|
8147
8560
|
* each engine falls through to its own native (and different) vector default.
|
|
8148
8561
|
*/
|
|
8149
|
-
const BUILT_IN_GRAPHICS_STYLE = {
|
|
8150
|
-
color: "#000000",
|
|
8151
|
-
strokeWidth: 2
|
|
8152
|
-
};
|
|
8562
|
+
const BUILT_IN_GRAPHICS_STYLE = { strokeWidth: 2 };
|
|
8153
8563
|
/**
|
|
8154
8564
|
* Built-in defaults for the interaction-affordance slots (ADR-0006, extended
|
|
8155
8565
|
* for the ADR-0017 transform box). The scale (corner) grips and the rotate
|
|
@@ -8237,7 +8647,7 @@ function mergeSlot(base, override) {
|
|
|
8237
8647
|
}
|
|
8238
8648
|
function styleToStyleOptions(style) {
|
|
8239
8649
|
const out = {};
|
|
8240
|
-
const strokeColor = style.strokeColor ?? style.color;
|
|
8650
|
+
const strokeColor = style.strokeColor ?? style.color ?? DEFAULT_SYMBOL_COLOR;
|
|
8241
8651
|
if (strokeColor !== void 0) out.strokeColor = strokeColor;
|
|
8242
8652
|
if (style.strokeWidth !== void 0) out.strokeWidth = style.strokeWidth;
|
|
8243
8653
|
if (style.strokeDash !== void 0) out.strokeDash = [...style.strokeDash];
|
|
@@ -8526,4 +8936,4 @@ function nearestWorldCopyPosition(position, refLng) {
|
|
|
8526
8936
|
return [nearestWorldCopyLng(position[0], refLng), ...position.slice(1)];
|
|
8527
8937
|
}
|
|
8528
8938
|
//#endregion
|
|
8529
|
-
export { BaseMapAdapter, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, HATCH_PATTERNS, ImageCapabilityRequiredError, MINE_PATTERN_TILE_SIZE, PointSymbolCapabilityRequiredError, PointerCallbackHub, TEXT_HALO_COLOR, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, TacticalDrawDestroyedError, bindAbortable, cloneImageGraphic, clonePointSymbol, coerceHandleStyle, combineWithHostSignal, effectivePointSymbolPixelSize, evaluatePickCandidates, fillPatternImageDimension, fillPatternImageId, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, imageFootprint, imageOpacity, isAnchoredGraphic, isImageGraphic, isMinePattern, isPointSymbol, isTacticalDrawAbortError, makeFillPatternImage, mergeNativeAndCachedPickFeatures, nearestWorldCopyLng, nearestWorldCopyPosition, normalizeImageGraphic, pickHitTolerance, pickPointerType, pointSymbolFootprint, renderOrderToVisualOrder, resizePointSymbolSize, resolveGroundTextSizePixels, resolvePickPresentation, resolveTextHaloWidth, resolveTextSizePixels, tryControlMeasureIdFromFeature, validateImageGraphic };
|
|
8939
|
+
export { BaseMapAdapter, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, HATCH_PATTERNS, ImageCapabilityRequiredError, MINE_PATTERN_TILE_SIZE, PointSymbolCapabilityRequiredError, PointerCallbackHub, TEXT_HALO_COLOR, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, TacticalDrawDestroyedError, alignPointSymbolIndicator, bindAbortable, cloneImageGraphic, clonePointSymbol, coerceHandleStyle, combineWithHostSignal, convertPointSymbolPlacement, effectivePointSymbolPixelSize, evaluatePickCandidates, fillPatternImageDimension, fillPatternImageId, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, imageFootprint, imageOpacity, isAnchoredGraphic, isImageGraphic, isMinePattern, isPointSymbol, isTacticalDrawAbortError, makeFillPatternImage, mergeNativeAndCachedPickFeatures, movePointSymbolLocation, nearestWorldCopyLng, nearestWorldCopyPosition, normalizeImageGraphic, pickHitTolerance, pickPointerType, placePointSymbol, pointSymbolFootprint, pointSymbolResource, renderOrderToVisualOrder, resizePointSymbolSize, resolveGroundTextSizePixels, resolvePickPresentation, resolvePointSymbolOffset, resolveTextHaloWidth, resolveTextSizePixels, scalePointSymbolLayout, tryControlMeasureIdFromFeature, validateImageGraphic };
|