@orbat-mapper/tactical-draw 0.2.0-alpha.21 → 0.2.0-alpha.23
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/README.md +16 -0
- package/dist/index.d.mts +97 -1
- package/dist/index.mjs +649 -113
- package/package.json +1 -1
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() {
|
|
@@ -1693,6 +1878,19 @@ function pickHandle(handles, pixel, tolerance, toPixel) {
|
|
|
1693
1878
|
}
|
|
1694
1879
|
return null;
|
|
1695
1880
|
}
|
|
1881
|
+
//#endregion
|
|
1882
|
+
//#region src/tactical-draw/rotation.ts
|
|
1883
|
+
/** Excalidraw-style rotation increment used while Shift is held. */
|
|
1884
|
+
const CONSTRAINED_ROTATION_STEP = Math.PI / 12;
|
|
1885
|
+
/** Normalize radians to the canonical clockwise [0, 2π) range. */
|
|
1886
|
+
function normalizeRotation(angle) {
|
|
1887
|
+
const turn = Math.PI * 2;
|
|
1888
|
+
return (angle % turn + turn) % turn;
|
|
1889
|
+
}
|
|
1890
|
+
/** Snap an absolute rotation angle to the nearest 15-degree increment. */
|
|
1891
|
+
function constrainRotation(angle) {
|
|
1892
|
+
return Math.round(angle / CONSTRAINED_ROTATION_STEP) * CONSTRAINED_ROTATION_STEP;
|
|
1893
|
+
}
|
|
1696
1894
|
function rotate(x, y, angle) {
|
|
1697
1895
|
const cos = Math.cos(angle);
|
|
1698
1896
|
const sin = Math.sin(angle);
|
|
@@ -1956,8 +2154,9 @@ function beginBoxDrag(args) {
|
|
|
1956
2154
|
const pivotPixel = box.center;
|
|
1957
2155
|
const pivot = fromPixel(pivotPixel);
|
|
1958
2156
|
const startAngle = Math.atan2(downPixel[1] - pivotPixel[1], downPixel[0] - pivotPixel[0]);
|
|
1959
|
-
const move = (pixel) => {
|
|
1960
|
-
const
|
|
2157
|
+
const move = (pixel, modifiers) => {
|
|
2158
|
+
const rawAngle = Math.atan2(pixel[1] - pivotPixel[1], pixel[0] - pivotPixel[0]) - startAngle;
|
|
2159
|
+
const angle = modifiers?.constrainRotation ? constrainRotation(box.angle + rawAngle) - box.angle : rawAngle;
|
|
1961
2160
|
return {
|
|
1962
2161
|
points: rotatePointsAround(preDragPoints, pivot, pivotPixel, angle, projection),
|
|
1963
2162
|
box: {
|
|
@@ -1971,7 +2170,7 @@ function beginBoxDrag(args) {
|
|
|
1971
2170
|
return {
|
|
1972
2171
|
kind,
|
|
1973
2172
|
move,
|
|
1974
|
-
end: (pixel) => move(pixel).points
|
|
2173
|
+
end: (pixel, modifiers) => move(pixel, modifiers).points
|
|
1975
2174
|
};
|
|
1976
2175
|
}
|
|
1977
2176
|
const origin = fromPixel(downPixel);
|
|
@@ -2035,6 +2234,102 @@ const BEGIN_DRAG_FOR_TEST = Symbol("EditSession.beginDragForTest");
|
|
|
2035
2234
|
*/
|
|
2036
2235
|
const END_DRAG_FOR_TEST = Symbol("EditSession.endDragForTest");
|
|
2037
2236
|
//#endregion
|
|
2237
|
+
//#region src/tactical-draw/session-history.ts
|
|
2238
|
+
/** @internal Controller-owned history stack. */
|
|
2239
|
+
function createSessionHistory(options) {
|
|
2240
|
+
const limit = Math.max(1, options.limit ?? 100);
|
|
2241
|
+
let entries = [{ value: options.clone(options.initial) }];
|
|
2242
|
+
let index = 0;
|
|
2243
|
+
let settled = false;
|
|
2244
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2245
|
+
function currentState() {
|
|
2246
|
+
if (settled) return {
|
|
2247
|
+
canUndo: false,
|
|
2248
|
+
canRedo: false
|
|
2249
|
+
};
|
|
2250
|
+
return {
|
|
2251
|
+
canUndo: index > 0,
|
|
2252
|
+
canRedo: index < entries.length - 1,
|
|
2253
|
+
...index > 0 && entries[index]?.label ? { undoLabel: entries[index].label } : {},
|
|
2254
|
+
...index < entries.length - 1 && entries[index + 1]?.label ? { redoLabel: entries[index + 1].label } : {}
|
|
2255
|
+
};
|
|
2256
|
+
}
|
|
2257
|
+
function notify() {
|
|
2258
|
+
const state = currentState();
|
|
2259
|
+
const errors = [];
|
|
2260
|
+
for (const listener of [...listeners]) try {
|
|
2261
|
+
listener(state);
|
|
2262
|
+
} catch (error) {
|
|
2263
|
+
errors.push(error);
|
|
2264
|
+
}
|
|
2265
|
+
if (errors.length === 1) throw errors[0];
|
|
2266
|
+
if (errors.length > 1) throw new AggregateError(errors, "SessionHistory subscriber errors");
|
|
2267
|
+
}
|
|
2268
|
+
function move(nextIndex, source) {
|
|
2269
|
+
if (settled || nextIndex < 0 || nextIndex >= entries.length) return false;
|
|
2270
|
+
const from = options.clone(entries[index].value);
|
|
2271
|
+
const to = options.clone(entries[nextIndex].value);
|
|
2272
|
+
try {
|
|
2273
|
+
if (!options.apply(from, to, source)) return false;
|
|
2274
|
+
} catch (error) {
|
|
2275
|
+
if (!options.isCurrent?.(to)) throw error;
|
|
2276
|
+
index = nextIndex;
|
|
2277
|
+
try {
|
|
2278
|
+
notify();
|
|
2279
|
+
} catch (notifyError) {
|
|
2280
|
+
throw new AggregateError([error, notifyError], "SessionHistory undo/redo errors");
|
|
2281
|
+
}
|
|
2282
|
+
throw error;
|
|
2283
|
+
}
|
|
2284
|
+
index = nextIndex;
|
|
2285
|
+
notify();
|
|
2286
|
+
return true;
|
|
2287
|
+
}
|
|
2288
|
+
return {
|
|
2289
|
+
get state() {
|
|
2290
|
+
return currentState();
|
|
2291
|
+
},
|
|
2292
|
+
undo() {
|
|
2293
|
+
return move(index - 1, "undo");
|
|
2294
|
+
},
|
|
2295
|
+
redo() {
|
|
2296
|
+
return move(index + 1, "redo");
|
|
2297
|
+
},
|
|
2298
|
+
subscribe(listener) {
|
|
2299
|
+
listener(currentState());
|
|
2300
|
+
if (settled) return () => {};
|
|
2301
|
+
listeners.add(listener);
|
|
2302
|
+
return () => listeners.delete(listener);
|
|
2303
|
+
},
|
|
2304
|
+
record(value, label) {
|
|
2305
|
+
if (settled || options.equals(entries[index].value, value)) return false;
|
|
2306
|
+
entries.splice(index + 1);
|
|
2307
|
+
entries.push({
|
|
2308
|
+
value,
|
|
2309
|
+
label
|
|
2310
|
+
});
|
|
2311
|
+
if (entries.length > limit + 1) entries.shift();
|
|
2312
|
+
index = entries.length - 1;
|
|
2313
|
+
notify();
|
|
2314
|
+
return true;
|
|
2315
|
+
},
|
|
2316
|
+
reset(value) {
|
|
2317
|
+
if (settled) return;
|
|
2318
|
+
entries = [{ value: options.clone(value) }];
|
|
2319
|
+
index = 0;
|
|
2320
|
+
notify();
|
|
2321
|
+
},
|
|
2322
|
+
settle() {
|
|
2323
|
+
if (settled) return;
|
|
2324
|
+
settled = true;
|
|
2325
|
+
entries = [];
|
|
2326
|
+
index = 0;
|
|
2327
|
+
notify();
|
|
2328
|
+
listeners.clear();
|
|
2329
|
+
}
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
//#endregion
|
|
2038
2333
|
//#region src/tactical-draw/box-features.ts
|
|
2039
2334
|
/**
|
|
2040
2335
|
* Rendering helpers shared by the single-measure edit controller
|
|
@@ -2325,7 +2620,7 @@ function mergeAmplifierPlacementField(existing, incoming) {
|
|
|
2325
2620
|
* - close / abort / signal / preempt / destroy paths
|
|
2326
2621
|
*/
|
|
2327
2622
|
function runEdit(opts) {
|
|
2328
|
-
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;
|
|
2329
2624
|
const renderOpts = graphicsStyle ? { graphicsStyle } : void 0;
|
|
2330
2625
|
let sizeAnchor = opts.sizeAnchor ?? "ground";
|
|
2331
2626
|
const metadata = getControlMeasureMetadata(measure.kind);
|
|
@@ -2386,6 +2681,28 @@ function runEdit(opts) {
|
|
|
2386
2681
|
* persisted on the measure; a fresh edit always starts back at `0`.
|
|
2387
2682
|
*/
|
|
2388
2683
|
let boxAngle = 0;
|
|
2684
|
+
function captureHistory() {
|
|
2685
|
+
return {
|
|
2686
|
+
points: workingPoints.map((point) => [...point]),
|
|
2687
|
+
options: { ...workingOptions ?? {} },
|
|
2688
|
+
style: currentStyle ? { ...currentStyle } : void 0,
|
|
2689
|
+
textAmplifiers: workingTextAmplifiers ? { ...workingTextAmplifiers } : void 0,
|
|
2690
|
+
amplifierPlacements: snapshotAmplifierPlacements(),
|
|
2691
|
+
sizeAnchor,
|
|
2692
|
+
nonGeometryDirty,
|
|
2693
|
+
boxAngle
|
|
2694
|
+
};
|
|
2695
|
+
}
|
|
2696
|
+
function restoreHistory(value) {
|
|
2697
|
+
replaceWorkingPoints(value.points);
|
|
2698
|
+
replaceWorkingOptions(value.options);
|
|
2699
|
+
currentStyle = value.style ? { ...value.style } : void 0;
|
|
2700
|
+
workingTextAmplifiers = value.textAmplifiers ? { ...value.textAmplifiers } : void 0;
|
|
2701
|
+
workingAmplifierPlacements = value.amplifierPlacements ? structuredClone(value.amplifierPlacements) : void 0;
|
|
2702
|
+
sizeAnchor = value.sizeAnchor;
|
|
2703
|
+
nonGeometryDirty = value.nonGeometryDirty;
|
|
2704
|
+
boxAngle = value.boxAngle;
|
|
2705
|
+
}
|
|
2389
2706
|
/**
|
|
2390
2707
|
* The transform box fitted around the *rendered* working-copy geometry
|
|
2391
2708
|
* (pixel space), or `null` when nothing projected this frame. Re-fitted
|
|
@@ -2612,12 +2929,13 @@ function runEdit(opts) {
|
|
|
2612
2929
|
* - Reject restores the working state to the pre-gesture snapshot and
|
|
2613
2930
|
* recomputes `dirty`. Close/abort settle the session as normal.
|
|
2614
2931
|
*/
|
|
2615
|
-
function emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements) {
|
|
2932
|
+
function emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, source = "gesture", historyLabel = "Edit", previousBoxAngle = boxAngle) {
|
|
2616
2933
|
if (ctx.settled) return;
|
|
2617
2934
|
let rejectRequested = false;
|
|
2618
2935
|
let closeRequested = false;
|
|
2619
2936
|
let abortRequested = false;
|
|
2620
2937
|
const event = {
|
|
2938
|
+
source,
|
|
2621
2939
|
graphic: currentRendered,
|
|
2622
2940
|
previous: previousRendered,
|
|
2623
2941
|
session,
|
|
@@ -2646,11 +2964,13 @@ function runEdit(opts) {
|
|
|
2646
2964
|
sizeAnchor = previousSizeAnchor;
|
|
2647
2965
|
workingTextAmplifiers = previousTextAmplifiers;
|
|
2648
2966
|
workingAmplifierPlacements = previousAmplifierPlacements;
|
|
2967
|
+
boxAngle = previousBoxAngle;
|
|
2649
2968
|
nonGeometryDirty = previousNonGeometryDirty;
|
|
2650
2969
|
renderPreview();
|
|
2651
2970
|
renderHandlesLayer();
|
|
2652
2971
|
recomputeDirty();
|
|
2653
2972
|
}
|
|
2973
|
+
if (!ctx.settled && !rejectRequested && (source === "gesture" || source === "programmatic")) history.record(captureHistory(), historyLabel);
|
|
2654
2974
|
if (errors.length === 1) throw errors[0];
|
|
2655
2975
|
if (errors.length > 1) throw new AggregateError(errors, "EditSession.onChange listener errors");
|
|
2656
2976
|
}
|
|
@@ -2664,7 +2984,7 @@ function runEdit(opts) {
|
|
|
2664
2984
|
* queued during the drag are applied after the listener emission (so the
|
|
2665
2985
|
* post-gesture render reflects the requested mode set).
|
|
2666
2986
|
*/
|
|
2667
|
-
function commitGesture(nextPoints, preGestureOptions, nextAmplifierPlacements) {
|
|
2987
|
+
function commitGesture(nextPoints, preGestureOptions, nextAmplifierPlacements, preGestureBoxAngle = boxAngle) {
|
|
2668
2988
|
if (ctx.settled) return;
|
|
2669
2989
|
const previousPoints = workingPoints.map((p) => [...p]);
|
|
2670
2990
|
const previousOptions = { ...preGestureOptions ?? workingOptions ?? {} };
|
|
@@ -2691,7 +3011,7 @@ function runEdit(opts) {
|
|
|
2691
3011
|
recomputeDirty();
|
|
2692
3012
|
dragInFlight = false;
|
|
2693
3013
|
drainPendingModes();
|
|
2694
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements);
|
|
3014
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, "gesture", "Edit", preGestureBoxAngle);
|
|
2695
3015
|
}
|
|
2696
3016
|
function setOptions(partial) {
|
|
2697
3017
|
if (ctx.settled) return;
|
|
@@ -2706,6 +3026,7 @@ function runEdit(opts) {
|
|
|
2706
3026
|
...workingOptions ?? {},
|
|
2707
3027
|
...partial ?? {}
|
|
2708
3028
|
});
|
|
3029
|
+
if (JSON.stringify(workingOptions) === JSON.stringify(previousOptions)) return;
|
|
2709
3030
|
let currentRendered;
|
|
2710
3031
|
try {
|
|
2711
3032
|
currentRendered = rebuildRendered();
|
|
@@ -2716,7 +3037,7 @@ function runEdit(opts) {
|
|
|
2716
3037
|
renderPreview();
|
|
2717
3038
|
nonGeometryDirty = true;
|
|
2718
3039
|
dirty = true;
|
|
2719
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements);
|
|
3040
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, "programmatic", "Change options");
|
|
2720
3041
|
}
|
|
2721
3042
|
function setStyle(partial) {
|
|
2722
3043
|
if (ctx.settled) return;
|
|
@@ -2731,6 +3052,7 @@ function runEdit(opts) {
|
|
|
2731
3052
|
...currentStyle ?? {},
|
|
2732
3053
|
...partial
|
|
2733
3054
|
};
|
|
3055
|
+
if (JSON.stringify(currentStyle) === JSON.stringify(previousStyle)) return;
|
|
2734
3056
|
let currentRendered;
|
|
2735
3057
|
try {
|
|
2736
3058
|
currentRendered = rebuildRendered();
|
|
@@ -2741,7 +3063,7 @@ function runEdit(opts) {
|
|
|
2741
3063
|
renderPreview();
|
|
2742
3064
|
nonGeometryDirty = true;
|
|
2743
3065
|
dirty = true;
|
|
2744
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements);
|
|
3066
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, "programmatic", "Change style");
|
|
2745
3067
|
}
|
|
2746
3068
|
/**
|
|
2747
3069
|
* Shallow-merge into the live working text amplifiers. Mirrors
|
|
@@ -2776,6 +3098,7 @@ function runEdit(opts) {
|
|
|
2776
3098
|
if (value !== void 0 && value.trim() !== "") merged[canonicalKey ?? key] = value;
|
|
2777
3099
|
}
|
|
2778
3100
|
workingTextAmplifiers = Object.keys(merged).length > 0 ? merged : void 0;
|
|
3101
|
+
if (JSON.stringify(workingTextAmplifiers) === JSON.stringify(previousTextAmplifiers)) return;
|
|
2779
3102
|
let currentRendered;
|
|
2780
3103
|
try {
|
|
2781
3104
|
currentRendered = rebuildRendered();
|
|
@@ -2786,9 +3109,9 @@ function runEdit(opts) {
|
|
|
2786
3109
|
renderPreview();
|
|
2787
3110
|
nonGeometryDirty = true;
|
|
2788
3111
|
dirty = true;
|
|
2789
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements);
|
|
3112
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, "programmatic", "Change text");
|
|
2790
3113
|
}
|
|
2791
|
-
function setAmplifierPlacements(partial) {
|
|
3114
|
+
function setAmplifierPlacements(partial, source = "programmatic", historyLabel = "Move label") {
|
|
2792
3115
|
if (ctx.settled) return;
|
|
2793
3116
|
const previousPoints = workingPoints.map((p) => [...p]);
|
|
2794
3117
|
const previousOptions = { ...workingOptions ?? {} };
|
|
@@ -2808,11 +3131,12 @@ function runEdit(opts) {
|
|
|
2808
3131
|
else next[field] = merged;
|
|
2809
3132
|
}
|
|
2810
3133
|
workingAmplifierPlacements = Object.keys(next).length > 0 ? next : void 0;
|
|
3134
|
+
if (JSON.stringify(workingAmplifierPlacements) === JSON.stringify(previousAmplifierPlacements)) return;
|
|
2811
3135
|
const currentRendered = rebuildRendered();
|
|
2812
3136
|
renderPreview();
|
|
2813
3137
|
nonGeometryDirty = true;
|
|
2814
3138
|
dirty = true;
|
|
2815
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements);
|
|
3139
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, source, historyLabel);
|
|
2816
3140
|
}
|
|
2817
3141
|
/**
|
|
2818
3142
|
* Flip the size anchor for the measure under edit (ADR-0020). Re-renders the
|
|
@@ -2835,7 +3159,7 @@ function runEdit(opts) {
|
|
|
2835
3159
|
const currentRendered = rebuildRendered();
|
|
2836
3160
|
nonGeometryDirty = true;
|
|
2837
3161
|
dirty = true;
|
|
2838
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements);
|
|
3162
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, "programmatic", "Change size anchor");
|
|
2839
3163
|
}
|
|
2840
3164
|
/** Apply a translate gesture by shifting every working point by `delta`. */
|
|
2841
3165
|
function commitTranslate(delta) {
|
|
@@ -2864,7 +3188,7 @@ function runEdit(opts) {
|
|
|
2864
3188
|
const pivot = box ? driver.fromPixel(box.center) : computeBoundingBoxCenter(workingPoints);
|
|
2865
3189
|
if (!pivot) return;
|
|
2866
3190
|
boxAngle += angle;
|
|
2867
|
-
commitGesture(rotatePointsAround(workingPoints, pivot, pivotPixel, angle, dragProjection), preFreezeOptions);
|
|
3191
|
+
commitGesture(rotatePointsAround(workingPoints, pivot, pivotPixel, angle, dragProjection), preFreezeOptions, void 0, boxAngle - angle);
|
|
2868
3192
|
}
|
|
2869
3193
|
function modesEqual(a, b) {
|
|
2870
3194
|
if (a.length !== b.length) return false;
|
|
@@ -2883,7 +3207,32 @@ function runEdit(opts) {
|
|
|
2883
3207
|
pendingModes = null;
|
|
2884
3208
|
applyModes(next);
|
|
2885
3209
|
}
|
|
3210
|
+
const history = createSessionHistory({
|
|
3211
|
+
initial: captureHistory(),
|
|
3212
|
+
clone: (value) => ({
|
|
3213
|
+
points: value.points.map((point) => [...point]),
|
|
3214
|
+
options: { ...value.options ?? {} },
|
|
3215
|
+
style: value.style ? { ...value.style } : void 0,
|
|
3216
|
+
textAmplifiers: value.textAmplifiers ? { ...value.textAmplifiers } : void 0,
|
|
3217
|
+
amplifierPlacements: value.amplifierPlacements ? structuredClone(value.amplifierPlacements) : void 0,
|
|
3218
|
+
sizeAnchor: value.sizeAnchor,
|
|
3219
|
+
nonGeometryDirty: value.nonGeometryDirty,
|
|
3220
|
+
boxAngle: value.boxAngle
|
|
3221
|
+
}),
|
|
3222
|
+
equals: (a, b) => JSON.stringify(a) === JSON.stringify(b),
|
|
3223
|
+
isCurrent: (value) => JSON.stringify(captureHistory()) === JSON.stringify(value),
|
|
3224
|
+
apply(from, to, source) {
|
|
3225
|
+
const previousRendered = rebuildRendered();
|
|
3226
|
+
restoreHistory(to);
|
|
3227
|
+
renderPreview();
|
|
3228
|
+
renderHandlesLayer();
|
|
3229
|
+
recomputeDirty();
|
|
3230
|
+
emitChange(previousRendered, from.points, from.options, from.style, from.nonGeometryDirty, from.sizeAnchor, rebuildRendered(), from.textAmplifiers, from.amplifierPlacements, source, "Edit", from.boxAngle);
|
|
3231
|
+
return !ctx.settled && JSON.stringify(captureHistory()) === JSON.stringify(to);
|
|
3232
|
+
}
|
|
3233
|
+
});
|
|
2886
3234
|
const session = {
|
|
3235
|
+
history,
|
|
2887
3236
|
graphic: measure,
|
|
2888
3237
|
get workingGraphic() {
|
|
2889
3238
|
return cloneControlMeasure({
|
|
@@ -2943,6 +3292,7 @@ function runEdit(opts) {
|
|
|
2943
3292
|
const current = session.workingGraphic;
|
|
2944
3293
|
const next = updater(current);
|
|
2945
3294
|
if (next.id !== measure.id || next.kind !== measure.kind) throw new TypeError("updateGraphic must preserve control-measure id and kind");
|
|
3295
|
+
if (JSON.stringify(next) === JSON.stringify(current)) return;
|
|
2946
3296
|
renderMeasureForAdapter(adapter, next, renderOpts);
|
|
2947
3297
|
workingPoints.splice(0, workingPoints.length, ...next.controlPoints.map((point) => [...point]));
|
|
2948
3298
|
workingOptions = { ...next.options ?? {} };
|
|
@@ -2953,7 +3303,7 @@ function runEdit(opts) {
|
|
|
2953
3303
|
renderPreview();
|
|
2954
3304
|
renderHandlesLayer();
|
|
2955
3305
|
recomputeDirty();
|
|
2956
|
-
emitChange(previous, previous.graphic.controlPoints.map((point) => [...point]), previous.graphic.options, previous.graphic.style, previousNonGeometryDirty, previousSizeAnchor, rebuildRendered(), previous.graphic.textAmplifiers, previous.graphic.amplifierPlacements);
|
|
3306
|
+
emitChange(previous, previous.graphic.controlPoints.map((point) => [...point]), previous.graphic.options, previous.graphic.style, previousNonGeometryDirty, previousSizeAnchor, rebuildRendered(), previous.graphic.textAmplifiers, previous.graphic.amplifierPlacements, "programmatic", "Update graphic");
|
|
2957
3307
|
},
|
|
2958
3308
|
setOptions(partial) {
|
|
2959
3309
|
setOptions(partial);
|
|
@@ -3004,6 +3354,38 @@ function runEdit(opts) {
|
|
|
3004
3354
|
}
|
|
3005
3355
|
};
|
|
3006
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
|
+
}
|
|
3007
3389
|
/**
|
|
3008
3390
|
* A live label *move* drag: direct label grab, the floating grip, or a body
|
|
3009
3391
|
* grab inside the selected label's transform box (ADR-0017 box machinery
|
|
@@ -3205,7 +3587,8 @@ function runEdit(opts) {
|
|
|
3205
3587
|
});
|
|
3206
3588
|
activeDrag = {
|
|
3207
3589
|
fsm,
|
|
3208
|
-
preDragPoints
|
|
3590
|
+
preDragPoints,
|
|
3591
|
+
controlPointIndex: hit.kind === "midpoint" ? hit.index + 1 : hit.index
|
|
3209
3592
|
};
|
|
3210
3593
|
if (fsm.kind === "midpoint") {
|
|
3211
3594
|
workingPoints.splice(0, workingPoints.length, ...initialPoints);
|
|
@@ -3250,17 +3633,19 @@ function runEdit(opts) {
|
|
|
3250
3633
|
}
|
|
3251
3634
|
}
|
|
3252
3635
|
};
|
|
3253
|
-
function paintDragSnapshot(pixel) {
|
|
3636
|
+
function paintDragSnapshot(pixel, info) {
|
|
3254
3637
|
if (!activeDrag) return;
|
|
3255
|
-
const
|
|
3638
|
+
const resolvedPixel = snappedPixel(pixel, info, "reshape", activeDrag.controlPointIndex);
|
|
3639
|
+
const snapshot = activeDrag.fsm.move(resolvedPixel);
|
|
3256
3640
|
workingPoints.splice(0, workingPoints.length, ...snapshot.points);
|
|
3257
3641
|
renderPreview();
|
|
3258
3642
|
renderHandlesLayer();
|
|
3259
3643
|
recomputeDirty();
|
|
3260
3644
|
}
|
|
3261
|
-
function paintBoxDragSnapshot(pixel) {
|
|
3645
|
+
function paintBoxDragSnapshot(pixel, info, constrainRotation = false) {
|
|
3262
3646
|
if (!activeBoxDrag) return;
|
|
3263
|
-
const
|
|
3647
|
+
const resolvedPixel = translatedBoxPixel(pixel, info, activeBoxDrag);
|
|
3648
|
+
const snapshot = activeBoxDrag.fsm.move(resolvedPixel, { constrainRotation });
|
|
3264
3649
|
activeBoxDrag.lastSnapshot = snapshot;
|
|
3265
3650
|
workingPoints.splice(0, workingPoints.length, ...snapshot.points);
|
|
3266
3651
|
workingAmplifierPlacements = transformAmplifierPlacements(activeBoxDrag.preDragAmplifierPlacements, activeBoxDrag.preDragBox, snapshot.box, driver);
|
|
@@ -3268,7 +3653,7 @@ function runEdit(opts) {
|
|
|
3268
3653
|
renderHandlesLayer();
|
|
3269
3654
|
recomputeDirty();
|
|
3270
3655
|
}
|
|
3271
|
-
const onDriverMove = (pixel) => {
|
|
3656
|
+
const onDriverMove = (pixel, info) => {
|
|
3272
3657
|
if (ctx.settled) return;
|
|
3273
3658
|
if (activeLabelRotateDrag) {
|
|
3274
3659
|
const drag = activeLabelRotateDrag;
|
|
@@ -3303,8 +3688,8 @@ function runEdit(opts) {
|
|
|
3303
3688
|
};
|
|
3304
3689
|
renderPreview();
|
|
3305
3690
|
renderHandlesLayer();
|
|
3306
|
-
} else if (activeDrag) paintDragSnapshot(pixel);
|
|
3307
|
-
else if (activeBoxDrag) paintBoxDragSnapshot(pixel);
|
|
3691
|
+
} else if (activeDrag) paintDragSnapshot(pixel, info);
|
|
3692
|
+
else if (activeBoxDrag) paintBoxDragSnapshot(pixel, info, info?.shiftKey);
|
|
3308
3693
|
};
|
|
3309
3694
|
/** Reset drag-in-flight bookkeeping shared by every "gesture ended" path. */
|
|
3310
3695
|
function endDragBookkeeping() {
|
|
@@ -3312,7 +3697,7 @@ function runEdit(opts) {
|
|
|
3312
3697
|
dragInFlight = false;
|
|
3313
3698
|
drainPendingModes();
|
|
3314
3699
|
}
|
|
3315
|
-
const onDriverUp = (pixel) => {
|
|
3700
|
+
const onDriverUp = (pixel, info) => {
|
|
3316
3701
|
if (activeLabelRotateDrag) {
|
|
3317
3702
|
const drag = activeLabelRotateDrag;
|
|
3318
3703
|
activeLabelRotateDrag = null;
|
|
@@ -3321,7 +3706,7 @@ function runEdit(opts) {
|
|
|
3321
3706
|
if (Math.hypot(pixel[0] - drag.downPixel[0], pixel[1] - drag.downPixel[1]) > CLICK_SLOP_PX$3) {
|
|
3322
3707
|
const delta = pointerAngleAround(pixel, drag.anchorPixel) - drag.startPointerAngle;
|
|
3323
3708
|
const rotation = drag.startRotation - delta;
|
|
3324
|
-
setAmplifierPlacements({ [drag.key]: { rotation } });
|
|
3709
|
+
setAmplifierPlacements({ [drag.key]: { rotation } }, "gesture", "Rotate label");
|
|
3325
3710
|
} else {
|
|
3326
3711
|
renderPreview();
|
|
3327
3712
|
renderHandlesLayer();
|
|
@@ -3335,7 +3720,7 @@ function runEdit(opts) {
|
|
|
3335
3720
|
const position = labelAnchorFromPointer(pixel, grabOffset);
|
|
3336
3721
|
workingAmplifierPlacements = previous;
|
|
3337
3722
|
endDragBookkeeping();
|
|
3338
|
-
if (position && Math.hypot(pixel[0] - downPixel[0], pixel[1] - downPixel[1]) > CLICK_SLOP_PX$3) setAmplifierPlacements({ [field]: position });
|
|
3723
|
+
if (position && Math.hypot(pixel[0] - downPixel[0], pixel[1] - downPixel[1]) > CLICK_SLOP_PX$3) setAmplifierPlacements({ [field]: position }, "gesture", "Move label");
|
|
3339
3724
|
else {
|
|
3340
3725
|
selectedLabelKey = selectedLabelKey === field ? null : field;
|
|
3341
3726
|
renderPreview();
|
|
@@ -3351,10 +3736,10 @@ function runEdit(opts) {
|
|
|
3351
3736
|
driver.setDragPanEnabled(true);
|
|
3352
3737
|
return;
|
|
3353
3738
|
}
|
|
3354
|
-
const { fsm, preDragPoints } = activeDrag;
|
|
3739
|
+
const { fsm, preDragPoints, controlPointIndex } = activeDrag;
|
|
3355
3740
|
activeDrag = null;
|
|
3356
3741
|
driver.setDragPanEnabled(true);
|
|
3357
|
-
const next = fsm.end(pixel);
|
|
3742
|
+
const next = fsm.end(snappedPixel(pixel, info, "reshape", controlPointIndex));
|
|
3358
3743
|
replaceWorkingPoints(preDragPoints);
|
|
3359
3744
|
commitGesture(next);
|
|
3360
3745
|
return;
|
|
@@ -3380,11 +3765,16 @@ function runEdit(opts) {
|
|
|
3380
3765
|
bodyClickPending = true;
|
|
3381
3766
|
return;
|
|
3382
3767
|
}
|
|
3383
|
-
const
|
|
3768
|
+
const finalPixel = translatedBoxPixel(pixel, info, {
|
|
3769
|
+
fsm,
|
|
3770
|
+
preDragPoints,
|
|
3771
|
+
downPixel
|
|
3772
|
+
});
|
|
3773
|
+
const finalSnapshot = fsm.move(finalPixel, { constrainRotation: info?.shiftKey });
|
|
3384
3774
|
replaceWorkingPoints(preDragPoints);
|
|
3385
3775
|
workingAmplifierPlacements = preDragAmplifierPlacements;
|
|
3386
3776
|
if (fsm.kind === "rotate") boxAngle += finalSnapshot.box.angle - preDragBox.angle;
|
|
3387
|
-
commitGesture(finalSnapshot.points, preDragOptions, transformAmplifierPlacements(preDragAmplifierPlacements, preDragBox, finalSnapshot.box, driver));
|
|
3777
|
+
commitGesture(finalSnapshot.points, preDragOptions, transformAmplifierPlacements(preDragAmplifierPlacements, preDragBox, finalSnapshot.box, driver), preDragBox.angle);
|
|
3388
3778
|
return;
|
|
3389
3779
|
}
|
|
3390
3780
|
};
|
|
@@ -3435,8 +3825,12 @@ function runEdit(opts) {
|
|
|
3435
3825
|
],
|
|
3436
3826
|
onViewChange: rescaleOnViewChange,
|
|
3437
3827
|
onSettled: (settlement) => {
|
|
3438
|
-
|
|
3439
|
-
|
|
3828
|
+
try {
|
|
3829
|
+
history.settle();
|
|
3830
|
+
} finally {
|
|
3831
|
+
if (onSettled) onSettled();
|
|
3832
|
+
settleFromInteraction(settlement, commitEmitter, settleEmitter);
|
|
3833
|
+
}
|
|
3440
3834
|
},
|
|
3441
3835
|
attach(c) {
|
|
3442
3836
|
ctx = c;
|
|
@@ -3641,6 +4035,25 @@ function runGroupTransform(opts) {
|
|
|
3641
4035
|
const settleEmitter = createSettleEmitter("TransformSession.onSettled listener errors");
|
|
3642
4036
|
/** Session-scoped box rotation (ADR-0017 decision #2), shared by the whole group. */
|
|
3643
4037
|
let boxAngle = 0;
|
|
4038
|
+
function captureHistory() {
|
|
4039
|
+
return {
|
|
4040
|
+
ids: members.map((member) => member.measure.id),
|
|
4041
|
+
points: clonePointsByMember(),
|
|
4042
|
+
amplifierPlacements: cloneAmplifierPlacementsByMember(),
|
|
4043
|
+
optionsOverrides: cloneOptionsOverrideByMember(),
|
|
4044
|
+
sizeAnchor,
|
|
4045
|
+
nonGeometryDirty,
|
|
4046
|
+
boxAngle
|
|
4047
|
+
};
|
|
4048
|
+
}
|
|
4049
|
+
function restoreHistory(value) {
|
|
4050
|
+
restorePointsByMember(value.points);
|
|
4051
|
+
applyAmplifierPlacementsByMember(value.amplifierPlacements.map((placements) => placements ? structuredClone(placements) : void 0));
|
|
4052
|
+
applyOptionsOverrides(value.optionsOverrides);
|
|
4053
|
+
sizeAnchor = value.sizeAnchor;
|
|
4054
|
+
nonGeometryDirty = value.nonGeometryDirty;
|
|
4055
|
+
boxAngle = value.boxAngle;
|
|
4056
|
+
}
|
|
3644
4057
|
/** The group box fitted over every member's rendered geometry, or `null` when nothing projects this frame. */
|
|
3645
4058
|
let groupBox = null;
|
|
3646
4059
|
let ctx;
|
|
@@ -3817,12 +4230,13 @@ function runGroupTransform(opts) {
|
|
|
3817
4230
|
* instance to all listeners in order, deferred `reject`/`close`/`abort`
|
|
3818
4231
|
* (close/abort override reject), exception collection with `AggregateError`.
|
|
3819
4232
|
*/
|
|
3820
|
-
function emitChange(previousRendered, previousPointsByMember, previousAmplifierPlacementsByMember, previousSizeAnchor, previousNonGeometryDirty, previousOptionsOverrideByMember, currentRendered) {
|
|
4233
|
+
function emitChange(previousRendered, previousPointsByMember, previousAmplifierPlacementsByMember, previousSizeAnchor, previousNonGeometryDirty, previousOptionsOverrideByMember, currentRendered, source = "gesture", historyLabel = "Transform", previousBoxAngle = boxAngle) {
|
|
3821
4234
|
if (ctx.settled) return;
|
|
3822
4235
|
let rejectRequested = false;
|
|
3823
4236
|
let closeRequested = false;
|
|
3824
4237
|
let abortRequested = false;
|
|
3825
4238
|
const event = {
|
|
4239
|
+
source,
|
|
3826
4240
|
graphics: currentRendered,
|
|
3827
4241
|
previous: previousRendered,
|
|
3828
4242
|
session,
|
|
@@ -3851,10 +4265,12 @@ function runGroupTransform(opts) {
|
|
|
3851
4265
|
sizeAnchor = previousSizeAnchor;
|
|
3852
4266
|
nonGeometryDirty = previousNonGeometryDirty;
|
|
3853
4267
|
applyOptionsOverrides(previousOptionsOverrideByMember);
|
|
4268
|
+
boxAngle = previousBoxAngle;
|
|
3854
4269
|
renderAllPreviews();
|
|
3855
4270
|
renderHandlesLayer();
|
|
3856
4271
|
recomputeDirty();
|
|
3857
4272
|
}
|
|
4273
|
+
if (!ctx.settled && !rejectRequested && members === membersAtEmission && (source === "gesture" || source === "programmatic")) history.record(captureHistory(), historyLabel);
|
|
3858
4274
|
if (errors.length === 1) throw errors[0];
|
|
3859
4275
|
if (errors.length > 1) throw new AggregateError(errors, "TransformSession.onChange listener errors");
|
|
3860
4276
|
}
|
|
@@ -3880,7 +4296,7 @@ function runGroupTransform(opts) {
|
|
|
3880
4296
|
}
|
|
3881
4297
|
return snapshots;
|
|
3882
4298
|
}
|
|
3883
|
-
function commitGesture(nextConcatPoints, preGestureOptionsOverrideByMember, nextAmplifierPlacementsByMember) {
|
|
4299
|
+
function commitGesture(nextConcatPoints, preGestureOptionsOverrideByMember, nextAmplifierPlacementsByMember, preGestureBoxAngle = boxAngle) {
|
|
3884
4300
|
if (ctx.settled) return;
|
|
3885
4301
|
const previousPointsByMember = clonePointsByMember();
|
|
3886
4302
|
const previousAmplifierPlacementsByMember = cloneAmplifierPlacementsByMember();
|
|
@@ -3901,7 +4317,7 @@ function runGroupTransform(opts) {
|
|
|
3901
4317
|
renderHandlesLayer();
|
|
3902
4318
|
recomputeDirty();
|
|
3903
4319
|
dragInFlight = false;
|
|
3904
|
-
emitChange(previousRendered, previousPointsByMember, previousAmplifierPlacementsByMember, sizeAnchor, nonGeometryDirty, previousOptionsOverrideByMember, currentRendered);
|
|
4320
|
+
emitChange(previousRendered, previousPointsByMember, previousAmplifierPlacementsByMember, sizeAnchor, nonGeometryDirty, previousOptionsOverrideByMember, currentRendered, "gesture", "Transform", preGestureBoxAngle);
|
|
3905
4321
|
}
|
|
3906
4322
|
function setSizeAnchor(next) {
|
|
3907
4323
|
if (ctx.settled || sizeAnchor === next) return;
|
|
@@ -3915,7 +4331,7 @@ function runGroupTransform(opts) {
|
|
|
3915
4331
|
const currentRendered = rebuildRenderedAll();
|
|
3916
4332
|
nonGeometryDirty = true;
|
|
3917
4333
|
dirty = true;
|
|
3918
|
-
emitChange(previousRendered, previousPointsByMember, previousAmplifierPlacementsByMember, previousSizeAnchor, previousNonGeometryDirty, previousOptionsOverrideByMember, currentRendered);
|
|
4334
|
+
emitChange(previousRendered, previousPointsByMember, previousAmplifierPlacementsByMember, previousSizeAnchor, previousNonGeometryDirty, previousOptionsOverrideByMember, currentRendered, "programmatic", "Change size anchor");
|
|
3919
4335
|
}
|
|
3920
4336
|
/**
|
|
3921
4337
|
* Cancel an in-flight box drag without committing it: restore the pre-drag
|
|
@@ -3962,13 +4378,42 @@ function runGroupTransform(opts) {
|
|
|
3962
4378
|
renderAllPreviews();
|
|
3963
4379
|
renderHandlesLayer();
|
|
3964
4380
|
recomputeDirty();
|
|
3965
|
-
|
|
4381
|
+
const notificationErrors = [];
|
|
4382
|
+
try {
|
|
4383
|
+
history.reset(captureHistory());
|
|
4384
|
+
} catch (error) {
|
|
4385
|
+
notificationErrors.push(error);
|
|
4386
|
+
}
|
|
4387
|
+
if (removedSnapshots.length > 0) try {
|
|
4388
|
+
commitEmitter.emit(removedSnapshots);
|
|
4389
|
+
} catch (error) {
|
|
4390
|
+
notificationErrors.push(error);
|
|
4391
|
+
}
|
|
4392
|
+
if (notificationErrors.length === 1) throw notificationErrors[0];
|
|
4393
|
+
if (notificationErrors.length > 1) throw new AggregateError(notificationErrors, "TransformSession.setGraphics notification errors");
|
|
3966
4394
|
return removedSnapshots;
|
|
3967
4395
|
}
|
|
3968
4396
|
function close() {
|
|
3969
4397
|
return commitClose();
|
|
3970
4398
|
}
|
|
4399
|
+
const history = createSessionHistory({
|
|
4400
|
+
initial: captureHistory(),
|
|
4401
|
+
clone: (value) => structuredClone(value),
|
|
4402
|
+
equals: (a, b) => JSON.stringify(a) === JSON.stringify(b),
|
|
4403
|
+
isCurrent: (value) => JSON.stringify(captureHistory()) === JSON.stringify(value),
|
|
4404
|
+
apply(from, to, source) {
|
|
4405
|
+
if (JSON.stringify(from.ids) !== JSON.stringify(to.ids)) return false;
|
|
4406
|
+
const previousRendered = rebuildRenderedAll();
|
|
4407
|
+
restoreHistory(to);
|
|
4408
|
+
renderAllPreviews();
|
|
4409
|
+
renderHandlesLayer();
|
|
4410
|
+
recomputeDirty();
|
|
4411
|
+
emitChange(previousRendered, from.points, from.amplifierPlacements, from.sizeAnchor, from.nonGeometryDirty, from.optionsOverrides, rebuildRenderedAll(), source, "Transform", from.boxAngle);
|
|
4412
|
+
return !ctx.settled && JSON.stringify(captureHistory()) === JSON.stringify(to);
|
|
4413
|
+
}
|
|
4414
|
+
});
|
|
3971
4415
|
const session = {
|
|
4416
|
+
history,
|
|
3972
4417
|
get graphics() {
|
|
3973
4418
|
return members.map((m) => m.measure);
|
|
3974
4419
|
},
|
|
@@ -4044,9 +4489,9 @@ function runGroupTransform(opts) {
|
|
|
4044
4489
|
dragInFlight = true;
|
|
4045
4490
|
driver.setDragPanEnabled(false);
|
|
4046
4491
|
};
|
|
4047
|
-
function paintBoxDragSnapshot(pixel) {
|
|
4492
|
+
function paintBoxDragSnapshot(pixel, constrainRotation = false) {
|
|
4048
4493
|
if (!activeBoxDrag) return;
|
|
4049
|
-
const snapshot = activeBoxDrag.fsm.move(pixel);
|
|
4494
|
+
const snapshot = activeBoxDrag.fsm.move(pixel, { constrainRotation });
|
|
4050
4495
|
activeBoxDrag.lastSnapshot = snapshot;
|
|
4051
4496
|
applyConcatenatedPoints(snapshot.points);
|
|
4052
4497
|
applyAmplifierPlacementsByMember(activeBoxDrag.preDragAmplifierPlacementsByMember.map((placements) => transformAmplifierPlacements(placements, activeBoxDrag.preDragBox, snapshot.box, driver)));
|
|
@@ -4054,11 +4499,11 @@ function runGroupTransform(opts) {
|
|
|
4054
4499
|
renderHandlesLayer();
|
|
4055
4500
|
recomputeDirty();
|
|
4056
4501
|
}
|
|
4057
|
-
const onDriverMove = (pixel) => {
|
|
4502
|
+
const onDriverMove = (pixel, info) => {
|
|
4058
4503
|
if (ctx.settled) return;
|
|
4059
|
-
if (activeBoxDrag) paintBoxDragSnapshot(pixel);
|
|
4504
|
+
if (activeBoxDrag) paintBoxDragSnapshot(pixel, info?.shiftKey);
|
|
4060
4505
|
};
|
|
4061
|
-
const onDriverUp = (pixel) => {
|
|
4506
|
+
const onDriverUp = (pixel, info) => {
|
|
4062
4507
|
if (!activeBoxDrag) return;
|
|
4063
4508
|
if (ctx.settled) {
|
|
4064
4509
|
activeBoxDrag = null;
|
|
@@ -4080,11 +4525,11 @@ function runGroupTransform(opts) {
|
|
|
4080
4525
|
bodyClickPending = true;
|
|
4081
4526
|
return;
|
|
4082
4527
|
}
|
|
4083
|
-
const finalSnapshot = fsm.move(pixel);
|
|
4528
|
+
const finalSnapshot = fsm.move(pixel, { constrainRotation: info?.shiftKey });
|
|
4084
4529
|
applyConcatenatedPoints(preDragPoints);
|
|
4085
4530
|
applyAmplifierPlacementsByMember(preDragAmplifierPlacementsByMember);
|
|
4086
4531
|
if (fsm.kind === "rotate") boxAngle += finalSnapshot.box.angle - preDragBox.angle;
|
|
4087
|
-
commitGesture(finalSnapshot.points, preDragOptionsOverrideByMember, preDragAmplifierPlacementsByMember.map((placements) => transformAmplifierPlacements(placements, preDragBox, finalSnapshot.box, driver)));
|
|
4532
|
+
commitGesture(finalSnapshot.points, preDragOptionsOverrideByMember, preDragAmplifierPlacementsByMember.map((placements) => transformAmplifierPlacements(placements, preDragBox, finalSnapshot.box, driver)), preDragBox.angle);
|
|
4088
4533
|
};
|
|
4089
4534
|
let pickPreview;
|
|
4090
4535
|
let pickHandles;
|
|
@@ -4130,8 +4575,12 @@ function runGroupTransform(opts) {
|
|
|
4130
4575
|
layers: [previewLayer, handlesLayer],
|
|
4131
4576
|
onViewChange: rescaleOnViewChange,
|
|
4132
4577
|
onSettled: (settlement) => {
|
|
4133
|
-
|
|
4134
|
-
|
|
4578
|
+
try {
|
|
4579
|
+
history.settle();
|
|
4580
|
+
} finally {
|
|
4581
|
+
if (onSettled) onSettled();
|
|
4582
|
+
if (settlement.status === "rejected") settleEmitter.emit(settleReasonFromError(settlement.error));
|
|
4583
|
+
}
|
|
4135
4584
|
},
|
|
4136
4585
|
attach(c) {
|
|
4137
4586
|
ctx = c;
|
|
@@ -4245,13 +4694,6 @@ function pointSymbolFeature(symbol, render, resolution) {
|
|
|
4245
4694
|
};
|
|
4246
4695
|
}
|
|
4247
4696
|
//#endregion
|
|
4248
|
-
//#region src/tactical-draw/rotation.ts
|
|
4249
|
-
/** Normalize radians to the canonical clockwise [0, 2π) range. */
|
|
4250
|
-
function normalizeRotation(angle) {
|
|
4251
|
-
const turn = Math.PI * 2;
|
|
4252
|
-
return (angle % turn + turn) % turn;
|
|
4253
|
-
}
|
|
4254
|
-
//#endregion
|
|
4255
4697
|
//#region src/tactical-draw/graphic-transform-controller.ts
|
|
4256
4698
|
const HIT_TOLERANCE_PX$1 = 12;
|
|
4257
4699
|
const CLICK_SLOP_PX$1 = 3;
|
|
@@ -4272,13 +4714,22 @@ function runGraphicTransform(opts) {
|
|
|
4272
4714
|
const commitEmitter = createCommitEmitter("GraphicTransformSession.onCommit listener errors");
|
|
4273
4715
|
const settleEmitter = createSettleEmitter("GraphicTransformSession.onSettled listener errors");
|
|
4274
4716
|
const renderOpts = graphicsStyle ? { graphicsStyle } : void 0;
|
|
4275
|
-
const
|
|
4717
|
+
const snapshotsFor = (source) => source.map((graphic) => isPointSymbol(graphic) ? {
|
|
4276
4718
|
graphic: clonePointSymbol(graphic),
|
|
4277
4719
|
render: capability.render(graphic)
|
|
4278
4720
|
} : {
|
|
4279
4721
|
graphic: cloneControlMeasure(graphic),
|
|
4280
4722
|
render: renderMeasureForAdapter(adapter, graphic, renderOpts)
|
|
4281
4723
|
});
|
|
4724
|
+
const snapshots = () => snapshotsFor(working);
|
|
4725
|
+
const captureHistory = () => ({
|
|
4726
|
+
working: working.map(cloneGraphic),
|
|
4727
|
+
boxAngle
|
|
4728
|
+
});
|
|
4729
|
+
const cloneHistory = (value) => ({
|
|
4730
|
+
working: value.working.map(cloneGraphic),
|
|
4731
|
+
boxAngle: value.boxAngle
|
|
4732
|
+
});
|
|
4282
4733
|
function renderAll() {
|
|
4283
4734
|
if (ctx.settled) return;
|
|
4284
4735
|
const features = [];
|
|
@@ -4345,12 +4796,14 @@ function runGraphicTransform(opts) {
|
|
|
4345
4796
|
return next;
|
|
4346
4797
|
});
|
|
4347
4798
|
}
|
|
4348
|
-
function emit(
|
|
4799
|
+
function emit(previousState, source) {
|
|
4800
|
+
const previous = snapshotsFor(previousState.working);
|
|
4349
4801
|
const current = snapshots();
|
|
4350
4802
|
let reject = false;
|
|
4351
4803
|
let close = false;
|
|
4352
4804
|
let abort = false;
|
|
4353
4805
|
const event = {
|
|
4806
|
+
source,
|
|
4354
4807
|
graphics: current,
|
|
4355
4808
|
previous,
|
|
4356
4809
|
session,
|
|
@@ -4367,14 +4820,30 @@ function runGraphicTransform(opts) {
|
|
|
4367
4820
|
if (abort) ctx.fail(new TacticalDrawAbortError("session"));
|
|
4368
4821
|
else if (close) ctx.commit(snapshots);
|
|
4369
4822
|
else if (reject && !ctx.settled) {
|
|
4370
|
-
working =
|
|
4823
|
+
working = previousState.working.map(cloneGraphic);
|
|
4824
|
+
boxAngle = previousState.boxAngle;
|
|
4371
4825
|
dirty = JSON.stringify(working) !== JSON.stringify(original);
|
|
4372
4826
|
renderAll();
|
|
4373
4827
|
}
|
|
4374
4828
|
if (errors.length === 1) throw errors[0];
|
|
4375
4829
|
if (errors.length > 1) throw new AggregateError(errors, "GraphicTransformSession.onChange errors");
|
|
4376
|
-
|
|
4830
|
+
return !ctx.settled && !reject;
|
|
4831
|
+
}
|
|
4832
|
+
const history = createSessionHistory({
|
|
4833
|
+
initial: captureHistory(),
|
|
4834
|
+
clone: cloneHistory,
|
|
4835
|
+
equals: (a, b) => a.boxAngle === b.boxAngle && JSON.stringify(a.working) === JSON.stringify(b.working),
|
|
4836
|
+
isCurrent: (value) => boxAngle === value.boxAngle && JSON.stringify(working) === JSON.stringify(value.working),
|
|
4837
|
+
apply(from, to, source) {
|
|
4838
|
+
working = to.working.map(cloneGraphic);
|
|
4839
|
+
boxAngle = to.boxAngle;
|
|
4840
|
+
dirty = JSON.stringify(working) !== JSON.stringify(original);
|
|
4841
|
+
renderAll();
|
|
4842
|
+
return emit(from, source);
|
|
4843
|
+
}
|
|
4844
|
+
});
|
|
4377
4845
|
const session = {
|
|
4846
|
+
history,
|
|
4378
4847
|
get graphics() {
|
|
4379
4848
|
return original.map(cloneGraphic);
|
|
4380
4849
|
},
|
|
@@ -4421,9 +4890,9 @@ function runGraphicTransform(opts) {
|
|
|
4421
4890
|
};
|
|
4422
4891
|
driver.setDragPanEnabled(false);
|
|
4423
4892
|
};
|
|
4424
|
-
const dragResult = (pixel) => {
|
|
4893
|
+
const dragResult = (pixel, constrainRotation = false) => {
|
|
4425
4894
|
if (!drag) return null;
|
|
4426
|
-
const result = drag.fsm.move(pixel);
|
|
4895
|
+
const result = drag.fsm.move(pixel, { constrainRotation });
|
|
4427
4896
|
const scale = drag.fsm.kind === "scale" ? Math.max(result.box.halfWidth / drag.startBox.halfWidth, result.box.halfHeight / drag.startBox.halfHeight) : 1;
|
|
4428
4897
|
const rotation = drag.fsm.kind === "rotate" ? result.box.angle - drag.startBox.angle : 0;
|
|
4429
4898
|
return {
|
|
@@ -4431,18 +4900,18 @@ function runGraphicTransform(opts) {
|
|
|
4431
4900
|
graphics: applyPoints(drag.start, result.points, scale, rotation)
|
|
4432
4901
|
};
|
|
4433
4902
|
};
|
|
4434
|
-
const onMove = (pixel) => {
|
|
4903
|
+
const onMove = (pixel, info) => {
|
|
4435
4904
|
const active = drag;
|
|
4436
|
-
const next = dragResult(pixel);
|
|
4905
|
+
const next = dragResult(pixel, info?.shiftKey);
|
|
4437
4906
|
if (!active || !next) return;
|
|
4438
4907
|
working = next.graphics;
|
|
4439
4908
|
active.liveBox = next.result.box;
|
|
4440
4909
|
renderAll();
|
|
4441
4910
|
};
|
|
4442
|
-
const onUp = (pixel) => {
|
|
4911
|
+
const onUp = (pixel, info) => {
|
|
4443
4912
|
if (!drag) return;
|
|
4444
4913
|
const active = drag;
|
|
4445
|
-
const next = dragResult(pixel);
|
|
4914
|
+
const next = dragResult(pixel, info?.shiftKey);
|
|
4446
4915
|
drag = null;
|
|
4447
4916
|
suppressClickAway = true;
|
|
4448
4917
|
driver.setDragPanEnabled(true);
|
|
@@ -4452,13 +4921,15 @@ function runGraphicTransform(opts) {
|
|
|
4452
4921
|
renderAll();
|
|
4453
4922
|
return;
|
|
4454
4923
|
}
|
|
4455
|
-
const
|
|
4456
|
-
const previousGraphics = working.map(cloneGraphic);
|
|
4924
|
+
const previousState = captureHistory();
|
|
4457
4925
|
working = next.graphics;
|
|
4458
4926
|
if (active.fsm.kind === "rotate") boxAngle += next.result.box.angle - active.startBox.angle;
|
|
4459
4927
|
dirty = JSON.stringify(working) !== JSON.stringify(original);
|
|
4460
4928
|
renderAll();
|
|
4461
|
-
emit(
|
|
4929
|
+
if (emit(previousState, "gesture")) {
|
|
4930
|
+
const label = `${active.fsm.kind[0].toUpperCase()}${active.fsm.kind.slice(1)}`;
|
|
4931
|
+
history.record(captureHistory(), label);
|
|
4932
|
+
}
|
|
4462
4933
|
};
|
|
4463
4934
|
let previewPick;
|
|
4464
4935
|
let handlesPick;
|
|
@@ -4481,8 +4952,12 @@ function runGraphicTransform(opts) {
|
|
|
4481
4952
|
layers: [previewLayer, handlesLayer],
|
|
4482
4953
|
onViewChange: renderAll,
|
|
4483
4954
|
onSettled(settlement) {
|
|
4484
|
-
|
|
4485
|
-
|
|
4955
|
+
try {
|
|
4956
|
+
history.settle();
|
|
4957
|
+
} finally {
|
|
4958
|
+
onSettled?.();
|
|
4959
|
+
settleFromInteraction(settlement, commitEmitter, settleEmitter);
|
|
4960
|
+
}
|
|
4486
4961
|
},
|
|
4487
4962
|
attach(interaction) {
|
|
4488
4963
|
ctx = interaction;
|
|
@@ -4548,7 +5023,7 @@ var PointSymbolCapabilityRequiredError = class extends Error {
|
|
|
4548
5023
|
//#region src/tactical-draw/point-symbol-draw-controller.ts
|
|
4549
5024
|
const PREVIEW_ID = "__td_point_symbol_preview__";
|
|
4550
5025
|
function runPointSymbolDraw(options) {
|
|
4551
|
-
const { adapter, capability, draft, previewLayer, generateId, signal, onSettled } = options;
|
|
5026
|
+
const { adapter, capability, draft, previewLayer, generateId, signal, onSettled, snap } = options;
|
|
4552
5027
|
let latestPosition = draft.position;
|
|
4553
5028
|
let ctx;
|
|
4554
5029
|
const savedCursor = "";
|
|
@@ -4568,13 +5043,13 @@ function runPointSymbolDraw(options) {
|
|
|
4568
5043
|
ctx.writeLayer(previewLayer, [plan.feature]);
|
|
4569
5044
|
};
|
|
4570
5045
|
const handlePointerMove = (event) => {
|
|
4571
|
-
latestPosition = event
|
|
5046
|
+
latestPosition = snappedDrawPosition(snap, event);
|
|
4572
5047
|
renderPreview();
|
|
4573
5048
|
};
|
|
4574
5049
|
const handleClick = (event) => {
|
|
4575
|
-
latestPosition = event
|
|
5050
|
+
latestPosition = snappedDrawPosition(snap, event);
|
|
4576
5051
|
ctx.commit(() => {
|
|
4577
|
-
const graphic = build(draft.id ?? generateId(),
|
|
5052
|
+
const graphic = build(draft.id ?? generateId(), latestPosition);
|
|
4578
5053
|
return {
|
|
4579
5054
|
graphic: clonePointSymbol(graphic),
|
|
4580
5055
|
render: capability.render(graphic)
|
|
@@ -4621,7 +5096,7 @@ function symbolBox(adapter, capability, symbol, padding = 0) {
|
|
|
4621
5096
|
};
|
|
4622
5097
|
}
|
|
4623
5098
|
function runPointSymbolEdit(opts) {
|
|
4624
|
-
const { adapter, capability, symbol, graphicsLayer, previewLayer, handlesLayer, signal, closeOnClickAway = true, interactionStyle, onSession, handleIdPrefix, restoreOriginalToGraphics, wasOnGraphicsLayer, onSettled, onClickAwayGraphicsPick } = opts;
|
|
5099
|
+
const { adapter, capability, symbol, graphicsLayer, previewLayer, handlesLayer, signal, closeOnClickAway = true, interactionStyle, onSession, handleIdPrefix, restoreOriginalToGraphics, wasOnGraphicsLayer, onSettled, onClickAwayGraphicsPick, snap } = opts;
|
|
4625
5100
|
let modes = normalisePointModes(opts.modes);
|
|
4626
5101
|
let working = clonePointSymbol(symbol);
|
|
4627
5102
|
let dirty = false;
|
|
@@ -4632,6 +5107,8 @@ function runPointSymbolEdit(opts) {
|
|
|
4632
5107
|
const listeners = /* @__PURE__ */ new Set();
|
|
4633
5108
|
const commitEmitter = createCommitEmitter("GraphicEditSession.onCommit listener errors");
|
|
4634
5109
|
const settleEmitter = createSettleEmitter("GraphicEditSession.onSettled listener errors");
|
|
5110
|
+
const captureHistory = () => ({ working: clonePointSymbol(working) });
|
|
5111
|
+
const cloneHistory = (value) => ({ working: clonePointSymbol(value.working) });
|
|
4635
5112
|
const snapshot = () => ({
|
|
4636
5113
|
graphic: clonePointSymbol(working),
|
|
4637
5114
|
render: capability.render(working)
|
|
@@ -4649,15 +5126,17 @@ function runPointSymbolEdit(opts) {
|
|
|
4649
5126
|
}
|
|
4650
5127
|
ctx.writeLayer(handlesLayer, handles);
|
|
4651
5128
|
};
|
|
4652
|
-
const
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
5129
|
+
const emitChange = (previousState, source) => {
|
|
5130
|
+
const previous = {
|
|
5131
|
+
graphic: clonePointSymbol(previousState.working),
|
|
5132
|
+
render: capability.render(previousState.working)
|
|
5133
|
+
};
|
|
4656
5134
|
const current = snapshot();
|
|
4657
5135
|
let reject = false;
|
|
4658
5136
|
let close = false;
|
|
4659
5137
|
let abort = false;
|
|
4660
5138
|
const event = {
|
|
5139
|
+
source,
|
|
4661
5140
|
graphic: current,
|
|
4662
5141
|
previous,
|
|
4663
5142
|
session,
|
|
@@ -4680,14 +5159,35 @@ function runPointSymbolEdit(opts) {
|
|
|
4680
5159
|
if (abort) ctx.fail(new TacticalDrawAbortError("session"));
|
|
4681
5160
|
else if (close) ctx.commit(snapshot);
|
|
4682
5161
|
else if (reject && !ctx.settled) {
|
|
4683
|
-
working = clonePointSymbol(
|
|
5162
|
+
working = clonePointSymbol(previousState.working);
|
|
4684
5163
|
dirty = JSON.stringify(working) !== JSON.stringify(symbol);
|
|
4685
5164
|
render();
|
|
4686
5165
|
}
|
|
4687
5166
|
if (errors.length === 1) throw errors[0];
|
|
4688
5167
|
if (errors.length > 1) throw new AggregateError(errors, "GraphicEditSession.onChange errors");
|
|
5168
|
+
return !ctx.settled && !reject;
|
|
5169
|
+
};
|
|
5170
|
+
const history = createSessionHistory({
|
|
5171
|
+
initial: captureHistory(),
|
|
5172
|
+
clone: cloneHistory,
|
|
5173
|
+
equals: (a, b) => JSON.stringify(a.working) === JSON.stringify(b.working),
|
|
5174
|
+
isCurrent: (value) => JSON.stringify(working) === JSON.stringify(value.working),
|
|
5175
|
+
apply(from, to, source) {
|
|
5176
|
+
working = clonePointSymbol(to.working);
|
|
5177
|
+
dirty = JSON.stringify(working) !== JSON.stringify(symbol);
|
|
5178
|
+
render();
|
|
5179
|
+
return emitChange(from, source);
|
|
5180
|
+
}
|
|
5181
|
+
});
|
|
5182
|
+
const apply = (next, source = "programmatic", label = "Update graphic") => {
|
|
5183
|
+
const previousState = captureHistory();
|
|
5184
|
+
working = clonePointSymbol(next);
|
|
5185
|
+
dirty = JSON.stringify(working) !== JSON.stringify(symbol);
|
|
5186
|
+
render();
|
|
5187
|
+
if (emitChange(previousState, source)) history.record(captureHistory(), label);
|
|
4689
5188
|
};
|
|
4690
5189
|
const session = {
|
|
5190
|
+
history,
|
|
4691
5191
|
graphic: clonePointSymbol(symbol),
|
|
4692
5192
|
get workingGraphic() {
|
|
4693
5193
|
return clonePointSymbol(working);
|
|
@@ -4711,11 +5211,10 @@ function runPointSymbolEdit(opts) {
|
|
|
4711
5211
|
},
|
|
4712
5212
|
updateGraphic(updater) {
|
|
4713
5213
|
if (ctx.settled) return;
|
|
4714
|
-
const previous = snapshot();
|
|
4715
5214
|
const next = updater(clonePointSymbol(working));
|
|
4716
5215
|
if (next.kind !== "point-symbol" || next.id !== symbol.id) throw new TypeError("updateGraphic must preserve point-symbol kind and id");
|
|
4717
5216
|
capability.render(next);
|
|
4718
|
-
apply(next,
|
|
5217
|
+
apply(next, "programmatic", "Update graphic");
|
|
4719
5218
|
},
|
|
4720
5219
|
onChange(listener) {
|
|
4721
5220
|
if (ctx.settled) return () => {};
|
|
@@ -4747,27 +5246,38 @@ function runPointSymbolEdit(opts) {
|
|
|
4747
5246
|
};
|
|
4748
5247
|
driver.setDragPanEnabled(false);
|
|
4749
5248
|
};
|
|
4750
|
-
const transformed = (pixel) => {
|
|
5249
|
+
const transformed = (pixel, info) => {
|
|
4751
5250
|
if (!drag) return null;
|
|
4752
5251
|
const next = clonePointSymbol(drag.start);
|
|
4753
|
-
if (drag.kind === "body")
|
|
4754
|
-
|
|
4755
|
-
|
|
5252
|
+
if (drag.kind === "body") {
|
|
5253
|
+
const unsnappedPosition = driver.fromPixel([drag.anchor[0] + pixel[0] - drag.down[0], drag.anchor[1] + pixel[1] - drag.down[1]]);
|
|
5254
|
+
const anchorPixel = driver.toPixel(unsnappedPosition) ?? pixel;
|
|
5255
|
+
next.position = (snap?.({
|
|
5256
|
+
coordinate: unsnappedPosition,
|
|
5257
|
+
pixel: anchorPixel,
|
|
5258
|
+
interaction: "translate",
|
|
5259
|
+
graphicId: symbol.id,
|
|
5260
|
+
originalEvent: info?.native
|
|
5261
|
+
}))?.coordinate ?? unsnappedPosition;
|
|
5262
|
+
} else if (drag.kind === "rotate") {
|
|
5263
|
+
const rotation = drag.start.rotation + pointerAngle(pixel, drag.anchor) - drag.startPointerAngle;
|
|
5264
|
+
next.rotation = normalizeRotation(info?.shiftKey ? constrainRotation(rotation) : rotation);
|
|
5265
|
+
} else {
|
|
4756
5266
|
const distance = Math.hypot(pixel[0] - drag.anchor[0], pixel[1] - drag.anchor[1]);
|
|
4757
5267
|
next.size.value = Math.max(.1, drag.start.size.value * (distance / drag.startDistance));
|
|
4758
5268
|
}
|
|
4759
5269
|
return next;
|
|
4760
5270
|
};
|
|
4761
|
-
const onMove = (pixel) => {
|
|
4762
|
-
const next = transformed(pixel);
|
|
5271
|
+
const onMove = (pixel, info) => {
|
|
5272
|
+
const next = transformed(pixel, info);
|
|
4763
5273
|
if (!next) return;
|
|
4764
5274
|
working = next;
|
|
4765
5275
|
render();
|
|
4766
5276
|
};
|
|
4767
|
-
const onUp = (pixel) => {
|
|
5277
|
+
const onUp = (pixel, info) => {
|
|
4768
5278
|
if (!drag) return;
|
|
4769
5279
|
const currentDrag = drag;
|
|
4770
|
-
const next = transformed(pixel);
|
|
5280
|
+
const next = transformed(pixel, info);
|
|
4771
5281
|
drag = null;
|
|
4772
5282
|
suppressClickAway = true;
|
|
4773
5283
|
driver.setDragPanEnabled(true);
|
|
@@ -4777,7 +5287,7 @@ function runPointSymbolEdit(opts) {
|
|
|
4777
5287
|
render();
|
|
4778
5288
|
return;
|
|
4779
5289
|
}
|
|
4780
|
-
apply(next);
|
|
5290
|
+
apply(next, "gesture", `${currentDrag.kind[0].toUpperCase()}${currentDrag.kind.slice(1)}`);
|
|
4781
5291
|
};
|
|
4782
5292
|
let previewPick;
|
|
4783
5293
|
let handlesPick;
|
|
@@ -4803,8 +5313,12 @@ function runPointSymbolEdit(opts) {
|
|
|
4803
5313
|
layers: [previewLayer, handlesLayer],
|
|
4804
5314
|
onViewChange: render,
|
|
4805
5315
|
onSettled(settlement) {
|
|
4806
|
-
|
|
4807
|
-
|
|
5316
|
+
try {
|
|
5317
|
+
history.settle();
|
|
5318
|
+
} finally {
|
|
5319
|
+
onSettled?.();
|
|
5320
|
+
settleFromInteraction(settlement, commitEmitter, settleEmitter);
|
|
5321
|
+
}
|
|
4808
5322
|
},
|
|
4809
5323
|
attach(interaction) {
|
|
4810
5324
|
ctx = interaction;
|
|
@@ -4860,6 +5374,8 @@ var TacticalDraw = class {
|
|
|
4860
5374
|
defaultInteractionStyle;
|
|
4861
5375
|
generateId;
|
|
4862
5376
|
pointSymbols;
|
|
5377
|
+
snappingOptions;
|
|
5378
|
+
resolveSnap;
|
|
4863
5379
|
destroyed = false;
|
|
4864
5380
|
hasRendered = false;
|
|
4865
5381
|
activeInteraction = new ActiveInteractionTracker();
|
|
@@ -4910,12 +5426,15 @@ var TacticalDraw = class {
|
|
|
4910
5426
|
* surrounding collection state.
|
|
4911
5427
|
*/
|
|
4912
5428
|
lastRenderedMeasures = [];
|
|
5429
|
+
snapGeometryFeatures = [];
|
|
4913
5430
|
constructor(adapter, options) {
|
|
4914
5431
|
this.adapter = adapter;
|
|
4915
5432
|
this.graphicsStyle = resolveStyleHints(BUILT_IN_GRAPHICS_STYLE, options?.graphicsStyle, void 0);
|
|
4916
5433
|
this.defaultInteractionStyle = options?.interactionStyle;
|
|
4917
5434
|
this.generateId = options?.generateId ?? (() => `td-${++this.idCounter}`);
|
|
4918
5435
|
this.pointSymbols = options?.pointSymbols;
|
|
5436
|
+
this.snappingOptions = options?.snapping;
|
|
5437
|
+
this.resolveSnap = createSnapResolver(adapter, () => this.lastRenderedMeasures, () => this.snapGeometryFeatures, () => this.snappingOptions);
|
|
4919
5438
|
const owned = /* @__PURE__ */ new Set();
|
|
4920
5439
|
const provided = options?.layers ?? {};
|
|
4921
5440
|
this.graphicsLayer = resolveSlot(adapter, provided.graphics, owned);
|
|
@@ -4929,6 +5448,16 @@ var TacticalDraw = class {
|
|
|
4929
5448
|
this.adapter.setLayerStyle(this.guideLayer, guideToStyleOptions(this.baseGuideStyle()));
|
|
4930
5449
|
this.adapter.onViewChange(this.handleViewChange);
|
|
4931
5450
|
}
|
|
5451
|
+
/** Replace snapping configuration without recreating the map adapter or façade. */
|
|
5452
|
+
setSnappingOptions(options) {
|
|
5453
|
+
this.assertAlive();
|
|
5454
|
+
this.clearSnapResult();
|
|
5455
|
+
this.snappingOptions = options;
|
|
5456
|
+
}
|
|
5457
|
+
/** Clear transient host affordances when snapping or its interaction ends. */
|
|
5458
|
+
clearSnapResult() {
|
|
5459
|
+
this.snappingOptions?.onSnap?.(null);
|
|
5460
|
+
}
|
|
4932
5461
|
/**
|
|
4933
5462
|
* Render `measures` into features and write them to the graphics layer.
|
|
4934
5463
|
* Routes every measure through [[renderMeasureForAdapter]] so pixel sizing is
|
|
@@ -4951,6 +5480,7 @@ var TacticalDraw = class {
|
|
|
4951
5480
|
}
|
|
4952
5481
|
this.adapter.reconcileLayerIconResources(this.graphicsLayer, [...resources.values()]);
|
|
4953
5482
|
this.adapter.updateLayerFeatures(this.graphicsLayer, features);
|
|
5483
|
+
this.snapGeometryFeatures = features;
|
|
4954
5484
|
}
|
|
4955
5485
|
/**
|
|
4956
5486
|
* Re-render the graphics layer on a view (zoom) change so pixel-denominated
|
|
@@ -5111,6 +5641,7 @@ var TacticalDraw = class {
|
|
|
5111
5641
|
};
|
|
5112
5642
|
const onSettled = () => {
|
|
5113
5643
|
clearSlot();
|
|
5644
|
+
this.clearSnapResult();
|
|
5114
5645
|
if (extraSettle) extraSettle();
|
|
5115
5646
|
};
|
|
5116
5647
|
this.activeInteraction.begin();
|
|
@@ -5164,7 +5695,8 @@ var TacticalDraw = class {
|
|
|
5164
5695
|
previewLayer: this.previewLayer,
|
|
5165
5696
|
generateId: this.generateId,
|
|
5166
5697
|
signal: options?.signal,
|
|
5167
|
-
onSettled
|
|
5698
|
+
onSettled,
|
|
5699
|
+
snap: this.resolveSnap
|
|
5168
5700
|
}), null);
|
|
5169
5701
|
}
|
|
5170
5702
|
const measureDraft = draft;
|
|
@@ -5196,7 +5728,8 @@ var TacticalDraw = class {
|
|
|
5196
5728
|
measureTextAmplifiers: measureDraft.textAmplifiers,
|
|
5197
5729
|
guide: options?.guide,
|
|
5198
5730
|
sizeAnchor: options?.sizeAnchor,
|
|
5199
|
-
onSettled
|
|
5731
|
+
onSettled,
|
|
5732
|
+
snap: this.resolveSnap
|
|
5200
5733
|
}) : runVariableLengthDraw({
|
|
5201
5734
|
adapter: this.adapter,
|
|
5202
5735
|
kind,
|
|
@@ -5213,7 +5746,8 @@ var TacticalDraw = class {
|
|
|
5213
5746
|
measureTextAmplifiers: measureDraft.textAmplifiers,
|
|
5214
5747
|
guide: options?.guide,
|
|
5215
5748
|
sizeAnchor: options?.sizeAnchor,
|
|
5216
|
-
onSettled
|
|
5749
|
+
onSettled,
|
|
5750
|
+
snap: this.resolveSnap
|
|
5217
5751
|
}), null, extraSettle);
|
|
5218
5752
|
}
|
|
5219
5753
|
edit(measure, options) {
|
|
@@ -5242,7 +5776,8 @@ var TacticalDraw = class {
|
|
|
5242
5776
|
restoreOriginalToGraphics,
|
|
5243
5777
|
wasOnGraphicsLayer,
|
|
5244
5778
|
onSettled,
|
|
5245
|
-
onClickAwayGraphicsPick: (event) => this.dispatchHostPick(this.graphicsLayer, event)
|
|
5779
|
+
onClickAwayGraphicsPick: (event) => this.dispatchHostPick(this.graphicsLayer, event),
|
|
5780
|
+
snap: this.resolveSnap
|
|
5246
5781
|
}), (handle) => {
|
|
5247
5782
|
this.activeEdit = handle;
|
|
5248
5783
|
return () => {
|
|
@@ -5282,7 +5817,8 @@ var TacticalDraw = class {
|
|
|
5282
5817
|
restoreOriginalToGraphics,
|
|
5283
5818
|
wasOnGraphicsLayer,
|
|
5284
5819
|
onSettled,
|
|
5285
|
-
onClickAwayGraphicsPick
|
|
5820
|
+
onClickAwayGraphicsPick,
|
|
5821
|
+
snap: this.resolveSnap
|
|
5286
5822
|
}), (handle) => {
|
|
5287
5823
|
this.activeEdit = handle;
|
|
5288
5824
|
return () => {
|