@orbat-mapper/tactical-draw 0.2.0-alpha.33 → 0.2.0-alpha.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +69 -435
- package/dist/index.mjs +545 -442
- package/dist/map-adapter-meRo5VY2.d.mts +361 -0
- package/dist/testing.d.mts +25 -0
- package/dist/testing.mjs +247 -0
- package/package.json +11 -2
package/dist/index.mjs
CHANGED
|
@@ -1,223 +1,4 @@
|
|
|
1
1
|
import { applyBoxTransformOptions, canonicalTextAmplifierKey, cloneControlMeasure, controlMeasureIdFromFeature, freezeOrientationOptions, getControlMeasureMetadata, getDefaultOptions, renderControlMeasure, resolveAmplifierPlacement, resolveStyleHints, roundToFixed } from "@orbat-mapper/control-measures";
|
|
2
|
-
//#region src/base-map-adapter.ts
|
|
3
|
-
/**
|
|
4
|
-
* Abstract base class shared by all `MapAdapter` implementations.
|
|
5
|
-
*
|
|
6
|
-
* Owns shared bookkeeping (layer registry, feature cache, event-token
|
|
7
|
-
* tracking) and orchestrates the public methods over a small
|
|
8
|
-
* protected primitive surface that subclasses implement. See
|
|
9
|
-
* `docs/adr/0001-map-adapter-base-class.md` for the rationale, in particular
|
|
10
|
-
* around the `applyFeatureOps` primitive shape.
|
|
11
|
-
*/
|
|
12
|
-
var BaseMapAdapter = class {
|
|
13
|
-
layers = /* @__PURE__ */ new Map();
|
|
14
|
-
eventTokens = /* @__PURE__ */ new Map();
|
|
15
|
-
viewChangeTokens = /* @__PURE__ */ new Map();
|
|
16
|
-
featureCache = /* @__PURE__ */ new Map();
|
|
17
|
-
featuresChangedListeners = /* @__PURE__ */ new Map();
|
|
18
|
-
iconResources = /* @__PURE__ */ new Map();
|
|
19
|
-
layerIconResourceIds = /* @__PURE__ */ new Map();
|
|
20
|
-
layerCounter = 0;
|
|
21
|
-
destroyed = false;
|
|
22
|
-
nextLayerId() {
|
|
23
|
-
return `layer-${++this.layerCounter}`;
|
|
24
|
-
}
|
|
25
|
-
addVectorLayer(options) {
|
|
26
|
-
const id = this.nextLayerId();
|
|
27
|
-
const handle = this.createLayer(id, options?.style);
|
|
28
|
-
this.layers.set(id, handle);
|
|
29
|
-
this.featureCache.set(id, []);
|
|
30
|
-
return id;
|
|
31
|
-
}
|
|
32
|
-
removeLayer(layerId) {
|
|
33
|
-
const handle = this.layers.get(layerId);
|
|
34
|
-
if (!handle) return;
|
|
35
|
-
this.destroyLayer(handle);
|
|
36
|
-
this.layers.delete(layerId);
|
|
37
|
-
this.featureCache.delete(layerId);
|
|
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();
|
|
52
|
-
}
|
|
53
|
-
setLayerFeatures(layerId, features) {
|
|
54
|
-
if (!this.layers.has(layerId)) return;
|
|
55
|
-
const next = [...features];
|
|
56
|
-
this.featureCache.set(layerId, next);
|
|
57
|
-
this.applyFeatureOps(layerId, {
|
|
58
|
-
adds: next,
|
|
59
|
-
updates: [],
|
|
60
|
-
full: next,
|
|
61
|
-
replace: true
|
|
62
|
-
});
|
|
63
|
-
this.emitLayerFeaturesChanged(layerId, next);
|
|
64
|
-
}
|
|
65
|
-
updateLayerFeatures(layerId, features) {
|
|
66
|
-
if (!this.layers.has(layerId)) return;
|
|
67
|
-
const existing = this.featureCache.get(layerId) ?? [];
|
|
68
|
-
const existingIds = /* @__PURE__ */ new Set();
|
|
69
|
-
for (const f of existing) if (f.id != null) existingIds.add(f.id);
|
|
70
|
-
const adds = [];
|
|
71
|
-
const updates = [];
|
|
72
|
-
const updateById = /* @__PURE__ */ new Map();
|
|
73
|
-
for (const f of features) {
|
|
74
|
-
if (f.id == null) continue;
|
|
75
|
-
if (existingIds.has(f.id)) updates.push(f);
|
|
76
|
-
else adds.push(f);
|
|
77
|
-
updateById.set(f.id, f);
|
|
78
|
-
}
|
|
79
|
-
const merged = [];
|
|
80
|
-
const removed = [];
|
|
81
|
-
for (const f of existing) if (f.id != null && updateById.has(f.id)) {
|
|
82
|
-
merged.push(updateById.get(f.id));
|
|
83
|
-
updateById.delete(f.id);
|
|
84
|
-
} else if (f.id != null && !updateById.has(f.id)) removed.push(f);
|
|
85
|
-
else merged.push(f);
|
|
86
|
-
for (const f of updateById.values()) merged.push(f);
|
|
87
|
-
this.featureCache.set(layerId, merged);
|
|
88
|
-
this.applyFeatureOps(layerId, {
|
|
89
|
-
adds,
|
|
90
|
-
updates,
|
|
91
|
-
removes: removed,
|
|
92
|
-
full: merged,
|
|
93
|
-
replace: false
|
|
94
|
-
});
|
|
95
|
-
this.emitLayerFeaturesChanged(layerId, merged);
|
|
96
|
-
}
|
|
97
|
-
removeFeatures(layerId, ids) {
|
|
98
|
-
if (!this.layers.has(layerId)) return;
|
|
99
|
-
if (ids.length === 0) return;
|
|
100
|
-
const existing = this.featureCache.get(layerId) ?? [];
|
|
101
|
-
const idSet = new Set(ids);
|
|
102
|
-
const merged = [];
|
|
103
|
-
const removed = [];
|
|
104
|
-
for (const f of existing) if (typeof f.id === "string" && idSet.has(f.id)) removed.push(f);
|
|
105
|
-
else merged.push(f);
|
|
106
|
-
if (removed.length === 0) return;
|
|
107
|
-
this.featureCache.set(layerId, merged);
|
|
108
|
-
this.applyFeatureOps(layerId, {
|
|
109
|
-
adds: [],
|
|
110
|
-
updates: [],
|
|
111
|
-
removes: removed,
|
|
112
|
-
full: merged,
|
|
113
|
-
replace: false
|
|
114
|
-
});
|
|
115
|
-
this.emitLayerFeaturesChanged(layerId, merged);
|
|
116
|
-
}
|
|
117
|
-
clearLayer(layerId) {
|
|
118
|
-
if (!this.layers.has(layerId)) return;
|
|
119
|
-
this.featureCache.set(layerId, []);
|
|
120
|
-
this.applyFeatureOps(layerId, {
|
|
121
|
-
adds: [],
|
|
122
|
-
updates: [],
|
|
123
|
-
full: [],
|
|
124
|
-
replace: true
|
|
125
|
-
});
|
|
126
|
-
this.emitLayerFeaturesChanged(layerId, []);
|
|
127
|
-
}
|
|
128
|
-
on(eventType, handler) {
|
|
129
|
-
const entry = this.attachNativeEvent(eventType, (lonLat, pixel, originalEvent) => {
|
|
130
|
-
handler({
|
|
131
|
-
type: eventType,
|
|
132
|
-
coordinate: lonLat,
|
|
133
|
-
pixel,
|
|
134
|
-
originalEvent
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
this.eventTokens.set(handler, entry);
|
|
138
|
-
}
|
|
139
|
-
off(eventType, handler) {
|
|
140
|
-
const entry = this.eventTokens.get(handler);
|
|
141
|
-
if (!entry) return;
|
|
142
|
-
this.detachNativeEvent(entry);
|
|
143
|
-
this.eventTokens.delete(handler);
|
|
144
|
-
}
|
|
145
|
-
onViewChange(handler) {
|
|
146
|
-
const token = this.attachViewChange(handler);
|
|
147
|
-
this.viewChangeTokens.set(handler, token);
|
|
148
|
-
}
|
|
149
|
-
offViewChange(handler) {
|
|
150
|
-
const token = this.viewChangeTokens.get(handler);
|
|
151
|
-
if (token === void 0) return;
|
|
152
|
-
this.detachViewChange(token);
|
|
153
|
-
this.viewChangeTokens.delete(handler);
|
|
154
|
-
}
|
|
155
|
-
destroy() {
|
|
156
|
-
if (this.destroyed) return;
|
|
157
|
-
try {
|
|
158
|
-
for (const entry of this.eventTokens.values()) this.detachNativeEvent(entry);
|
|
159
|
-
this.eventTokens.clear();
|
|
160
|
-
for (const token of this.viewChangeTokens.values()) this.detachViewChange(token);
|
|
161
|
-
this.viewChangeTokens.clear();
|
|
162
|
-
for (const handle of this.layers.values()) this.destroyLayer(handle);
|
|
163
|
-
this.layers.clear();
|
|
164
|
-
this.featureCache.clear();
|
|
165
|
-
this.featuresChangedListeners.clear();
|
|
166
|
-
for (const id of this.iconResources.keys()) this.removeIconResource(id);
|
|
167
|
-
this.iconResources.clear();
|
|
168
|
-
this.layerIconResourceIds.clear();
|
|
169
|
-
this.destroyEngineResources();
|
|
170
|
-
} finally {
|
|
171
|
-
this.destroyed = true;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
emitLayerFeaturesChanged(layerId, features) {
|
|
175
|
-
const listeners = this.featuresChangedListeners.get(layerId);
|
|
176
|
-
if (!listeners) return;
|
|
177
|
-
for (const fn of listeners) fn(features);
|
|
178
|
-
}
|
|
179
|
-
onLayerFeaturesChanged(layerId, listener) {
|
|
180
|
-
let listeners = this.featuresChangedListeners.get(layerId);
|
|
181
|
-
if (!listeners) {
|
|
182
|
-
listeners = /* @__PURE__ */ new Set();
|
|
183
|
-
this.featuresChangedListeners.set(layerId, listeners);
|
|
184
|
-
}
|
|
185
|
-
listeners.add(listener);
|
|
186
|
-
return () => {
|
|
187
|
-
const set = this.featuresChangedListeners.get(layerId);
|
|
188
|
-
if (!set) return;
|
|
189
|
-
set.delete(listener);
|
|
190
|
-
if (set.size === 0) this.featuresChangedListeners.delete(layerId);
|
|
191
|
-
};
|
|
192
|
-
}
|
|
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
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Default for engines/views that don't rotate (Leaflet). Rotating engines
|
|
212
|
-
* (MapLibre, OpenLayers) override this.
|
|
213
|
-
*/
|
|
214
|
-
getBearing() {
|
|
215
|
-
return 0;
|
|
216
|
-
}
|
|
217
|
-
getLayerFeatures(layerId) {
|
|
218
|
-
return this.featureCache.get(layerId) ?? [];
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
2
|
function pickPointerType(originalEvent) {
|
|
222
3
|
if (typeof PointerEvent !== "undefined" && originalEvent instanceof PointerEvent) {
|
|
223
4
|
if (originalEvent.pointerType === "touch") return "touch";
|
|
@@ -318,11 +99,9 @@ function mergeNativeAndCachedPickFeatures(nativeFeatures, cachedFeatures) {
|
|
|
318
99
|
* primary candidate.
|
|
319
100
|
*/
|
|
320
101
|
function evaluatePickCandidates(inputs, context) {
|
|
102
|
+
const candidates = eligibleCandidates(inputs, context);
|
|
321
103
|
let best = null;
|
|
322
|
-
for (const
|
|
323
|
-
const candidate = buildCandidate(input, context);
|
|
324
|
-
if (candidate && (best === null || compareCandidates(candidate, best) < 0)) best = candidate;
|
|
325
|
-
}
|
|
104
|
+
for (const candidate of candidates) if (best === null || compareCandidates(candidate, best) < 0) best = candidate;
|
|
326
105
|
return best;
|
|
327
106
|
}
|
|
328
107
|
function buildCandidate(input, context) {
|
|
@@ -337,10 +116,17 @@ function buildCandidate(input, context) {
|
|
|
337
116
|
viewport: context.viewport,
|
|
338
117
|
presentation,
|
|
339
118
|
visualOrder: input.visualOrder,
|
|
119
|
+
...input.featureGroupOrder !== void 0 ? { featureGroupOrder: input.featureGroupOrder } : {},
|
|
340
120
|
...evaluated.iconFootprint ? { iconFootprint: evaluated.iconFootprint } : {},
|
|
341
121
|
...input.labelExtent ? { labelExtent: input.labelExtent } : {}
|
|
342
122
|
};
|
|
343
123
|
}
|
|
124
|
+
function eligibleCandidates(inputs, context) {
|
|
125
|
+
const candidates = inputs.map((input) => buildCandidate(input, context)).filter((candidate) => candidate !== null);
|
|
126
|
+
const opaqueCovers = candidates.filter(isOpaqueFillInterior);
|
|
127
|
+
if (opaqueCovers.length === 0) return candidates;
|
|
128
|
+
return candidates.filter((candidate) => !opaqueCovers.some((cover) => cover.featureGroupOrder !== void 0 && candidate.featureGroupOrder !== void 0 && cover.featureGroupOrder < candidate.featureGroupOrder));
|
|
129
|
+
}
|
|
344
130
|
function compareCandidates(a, b) {
|
|
345
131
|
const rankA = selectionSurfaceRank(a);
|
|
346
132
|
const rankDifference = rankA - selectionSurfaceRank(b);
|
|
@@ -349,8 +135,13 @@ function compareCandidates(a, b) {
|
|
|
349
135
|
const distanceDifference = a.hit.distance - b.hit.distance;
|
|
350
136
|
if (distanceDifference !== 0) return distanceDifference;
|
|
351
137
|
}
|
|
138
|
+
if (a.featureGroupOrder !== void 0 && b.featureGroupOrder !== void 0 && a.featureGroupOrder !== b.featureGroupOrder) return a.featureGroupOrder - b.featureGroupOrder;
|
|
352
139
|
return a.visualOrder - b.visualOrder;
|
|
353
140
|
}
|
|
141
|
+
function isOpaqueFillInterior(candidate) {
|
|
142
|
+
const fill = candidate.presentation.fill;
|
|
143
|
+
return candidate.hit.kind === "polygon-interior" && fill?.opacity === 1 && (fill.pattern === void 0 || fill.pattern === "solid");
|
|
144
|
+
}
|
|
354
145
|
function selectionSurfaceRank(candidate) {
|
|
355
146
|
if (candidate.hit.kind !== "polygon-interior") return 0;
|
|
356
147
|
return (candidate.presentation.fill?.opacity ?? 0) > 0 ? 1 : 2;
|
|
@@ -645,6 +436,196 @@ function numberArray(value) {
|
|
|
645
436
|
return Array.isArray(value) && value.every((item) => typeof item === "number") ? [...value] : void 0;
|
|
646
437
|
}
|
|
647
438
|
//#endregion
|
|
439
|
+
//#region src/base-map-adapter.ts
|
|
440
|
+
/**
|
|
441
|
+
* Abstract base class shared by all `MapAdapter` implementations.
|
|
442
|
+
*
|
|
443
|
+
* Owns shared bookkeeping (layer registry, feature cache, event-token
|
|
444
|
+
* tracking) and orchestrates the public methods over a small protected
|
|
445
|
+
* grouped desired-state primitive that subclasses implement.
|
|
446
|
+
*/
|
|
447
|
+
var BaseMapAdapter = class {
|
|
448
|
+
layers = /* @__PURE__ */ new Map();
|
|
449
|
+
eventTokens = /* @__PURE__ */ new Map();
|
|
450
|
+
viewChangeTokens = /* @__PURE__ */ new Map();
|
|
451
|
+
featureCache = /* @__PURE__ */ new Map();
|
|
452
|
+
featuresChangedListeners = /* @__PURE__ */ new Map();
|
|
453
|
+
iconResources = /* @__PURE__ */ new Map();
|
|
454
|
+
featureGroupState = /* @__PURE__ */ new Map();
|
|
455
|
+
layerIconResourceIds = /* @__PURE__ */ new Map();
|
|
456
|
+
layerCounter = 0;
|
|
457
|
+
destroyed = false;
|
|
458
|
+
nextLayerId() {
|
|
459
|
+
return `layer-${++this.layerCounter}`;
|
|
460
|
+
}
|
|
461
|
+
addVectorLayer(options) {
|
|
462
|
+
const id = this.nextLayerId();
|
|
463
|
+
const handle = this.createLayer(id, options?.style);
|
|
464
|
+
this.layers.set(id, handle);
|
|
465
|
+
this.featureCache.set(id, []);
|
|
466
|
+
this.featureGroupState.set(id, {
|
|
467
|
+
groups: [],
|
|
468
|
+
orderByFeatureId: /* @__PURE__ */ new Map()
|
|
469
|
+
});
|
|
470
|
+
return id;
|
|
471
|
+
}
|
|
472
|
+
removeLayer(layerId) {
|
|
473
|
+
const handle = this.layers.get(layerId);
|
|
474
|
+
if (!handle) return;
|
|
475
|
+
this.destroyLayer(handle);
|
|
476
|
+
this.layers.delete(layerId);
|
|
477
|
+
this.featureCache.delete(layerId);
|
|
478
|
+
this.featureGroupState.delete(layerId);
|
|
479
|
+
this.featuresChangedListeners.delete(layerId);
|
|
480
|
+
this.layerIconResourceIds.delete(layerId);
|
|
481
|
+
this.pruneIconResources();
|
|
482
|
+
}
|
|
483
|
+
setLayerFeatureGroups(layerId, groups) {
|
|
484
|
+
validateFeatureGroups(groups);
|
|
485
|
+
if (!this.layers.has(layerId)) return;
|
|
486
|
+
const nextGroups = groups.map((group) => ({
|
|
487
|
+
id: group.id,
|
|
488
|
+
features: [...group.features],
|
|
489
|
+
iconResources: [...group.iconResources]
|
|
490
|
+
}));
|
|
491
|
+
const nextFeatures = nextGroups.flatMap((group) => group.features);
|
|
492
|
+
const nextResourcesById = /* @__PURE__ */ new Map();
|
|
493
|
+
for (const group of nextGroups) for (const resource of group.iconResources) nextResourcesById.set(resource.id, resource);
|
|
494
|
+
const nextResources = [...nextResourcesById.values()];
|
|
495
|
+
const resourceIds = new Set(nextResourcesById.keys());
|
|
496
|
+
for (const resource of nextResources) this.iconResources.set(resource.id, resource);
|
|
497
|
+
this.layerIconResourceIds.set(layerId, resourceIds);
|
|
498
|
+
this.applyIconResources(nextResources);
|
|
499
|
+
this.featureGroupState.set(layerId, {
|
|
500
|
+
groups: nextGroups,
|
|
501
|
+
orderByFeatureId: indexFeatureGroupOrder(nextGroups)
|
|
502
|
+
});
|
|
503
|
+
this.featureCache.set(layerId, nextFeatures);
|
|
504
|
+
this.applyFeatureGroups(layerId, nextGroups);
|
|
505
|
+
this.emitLayerFeaturesChanged(layerId, nextFeatures);
|
|
506
|
+
this.pruneIconResources(true);
|
|
507
|
+
}
|
|
508
|
+
on(eventType, handler) {
|
|
509
|
+
const entry = this.attachNativeEvent(eventType, (lonLat, pixel, originalEvent) => {
|
|
510
|
+
handler({
|
|
511
|
+
type: eventType,
|
|
512
|
+
coordinate: lonLat,
|
|
513
|
+
pixel,
|
|
514
|
+
originalEvent
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
this.eventTokens.set(handler, entry);
|
|
518
|
+
}
|
|
519
|
+
off(eventType, handler) {
|
|
520
|
+
const entry = this.eventTokens.get(handler);
|
|
521
|
+
if (!entry) return;
|
|
522
|
+
this.detachNativeEvent(entry);
|
|
523
|
+
this.eventTokens.delete(handler);
|
|
524
|
+
}
|
|
525
|
+
onViewChange(handler) {
|
|
526
|
+
const token = this.attachViewChange(handler);
|
|
527
|
+
this.viewChangeTokens.set(handler, token);
|
|
528
|
+
}
|
|
529
|
+
offViewChange(handler) {
|
|
530
|
+
const token = this.viewChangeTokens.get(handler);
|
|
531
|
+
if (token === void 0) return;
|
|
532
|
+
this.detachViewChange(token);
|
|
533
|
+
this.viewChangeTokens.delete(handler);
|
|
534
|
+
}
|
|
535
|
+
destroy() {
|
|
536
|
+
if (this.destroyed) return;
|
|
537
|
+
try {
|
|
538
|
+
for (const entry of this.eventTokens.values()) this.detachNativeEvent(entry);
|
|
539
|
+
this.eventTokens.clear();
|
|
540
|
+
for (const token of this.viewChangeTokens.values()) this.detachViewChange(token);
|
|
541
|
+
this.viewChangeTokens.clear();
|
|
542
|
+
for (const handle of this.layers.values()) this.destroyLayer(handle);
|
|
543
|
+
this.layers.clear();
|
|
544
|
+
this.featureCache.clear();
|
|
545
|
+
this.featureGroupState.clear();
|
|
546
|
+
this.featuresChangedListeners.clear();
|
|
547
|
+
for (const id of this.iconResources.keys()) this.removeIconResource(id);
|
|
548
|
+
this.iconResources.clear();
|
|
549
|
+
this.layerIconResourceIds.clear();
|
|
550
|
+
this.destroyEngineResources();
|
|
551
|
+
} finally {
|
|
552
|
+
this.destroyed = true;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
emitLayerFeaturesChanged(layerId, features) {
|
|
556
|
+
const listeners = this.featuresChangedListeners.get(layerId);
|
|
557
|
+
if (!listeners) return;
|
|
558
|
+
for (const fn of listeners) fn(features);
|
|
559
|
+
}
|
|
560
|
+
onLayerFeaturesChanged(layerId, listener) {
|
|
561
|
+
let listeners = this.featuresChangedListeners.get(layerId);
|
|
562
|
+
if (!listeners) {
|
|
563
|
+
listeners = /* @__PURE__ */ new Set();
|
|
564
|
+
this.featuresChangedListeners.set(layerId, listeners);
|
|
565
|
+
}
|
|
566
|
+
listeners.add(listener);
|
|
567
|
+
return () => {
|
|
568
|
+
const set = this.featuresChangedListeners.get(layerId);
|
|
569
|
+
if (!set) return;
|
|
570
|
+
set.delete(listener);
|
|
571
|
+
if (set.size === 0) this.featuresChangedListeners.delete(layerId);
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
destroyEngineResources() {}
|
|
575
|
+
applyIconResources(resources) {}
|
|
576
|
+
getIconResource(id) {
|
|
577
|
+
return this.iconResources.get(id);
|
|
578
|
+
}
|
|
579
|
+
getFeatureGroupOrder(layerId, featureId) {
|
|
580
|
+
if (featureId === void 0) return void 0;
|
|
581
|
+
return this.featureGroupState.get(layerId)?.orderByFeatureId.get(featureId);
|
|
582
|
+
}
|
|
583
|
+
removeIconResource(id) {}
|
|
584
|
+
pruneIconResources(removeAllUnused = false) {
|
|
585
|
+
const active = /* @__PURE__ */ new Set();
|
|
586
|
+
for (const ids of this.layerIconResourceIds.values()) for (const id of ids) active.add(id);
|
|
587
|
+
if (!removeAllUnused && this.iconResources.size <= active.size + 32) return;
|
|
588
|
+
for (const id of this.iconResources.keys()) {
|
|
589
|
+
if (active.has(id)) continue;
|
|
590
|
+
this.iconResources.delete(id);
|
|
591
|
+
this.removeIconResource(id);
|
|
592
|
+
if (!removeAllUnused && this.iconResources.size <= active.size + 32) break;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Default for engines/views that don't rotate (Leaflet). Rotating engines
|
|
597
|
+
* (MapLibre, OpenLayers) override this.
|
|
598
|
+
*/
|
|
599
|
+
getBearing() {
|
|
600
|
+
return 0;
|
|
601
|
+
}
|
|
602
|
+
getLayerFeatures(layerId) {
|
|
603
|
+
return this.featureCache.get(layerId) ?? [];
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
function indexFeatureGroupOrder(groups) {
|
|
607
|
+
const orderByFeatureId = /* @__PURE__ */ new Map();
|
|
608
|
+
groups.forEach((group, index) => {
|
|
609
|
+
const visualOrder = renderOrderToVisualOrder(index, groups.length);
|
|
610
|
+
for (const feature of group.features) if (feature.id != null) orderByFeatureId.set(feature.id, visualOrder);
|
|
611
|
+
});
|
|
612
|
+
return orderByFeatureId;
|
|
613
|
+
}
|
|
614
|
+
function validateFeatureGroups(groups) {
|
|
615
|
+
const groupIds = /* @__PURE__ */ new Set();
|
|
616
|
+
const featureIds = /* @__PURE__ */ new Set();
|
|
617
|
+
for (const group of groups) {
|
|
618
|
+
if (typeof group.id !== "string" || group.id.trim().length === 0) throw new Error("Feature group id must be a non-empty string");
|
|
619
|
+
if (groupIds.has(group.id)) throw new Error(`Duplicate feature group id: ${group.id}`);
|
|
620
|
+
groupIds.add(group.id);
|
|
621
|
+
for (const feature of group.features) {
|
|
622
|
+
if (feature.id == null) throw new Error(`Feature in group "${group.id}" is missing an id`);
|
|
623
|
+
if (featureIds.has(feature.id)) throw new Error(`Duplicate feature id: ${String(feature.id)}`);
|
|
624
|
+
featureIds.add(feature.id);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
//#endregion
|
|
648
629
|
//#region src/feature-style.ts
|
|
649
630
|
/**
|
|
650
631
|
* Coerce a raw `properties.style` value into a `HandleStyle` for the
|
|
@@ -700,6 +681,17 @@ function resolveTextSizePixels(props, resolution) {
|
|
|
700
681
|
}
|
|
701
682
|
return resolveGroundTextSizePixels(meters, resolution, props);
|
|
702
683
|
}
|
|
684
|
+
/** Fallback halo color when a `textHalo` feature carries no `textHaloColor`. */
|
|
685
|
+
const TEXT_HALO_COLOR = "#ffffff";
|
|
686
|
+
/**
|
|
687
|
+
* Outward depth (px) of a `textHalo` halo from the glyph edge, scaled to font
|
|
688
|
+
* size. This is the halo's *outward* reach, not a stroke width — a canvas
|
|
689
|
+
* stroke straddles the glyph edge (half in, half out), so a caller drawing
|
|
690
|
+
* one must double this value to get the same visible depth.
|
|
691
|
+
*/
|
|
692
|
+
function resolveTextHaloWidth(textSizePixels) {
|
|
693
|
+
return Math.max(1.5, (textSizePixels ?? 24) / 8);
|
|
694
|
+
}
|
|
703
695
|
//#endregion
|
|
704
696
|
//#region src/gestures/hit-test.ts
|
|
705
697
|
/** Clamped parameter t ∈ [0, 1] of `pixel` projected onto the segment a→b in pixel space, or null for a zero-length segment. */
|
|
@@ -1251,23 +1243,15 @@ function runInteraction(spec) {
|
|
|
1251
1243
|
resolveFn = resolve;
|
|
1252
1244
|
rejectFn = reject;
|
|
1253
1245
|
});
|
|
1254
|
-
const
|
|
1255
|
-
for (const layer of layers) idsByLayer.set(layer, /* @__PURE__ */ new Set());
|
|
1246
|
+
const ownedLayers = new Set(layers);
|
|
1256
1247
|
let controllerDetach = null;
|
|
1257
1248
|
let controllerOnAbort = null;
|
|
1258
|
-
function writeLayer(layer,
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
if (!set) return;
|
|
1262
|
-
set.clear();
|
|
1263
|
-
for (const f of features) if (typeof f.id === "string") set.add(f.id);
|
|
1249
|
+
function writeLayer(layer, groups) {
|
|
1250
|
+
if (!ownedLayers.has(layer)) return;
|
|
1251
|
+
adapter.setLayerFeatureGroups(layer, groups);
|
|
1264
1252
|
}
|
|
1265
1253
|
function clearLayers() {
|
|
1266
|
-
for (const
|
|
1267
|
-
if (set.size === 0) continue;
|
|
1268
|
-
adapter.removeFeatures(layer, [...set]);
|
|
1269
|
-
set.clear();
|
|
1270
|
-
}
|
|
1254
|
+
for (const layer of ownedLayers) adapter.setLayerFeatureGroups(layer, []);
|
|
1271
1255
|
}
|
|
1272
1256
|
const handleKeydown = (event) => {
|
|
1273
1257
|
if (settled) return;
|
|
@@ -1805,6 +1789,7 @@ function createCommitEmitter(listenerErrorsMessage) {
|
|
|
1805
1789
|
//#endregion
|
|
1806
1790
|
//#region src/tactical-draw/draw-controller.ts
|
|
1807
1791
|
const PREVIEW_CM_ID$1 = "__td_preview__";
|
|
1792
|
+
const GUIDE_GROUP_ID = "__td_draw_guide__";
|
|
1808
1793
|
const PREVIEW_CURSOR$1 = "crosshair";
|
|
1809
1794
|
/**
|
|
1810
1795
|
* Drive an interactive fixed-length draw against a `MapAdapter`.
|
|
@@ -1919,16 +1904,32 @@ function runFixedLengthDraw(options) {
|
|
|
1919
1904
|
function renderPreview() {
|
|
1920
1905
|
const raw = rubberBand !== void 0 ? [...committed, rubberBand] : [...committed];
|
|
1921
1906
|
if (raw.length === 0) {
|
|
1922
|
-
ctx.writeLayer(previewLayer, [
|
|
1923
|
-
|
|
1907
|
+
ctx.writeLayer(previewLayer, [{
|
|
1908
|
+
id: PREVIEW_CM_ID$1,
|
|
1909
|
+
features: [],
|
|
1910
|
+
iconResources: []
|
|
1911
|
+
}]);
|
|
1912
|
+
ctx.writeLayer(guideLayer, [{
|
|
1913
|
+
id: GUIDE_GROUP_ID,
|
|
1914
|
+
features: [],
|
|
1915
|
+
iconResources: []
|
|
1916
|
+
}]);
|
|
1924
1917
|
return;
|
|
1925
1918
|
}
|
|
1926
1919
|
const previewThreshold = rule?.minimumPreviewPoints ?? required;
|
|
1927
1920
|
const features = buildPreviewFeatures$1(adapter, kind, rule && raw.length >= previewThreshold ? rule.derive(raw) : raw, measureOptions, measureStyle, measureTextAmplifiers, renderOpts);
|
|
1928
|
-
ctx.writeLayer(previewLayer,
|
|
1921
|
+
ctx.writeLayer(previewLayer, [{
|
|
1922
|
+
id: PREVIEW_CM_ID$1,
|
|
1923
|
+
features,
|
|
1924
|
+
iconResources: []
|
|
1925
|
+
}]);
|
|
1929
1926
|
const guidePoints = rule?.guidePoints ? rule.guidePoints(raw) : raw;
|
|
1930
1927
|
const guideFeatures = guideEnabled ? buildGuideFeatures(guidePoints, isAreaKind) : [];
|
|
1931
|
-
ctx.writeLayer(guideLayer,
|
|
1928
|
+
ctx.writeLayer(guideLayer, [{
|
|
1929
|
+
id: GUIDE_GROUP_ID,
|
|
1930
|
+
features: guideFeatures,
|
|
1931
|
+
iconResources: []
|
|
1932
|
+
}]);
|
|
1932
1933
|
}
|
|
1933
1934
|
function rescaleOnViewChange() {
|
|
1934
1935
|
if (!measureRendersFromViewState({
|
|
@@ -2207,9 +2208,17 @@ function runVariableLengthDraw(options) {
|
|
|
2207
2208
|
} else points = [...committed];
|
|
2208
2209
|
if (rule) points = rule.derive(points);
|
|
2209
2210
|
const previewFeatures = points.length === 0 ? [] : buildPreviewFeatures(adapter, kind, points, measureOptions, measureStyle, measureTextAmplifiers, renderOpts);
|
|
2210
|
-
ctx.writeLayer(previewLayer,
|
|
2211
|
+
ctx.writeLayer(previewLayer, [{
|
|
2212
|
+
id: "__td_variable_draw_preview__",
|
|
2213
|
+
features: previewFeatures,
|
|
2214
|
+
iconResources: []
|
|
2215
|
+
}]);
|
|
2211
2216
|
const guideFeatures = points.length === 0 || !guideEnabled ? [] : buildGuideFeatures(points, isAreaKind, trailingFixed);
|
|
2212
|
-
ctx.writeLayer(guideLayer,
|
|
2217
|
+
ctx.writeLayer(guideLayer, [{
|
|
2218
|
+
id: "__td_variable_draw_guide__",
|
|
2219
|
+
features: guideFeatures,
|
|
2220
|
+
iconResources: []
|
|
2221
|
+
}]);
|
|
2213
2222
|
}
|
|
2214
2223
|
function rescaleOnViewChange() {
|
|
2215
2224
|
if (!measureRendersFromViewState({
|
|
@@ -2375,6 +2384,7 @@ function runEditInteraction(spec) {
|
|
|
2375
2384
|
spec.onForeignPick?.(localGraphics);
|
|
2376
2385
|
return;
|
|
2377
2386
|
}
|
|
2387
|
+
if (localGraphics && spec.ownsGraphic(localGraphics.id)) return;
|
|
2378
2388
|
if (localPreview) return;
|
|
2379
2389
|
if (wasBodyClick) return;
|
|
2380
2390
|
spec.close();
|
|
@@ -3563,15 +3573,15 @@ const ALLOWED_EDIT_MODES = new Set([
|
|
|
3563
3573
|
* Drive an interactive edit against a `MapAdapter`. Implements the edit
|
|
3564
3574
|
* lifecycle (issue #48) plus the ADR-0017 transform box:
|
|
3565
3575
|
*
|
|
3566
|
-
* -
|
|
3567
|
-
* -
|
|
3576
|
+
* - working-render publication into the host-owned Graphic stack slot
|
|
3577
|
+
* - reshape-handle rendering and the oriented transform box
|
|
3568
3578
|
* (corner scale handles, rotate grip, drag-anywhere-inside body translate)
|
|
3569
3579
|
* - dirty tracking via [[DIRTY_EPSILON]] tolerance
|
|
3570
3580
|
* - default click-away close policy
|
|
3571
3581
|
* - close / abort / signal / preempt / destroy paths
|
|
3572
3582
|
*/
|
|
3573
3583
|
function runEdit(opts) {
|
|
3574
|
-
const { adapter, measure, graphicsLayer, previewLayer, guideLayer, handlesLayer, graphicsStyle, signal, closeOnClickAway = true, modes: initialModes, interactionStyle, onSession, handleIdPrefix, guide: guideOpt, settleToGraphics,
|
|
3584
|
+
const { adapter, measure, graphicsLayer, previewLayer, guideLayer, handlesLayer, graphicsStyle, signal, closeOnClickAway = true, modes: initialModes, interactionStyle, onSession, handleIdPrefix, guide: guideOpt, settleToGraphics, publishWorkingGraphic, onSettled, onClickAwayGraphicsPick, snap } = opts;
|
|
3575
3585
|
const renderOpts = graphicsStyle ? { graphicsStyle } : void 0;
|
|
3576
3586
|
let sizeAnchor = opts.sizeAnchor ?? "ground";
|
|
3577
3587
|
const metadata = getControlMeasureMetadata(measure.kind);
|
|
@@ -3684,8 +3694,8 @@ function runEdit(opts) {
|
|
|
3684
3694
|
* while a box gesture (`activeBoxDrag`) is in flight.
|
|
3685
3695
|
*/
|
|
3686
3696
|
let box = null;
|
|
3687
|
-
/** The
|
|
3688
|
-
let
|
|
3697
|
+
/** The last working-render features, reused to refit `box` without a second render pass. */
|
|
3698
|
+
let lastWorkingFeatures = [];
|
|
3689
3699
|
let ctx;
|
|
3690
3700
|
let driver;
|
|
3691
3701
|
let shell;
|
|
@@ -3699,7 +3709,7 @@ function runEdit(opts) {
|
|
|
3699
3709
|
resolution: adapter.getResolution(),
|
|
3700
3710
|
bearing: adapter.getBearing()
|
|
3701
3711
|
}),
|
|
3702
|
-
features: () =>
|
|
3712
|
+
features: () => lastWorkingFeatures,
|
|
3703
3713
|
snapshotAmplifierPlacements,
|
|
3704
3714
|
handleIdPrefix,
|
|
3705
3715
|
interactionStyle
|
|
@@ -3735,7 +3745,7 @@ function runEdit(opts) {
|
|
|
3735
3745
|
});
|
|
3736
3746
|
}
|
|
3737
3747
|
/**
|
|
3738
|
-
* Re-fit `box` from `
|
|
3748
|
+
* Re-fit `box` from `lastWorkingFeatures`. A no-op while a box gesture is
|
|
3739
3749
|
* in flight — that drag paints the box rigidly from its own snapshot
|
|
3740
3750
|
* (`BoxDragSnapshot.box`) instead of a re-fit, so a corner/rotate/body drag
|
|
3741
3751
|
* doesn't fight its own paint. Text is the exception: its box follows the
|
|
@@ -3744,14 +3754,9 @@ function runEdit(opts) {
|
|
|
3744
3754
|
*/
|
|
3745
3755
|
function refitBox() {
|
|
3746
3756
|
if (activeBoxDrag && !isTextGraphic) return;
|
|
3747
|
-
box = fitBoxFromFeatures(
|
|
3757
|
+
box = fitBoxFromFeatures(lastWorkingFeatures);
|
|
3748
3758
|
}
|
|
3749
|
-
function
|
|
3750
|
-
if (!wasOnGraphicsLayer) return;
|
|
3751
|
-
const originalFeatureIds = renderMeasureForAdapter(adapter, measure, renderOpts).features.map((f) => f.id).filter((id) => typeof id === "string");
|
|
3752
|
-
if (originalFeatureIds.length > 0) adapter.removeFeatures(graphicsLayer, originalFeatureIds);
|
|
3753
|
-
}
|
|
3754
|
-
function renderPreview() {
|
|
3759
|
+
function renderWorkingGraphic() {
|
|
3755
3760
|
if (ctx.settled) return;
|
|
3756
3761
|
const working = {
|
|
3757
3762
|
...measure,
|
|
@@ -3769,19 +3774,23 @@ function runEdit(opts) {
|
|
|
3769
3774
|
} catch {
|
|
3770
3775
|
features = [];
|
|
3771
3776
|
}
|
|
3772
|
-
|
|
3773
|
-
|
|
3777
|
+
publishWorkingGraphic(working, features);
|
|
3778
|
+
lastWorkingFeatures = features;
|
|
3774
3779
|
refitBox();
|
|
3775
3780
|
const guidePoints = rule?.guidePoints ? rule.guidePoints(workingPoints) : workingPoints;
|
|
3776
3781
|
const guideFeatures = guideEnabled ? buildGuideFeatures(guidePoints, isAreaKind, trailingFixedSlots) : [];
|
|
3777
|
-
ctx.writeLayer(guideLayer,
|
|
3782
|
+
ctx.writeLayer(guideLayer, [{
|
|
3783
|
+
id: `__td_edit_guide__:${measure.id}`,
|
|
3784
|
+
features: guideFeatures,
|
|
3785
|
+
iconResources: []
|
|
3786
|
+
}]);
|
|
3778
3787
|
}
|
|
3779
3788
|
function rescaleOnViewChange() {
|
|
3780
3789
|
if (ctx.settled) return;
|
|
3781
3790
|
if (measureRendersFromViewState({
|
|
3782
3791
|
kind: measure.kind,
|
|
3783
3792
|
options: workingOptions
|
|
3784
|
-
}))
|
|
3793
|
+
})) renderWorkingGraphic();
|
|
3785
3794
|
else refitBox();
|
|
3786
3795
|
renderHandlesLayer();
|
|
3787
3796
|
}
|
|
@@ -3800,10 +3809,14 @@ function runEdit(opts) {
|
|
|
3800
3809
|
features.push(...toFeatures(boxToHandles(liveBox, dragProjection.fromPixel), handleIdPrefix, interactionStyle));
|
|
3801
3810
|
}
|
|
3802
3811
|
}
|
|
3803
|
-
ctx.writeLayer(handlesLayer,
|
|
3812
|
+
ctx.writeLayer(handlesLayer, [{
|
|
3813
|
+
id: `__td_edit_handles__:${measure.id}`,
|
|
3814
|
+
features,
|
|
3815
|
+
iconResources: []
|
|
3816
|
+
}]);
|
|
3804
3817
|
}
|
|
3805
3818
|
/**
|
|
3806
|
-
* Repaint the preview and, in lockstep, the handles. `
|
|
3819
|
+
* Repaint the preview and, in lockstep, the handles. `renderWorkingGraphic` re-fits
|
|
3807
3820
|
* `box` (for Text, the footprint depends on options — size/rotation/
|
|
3808
3821
|
* alignment/text), so the transform-box outline and its corner/rotate handles
|
|
3809
3822
|
* must repaint to match; otherwise they stay where the old options placed them
|
|
@@ -3811,8 +3824,8 @@ function runEdit(opts) {
|
|
|
3811
3824
|
* (#scale-handle desync). Used by the non-geometry option/style/amplifier
|
|
3812
3825
|
* setters, which change what the preview renders without moving a point.
|
|
3813
3826
|
*/
|
|
3814
|
-
function
|
|
3815
|
-
|
|
3827
|
+
function renderWorkingGraphicAndHandles() {
|
|
3828
|
+
renderWorkingGraphic();
|
|
3816
3829
|
renderHandlesLayer();
|
|
3817
3830
|
}
|
|
3818
3831
|
/**
|
|
@@ -3893,7 +3906,7 @@ function runEdit(opts) {
|
|
|
3893
3906
|
workingAmplifierPlacements = previousAmplifierPlacements;
|
|
3894
3907
|
boxAngle = previousBoxAngle;
|
|
3895
3908
|
nonGeometryDirty = previousNonGeometryDirty;
|
|
3896
|
-
|
|
3909
|
+
renderWorkingGraphic();
|
|
3897
3910
|
renderHandlesLayer();
|
|
3898
3911
|
recomputeDirty();
|
|
3899
3912
|
},
|
|
@@ -3932,7 +3945,7 @@ function runEdit(opts) {
|
|
|
3932
3945
|
drainPendingModes();
|
|
3933
3946
|
throw err;
|
|
3934
3947
|
}
|
|
3935
|
-
|
|
3948
|
+
renderWorkingGraphic();
|
|
3936
3949
|
renderHandlesLayer();
|
|
3937
3950
|
recomputeDirty();
|
|
3938
3951
|
dragInFlight = false;
|
|
@@ -3960,7 +3973,7 @@ function runEdit(opts) {
|
|
|
3960
3973
|
replaceWorkingOptions(previousOptions);
|
|
3961
3974
|
throw err;
|
|
3962
3975
|
}
|
|
3963
|
-
|
|
3976
|
+
renderWorkingGraphicAndHandles();
|
|
3964
3977
|
nonGeometryDirty = true;
|
|
3965
3978
|
dirty = true;
|
|
3966
3979
|
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, "programmatic", "Change options");
|
|
@@ -3986,7 +3999,7 @@ function runEdit(opts) {
|
|
|
3986
3999
|
currentStyle = previousStyle;
|
|
3987
4000
|
throw err;
|
|
3988
4001
|
}
|
|
3989
|
-
|
|
4002
|
+
renderWorkingGraphicAndHandles();
|
|
3990
4003
|
nonGeometryDirty = true;
|
|
3991
4004
|
dirty = true;
|
|
3992
4005
|
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, "programmatic", "Change style");
|
|
@@ -3996,7 +4009,7 @@ function runEdit(opts) {
|
|
|
3996
4009
|
* [[setOptions]] exactly (dirty tracking, re-render, change emission) —
|
|
3997
4010
|
* see that function for the shared shape. A merge value of `undefined` or
|
|
3998
4011
|
* an empty/whitespace-only string deletes the key; the record collapses to
|
|
3999
|
-
* `undefined` once empty so `
|
|
4012
|
+
* `undefined` once empty so `renderWorkingGraphic`/`rebuildRenderedWith` omit the
|
|
4000
4013
|
* key rather than persist `{}`.
|
|
4001
4014
|
*
|
|
4002
4015
|
* Each patch key resolves to its canonical field key and evicts every
|
|
@@ -4032,7 +4045,7 @@ function runEdit(opts) {
|
|
|
4032
4045
|
workingTextAmplifiers = previousTextAmplifiers;
|
|
4033
4046
|
throw err;
|
|
4034
4047
|
}
|
|
4035
|
-
|
|
4048
|
+
renderWorkingGraphicAndHandles();
|
|
4036
4049
|
nonGeometryDirty = true;
|
|
4037
4050
|
dirty = true;
|
|
4038
4051
|
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, "programmatic", "Change text");
|
|
@@ -4059,7 +4072,7 @@ function runEdit(opts) {
|
|
|
4059
4072
|
workingAmplifierPlacements = Object.keys(next).length > 0 ? next : void 0;
|
|
4060
4073
|
if (JSON.stringify(workingAmplifierPlacements) === JSON.stringify(previousAmplifierPlacements)) return;
|
|
4061
4074
|
const currentRendered = rebuildRendered();
|
|
4062
|
-
|
|
4075
|
+
renderWorkingGraphicAndHandles();
|
|
4063
4076
|
nonGeometryDirty = true;
|
|
4064
4077
|
dirty = true;
|
|
4065
4078
|
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, source, historyLabel);
|
|
@@ -4067,7 +4080,7 @@ function runEdit(opts) {
|
|
|
4067
4080
|
/**
|
|
4068
4081
|
* Flip the size anchor for the measure under edit (ADR-0020). Re-renders the
|
|
4069
4082
|
* emitted snapshot through `rebuildRendered` (which reads `sizeAnchor` live)
|
|
4070
|
-
* and emits a change so hosts see the re-anchored measure. The
|
|
4083
|
+
* and emits a change so hosts see the re-anchored measure. The working render
|
|
4071
4084
|
* is unaffected — it renders from `workingOptions`, which still carry the
|
|
4072
4085
|
* pixel size, so the in-flight symbol stays screen-locked until commit.
|
|
4073
4086
|
*/
|
|
@@ -4121,7 +4134,7 @@ function runEdit(opts) {
|
|
|
4121
4134
|
apply(from, to, source) {
|
|
4122
4135
|
const previousRendered = rebuildRendered();
|
|
4123
4136
|
restoreHistory(to);
|
|
4124
|
-
|
|
4137
|
+
renderWorkingGraphic();
|
|
4125
4138
|
renderHandlesLayer();
|
|
4126
4139
|
recomputeDirty();
|
|
4127
4140
|
emitChange(previousRendered, from.points, from.options, from.style, from.nonGeometryDirty, from.sizeAnchor, rebuildRendered(), from.textAmplifiers, from.amplifierPlacements, source, "Edit", from.boxAngle);
|
|
@@ -4197,7 +4210,7 @@ function runEdit(opts) {
|
|
|
4197
4210
|
workingTextAmplifiers = next.textAmplifiers ? { ...next.textAmplifiers } : void 0;
|
|
4198
4211
|
workingAmplifierPlacements = next.amplifierPlacements ? structuredClone(next.amplifierPlacements) : void 0;
|
|
4199
4212
|
nonGeometryDirty = true;
|
|
4200
|
-
|
|
4213
|
+
renderWorkingGraphic();
|
|
4201
4214
|
renderHandlesLayer();
|
|
4202
4215
|
recomputeDirty();
|
|
4203
4216
|
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");
|
|
@@ -4353,7 +4366,7 @@ function runEdit(opts) {
|
|
|
4353
4366
|
emitTransient("start", fsm.kind === "midpoint" ? initialPoints : preDragPoints, activeDrag.controlPointIndex);
|
|
4354
4367
|
if (fsm.kind === "midpoint") {
|
|
4355
4368
|
workingPoints.splice(0, workingPoints.length, ...initialPoints);
|
|
4356
|
-
|
|
4369
|
+
renderWorkingGraphic();
|
|
4357
4370
|
renderHandlesLayer();
|
|
4358
4371
|
}
|
|
4359
4372
|
dragInFlight = true;
|
|
@@ -4366,7 +4379,7 @@ function runEdit(opts) {
|
|
|
4366
4379
|
const preDragOptions = { ...workingOptions ?? {} };
|
|
4367
4380
|
if (boxHit.kind === "rotate") {
|
|
4368
4381
|
freezeOrientationForRotate();
|
|
4369
|
-
|
|
4382
|
+
renderWorkingGraphic();
|
|
4370
4383
|
renderHandlesLayer();
|
|
4371
4384
|
}
|
|
4372
4385
|
const preDragPoints = workingPoints.map((p) => [...p]);
|
|
@@ -4400,7 +4413,7 @@ function runEdit(opts) {
|
|
|
4400
4413
|
const resolvedPixel = snappedPixel(pixel, info, "reshape", activeDrag.controlPointIndex);
|
|
4401
4414
|
const snapshot = activeDrag.fsm.move(resolvedPixel);
|
|
4402
4415
|
workingPoints.splice(0, workingPoints.length, ...snapshot.points);
|
|
4403
|
-
|
|
4416
|
+
renderWorkingGraphic();
|
|
4404
4417
|
renderHandlesLayer();
|
|
4405
4418
|
recomputeDirty();
|
|
4406
4419
|
emitTransient("move", snapshot.points, activeDrag.controlPointIndex);
|
|
@@ -4433,7 +4446,7 @@ function runEdit(opts) {
|
|
|
4433
4446
|
workingPoints.splice(0, workingPoints.length, ...activeBoxDrag.preDragPoints);
|
|
4434
4447
|
workingOptions = foldTextTransform(activeBoxDrag.preDragBox, snapshot.box, activeBoxDrag.fsm.kind, activeBoxDrag.preDragOptions);
|
|
4435
4448
|
nonGeometryDirty = true;
|
|
4436
|
-
|
|
4449
|
+
renderWorkingGraphic();
|
|
4437
4450
|
renderHandlesLayer();
|
|
4438
4451
|
recomputeDirty();
|
|
4439
4452
|
emitTransient("move", activeBoxDrag.preDragPoints, null);
|
|
@@ -4441,7 +4454,7 @@ function runEdit(opts) {
|
|
|
4441
4454
|
}
|
|
4442
4455
|
workingPoints.splice(0, workingPoints.length, ...snapshot.points);
|
|
4443
4456
|
workingAmplifierPlacements = transformAmplifierPlacements(activeBoxDrag.preDragAmplifierPlacements, activeBoxDrag.preDragBox, snapshot.box, driver);
|
|
4444
|
-
|
|
4457
|
+
renderWorkingGraphic();
|
|
4445
4458
|
renderHandlesLayer();
|
|
4446
4459
|
recomputeDirty();
|
|
4447
4460
|
emitTransient("move", snapshot.points, null);
|
|
@@ -4456,7 +4469,7 @@ function runEdit(opts) {
|
|
|
4456
4469
|
...workingAmplifierPlacements ?? {},
|
|
4457
4470
|
...merged !== void 0 ? { [patch.field]: merged } : {}
|
|
4458
4471
|
};
|
|
4459
|
-
|
|
4472
|
+
renderWorkingGraphic();
|
|
4460
4473
|
renderHandlesLayer();
|
|
4461
4474
|
}
|
|
4462
4475
|
} else if (activeDrag) paintDragSnapshot(pixel, info);
|
|
@@ -4476,7 +4489,7 @@ function runEdit(opts) {
|
|
|
4476
4489
|
if (labelOutcome.kind === "rotate") setAmplifierPlacements({ [labelOutcome.key]: { rotation: labelOutcome.rotation } }, "gesture", "Rotate label");
|
|
4477
4490
|
else if (labelOutcome.kind === "move") setAmplifierPlacements({ [labelOutcome.field]: labelOutcome.position }, "gesture", "Move label");
|
|
4478
4491
|
else {
|
|
4479
|
-
|
|
4492
|
+
renderWorkingGraphic();
|
|
4480
4493
|
renderHandlesLayer();
|
|
4481
4494
|
}
|
|
4482
4495
|
shell.suppressNextClickAway();
|
|
@@ -4514,7 +4527,7 @@ function runEdit(opts) {
|
|
|
4514
4527
|
replaceWorkingPoints(preDragPoints);
|
|
4515
4528
|
workingAmplifierPlacements = preDragAmplifierPlacements;
|
|
4516
4529
|
emitTransient("end", preDragPoints, null);
|
|
4517
|
-
|
|
4530
|
+
renderWorkingGraphic();
|
|
4518
4531
|
renderHandlesLayer();
|
|
4519
4532
|
recomputeDirty();
|
|
4520
4533
|
endDragBookkeeping();
|
|
@@ -4544,11 +4557,7 @@ function runEdit(opts) {
|
|
|
4544
4557
|
const core = runEditInteraction({
|
|
4545
4558
|
adapter,
|
|
4546
4559
|
signal,
|
|
4547
|
-
layers: [
|
|
4548
|
-
previewLayer,
|
|
4549
|
-
guideLayer,
|
|
4550
|
-
handlesLayer
|
|
4551
|
-
],
|
|
4560
|
+
layers: [guideLayer, handlesLayer],
|
|
4552
4561
|
graphicsLayer,
|
|
4553
4562
|
previewLayer,
|
|
4554
4563
|
handlesLayer,
|
|
@@ -4582,11 +4591,7 @@ function runEdit(opts) {
|
|
|
4582
4591
|
ctx = interaction;
|
|
4583
4592
|
driver = createdDriver;
|
|
4584
4593
|
shell = createdShell;
|
|
4585
|
-
|
|
4586
|
-
if (wasOnGraphicsLayer) {
|
|
4587
|
-
adapter.deferLayerRemovalUntilLayerUpdate?.(graphicsLayer, previewLayer);
|
|
4588
|
-
liftOriginalFromGraphics();
|
|
4589
|
-
}
|
|
4594
|
+
renderWorkingGraphic();
|
|
4590
4595
|
renderHandlesLayer();
|
|
4591
4596
|
recomputeDirty();
|
|
4592
4597
|
if (onSession) onSession(session);
|
|
@@ -4597,6 +4602,7 @@ function runEdit(opts) {
|
|
|
4597
4602
|
get settled() {
|
|
4598
4603
|
return core.settled;
|
|
4599
4604
|
},
|
|
4605
|
+
usesGraphicStack: true,
|
|
4600
4606
|
measureId: measure.id,
|
|
4601
4607
|
abort(reason, cause) {
|
|
4602
4608
|
core.abort(reason, cause);
|
|
@@ -4605,7 +4611,7 @@ function runEdit(opts) {
|
|
|
4605
4611
|
if (core.settled) return;
|
|
4606
4612
|
if (stylesEqual(currentStyle, next.style)) return;
|
|
4607
4613
|
currentStyle = next.style;
|
|
4608
|
-
|
|
4614
|
+
renderWorkingGraphic();
|
|
4609
4615
|
}
|
|
4610
4616
|
};
|
|
4611
4617
|
}
|
|
@@ -4772,8 +4778,8 @@ function scalePointSymbolStates(pre, scale, rotation) {
|
|
|
4772
4778
|
* position, scales its size value, and rotates its rotation — its persisted
|
|
4773
4779
|
* size unit is never converted.
|
|
4774
4780
|
*
|
|
4775
|
-
* -
|
|
4776
|
-
* -
|
|
4781
|
+
* - replace each host-provided Graphic slot with its per-member working copy
|
|
4782
|
+
* - use the preview layer only for members that have no host-provided slot
|
|
4777
4783
|
* - group box = fit over the union of every member's rendered pixel footprint
|
|
4778
4784
|
* at the session `boxAngle`; each member also gets its own thin outline
|
|
4779
4785
|
* - dirty tracking, close/abort/signal/preempt/destroy, click-away — same
|
|
@@ -4782,7 +4788,7 @@ function scalePointSymbolStates(pre, scale, rotation) {
|
|
|
4782
4788
|
* a live session without a session boundary
|
|
4783
4789
|
*/
|
|
4784
4790
|
function runGroupTransform(opts) {
|
|
4785
|
-
const { adapter, capability, graphics, graphicsLayer, previewLayer, handlesLayer, graphicsStyle, signal, closeOnClickAway = true, interactionStyle, onSession, handleIdPrefix,
|
|
4791
|
+
const { adapter, capability, graphics, graphicsLayer, previewLayer, handlesLayer, graphicsStyle, signal, closeOnClickAway = true, interactionStyle, onSession, handleIdPrefix, publishWorkingGraphics, settleWorkingGraphics, isOnGraphicsLayer, onSettled, onClickAwayGraphicsPick } = opts;
|
|
4786
4792
|
assertCapabilityFor(graphics);
|
|
4787
4793
|
const renderOpts = graphicsStyle ? { graphicsStyle } : void 0;
|
|
4788
4794
|
let sizeAnchor = opts.sizeAnchor ?? "ground";
|
|
@@ -4808,7 +4814,8 @@ function runGroupTransform(opts) {
|
|
|
4808
4814
|
workingAmplifierPlacements: void 0,
|
|
4809
4815
|
optionsOverride: null,
|
|
4810
4816
|
box: null,
|
|
4811
|
-
lastBoxPixels: []
|
|
4817
|
+
lastBoxPixels: [],
|
|
4818
|
+
usesGraphicStack: isOnGraphicsLayer(graphic.id)
|
|
4812
4819
|
};
|
|
4813
4820
|
}
|
|
4814
4821
|
const editStartPoints = graphic.controlPoints.map((p) => [...p]);
|
|
@@ -4823,17 +4830,12 @@ function runGroupTransform(opts) {
|
|
|
4823
4830
|
workingAmplifierPlacements: graphic.amplifierPlacements ? structuredClone(graphic.amplifierPlacements) : void 0,
|
|
4824
4831
|
optionsOverride: null,
|
|
4825
4832
|
box: null,
|
|
4826
|
-
lastBoxPixels: []
|
|
4833
|
+
lastBoxPixels: [],
|
|
4834
|
+
usesGraphicStack: isOnGraphicsLayer(graphic.id)
|
|
4827
4835
|
};
|
|
4828
4836
|
}
|
|
4829
4837
|
/** The single mutable source of truth for the current member set. */
|
|
4830
4838
|
let members = graphics.map(createMemberState);
|
|
4831
|
-
/**
|
|
4832
|
-
* `true` once any member has been lifted off the graphics layer (initial or a
|
|
4833
|
-
* mid-session `setGraphics` add). Gates the abort-path restore so a session
|
|
4834
|
-
* that never touched the graphics layer doesn't rewrite it on abort.
|
|
4835
|
-
*/
|
|
4836
|
-
let anyLiftedFromGraphics = false;
|
|
4837
4839
|
/** A member's own options merged with an override patch (a null override leaves them unchanged). */
|
|
4838
4840
|
function mergeOptions(base, override) {
|
|
4839
4841
|
if (!override) return base;
|
|
@@ -4882,7 +4884,7 @@ function runGroupTransform(opts) {
|
|
|
4882
4884
|
render: renderMeasureForAdapter(adapter, committed, renderOpts)
|
|
4883
4885
|
};
|
|
4884
4886
|
}
|
|
4885
|
-
/** The working graphic for member `i` (measure or point symbol), for
|
|
4887
|
+
/** The working graphic for member `i` (measure or point symbol), for paint and `workingGraphics`. */
|
|
4886
4888
|
function workingGraphicFor(i) {
|
|
4887
4889
|
const member = members[i];
|
|
4888
4890
|
if (isPointSymbolMember(member)) return buildWorkingPointSymbol(member);
|
|
@@ -4937,20 +4939,6 @@ function runGroupTransform(opts) {
|
|
|
4937
4939
|
toPixel: (lonLat) => driver.toPixel(lonLat),
|
|
4938
4940
|
fromPixel: (pixel) => driver.fromPixel(pixel)
|
|
4939
4941
|
};
|
|
4940
|
-
/**
|
|
4941
|
-
* Lift `member` off the graphics layer when the host reports it currently
|
|
4942
|
-
* there, flipping `anyLiftedFromGraphics` so the abort path knows to restore.
|
|
4943
|
-
* Used for both initial and mid-session-added members.
|
|
4944
|
-
*/
|
|
4945
|
-
function liftMemberFromGraphics(member) {
|
|
4946
|
-
if (!isOnGraphicsLayer(member.graphic.id)) return;
|
|
4947
|
-
if (isPointSymbol(member.graphic)) adapter.removeFeatures(graphicsLayer, [`${member.graphic.id}:point-symbol`]);
|
|
4948
|
-
else {
|
|
4949
|
-
const ids = renderMeasureForAdapter(adapter, member.graphic, renderOpts).features.map((f) => f.id).filter((id) => typeof id === "string");
|
|
4950
|
-
if (ids.length > 0) adapter.removeFeatures(graphicsLayer, ids);
|
|
4951
|
-
}
|
|
4952
|
-
anyLiftedFromGraphics = true;
|
|
4953
|
-
}
|
|
4954
4942
|
function pixelsFromFeatures(features) {
|
|
4955
4943
|
const positions = [];
|
|
4956
4944
|
for (const f of features) if (f.geometry) collectFeaturePositions(f.geometry, positions);
|
|
@@ -5020,16 +5008,17 @@ function runGroupTransform(opts) {
|
|
|
5020
5008
|
}
|
|
5021
5009
|
function renderAllPreviews() {
|
|
5022
5010
|
if (ctx.settled) return;
|
|
5023
|
-
const
|
|
5024
|
-
const
|
|
5011
|
+
const previewGroups = [];
|
|
5012
|
+
const workingGroups = [];
|
|
5025
5013
|
members.forEach((member, i) => {
|
|
5026
5014
|
let features;
|
|
5015
|
+
let iconResources = [];
|
|
5027
5016
|
let boxPixels = [];
|
|
5028
5017
|
try {
|
|
5029
5018
|
if (isPointSymbolMember(member)) {
|
|
5030
5019
|
const symbol = buildWorkingPointSymbol(member);
|
|
5031
5020
|
const plan = renderPointSymbolForAdapter(adapter, capability, symbol);
|
|
5032
|
-
|
|
5021
|
+
iconResources = [plan.resource];
|
|
5033
5022
|
features = [plan.feature];
|
|
5034
5023
|
const footprint = pointSymbolFootprint(symbol, plan.render, adapter);
|
|
5035
5024
|
if (footprint) boxPixels = footprint.corners;
|
|
@@ -5043,10 +5032,19 @@ function runGroupTransform(opts) {
|
|
|
5043
5032
|
boxPixels = [];
|
|
5044
5033
|
}
|
|
5045
5034
|
member.lastBoxPixels = boxPixels;
|
|
5046
|
-
|
|
5035
|
+
if (member.usesGraphicStack) workingGroups.push({
|
|
5036
|
+
id: member.graphic.id,
|
|
5037
|
+
features,
|
|
5038
|
+
iconResources
|
|
5039
|
+
});
|
|
5040
|
+
else previewGroups.push({
|
|
5041
|
+
id: member.graphic.id,
|
|
5042
|
+
features,
|
|
5043
|
+
iconResources
|
|
5044
|
+
});
|
|
5047
5045
|
});
|
|
5048
|
-
|
|
5049
|
-
ctx.writeLayer(previewLayer,
|
|
5046
|
+
publishWorkingGraphics(workingGroups);
|
|
5047
|
+
ctx.writeLayer(previewLayer, previewGroups);
|
|
5050
5048
|
refitBoxes();
|
|
5051
5049
|
}
|
|
5052
5050
|
function rescaleOnViewChange() {
|
|
@@ -5067,7 +5065,11 @@ function runGroupTransform(opts) {
|
|
|
5067
5065
|
features.push(...toFeatures(boxToHandles(liveGroupBox, dragProjection.fromPixel), handleIdPrefix, interactionStyle));
|
|
5068
5066
|
}
|
|
5069
5067
|
features.push(...memberOutlineFeatures(members.flatMap((member) => member.box ? [[member.graphic.id, member.box]] : []), dragProjection.fromPixel, handleIdPrefix, interactionStyle?.boxOutline));
|
|
5070
|
-
ctx.writeLayer(handlesLayer,
|
|
5068
|
+
ctx.writeLayer(handlesLayer, [{
|
|
5069
|
+
id: "__td_transform_handles__",
|
|
5070
|
+
features,
|
|
5071
|
+
iconResources: []
|
|
5072
|
+
}]);
|
|
5071
5073
|
}
|
|
5072
5074
|
/**
|
|
5073
5075
|
* Builds every member's committed snapshot against a given override array
|
|
@@ -5300,13 +5302,13 @@ function runGroupTransform(opts) {
|
|
|
5300
5302
|
for (const graphic of next) {
|
|
5301
5303
|
const existing = currentById.get(graphic.id);
|
|
5302
5304
|
if (existing) nextMembers.push(existing);
|
|
5303
|
-
else
|
|
5304
|
-
const member = createMemberState(graphic);
|
|
5305
|
-
liftMemberFromGraphics(member);
|
|
5306
|
-
nextMembers.push(member);
|
|
5307
|
-
}
|
|
5305
|
+
else nextMembers.push(createMemberState(graphic));
|
|
5308
5306
|
}
|
|
5309
5307
|
members = nextMembers;
|
|
5308
|
+
if (removedSnapshots.length > 0) settleWorkingGraphics(removedSnapshots.map((snapshot) => ({
|
|
5309
|
+
id: snapshot.graphic.id,
|
|
5310
|
+
committed: snapshot.graphic
|
|
5311
|
+
})));
|
|
5310
5312
|
renderAllPreviews();
|
|
5311
5313
|
renderHandlesLayer();
|
|
5312
5314
|
recomputeDirty();
|
|
@@ -5325,6 +5327,28 @@ function runGroupTransform(opts) {
|
|
|
5325
5327
|
if (notificationErrors.length > 1) throw new AggregateError(notificationErrors, "TransformSession.setGraphics notification errors");
|
|
5326
5328
|
return removedSnapshots;
|
|
5327
5329
|
}
|
|
5330
|
+
function syncHostGraphics(hostGraphics) {
|
|
5331
|
+
if (ctx.settled) return;
|
|
5332
|
+
const hostIds = new Set(hostGraphics.map((graphic) => graphic.id));
|
|
5333
|
+
const withdrawnIds = members.filter((member) => !hostIds.has(member.graphic.id)).map((member) => member.graphic.id);
|
|
5334
|
+
const membershipChanged = withdrawnIds.length > 0;
|
|
5335
|
+
const surfaceChanged = members.some((member) => member.usesGraphicStack !== hostIds.has(member.graphic.id));
|
|
5336
|
+
if (!membershipChanged && !surfaceChanged) return;
|
|
5337
|
+
cancelActiveBoxDrag();
|
|
5338
|
+
if (withdrawnIds.length > 0) {
|
|
5339
|
+
settleWorkingGraphics(withdrawnIds.map((id) => ({ id })));
|
|
5340
|
+
members = members.filter((member) => hostIds.has(member.graphic.id));
|
|
5341
|
+
}
|
|
5342
|
+
for (const member of members) member.usesGraphicStack = true;
|
|
5343
|
+
if (members.length === 0) {
|
|
5344
|
+
ctx.fail(new TacticalDrawAbortError("removed"));
|
|
5345
|
+
return;
|
|
5346
|
+
}
|
|
5347
|
+
renderAllPreviews();
|
|
5348
|
+
renderHandlesLayer();
|
|
5349
|
+
recomputeDirty();
|
|
5350
|
+
if (membershipChanged) history.reset(captureHistory());
|
|
5351
|
+
}
|
|
5328
5352
|
function close() {
|
|
5329
5353
|
return commitClose();
|
|
5330
5354
|
}
|
|
@@ -5367,6 +5391,7 @@ function runGroupTransform(opts) {
|
|
|
5367
5391
|
setSizeAnchor(next);
|
|
5368
5392
|
},
|
|
5369
5393
|
setGraphics,
|
|
5394
|
+
syncHostGraphics,
|
|
5370
5395
|
close,
|
|
5371
5396
|
abort() {
|
|
5372
5397
|
if (ctx.settled) return;
|
|
@@ -5487,6 +5512,12 @@ function runGroupTransform(opts) {
|
|
|
5487
5512
|
handlesLayer,
|
|
5488
5513
|
closeOnClickAway,
|
|
5489
5514
|
onViewChange: rescaleOnViewChange,
|
|
5515
|
+
onBeforeTeardown: (settlement) => {
|
|
5516
|
+
settleWorkingGraphics(settlement.status === "committed" ? settlement.value.map((snapshot) => ({
|
|
5517
|
+
id: snapshot.graphic.id,
|
|
5518
|
+
committed: snapshot.graphic
|
|
5519
|
+
})) : members.map((member) => ({ id: member.graphic.id })));
|
|
5520
|
+
},
|
|
5490
5521
|
onDown: onDriverDown,
|
|
5491
5522
|
onMove: onDriverMove,
|
|
5492
5523
|
onUp: onDriverUp,
|
|
@@ -5506,16 +5537,11 @@ function runGroupTransform(opts) {
|
|
|
5506
5537
|
activeBoxDrag = null;
|
|
5507
5538
|
dragInFlight = false;
|
|
5508
5539
|
}
|
|
5509
|
-
adapter.reconcileLayerIconResources(previewLayer, []);
|
|
5510
|
-
},
|
|
5511
|
-
onAbort: () => {
|
|
5512
|
-
if (anyLiftedFromGraphics) restoreOriginalToGraphics();
|
|
5513
5540
|
},
|
|
5514
5541
|
attach(interaction, createdDriver, createdShell) {
|
|
5515
5542
|
ctx = interaction;
|
|
5516
5543
|
driver = createdDriver;
|
|
5517
5544
|
shell = createdShell;
|
|
5518
|
-
for (const member of members) liftMemberFromGraphics(member);
|
|
5519
5545
|
renderAllPreviews();
|
|
5520
5546
|
renderHandlesLayer();
|
|
5521
5547
|
recomputeDirty();
|
|
@@ -5532,7 +5558,8 @@ function runGroupTransform(opts) {
|
|
|
5532
5558
|
},
|
|
5533
5559
|
abort(reason, cause) {
|
|
5534
5560
|
core.abort(reason, cause);
|
|
5535
|
-
}
|
|
5561
|
+
},
|
|
5562
|
+
syncHostGraphics
|
|
5536
5563
|
};
|
|
5537
5564
|
}
|
|
5538
5565
|
//#endregion
|
|
@@ -5555,8 +5582,11 @@ function runPointSymbolDraw(options) {
|
|
|
5555
5582
|
const renderPreview = () => {
|
|
5556
5583
|
if (!latestPosition) return;
|
|
5557
5584
|
const plan = renderPointSymbolForAdapter(adapter, capability, build(PREVIEW_ID, latestPosition));
|
|
5558
|
-
|
|
5559
|
-
|
|
5585
|
+
ctx.writeLayer(previewLayer, [{
|
|
5586
|
+
id: PREVIEW_ID,
|
|
5587
|
+
features: [plan.feature],
|
|
5588
|
+
iconResources: [plan.resource]
|
|
5589
|
+
}]);
|
|
5560
5590
|
};
|
|
5561
5591
|
const handlePointerMove = (event) => {
|
|
5562
5592
|
latestPosition = snappedDrawPosition(snap, event);
|
|
@@ -5583,7 +5613,6 @@ function runPointSymbolDraw(options) {
|
|
|
5583
5613
|
interaction.onTeardown(() => {
|
|
5584
5614
|
adapter.off("pointermove", handlePointerMove);
|
|
5585
5615
|
adapter.off("click", handleClick);
|
|
5586
|
-
adapter.reconcileLayerIconResources(previewLayer, []);
|
|
5587
5616
|
adapter.setCursor(savedCursor);
|
|
5588
5617
|
});
|
|
5589
5618
|
adapter.setCursor("crosshair");
|
|
@@ -5600,8 +5629,8 @@ function normalisePointModes(input) {
|
|
|
5600
5629
|
for (const mode of modes) if (mode !== "transform" && mode !== "delete") throw new TypeError(`Point-symbol editing does not support mode "${mode}"`);
|
|
5601
5630
|
return modes.includes("delete") ? ["delete"] : [...new Set(modes)];
|
|
5602
5631
|
}
|
|
5603
|
-
function symbolBox(adapter,
|
|
5604
|
-
const footprint = pointSymbolFootprint(symbol,
|
|
5632
|
+
function symbolBox(adapter, symbol, rendered, padding = 0) {
|
|
5633
|
+
const footprint = pointSymbolFootprint(symbol, rendered, adapter);
|
|
5605
5634
|
if (!footprint) return null;
|
|
5606
5635
|
return {
|
|
5607
5636
|
...footprint.box,
|
|
@@ -5610,8 +5639,9 @@ function symbolBox(adapter, capability, symbol, padding = 0) {
|
|
|
5610
5639
|
};
|
|
5611
5640
|
}
|
|
5612
5641
|
function runPointSymbolEdit(opts) {
|
|
5613
|
-
const { adapter, capability, symbol, graphicsLayer, previewLayer, handlesLayer, signal, closeOnClickAway = true, interactionStyle, onSession, handleIdPrefix, settleToGraphics, wasOnGraphicsLayer, onSettled, onClickAwayGraphicsPick, snap } = opts;
|
|
5642
|
+
const { adapter, capability, symbol, graphicsLayer, previewLayer, handlesLayer, signal, closeOnClickAway = true, interactionStyle, onSession, handleIdPrefix, settleToGraphics, publishWorkingGraphic, wasOnGraphicsLayer, onSettled, onClickAwayGraphicsPick, snap } = opts;
|
|
5614
5643
|
let modes = normalisePointModes(opts.modes);
|
|
5644
|
+
let usesGraphicStack = wasOnGraphicsLayer;
|
|
5615
5645
|
let working = clonePointSymbol(symbol);
|
|
5616
5646
|
let dirty = false;
|
|
5617
5647
|
let box = null;
|
|
@@ -5636,15 +5666,29 @@ function runPointSymbolEdit(opts) {
|
|
|
5636
5666
|
const render = () => {
|
|
5637
5667
|
if (ctx.settled) return;
|
|
5638
5668
|
const plan = renderPointSymbolForAdapter(adapter, capability, working);
|
|
5639
|
-
adapter
|
|
5640
|
-
ctx.writeLayer(previewLayer, [plan.feature]);
|
|
5641
|
-
box = symbolBox(adapter, capability, working, interactionStyle?.boxPadding);
|
|
5669
|
+
const nextBox = symbolBox(adapter, working, plan.render, interactionStyle?.boxPadding);
|
|
5642
5670
|
const handles = [];
|
|
5643
|
-
if (modes.includes("transform") &&
|
|
5644
|
-
handles.push(boxOutlineFeature(
|
|
5645
|
-
handles.push(...toFeatures(boxToHandles(
|
|
5646
|
-
}
|
|
5647
|
-
|
|
5671
|
+
if (modes.includes("transform") && nextBox) {
|
|
5672
|
+
handles.push(boxOutlineFeature(nextBox, (pixel) => driver.fromPixel(pixel), `${handleIdPrefix}:box-outline`, interactionStyle?.boxOutline));
|
|
5673
|
+
handles.push(...toFeatures(boxToHandles(nextBox, (pixel) => driver.fromPixel(pixel)), handleIdPrefix, interactionStyle));
|
|
5674
|
+
}
|
|
5675
|
+
publishWorkingGraphic({
|
|
5676
|
+
id: working.id,
|
|
5677
|
+
features: [plan.feature],
|
|
5678
|
+
iconResources: [plan.resource]
|
|
5679
|
+
});
|
|
5680
|
+
if (usesGraphicStack) ctx.writeLayer(previewLayer, []);
|
|
5681
|
+
else ctx.writeLayer(previewLayer, [{
|
|
5682
|
+
id: `__td_point_symbol_edit_preview__:${working.id}`,
|
|
5683
|
+
features: [plan.feature],
|
|
5684
|
+
iconResources: [plan.resource]
|
|
5685
|
+
}]);
|
|
5686
|
+
box = nextBox;
|
|
5687
|
+
ctx.writeLayer(handlesLayer, [{
|
|
5688
|
+
id: `__td_point_symbol_edit_handles__:${working.id}`,
|
|
5689
|
+
features: handles,
|
|
5690
|
+
iconResources: []
|
|
5691
|
+
}]);
|
|
5648
5692
|
};
|
|
5649
5693
|
const emitChange = (previousState, source, onAccepted) => {
|
|
5650
5694
|
const previous = {
|
|
@@ -5809,16 +5853,11 @@ function runPointSymbolEdit(opts) {
|
|
|
5809
5853
|
historySettle: () => history.settle(),
|
|
5810
5854
|
facadeOnSettled: onSettled,
|
|
5811
5855
|
emitSettle: (settlement) => settleFromInteraction(settlement, commitEmitter, settleEmitter),
|
|
5812
|
-
extraTeardown: () => adapter.reconcileLayerIconResources(previewLayer, []),
|
|
5813
5856
|
attach(interaction, createdDriver, createdShell) {
|
|
5814
5857
|
ctx = interaction;
|
|
5815
5858
|
driver = createdDriver;
|
|
5816
5859
|
shell = createdShell;
|
|
5817
5860
|
render();
|
|
5818
|
-
if (wasOnGraphicsLayer) {
|
|
5819
|
-
adapter.deferLayerRemovalUntilLayerUpdate?.(graphicsLayer, previewLayer);
|
|
5820
|
-
adapter.removeFeatures(graphicsLayer, [`${symbol.id}:point-symbol`]);
|
|
5821
|
-
}
|
|
5822
5861
|
onSession?.(session);
|
|
5823
5862
|
}
|
|
5824
5863
|
});
|
|
@@ -5827,11 +5866,55 @@ function runPointSymbolEdit(opts) {
|
|
|
5827
5866
|
get settled() {
|
|
5828
5867
|
return handle.settled;
|
|
5829
5868
|
},
|
|
5869
|
+
get usesGraphicStack() {
|
|
5870
|
+
return usesGraphicStack;
|
|
5871
|
+
},
|
|
5830
5872
|
measureId: symbol.id,
|
|
5831
|
-
abort: handle.abort
|
|
5873
|
+
abort: handle.abort,
|
|
5874
|
+
syncHostMembership(present) {
|
|
5875
|
+
if (handle.settled || usesGraphicStack === present) return;
|
|
5876
|
+
const previous = usesGraphicStack;
|
|
5877
|
+
usesGraphicStack = present;
|
|
5878
|
+
try {
|
|
5879
|
+
render();
|
|
5880
|
+
} catch (error) {
|
|
5881
|
+
usesGraphicStack = previous;
|
|
5882
|
+
throw error;
|
|
5883
|
+
}
|
|
5884
|
+
}
|
|
5832
5885
|
};
|
|
5833
5886
|
}
|
|
5834
5887
|
//#endregion
|
|
5888
|
+
//#region src/tactical-draw/graphic-stack-coordinator.ts
|
|
5889
|
+
/**
|
|
5890
|
+
* Combines host-owned Graphic order with live or settled render overrides.
|
|
5891
|
+
*
|
|
5892
|
+
* The host always owns membership and order. A working override only replaces
|
|
5893
|
+
* the contents of its matching host slot; a committed override survives until
|
|
5894
|
+
* the host next acknowledges that Graphic in `render()`.
|
|
5895
|
+
*/
|
|
5896
|
+
var GraphicStackCoordinator = class {
|
|
5897
|
+
overrides = /* @__PURE__ */ new Map();
|
|
5898
|
+
acknowledgeHostRender(graphics) {
|
|
5899
|
+
for (const graphic of graphics) if (this.overrides.get(graphic.id)?.settled) this.overrides.delete(graphic.id);
|
|
5900
|
+
}
|
|
5901
|
+
publishWorking(group) {
|
|
5902
|
+
this.overrides.set(group.id, {
|
|
5903
|
+
group,
|
|
5904
|
+
settled: false
|
|
5905
|
+
});
|
|
5906
|
+
}
|
|
5907
|
+
settle(graphicId, committed) {
|
|
5908
|
+
const override = this.overrides.get(graphicId);
|
|
5909
|
+
if (!override) return;
|
|
5910
|
+
if (committed) override.settled = true;
|
|
5911
|
+
else this.overrides.delete(graphicId);
|
|
5912
|
+
}
|
|
5913
|
+
desiredGroups(graphics, renderHostGraphic) {
|
|
5914
|
+
return graphics.map((graphic) => this.overrides.get(graphic.id)?.group ?? renderHostGraphic(graphic));
|
|
5915
|
+
}
|
|
5916
|
+
};
|
|
5917
|
+
//#endregion
|
|
5835
5918
|
//#region src/tactical-draw/tactical-draw.ts
|
|
5836
5919
|
/**
|
|
5837
5920
|
* `TacticalDraw` façade — slice #6 (issue #45).
|
|
@@ -5854,6 +5937,7 @@ var TacticalDraw = class {
|
|
|
5854
5937
|
pointSymbols;
|
|
5855
5938
|
snappingOptions;
|
|
5856
5939
|
resolveSnap;
|
|
5940
|
+
graphicStack = new GraphicStackCoordinator();
|
|
5857
5941
|
destroyed = false;
|
|
5858
5942
|
hasRendered = false;
|
|
5859
5943
|
activeInteraction = new ActiveInteractionTracker();
|
|
@@ -5897,12 +5981,7 @@ var TacticalDraw = class {
|
|
|
5897
5981
|
idCounter = 0;
|
|
5898
5982
|
editCounter = 0;
|
|
5899
5983
|
transformCounter = 0;
|
|
5900
|
-
/**
|
|
5901
|
-
* Snapshot of the host's last `render()` argument. The edit slice uses it
|
|
5902
|
-
* to recompute the graphics-layer state during a lift / restore — the
|
|
5903
|
-
* façade owns the policy because controllers shouldn't need to know the
|
|
5904
|
-
* surrounding collection state.
|
|
5905
|
-
*/
|
|
5984
|
+
/** Snapshot of the host's latest authoritative Graphic membership and order. */
|
|
5906
5985
|
lastRenderedMeasures = [];
|
|
5907
5986
|
snapGeometryFeatures = [];
|
|
5908
5987
|
/**
|
|
@@ -5952,53 +6031,65 @@ var TacticalDraw = class {
|
|
|
5952
6031
|
clearSnapResult() {
|
|
5953
6032
|
this.snappingOptions?.onSnap?.(null);
|
|
5954
6033
|
}
|
|
5955
|
-
/**
|
|
5956
|
-
|
|
5957
|
-
* Routes every measure through [[renderMeasureForAdapter]] so pixel sizing is
|
|
5958
|
-
* resolved against the live map. Callers own any edit-exclusion policy — see
|
|
5959
|
-
* `render()` (excludes the edited measure, owned by the preview layer) versus
|
|
5960
|
-
* `settleToGraphics` (no exclusion, the edit is ending).
|
|
5961
|
-
*/
|
|
5962
|
-
renderMeasuresToGraphics(graphics) {
|
|
5963
|
-
const features = [];
|
|
5964
|
-
const resources = /* @__PURE__ */ new Map();
|
|
6034
|
+
/** Render one atomic Graphic group for the engine-neutral graphics stack. */
|
|
6035
|
+
renderGraphicGroup(graphic) {
|
|
5965
6036
|
const renderOpts = { graphicsStyle: this.graphicsStyle };
|
|
5966
|
-
|
|
6037
|
+
if (isPointSymbol(graphic)) {
|
|
5967
6038
|
if (!this.pointSymbols) throw new PointSymbolCapabilityRequiredError();
|
|
5968
6039
|
const plan = renderPointSymbolForAdapter(this.adapter, this.pointSymbols, graphic);
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
6040
|
+
return {
|
|
6041
|
+
id: graphic.id,
|
|
6042
|
+
features: [plan.feature],
|
|
6043
|
+
iconResources: [plan.resource]
|
|
6044
|
+
};
|
|
6045
|
+
}
|
|
6046
|
+
const render = renderMeasureForAdapter(this.adapter, graphic, renderOpts);
|
|
6047
|
+
return {
|
|
6048
|
+
id: graphic.id,
|
|
6049
|
+
features: render.features,
|
|
6050
|
+
iconResources: []
|
|
6051
|
+
};
|
|
6052
|
+
}
|
|
6053
|
+
/** Submit one complete grouped desired state for the host stack plus overrides. */
|
|
6054
|
+
reconcileGraphicsStack(graphics = this.lastRenderedMeasures) {
|
|
6055
|
+
const groups = this.graphicStack.desiredGroups(graphics, (graphic) => this.renderGraphicGroup(graphic));
|
|
6056
|
+
this.adapter.setLayerFeatureGroups(this.graphicsLayer, groups);
|
|
6057
|
+
this.snapGeometryFeatures = groups.flatMap((group) => group.features);
|
|
6058
|
+
}
|
|
6059
|
+
/** Atomically publish several live renders into their host-owned Graphic slots. */
|
|
6060
|
+
publishWorkingGraphics(groups) {
|
|
6061
|
+
for (const group of groups) this.graphicStack.publishWorking(group);
|
|
6062
|
+
this.reconcileGraphicsStack();
|
|
6063
|
+
}
|
|
6064
|
+
/** Settle several stack overrides in one graphics-layer reconciliation. */
|
|
6065
|
+
settleWorkingGraphics(settlements) {
|
|
6066
|
+
for (const { id, committed } of settlements) {
|
|
6067
|
+
if (committed) this.graphicStack.publishWorking(this.renderGraphicGroup(committed));
|
|
6068
|
+
this.graphicStack.settle(id, committed !== void 0);
|
|
5974
6069
|
}
|
|
5975
|
-
this.
|
|
5976
|
-
this.adapter.updateLayerFeatures(this.graphicsLayer, features);
|
|
5977
|
-
this.snapGeometryFeatures = features;
|
|
6070
|
+
this.reconcileGraphicsStack();
|
|
5978
6071
|
}
|
|
5979
6072
|
/**
|
|
5980
6073
|
* Re-render the graphics layer on a view (zoom) change so pixel-denominated
|
|
5981
6074
|
* and not-yet-committed label-sized measures rescale live. Skipped entirely
|
|
5982
6075
|
* when nothing rendered still reads live view state — meter geometry with a
|
|
5983
6076
|
* captured label size is zoom-independent, so a rebuild would be wasted
|
|
5984
|
-
* layer writes.
|
|
5985
|
-
*
|
|
5986
|
-
*
|
|
6077
|
+
* layer writes. A single-control-measure edit publishes through the stack
|
|
6078
|
+
* coordinator; point symbols and transform members already present in the
|
|
6079
|
+
* host stack do the same. Graphics without a host slot remain on preview.
|
|
5987
6080
|
*/
|
|
5988
6081
|
handleViewChange = () => {
|
|
5989
6082
|
if (this.destroyed || !this.hasRendered) return;
|
|
5990
6083
|
this.renderHighlights();
|
|
5991
|
-
const excluded = this.
|
|
6084
|
+
const excluded = this.graphicsStackExcludedIds();
|
|
5992
6085
|
const visible = excluded.size > 0 ? this.lastRenderedMeasures.filter((m) => !excluded.has(m.id)) : this.lastRenderedMeasures;
|
|
5993
6086
|
const nativeGroundIconScaling = this.adapter.scalesGroundPointSymbolsNatively === true;
|
|
5994
6087
|
if (!visible.some((g) => graphicRendersFromViewState(g, nativeGroundIconScaling))) return;
|
|
5995
|
-
this.
|
|
6088
|
+
this.reconcileGraphicsStack(visible);
|
|
5996
6089
|
};
|
|
5997
6090
|
/**
|
|
5998
|
-
* Every
|
|
5999
|
-
*
|
|
6000
|
-
* reconciliation because the preview layer (not graphics) owns their
|
|
6001
|
-
* working copy. Empty while idle.
|
|
6091
|
+
* Every Graphic id owned by an active interaction. Used for highlight and
|
|
6092
|
+
* pick routing; not every such Graphic is excluded from stack reconciliation.
|
|
6002
6093
|
*/
|
|
6003
6094
|
excludedInteractionIds() {
|
|
6004
6095
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -6006,6 +6097,13 @@ var TacticalDraw = class {
|
|
|
6006
6097
|
for (const id of this.transformMemberIds() ?? []) ids.add(id);
|
|
6007
6098
|
return ids;
|
|
6008
6099
|
}
|
|
6100
|
+
/** Interaction ids still rendered on the legacy preview surface. */
|
|
6101
|
+
graphicsStackExcludedIds() {
|
|
6102
|
+
const ids = this.excludedInteractionIds();
|
|
6103
|
+
if (this.activeEdit?.usesGraphicStack) ids.delete(this.activeEdit.measureId);
|
|
6104
|
+
for (const id of this.transformMemberIds() ?? []) ids.delete(id);
|
|
6105
|
+
return ids;
|
|
6106
|
+
}
|
|
6009
6107
|
/**
|
|
6010
6108
|
* Live member ids of the active group transform, or `undefined` when none.
|
|
6011
6109
|
* Falls back to the session ref because `onSession` fires from inside the
|
|
@@ -6024,24 +6122,24 @@ var TacticalDraw = class {
|
|
|
6024
6122
|
* Idempotent. Throws synchronously on duplicate measure ids *before*
|
|
6025
6123
|
* mutating any layer. An empty list before any prior render is a no-op.
|
|
6026
6124
|
*
|
|
6027
|
-
* Cross-layer semantics with an active edit
|
|
6028
|
-
* - The
|
|
6029
|
-
*
|
|
6030
|
-
* must stay in place.
|
|
6125
|
+
* Cross-layer semantics with an active edit:
|
|
6126
|
+
* - The stack coordinator replaces the edited Graphic's host-owned slot
|
|
6127
|
+
* with its working render; membership and order remain host-authoritative.
|
|
6031
6128
|
* - If the edited measure is absent from `measures`, the pending `edit`
|
|
6032
6129
|
* promise rejects on the **next microtask** with reason `"removed"`.
|
|
6033
6130
|
* `render` itself returns normally so host collection ops stay synchronous.
|
|
6034
|
-
* - If the edited measure is present and its `style` changed,
|
|
6035
|
-
* picks up the new style on this render. `controlPoints` changes on the
|
|
6131
|
+
* - If the edited measure is present and its `style` changed, its working
|
|
6132
|
+
* group picks up the new style on this render. `controlPoints` changes on the
|
|
6036
6133
|
* edited measure are routed nowhere — the user's in-flight drag is never
|
|
6037
|
-
* yanked out from under them.
|
|
6134
|
+
* yanked out from under them. A point symbol's complete working group remains
|
|
6135
|
+
* authoritative until settle, including any changed icon resource.
|
|
6038
6136
|
*
|
|
6039
6137
|
* Cross-layer semantics with an active draw: `render` only touches the
|
|
6040
6138
|
* graphics layer, so the draw's preview is undisturbed.
|
|
6041
6139
|
*
|
|
6042
6140
|
* Reentrancy: calling `td.render(...)` from inside `EditSession.onChange` is
|
|
6043
|
-
* safe — `render` never re-enters the active edit or its
|
|
6044
|
-
*
|
|
6141
|
+
* safe — `render` never re-enters the active edit or its handle layer (the
|
|
6142
|
+
* only edit-facing side effect, `notifyExternalMeasure`, is a
|
|
6045
6143
|
* pure style replay).
|
|
6046
6144
|
*/
|
|
6047
6145
|
render(measures) {
|
|
@@ -6051,39 +6149,36 @@ var TacticalDraw = class {
|
|
|
6051
6149
|
if (seen.has(m.id)) throw new Error(`TacticalDraw.render: duplicate graphic id "${m.id}"`);
|
|
6052
6150
|
seen.add(m.id);
|
|
6053
6151
|
}
|
|
6054
|
-
const
|
|
6152
|
+
const activeEdit = this.activeEdit;
|
|
6153
|
+
const constructingSession = activeEdit ? null : this.activeInteraction.session;
|
|
6154
|
+
const constructingGraphic = constructingSession && "workingGraphic" in constructingSession ? constructingSession.workingGraphic : void 0;
|
|
6155
|
+
const editedId = activeEdit?.measureId ?? constructingGraphic?.id;
|
|
6055
6156
|
let externalEdited;
|
|
6056
6157
|
let editedRemoved = false;
|
|
6057
6158
|
if (editedId !== void 0) {
|
|
6058
6159
|
const candidate = measures.find((m) => m.id === editedId);
|
|
6059
|
-
|
|
6060
|
-
|
|
6160
|
+
if (activeEdit && "notifyExternalMeasure" in activeEdit || constructingGraphic !== void 0 && !isPointSymbol(constructingGraphic)) {
|
|
6161
|
+
externalEdited = candidate && !isPointSymbol(candidate) ? candidate : void 0;
|
|
6162
|
+
editedRemoved = externalEdited === void 0;
|
|
6163
|
+
} else editedRemoved = candidate === void 0 || !isPointSymbol(candidate);
|
|
6061
6164
|
}
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6165
|
+
this.lastRenderedMeasures = measures.slice();
|
|
6166
|
+
this.graphicStack.acknowledgeHostRender(measures);
|
|
6167
|
+
if (activeEdit && "syncHostMembership" in activeEdit && measures.some((graphic) => graphic.id === activeEdit.measureId && isPointSymbol(graphic))) activeEdit.syncHostMembership(true);
|
|
6168
|
+
(this.activeTransform ?? this.activeTransformSessionRef)?.syncHostGraphics(measures);
|
|
6169
|
+
const excluded = this.graphicsStackExcludedIds();
|
|
6065
6170
|
const visible = excluded.size > 0 ? measures.filter((m) => !excluded.has(m.id)) : measures;
|
|
6066
6171
|
if (!(visible.length === 0 && !this.hasRendered)) {
|
|
6067
|
-
this.
|
|
6172
|
+
this.reconcileGraphicsStack(visible);
|
|
6068
6173
|
this.hasRendered = true;
|
|
6069
6174
|
}
|
|
6070
|
-
this.lastRenderedMeasures = measures.slice();
|
|
6071
6175
|
if (externalEdited && this.activeEdit && "notifyExternalMeasure" in this.activeEdit) this.activeEdit.notifyExternalMeasure(externalEdited);
|
|
6072
|
-
if (editedRemoved
|
|
6176
|
+
if (editedRemoved) {
|
|
6073
6177
|
const editHandle = this.activeEdit;
|
|
6074
|
-
queueMicrotask(() => {
|
|
6178
|
+
if (editHandle) queueMicrotask(() => {
|
|
6075
6179
|
editHandle.abort("removed");
|
|
6076
6180
|
});
|
|
6077
|
-
|
|
6078
|
-
if (transformMemberRemoved) {
|
|
6079
|
-
const sessionRef = this.activeTransformSessionRef;
|
|
6080
|
-
queueMicrotask(() => {
|
|
6081
|
-
const handle = this.activeTransform;
|
|
6082
|
-
if (!handle || handle.settled) return;
|
|
6083
|
-
if (this.activeTransformSessionRef !== sessionRef) return;
|
|
6084
|
-
const rendered = new Set(this.lastRenderedMeasures.map((m) => m.id));
|
|
6085
|
-
if (handle.graphicIds.some((id) => !rendered.has(id))) handle.abort("removed");
|
|
6086
|
-
});
|
|
6181
|
+
else if (this.activeInteraction.isStarting) this.activeInteraction.cancel("removed");
|
|
6087
6182
|
}
|
|
6088
6183
|
this.renderHighlights();
|
|
6089
6184
|
}
|
|
@@ -6092,7 +6187,7 @@ var TacticalDraw = class {
|
|
|
6092
6187
|
* idempotent (mirrors {@link render}): a non-interactive outline box is drawn
|
|
6093
6188
|
* around each still-rendered graphic on the dedicated highlight layer — no
|
|
6094
6189
|
* draw / edit / transform session, no promise, no pick subscription. Passing
|
|
6095
|
-
* `[]` clears every highlight. Unknown ids and ids currently
|
|
6190
|
+
* `[]` clears every highlight. Unknown ids and ids currently active in an
|
|
6096
6191
|
* active edit / transform session are skipped (the live session's own visuals
|
|
6097
6192
|
* supersede the passive outline). Hosts use it for Excalidraw-style
|
|
6098
6193
|
* "locked selection" chrome.
|
|
@@ -6105,9 +6200,9 @@ var TacticalDraw = class {
|
|
|
6105
6200
|
/**
|
|
6106
6201
|
* Project the current {@link highlightedIds} into one outline `LineString`
|
|
6107
6202
|
* per graphic and reconcile them onto the highlight layer. Idempotent and
|
|
6108
|
-
*
|
|
6109
|
-
*
|
|
6110
|
-
* unknown (not in the last render) or
|
|
6203
|
+
* grouped by graphic id, so desired-state replacement removes stale outlines
|
|
6204
|
+
* and an unchanged set is a cheap no-op write. Skips ids that are
|
|
6205
|
+
* unknown (not in the last render) or active in an interaction session, and
|
|
6111
6206
|
* bails gracefully when the adapter cannot invert pixels to coordinates
|
|
6112
6207
|
* (`getCoordinateFromPixel` is optional in the MapAdapter ABI).
|
|
6113
6208
|
*/
|
|
@@ -6140,7 +6235,11 @@ var TacticalDraw = class {
|
|
|
6140
6235
|
const feature = boxOutlineFeature(box, invert, `__td_highlight__:${id}`, style);
|
|
6141
6236
|
if (!invertFailed) features.push(feature);
|
|
6142
6237
|
}
|
|
6143
|
-
this.adapter.
|
|
6238
|
+
this.adapter.setLayerFeatureGroups(this.highlightLayer, features.map((feature) => ({
|
|
6239
|
+
id: String(feature.id),
|
|
6240
|
+
features: [feature],
|
|
6241
|
+
iconResources: []
|
|
6242
|
+
})));
|
|
6144
6243
|
this.hasHighlightOutlines = features.length > 0;
|
|
6145
6244
|
}
|
|
6146
6245
|
/**
|
|
@@ -6342,10 +6441,10 @@ var TacticalDraw = class {
|
|
|
6342
6441
|
if (preempt) return preempt;
|
|
6343
6442
|
const wasOnGraphicsLayer = this.lastRenderedMeasures.some((m) => m.id === measure.id);
|
|
6344
6443
|
const settleToGraphics = (settledSymbol) => {
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6444
|
+
this.settleWorkingGraphics([{
|
|
6445
|
+
id: measure.id,
|
|
6446
|
+
committed: settledSymbol
|
|
6447
|
+
}]);
|
|
6349
6448
|
};
|
|
6350
6449
|
const handleIdPrefix = `__td_edit_${++this.editCounter}__`;
|
|
6351
6450
|
return this.runWithSlot((onSettled) => runPointSymbolEdit({
|
|
@@ -6359,14 +6458,16 @@ var TacticalDraw = class {
|
|
|
6359
6458
|
closeOnClickAway: options?.closeOnClickAway,
|
|
6360
6459
|
modes: options?.modes,
|
|
6361
6460
|
interactionStyle: this.resolveInteractionStyle(options?.interactionStyle),
|
|
6362
|
-
onSession: options?.onSession,
|
|
6461
|
+
onSession: this.trackSession(options?.onSession),
|
|
6363
6462
|
handleIdPrefix,
|
|
6364
6463
|
settleToGraphics,
|
|
6464
|
+
publishWorkingGraphic: (group) => this.publishWorkingGraphics([group]),
|
|
6365
6465
|
wasOnGraphicsLayer,
|
|
6366
6466
|
onSettled,
|
|
6367
6467
|
onClickAwayGraphicsPick: (event) => this.dispatchHostPick(this.graphicsLayer, event),
|
|
6368
6468
|
snap: this.resolveSnap
|
|
6369
6469
|
}), (handle) => {
|
|
6470
|
+
handle.syncHostMembership(this.lastRenderedMeasures.some((graphic) => graphic.id === handle.measureId && isPointSymbol(graphic)));
|
|
6370
6471
|
this.activeEdit = handle;
|
|
6371
6472
|
return () => {
|
|
6372
6473
|
if (this.activeEdit === handle) this.activeEdit = null;
|
|
@@ -6376,12 +6477,11 @@ var TacticalDraw = class {
|
|
|
6376
6477
|
normaliseModes(options?.modes);
|
|
6377
6478
|
const preempt = this.guardAndPreempt();
|
|
6378
6479
|
if (preempt) return preempt;
|
|
6379
|
-
const wasOnGraphicsLayer = this.lastRenderedMeasures.some((m) => m.id === measure.id);
|
|
6380
6480
|
const settleToGraphics = (settledMeasure) => {
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6481
|
+
this.settleWorkingGraphics([{
|
|
6482
|
+
id: measure.id,
|
|
6483
|
+
committed: settledMeasure
|
|
6484
|
+
}]);
|
|
6385
6485
|
};
|
|
6386
6486
|
const handleIdPrefix = `__td_edit_${++this.editCounter}__`;
|
|
6387
6487
|
const mergedInteractionStyle = this.resolveInteractionStyle(options?.interactionStyle);
|
|
@@ -6406,7 +6506,11 @@ var TacticalDraw = class {
|
|
|
6406
6506
|
guide: options?.guide,
|
|
6407
6507
|
sizeAnchor: options?.sizeAnchor,
|
|
6408
6508
|
settleToGraphics,
|
|
6409
|
-
|
|
6509
|
+
publishWorkingGraphic: (graphic, features) => this.publishWorkingGraphics([{
|
|
6510
|
+
id: graphic.id,
|
|
6511
|
+
features,
|
|
6512
|
+
iconResources: []
|
|
6513
|
+
}]),
|
|
6410
6514
|
onSettled,
|
|
6411
6515
|
onClickAwayGraphicsPick,
|
|
6412
6516
|
snap: this.resolveSnap
|
|
@@ -6429,9 +6533,6 @@ var TacticalDraw = class {
|
|
|
6429
6533
|
const preempt = this.guardAndPreempt();
|
|
6430
6534
|
if (preempt) return preempt;
|
|
6431
6535
|
const isOnGraphicsLayer = (id) => this.lastRenderedMeasures.some((m) => m.id === id);
|
|
6432
|
-
const restoreOriginalToGraphics = () => {
|
|
6433
|
-
this.renderMeasuresToGraphics(this.lastRenderedMeasures);
|
|
6434
|
-
};
|
|
6435
6536
|
const handleIdPrefix = `__td_transform_${++this.transformCounter}__`;
|
|
6436
6537
|
const mergedInteractionStyle = this.resolveInteractionStyle(options?.interactionStyle);
|
|
6437
6538
|
const onClickAwayGraphicsPick = (event) => {
|
|
@@ -6454,7 +6555,8 @@ var TacticalDraw = class {
|
|
|
6454
6555
|
onSession: this.trackTransformSession(options?.onSession),
|
|
6455
6556
|
handleIdPrefix,
|
|
6456
6557
|
sizeAnchor: options?.sizeAnchor,
|
|
6457
|
-
|
|
6558
|
+
publishWorkingGraphics: (groups) => this.publishWorkingGraphics(groups),
|
|
6559
|
+
settleWorkingGraphics: (settlements) => this.settleWorkingGraphics(settlements),
|
|
6458
6560
|
isOnGraphicsLayer,
|
|
6459
6561
|
onSettled,
|
|
6460
6562
|
onClickAwayGraphicsPick
|
|
@@ -6736,7 +6838,8 @@ var TacticalDraw = class {
|
|
|
6736
6838
|
*/
|
|
6737
6839
|
routePick(layerId, event) {
|
|
6738
6840
|
const excluded = this.excludedInteractionIds();
|
|
6739
|
-
if (layerId === this.graphicsLayer && excluded.size > 0
|
|
6841
|
+
if (layerId === this.graphicsLayer && excluded.size > 0) {
|
|
6842
|
+
if (excluded.has(event.id)) return;
|
|
6740
6843
|
this.deferredGraphicsPick = event;
|
|
6741
6844
|
return;
|
|
6742
6845
|
}
|
|
@@ -7091,4 +7194,4 @@ function nearestWorldCopyPosition(position, refLng) {
|
|
|
7091
7194
|
return [nearestWorldCopyLng(position[0], refLng), ...position.slice(1)];
|
|
7092
7195
|
}
|
|
7093
7196
|
//#endregion
|
|
7094
|
-
export { BaseMapAdapter, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, HATCH_PATTERNS, PointSymbolCapabilityRequiredError, PointerCallbackHub, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, TacticalDrawDestroyedError, bindAbortable, clonePointSymbol, coerceHandleStyle, combineWithHostSignal, effectivePointSymbolPixelSize, evaluatePickCandidates, fillPatternImageDimension, fillPatternImageId, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, isPointSymbol, isTacticalDrawAbortError, makeFillPatternImage, mergeNativeAndCachedPickFeatures, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, pickPointerType, pointSymbolFootprint, renderOrderToVisualOrder, resizePointSymbolSize, resolveGroundTextSizePixels, resolvePickPresentation, resolveTextSizePixels, tryControlMeasureIdFromFeature };
|
|
7197
|
+
export { BaseMapAdapter, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, HATCH_PATTERNS, PointSymbolCapabilityRequiredError, PointerCallbackHub, TEXT_HALO_COLOR, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, TacticalDrawDestroyedError, bindAbortable, clonePointSymbol, coerceHandleStyle, combineWithHostSignal, effectivePointSymbolPixelSize, evaluatePickCandidates, fillPatternImageDimension, fillPatternImageId, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, isPointSymbol, isTacticalDrawAbortError, makeFillPatternImage, mergeNativeAndCachedPickFeatures, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, pickPointerType, pointSymbolFootprint, renderOrderToVisualOrder, resizePointSymbolSize, resolveGroundTextSizePixels, resolvePickPresentation, resolveTextHaloWidth, resolveTextSizePixels, tryControlMeasureIdFromFeature };
|