@orbat-mapper/tactical-draw 0.2.0-alpha.19 → 0.2.0-alpha.20
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 +11 -7
- package/dist/index.d.mts +248 -70
- package/dist/index.mjs +900 -117
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -15,6 +15,8 @@ var BaseMapAdapter = class {
|
|
|
15
15
|
viewChangeTokens = /* @__PURE__ */ new Map();
|
|
16
16
|
featureCache = /* @__PURE__ */ new Map();
|
|
17
17
|
featuresChangedListeners = /* @__PURE__ */ new Map();
|
|
18
|
+
iconResources = /* @__PURE__ */ new Map();
|
|
19
|
+
layerIconResourceIds = /* @__PURE__ */ new Map();
|
|
18
20
|
layerCounter = 0;
|
|
19
21
|
destroyed = false;
|
|
20
22
|
nextLayerId() {
|
|
@@ -34,6 +36,19 @@ var BaseMapAdapter = class {
|
|
|
34
36
|
this.layers.delete(layerId);
|
|
35
37
|
this.featureCache.delete(layerId);
|
|
36
38
|
this.featuresChangedListeners.delete(layerId);
|
|
39
|
+
this.layerIconResourceIds.delete(layerId);
|
|
40
|
+
this.pruneIconResources();
|
|
41
|
+
}
|
|
42
|
+
reconcileLayerIconResources(layerId, resources) {
|
|
43
|
+
if (!this.layers.has(layerId)) return;
|
|
44
|
+
const ids = /* @__PURE__ */ new Set();
|
|
45
|
+
for (const resource of resources) {
|
|
46
|
+
ids.add(resource.id);
|
|
47
|
+
this.iconResources.set(resource.id, resource);
|
|
48
|
+
}
|
|
49
|
+
this.layerIconResourceIds.set(layerId, ids);
|
|
50
|
+
this.applyIconResources(resources);
|
|
51
|
+
this.pruneIconResources();
|
|
37
52
|
}
|
|
38
53
|
setLayerFeatures(layerId, features) {
|
|
39
54
|
if (!this.layers.has(layerId)) return;
|
|
@@ -148,6 +163,9 @@ var BaseMapAdapter = class {
|
|
|
148
163
|
this.layers.clear();
|
|
149
164
|
this.featureCache.clear();
|
|
150
165
|
this.featuresChangedListeners.clear();
|
|
166
|
+
for (const id of this.iconResources.keys()) this.removeIconResource(id);
|
|
167
|
+
this.iconResources.clear();
|
|
168
|
+
this.layerIconResourceIds.clear();
|
|
151
169
|
this.destroyEngineResources();
|
|
152
170
|
} finally {
|
|
153
171
|
this.destroyed = true;
|
|
@@ -173,6 +191,22 @@ var BaseMapAdapter = class {
|
|
|
173
191
|
};
|
|
174
192
|
}
|
|
175
193
|
destroyEngineResources() {}
|
|
194
|
+
applyIconResources(resources) {}
|
|
195
|
+
getIconResource(id) {
|
|
196
|
+
return this.iconResources.get(id);
|
|
197
|
+
}
|
|
198
|
+
removeIconResource(id) {}
|
|
199
|
+
pruneIconResources() {
|
|
200
|
+
const active = /* @__PURE__ */ new Set();
|
|
201
|
+
for (const ids of this.layerIconResourceIds.values()) for (const id of ids) active.add(id);
|
|
202
|
+
if (this.iconResources.size <= active.size + 32) return;
|
|
203
|
+
for (const id of this.iconResources.keys()) {
|
|
204
|
+
if (active.has(id)) continue;
|
|
205
|
+
this.iconResources.delete(id);
|
|
206
|
+
this.removeIconResource(id);
|
|
207
|
+
if (this.iconResources.size <= active.size + 32) break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
176
210
|
/**
|
|
177
211
|
* Default for engines/views that don't rotate (Leaflet). Rotating engines
|
|
178
212
|
* (MapLibre, OpenLayers) override this.
|
|
@@ -206,6 +240,8 @@ function mapEventHitTolerance(originalEvent) {
|
|
|
206
240
|
return direct;
|
|
207
241
|
}
|
|
208
242
|
function tryControlMeasureIdFromFeature(feature) {
|
|
243
|
+
const graphicId = feature.properties?.graphicId;
|
|
244
|
+
if (typeof graphicId === "string" && graphicId.length > 0) return graphicId;
|
|
209
245
|
try {
|
|
210
246
|
return controlMeasureIdFromFeature(feature);
|
|
211
247
|
} catch {
|
|
@@ -335,6 +371,33 @@ function hitTestFeature(pixel, features, hitTolerance, toPixel) {
|
|
|
335
371
|
}
|
|
336
372
|
return null;
|
|
337
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* Test a rendered icon-backed Point feature against its actual pixel footprint.
|
|
376
|
+
* The feature carries the adapter-neutral `iconScale` / `iconRotation` values
|
|
377
|
+
* emitted by TacticalDraw; `resource.anchor` is the placement anchor within the
|
|
378
|
+
* unscaled SVG viewport. A positive tolerance pads all four sides.
|
|
379
|
+
*/
|
|
380
|
+
function hitTestIconFeature(pixel, feature, resource, hitTolerance, toPixel, bearing = 0) {
|
|
381
|
+
if (feature.geometry.type !== "Point") return false;
|
|
382
|
+
const anchor = toPixel(feature.geometry.coordinates);
|
|
383
|
+
if (!anchor) return false;
|
|
384
|
+
const rawScale = feature.properties?.iconScale;
|
|
385
|
+
const scale = typeof rawScale === "number" ? rawScale : 1;
|
|
386
|
+
if (!Number.isFinite(scale) || scale <= 0) return false;
|
|
387
|
+
const rawRotation = feature.properties?.iconRotation;
|
|
388
|
+
const angle = (typeof rawRotation === "number" && Number.isFinite(rawRotation) ? rawRotation : 0) - bearing;
|
|
389
|
+
const cos = Math.cos(angle);
|
|
390
|
+
const sin = Math.sin(angle);
|
|
391
|
+
const dx = pixel[0] - anchor[0];
|
|
392
|
+
const dy = pixel[1] - anchor[1];
|
|
393
|
+
const localX = dx * cos + dy * sin;
|
|
394
|
+
const localY = -dx * sin + dy * cos;
|
|
395
|
+
const minX = -resource.anchor[0] * scale - hitTolerance;
|
|
396
|
+
const maxX = (resource.width - resource.anchor[0]) * scale + hitTolerance;
|
|
397
|
+
const minY = -resource.anchor[1] * scale - hitTolerance;
|
|
398
|
+
const maxY = (resource.height - resource.anchor[1]) * scale + hitTolerance;
|
|
399
|
+
return localX >= minX && localX <= maxX && localY >= minY && localY <= maxY;
|
|
400
|
+
}
|
|
338
401
|
//#endregion
|
|
339
402
|
//#region src/gestures/hit-tolerance.ts
|
|
340
403
|
const TOUCH_HIT_TOLERANCE_PX = 22;
|
|
@@ -388,6 +451,14 @@ var PointerCallbackHub = class {
|
|
|
388
451
|
}
|
|
389
452
|
};
|
|
390
453
|
//#endregion
|
|
454
|
+
//#region src/point-symbol.ts
|
|
455
|
+
function isPointSymbol(graphic) {
|
|
456
|
+
return typeof graphic === "object" && graphic !== null && "kind" in graphic && graphic.kind === "point-symbol";
|
|
457
|
+
}
|
|
458
|
+
function clonePointSymbol(symbol) {
|
|
459
|
+
return structuredClone(symbol);
|
|
460
|
+
}
|
|
461
|
+
//#endregion
|
|
391
462
|
//#region src/tactical-draw/active-interaction.ts
|
|
392
463
|
/**
|
|
393
464
|
* Owns the facade's public interaction state: the abort handle used by
|
|
@@ -1025,7 +1096,7 @@ function runFixedLengthDraw(options) {
|
|
|
1025
1096
|
});
|
|
1026
1097
|
const measure = sizeAnchor === "screen" ? captured : bakePixelSizeToGround(captured, adapter);
|
|
1027
1098
|
return {
|
|
1028
|
-
|
|
1099
|
+
graphic: cloneControlMeasure(measure),
|
|
1029
1100
|
render: renderMeasureForAdapter(adapter, measure, renderOpts)
|
|
1030
1101
|
};
|
|
1031
1102
|
}
|
|
@@ -1175,7 +1246,7 @@ function createSettleEmitter(listenerErrorsMessage) {
|
|
|
1175
1246
|
* `EditSession.onCommit`, and `DrawSession.onCommit`: a listener set with
|
|
1176
1247
|
* synchronous, exception-collecting emission (single rethrow, `AggregateError`
|
|
1177
1248
|
* otherwise). Unlike `createSettleEmitter`, `emit` may be called more than
|
|
1178
|
-
* once — a transform's `
|
|
1249
|
+
* once — a transform's `setGraphics` can commit several removal batches before
|
|
1179
1250
|
* the close batch — and there is no latch or late-registration path:
|
|
1180
1251
|
* registration-time `ctx.settled` guards stay in the controllers, mirroring
|
|
1181
1252
|
* `onChange`.
|
|
@@ -1397,7 +1468,7 @@ function runVariableLengthDraw(options) {
|
|
|
1397
1468
|
});
|
|
1398
1469
|
const measure = sizeAnchor === "screen" ? built : bakePixelSizeToGround(built, adapter);
|
|
1399
1470
|
return {
|
|
1400
|
-
|
|
1471
|
+
graphic: cloneControlMeasure(measure),
|
|
1401
1472
|
render: renderMeasureForAdapter(adapter, measure, renderOpts)
|
|
1402
1473
|
};
|
|
1403
1474
|
}
|
|
@@ -2188,7 +2259,7 @@ const LABEL_HANDLE_OFFSET_PX = 14;
|
|
|
2188
2259
|
* Corner/rotate drags commit even at zero movement (matches the pre-ADR-0017
|
|
2189
2260
|
* translate/rotate seams).
|
|
2190
2261
|
*/
|
|
2191
|
-
const CLICK_SLOP_PX$
|
|
2262
|
+
const CLICK_SLOP_PX$3 = 3;
|
|
2192
2263
|
const ALLOWED_EDIT_MODES = new Set([
|
|
2193
2264
|
"reshape",
|
|
2194
2265
|
"transform",
|
|
@@ -2464,7 +2535,7 @@ function runEdit(opts) {
|
|
|
2464
2535
|
else delete working.amplifierPlacements;
|
|
2465
2536
|
const committed = sizeAnchor === "screen" ? working : bakePixelSizeToGround(working, adapter);
|
|
2466
2537
|
return {
|
|
2467
|
-
|
|
2538
|
+
graphic: cloneControlMeasure(committed),
|
|
2468
2539
|
render: renderMeasureForAdapter(adapter, committed, renderOpts)
|
|
2469
2540
|
};
|
|
2470
2541
|
}
|
|
@@ -2519,7 +2590,7 @@ function runEdit(opts) {
|
|
|
2519
2590
|
let closeRequested = false;
|
|
2520
2591
|
let abortRequested = false;
|
|
2521
2592
|
const event = {
|
|
2522
|
-
|
|
2593
|
+
graphic: currentRendered,
|
|
2523
2594
|
previous: previousRendered,
|
|
2524
2595
|
session,
|
|
2525
2596
|
reject() {
|
|
@@ -2785,7 +2856,17 @@ function runEdit(opts) {
|
|
|
2785
2856
|
applyModes(next);
|
|
2786
2857
|
}
|
|
2787
2858
|
const session = {
|
|
2788
|
-
measure,
|
|
2859
|
+
graphic: measure,
|
|
2860
|
+
get workingGraphic() {
|
|
2861
|
+
return cloneControlMeasure({
|
|
2862
|
+
...measure,
|
|
2863
|
+
controlPoints: workingPoints.map((point) => [...point]),
|
|
2864
|
+
options: { ...workingOptions ?? {} },
|
|
2865
|
+
style: currentStyle ? { ...currentStyle } : void 0,
|
|
2866
|
+
textAmplifiers: workingTextAmplifiers ? { ...workingTextAmplifiers } : void 0,
|
|
2867
|
+
amplifierPlacements: snapshotAmplifierPlacements()
|
|
2868
|
+
});
|
|
2869
|
+
},
|
|
2789
2870
|
get controlPoints() {
|
|
2790
2871
|
return workingPoints.map((p) => [...p]);
|
|
2791
2872
|
},
|
|
@@ -2826,6 +2907,26 @@ function runEdit(opts) {
|
|
|
2826
2907
|
}
|
|
2827
2908
|
applyModes(normalised);
|
|
2828
2909
|
},
|
|
2910
|
+
updateGraphic(updater) {
|
|
2911
|
+
if (ctx.settled) return;
|
|
2912
|
+
const previous = rebuildRendered();
|
|
2913
|
+
const previousNonGeometryDirty = nonGeometryDirty;
|
|
2914
|
+
const previousSizeAnchor = sizeAnchor;
|
|
2915
|
+
const current = session.workingGraphic;
|
|
2916
|
+
const next = updater(current);
|
|
2917
|
+
if (next.id !== measure.id || next.kind !== measure.kind) throw new TypeError("updateGraphic must preserve control-measure id and kind");
|
|
2918
|
+
renderMeasureForAdapter(adapter, next, renderOpts);
|
|
2919
|
+
workingPoints.splice(0, workingPoints.length, ...next.controlPoints.map((point) => [...point]));
|
|
2920
|
+
workingOptions = { ...next.options ?? {} };
|
|
2921
|
+
currentStyle = next.style ? { ...next.style } : void 0;
|
|
2922
|
+
workingTextAmplifiers = next.textAmplifiers ? { ...next.textAmplifiers } : void 0;
|
|
2923
|
+
workingAmplifierPlacements = next.amplifierPlacements ? structuredClone(next.amplifierPlacements) : void 0;
|
|
2924
|
+
nonGeometryDirty = true;
|
|
2925
|
+
renderPreview();
|
|
2926
|
+
renderHandlesLayer();
|
|
2927
|
+
recomputeDirty();
|
|
2928
|
+
emitChange(previous, previous.graphic.controlPoints.map((point) => [...point]), previous.graphic.options, previous.graphic.style, previousNonGeometryDirty, previousSizeAnchor, rebuildRendered(), previous.graphic.textAmplifiers, previous.graphic.amplifierPlacements);
|
|
2929
|
+
},
|
|
2829
2930
|
setOptions(partial) {
|
|
2830
2931
|
setOptions(partial);
|
|
2831
2932
|
},
|
|
@@ -3189,7 +3290,7 @@ function runEdit(opts) {
|
|
|
3189
3290
|
activeLabelRotateDrag = null;
|
|
3190
3291
|
workingAmplifierPlacements = drag.previous;
|
|
3191
3292
|
endDragBookkeeping();
|
|
3192
|
-
if (Math.hypot(pixel[0] - drag.downPixel[0], pixel[1] - drag.downPixel[1]) > CLICK_SLOP_PX$
|
|
3293
|
+
if (Math.hypot(pixel[0] - drag.downPixel[0], pixel[1] - drag.downPixel[1]) > CLICK_SLOP_PX$3) {
|
|
3193
3294
|
const delta = pointerAngleAround(pixel, drag.anchorPixel) - drag.startPointerAngle;
|
|
3194
3295
|
const rotation = drag.startRotation - delta;
|
|
3195
3296
|
setAmplifierPlacements({ [drag.key]: { rotation } });
|
|
@@ -3206,7 +3307,7 @@ function runEdit(opts) {
|
|
|
3206
3307
|
const position = labelAnchorFromPointer(pixel, grabOffset);
|
|
3207
3308
|
workingAmplifierPlacements = previous;
|
|
3208
3309
|
endDragBookkeeping();
|
|
3209
|
-
if (position && Math.hypot(pixel[0] - downPixel[0], pixel[1] - downPixel[1]) > CLICK_SLOP_PX$
|
|
3310
|
+
if (position && Math.hypot(pixel[0] - downPixel[0], pixel[1] - downPixel[1]) > CLICK_SLOP_PX$3) setAmplifierPlacements({ [field]: position });
|
|
3210
3311
|
else {
|
|
3211
3312
|
selectedLabelKey = selectedLabelKey === field ? null : field;
|
|
3212
3313
|
renderPreview();
|
|
@@ -3241,7 +3342,7 @@ function runEdit(opts) {
|
|
|
3241
3342
|
activeBoxDrag = null;
|
|
3242
3343
|
driver.setDragPanEnabled(true);
|
|
3243
3344
|
const distance = Math.hypot(pixel[0] - downPixel[0], pixel[1] - downPixel[1]);
|
|
3244
|
-
if (fsm.kind === "body" && distance <= CLICK_SLOP_PX$
|
|
3345
|
+
if (fsm.kind === "body" && distance <= CLICK_SLOP_PX$3) {
|
|
3245
3346
|
replaceWorkingPoints(preDragPoints);
|
|
3246
3347
|
workingAmplifierPlacements = preDragAmplifierPlacements;
|
|
3247
3348
|
renderPreview();
|
|
@@ -3414,7 +3515,7 @@ const HANDLE_HIT_TOLERANCE_PX = 12;
|
|
|
3414
3515
|
* click, not a gesture — same rationale as `edit-controller.ts`'s constant of
|
|
3415
3516
|
* the same name.
|
|
3416
3517
|
*/
|
|
3417
|
-
const CLICK_SLOP_PX = 3;
|
|
3518
|
+
const CLICK_SLOP_PX$2 = 3;
|
|
3418
3519
|
/**
|
|
3419
3520
|
* Drive an interactive group transform against a `MapAdapter` — the leaner,
|
|
3420
3521
|
* multi-select sibling of `runEdit` (ADR-0017 "Multi-select" slice). No
|
|
@@ -3430,7 +3531,7 @@ const CLICK_SLOP_PX = 3;
|
|
|
3430
3531
|
* thin outline (its own fit at the same angle)
|
|
3431
3532
|
* - dirty tracking, close/abort/signal/preempt/destroy, click-away — same
|
|
3432
3533
|
* contracts as `runEdit`, scoped to the group
|
|
3433
|
-
* - membership is mutable via `session.
|
|
3534
|
+
* - membership is mutable via `session.setGraphics` (#187): members join/leave
|
|
3434
3535
|
* a live session without a session boundary
|
|
3435
3536
|
*/
|
|
3436
3537
|
function runGroupTransform(opts) {
|
|
@@ -3454,7 +3555,7 @@ function runGroupTransform(opts) {
|
|
|
3454
3555
|
let members = measures.map(createMemberState);
|
|
3455
3556
|
/**
|
|
3456
3557
|
* `true` once any member has been lifted off the graphics layer (initial or a
|
|
3457
|
-
* mid-session `
|
|
3558
|
+
* mid-session `setGraphics` add). Gates the abort-path restore so a session
|
|
3458
3559
|
* that never touched the graphics layer doesn't rewrite it on abort.
|
|
3459
3560
|
*/
|
|
3460
3561
|
let anyLiftedFromGraphics = false;
|
|
@@ -3481,7 +3582,7 @@ function runGroupTransform(opts) {
|
|
|
3481
3582
|
};
|
|
3482
3583
|
const committed = sizeAnchor === "screen" ? working : bakePixelSizeToGround(working, adapter);
|
|
3483
3584
|
return {
|
|
3484
|
-
|
|
3585
|
+
graphic: cloneControlMeasure(committed),
|
|
3485
3586
|
render: renderMeasureForAdapter(adapter, committed, renderOpts)
|
|
3486
3587
|
};
|
|
3487
3588
|
}
|
|
@@ -3711,7 +3812,7 @@ function runGroupTransform(opts) {
|
|
|
3711
3812
|
let closeRequested = false;
|
|
3712
3813
|
let abortRequested = false;
|
|
3713
3814
|
const event = {
|
|
3714
|
-
|
|
3815
|
+
graphics: currentRendered,
|
|
3715
3816
|
previous: previousRendered,
|
|
3716
3817
|
session,
|
|
3717
3818
|
reject() {
|
|
@@ -3808,7 +3909,7 @@ function runGroupTransform(opts) {
|
|
|
3808
3909
|
/**
|
|
3809
3910
|
* Cancel an in-flight box drag without committing it: restore the pre-drag
|
|
3810
3911
|
* geometry and option overrides, re-enable drag pan, and clear the drag. Used
|
|
3811
|
-
* by `
|
|
3912
|
+
* by `setGraphics` so a membership change never bakes a half-finished drag; the
|
|
3812
3913
|
* caller is responsible for the follow-up repaint (`renderAllPreviews` /
|
|
3813
3914
|
* `renderHandlesLayer` / `recomputeDirty`), so this leaves the paint to it
|
|
3814
3915
|
* rather than rendering the outgoing member set only to redo it immediately.
|
|
@@ -3823,11 +3924,11 @@ function runGroupTransform(opts) {
|
|
|
3823
3924
|
applyOptionsOverrides(preDragOptionsOverrideByMember);
|
|
3824
3925
|
dragInFlight = false;
|
|
3825
3926
|
}
|
|
3826
|
-
function
|
|
3927
|
+
function setGraphics(next) {
|
|
3827
3928
|
if (ctx.settled) return void 0;
|
|
3828
3929
|
const seen = /* @__PURE__ */ new Set();
|
|
3829
3930
|
for (const m of next) {
|
|
3830
|
-
if (seen.has(m.id)) throw new TypeError(`TransformSession.
|
|
3931
|
+
if (seen.has(m.id)) throw new TypeError(`TransformSession.setGraphics: duplicate control measure id "${m.id}"`);
|
|
3831
3932
|
seen.add(m.id);
|
|
3832
3933
|
}
|
|
3833
3934
|
if (next.length === 0) return close();
|
|
@@ -3857,11 +3958,11 @@ function runGroupTransform(opts) {
|
|
|
3857
3958
|
return commitClose();
|
|
3858
3959
|
}
|
|
3859
3960
|
const session = {
|
|
3860
|
-
get
|
|
3961
|
+
get graphics() {
|
|
3861
3962
|
return members.map((m) => m.measure);
|
|
3862
3963
|
},
|
|
3863
|
-
controlPointsFor(
|
|
3864
|
-
const member = members.find((m) => m.measure.id ===
|
|
3964
|
+
controlPointsFor(graphicId) {
|
|
3965
|
+
const member = members.find((m) => m.measure.id === graphicId);
|
|
3865
3966
|
if (!member) return void 0;
|
|
3866
3967
|
return member.workingPoints.map((p) => [...p]);
|
|
3867
3968
|
},
|
|
@@ -3874,7 +3975,7 @@ function runGroupTransform(opts) {
|
|
|
3874
3975
|
setSizeAnchor(next) {
|
|
3875
3976
|
setSizeAnchor(next);
|
|
3876
3977
|
},
|
|
3877
|
-
|
|
3978
|
+
setGraphics,
|
|
3878
3979
|
close,
|
|
3879
3980
|
abort() {
|
|
3880
3981
|
if (ctx.settled) return;
|
|
@@ -3958,7 +4059,7 @@ function runGroupTransform(opts) {
|
|
|
3958
4059
|
activeBoxDrag = null;
|
|
3959
4060
|
driver.setDragPanEnabled(true);
|
|
3960
4061
|
const distance = Math.hypot(pixel[0] - downPixel[0], pixel[1] - downPixel[1]);
|
|
3961
|
-
if (fsm.kind === "body" && distance <= CLICK_SLOP_PX) {
|
|
4062
|
+
if (fsm.kind === "body" && distance <= CLICK_SLOP_PX$2) {
|
|
3962
4063
|
applyConcatenatedPoints(preDragPoints);
|
|
3963
4064
|
applyAmplifierPlacementsByMember(preDragAmplifierPlacementsByMember);
|
|
3964
4065
|
renderAllPreviews();
|
|
@@ -4060,7 +4161,7 @@ function runGroupTransform(opts) {
|
|
|
4060
4161
|
get settled() {
|
|
4061
4162
|
return core.settled;
|
|
4062
4163
|
},
|
|
4063
|
-
get
|
|
4164
|
+
get graphicIds() {
|
|
4064
4165
|
return members.map((m) => m.measure.id);
|
|
4065
4166
|
},
|
|
4066
4167
|
abort(reason, cause) {
|
|
@@ -4069,6 +4170,342 @@ function runGroupTransform(opts) {
|
|
|
4069
4170
|
};
|
|
4070
4171
|
}
|
|
4071
4172
|
//#endregion
|
|
4173
|
+
//#region src/tactical-draw/point-symbol-rendering.ts
|
|
4174
|
+
function effectivePointSymbolPixelSize(symbol, resolution) {
|
|
4175
|
+
if (symbol.size.unit === "pixels") return symbol.size.value;
|
|
4176
|
+
return resolution !== void 0 && resolution > 0 ? symbol.size.value / resolution : symbol.size.value;
|
|
4177
|
+
}
|
|
4178
|
+
function pointSymbolPixelScale(symbol, render, resolution) {
|
|
4179
|
+
if (!Number.isFinite(render.octagonSize) || render.octagonSize <= 0) throw new TypeError("PointSymbolRender.octagonSize must be a positive finite number");
|
|
4180
|
+
return effectivePointSymbolPixelSize(symbol, resolution) / render.octagonSize;
|
|
4181
|
+
}
|
|
4182
|
+
/** Complete rotated logical footprint for selection and transform chrome. */
|
|
4183
|
+
function pointSymbolFootprint(symbol, render, adapter) {
|
|
4184
|
+
const anchor = adapter.getPixelFromCoordinate(symbol.position);
|
|
4185
|
+
if (!anchor) return null;
|
|
4186
|
+
const scale = pointSymbolPixelScale(symbol, render, adapter.getResolution());
|
|
4187
|
+
const angle = symbol.rotation - adapter.getBearing();
|
|
4188
|
+
const centerOffsetX = (render.width / 2 - render.anchor.x) * scale;
|
|
4189
|
+
const centerOffsetY = (render.height / 2 - render.anchor.y) * scale;
|
|
4190
|
+
const cos = Math.cos(angle);
|
|
4191
|
+
const sin = Math.sin(angle);
|
|
4192
|
+
const box = {
|
|
4193
|
+
center: [anchor[0] + centerOffsetX * cos - centerOffsetY * sin, anchor[1] + centerOffsetX * sin + centerOffsetY * cos],
|
|
4194
|
+
halfWidth: render.width * scale / 2,
|
|
4195
|
+
halfHeight: render.height * scale / 2,
|
|
4196
|
+
angle
|
|
4197
|
+
};
|
|
4198
|
+
return {
|
|
4199
|
+
box,
|
|
4200
|
+
corners: boxCorners(box)
|
|
4201
|
+
};
|
|
4202
|
+
}
|
|
4203
|
+
function renderPointSymbolForAdapter(adapter, capability, symbol) {
|
|
4204
|
+
const render = capability.render(symbol);
|
|
4205
|
+
return {
|
|
4206
|
+
render,
|
|
4207
|
+
resource: {
|
|
4208
|
+
id: render.resourceId,
|
|
4209
|
+
svg: render.svg,
|
|
4210
|
+
width: render.width,
|
|
4211
|
+
height: render.height,
|
|
4212
|
+
anchor: [render.anchor.x, render.anchor.y]
|
|
4213
|
+
},
|
|
4214
|
+
feature: pointSymbolFeature(symbol, render, adapter.getResolution())
|
|
4215
|
+
};
|
|
4216
|
+
}
|
|
4217
|
+
function pointSymbolFeature(symbol, render, resolution) {
|
|
4218
|
+
return {
|
|
4219
|
+
type: "Feature",
|
|
4220
|
+
id: `${symbol.id}:point-symbol`,
|
|
4221
|
+
geometry: {
|
|
4222
|
+
type: "Point",
|
|
4223
|
+
coordinates: [...symbol.position]
|
|
4224
|
+
},
|
|
4225
|
+
properties: {
|
|
4226
|
+
...symbol.properties ?? {},
|
|
4227
|
+
graphicId: symbol.id,
|
|
4228
|
+
graphicKind: "point-symbol",
|
|
4229
|
+
iconId: render.resourceId,
|
|
4230
|
+
iconScale: pointSymbolPixelScale(symbol, render, resolution),
|
|
4231
|
+
iconRotation: symbol.rotation,
|
|
4232
|
+
iconOpacity: symbol.style?.opacity ?? 1
|
|
4233
|
+
}
|
|
4234
|
+
};
|
|
4235
|
+
}
|
|
4236
|
+
//#endregion
|
|
4237
|
+
//#region src/tactical-draw/rotation.ts
|
|
4238
|
+
/** Normalize radians to the canonical clockwise [0, 2π) range. */
|
|
4239
|
+
function normalizeRotation(angle) {
|
|
4240
|
+
const turn = Math.PI * 2;
|
|
4241
|
+
return (angle % turn + turn) % turn;
|
|
4242
|
+
}
|
|
4243
|
+
//#endregion
|
|
4244
|
+
//#region src/tactical-draw/graphic-transform-controller.ts
|
|
4245
|
+
const HIT_TOLERANCE_PX$1 = 12;
|
|
4246
|
+
const CLICK_SLOP_PX$1 = 3;
|
|
4247
|
+
function cloneGraphic(graphic) {
|
|
4248
|
+
return isPointSymbol(graphic) ? clonePointSymbol(graphic) : cloneControlMeasure(graphic);
|
|
4249
|
+
}
|
|
4250
|
+
function runGraphicTransform(opts) {
|
|
4251
|
+
const { adapter, capability, graphics, graphicsLayer, previewLayer, handlesLayer, graphicsStyle, signal, closeOnClickAway = true, interactionStyle, onSession, handleIdPrefix, restoreOriginalToGraphics, isOnGraphicsLayer, onSettled, onClickAwayGraphicsPick } = opts;
|
|
4252
|
+
const original = graphics.map(cloneGraphic);
|
|
4253
|
+
let working = original.map(cloneGraphic);
|
|
4254
|
+
let dirty = false;
|
|
4255
|
+
let boxAngle = 0;
|
|
4256
|
+
let box = null;
|
|
4257
|
+
let ctx;
|
|
4258
|
+
let driver;
|
|
4259
|
+
let drag = null;
|
|
4260
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
4261
|
+
const commitEmitter = createCommitEmitter("GraphicTransformSession.onCommit listener errors");
|
|
4262
|
+
const settleEmitter = createSettleEmitter("GraphicTransformSession.onSettled listener errors");
|
|
4263
|
+
const renderOpts = graphicsStyle ? { graphicsStyle } : void 0;
|
|
4264
|
+
const snapshots = () => working.map((graphic) => isPointSymbol(graphic) ? {
|
|
4265
|
+
graphic: clonePointSymbol(graphic),
|
|
4266
|
+
render: capability.render(graphic)
|
|
4267
|
+
} : {
|
|
4268
|
+
graphic: cloneControlMeasure(graphic),
|
|
4269
|
+
render: renderMeasureForAdapter(adapter, graphic, renderOpts)
|
|
4270
|
+
});
|
|
4271
|
+
function renderAll() {
|
|
4272
|
+
if (ctx.settled) return;
|
|
4273
|
+
const features = [];
|
|
4274
|
+
const resources = /* @__PURE__ */ new Map();
|
|
4275
|
+
const pixels = [];
|
|
4276
|
+
for (const graphic of working) if (isPointSymbol(graphic)) {
|
|
4277
|
+
const plan = renderPointSymbolForAdapter(adapter, capability, graphic);
|
|
4278
|
+
resources.set(plan.resource.id, plan.resource);
|
|
4279
|
+
features.push(plan.feature);
|
|
4280
|
+
const footprint = pointSymbolFootprint(graphic, plan.render, adapter);
|
|
4281
|
+
if (footprint) pixels.push(...footprint.corners);
|
|
4282
|
+
} else {
|
|
4283
|
+
const rendered = renderMeasureForAdapter(adapter, graphic, renderOpts);
|
|
4284
|
+
features.push(...rendered.features);
|
|
4285
|
+
for (const feature of rendered.features) {
|
|
4286
|
+
const positions = [];
|
|
4287
|
+
if (feature.geometry) collectFeaturePositions(feature.geometry, positions);
|
|
4288
|
+
for (const position of positions) {
|
|
4289
|
+
const pixel = driver.toPixel(position);
|
|
4290
|
+
if (pixel) pixels.push(pixel);
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
adapter.reconcileLayerIconResources(previewLayer, [...resources.values()]);
|
|
4295
|
+
ctx.writeLayer(previewLayer, features);
|
|
4296
|
+
if (!drag) box = fitTransformBox(pixels, boxAngle, interactionStyle?.boxPadding);
|
|
4297
|
+
const liveBox = drag?.liveBox ?? box;
|
|
4298
|
+
const handles = [];
|
|
4299
|
+
if (liveBox) {
|
|
4300
|
+
handles.push(boxOutlineFeature(liveBox, (pixel) => driver.fromPixel(pixel), `${handleIdPrefix}:box-outline`, interactionStyle?.boxOutline));
|
|
4301
|
+
handles.push(...toFeatures(boxToHandles(liveBox, (pixel) => driver.fromPixel(pixel)), handleIdPrefix, interactionStyle));
|
|
4302
|
+
}
|
|
4303
|
+
ctx.writeLayer(handlesLayer, handles);
|
|
4304
|
+
}
|
|
4305
|
+
function anchorPoints(source) {
|
|
4306
|
+
const points = [];
|
|
4307
|
+
for (const graphic of source) if (isPointSymbol(graphic)) points.push([...graphic.position]);
|
|
4308
|
+
else points.push(...graphic.controlPoints.map((point) => [...point]));
|
|
4309
|
+
return points;
|
|
4310
|
+
}
|
|
4311
|
+
function applyPoints(source, points, scale, rotation) {
|
|
4312
|
+
let offset = 0;
|
|
4313
|
+
return source.map((graphic) => {
|
|
4314
|
+
if (isPointSymbol(graphic)) {
|
|
4315
|
+
const next = clonePointSymbol(graphic);
|
|
4316
|
+
next.position = [...points[offset++]];
|
|
4317
|
+
next.size.value = Math.max(.1, next.size.value * scale);
|
|
4318
|
+
next.rotation = normalizeRotation(next.rotation + rotation);
|
|
4319
|
+
return next;
|
|
4320
|
+
}
|
|
4321
|
+
const next = cloneControlMeasure(graphic);
|
|
4322
|
+
const count = next.controlPoints.length;
|
|
4323
|
+
next.controlPoints = points.slice(offset, offset + count).map((point) => [...point]);
|
|
4324
|
+
offset += count;
|
|
4325
|
+
return next;
|
|
4326
|
+
});
|
|
4327
|
+
}
|
|
4328
|
+
function emit(previous, previousGraphics) {
|
|
4329
|
+
const current = snapshots();
|
|
4330
|
+
let reject = false;
|
|
4331
|
+
let close = false;
|
|
4332
|
+
let abort = false;
|
|
4333
|
+
const event = {
|
|
4334
|
+
graphics: current,
|
|
4335
|
+
previous,
|
|
4336
|
+
session,
|
|
4337
|
+
reject: () => reject = true,
|
|
4338
|
+
close: () => close = true,
|
|
4339
|
+
abort: () => abort = true
|
|
4340
|
+
};
|
|
4341
|
+
const errors = [];
|
|
4342
|
+
for (const listener of [...listeners]) try {
|
|
4343
|
+
listener(event);
|
|
4344
|
+
} catch (error) {
|
|
4345
|
+
errors.push(error);
|
|
4346
|
+
}
|
|
4347
|
+
if (abort) ctx.fail(new TacticalDrawAbortError("session"));
|
|
4348
|
+
else if (close) ctx.commit(snapshots);
|
|
4349
|
+
else if (reject && !ctx.settled) {
|
|
4350
|
+
working = previousGraphics.map(cloneGraphic);
|
|
4351
|
+
dirty = JSON.stringify(working) !== JSON.stringify(original);
|
|
4352
|
+
renderAll();
|
|
4353
|
+
}
|
|
4354
|
+
if (errors.length === 1) throw errors[0];
|
|
4355
|
+
if (errors.length > 1) throw new AggregateError(errors, "GraphicTransformSession.onChange errors");
|
|
4356
|
+
}
|
|
4357
|
+
const session = {
|
|
4358
|
+
get graphics() {
|
|
4359
|
+
return original.map(cloneGraphic);
|
|
4360
|
+
},
|
|
4361
|
+
get workingGraphics() {
|
|
4362
|
+
return working.map(cloneGraphic);
|
|
4363
|
+
},
|
|
4364
|
+
get dirty() {
|
|
4365
|
+
return dirty;
|
|
4366
|
+
},
|
|
4367
|
+
close: () => ctx.commit(snapshots),
|
|
4368
|
+
abort: () => ctx.fail(new TacticalDrawAbortError("session")),
|
|
4369
|
+
onChange(listener) {
|
|
4370
|
+
if (ctx.settled) return () => {};
|
|
4371
|
+
listeners.add(listener);
|
|
4372
|
+
return () => listeners.delete(listener);
|
|
4373
|
+
},
|
|
4374
|
+
onCommit(listener) {
|
|
4375
|
+
if (ctx.settled) return () => {};
|
|
4376
|
+
return commitEmitter.register(listener);
|
|
4377
|
+
},
|
|
4378
|
+
onSettled: (listener) => settleEmitter.register(listener)
|
|
4379
|
+
};
|
|
4380
|
+
const onDown = (pixel, info) => {
|
|
4381
|
+
if (!box || ctx.settled) return;
|
|
4382
|
+
const hit = pickBoxHandle(box, pixel, info?.hitTolerance ?? HIT_TOLERANCE_PX$1);
|
|
4383
|
+
if (!hit) return;
|
|
4384
|
+
const start = working.map(cloneGraphic);
|
|
4385
|
+
drag = {
|
|
4386
|
+
fsm: beginBoxDrag({
|
|
4387
|
+
kind: hit.kind,
|
|
4388
|
+
downPixel: pixel,
|
|
4389
|
+
points: anchorPoints(start),
|
|
4390
|
+
projection: {
|
|
4391
|
+
toPixel: (p) => driver.toPixel(p),
|
|
4392
|
+
fromPixel: (p) => driver.fromPixel(p)
|
|
4393
|
+
},
|
|
4394
|
+
box,
|
|
4395
|
+
cornerIndex: hit.kind === "scale" ? hit.index : void 0
|
|
4396
|
+
}),
|
|
4397
|
+
start,
|
|
4398
|
+
startBox: box,
|
|
4399
|
+
liveBox: box,
|
|
4400
|
+
down: [...pixel]
|
|
4401
|
+
};
|
|
4402
|
+
driver.setDragPanEnabled(false);
|
|
4403
|
+
};
|
|
4404
|
+
const dragResult = (pixel) => {
|
|
4405
|
+
if (!drag) return null;
|
|
4406
|
+
const result = drag.fsm.move(pixel);
|
|
4407
|
+
const scale = drag.fsm.kind === "scale" ? Math.max(result.box.halfWidth / drag.startBox.halfWidth, result.box.halfHeight / drag.startBox.halfHeight) : 1;
|
|
4408
|
+
const rotation = drag.fsm.kind === "rotate" ? result.box.angle - drag.startBox.angle : 0;
|
|
4409
|
+
return {
|
|
4410
|
+
result,
|
|
4411
|
+
graphics: applyPoints(drag.start, result.points, scale, rotation)
|
|
4412
|
+
};
|
|
4413
|
+
};
|
|
4414
|
+
const onMove = (pixel) => {
|
|
4415
|
+
const active = drag;
|
|
4416
|
+
const next = dragResult(pixel);
|
|
4417
|
+
if (!active || !next) return;
|
|
4418
|
+
working = next.graphics;
|
|
4419
|
+
active.liveBox = next.result.box;
|
|
4420
|
+
renderAll();
|
|
4421
|
+
};
|
|
4422
|
+
const onUp = (pixel) => {
|
|
4423
|
+
if (!drag) return;
|
|
4424
|
+
const active = drag;
|
|
4425
|
+
const next = dragResult(pixel);
|
|
4426
|
+
drag = null;
|
|
4427
|
+
suppressClickAway = true;
|
|
4428
|
+
driver.setDragPanEnabled(true);
|
|
4429
|
+
working = active.start.map(cloneGraphic);
|
|
4430
|
+
if (!next) return;
|
|
4431
|
+
if (active.fsm.kind === "body" && Math.hypot(pixel[0] - active.down[0], pixel[1] - active.down[1]) <= CLICK_SLOP_PX$1) {
|
|
4432
|
+
renderAll();
|
|
4433
|
+
return;
|
|
4434
|
+
}
|
|
4435
|
+
const previous = snapshots();
|
|
4436
|
+
const previousGraphics = working.map(cloneGraphic);
|
|
4437
|
+
working = next.graphics;
|
|
4438
|
+
if (active.fsm.kind === "rotate") boxAngle += next.result.box.angle - active.startBox.angle;
|
|
4439
|
+
dirty = JSON.stringify(working) !== JSON.stringify(original);
|
|
4440
|
+
renderAll();
|
|
4441
|
+
emit(previous, previousGraphics);
|
|
4442
|
+
};
|
|
4443
|
+
let previewPick;
|
|
4444
|
+
let handlesPick;
|
|
4445
|
+
let graphicsPick;
|
|
4446
|
+
let suppressClickAway = false;
|
|
4447
|
+
const onClick = () => {
|
|
4448
|
+
const keepOpen = previewPick || handlesPick;
|
|
4449
|
+
const deferred = graphicsPick;
|
|
4450
|
+
previewPick = handlesPick = graphicsPick = void 0;
|
|
4451
|
+
if (suppressClickAway) {
|
|
4452
|
+
suppressClickAway = false;
|
|
4453
|
+
return;
|
|
4454
|
+
}
|
|
4455
|
+
if (!closeOnClickAway || keepOpen || drag) return;
|
|
4456
|
+
if (ctx.commit(snapshots) && deferred) onClickAwayGraphicsPick?.(deferred);
|
|
4457
|
+
};
|
|
4458
|
+
const core = runInteraction({
|
|
4459
|
+
adapter,
|
|
4460
|
+
signal,
|
|
4461
|
+
layers: [previewLayer, handlesLayer],
|
|
4462
|
+
onViewChange: renderAll,
|
|
4463
|
+
onSettled(settlement) {
|
|
4464
|
+
onSettled?.();
|
|
4465
|
+
settleFromInteraction(settlement, commitEmitter, settleEmitter);
|
|
4466
|
+
},
|
|
4467
|
+
attach(interaction) {
|
|
4468
|
+
ctx = interaction;
|
|
4469
|
+
driver = adapter.createEditPointerDriver();
|
|
4470
|
+
const unsubs = [
|
|
4471
|
+
driver.onPointerDown(onDown),
|
|
4472
|
+
driver.onPointerMove(onMove),
|
|
4473
|
+
driver.onPointerUp(onUp),
|
|
4474
|
+
adapter.onPick(previewLayer, (event) => previewPick = event),
|
|
4475
|
+
adapter.onPick(handlesLayer, (event) => handlesPick = event),
|
|
4476
|
+
adapter.onPick(graphicsLayer, (event) => graphicsPick = event)
|
|
4477
|
+
];
|
|
4478
|
+
interaction.onTeardown(() => {
|
|
4479
|
+
unsubs.forEach((unsubscribe) => unsubscribe());
|
|
4480
|
+
adapter.off("click", onClick);
|
|
4481
|
+
driver.dispose();
|
|
4482
|
+
adapter.reconcileLayerIconResources(previewLayer, []);
|
|
4483
|
+
}, restoreOriginalToGraphics);
|
|
4484
|
+
for (const graphic of working) {
|
|
4485
|
+
if (!isOnGraphicsLayer(graphic.id)) continue;
|
|
4486
|
+
if (isPointSymbol(graphic)) adapter.removeFeatures(graphicsLayer, [`${graphic.id}:point-symbol`]);
|
|
4487
|
+
else {
|
|
4488
|
+
const ids = renderMeasureForAdapter(adapter, graphic, renderOpts).features.map((feature) => feature.id).filter((id) => typeof id === "string");
|
|
4489
|
+
adapter.removeFeatures(graphicsLayer, ids);
|
|
4490
|
+
}
|
|
4491
|
+
}
|
|
4492
|
+
adapter.on("click", onClick);
|
|
4493
|
+
renderAll();
|
|
4494
|
+
onSession?.(session);
|
|
4495
|
+
}
|
|
4496
|
+
});
|
|
4497
|
+
return {
|
|
4498
|
+
promise: core.promise,
|
|
4499
|
+
get settled() {
|
|
4500
|
+
return core.settled;
|
|
4501
|
+
},
|
|
4502
|
+
get graphicIds() {
|
|
4503
|
+
return working.map((graphic) => graphic.id);
|
|
4504
|
+
},
|
|
4505
|
+
abort: core.abort
|
|
4506
|
+
};
|
|
4507
|
+
}
|
|
4508
|
+
//#endregion
|
|
4072
4509
|
//#region src/tactical-draw/errors.ts
|
|
4073
4510
|
/**
|
|
4074
4511
|
* Error thrown when any `TacticalDraw` method is called after `destroy()`.
|
|
@@ -4081,6 +4518,308 @@ var TacticalDrawDestroyedError = class TacticalDrawDestroyedError extends Error
|
|
|
4081
4518
|
Object.setPrototypeOf(this, TacticalDrawDestroyedError.prototype);
|
|
4082
4519
|
}
|
|
4083
4520
|
};
|
|
4521
|
+
var PointSymbolCapabilityRequiredError = class extends Error {
|
|
4522
|
+
constructor() {
|
|
4523
|
+
super("TacticalDraw: point-symbol capability is not configured");
|
|
4524
|
+
this.name = "PointSymbolCapabilityRequiredError";
|
|
4525
|
+
}
|
|
4526
|
+
};
|
|
4527
|
+
//#endregion
|
|
4528
|
+
//#region src/tactical-draw/point-symbol-draw-controller.ts
|
|
4529
|
+
const PREVIEW_ID = "__td_point_symbol_preview__";
|
|
4530
|
+
function runPointSymbolDraw(options) {
|
|
4531
|
+
const { adapter, capability, draft, previewLayer, generateId, signal, onSettled } = options;
|
|
4532
|
+
let latestPosition = draft.position;
|
|
4533
|
+
let ctx;
|
|
4534
|
+
const savedCursor = "";
|
|
4535
|
+
const build = (id, position) => ({
|
|
4536
|
+
...structuredClone(draft),
|
|
4537
|
+
id,
|
|
4538
|
+
kind: "point-symbol",
|
|
4539
|
+
sidc: draft.sidc,
|
|
4540
|
+
position: [...position],
|
|
4541
|
+
rotation: normalizeRotation(draft.rotation ?? capability.defaultRotation),
|
|
4542
|
+
size: structuredClone(draft.size ?? capability.defaultSize)
|
|
4543
|
+
});
|
|
4544
|
+
const renderPreview = () => {
|
|
4545
|
+
if (!latestPosition) return;
|
|
4546
|
+
const plan = renderPointSymbolForAdapter(adapter, capability, build(PREVIEW_ID, latestPosition));
|
|
4547
|
+
adapter.reconcileLayerIconResources(previewLayer, [plan.resource]);
|
|
4548
|
+
ctx.writeLayer(previewLayer, [plan.feature]);
|
|
4549
|
+
};
|
|
4550
|
+
const handlePointerMove = (event) => {
|
|
4551
|
+
latestPosition = event.coordinate;
|
|
4552
|
+
renderPreview();
|
|
4553
|
+
};
|
|
4554
|
+
const handleClick = (event) => {
|
|
4555
|
+
latestPosition = event.coordinate;
|
|
4556
|
+
ctx.commit(() => {
|
|
4557
|
+
const graphic = build(draft.id ?? generateId(), event.coordinate);
|
|
4558
|
+
return {
|
|
4559
|
+
graphic: clonePointSymbol(graphic),
|
|
4560
|
+
render: capability.render(graphic)
|
|
4561
|
+
};
|
|
4562
|
+
});
|
|
4563
|
+
};
|
|
4564
|
+
return runInteraction({
|
|
4565
|
+
adapter,
|
|
4566
|
+
signal,
|
|
4567
|
+
layers: [previewLayer],
|
|
4568
|
+
onViewChange: renderPreview,
|
|
4569
|
+
onSettled,
|
|
4570
|
+
attach(interaction) {
|
|
4571
|
+
ctx = interaction;
|
|
4572
|
+
interaction.onTeardown(() => {
|
|
4573
|
+
adapter.off("pointermove", handlePointerMove);
|
|
4574
|
+
adapter.off("click", handleClick);
|
|
4575
|
+
adapter.reconcileLayerIconResources(previewLayer, []);
|
|
4576
|
+
adapter.setCursor(savedCursor);
|
|
4577
|
+
});
|
|
4578
|
+
adapter.setCursor("crosshair");
|
|
4579
|
+
adapter.on("pointermove", handlePointerMove);
|
|
4580
|
+
adapter.on("click", handleClick);
|
|
4581
|
+
renderPreview();
|
|
4582
|
+
}
|
|
4583
|
+
});
|
|
4584
|
+
}
|
|
4585
|
+
//#endregion
|
|
4586
|
+
//#region src/tactical-draw/point-symbol-edit-controller.ts
|
|
4587
|
+
const HIT_TOLERANCE_PX = 12;
|
|
4588
|
+
const CLICK_SLOP_PX = 3;
|
|
4589
|
+
function normalisePointModes(input) {
|
|
4590
|
+
const modes = input ?? ["transform"];
|
|
4591
|
+
for (const mode of modes) if (mode !== "transform" && mode !== "delete") throw new TypeError(`Point-symbol editing does not support mode "${mode}"`);
|
|
4592
|
+
return modes.includes("delete") ? ["delete"] : [...new Set(modes)];
|
|
4593
|
+
}
|
|
4594
|
+
function symbolBox(adapter, capability, symbol, padding = 0) {
|
|
4595
|
+
const footprint = pointSymbolFootprint(symbol, capability.render(symbol), adapter);
|
|
4596
|
+
if (!footprint) return null;
|
|
4597
|
+
return {
|
|
4598
|
+
...footprint.box,
|
|
4599
|
+
halfWidth: Math.max(1, footprint.box.halfWidth) + padding,
|
|
4600
|
+
halfHeight: Math.max(1, footprint.box.halfHeight) + padding
|
|
4601
|
+
};
|
|
4602
|
+
}
|
|
4603
|
+
function runPointSymbolEdit(opts) {
|
|
4604
|
+
const { adapter, capability, symbol, graphicsLayer, previewLayer, handlesLayer, signal, closeOnClickAway = true, interactionStyle, onSession, handleIdPrefix, restoreOriginalToGraphics, wasOnGraphicsLayer, onSettled, onClickAwayGraphicsPick } = opts;
|
|
4605
|
+
let modes = normalisePointModes(opts.modes);
|
|
4606
|
+
let working = clonePointSymbol(symbol);
|
|
4607
|
+
let dirty = false;
|
|
4608
|
+
let box = null;
|
|
4609
|
+
let drag = null;
|
|
4610
|
+
let ctx;
|
|
4611
|
+
let driver;
|
|
4612
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
4613
|
+
const commitEmitter = createCommitEmitter("GraphicEditSession.onCommit listener errors");
|
|
4614
|
+
const settleEmitter = createSettleEmitter("GraphicEditSession.onSettled listener errors");
|
|
4615
|
+
const snapshot = () => ({
|
|
4616
|
+
graphic: clonePointSymbol(working),
|
|
4617
|
+
render: capability.render(working)
|
|
4618
|
+
});
|
|
4619
|
+
const render = () => {
|
|
4620
|
+
if (ctx.settled) return;
|
|
4621
|
+
const plan = renderPointSymbolForAdapter(adapter, capability, working);
|
|
4622
|
+
adapter.reconcileLayerIconResources(previewLayer, [plan.resource]);
|
|
4623
|
+
ctx.writeLayer(previewLayer, [plan.feature]);
|
|
4624
|
+
box = symbolBox(adapter, capability, working, interactionStyle?.boxPadding);
|
|
4625
|
+
const handles = [];
|
|
4626
|
+
if (modes.includes("transform") && box) {
|
|
4627
|
+
handles.push(boxOutlineFeature(box, (pixel) => driver.fromPixel(pixel), `${handleIdPrefix}:box-outline`, interactionStyle?.boxOutline));
|
|
4628
|
+
handles.push(...toFeatures(boxToHandles(box, (pixel) => driver.fromPixel(pixel)), handleIdPrefix, interactionStyle));
|
|
4629
|
+
}
|
|
4630
|
+
ctx.writeLayer(handlesLayer, handles);
|
|
4631
|
+
};
|
|
4632
|
+
const apply = (next, previous = snapshot()) => {
|
|
4633
|
+
working = clonePointSymbol(next);
|
|
4634
|
+
dirty = JSON.stringify(working) !== JSON.stringify(symbol);
|
|
4635
|
+
render();
|
|
4636
|
+
const current = snapshot();
|
|
4637
|
+
let reject = false;
|
|
4638
|
+
let close = false;
|
|
4639
|
+
let abort = false;
|
|
4640
|
+
const event = {
|
|
4641
|
+
graphic: current,
|
|
4642
|
+
previous,
|
|
4643
|
+
session,
|
|
4644
|
+
reject() {
|
|
4645
|
+
reject = true;
|
|
4646
|
+
},
|
|
4647
|
+
close() {
|
|
4648
|
+
close = true;
|
|
4649
|
+
},
|
|
4650
|
+
abort() {
|
|
4651
|
+
abort = true;
|
|
4652
|
+
}
|
|
4653
|
+
};
|
|
4654
|
+
const errors = [];
|
|
4655
|
+
for (const listener of [...listeners]) try {
|
|
4656
|
+
listener(event);
|
|
4657
|
+
} catch (error) {
|
|
4658
|
+
errors.push(error);
|
|
4659
|
+
}
|
|
4660
|
+
if (abort) ctx.fail(new TacticalDrawAbortError("session"));
|
|
4661
|
+
else if (close) ctx.commit(snapshot);
|
|
4662
|
+
else if (reject && !ctx.settled) {
|
|
4663
|
+
working = clonePointSymbol(previous.graphic);
|
|
4664
|
+
dirty = JSON.stringify(working) !== JSON.stringify(symbol);
|
|
4665
|
+
render();
|
|
4666
|
+
}
|
|
4667
|
+
if (errors.length === 1) throw errors[0];
|
|
4668
|
+
if (errors.length > 1) throw new AggregateError(errors, "GraphicEditSession.onChange errors");
|
|
4669
|
+
};
|
|
4670
|
+
const session = {
|
|
4671
|
+
graphic: clonePointSymbol(symbol),
|
|
4672
|
+
get workingGraphic() {
|
|
4673
|
+
return clonePointSymbol(working);
|
|
4674
|
+
},
|
|
4675
|
+
get dirty() {
|
|
4676
|
+
return dirty;
|
|
4677
|
+
},
|
|
4678
|
+
get modes() {
|
|
4679
|
+
return modes;
|
|
4680
|
+
},
|
|
4681
|
+
close() {
|
|
4682
|
+
return ctx.commit(snapshot);
|
|
4683
|
+
},
|
|
4684
|
+
abort() {
|
|
4685
|
+
ctx.fail(new TacticalDrawAbortError("session"));
|
|
4686
|
+
},
|
|
4687
|
+
setModes(next) {
|
|
4688
|
+
if (ctx.settled) return;
|
|
4689
|
+
modes = normalisePointModes(next);
|
|
4690
|
+
render();
|
|
4691
|
+
},
|
|
4692
|
+
updateGraphic(updater) {
|
|
4693
|
+
if (ctx.settled) return;
|
|
4694
|
+
const previous = snapshot();
|
|
4695
|
+
const next = updater(clonePointSymbol(working));
|
|
4696
|
+
if (next.kind !== "point-symbol" || next.id !== symbol.id) throw new TypeError("updateGraphic must preserve point-symbol kind and id");
|
|
4697
|
+
capability.render(next);
|
|
4698
|
+
apply(next, previous);
|
|
4699
|
+
},
|
|
4700
|
+
onChange(listener) {
|
|
4701
|
+
if (ctx.settled) return () => {};
|
|
4702
|
+
listeners.add(listener);
|
|
4703
|
+
return () => listeners.delete(listener);
|
|
4704
|
+
},
|
|
4705
|
+
onCommit(listener) {
|
|
4706
|
+
if (ctx.settled) return () => {};
|
|
4707
|
+
return commitEmitter.register(listener);
|
|
4708
|
+
},
|
|
4709
|
+
onSettled(listener) {
|
|
4710
|
+
return settleEmitter.register(listener);
|
|
4711
|
+
}
|
|
4712
|
+
};
|
|
4713
|
+
const pointerAngle = (pixel, anchor) => Math.atan2(pixel[1] - anchor[1], pixel[0] - anchor[0]);
|
|
4714
|
+
const onDown = (pixel, info) => {
|
|
4715
|
+
if (ctx.settled || !box || !modes.includes("transform")) return;
|
|
4716
|
+
const hit = pickBoxHandle(box, pixel, info?.hitTolerance ?? HIT_TOLERANCE_PX);
|
|
4717
|
+
if (!hit) return;
|
|
4718
|
+
const anchor = driver.toPixel(working.position);
|
|
4719
|
+
if (!anchor) return;
|
|
4720
|
+
drag = {
|
|
4721
|
+
kind: hit.kind,
|
|
4722
|
+
down: [...pixel],
|
|
4723
|
+
start: clonePointSymbol(working),
|
|
4724
|
+
anchor,
|
|
4725
|
+
startPointerAngle: pointerAngle(pixel, anchor),
|
|
4726
|
+
startDistance: Math.max(1, Math.hypot(pixel[0] - anchor[0], pixel[1] - anchor[1]))
|
|
4727
|
+
};
|
|
4728
|
+
driver.setDragPanEnabled(false);
|
|
4729
|
+
};
|
|
4730
|
+
const transformed = (pixel) => {
|
|
4731
|
+
if (!drag) return null;
|
|
4732
|
+
const next = clonePointSymbol(drag.start);
|
|
4733
|
+
if (drag.kind === "body") next.position = driver.fromPixel([drag.anchor[0] + pixel[0] - drag.down[0], drag.anchor[1] + pixel[1] - drag.down[1]]);
|
|
4734
|
+
else if (drag.kind === "rotate") next.rotation = normalizeRotation(drag.start.rotation + pointerAngle(pixel, drag.anchor) - drag.startPointerAngle);
|
|
4735
|
+
else {
|
|
4736
|
+
const distance = Math.hypot(pixel[0] - drag.anchor[0], pixel[1] - drag.anchor[1]);
|
|
4737
|
+
next.size.value = Math.max(.1, drag.start.size.value * (distance / drag.startDistance));
|
|
4738
|
+
}
|
|
4739
|
+
return next;
|
|
4740
|
+
};
|
|
4741
|
+
const onMove = (pixel) => {
|
|
4742
|
+
const next = transformed(pixel);
|
|
4743
|
+
if (!next) return;
|
|
4744
|
+
working = next;
|
|
4745
|
+
render();
|
|
4746
|
+
};
|
|
4747
|
+
const onUp = (pixel) => {
|
|
4748
|
+
if (!drag) return;
|
|
4749
|
+
const currentDrag = drag;
|
|
4750
|
+
const next = transformed(pixel);
|
|
4751
|
+
drag = null;
|
|
4752
|
+
suppressClickAway = true;
|
|
4753
|
+
driver.setDragPanEnabled(true);
|
|
4754
|
+
working = clonePointSymbol(currentDrag.start);
|
|
4755
|
+
if (!next) return;
|
|
4756
|
+
if (currentDrag.kind === "body" && Math.hypot(pixel[0] - currentDrag.down[0], pixel[1] - currentDrag.down[1]) <= CLICK_SLOP_PX) {
|
|
4757
|
+
render();
|
|
4758
|
+
return;
|
|
4759
|
+
}
|
|
4760
|
+
apply(next);
|
|
4761
|
+
};
|
|
4762
|
+
let previewPick;
|
|
4763
|
+
let handlesPick;
|
|
4764
|
+
let graphicsPick;
|
|
4765
|
+
let suppressClickAway = false;
|
|
4766
|
+
const onPreviewPick = (event) => previewPick = event;
|
|
4767
|
+
const onHandlesPick = (event) => handlesPick = event;
|
|
4768
|
+
const onGraphicsPick = (event) => graphicsPick = event;
|
|
4769
|
+
const onClick = () => {
|
|
4770
|
+
const keepOpen = previewPick || handlesPick;
|
|
4771
|
+
const deferred = graphicsPick;
|
|
4772
|
+
previewPick = handlesPick = graphicsPick = void 0;
|
|
4773
|
+
if (suppressClickAway) {
|
|
4774
|
+
suppressClickAway = false;
|
|
4775
|
+
return;
|
|
4776
|
+
}
|
|
4777
|
+
if (!closeOnClickAway || keepOpen || drag) return;
|
|
4778
|
+
if (ctx.commit(snapshot) && deferred) onClickAwayGraphicsPick?.(deferred);
|
|
4779
|
+
};
|
|
4780
|
+
const handle = runInteraction({
|
|
4781
|
+
adapter,
|
|
4782
|
+
signal,
|
|
4783
|
+
layers: [previewLayer, handlesLayer],
|
|
4784
|
+
onViewChange: render,
|
|
4785
|
+
onSettled(settlement) {
|
|
4786
|
+
onSettled?.();
|
|
4787
|
+
settleFromInteraction(settlement, commitEmitter, settleEmitter);
|
|
4788
|
+
},
|
|
4789
|
+
attach(interaction) {
|
|
4790
|
+
ctx = interaction;
|
|
4791
|
+
driver = adapter.createEditPointerDriver();
|
|
4792
|
+
const unsubs = [
|
|
4793
|
+
driver.onPointerDown(onDown),
|
|
4794
|
+
driver.onPointerMove(onMove),
|
|
4795
|
+
driver.onPointerUp(onUp),
|
|
4796
|
+
adapter.onPick(previewLayer, onPreviewPick),
|
|
4797
|
+
adapter.onPick(handlesLayer, onHandlesPick),
|
|
4798
|
+
adapter.onPick(graphicsLayer, onGraphicsPick)
|
|
4799
|
+
];
|
|
4800
|
+
interaction.onTeardown(() => {
|
|
4801
|
+
unsubs.forEach((unsubscribe) => unsubscribe());
|
|
4802
|
+
adapter.off("click", onClick);
|
|
4803
|
+
driver.dispose();
|
|
4804
|
+
adapter.reconcileLayerIconResources(previewLayer, []);
|
|
4805
|
+
}, () => {
|
|
4806
|
+
if (wasOnGraphicsLayer) restoreOriginalToGraphics();
|
|
4807
|
+
});
|
|
4808
|
+
if (wasOnGraphicsLayer) adapter.removeFeatures(graphicsLayer, [`${symbol.id}:point-symbol`]);
|
|
4809
|
+
adapter.on("click", onClick);
|
|
4810
|
+
render();
|
|
4811
|
+
onSession?.(session);
|
|
4812
|
+
}
|
|
4813
|
+
});
|
|
4814
|
+
return {
|
|
4815
|
+
promise: handle.promise,
|
|
4816
|
+
get settled() {
|
|
4817
|
+
return handle.settled;
|
|
4818
|
+
},
|
|
4819
|
+
measureId: symbol.id,
|
|
4820
|
+
abort: handle.abort
|
|
4821
|
+
};
|
|
4822
|
+
}
|
|
4084
4823
|
//#endregion
|
|
4085
4824
|
//#region src/tactical-draw/tactical-draw.ts
|
|
4086
4825
|
/**
|
|
@@ -4100,6 +4839,7 @@ var TacticalDraw = class {
|
|
|
4100
4839
|
graphicsStyle;
|
|
4101
4840
|
defaultInteractionStyle;
|
|
4102
4841
|
generateId;
|
|
4842
|
+
pointSymbols;
|
|
4103
4843
|
destroyed = false;
|
|
4104
4844
|
hasRendered = false;
|
|
4105
4845
|
activeInteraction = new ActiveInteractionTracker();
|
|
@@ -4114,7 +4854,7 @@ var TacticalDraw = class {
|
|
|
4114
4854
|
* Tracked alongside `activeInteraction` whenever the interaction is a group
|
|
4115
4855
|
* transform (`editMany`). The `render()` / view-change paths need every
|
|
4116
4856
|
* member's id (to exclude all of them from reconcile) — mirrors `activeEdit`
|
|
4117
|
-
* but for `TransformHandle`'s `
|
|
4857
|
+
* but for `TransformHandle`'s `graphicIds` array.
|
|
4118
4858
|
*/
|
|
4119
4859
|
activeTransform = null;
|
|
4120
4860
|
/**
|
|
@@ -4125,7 +4865,7 @@ var TacticalDraw = class {
|
|
|
4125
4865
|
*/
|
|
4126
4866
|
activeTransformSessionRef = null;
|
|
4127
4867
|
/**
|
|
4128
|
-
* Host pick handlers registered via `td.onPick` / `td.
|
|
4868
|
+
* Host pick handlers registered via `td.onPick` / `td.onGraphicPick`. The
|
|
4129
4869
|
* façade subscribes once to `adapter.onPick` per layer and routes events
|
|
4130
4870
|
* through this map so that close-then-pick ordering during an active edit can
|
|
4131
4871
|
* be enforced (issue #52).
|
|
@@ -4155,6 +4895,7 @@ var TacticalDraw = class {
|
|
|
4155
4895
|
this.graphicsStyle = resolveStyleHints(BUILT_IN_GRAPHICS_STYLE, options?.graphicsStyle, void 0);
|
|
4156
4896
|
this.defaultInteractionStyle = options?.interactionStyle;
|
|
4157
4897
|
this.generateId = options?.generateId ?? (() => `td-${++this.idCounter}`);
|
|
4898
|
+
this.pointSymbols = options?.pointSymbols;
|
|
4158
4899
|
const owned = /* @__PURE__ */ new Set();
|
|
4159
4900
|
const provided = options?.layers ?? {};
|
|
4160
4901
|
this.graphicsLayer = resolveSlot(adapter, provided.graphics, owned);
|
|
@@ -4175,13 +4916,20 @@ var TacticalDraw = class {
|
|
|
4175
4916
|
* `render()` (excludes the edited measure, owned by the preview layer) versus
|
|
4176
4917
|
* `restoreOriginalToGraphics` (no exclusion, the edit is ending).
|
|
4177
4918
|
*/
|
|
4178
|
-
renderMeasuresToGraphics(
|
|
4919
|
+
renderMeasuresToGraphics(graphics) {
|
|
4179
4920
|
const features = [];
|
|
4921
|
+
const resources = /* @__PURE__ */ new Map();
|
|
4180
4922
|
const renderOpts = { graphicsStyle: this.graphicsStyle };
|
|
4181
|
-
for (const
|
|
4182
|
-
|
|
4183
|
-
|
|
4923
|
+
for (const graphic of graphics) if (isPointSymbol(graphic)) {
|
|
4924
|
+
if (!this.pointSymbols) throw new PointSymbolCapabilityRequiredError();
|
|
4925
|
+
const plan = renderPointSymbolForAdapter(this.adapter, this.pointSymbols, graphic);
|
|
4926
|
+
resources.set(plan.resource.id, plan.resource);
|
|
4927
|
+
features.push(plan.feature);
|
|
4928
|
+
} else {
|
|
4929
|
+
const render = renderMeasureForAdapter(this.adapter, graphic, renderOpts);
|
|
4930
|
+
for (const feature of render.features) features.push(feature);
|
|
4184
4931
|
}
|
|
4932
|
+
this.adapter.reconcileLayerIconResources(this.graphicsLayer, [...resources.values()]);
|
|
4185
4933
|
this.adapter.updateLayerFeatures(this.graphicsLayer, features);
|
|
4186
4934
|
}
|
|
4187
4935
|
/**
|
|
@@ -4197,7 +4945,7 @@ var TacticalDraw = class {
|
|
|
4197
4945
|
if (this.destroyed || !this.hasRendered) return;
|
|
4198
4946
|
const excluded = this.excludedInteractionIds();
|
|
4199
4947
|
const visible = excluded.size > 0 ? this.lastRenderedMeasures.filter((m) => !excluded.has(m.id)) : this.lastRenderedMeasures;
|
|
4200
|
-
if (!visible.some(
|
|
4948
|
+
if (!visible.some(graphicRendersFromViewState)) return;
|
|
4201
4949
|
this.renderMeasuresToGraphics(visible);
|
|
4202
4950
|
};
|
|
4203
4951
|
/**
|
|
@@ -4220,8 +4968,10 @@ var TacticalDraw = class {
|
|
|
4220
4968
|
* `render()` from inside `onSession` still sees live membership.
|
|
4221
4969
|
*/
|
|
4222
4970
|
transformMemberIds() {
|
|
4223
|
-
if (this.activeTransform) return this.activeTransform.
|
|
4224
|
-
|
|
4971
|
+
if (this.activeTransform) return this.activeTransform.graphicIds;
|
|
4972
|
+
const session = this.activeTransformSessionRef;
|
|
4973
|
+
if (!session) return void 0;
|
|
4974
|
+
return session.graphics.map((graphic) => graphic.id);
|
|
4225
4975
|
}
|
|
4226
4976
|
/**
|
|
4227
4977
|
* Reconcile the graphics layer against the authoritative list of measures.
|
|
@@ -4252,14 +5002,15 @@ var TacticalDraw = class {
|
|
|
4252
5002
|
this.assertAlive();
|
|
4253
5003
|
const seen = /* @__PURE__ */ new Set();
|
|
4254
5004
|
for (const m of measures) {
|
|
4255
|
-
if (seen.has(m.id)) throw new Error(`TacticalDraw.render: duplicate
|
|
5005
|
+
if (seen.has(m.id)) throw new Error(`TacticalDraw.render: duplicate graphic id "${m.id}"`);
|
|
4256
5006
|
seen.add(m.id);
|
|
4257
5007
|
}
|
|
4258
5008
|
const editedId = this.activeEdit?.measureId;
|
|
4259
5009
|
let externalEdited;
|
|
4260
5010
|
let editedRemoved = false;
|
|
4261
5011
|
if (editedId !== void 0) {
|
|
4262
|
-
|
|
5012
|
+
const candidate = measures.find((m) => m.id === editedId);
|
|
5013
|
+
externalEdited = candidate && !isPointSymbol(candidate) ? candidate : void 0;
|
|
4263
5014
|
editedRemoved = externalEdited === void 0;
|
|
4264
5015
|
}
|
|
4265
5016
|
const transformIds = this.transformMemberIds();
|
|
@@ -4271,7 +5022,7 @@ var TacticalDraw = class {
|
|
|
4271
5022
|
this.hasRendered = true;
|
|
4272
5023
|
}
|
|
4273
5024
|
this.lastRenderedMeasures = measures.slice();
|
|
4274
|
-
if (externalEdited && this.activeEdit) this.activeEdit.notifyExternalMeasure(externalEdited);
|
|
5025
|
+
if (externalEdited && this.activeEdit && "notifyExternalMeasure" in this.activeEdit) this.activeEdit.notifyExternalMeasure(externalEdited);
|
|
4275
5026
|
if (editedRemoved && this.activeEdit) {
|
|
4276
5027
|
const editHandle = this.activeEdit;
|
|
4277
5028
|
queueMicrotask(() => {
|
|
@@ -4285,7 +5036,7 @@ var TacticalDraw = class {
|
|
|
4285
5036
|
if (!handle || handle.settled) return;
|
|
4286
5037
|
if (this.activeTransformSessionRef !== sessionRef) return;
|
|
4287
5038
|
const rendered = new Set(this.lastRenderedMeasures.map((m) => m.id));
|
|
4288
|
-
if (handle.
|
|
5039
|
+
if (handle.graphicIds.some((id) => !rendered.has(id))) handle.abort("removed");
|
|
4289
5040
|
});
|
|
4290
5041
|
}
|
|
4291
5042
|
}
|
|
@@ -4379,22 +5130,25 @@ var TacticalDraw = class {
|
|
|
4379
5130
|
hostOnSession?.(session);
|
|
4380
5131
|
};
|
|
4381
5132
|
}
|
|
4382
|
-
/**
|
|
4383
|
-
* Start an interactive draw. Fixed-length kinds commit automatically on the
|
|
4384
|
-
* last required click. Variable-length kinds surface a `DrawSession` via
|
|
4385
|
-
* `opts.onSession` and commit on `session.commit()`, `dblclick`, or a second
|
|
4386
|
-
* click on the last added control point. All paths reject with
|
|
4387
|
-
* `TacticalDrawAbortError` on signal / Escape / destroy / `session.abort()`.
|
|
4388
|
-
*
|
|
4389
|
-
* For a variable-length draw, `DrawSession.onCommit` is the single delivery
|
|
4390
|
-
* channel for the committed snapshot and `DrawSession.onSettled` is the
|
|
4391
|
-
* lifecycle signal for when the session is over and why (#197) — together
|
|
4392
|
-
* they make the returned promise non-load-bearing for hosts that adopt them.
|
|
4393
|
-
* Fixed-length kinds have no session and settle only through the promise.
|
|
4394
|
-
*/
|
|
4395
5133
|
draw(draft, options) {
|
|
4396
5134
|
this.assertAlive();
|
|
4397
|
-
|
|
5135
|
+
if (draft.kind === "point-symbol") {
|
|
5136
|
+
if (!this.pointSymbols) throw new PointSymbolCapabilityRequiredError();
|
|
5137
|
+
const preempt = this.guardAndPreempt();
|
|
5138
|
+
if (preempt) return preempt;
|
|
5139
|
+
const pointDraft = draft;
|
|
5140
|
+
return this.runWithSlot((onSettled) => runPointSymbolDraw({
|
|
5141
|
+
adapter: this.adapter,
|
|
5142
|
+
capability: this.pointSymbols,
|
|
5143
|
+
draft: pointDraft,
|
|
5144
|
+
previewLayer: this.previewLayer,
|
|
5145
|
+
generateId: this.generateId,
|
|
5146
|
+
signal: options?.signal,
|
|
5147
|
+
onSettled
|
|
5148
|
+
}), null);
|
|
5149
|
+
}
|
|
5150
|
+
const measureDraft = draft;
|
|
5151
|
+
const kind = measureDraft.kind;
|
|
4398
5152
|
const preempt = this.guardAndPreempt();
|
|
4399
5153
|
if (preempt) return preempt;
|
|
4400
5154
|
const fixedLength = isFixedLength(getControlMeasureMetadata(kind));
|
|
@@ -4414,12 +5168,12 @@ var TacticalDraw = class {
|
|
|
4414
5168
|
previewLayer: this.previewLayer,
|
|
4415
5169
|
guideLayer: this.guideLayer,
|
|
4416
5170
|
graphicsStyle: this.graphicsStyle,
|
|
4417
|
-
generateId: () =>
|
|
5171
|
+
generateId: () => measureDraft.id ?? this.generateId(),
|
|
4418
5172
|
signal: options?.signal,
|
|
4419
|
-
measureOptions:
|
|
4420
|
-
measureStyle:
|
|
4421
|
-
measureProperties:
|
|
4422
|
-
measureTextAmplifiers:
|
|
5173
|
+
measureOptions: measureDraft.options,
|
|
5174
|
+
measureStyle: measureDraft.style,
|
|
5175
|
+
measureProperties: measureDraft.properties,
|
|
5176
|
+
measureTextAmplifiers: measureDraft.textAmplifiers,
|
|
4423
5177
|
guide: options?.guide,
|
|
4424
5178
|
sizeAnchor: options?.sizeAnchor,
|
|
4425
5179
|
onSettled
|
|
@@ -4429,36 +5183,53 @@ var TacticalDraw = class {
|
|
|
4429
5183
|
previewLayer: this.previewLayer,
|
|
4430
5184
|
guideLayer: this.guideLayer,
|
|
4431
5185
|
graphicsStyle: this.graphicsStyle,
|
|
4432
|
-
generateId: () =>
|
|
5186
|
+
generateId: () => measureDraft.id ?? this.generateId(),
|
|
4433
5187
|
signal: options?.signal,
|
|
4434
5188
|
onSession: this.trackSession(options?.onSession),
|
|
4435
|
-
seed:
|
|
4436
|
-
measureOptions:
|
|
4437
|
-
measureStyle:
|
|
4438
|
-
measureProperties:
|
|
4439
|
-
measureTextAmplifiers:
|
|
5189
|
+
seed: measureDraft.controlPoints,
|
|
5190
|
+
measureOptions: measureDraft.options,
|
|
5191
|
+
measureStyle: measureDraft.style,
|
|
5192
|
+
measureProperties: measureDraft.properties,
|
|
5193
|
+
measureTextAmplifiers: measureDraft.textAmplifiers,
|
|
4440
5194
|
guide: options?.guide,
|
|
4441
5195
|
sizeAnchor: options?.sizeAnchor,
|
|
4442
5196
|
onSettled
|
|
4443
5197
|
}), null, extraSettle);
|
|
4444
5198
|
}
|
|
4445
|
-
/**
|
|
4446
|
-
* Start an interactive edit on a single control measure. Returns a Promise
|
|
4447
|
-
* that resolves with a `ControlMeasureSnapshot` of the working state on
|
|
4448
|
-
* `session.close()` and rejects with `TacticalDrawAbortError` on
|
|
4449
|
-
* signal / Escape / destroy / preempt / `session.abort()`.
|
|
4450
|
-
*
|
|
4451
|
-
* Default click-away (`closeOnClickAway: true`) closes the edit on a click
|
|
4452
|
-
* outside the preview / handle features. Hosts that own close policy pass
|
|
4453
|
-
* `false`.
|
|
4454
|
-
*
|
|
4455
|
-
* `EditSession.onCommit` is the single delivery channel for the committed
|
|
4456
|
-
* snapshot and `EditSession.onSettled` is the lifecycle signal for when the
|
|
4457
|
-
* session is over and why (#197) — together they make the returned promise
|
|
4458
|
-
* non-load-bearing for hosts that adopt them.
|
|
4459
|
-
*/
|
|
4460
5199
|
edit(measure, options) {
|
|
4461
5200
|
this.assertAlive();
|
|
5201
|
+
if (isPointSymbol(measure)) {
|
|
5202
|
+
normalisePointEditModes(options?.modes);
|
|
5203
|
+
if (!this.pointSymbols) throw new PointSymbolCapabilityRequiredError();
|
|
5204
|
+
const preempt = this.guardAndPreempt();
|
|
5205
|
+
if (preempt) return preempt;
|
|
5206
|
+
const wasOnGraphicsLayer = this.lastRenderedMeasures.some((m) => m.id === measure.id);
|
|
5207
|
+
const restoreOriginalToGraphics = () => this.renderMeasuresToGraphics(this.lastRenderedMeasures);
|
|
5208
|
+
const handleIdPrefix = `__td_edit_${++this.editCounter}__`;
|
|
5209
|
+
return this.runWithSlot((onSettled) => runPointSymbolEdit({
|
|
5210
|
+
adapter: this.adapter,
|
|
5211
|
+
capability: this.pointSymbols,
|
|
5212
|
+
symbol: measure,
|
|
5213
|
+
graphicsLayer: this.graphicsLayer,
|
|
5214
|
+
previewLayer: this.previewLayer,
|
|
5215
|
+
handlesLayer: this.handlesLayer,
|
|
5216
|
+
signal: options?.signal,
|
|
5217
|
+
closeOnClickAway: options?.closeOnClickAway,
|
|
5218
|
+
modes: options?.modes,
|
|
5219
|
+
interactionStyle: this.resolveInteractionStyle(options?.interactionStyle),
|
|
5220
|
+
onSession: options?.onSession,
|
|
5221
|
+
handleIdPrefix,
|
|
5222
|
+
restoreOriginalToGraphics,
|
|
5223
|
+
wasOnGraphicsLayer,
|
|
5224
|
+
onSettled,
|
|
5225
|
+
onClickAwayGraphicsPick: (event) => this.dispatchHostPick(this.graphicsLayer, event)
|
|
5226
|
+
}), (handle) => {
|
|
5227
|
+
this.activeEdit = handle;
|
|
5228
|
+
return () => {
|
|
5229
|
+
if (this.activeEdit === handle) this.activeEdit = null;
|
|
5230
|
+
};
|
|
5231
|
+
});
|
|
5232
|
+
}
|
|
4462
5233
|
normaliseModes(options?.modes);
|
|
4463
5234
|
const preempt = this.guardAndPreempt();
|
|
4464
5235
|
if (preempt) return preempt;
|
|
@@ -4499,38 +5270,16 @@ var TacticalDraw = class {
|
|
|
4499
5270
|
};
|
|
4500
5271
|
}, revertGuideStyle);
|
|
4501
5272
|
}
|
|
4502
|
-
/**
|
|
4503
|
-
* Start an interactive group transform (multi-select) over `measures`
|
|
4504
|
-
* (ADR-0017 "Multi-select" slice). Every member is lifted onto the shared
|
|
4505
|
-
* preview layer; the box (corner scale, rotate grip, drag-anywhere body
|
|
4506
|
-
* translate) applies rigidly to all of them at once. Returns a Promise that
|
|
4507
|
-
* resolves with `ControlMeasureSnapshot[]` on `session.close()` and rejects
|
|
4508
|
-
* with `TacticalDrawAbortError` on signal / Escape / destroy / preempt /
|
|
4509
|
-
* `session.abort()`. Key the resolved snapshots by `snapshot.measure.id`:
|
|
4510
|
-
* the member set is mutable (`session.setMembers`, #187) so snapshot arrays
|
|
4511
|
-
* are id-keyed, not index-aligned with this input. The promise resolves with
|
|
4512
|
-
* the members in the set at close time; a member removed mid-session had its
|
|
4513
|
-
* exit snapshot returned from the `setMembers` call that removed it.
|
|
4514
|
-
*
|
|
4515
|
-
* `measures.length` must be >= 1; a single-element array behaves like an
|
|
4516
|
-
* `edit` with only `transform` mode. Throws `TypeError` synchronously (before
|
|
4517
|
-
* any preemption) for an empty array or duplicate ids.
|
|
4518
|
-
*
|
|
4519
|
-
* For a selection that changes over time (shift-click toggle, marquee
|
|
4520
|
-
* replace), prefer [[syncTransformMembers]] over hand-rolling the
|
|
4521
|
-
* live-session-vs-fresh-`editMany` branch, and subscribe to
|
|
4522
|
-
* `TransformSession.onCommit` as the single fold path for exit snapshots
|
|
4523
|
-
* and `TransformSession.onSettled` as the lifecycle signal for when the
|
|
4524
|
-
* session is over and why.
|
|
4525
|
-
*/
|
|
4526
5273
|
editMany(measures, options) {
|
|
4527
5274
|
this.assertAlive();
|
|
4528
5275
|
if (measures.length === 0) throw new TypeError("td.editMany: measures must not be empty");
|
|
4529
5276
|
const seenIds = /* @__PURE__ */ new Set();
|
|
4530
5277
|
for (const m of measures) {
|
|
4531
|
-
if (seenIds.has(m.id)) throw new TypeError(`td.editMany: duplicate
|
|
5278
|
+
if (seenIds.has(m.id)) throw new TypeError(`td.editMany: duplicate graphic id "${m.id}"`);
|
|
4532
5279
|
seenIds.add(m.id);
|
|
4533
5280
|
}
|
|
5281
|
+
const hasPointSymbols = measures.some(isPointSymbol);
|
|
5282
|
+
if (hasPointSymbols && !this.pointSymbols) throw new PointSymbolCapabilityRequiredError();
|
|
4534
5283
|
const preempt = this.guardAndPreempt();
|
|
4535
5284
|
if (preempt) return preempt;
|
|
4536
5285
|
const isOnGraphicsLayer = (id) => this.lastRenderedMeasures.some((m) => m.id === id);
|
|
@@ -4545,6 +5294,29 @@ var TacticalDraw = class {
|
|
|
4545
5294
|
const extraSettle = () => {
|
|
4546
5295
|
this.activeTransformSessionRef = null;
|
|
4547
5296
|
};
|
|
5297
|
+
if (hasPointSymbols) return this.runWithSlot((onSettled) => runGraphicTransform({
|
|
5298
|
+
adapter: this.adapter,
|
|
5299
|
+
capability: this.pointSymbols,
|
|
5300
|
+
graphics: measures,
|
|
5301
|
+
graphicsLayer: this.graphicsLayer,
|
|
5302
|
+
previewLayer: this.previewLayer,
|
|
5303
|
+
handlesLayer: this.handlesLayer,
|
|
5304
|
+
graphicsStyle: this.graphicsStyle,
|
|
5305
|
+
signal: options?.signal,
|
|
5306
|
+
closeOnClickAway: options?.closeOnClickAway,
|
|
5307
|
+
interactionStyle: mergedInteractionStyle,
|
|
5308
|
+
onSession: this.trackTransformSession(options?.onSession),
|
|
5309
|
+
handleIdPrefix,
|
|
5310
|
+
restoreOriginalToGraphics,
|
|
5311
|
+
isOnGraphicsLayer,
|
|
5312
|
+
onSettled,
|
|
5313
|
+
onClickAwayGraphicsPick
|
|
5314
|
+
}), (handle) => {
|
|
5315
|
+
this.activeTransform = handle;
|
|
5316
|
+
return () => {
|
|
5317
|
+
if (this.activeTransform === handle) this.activeTransform = null;
|
|
5318
|
+
};
|
|
5319
|
+
}, extraSettle);
|
|
4548
5320
|
return this.runWithSlot((onSettled) => runGroupTransform({
|
|
4549
5321
|
adapter: this.adapter,
|
|
4550
5322
|
measures,
|
|
@@ -4570,24 +5342,26 @@ var TacticalDraw = class {
|
|
|
4570
5342
|
}, extraSettle);
|
|
4571
5343
|
}
|
|
4572
5344
|
/**
|
|
4573
|
-
* One-call selection-sync: mutate
|
|
4574
|
-
* place, or start a fresh `editMany` when no
|
|
4575
|
-
*
|
|
4576
|
-
*
|
|
5345
|
+
* One-call selection-sync: mutate a live control-measure-only transform's
|
|
5346
|
+
* member set in place, or start a fresh `editMany` when no compatible
|
|
5347
|
+
* transform is live (#191). A selection containing a point symbol uses the
|
|
5348
|
+
* unified graphic transform session, which does not yet support mutable
|
|
5349
|
+
* membership; changing that selection preempts the current session and starts
|
|
5350
|
+
* a new one, resetting its box angle and other session-local state.
|
|
4577
5351
|
*
|
|
4578
5352
|
* The facade knows session liveness synchronously — `activeTransformSessionRef`
|
|
4579
5353
|
* is cleared inside the settle path, before control returns to host code — so
|
|
4580
|
-
* unlike a host's own `activeTransformSession ?
|
|
5354
|
+
* unlike a host's own `activeTransformSession ? setGraphics : editMany` check,
|
|
4581
5355
|
* this can never observe a stale ref pointing at an already-settled session.
|
|
4582
5356
|
* The settled-race fall-through hosts had to hand-roll for that gap is owned
|
|
4583
|
-
* here instead: a `
|
|
5357
|
+
* here instead: a `setGraphics` call that (implausibly) still returns
|
|
4584
5358
|
* `undefined` falls through to a fresh `editMany` rather than silently
|
|
4585
5359
|
* dropping the selection.
|
|
4586
5360
|
*
|
|
4587
|
-
* `
|
|
5361
|
+
* `graphics: []` closes any live session (committing every member) and is a
|
|
4588
5362
|
* no-op while idle. With a live session, `options` is ignored — session-scoped
|
|
4589
5363
|
* state (`sizeAnchor`, box angle, close-on-click-away policy) is sticky per
|
|
4590
|
-
* `
|
|
5364
|
+
* `setGraphics`'s contract; `options` applies only when this call starts a
|
|
4591
5365
|
* fresh `editMany`. Exit snapshots are delivered via the session's
|
|
4592
5366
|
* `onCommit` — subscribe once in `options.onSession`. Session lifecycle —
|
|
4593
5367
|
* including an Escape / preempt / destroy abort the host did not itself
|
|
@@ -4599,14 +5373,17 @@ var TacticalDraw = class {
|
|
|
4599
5373
|
* either path. Starting a fresh transform preempts any other in-flight
|
|
4600
5374
|
* draw/edit exactly like `editMany`.
|
|
4601
5375
|
*/
|
|
4602
|
-
|
|
5376
|
+
syncTransformGraphics(graphics, options) {
|
|
4603
5377
|
this.assertAlive();
|
|
4604
5378
|
const session = this.activeTransformSessionRef;
|
|
4605
|
-
if (session) {
|
|
4606
|
-
if (session.
|
|
5379
|
+
if (session && "setGraphics" in session && graphics.every((graphic) => !isPointSymbol(graphic))) {
|
|
5380
|
+
if (session.setGraphics(graphics) !== void 0) return;
|
|
4607
5381
|
}
|
|
4608
|
-
if (
|
|
4609
|
-
|
|
5382
|
+
if (graphics.length === 0) {
|
|
5383
|
+
session?.close();
|
|
5384
|
+
return;
|
|
5385
|
+
}
|
|
5386
|
+
this.editMany([...graphics], options).catch(ignoreAbort);
|
|
4610
5387
|
}
|
|
4611
5388
|
/**
|
|
4612
5389
|
* Subscribe to picks on committed control measures.
|
|
@@ -4619,7 +5396,7 @@ var TacticalDraw = class {
|
|
|
4619
5396
|
* Returns an idempotent unsubscribe function. An optional `signal` removes
|
|
4620
5397
|
* the handler when aborted.
|
|
4621
5398
|
*/
|
|
4622
|
-
|
|
5399
|
+
onGraphicPick(handler, opts) {
|
|
4623
5400
|
this.assertAlive();
|
|
4624
5401
|
return this.onPick(this.graphicsLayer, handler, opts);
|
|
4625
5402
|
}
|
|
@@ -4873,6 +5650,12 @@ var TacticalDraw = class {
|
|
|
4873
5650
|
if (this.destroyed) throw new TacticalDrawDestroyedError();
|
|
4874
5651
|
}
|
|
4875
5652
|
};
|
|
5653
|
+
function graphicRendersFromViewState(graphic) {
|
|
5654
|
+
return isPointSymbol(graphic) ? graphic.size.unit === "meters" : measureRendersFromViewState(graphic);
|
|
5655
|
+
}
|
|
5656
|
+
function normalisePointEditModes(modes) {
|
|
5657
|
+
for (const mode of modes ?? ["transform"]) if (mode !== "transform" && mode !== "delete") throw new TypeError(`Point-symbol editing does not support mode "${mode}"`);
|
|
5658
|
+
}
|
|
4876
5659
|
/**
|
|
4877
5660
|
* Built-in default graphics style. Ensures OL / MapLibre / Leaflet render
|
|
4878
5661
|
* identically when neither host nor measure supplies a style — without it,
|
|
@@ -4997,4 +5780,4 @@ function nearestWorldCopyPosition(position, refLng) {
|
|
|
4997
5780
|
return [nearestWorldCopyLng(position[0], refLng), ...position.slice(1)];
|
|
4998
5781
|
}
|
|
4999
5782
|
//#endregion
|
|
5000
|
-
export { BaseMapAdapter, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, PointerCallbackHub, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, TacticalDrawDestroyedError, bindAbortable, coerceHandleStyle, combineWithHostSignal, getSizeAnchor, hitTestFeature, ignoreAbort, isTacticalDrawAbortError, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, resolveGroundTextSizePixels, tryControlMeasureIdFromFeature };
|
|
5783
|
+
export { BaseMapAdapter, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, PointSymbolCapabilityRequiredError, PointerCallbackHub, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, TacticalDrawDestroyedError, bindAbortable, clonePointSymbol, coerceHandleStyle, combineWithHostSignal, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, isPointSymbol, isTacticalDrawAbortError, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, pointSymbolFootprint, resolveGroundTextSizePixels, tryControlMeasureIdFromFeature };
|