@orbat-mapper/tactical-draw 0.2.0-alpha.22 → 0.2.0-alpha.24
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 +68 -2
- package/dist/index.mjs +319 -49
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -264,6 +264,12 @@ interface MapAdapter {
|
|
|
264
264
|
*/
|
|
265
265
|
onViewChange(handler: () => void): void;
|
|
266
266
|
offViewChange(handler: () => void): void;
|
|
267
|
+
/**
|
|
268
|
+
* The engine evaluates the meters→pixels clamp for ground-sized point
|
|
269
|
+
* symbols inside its own style layer, so meters-sized point symbols don't
|
|
270
|
+
* need view-change re-renders for size.
|
|
271
|
+
*/
|
|
272
|
+
readonly scalesGroundPointSymbolsNatively?: boolean;
|
|
267
273
|
/**
|
|
268
274
|
* Construct an {@link EditPointerDriver} for `TacticalDraw`'s edit
|
|
269
275
|
* controller. The controller subscribes to pointer events on the returned
|
|
@@ -465,7 +471,9 @@ type PointSymbolSize = {
|
|
|
465
471
|
unit: "pixels";
|
|
466
472
|
} | {
|
|
467
473
|
value: number;
|
|
468
|
-
unit: "meters";
|
|
474
|
+
unit: "meters"; /** Floor for the on-screen projection of the ground-based size; none when omitted. */
|
|
475
|
+
minPixels?: number; /** Cap on the on-screen projection of the ground-based size — typically the pixel size the symbol had when anchored to ground; unclamped when omitted. */
|
|
476
|
+
maxPixels?: number;
|
|
469
477
|
};
|
|
470
478
|
interface PointSymbolGraphicModifiers {
|
|
471
479
|
/** Radians clockwise, relative to the point symbol's own orientation. */
|
|
@@ -1184,6 +1192,55 @@ interface DrawSession {
|
|
|
1184
1192
|
onSettled(handler: (reason: SettleReason) => void): () => void;
|
|
1185
1193
|
}
|
|
1186
1194
|
//#endregion
|
|
1195
|
+
//#region src/tactical-draw/snapping.d.ts
|
|
1196
|
+
type SnapInteraction = "draw" | "reshape" | "translate" | "label-drag";
|
|
1197
|
+
interface SnapCandidateRequest {
|
|
1198
|
+
coordinate: Position;
|
|
1199
|
+
pixel: PixelCoordinate;
|
|
1200
|
+
interaction: SnapInteraction;
|
|
1201
|
+
graphicId?: string;
|
|
1202
|
+
controlPointIndex?: number;
|
|
1203
|
+
originalEvent?: unknown;
|
|
1204
|
+
}
|
|
1205
|
+
interface SnapCandidate {
|
|
1206
|
+
id: string;
|
|
1207
|
+
coordinate: Position;
|
|
1208
|
+
kind: string;
|
|
1209
|
+
graphicId?: string;
|
|
1210
|
+
controlPointIndex?: number;
|
|
1211
|
+
priority?: number;
|
|
1212
|
+
}
|
|
1213
|
+
type SnapCandidateProvider = (request: SnapCandidateRequest) => readonly SnapCandidate[];
|
|
1214
|
+
interface SnapResult {
|
|
1215
|
+
coordinate: Position;
|
|
1216
|
+
pixel: PixelCoordinate;
|
|
1217
|
+
candidate: SnapCandidate;
|
|
1218
|
+
distancePixels: number;
|
|
1219
|
+
}
|
|
1220
|
+
interface GraphicGeometrySnappingOptions {
|
|
1221
|
+
/** Snap to the nearest point along generated linework and polygon boundaries. Default `true`. */
|
|
1222
|
+
edges?: boolean;
|
|
1223
|
+
/** Snap to generated geometry vertices. Default `false`. */
|
|
1224
|
+
vertices?: boolean;
|
|
1225
|
+
}
|
|
1226
|
+
interface SnappingOptions {
|
|
1227
|
+
enabled?: boolean;
|
|
1228
|
+
tolerancePixels?: number;
|
|
1229
|
+
sources?: {
|
|
1230
|
+
graphics?: boolean;
|
|
1231
|
+
/**
|
|
1232
|
+
* Snap to generated control-measure geometry. `true` enables edges with
|
|
1233
|
+
* vertices disabled; pass an object to select either candidate type.
|
|
1234
|
+
* Text/label point features and the active graphic are excluded.
|
|
1235
|
+
*/
|
|
1236
|
+
graphicGeometry?: boolean | GraphicGeometrySnappingOptions;
|
|
1237
|
+
external?: SnapCandidateProvider;
|
|
1238
|
+
};
|
|
1239
|
+
bypass?: (originalEvent: unknown) => boolean;
|
|
1240
|
+
/** Called for each resolution attempt; `null` clears a host-rendered indicator. */
|
|
1241
|
+
onSnap?: (result: SnapResult | null) => void;
|
|
1242
|
+
}
|
|
1243
|
+
//#endregion
|
|
1187
1244
|
//#region src/tactical-draw/point-symbol-draw-controller.d.ts
|
|
1188
1245
|
type DrawPointSymbolDraft = Pick<PointSymbol, "kind" | "sidc"> & Partial<Omit<PointSymbol, "kind" | "sidc">>;
|
|
1189
1246
|
//#endregion
|
|
@@ -1234,6 +1291,8 @@ interface TacticalDrawOptions {
|
|
|
1234
1291
|
* the draw API itself are deferred (PRD #40 user-story-5 note).
|
|
1235
1292
|
*/
|
|
1236
1293
|
generateId?: () => string;
|
|
1294
|
+
/** Optional engine-level snapping for draw and edit interactions. */
|
|
1295
|
+
snapping?: SnappingOptions;
|
|
1237
1296
|
}
|
|
1238
1297
|
interface EditOptions {
|
|
1239
1298
|
signal?: AbortSignal;
|
|
@@ -1382,6 +1441,8 @@ declare class TacticalDraw {
|
|
|
1382
1441
|
private readonly defaultInteractionStyle;
|
|
1383
1442
|
private readonly generateId;
|
|
1384
1443
|
private readonly pointSymbols;
|
|
1444
|
+
private snappingOptions;
|
|
1445
|
+
private readonly resolveSnap;
|
|
1385
1446
|
private destroyed;
|
|
1386
1447
|
private hasRendered;
|
|
1387
1448
|
private readonly activeInteraction;
|
|
@@ -1432,7 +1493,12 @@ declare class TacticalDraw {
|
|
|
1432
1493
|
* surrounding collection state.
|
|
1433
1494
|
*/
|
|
1434
1495
|
private lastRenderedMeasures;
|
|
1496
|
+
private snapGeometryFeatures;
|
|
1435
1497
|
constructor(adapter: MapAdapter, options?: TacticalDrawOptions);
|
|
1498
|
+
/** Replace snapping configuration without recreating the map adapter or façade. */
|
|
1499
|
+
setSnappingOptions(options?: SnappingOptions): void;
|
|
1500
|
+
/** Clear transient host affordances when snapping or its interaction ends. */
|
|
1501
|
+
private clearSnapResult;
|
|
1436
1502
|
/**
|
|
1437
1503
|
* Render `measures` into features and write them to the graphics layer.
|
|
1438
1504
|
* Routes every measure through [[renderMeasureForAdapter]] so pixel sizing is
|
|
@@ -1820,4 +1886,4 @@ declare function nearestWorldCopyLng(lng: number, refLng: number): number;
|
|
|
1820
1886
|
/** `nearestWorldCopyLng` applied to a lon/lat position. */
|
|
1821
1887
|
declare function nearestWorldCopyPosition(position: Position, refLng: number): Position;
|
|
1822
1888
|
//#endregion
|
|
1823
|
-
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 GraphicRender, type GraphicSnapshot, type GraphicTransformChangeEvent, type GraphicTransformSession, type GuideStyle, type HandleStyle, type IconResource, type InteractionHit, type InteractionStyle, type LayerId, type MapAdapter, type MapCursor, type MapEvent, type MapEventHandler, type NativeEventEntry, type PickEvent, type PickHandler, type PickOptions, type PixelCoordinate, type PointSymbol, type PointSymbolAnchor, type PointSymbolCapability, PointSymbolCapabilityRequiredError, type PointSymbolColors, type PointSymbolFootprint, type PointSymbolGraphicModifiers, type PointSymbolRender, type PointSymbolSize, type PointSymbolStyle, type PointerCallback, PointerCallbackHub, type PointerEventInfo, type SessionChangeSource, type SessionHistory, type SessionHistoryState, type SettleReason, type SizeAnchor, type StyleOptions, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, type TacticalDrawAbortReason, TacticalDrawDestroyedError, type TacticalDrawLayerOptions, type TacticalDrawOptions, type TextStyleOptions, type ToPixel, type TransformBox, type TransformChangeEvent, type TransformSession, bindAbortable, clonePointSymbol, coerceHandleStyle, combineWithHostSignal, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, isPointSymbol, isTacticalDrawAbortError, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, pointSymbolFootprint, resolveGroundTextSizePixels, tryControlMeasureIdFromFeature };
|
|
1889
|
+
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 GraphicTransformChangeEvent, type GraphicTransformSession, type GuideStyle, type HandleStyle, type IconResource, type InteractionHit, type InteractionStyle, type LayerId, type MapAdapter, type MapCursor, type MapEvent, type MapEventHandler, type NativeEventEntry, type PickEvent, type PickHandler, type PickOptions, type PixelCoordinate, type PointSymbol, type PointSymbolAnchor, type PointSymbolCapability, PointSymbolCapabilityRequiredError, type PointSymbolColors, type PointSymbolFootprint, type PointSymbolGraphicModifiers, type PointSymbolRender, type PointSymbolSize, type PointSymbolStyle, type PointerCallback, PointerCallbackHub, type PointerEventInfo, type SessionChangeSource, type SessionHistory, type SessionHistoryState, type SettleReason, type SizeAnchor, type SnapCandidate, type SnapCandidateProvider, type SnapCandidateRequest, type SnapInteraction, type SnapResult, type SnappingOptions, type StyleOptions, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, type TacticalDrawAbortReason, TacticalDrawDestroyedError, type TacticalDrawLayerOptions, type TacticalDrawOptions, type TextStyleOptions, type ToPixel, type TransformBox, type TransformChangeEvent, type TransformSession, bindAbortable, clonePointSymbol, coerceHandleStyle, combineWithHostSignal, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, isPointSymbol, isTacticalDrawAbortError, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, pointSymbolFootprint, resolveGroundTextSizePixels, tryControlMeasureIdFromFeature };
|
package/dist/index.mjs
CHANGED
|
@@ -298,21 +298,24 @@ function resolveGroundTextSizePixels(textSizeMeters, resolution, text) {
|
|
|
298
298
|
}
|
|
299
299
|
//#endregion
|
|
300
300
|
//#region src/gestures/hit-test.ts
|
|
301
|
-
/**
|
|
302
|
-
function
|
|
303
|
-
const a = toPixel(p1);
|
|
304
|
-
const b = toPixel(p2);
|
|
305
|
-
if (!a || !b) return null;
|
|
301
|
+
/** Clamped parameter t ∈ [0, 1] of `pixel` projected onto the segment a→b in pixel space, or null for a zero-length segment. */
|
|
302
|
+
function clampedSegmentParameter(pixel, a, b) {
|
|
306
303
|
const dx = b[0] - a[0];
|
|
307
304
|
const dy = b[1] - a[1];
|
|
308
305
|
const lengthSq = dx * dx + dy * dy;
|
|
309
306
|
if (lengthSq === 0) return null;
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const
|
|
307
|
+
const t = ((pixel[0] - a[0]) * dx + (pixel[1] - a[1]) * dy) / lengthSq;
|
|
308
|
+
return Math.max(0, Math.min(1, t));
|
|
309
|
+
}
|
|
310
|
+
/** Distance from `pixel` to the segment p1→p2 in pixel space, or null if either endpoint fails to project or the segment has zero length. */
|
|
311
|
+
function pixelDistanceToSegment(pixel, p1, p2, toPixel) {
|
|
312
|
+
const a = toPixel(p1);
|
|
313
|
+
const b = toPixel(p2);
|
|
314
|
+
if (!a || !b) return null;
|
|
315
|
+
const t = clampedSegmentParameter(pixel, a, b);
|
|
316
|
+
if (t === null) return null;
|
|
317
|
+
const ddx = pixel[0] - (a[0] + t * (b[0] - a[0]));
|
|
318
|
+
const ddy = pixel[1] - (a[1] + t * (b[1] - a[1]));
|
|
316
319
|
return Math.sqrt(ddx * ddx + ddy * ddy);
|
|
317
320
|
}
|
|
318
321
|
/** Even-odd point-in-ring test in pixel space. */
|
|
@@ -1013,6 +1016,188 @@ function buildGuideFeatures(points, isAreaKind, trailingFixedSlots = 0) {
|
|
|
1013
1016
|
return features;
|
|
1014
1017
|
}
|
|
1015
1018
|
//#endregion
|
|
1019
|
+
//#region src/tactical-draw/snapping.ts
|
|
1020
|
+
/** Resolve a draw-interaction map event through `snap`, falling back to the event coordinate. */
|
|
1021
|
+
function snappedDrawPosition(snap, event) {
|
|
1022
|
+
return snap?.({
|
|
1023
|
+
coordinate: event.coordinate,
|
|
1024
|
+
pixel: event.pixel,
|
|
1025
|
+
interaction: "draw",
|
|
1026
|
+
originalEvent: event.originalEvent
|
|
1027
|
+
})?.coordinate ?? event.coordinate;
|
|
1028
|
+
}
|
|
1029
|
+
const DEFAULT_TOLERANCE_PIXELS = 12;
|
|
1030
|
+
function graphicCandidates(graphics) {
|
|
1031
|
+
const candidates = [];
|
|
1032
|
+
for (const graphic of graphics) {
|
|
1033
|
+
if (isPointSymbol(graphic)) {
|
|
1034
|
+
candidates.push({
|
|
1035
|
+
id: `${graphic.id}:anchor`,
|
|
1036
|
+
coordinate: graphic.position,
|
|
1037
|
+
kind: "anchor",
|
|
1038
|
+
graphicId: graphic.id
|
|
1039
|
+
});
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
graphic.controlPoints.forEach((coordinate, controlPointIndex) => {
|
|
1043
|
+
candidates.push({
|
|
1044
|
+
id: `${graphic.id}:control-point:${controlPointIndex}`,
|
|
1045
|
+
coordinate,
|
|
1046
|
+
kind: "control-point",
|
|
1047
|
+
graphicId: graphic.id,
|
|
1048
|
+
controlPointIndex
|
|
1049
|
+
});
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
return candidates;
|
|
1053
|
+
}
|
|
1054
|
+
function isSelfCandidate(candidate, request) {
|
|
1055
|
+
if (request.graphicId === void 0 || candidate.graphicId !== request.graphicId) return false;
|
|
1056
|
+
if (request.interaction === "reshape" && request.controlPointIndex !== void 0) return candidate.controlPointIndex === void 0 || candidate.controlPointIndex === request.controlPointIndex;
|
|
1057
|
+
return true;
|
|
1058
|
+
}
|
|
1059
|
+
function isLabelFeature(feature) {
|
|
1060
|
+
const properties = feature.properties;
|
|
1061
|
+
return properties?.part === "label" || typeof properties?.labelPlacementKey === "string" || typeof properties?.text === "string";
|
|
1062
|
+
}
|
|
1063
|
+
function collectGeometryPaths(geometry, featureId, graphicId, out) {
|
|
1064
|
+
const add = (positions, suffix) => {
|
|
1065
|
+
if (positions.length > 0) out.push({
|
|
1066
|
+
id: `${featureId}:${suffix}`,
|
|
1067
|
+
graphicId,
|
|
1068
|
+
positions
|
|
1069
|
+
});
|
|
1070
|
+
};
|
|
1071
|
+
switch (geometry.type) {
|
|
1072
|
+
case "LineString":
|
|
1073
|
+
add(geometry.coordinates, "line");
|
|
1074
|
+
break;
|
|
1075
|
+
case "MultiLineString":
|
|
1076
|
+
geometry.coordinates.forEach((line, index) => add(line, `line:${index}`));
|
|
1077
|
+
break;
|
|
1078
|
+
case "Polygon":
|
|
1079
|
+
geometry.coordinates.forEach((ring, index) => add(ring, `ring:${index}`));
|
|
1080
|
+
break;
|
|
1081
|
+
case "MultiPolygon":
|
|
1082
|
+
geometry.coordinates.forEach((polygon, polygonIndex) => polygon.forEach((ring, ringIndex) => add(ring, `ring:${polygonIndex}:${ringIndex}`)));
|
|
1083
|
+
break;
|
|
1084
|
+
case "GeometryCollection":
|
|
1085
|
+
geometry.geometries.forEach((child, index) => collectGeometryPaths(child, `${featureId}:geometry:${index}`, graphicId, out));
|
|
1086
|
+
break;
|
|
1087
|
+
case "Point":
|
|
1088
|
+
case "MultiPoint": break;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
function indexGeometryFeatures(features) {
|
|
1092
|
+
const paths = [];
|
|
1093
|
+
features.forEach((feature, index) => {
|
|
1094
|
+
if (!feature.geometry || isLabelFeature(feature)) return;
|
|
1095
|
+
const graphicId = tryControlMeasureIdFromFeature(feature);
|
|
1096
|
+
if (!graphicId) return;
|
|
1097
|
+
const featureId = typeof feature.id === "string" ? feature.id : `${graphicId}:feature:${index}`;
|
|
1098
|
+
collectGeometryPaths(feature.geometry, featureId, graphicId, paths);
|
|
1099
|
+
});
|
|
1100
|
+
return paths;
|
|
1101
|
+
}
|
|
1102
|
+
function interpolatePosition(a, b, t) {
|
|
1103
|
+
const dimensions = Math.min(a.length, b.length);
|
|
1104
|
+
const position = [];
|
|
1105
|
+
for (let index = 0; index < dimensions; index += 1) position.push(a[index] + (b[index] - a[index]) * t);
|
|
1106
|
+
return position;
|
|
1107
|
+
}
|
|
1108
|
+
function generatedGeometryCandidates(adapter, paths, request, options) {
|
|
1109
|
+
const resolved = options === true ? {} : options;
|
|
1110
|
+
const edges = resolved.edges ?? true;
|
|
1111
|
+
const vertices = resolved.vertices ?? false;
|
|
1112
|
+
const candidates = [];
|
|
1113
|
+
for (const path of paths) {
|
|
1114
|
+
if (vertices) path.positions.forEach((coordinate, index) => {
|
|
1115
|
+
candidates.push({
|
|
1116
|
+
id: `${path.id}:vertex:${index}`,
|
|
1117
|
+
coordinate,
|
|
1118
|
+
kind: "graphic-vertex",
|
|
1119
|
+
graphicId: path.graphicId
|
|
1120
|
+
});
|
|
1121
|
+
});
|
|
1122
|
+
if (!edges) continue;
|
|
1123
|
+
for (let index = 1; index < path.positions.length; index += 1) {
|
|
1124
|
+
const a = path.positions[index - 1];
|
|
1125
|
+
const b = path.positions[index];
|
|
1126
|
+
const aPixel = adapter.getPixelFromCoordinate(a);
|
|
1127
|
+
const bPixel = adapter.getPixelFromCoordinate(b);
|
|
1128
|
+
if (!aPixel || !bPixel) continue;
|
|
1129
|
+
const t = clampedSegmentParameter(request.pixel, aPixel, bPixel) ?? 0;
|
|
1130
|
+
candidates.push({
|
|
1131
|
+
id: `${path.id}:edge:${index - 1}`,
|
|
1132
|
+
coordinate: interpolatePosition(a, b, t),
|
|
1133
|
+
kind: "graphic-edge",
|
|
1134
|
+
graphicId: path.graphicId
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
return candidates;
|
|
1139
|
+
}
|
|
1140
|
+
function createSnapResolver(adapter, getGraphics, getGeometryFeatures, getOptions) {
|
|
1141
|
+
let indexedGraphics;
|
|
1142
|
+
let indexedCandidates = [];
|
|
1143
|
+
let indexedFeatures;
|
|
1144
|
+
let indexedGeometryPaths = [];
|
|
1145
|
+
const builtInCandidates = () => {
|
|
1146
|
+
const graphics = getGraphics();
|
|
1147
|
+
if (graphics !== indexedGraphics) {
|
|
1148
|
+
indexedGraphics = graphics;
|
|
1149
|
+
indexedCandidates = graphicCandidates(graphics);
|
|
1150
|
+
}
|
|
1151
|
+
return indexedCandidates;
|
|
1152
|
+
};
|
|
1153
|
+
const geometryPaths = () => {
|
|
1154
|
+
const features = getGeometryFeatures();
|
|
1155
|
+
if (features !== indexedFeatures) {
|
|
1156
|
+
indexedFeatures = features;
|
|
1157
|
+
indexedGeometryPaths = indexGeometryFeatures(features);
|
|
1158
|
+
}
|
|
1159
|
+
return indexedGeometryPaths;
|
|
1160
|
+
};
|
|
1161
|
+
return (request) => {
|
|
1162
|
+
const options = getOptions();
|
|
1163
|
+
const clear = () => options?.onSnap?.(null);
|
|
1164
|
+
if (!options?.enabled || options.bypass?.(request.originalEvent)) {
|
|
1165
|
+
clear();
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
const tolerance = options.tolerancePixels ?? DEFAULT_TOLERANCE_PIXELS;
|
|
1169
|
+
if (!Number.isFinite(tolerance) || tolerance < 0) {
|
|
1170
|
+
clear();
|
|
1171
|
+
return null;
|
|
1172
|
+
}
|
|
1173
|
+
const candidateLists = [];
|
|
1174
|
+
if (options.sources?.graphics !== false) candidateLists.push(builtInCandidates());
|
|
1175
|
+
const graphicGeometry = options.sources?.graphicGeometry;
|
|
1176
|
+
if (graphicGeometry) candidateLists.push(generatedGeometryCandidates(adapter, geometryPaths(), request, graphicGeometry));
|
|
1177
|
+
const external = options.sources?.external?.(request);
|
|
1178
|
+
if (external) candidateLists.push(external);
|
|
1179
|
+
let best = null;
|
|
1180
|
+
for (const list of candidateLists) for (const candidate of list) {
|
|
1181
|
+
if (isSelfCandidate(candidate, request)) continue;
|
|
1182
|
+
const pixel = adapter.getPixelFromCoordinate(candidate.coordinate);
|
|
1183
|
+
if (!pixel) continue;
|
|
1184
|
+
const distancePixels = Math.hypot(pixel[0] - request.pixel[0], pixel[1] - request.pixel[1]);
|
|
1185
|
+
if (distancePixels > tolerance) continue;
|
|
1186
|
+
const priority = candidate.priority ?? 0;
|
|
1187
|
+
const bestPriority = best?.candidate.priority ?? 0;
|
|
1188
|
+
if (best && (distancePixels > best.distancePixels || distancePixels === best.distancePixels && priority <= bestPriority)) continue;
|
|
1189
|
+
best = {
|
|
1190
|
+
coordinate: [...candidate.coordinate],
|
|
1191
|
+
pixel: [...pixel],
|
|
1192
|
+
candidate,
|
|
1193
|
+
distancePixels
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
options.onSnap?.(best);
|
|
1197
|
+
return best;
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
//#endregion
|
|
1016
1201
|
//#region src/tactical-draw/draw-controller.ts
|
|
1017
1202
|
const PREVIEW_CM_ID$1 = "__td_preview__";
|
|
1018
1203
|
const PREVIEW_CURSOR$1 = "crosshair";
|
|
@@ -1030,7 +1215,7 @@ const PREVIEW_CURSOR$1 = "crosshair";
|
|
|
1030
1215
|
* kinds need no host UI.
|
|
1031
1216
|
*/
|
|
1032
1217
|
function runFixedLengthDraw(options) {
|
|
1033
|
-
const { adapter, kind, previewLayer, guideLayer, graphicsStyle, generateId, signal, measureOptions, measureStyle, measureProperties, measureTextAmplifiers, guide: guideOpt, sizeAnchor, onSettled } = options;
|
|
1218
|
+
const { adapter, kind, previewLayer, guideLayer, graphicsStyle, generateId, signal, measureOptions, measureStyle, measureProperties, measureTextAmplifiers, guide: guideOpt, sizeAnchor, onSettled, snap } = options;
|
|
1034
1219
|
const metadata = getControlMeasureMetadata(kind);
|
|
1035
1220
|
const rule = metadata.rule;
|
|
1036
1221
|
const min = metadata.minCoordinates;
|
|
@@ -1048,7 +1233,7 @@ function runFixedLengthDraw(options) {
|
|
|
1048
1233
|
const savedCursor = "";
|
|
1049
1234
|
const handleClick = (event) => {
|
|
1050
1235
|
if (ctx.settled) return;
|
|
1051
|
-
committed.push(event
|
|
1236
|
+
committed.push(snappedDrawPosition(snap, event));
|
|
1052
1237
|
rubberBand = void 0;
|
|
1053
1238
|
if (committed.length >= required) {
|
|
1054
1239
|
ctx.commit(buildSnapshot);
|
|
@@ -1059,7 +1244,7 @@ function runFixedLengthDraw(options) {
|
|
|
1059
1244
|
const handlePointerMove = (event) => {
|
|
1060
1245
|
if (ctx.settled) return;
|
|
1061
1246
|
if (committed.length === 0) return;
|
|
1062
|
-
rubberBand = event
|
|
1247
|
+
rubberBand = snappedDrawPosition(snap, event);
|
|
1063
1248
|
renderPreview();
|
|
1064
1249
|
};
|
|
1065
1250
|
function renderPreview() {
|
|
@@ -1285,7 +1470,7 @@ const PREVIEW_CURSOR = "crosshair";
|
|
|
1285
1470
|
* added point again" as additional gesture-level commit triggers.
|
|
1286
1471
|
*/
|
|
1287
1472
|
function runVariableLengthDraw(options) {
|
|
1288
|
-
const { adapter, kind, previewLayer, guideLayer, graphicsStyle, generateId, signal, onSession, seed, measureOptions, measureStyle, measureProperties, measureTextAmplifiers, guide: guideOpt, sizeAnchor, onSettled } = options;
|
|
1473
|
+
const { adapter, kind, previewLayer, guideLayer, graphicsStyle, generateId, signal, onSession, seed, measureOptions, measureStyle, measureProperties, measureTextAmplifiers, guide: guideOpt, sizeAnchor, onSettled, snap } = options;
|
|
1289
1474
|
const metadata = getControlMeasureMetadata(kind);
|
|
1290
1475
|
if (typeof metadata.minCoordinates !== "number") throw new Error(`runVariableLengthDraw: kind "${kind}" has no minCoordinates metadata`);
|
|
1291
1476
|
const min = metadata.minCoordinates;
|
|
@@ -1392,7 +1577,7 @@ function runVariableLengthDraw(options) {
|
|
|
1392
1577
|
}
|
|
1393
1578
|
const handleClick = (event) => {
|
|
1394
1579
|
if (ctx.settled) return;
|
|
1395
|
-
const pos = roundControlPoint(event
|
|
1580
|
+
const pos = roundControlPoint(snappedDrawPosition(snap, event));
|
|
1396
1581
|
const lastSpineIdx = committed.length - 1 - trailingFixed;
|
|
1397
1582
|
const finishIndices = closedRing && lastSpineIdx > 0 ? [lastSpineIdx, 0] : [lastSpineIdx];
|
|
1398
1583
|
for (let i = 0; i < committed.length; i++) if (positionsEqual(pos, committed[i])) {
|
|
@@ -1433,7 +1618,7 @@ function runVariableLengthDraw(options) {
|
|
|
1433
1618
|
const handlePointerMove = (event) => {
|
|
1434
1619
|
if (ctx.settled) return;
|
|
1435
1620
|
if (committed.length === 0) return;
|
|
1436
|
-
rubberBand = event
|
|
1621
|
+
rubberBand = snappedDrawPosition(snap, event);
|
|
1437
1622
|
renderPreview();
|
|
1438
1623
|
};
|
|
1439
1624
|
function renderPreview() {
|
|
@@ -2435,7 +2620,7 @@ function mergeAmplifierPlacementField(existing, incoming) {
|
|
|
2435
2620
|
* - close / abort / signal / preempt / destroy paths
|
|
2436
2621
|
*/
|
|
2437
2622
|
function runEdit(opts) {
|
|
2438
|
-
const { adapter, measure, graphicsLayer, previewLayer, guideLayer, handlesLayer, graphicsStyle, signal, closeOnClickAway = true, modes: initialModes, interactionStyle, onSession, handleIdPrefix, guide: guideOpt, restoreOriginalToGraphics, wasOnGraphicsLayer, onSettled, onClickAwayGraphicsPick } = opts;
|
|
2623
|
+
const { adapter, measure, graphicsLayer, previewLayer, guideLayer, handlesLayer, graphicsStyle, signal, closeOnClickAway = true, modes: initialModes, interactionStyle, onSession, handleIdPrefix, guide: guideOpt, restoreOriginalToGraphics, wasOnGraphicsLayer, onSettled, onClickAwayGraphicsPick, snap } = opts;
|
|
2439
2624
|
const renderOpts = graphicsStyle ? { graphicsStyle } : void 0;
|
|
2440
2625
|
let sizeAnchor = opts.sizeAnchor ?? "ground";
|
|
2441
2626
|
const metadata = getControlMeasureMetadata(measure.kind);
|
|
@@ -3169,6 +3354,38 @@ function runEdit(opts) {
|
|
|
3169
3354
|
}
|
|
3170
3355
|
};
|
|
3171
3356
|
let activeDrag = null;
|
|
3357
|
+
function snappedPixel(pixel, info, interaction, controlPointIndex) {
|
|
3358
|
+
if (!snap) return pixel;
|
|
3359
|
+
return snap({
|
|
3360
|
+
coordinate: driver.fromPixel(pixel),
|
|
3361
|
+
pixel,
|
|
3362
|
+
interaction,
|
|
3363
|
+
graphicId: measure.id,
|
|
3364
|
+
controlPointIndex,
|
|
3365
|
+
originalEvent: info?.native
|
|
3366
|
+
})?.pixel ?? pixel;
|
|
3367
|
+
}
|
|
3368
|
+
function translatedBoxPixel(pixel, info, drag) {
|
|
3369
|
+
if (!snap || drag.fsm.kind !== "body") return pixel;
|
|
3370
|
+
const first = drag.preDragPoints[0];
|
|
3371
|
+
if (!first) return pixel;
|
|
3372
|
+
const cursor = driver.fromPixel(pixel);
|
|
3373
|
+
const origin = driver.fromPixel(drag.downPixel);
|
|
3374
|
+
const anchor = [...first];
|
|
3375
|
+
anchor[0] = first[0] + cursor[0] - origin[0];
|
|
3376
|
+
anchor[1] = first[1] + cursor[1] - origin[1];
|
|
3377
|
+
const anchorPixel = driver.toPixel(anchor);
|
|
3378
|
+
if (!anchorPixel) return pixel;
|
|
3379
|
+
const result = snap({
|
|
3380
|
+
coordinate: anchor,
|
|
3381
|
+
pixel: anchorPixel,
|
|
3382
|
+
interaction: "translate",
|
|
3383
|
+
graphicId: measure.id,
|
|
3384
|
+
originalEvent: info?.native
|
|
3385
|
+
});
|
|
3386
|
+
if (!result) return pixel;
|
|
3387
|
+
return [pixel[0] + result.pixel[0] - anchorPixel[0], pixel[1] + result.pixel[1] - anchorPixel[1]];
|
|
3388
|
+
}
|
|
3172
3389
|
/**
|
|
3173
3390
|
* A live label *move* drag: direct label grab, the floating grip, or a body
|
|
3174
3391
|
* grab inside the selected label's transform box (ADR-0017 box machinery
|
|
@@ -3370,7 +3587,8 @@ function runEdit(opts) {
|
|
|
3370
3587
|
});
|
|
3371
3588
|
activeDrag = {
|
|
3372
3589
|
fsm,
|
|
3373
|
-
preDragPoints
|
|
3590
|
+
preDragPoints,
|
|
3591
|
+
controlPointIndex: hit.kind === "midpoint" ? hit.index + 1 : hit.index
|
|
3374
3592
|
};
|
|
3375
3593
|
if (fsm.kind === "midpoint") {
|
|
3376
3594
|
workingPoints.splice(0, workingPoints.length, ...initialPoints);
|
|
@@ -3415,17 +3633,19 @@ function runEdit(opts) {
|
|
|
3415
3633
|
}
|
|
3416
3634
|
}
|
|
3417
3635
|
};
|
|
3418
|
-
function paintDragSnapshot(pixel) {
|
|
3636
|
+
function paintDragSnapshot(pixel, info) {
|
|
3419
3637
|
if (!activeDrag) return;
|
|
3420
|
-
const
|
|
3638
|
+
const resolvedPixel = snappedPixel(pixel, info, "reshape", activeDrag.controlPointIndex);
|
|
3639
|
+
const snapshot = activeDrag.fsm.move(resolvedPixel);
|
|
3421
3640
|
workingPoints.splice(0, workingPoints.length, ...snapshot.points);
|
|
3422
3641
|
renderPreview();
|
|
3423
3642
|
renderHandlesLayer();
|
|
3424
3643
|
recomputeDirty();
|
|
3425
3644
|
}
|
|
3426
|
-
function paintBoxDragSnapshot(pixel, constrainRotation = false) {
|
|
3645
|
+
function paintBoxDragSnapshot(pixel, info, constrainRotation = false) {
|
|
3427
3646
|
if (!activeBoxDrag) return;
|
|
3428
|
-
const
|
|
3647
|
+
const resolvedPixel = translatedBoxPixel(pixel, info, activeBoxDrag);
|
|
3648
|
+
const snapshot = activeBoxDrag.fsm.move(resolvedPixel, { constrainRotation });
|
|
3429
3649
|
activeBoxDrag.lastSnapshot = snapshot;
|
|
3430
3650
|
workingPoints.splice(0, workingPoints.length, ...snapshot.points);
|
|
3431
3651
|
workingAmplifierPlacements = transformAmplifierPlacements(activeBoxDrag.preDragAmplifierPlacements, activeBoxDrag.preDragBox, snapshot.box, driver);
|
|
@@ -3468,8 +3688,8 @@ function runEdit(opts) {
|
|
|
3468
3688
|
};
|
|
3469
3689
|
renderPreview();
|
|
3470
3690
|
renderHandlesLayer();
|
|
3471
|
-
} else if (activeDrag) paintDragSnapshot(pixel);
|
|
3472
|
-
else if (activeBoxDrag) paintBoxDragSnapshot(pixel, info?.shiftKey);
|
|
3691
|
+
} else if (activeDrag) paintDragSnapshot(pixel, info);
|
|
3692
|
+
else if (activeBoxDrag) paintBoxDragSnapshot(pixel, info, info?.shiftKey);
|
|
3473
3693
|
};
|
|
3474
3694
|
/** Reset drag-in-flight bookkeeping shared by every "gesture ended" path. */
|
|
3475
3695
|
function endDragBookkeeping() {
|
|
@@ -3516,10 +3736,10 @@ function runEdit(opts) {
|
|
|
3516
3736
|
driver.setDragPanEnabled(true);
|
|
3517
3737
|
return;
|
|
3518
3738
|
}
|
|
3519
|
-
const { fsm, preDragPoints } = activeDrag;
|
|
3739
|
+
const { fsm, preDragPoints, controlPointIndex } = activeDrag;
|
|
3520
3740
|
activeDrag = null;
|
|
3521
3741
|
driver.setDragPanEnabled(true);
|
|
3522
|
-
const next = fsm.end(pixel);
|
|
3742
|
+
const next = fsm.end(snappedPixel(pixel, info, "reshape", controlPointIndex));
|
|
3523
3743
|
replaceWorkingPoints(preDragPoints);
|
|
3524
3744
|
commitGesture(next);
|
|
3525
3745
|
return;
|
|
@@ -3545,7 +3765,12 @@ function runEdit(opts) {
|
|
|
3545
3765
|
bodyClickPending = true;
|
|
3546
3766
|
return;
|
|
3547
3767
|
}
|
|
3548
|
-
const
|
|
3768
|
+
const finalPixel = translatedBoxPixel(pixel, info, {
|
|
3769
|
+
fsm,
|
|
3770
|
+
preDragPoints,
|
|
3771
|
+
downPixel
|
|
3772
|
+
});
|
|
3773
|
+
const finalSnapshot = fsm.move(finalPixel, { constrainRotation: info?.shiftKey });
|
|
3549
3774
|
replaceWorkingPoints(preDragPoints);
|
|
3550
3775
|
workingAmplifierPlacements = preDragAmplifierPlacements;
|
|
3551
3776
|
if (fsm.kind === "rotate") boxAngle += finalSnapshot.box.angle - preDragBox.angle;
|
|
@@ -4408,7 +4633,10 @@ function runGroupTransform(opts) {
|
|
|
4408
4633
|
//#region src/tactical-draw/point-symbol-rendering.ts
|
|
4409
4634
|
function effectivePointSymbolPixelSize(symbol, resolution) {
|
|
4410
4635
|
if (symbol.size.unit === "pixels") return symbol.size.value;
|
|
4411
|
-
|
|
4636
|
+
if (resolution === void 0 || resolution <= 0) return symbol.size.value;
|
|
4637
|
+
const px = symbol.size.value / resolution;
|
|
4638
|
+
const { minPixels, maxPixels } = symbol.size;
|
|
4639
|
+
return Math.max(Number.isFinite(minPixels) && minPixels > 0 ? minPixels : 0, Math.min(Number.isFinite(maxPixels) && maxPixels > 0 ? maxPixels : Infinity, px));
|
|
4412
4640
|
}
|
|
4413
4641
|
function pointSymbolPixelScale(symbol, render, resolution) {
|
|
4414
4642
|
if (!Number.isFinite(render.octagonSize) || render.octagonSize <= 0) throw new TypeError("PointSymbolRender.octagonSize must be a positive finite number");
|
|
@@ -4450,6 +4678,12 @@ function renderPointSymbolForAdapter(adapter, capability, symbol) {
|
|
|
4450
4678
|
};
|
|
4451
4679
|
}
|
|
4452
4680
|
function pointSymbolFeature(symbol, render, resolution) {
|
|
4681
|
+
const groundSizeProperties = symbol.size.unit === "meters" ? {
|
|
4682
|
+
iconSizeMeters: symbol.size.value,
|
|
4683
|
+
iconOctagonSize: render.octagonSize,
|
|
4684
|
+
...boundProperty("iconMinPixels", symbol.size.minPixels),
|
|
4685
|
+
...boundProperty("iconMaxPixels", symbol.size.maxPixels)
|
|
4686
|
+
} : void 0;
|
|
4453
4687
|
return {
|
|
4454
4688
|
type: "Feature",
|
|
4455
4689
|
id: `${symbol.id}:point-symbol`,
|
|
@@ -4464,10 +4698,14 @@ function pointSymbolFeature(symbol, render, resolution) {
|
|
|
4464
4698
|
iconId: render.resourceId,
|
|
4465
4699
|
iconScale: pointSymbolPixelScale(symbol, render, resolution),
|
|
4466
4700
|
iconRotation: symbol.rotation,
|
|
4467
|
-
iconOpacity: symbol.style?.opacity ?? 1
|
|
4701
|
+
iconOpacity: symbol.style?.opacity ?? 1,
|
|
4702
|
+
...groundSizeProperties
|
|
4468
4703
|
}
|
|
4469
4704
|
};
|
|
4470
4705
|
}
|
|
4706
|
+
function boundProperty(key, value) {
|
|
4707
|
+
return value !== void 0 && Number.isFinite(value) && value > 0 ? { [key]: value } : {};
|
|
4708
|
+
}
|
|
4471
4709
|
//#endregion
|
|
4472
4710
|
//#region src/tactical-draw/graphic-transform-controller.ts
|
|
4473
4711
|
const HIT_TOLERANCE_PX$1 = 12;
|
|
@@ -4798,7 +5036,7 @@ var PointSymbolCapabilityRequiredError = class extends Error {
|
|
|
4798
5036
|
//#region src/tactical-draw/point-symbol-draw-controller.ts
|
|
4799
5037
|
const PREVIEW_ID = "__td_point_symbol_preview__";
|
|
4800
5038
|
function runPointSymbolDraw(options) {
|
|
4801
|
-
const { adapter, capability, draft, previewLayer, generateId, signal, onSettled } = options;
|
|
5039
|
+
const { adapter, capability, draft, previewLayer, generateId, signal, onSettled, snap } = options;
|
|
4802
5040
|
let latestPosition = draft.position;
|
|
4803
5041
|
let ctx;
|
|
4804
5042
|
const savedCursor = "";
|
|
@@ -4818,13 +5056,13 @@ function runPointSymbolDraw(options) {
|
|
|
4818
5056
|
ctx.writeLayer(previewLayer, [plan.feature]);
|
|
4819
5057
|
};
|
|
4820
5058
|
const handlePointerMove = (event) => {
|
|
4821
|
-
latestPosition = event
|
|
5059
|
+
latestPosition = snappedDrawPosition(snap, event);
|
|
4822
5060
|
renderPreview();
|
|
4823
5061
|
};
|
|
4824
5062
|
const handleClick = (event) => {
|
|
4825
|
-
latestPosition = event
|
|
5063
|
+
latestPosition = snappedDrawPosition(snap, event);
|
|
4826
5064
|
ctx.commit(() => {
|
|
4827
|
-
const graphic = build(draft.id ?? generateId(),
|
|
5065
|
+
const graphic = build(draft.id ?? generateId(), latestPosition);
|
|
4828
5066
|
return {
|
|
4829
5067
|
graphic: clonePointSymbol(graphic),
|
|
4830
5068
|
render: capability.render(graphic)
|
|
@@ -4871,7 +5109,7 @@ function symbolBox(adapter, capability, symbol, padding = 0) {
|
|
|
4871
5109
|
};
|
|
4872
5110
|
}
|
|
4873
5111
|
function runPointSymbolEdit(opts) {
|
|
4874
|
-
const { adapter, capability, symbol, graphicsLayer, previewLayer, handlesLayer, signal, closeOnClickAway = true, interactionStyle, onSession, handleIdPrefix, restoreOriginalToGraphics, wasOnGraphicsLayer, onSettled, onClickAwayGraphicsPick } = opts;
|
|
5112
|
+
const { adapter, capability, symbol, graphicsLayer, previewLayer, handlesLayer, signal, closeOnClickAway = true, interactionStyle, onSession, handleIdPrefix, restoreOriginalToGraphics, wasOnGraphicsLayer, onSettled, onClickAwayGraphicsPick, snap } = opts;
|
|
4875
5113
|
let modes = normalisePointModes(opts.modes);
|
|
4876
5114
|
let working = clonePointSymbol(symbol);
|
|
4877
5115
|
let dirty = false;
|
|
@@ -5021,13 +5259,22 @@ function runPointSymbolEdit(opts) {
|
|
|
5021
5259
|
};
|
|
5022
5260
|
driver.setDragPanEnabled(false);
|
|
5023
5261
|
};
|
|
5024
|
-
const transformed = (pixel,
|
|
5262
|
+
const transformed = (pixel, info) => {
|
|
5025
5263
|
if (!drag) return null;
|
|
5026
5264
|
const next = clonePointSymbol(drag.start);
|
|
5027
|
-
if (drag.kind === "body")
|
|
5028
|
-
|
|
5265
|
+
if (drag.kind === "body") {
|
|
5266
|
+
const unsnappedPosition = driver.fromPixel([drag.anchor[0] + pixel[0] - drag.down[0], drag.anchor[1] + pixel[1] - drag.down[1]]);
|
|
5267
|
+
const anchorPixel = driver.toPixel(unsnappedPosition) ?? pixel;
|
|
5268
|
+
next.position = (snap?.({
|
|
5269
|
+
coordinate: unsnappedPosition,
|
|
5270
|
+
pixel: anchorPixel,
|
|
5271
|
+
interaction: "translate",
|
|
5272
|
+
graphicId: symbol.id,
|
|
5273
|
+
originalEvent: info?.native
|
|
5274
|
+
}))?.coordinate ?? unsnappedPosition;
|
|
5275
|
+
} else if (drag.kind === "rotate") {
|
|
5029
5276
|
const rotation = drag.start.rotation + pointerAngle(pixel, drag.anchor) - drag.startPointerAngle;
|
|
5030
|
-
next.rotation = normalizeRotation(
|
|
5277
|
+
next.rotation = normalizeRotation(info?.shiftKey ? constrainRotation(rotation) : rotation);
|
|
5031
5278
|
} else {
|
|
5032
5279
|
const distance = Math.hypot(pixel[0] - drag.anchor[0], pixel[1] - drag.anchor[1]);
|
|
5033
5280
|
next.size.value = Math.max(.1, drag.start.size.value * (distance / drag.startDistance));
|
|
@@ -5035,7 +5282,7 @@ function runPointSymbolEdit(opts) {
|
|
|
5035
5282
|
return next;
|
|
5036
5283
|
};
|
|
5037
5284
|
const onMove = (pixel, info) => {
|
|
5038
|
-
const next = transformed(pixel, info
|
|
5285
|
+
const next = transformed(pixel, info);
|
|
5039
5286
|
if (!next) return;
|
|
5040
5287
|
working = next;
|
|
5041
5288
|
render();
|
|
@@ -5043,7 +5290,7 @@ function runPointSymbolEdit(opts) {
|
|
|
5043
5290
|
const onUp = (pixel, info) => {
|
|
5044
5291
|
if (!drag) return;
|
|
5045
5292
|
const currentDrag = drag;
|
|
5046
|
-
const next = transformed(pixel, info
|
|
5293
|
+
const next = transformed(pixel, info);
|
|
5047
5294
|
drag = null;
|
|
5048
5295
|
suppressClickAway = true;
|
|
5049
5296
|
driver.setDragPanEnabled(true);
|
|
@@ -5140,6 +5387,8 @@ var TacticalDraw = class {
|
|
|
5140
5387
|
defaultInteractionStyle;
|
|
5141
5388
|
generateId;
|
|
5142
5389
|
pointSymbols;
|
|
5390
|
+
snappingOptions;
|
|
5391
|
+
resolveSnap;
|
|
5143
5392
|
destroyed = false;
|
|
5144
5393
|
hasRendered = false;
|
|
5145
5394
|
activeInteraction = new ActiveInteractionTracker();
|
|
@@ -5190,12 +5439,15 @@ var TacticalDraw = class {
|
|
|
5190
5439
|
* surrounding collection state.
|
|
5191
5440
|
*/
|
|
5192
5441
|
lastRenderedMeasures = [];
|
|
5442
|
+
snapGeometryFeatures = [];
|
|
5193
5443
|
constructor(adapter, options) {
|
|
5194
5444
|
this.adapter = adapter;
|
|
5195
5445
|
this.graphicsStyle = resolveStyleHints(BUILT_IN_GRAPHICS_STYLE, options?.graphicsStyle, void 0);
|
|
5196
5446
|
this.defaultInteractionStyle = options?.interactionStyle;
|
|
5197
5447
|
this.generateId = options?.generateId ?? (() => `td-${++this.idCounter}`);
|
|
5198
5448
|
this.pointSymbols = options?.pointSymbols;
|
|
5449
|
+
this.snappingOptions = options?.snapping;
|
|
5450
|
+
this.resolveSnap = createSnapResolver(adapter, () => this.lastRenderedMeasures, () => this.snapGeometryFeatures, () => this.snappingOptions);
|
|
5199
5451
|
const owned = /* @__PURE__ */ new Set();
|
|
5200
5452
|
const provided = options?.layers ?? {};
|
|
5201
5453
|
this.graphicsLayer = resolveSlot(adapter, provided.graphics, owned);
|
|
@@ -5209,6 +5461,16 @@ var TacticalDraw = class {
|
|
|
5209
5461
|
this.adapter.setLayerStyle(this.guideLayer, guideToStyleOptions(this.baseGuideStyle()));
|
|
5210
5462
|
this.adapter.onViewChange(this.handleViewChange);
|
|
5211
5463
|
}
|
|
5464
|
+
/** Replace snapping configuration without recreating the map adapter or façade. */
|
|
5465
|
+
setSnappingOptions(options) {
|
|
5466
|
+
this.assertAlive();
|
|
5467
|
+
this.clearSnapResult();
|
|
5468
|
+
this.snappingOptions = options;
|
|
5469
|
+
}
|
|
5470
|
+
/** Clear transient host affordances when snapping or its interaction ends. */
|
|
5471
|
+
clearSnapResult() {
|
|
5472
|
+
this.snappingOptions?.onSnap?.(null);
|
|
5473
|
+
}
|
|
5212
5474
|
/**
|
|
5213
5475
|
* Render `measures` into features and write them to the graphics layer.
|
|
5214
5476
|
* Routes every measure through [[renderMeasureForAdapter]] so pixel sizing is
|
|
@@ -5231,6 +5493,7 @@ var TacticalDraw = class {
|
|
|
5231
5493
|
}
|
|
5232
5494
|
this.adapter.reconcileLayerIconResources(this.graphicsLayer, [...resources.values()]);
|
|
5233
5495
|
this.adapter.updateLayerFeatures(this.graphicsLayer, features);
|
|
5496
|
+
this.snapGeometryFeatures = features;
|
|
5234
5497
|
}
|
|
5235
5498
|
/**
|
|
5236
5499
|
* Re-render the graphics layer on a view (zoom) change so pixel-denominated
|
|
@@ -5245,7 +5508,8 @@ var TacticalDraw = class {
|
|
|
5245
5508
|
if (this.destroyed || !this.hasRendered) return;
|
|
5246
5509
|
const excluded = this.excludedInteractionIds();
|
|
5247
5510
|
const visible = excluded.size > 0 ? this.lastRenderedMeasures.filter((m) => !excluded.has(m.id)) : this.lastRenderedMeasures;
|
|
5248
|
-
|
|
5511
|
+
const nativeGroundIconScaling = this.adapter.scalesGroundPointSymbolsNatively === true;
|
|
5512
|
+
if (!visible.some((g) => graphicRendersFromViewState(g, nativeGroundIconScaling))) return;
|
|
5249
5513
|
this.renderMeasuresToGraphics(visible);
|
|
5250
5514
|
};
|
|
5251
5515
|
/**
|
|
@@ -5391,6 +5655,7 @@ var TacticalDraw = class {
|
|
|
5391
5655
|
};
|
|
5392
5656
|
const onSettled = () => {
|
|
5393
5657
|
clearSlot();
|
|
5658
|
+
this.clearSnapResult();
|
|
5394
5659
|
if (extraSettle) extraSettle();
|
|
5395
5660
|
};
|
|
5396
5661
|
this.activeInteraction.begin();
|
|
@@ -5444,7 +5709,8 @@ var TacticalDraw = class {
|
|
|
5444
5709
|
previewLayer: this.previewLayer,
|
|
5445
5710
|
generateId: this.generateId,
|
|
5446
5711
|
signal: options?.signal,
|
|
5447
|
-
onSettled
|
|
5712
|
+
onSettled,
|
|
5713
|
+
snap: this.resolveSnap
|
|
5448
5714
|
}), null);
|
|
5449
5715
|
}
|
|
5450
5716
|
const measureDraft = draft;
|
|
@@ -5476,7 +5742,8 @@ var TacticalDraw = class {
|
|
|
5476
5742
|
measureTextAmplifiers: measureDraft.textAmplifiers,
|
|
5477
5743
|
guide: options?.guide,
|
|
5478
5744
|
sizeAnchor: options?.sizeAnchor,
|
|
5479
|
-
onSettled
|
|
5745
|
+
onSettled,
|
|
5746
|
+
snap: this.resolveSnap
|
|
5480
5747
|
}) : runVariableLengthDraw({
|
|
5481
5748
|
adapter: this.adapter,
|
|
5482
5749
|
kind,
|
|
@@ -5493,7 +5760,8 @@ var TacticalDraw = class {
|
|
|
5493
5760
|
measureTextAmplifiers: measureDraft.textAmplifiers,
|
|
5494
5761
|
guide: options?.guide,
|
|
5495
5762
|
sizeAnchor: options?.sizeAnchor,
|
|
5496
|
-
onSettled
|
|
5763
|
+
onSettled,
|
|
5764
|
+
snap: this.resolveSnap
|
|
5497
5765
|
}), null, extraSettle);
|
|
5498
5766
|
}
|
|
5499
5767
|
edit(measure, options) {
|
|
@@ -5522,7 +5790,8 @@ var TacticalDraw = class {
|
|
|
5522
5790
|
restoreOriginalToGraphics,
|
|
5523
5791
|
wasOnGraphicsLayer,
|
|
5524
5792
|
onSettled,
|
|
5525
|
-
onClickAwayGraphicsPick: (event) => this.dispatchHostPick(this.graphicsLayer, event)
|
|
5793
|
+
onClickAwayGraphicsPick: (event) => this.dispatchHostPick(this.graphicsLayer, event),
|
|
5794
|
+
snap: this.resolveSnap
|
|
5526
5795
|
}), (handle) => {
|
|
5527
5796
|
this.activeEdit = handle;
|
|
5528
5797
|
return () => {
|
|
@@ -5562,7 +5831,8 @@ var TacticalDraw = class {
|
|
|
5562
5831
|
restoreOriginalToGraphics,
|
|
5563
5832
|
wasOnGraphicsLayer,
|
|
5564
5833
|
onSettled,
|
|
5565
|
-
onClickAwayGraphicsPick
|
|
5834
|
+
onClickAwayGraphicsPick,
|
|
5835
|
+
snap: this.resolveSnap
|
|
5566
5836
|
}), (handle) => {
|
|
5567
5837
|
this.activeEdit = handle;
|
|
5568
5838
|
return () => {
|
|
@@ -5950,8 +6220,8 @@ var TacticalDraw = class {
|
|
|
5950
6220
|
if (this.destroyed) throw new TacticalDrawDestroyedError();
|
|
5951
6221
|
}
|
|
5952
6222
|
};
|
|
5953
|
-
function graphicRendersFromViewState(graphic) {
|
|
5954
|
-
return isPointSymbol(graphic) ? graphic.size.unit === "meters" : measureRendersFromViewState(graphic);
|
|
6223
|
+
function graphicRendersFromViewState(graphic, nativeGroundIconScaling) {
|
|
6224
|
+
return isPointSymbol(graphic) ? graphic.size.unit === "meters" && !nativeGroundIconScaling : measureRendersFromViewState(graphic);
|
|
5955
6225
|
}
|
|
5956
6226
|
function normalisePointEditModes(modes) {
|
|
5957
6227
|
for (const mode of modes ?? ["transform"]) if (mode !== "transform" && mode !== "delete") throw new TypeError(`Point-symbol editing does not support mode "${mode}"`);
|
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.24",
|
|
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",
|