@orbat-mapper/tactical-draw 0.2.0-alpha.21 → 0.2.0-alpha.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/index.d.mts +39 -1
- package/dist/index.mjs +358 -78
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -87,6 +87,22 @@ if (session && "canCommit" in session) {
|
|
|
87
87
|
}
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
+
Live edit and transform sessions expose a session-local `history`. Completed
|
|
91
|
+
gestures and authored programmatic mutations are undoable without settling the
|
|
92
|
+
session; pointer-move preview frames are not history steps:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
const active = td.activeSession;
|
|
96
|
+
const history =
|
|
97
|
+
(active && "history" in active ? active.history : null) ?? td.activeTransformSession?.history;
|
|
98
|
+
if (history) history.undo();
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`history.subscribe(...)` reports `canUndo`, `canRedo`, and optional operation
|
|
102
|
+
labels. Committing folds only the selected working state into the host document
|
|
103
|
+
as one operation. Changing a transform session's members with `setGraphics()`
|
|
104
|
+
establishes a new local baseline and clears both stacks.
|
|
105
|
+
|
|
90
106
|
## Bring your own engine
|
|
91
107
|
|
|
92
108
|
Implement `MapAdapter` (or extend `BaseMapAdapter`) against your map engine
|
package/dist/index.d.mts
CHANGED
|
@@ -12,6 +12,11 @@ interface PointerEventInfo {
|
|
|
12
12
|
* mouse paths populate it; touch leaves it `undefined`.
|
|
13
13
|
*/
|
|
14
14
|
altKey?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Whether Shift was held for this pointer event. Rotation gestures use the
|
|
17
|
+
* live value to constrain transform-box angles to 15-degree increments.
|
|
18
|
+
*/
|
|
19
|
+
shiftKey?: boolean;
|
|
15
20
|
/** Engine-native event, for drivers that need to call preventDefault etc. */
|
|
16
21
|
native?: unknown;
|
|
17
22
|
}
|
|
@@ -672,6 +677,22 @@ type EditMode = "reshape" | "transform" | "labeldrag" | "delete";
|
|
|
672
677
|
*/
|
|
673
678
|
type SettleReason = "close" | "error" | TacticalDrawAbortReason;
|
|
674
679
|
//#endregion
|
|
680
|
+
//#region src/tactical-draw/session-history.d.ts
|
|
681
|
+
type SessionChangeSource = "gesture" | "undo" | "redo" | "programmatic";
|
|
682
|
+
interface SessionHistoryState {
|
|
683
|
+
readonly canUndo: boolean;
|
|
684
|
+
readonly canRedo: boolean;
|
|
685
|
+
readonly undoLabel?: string;
|
|
686
|
+
readonly redoLabel?: string;
|
|
687
|
+
}
|
|
688
|
+
interface SessionHistory {
|
|
689
|
+
/** Current availability and optional labels. Each session retains at most 100 completed steps. */
|
|
690
|
+
readonly state: SessionHistoryState;
|
|
691
|
+
undo(): boolean;
|
|
692
|
+
redo(): boolean;
|
|
693
|
+
subscribe(listener: (state: SessionHistoryState) => void): () => void;
|
|
694
|
+
}
|
|
695
|
+
//#endregion
|
|
675
696
|
//#region src/tactical-draw/edit-session.d.ts
|
|
676
697
|
/**
|
|
677
698
|
* Surface delivered via `td.edit(measure, opts).onSession` and synchronously
|
|
@@ -683,6 +704,8 @@ type SettleReason = "close" | "error" | TacticalDrawAbortReason;
|
|
|
683
704
|
* asynchronously).
|
|
684
705
|
*/
|
|
685
706
|
interface EditSession {
|
|
707
|
+
/** Undo/redo for completed mutations within this live session. */
|
|
708
|
+
readonly history: SessionHistory;
|
|
686
709
|
/** Immutable edit-start input. */
|
|
687
710
|
readonly graphic: ControlMeasure;
|
|
688
711
|
/** Live complete working graphic. Copy on read. */
|
|
@@ -820,6 +843,8 @@ interface EditSession {
|
|
|
820
843
|
* observe partial state. See PRD #40 user-stories 23–24.
|
|
821
844
|
*/
|
|
822
845
|
interface EditChangeEvent {
|
|
846
|
+
/** How this working-copy transition was initiated. */
|
|
847
|
+
readonly source: SessionChangeSource;
|
|
823
848
|
/** Working state after the completed gesture. */
|
|
824
849
|
readonly graphic: GraphicSnapshot<ControlMeasure>;
|
|
825
850
|
/** State immediately before this gesture. */
|
|
@@ -840,6 +865,7 @@ interface EditChangeEvent {
|
|
|
840
865
|
//#endregion
|
|
841
866
|
//#region src/tactical-draw/graphic-edit-session.d.ts
|
|
842
867
|
interface GraphicEditChangeEvent<G extends Graphic = Graphic> {
|
|
868
|
+
readonly source: SessionChangeSource;
|
|
843
869
|
readonly graphic: GraphicSnapshot<G>;
|
|
844
870
|
readonly previous: GraphicSnapshot<G>;
|
|
845
871
|
readonly session: GraphicEditSession<G>;
|
|
@@ -849,6 +875,7 @@ interface GraphicEditChangeEvent<G extends Graphic = Graphic> {
|
|
|
849
875
|
}
|
|
850
876
|
/** Common selection/editing contract shared by control measures and point symbols. */
|
|
851
877
|
interface GraphicEditSession<G extends Graphic = Graphic> {
|
|
878
|
+
readonly history: SessionHistory;
|
|
852
879
|
/** Immutable edit-start input. */
|
|
853
880
|
readonly graphic: G;
|
|
854
881
|
/** Live working graphic. Copy on read. */
|
|
@@ -885,6 +912,8 @@ interface GraphicEditSession<G extends Graphic = Graphic> {
|
|
|
885
912
|
* delivers, asynchronously).
|
|
886
913
|
*/
|
|
887
914
|
interface TransformSession {
|
|
915
|
+
/** Undo/redo for completed gestures within the current membership baseline. */
|
|
916
|
+
readonly history: SessionHistory;
|
|
888
917
|
/**
|
|
889
918
|
* Live view of the current members' join-time input graphics — a fresh array
|
|
890
919
|
* on each read, in current membership order. Grows/shrinks as [[setGraphics]]
|
|
@@ -926,6 +955,11 @@ interface TransformSession {
|
|
|
926
955
|
* current box angle. Session-scoped state (`sizeAnchor`, box angle,
|
|
927
956
|
* non-geometry dirty) is unchanged.
|
|
928
957
|
*
|
|
958
|
+
* Calling this establishes a new session-history baseline: both undo and
|
|
959
|
+
* redo are cleared after retained members carry their working state
|
|
960
|
+
* forward. Membership changes and already-emitted exit snapshots are not
|
|
961
|
+
* undoable within the live session.
|
|
962
|
+
*
|
|
929
963
|
* **Return value** — the committed exit snapshots of the members removed *by
|
|
930
964
|
* this call*, built exactly like [[close]]'s snapshots (pixel size baked to
|
|
931
965
|
* ground unless `sizeAnchor` is `"screen"`, then cloned and rendered); an
|
|
@@ -1036,6 +1070,8 @@ interface TransformSession {
|
|
|
1036
1070
|
* `EditChangeEvent` for the identical emission contract this mirrors.
|
|
1037
1071
|
*/
|
|
1038
1072
|
interface TransformChangeEvent {
|
|
1073
|
+
/** How this working-copy transition was initiated. */
|
|
1074
|
+
readonly source: SessionChangeSource;
|
|
1039
1075
|
/** Working state after the completed gesture, index-aligned with `session.graphics` as of this emission. */
|
|
1040
1076
|
readonly graphics: GraphicSnapshot<ControlMeasure>[];
|
|
1041
1077
|
/** State immediately before this gesture, index-aligned with `session.graphics` as of this emission. */
|
|
@@ -1058,6 +1094,7 @@ interface TransformChangeEvent {
|
|
|
1058
1094
|
//#endregion
|
|
1059
1095
|
//#region src/tactical-draw/graphic-transform-session.d.ts
|
|
1060
1096
|
interface GraphicTransformChangeEvent {
|
|
1097
|
+
readonly source: SessionChangeSource;
|
|
1061
1098
|
readonly graphics: GraphicSnapshot[];
|
|
1062
1099
|
readonly previous: GraphicSnapshot[];
|
|
1063
1100
|
readonly session: GraphicTransformSession;
|
|
@@ -1066,6 +1103,7 @@ interface GraphicTransformChangeEvent {
|
|
|
1066
1103
|
abort(): void;
|
|
1067
1104
|
}
|
|
1068
1105
|
interface GraphicTransformSession {
|
|
1106
|
+
readonly history: SessionHistory;
|
|
1069
1107
|
readonly graphics: readonly Graphic[];
|
|
1070
1108
|
readonly workingGraphics: readonly Graphic[];
|
|
1071
1109
|
readonly dirty: boolean;
|
|
@@ -1782,4 +1820,4 @@ declare function nearestWorldCopyLng(lng: number, refLng: number): number;
|
|
|
1782
1820
|
/** `nearestWorldCopyLng` applied to a lon/lat position. */
|
|
1783
1821
|
declare function nearestWorldCopyPosition(position: Position, refLng: number): Position;
|
|
1784
1822
|
//#endregion
|
|
1785
|
-
export { BaseMapAdapter, type CombinedAbort, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, type DrawMeasureDraft, type DrawOptions, type DrawPointSymbolDraft, type DrawSession, type EditChangeEvent, type EditManyOptions, type EditMode, type EditOptions, type EditPointerDriver, type EditSession, type FeatureOps, type Graphic, type GraphicEditChangeEvent, type GraphicEditSession, type GraphicRender, type GraphicSnapshot, type GraphicTransformChangeEvent, type GraphicTransformSession, type GuideStyle, type HandleStyle, type IconResource, type InteractionHit, type InteractionStyle, type LayerId, type MapAdapter, type MapCursor, type MapEvent, type MapEventHandler, type NativeEventEntry, type PickEvent, type PickHandler, type PickOptions, type PixelCoordinate, type PointSymbol, type PointSymbolAnchor, type PointSymbolCapability, PointSymbolCapabilityRequiredError, type PointSymbolColors, type PointSymbolFootprint, type PointSymbolGraphicModifiers, type PointSymbolRender, type PointSymbolSize, type PointSymbolStyle, type PointerCallback, PointerCallbackHub, type PointerEventInfo, type SettleReason, type SizeAnchor, type StyleOptions, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, type TacticalDrawAbortReason, TacticalDrawDestroyedError, type TacticalDrawLayerOptions, type TacticalDrawOptions, type TextStyleOptions, type ToPixel, type TransformBox, type TransformChangeEvent, type TransformSession, bindAbortable, clonePointSymbol, coerceHandleStyle, combineWithHostSignal, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, isPointSymbol, isTacticalDrawAbortError, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, pointSymbolFootprint, resolveGroundTextSizePixels, tryControlMeasureIdFromFeature };
|
|
1823
|
+
export { BaseMapAdapter, type CombinedAbort, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, type DrawMeasureDraft, type DrawOptions, type DrawPointSymbolDraft, type DrawSession, type EditChangeEvent, type EditManyOptions, type EditMode, type EditOptions, type EditPointerDriver, type EditSession, type FeatureOps, type Graphic, type GraphicEditChangeEvent, type GraphicEditSession, type GraphicRender, type GraphicSnapshot, type GraphicTransformChangeEvent, type GraphicTransformSession, type GuideStyle, type HandleStyle, type IconResource, type InteractionHit, type InteractionStyle, type LayerId, type MapAdapter, type MapCursor, type MapEvent, type MapEventHandler, type NativeEventEntry, type PickEvent, type PickHandler, type PickOptions, type PixelCoordinate, type PointSymbol, type PointSymbolAnchor, type PointSymbolCapability, PointSymbolCapabilityRequiredError, type PointSymbolColors, type PointSymbolFootprint, type PointSymbolGraphicModifiers, type PointSymbolRender, type PointSymbolSize, type PointSymbolStyle, type PointerCallback, PointerCallbackHub, type PointerEventInfo, type SessionChangeSource, type SessionHistory, type SessionHistoryState, type SettleReason, type SizeAnchor, type StyleOptions, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, type TacticalDrawAbortReason, TacticalDrawDestroyedError, type TacticalDrawLayerOptions, type TacticalDrawOptions, type TextStyleOptions, type ToPixel, type TransformBox, type TransformChangeEvent, type TransformSession, bindAbortable, clonePointSymbol, coerceHandleStyle, combineWithHostSignal, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, isPointSymbol, isTacticalDrawAbortError, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, pointSymbolFootprint, resolveGroundTextSizePixels, tryControlMeasureIdFromFeature };
|
package/dist/index.mjs
CHANGED
|
@@ -1693,6 +1693,19 @@ function pickHandle(handles, pixel, tolerance, toPixel) {
|
|
|
1693
1693
|
}
|
|
1694
1694
|
return null;
|
|
1695
1695
|
}
|
|
1696
|
+
//#endregion
|
|
1697
|
+
//#region src/tactical-draw/rotation.ts
|
|
1698
|
+
/** Excalidraw-style rotation increment used while Shift is held. */
|
|
1699
|
+
const CONSTRAINED_ROTATION_STEP = Math.PI / 12;
|
|
1700
|
+
/** Normalize radians to the canonical clockwise [0, 2π) range. */
|
|
1701
|
+
function normalizeRotation(angle) {
|
|
1702
|
+
const turn = Math.PI * 2;
|
|
1703
|
+
return (angle % turn + turn) % turn;
|
|
1704
|
+
}
|
|
1705
|
+
/** Snap an absolute rotation angle to the nearest 15-degree increment. */
|
|
1706
|
+
function constrainRotation(angle) {
|
|
1707
|
+
return Math.round(angle / CONSTRAINED_ROTATION_STEP) * CONSTRAINED_ROTATION_STEP;
|
|
1708
|
+
}
|
|
1696
1709
|
function rotate(x, y, angle) {
|
|
1697
1710
|
const cos = Math.cos(angle);
|
|
1698
1711
|
const sin = Math.sin(angle);
|
|
@@ -1956,8 +1969,9 @@ function beginBoxDrag(args) {
|
|
|
1956
1969
|
const pivotPixel = box.center;
|
|
1957
1970
|
const pivot = fromPixel(pivotPixel);
|
|
1958
1971
|
const startAngle = Math.atan2(downPixel[1] - pivotPixel[1], downPixel[0] - pivotPixel[0]);
|
|
1959
|
-
const move = (pixel) => {
|
|
1960
|
-
const
|
|
1972
|
+
const move = (pixel, modifiers) => {
|
|
1973
|
+
const rawAngle = Math.atan2(pixel[1] - pivotPixel[1], pixel[0] - pivotPixel[0]) - startAngle;
|
|
1974
|
+
const angle = modifiers?.constrainRotation ? constrainRotation(box.angle + rawAngle) - box.angle : rawAngle;
|
|
1961
1975
|
return {
|
|
1962
1976
|
points: rotatePointsAround(preDragPoints, pivot, pivotPixel, angle, projection),
|
|
1963
1977
|
box: {
|
|
@@ -1971,7 +1985,7 @@ function beginBoxDrag(args) {
|
|
|
1971
1985
|
return {
|
|
1972
1986
|
kind,
|
|
1973
1987
|
move,
|
|
1974
|
-
end: (pixel) => move(pixel).points
|
|
1988
|
+
end: (pixel, modifiers) => move(pixel, modifiers).points
|
|
1975
1989
|
};
|
|
1976
1990
|
}
|
|
1977
1991
|
const origin = fromPixel(downPixel);
|
|
@@ -2035,6 +2049,102 @@ const BEGIN_DRAG_FOR_TEST = Symbol("EditSession.beginDragForTest");
|
|
|
2035
2049
|
*/
|
|
2036
2050
|
const END_DRAG_FOR_TEST = Symbol("EditSession.endDragForTest");
|
|
2037
2051
|
//#endregion
|
|
2052
|
+
//#region src/tactical-draw/session-history.ts
|
|
2053
|
+
/** @internal Controller-owned history stack. */
|
|
2054
|
+
function createSessionHistory(options) {
|
|
2055
|
+
const limit = Math.max(1, options.limit ?? 100);
|
|
2056
|
+
let entries = [{ value: options.clone(options.initial) }];
|
|
2057
|
+
let index = 0;
|
|
2058
|
+
let settled = false;
|
|
2059
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2060
|
+
function currentState() {
|
|
2061
|
+
if (settled) return {
|
|
2062
|
+
canUndo: false,
|
|
2063
|
+
canRedo: false
|
|
2064
|
+
};
|
|
2065
|
+
return {
|
|
2066
|
+
canUndo: index > 0,
|
|
2067
|
+
canRedo: index < entries.length - 1,
|
|
2068
|
+
...index > 0 && entries[index]?.label ? { undoLabel: entries[index].label } : {},
|
|
2069
|
+
...index < entries.length - 1 && entries[index + 1]?.label ? { redoLabel: entries[index + 1].label } : {}
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
function notify() {
|
|
2073
|
+
const state = currentState();
|
|
2074
|
+
const errors = [];
|
|
2075
|
+
for (const listener of [...listeners]) try {
|
|
2076
|
+
listener(state);
|
|
2077
|
+
} catch (error) {
|
|
2078
|
+
errors.push(error);
|
|
2079
|
+
}
|
|
2080
|
+
if (errors.length === 1) throw errors[0];
|
|
2081
|
+
if (errors.length > 1) throw new AggregateError(errors, "SessionHistory subscriber errors");
|
|
2082
|
+
}
|
|
2083
|
+
function move(nextIndex, source) {
|
|
2084
|
+
if (settled || nextIndex < 0 || nextIndex >= entries.length) return false;
|
|
2085
|
+
const from = options.clone(entries[index].value);
|
|
2086
|
+
const to = options.clone(entries[nextIndex].value);
|
|
2087
|
+
try {
|
|
2088
|
+
if (!options.apply(from, to, source)) return false;
|
|
2089
|
+
} catch (error) {
|
|
2090
|
+
if (!options.isCurrent?.(to)) throw error;
|
|
2091
|
+
index = nextIndex;
|
|
2092
|
+
try {
|
|
2093
|
+
notify();
|
|
2094
|
+
} catch (notifyError) {
|
|
2095
|
+
throw new AggregateError([error, notifyError], "SessionHistory undo/redo errors");
|
|
2096
|
+
}
|
|
2097
|
+
throw error;
|
|
2098
|
+
}
|
|
2099
|
+
index = nextIndex;
|
|
2100
|
+
notify();
|
|
2101
|
+
return true;
|
|
2102
|
+
}
|
|
2103
|
+
return {
|
|
2104
|
+
get state() {
|
|
2105
|
+
return currentState();
|
|
2106
|
+
},
|
|
2107
|
+
undo() {
|
|
2108
|
+
return move(index - 1, "undo");
|
|
2109
|
+
},
|
|
2110
|
+
redo() {
|
|
2111
|
+
return move(index + 1, "redo");
|
|
2112
|
+
},
|
|
2113
|
+
subscribe(listener) {
|
|
2114
|
+
listener(currentState());
|
|
2115
|
+
if (settled) return () => {};
|
|
2116
|
+
listeners.add(listener);
|
|
2117
|
+
return () => listeners.delete(listener);
|
|
2118
|
+
},
|
|
2119
|
+
record(value, label) {
|
|
2120
|
+
if (settled || options.equals(entries[index].value, value)) return false;
|
|
2121
|
+
entries.splice(index + 1);
|
|
2122
|
+
entries.push({
|
|
2123
|
+
value,
|
|
2124
|
+
label
|
|
2125
|
+
});
|
|
2126
|
+
if (entries.length > limit + 1) entries.shift();
|
|
2127
|
+
index = entries.length - 1;
|
|
2128
|
+
notify();
|
|
2129
|
+
return true;
|
|
2130
|
+
},
|
|
2131
|
+
reset(value) {
|
|
2132
|
+
if (settled) return;
|
|
2133
|
+
entries = [{ value: options.clone(value) }];
|
|
2134
|
+
index = 0;
|
|
2135
|
+
notify();
|
|
2136
|
+
},
|
|
2137
|
+
settle() {
|
|
2138
|
+
if (settled) return;
|
|
2139
|
+
settled = true;
|
|
2140
|
+
entries = [];
|
|
2141
|
+
index = 0;
|
|
2142
|
+
notify();
|
|
2143
|
+
listeners.clear();
|
|
2144
|
+
}
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
//#endregion
|
|
2038
2148
|
//#region src/tactical-draw/box-features.ts
|
|
2039
2149
|
/**
|
|
2040
2150
|
* Rendering helpers shared by the single-measure edit controller
|
|
@@ -2386,6 +2496,28 @@ function runEdit(opts) {
|
|
|
2386
2496
|
* persisted on the measure; a fresh edit always starts back at `0`.
|
|
2387
2497
|
*/
|
|
2388
2498
|
let boxAngle = 0;
|
|
2499
|
+
function captureHistory() {
|
|
2500
|
+
return {
|
|
2501
|
+
points: workingPoints.map((point) => [...point]),
|
|
2502
|
+
options: { ...workingOptions ?? {} },
|
|
2503
|
+
style: currentStyle ? { ...currentStyle } : void 0,
|
|
2504
|
+
textAmplifiers: workingTextAmplifiers ? { ...workingTextAmplifiers } : void 0,
|
|
2505
|
+
amplifierPlacements: snapshotAmplifierPlacements(),
|
|
2506
|
+
sizeAnchor,
|
|
2507
|
+
nonGeometryDirty,
|
|
2508
|
+
boxAngle
|
|
2509
|
+
};
|
|
2510
|
+
}
|
|
2511
|
+
function restoreHistory(value) {
|
|
2512
|
+
replaceWorkingPoints(value.points);
|
|
2513
|
+
replaceWorkingOptions(value.options);
|
|
2514
|
+
currentStyle = value.style ? { ...value.style } : void 0;
|
|
2515
|
+
workingTextAmplifiers = value.textAmplifiers ? { ...value.textAmplifiers } : void 0;
|
|
2516
|
+
workingAmplifierPlacements = value.amplifierPlacements ? structuredClone(value.amplifierPlacements) : void 0;
|
|
2517
|
+
sizeAnchor = value.sizeAnchor;
|
|
2518
|
+
nonGeometryDirty = value.nonGeometryDirty;
|
|
2519
|
+
boxAngle = value.boxAngle;
|
|
2520
|
+
}
|
|
2389
2521
|
/**
|
|
2390
2522
|
* The transform box fitted around the *rendered* working-copy geometry
|
|
2391
2523
|
* (pixel space), or `null` when nothing projected this frame. Re-fitted
|
|
@@ -2612,12 +2744,13 @@ function runEdit(opts) {
|
|
|
2612
2744
|
* - Reject restores the working state to the pre-gesture snapshot and
|
|
2613
2745
|
* recomputes `dirty`. Close/abort settle the session as normal.
|
|
2614
2746
|
*/
|
|
2615
|
-
function emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements) {
|
|
2747
|
+
function emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, source = "gesture", historyLabel = "Edit", previousBoxAngle = boxAngle) {
|
|
2616
2748
|
if (ctx.settled) return;
|
|
2617
2749
|
let rejectRequested = false;
|
|
2618
2750
|
let closeRequested = false;
|
|
2619
2751
|
let abortRequested = false;
|
|
2620
2752
|
const event = {
|
|
2753
|
+
source,
|
|
2621
2754
|
graphic: currentRendered,
|
|
2622
2755
|
previous: previousRendered,
|
|
2623
2756
|
session,
|
|
@@ -2646,11 +2779,13 @@ function runEdit(opts) {
|
|
|
2646
2779
|
sizeAnchor = previousSizeAnchor;
|
|
2647
2780
|
workingTextAmplifiers = previousTextAmplifiers;
|
|
2648
2781
|
workingAmplifierPlacements = previousAmplifierPlacements;
|
|
2782
|
+
boxAngle = previousBoxAngle;
|
|
2649
2783
|
nonGeometryDirty = previousNonGeometryDirty;
|
|
2650
2784
|
renderPreview();
|
|
2651
2785
|
renderHandlesLayer();
|
|
2652
2786
|
recomputeDirty();
|
|
2653
2787
|
}
|
|
2788
|
+
if (!ctx.settled && !rejectRequested && (source === "gesture" || source === "programmatic")) history.record(captureHistory(), historyLabel);
|
|
2654
2789
|
if (errors.length === 1) throw errors[0];
|
|
2655
2790
|
if (errors.length > 1) throw new AggregateError(errors, "EditSession.onChange listener errors");
|
|
2656
2791
|
}
|
|
@@ -2664,7 +2799,7 @@ function runEdit(opts) {
|
|
|
2664
2799
|
* queued during the drag are applied after the listener emission (so the
|
|
2665
2800
|
* post-gesture render reflects the requested mode set).
|
|
2666
2801
|
*/
|
|
2667
|
-
function commitGesture(nextPoints, preGestureOptions, nextAmplifierPlacements) {
|
|
2802
|
+
function commitGesture(nextPoints, preGestureOptions, nextAmplifierPlacements, preGestureBoxAngle = boxAngle) {
|
|
2668
2803
|
if (ctx.settled) return;
|
|
2669
2804
|
const previousPoints = workingPoints.map((p) => [...p]);
|
|
2670
2805
|
const previousOptions = { ...preGestureOptions ?? workingOptions ?? {} };
|
|
@@ -2691,7 +2826,7 @@ function runEdit(opts) {
|
|
|
2691
2826
|
recomputeDirty();
|
|
2692
2827
|
dragInFlight = false;
|
|
2693
2828
|
drainPendingModes();
|
|
2694
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements);
|
|
2829
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, "gesture", "Edit", preGestureBoxAngle);
|
|
2695
2830
|
}
|
|
2696
2831
|
function setOptions(partial) {
|
|
2697
2832
|
if (ctx.settled) return;
|
|
@@ -2706,6 +2841,7 @@ function runEdit(opts) {
|
|
|
2706
2841
|
...workingOptions ?? {},
|
|
2707
2842
|
...partial ?? {}
|
|
2708
2843
|
});
|
|
2844
|
+
if (JSON.stringify(workingOptions) === JSON.stringify(previousOptions)) return;
|
|
2709
2845
|
let currentRendered;
|
|
2710
2846
|
try {
|
|
2711
2847
|
currentRendered = rebuildRendered();
|
|
@@ -2716,7 +2852,7 @@ function runEdit(opts) {
|
|
|
2716
2852
|
renderPreview();
|
|
2717
2853
|
nonGeometryDirty = true;
|
|
2718
2854
|
dirty = true;
|
|
2719
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements);
|
|
2855
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, "programmatic", "Change options");
|
|
2720
2856
|
}
|
|
2721
2857
|
function setStyle(partial) {
|
|
2722
2858
|
if (ctx.settled) return;
|
|
@@ -2731,6 +2867,7 @@ function runEdit(opts) {
|
|
|
2731
2867
|
...currentStyle ?? {},
|
|
2732
2868
|
...partial
|
|
2733
2869
|
};
|
|
2870
|
+
if (JSON.stringify(currentStyle) === JSON.stringify(previousStyle)) return;
|
|
2734
2871
|
let currentRendered;
|
|
2735
2872
|
try {
|
|
2736
2873
|
currentRendered = rebuildRendered();
|
|
@@ -2741,7 +2878,7 @@ function runEdit(opts) {
|
|
|
2741
2878
|
renderPreview();
|
|
2742
2879
|
nonGeometryDirty = true;
|
|
2743
2880
|
dirty = true;
|
|
2744
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements);
|
|
2881
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, "programmatic", "Change style");
|
|
2745
2882
|
}
|
|
2746
2883
|
/**
|
|
2747
2884
|
* Shallow-merge into the live working text amplifiers. Mirrors
|
|
@@ -2776,6 +2913,7 @@ function runEdit(opts) {
|
|
|
2776
2913
|
if (value !== void 0 && value.trim() !== "") merged[canonicalKey ?? key] = value;
|
|
2777
2914
|
}
|
|
2778
2915
|
workingTextAmplifiers = Object.keys(merged).length > 0 ? merged : void 0;
|
|
2916
|
+
if (JSON.stringify(workingTextAmplifiers) === JSON.stringify(previousTextAmplifiers)) return;
|
|
2779
2917
|
let currentRendered;
|
|
2780
2918
|
try {
|
|
2781
2919
|
currentRendered = rebuildRendered();
|
|
@@ -2786,9 +2924,9 @@ function runEdit(opts) {
|
|
|
2786
2924
|
renderPreview();
|
|
2787
2925
|
nonGeometryDirty = true;
|
|
2788
2926
|
dirty = true;
|
|
2789
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements);
|
|
2927
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, "programmatic", "Change text");
|
|
2790
2928
|
}
|
|
2791
|
-
function setAmplifierPlacements(partial) {
|
|
2929
|
+
function setAmplifierPlacements(partial, source = "programmatic", historyLabel = "Move label") {
|
|
2792
2930
|
if (ctx.settled) return;
|
|
2793
2931
|
const previousPoints = workingPoints.map((p) => [...p]);
|
|
2794
2932
|
const previousOptions = { ...workingOptions ?? {} };
|
|
@@ -2808,11 +2946,12 @@ function runEdit(opts) {
|
|
|
2808
2946
|
else next[field] = merged;
|
|
2809
2947
|
}
|
|
2810
2948
|
workingAmplifierPlacements = Object.keys(next).length > 0 ? next : void 0;
|
|
2949
|
+
if (JSON.stringify(workingAmplifierPlacements) === JSON.stringify(previousAmplifierPlacements)) return;
|
|
2811
2950
|
const currentRendered = rebuildRendered();
|
|
2812
2951
|
renderPreview();
|
|
2813
2952
|
nonGeometryDirty = true;
|
|
2814
2953
|
dirty = true;
|
|
2815
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements);
|
|
2954
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, sizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, source, historyLabel);
|
|
2816
2955
|
}
|
|
2817
2956
|
/**
|
|
2818
2957
|
* Flip the size anchor for the measure under edit (ADR-0020). Re-renders the
|
|
@@ -2835,7 +2974,7 @@ function runEdit(opts) {
|
|
|
2835
2974
|
const currentRendered = rebuildRendered();
|
|
2836
2975
|
nonGeometryDirty = true;
|
|
2837
2976
|
dirty = true;
|
|
2838
|
-
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements);
|
|
2977
|
+
emitChange(previousRendered, previousPoints, previousOptions, previousStyle, previousNonGeometryDirty, previousSizeAnchor, currentRendered, previousTextAmplifiers, previousAmplifierPlacements, "programmatic", "Change size anchor");
|
|
2839
2978
|
}
|
|
2840
2979
|
/** Apply a translate gesture by shifting every working point by `delta`. */
|
|
2841
2980
|
function commitTranslate(delta) {
|
|
@@ -2864,7 +3003,7 @@ function runEdit(opts) {
|
|
|
2864
3003
|
const pivot = box ? driver.fromPixel(box.center) : computeBoundingBoxCenter(workingPoints);
|
|
2865
3004
|
if (!pivot) return;
|
|
2866
3005
|
boxAngle += angle;
|
|
2867
|
-
commitGesture(rotatePointsAround(workingPoints, pivot, pivotPixel, angle, dragProjection), preFreezeOptions);
|
|
3006
|
+
commitGesture(rotatePointsAround(workingPoints, pivot, pivotPixel, angle, dragProjection), preFreezeOptions, void 0, boxAngle - angle);
|
|
2868
3007
|
}
|
|
2869
3008
|
function modesEqual(a, b) {
|
|
2870
3009
|
if (a.length !== b.length) return false;
|
|
@@ -2883,7 +3022,32 @@ function runEdit(opts) {
|
|
|
2883
3022
|
pendingModes = null;
|
|
2884
3023
|
applyModes(next);
|
|
2885
3024
|
}
|
|
3025
|
+
const history = createSessionHistory({
|
|
3026
|
+
initial: captureHistory(),
|
|
3027
|
+
clone: (value) => ({
|
|
3028
|
+
points: value.points.map((point) => [...point]),
|
|
3029
|
+
options: { ...value.options ?? {} },
|
|
3030
|
+
style: value.style ? { ...value.style } : void 0,
|
|
3031
|
+
textAmplifiers: value.textAmplifiers ? { ...value.textAmplifiers } : void 0,
|
|
3032
|
+
amplifierPlacements: value.amplifierPlacements ? structuredClone(value.amplifierPlacements) : void 0,
|
|
3033
|
+
sizeAnchor: value.sizeAnchor,
|
|
3034
|
+
nonGeometryDirty: value.nonGeometryDirty,
|
|
3035
|
+
boxAngle: value.boxAngle
|
|
3036
|
+
}),
|
|
3037
|
+
equals: (a, b) => JSON.stringify(a) === JSON.stringify(b),
|
|
3038
|
+
isCurrent: (value) => JSON.stringify(captureHistory()) === JSON.stringify(value),
|
|
3039
|
+
apply(from, to, source) {
|
|
3040
|
+
const previousRendered = rebuildRendered();
|
|
3041
|
+
restoreHistory(to);
|
|
3042
|
+
renderPreview();
|
|
3043
|
+
renderHandlesLayer();
|
|
3044
|
+
recomputeDirty();
|
|
3045
|
+
emitChange(previousRendered, from.points, from.options, from.style, from.nonGeometryDirty, from.sizeAnchor, rebuildRendered(), from.textAmplifiers, from.amplifierPlacements, source, "Edit", from.boxAngle);
|
|
3046
|
+
return !ctx.settled && JSON.stringify(captureHistory()) === JSON.stringify(to);
|
|
3047
|
+
}
|
|
3048
|
+
});
|
|
2886
3049
|
const session = {
|
|
3050
|
+
history,
|
|
2887
3051
|
graphic: measure,
|
|
2888
3052
|
get workingGraphic() {
|
|
2889
3053
|
return cloneControlMeasure({
|
|
@@ -2943,6 +3107,7 @@ function runEdit(opts) {
|
|
|
2943
3107
|
const current = session.workingGraphic;
|
|
2944
3108
|
const next = updater(current);
|
|
2945
3109
|
if (next.id !== measure.id || next.kind !== measure.kind) throw new TypeError("updateGraphic must preserve control-measure id and kind");
|
|
3110
|
+
if (JSON.stringify(next) === JSON.stringify(current)) return;
|
|
2946
3111
|
renderMeasureForAdapter(adapter, next, renderOpts);
|
|
2947
3112
|
workingPoints.splice(0, workingPoints.length, ...next.controlPoints.map((point) => [...point]));
|
|
2948
3113
|
workingOptions = { ...next.options ?? {} };
|
|
@@ -2953,7 +3118,7 @@ function runEdit(opts) {
|
|
|
2953
3118
|
renderPreview();
|
|
2954
3119
|
renderHandlesLayer();
|
|
2955
3120
|
recomputeDirty();
|
|
2956
|
-
emitChange(previous, previous.graphic.controlPoints.map((point) => [...point]), previous.graphic.options, previous.graphic.style, previousNonGeometryDirty, previousSizeAnchor, rebuildRendered(), previous.graphic.textAmplifiers, previous.graphic.amplifierPlacements);
|
|
3121
|
+
emitChange(previous, previous.graphic.controlPoints.map((point) => [...point]), previous.graphic.options, previous.graphic.style, previousNonGeometryDirty, previousSizeAnchor, rebuildRendered(), previous.graphic.textAmplifiers, previous.graphic.amplifierPlacements, "programmatic", "Update graphic");
|
|
2957
3122
|
},
|
|
2958
3123
|
setOptions(partial) {
|
|
2959
3124
|
setOptions(partial);
|
|
@@ -3258,9 +3423,9 @@ function runEdit(opts) {
|
|
|
3258
3423
|
renderHandlesLayer();
|
|
3259
3424
|
recomputeDirty();
|
|
3260
3425
|
}
|
|
3261
|
-
function paintBoxDragSnapshot(pixel) {
|
|
3426
|
+
function paintBoxDragSnapshot(pixel, constrainRotation = false) {
|
|
3262
3427
|
if (!activeBoxDrag) return;
|
|
3263
|
-
const snapshot = activeBoxDrag.fsm.move(pixel);
|
|
3428
|
+
const snapshot = activeBoxDrag.fsm.move(pixel, { constrainRotation });
|
|
3264
3429
|
activeBoxDrag.lastSnapshot = snapshot;
|
|
3265
3430
|
workingPoints.splice(0, workingPoints.length, ...snapshot.points);
|
|
3266
3431
|
workingAmplifierPlacements = transformAmplifierPlacements(activeBoxDrag.preDragAmplifierPlacements, activeBoxDrag.preDragBox, snapshot.box, driver);
|
|
@@ -3268,7 +3433,7 @@ function runEdit(opts) {
|
|
|
3268
3433
|
renderHandlesLayer();
|
|
3269
3434
|
recomputeDirty();
|
|
3270
3435
|
}
|
|
3271
|
-
const onDriverMove = (pixel) => {
|
|
3436
|
+
const onDriverMove = (pixel, info) => {
|
|
3272
3437
|
if (ctx.settled) return;
|
|
3273
3438
|
if (activeLabelRotateDrag) {
|
|
3274
3439
|
const drag = activeLabelRotateDrag;
|
|
@@ -3304,7 +3469,7 @@ function runEdit(opts) {
|
|
|
3304
3469
|
renderPreview();
|
|
3305
3470
|
renderHandlesLayer();
|
|
3306
3471
|
} else if (activeDrag) paintDragSnapshot(pixel);
|
|
3307
|
-
else if (activeBoxDrag) paintBoxDragSnapshot(pixel);
|
|
3472
|
+
else if (activeBoxDrag) paintBoxDragSnapshot(pixel, info?.shiftKey);
|
|
3308
3473
|
};
|
|
3309
3474
|
/** Reset drag-in-flight bookkeeping shared by every "gesture ended" path. */
|
|
3310
3475
|
function endDragBookkeeping() {
|
|
@@ -3312,7 +3477,7 @@ function runEdit(opts) {
|
|
|
3312
3477
|
dragInFlight = false;
|
|
3313
3478
|
drainPendingModes();
|
|
3314
3479
|
}
|
|
3315
|
-
const onDriverUp = (pixel) => {
|
|
3480
|
+
const onDriverUp = (pixel, info) => {
|
|
3316
3481
|
if (activeLabelRotateDrag) {
|
|
3317
3482
|
const drag = activeLabelRotateDrag;
|
|
3318
3483
|
activeLabelRotateDrag = null;
|
|
@@ -3321,7 +3486,7 @@ function runEdit(opts) {
|
|
|
3321
3486
|
if (Math.hypot(pixel[0] - drag.downPixel[0], pixel[1] - drag.downPixel[1]) > CLICK_SLOP_PX$3) {
|
|
3322
3487
|
const delta = pointerAngleAround(pixel, drag.anchorPixel) - drag.startPointerAngle;
|
|
3323
3488
|
const rotation = drag.startRotation - delta;
|
|
3324
|
-
setAmplifierPlacements({ [drag.key]: { rotation } });
|
|
3489
|
+
setAmplifierPlacements({ [drag.key]: { rotation } }, "gesture", "Rotate label");
|
|
3325
3490
|
} else {
|
|
3326
3491
|
renderPreview();
|
|
3327
3492
|
renderHandlesLayer();
|
|
@@ -3335,7 +3500,7 @@ function runEdit(opts) {
|
|
|
3335
3500
|
const position = labelAnchorFromPointer(pixel, grabOffset);
|
|
3336
3501
|
workingAmplifierPlacements = previous;
|
|
3337
3502
|
endDragBookkeeping();
|
|
3338
|
-
if (position && Math.hypot(pixel[0] - downPixel[0], pixel[1] - downPixel[1]) > CLICK_SLOP_PX$3) setAmplifierPlacements({ [field]: position });
|
|
3503
|
+
if (position && Math.hypot(pixel[0] - downPixel[0], pixel[1] - downPixel[1]) > CLICK_SLOP_PX$3) setAmplifierPlacements({ [field]: position }, "gesture", "Move label");
|
|
3339
3504
|
else {
|
|
3340
3505
|
selectedLabelKey = selectedLabelKey === field ? null : field;
|
|
3341
3506
|
renderPreview();
|
|
@@ -3380,11 +3545,11 @@ function runEdit(opts) {
|
|
|
3380
3545
|
bodyClickPending = true;
|
|
3381
3546
|
return;
|
|
3382
3547
|
}
|
|
3383
|
-
const finalSnapshot = fsm.move(pixel);
|
|
3548
|
+
const finalSnapshot = fsm.move(pixel, { constrainRotation: info?.shiftKey });
|
|
3384
3549
|
replaceWorkingPoints(preDragPoints);
|
|
3385
3550
|
workingAmplifierPlacements = preDragAmplifierPlacements;
|
|
3386
3551
|
if (fsm.kind === "rotate") boxAngle += finalSnapshot.box.angle - preDragBox.angle;
|
|
3387
|
-
commitGesture(finalSnapshot.points, preDragOptions, transformAmplifierPlacements(preDragAmplifierPlacements, preDragBox, finalSnapshot.box, driver));
|
|
3552
|
+
commitGesture(finalSnapshot.points, preDragOptions, transformAmplifierPlacements(preDragAmplifierPlacements, preDragBox, finalSnapshot.box, driver), preDragBox.angle);
|
|
3388
3553
|
return;
|
|
3389
3554
|
}
|
|
3390
3555
|
};
|
|
@@ -3435,8 +3600,12 @@ function runEdit(opts) {
|
|
|
3435
3600
|
],
|
|
3436
3601
|
onViewChange: rescaleOnViewChange,
|
|
3437
3602
|
onSettled: (settlement) => {
|
|
3438
|
-
|
|
3439
|
-
|
|
3603
|
+
try {
|
|
3604
|
+
history.settle();
|
|
3605
|
+
} finally {
|
|
3606
|
+
if (onSettled) onSettled();
|
|
3607
|
+
settleFromInteraction(settlement, commitEmitter, settleEmitter);
|
|
3608
|
+
}
|
|
3440
3609
|
},
|
|
3441
3610
|
attach(c) {
|
|
3442
3611
|
ctx = c;
|
|
@@ -3641,6 +3810,25 @@ function runGroupTransform(opts) {
|
|
|
3641
3810
|
const settleEmitter = createSettleEmitter("TransformSession.onSettled listener errors");
|
|
3642
3811
|
/** Session-scoped box rotation (ADR-0017 decision #2), shared by the whole group. */
|
|
3643
3812
|
let boxAngle = 0;
|
|
3813
|
+
function captureHistory() {
|
|
3814
|
+
return {
|
|
3815
|
+
ids: members.map((member) => member.measure.id),
|
|
3816
|
+
points: clonePointsByMember(),
|
|
3817
|
+
amplifierPlacements: cloneAmplifierPlacementsByMember(),
|
|
3818
|
+
optionsOverrides: cloneOptionsOverrideByMember(),
|
|
3819
|
+
sizeAnchor,
|
|
3820
|
+
nonGeometryDirty,
|
|
3821
|
+
boxAngle
|
|
3822
|
+
};
|
|
3823
|
+
}
|
|
3824
|
+
function restoreHistory(value) {
|
|
3825
|
+
restorePointsByMember(value.points);
|
|
3826
|
+
applyAmplifierPlacementsByMember(value.amplifierPlacements.map((placements) => placements ? structuredClone(placements) : void 0));
|
|
3827
|
+
applyOptionsOverrides(value.optionsOverrides);
|
|
3828
|
+
sizeAnchor = value.sizeAnchor;
|
|
3829
|
+
nonGeometryDirty = value.nonGeometryDirty;
|
|
3830
|
+
boxAngle = value.boxAngle;
|
|
3831
|
+
}
|
|
3644
3832
|
/** The group box fitted over every member's rendered geometry, or `null` when nothing projects this frame. */
|
|
3645
3833
|
let groupBox = null;
|
|
3646
3834
|
let ctx;
|
|
@@ -3817,12 +4005,13 @@ function runGroupTransform(opts) {
|
|
|
3817
4005
|
* instance to all listeners in order, deferred `reject`/`close`/`abort`
|
|
3818
4006
|
* (close/abort override reject), exception collection with `AggregateError`.
|
|
3819
4007
|
*/
|
|
3820
|
-
function emitChange(previousRendered, previousPointsByMember, previousAmplifierPlacementsByMember, previousSizeAnchor, previousNonGeometryDirty, previousOptionsOverrideByMember, currentRendered) {
|
|
4008
|
+
function emitChange(previousRendered, previousPointsByMember, previousAmplifierPlacementsByMember, previousSizeAnchor, previousNonGeometryDirty, previousOptionsOverrideByMember, currentRendered, source = "gesture", historyLabel = "Transform", previousBoxAngle = boxAngle) {
|
|
3821
4009
|
if (ctx.settled) return;
|
|
3822
4010
|
let rejectRequested = false;
|
|
3823
4011
|
let closeRequested = false;
|
|
3824
4012
|
let abortRequested = false;
|
|
3825
4013
|
const event = {
|
|
4014
|
+
source,
|
|
3826
4015
|
graphics: currentRendered,
|
|
3827
4016
|
previous: previousRendered,
|
|
3828
4017
|
session,
|
|
@@ -3851,10 +4040,12 @@ function runGroupTransform(opts) {
|
|
|
3851
4040
|
sizeAnchor = previousSizeAnchor;
|
|
3852
4041
|
nonGeometryDirty = previousNonGeometryDirty;
|
|
3853
4042
|
applyOptionsOverrides(previousOptionsOverrideByMember);
|
|
4043
|
+
boxAngle = previousBoxAngle;
|
|
3854
4044
|
renderAllPreviews();
|
|
3855
4045
|
renderHandlesLayer();
|
|
3856
4046
|
recomputeDirty();
|
|
3857
4047
|
}
|
|
4048
|
+
if (!ctx.settled && !rejectRequested && members === membersAtEmission && (source === "gesture" || source === "programmatic")) history.record(captureHistory(), historyLabel);
|
|
3858
4049
|
if (errors.length === 1) throw errors[0];
|
|
3859
4050
|
if (errors.length > 1) throw new AggregateError(errors, "TransformSession.onChange listener errors");
|
|
3860
4051
|
}
|
|
@@ -3880,7 +4071,7 @@ function runGroupTransform(opts) {
|
|
|
3880
4071
|
}
|
|
3881
4072
|
return snapshots;
|
|
3882
4073
|
}
|
|
3883
|
-
function commitGesture(nextConcatPoints, preGestureOptionsOverrideByMember, nextAmplifierPlacementsByMember) {
|
|
4074
|
+
function commitGesture(nextConcatPoints, preGestureOptionsOverrideByMember, nextAmplifierPlacementsByMember, preGestureBoxAngle = boxAngle) {
|
|
3884
4075
|
if (ctx.settled) return;
|
|
3885
4076
|
const previousPointsByMember = clonePointsByMember();
|
|
3886
4077
|
const previousAmplifierPlacementsByMember = cloneAmplifierPlacementsByMember();
|
|
@@ -3901,7 +4092,7 @@ function runGroupTransform(opts) {
|
|
|
3901
4092
|
renderHandlesLayer();
|
|
3902
4093
|
recomputeDirty();
|
|
3903
4094
|
dragInFlight = false;
|
|
3904
|
-
emitChange(previousRendered, previousPointsByMember, previousAmplifierPlacementsByMember, sizeAnchor, nonGeometryDirty, previousOptionsOverrideByMember, currentRendered);
|
|
4095
|
+
emitChange(previousRendered, previousPointsByMember, previousAmplifierPlacementsByMember, sizeAnchor, nonGeometryDirty, previousOptionsOverrideByMember, currentRendered, "gesture", "Transform", preGestureBoxAngle);
|
|
3905
4096
|
}
|
|
3906
4097
|
function setSizeAnchor(next) {
|
|
3907
4098
|
if (ctx.settled || sizeAnchor === next) return;
|
|
@@ -3915,7 +4106,7 @@ function runGroupTransform(opts) {
|
|
|
3915
4106
|
const currentRendered = rebuildRenderedAll();
|
|
3916
4107
|
nonGeometryDirty = true;
|
|
3917
4108
|
dirty = true;
|
|
3918
|
-
emitChange(previousRendered, previousPointsByMember, previousAmplifierPlacementsByMember, previousSizeAnchor, previousNonGeometryDirty, previousOptionsOverrideByMember, currentRendered);
|
|
4109
|
+
emitChange(previousRendered, previousPointsByMember, previousAmplifierPlacementsByMember, previousSizeAnchor, previousNonGeometryDirty, previousOptionsOverrideByMember, currentRendered, "programmatic", "Change size anchor");
|
|
3919
4110
|
}
|
|
3920
4111
|
/**
|
|
3921
4112
|
* Cancel an in-flight box drag without committing it: restore the pre-drag
|
|
@@ -3962,13 +4153,42 @@ function runGroupTransform(opts) {
|
|
|
3962
4153
|
renderAllPreviews();
|
|
3963
4154
|
renderHandlesLayer();
|
|
3964
4155
|
recomputeDirty();
|
|
3965
|
-
|
|
4156
|
+
const notificationErrors = [];
|
|
4157
|
+
try {
|
|
4158
|
+
history.reset(captureHistory());
|
|
4159
|
+
} catch (error) {
|
|
4160
|
+
notificationErrors.push(error);
|
|
4161
|
+
}
|
|
4162
|
+
if (removedSnapshots.length > 0) try {
|
|
4163
|
+
commitEmitter.emit(removedSnapshots);
|
|
4164
|
+
} catch (error) {
|
|
4165
|
+
notificationErrors.push(error);
|
|
4166
|
+
}
|
|
4167
|
+
if (notificationErrors.length === 1) throw notificationErrors[0];
|
|
4168
|
+
if (notificationErrors.length > 1) throw new AggregateError(notificationErrors, "TransformSession.setGraphics notification errors");
|
|
3966
4169
|
return removedSnapshots;
|
|
3967
4170
|
}
|
|
3968
4171
|
function close() {
|
|
3969
4172
|
return commitClose();
|
|
3970
4173
|
}
|
|
4174
|
+
const history = createSessionHistory({
|
|
4175
|
+
initial: captureHistory(),
|
|
4176
|
+
clone: (value) => structuredClone(value),
|
|
4177
|
+
equals: (a, b) => JSON.stringify(a) === JSON.stringify(b),
|
|
4178
|
+
isCurrent: (value) => JSON.stringify(captureHistory()) === JSON.stringify(value),
|
|
4179
|
+
apply(from, to, source) {
|
|
4180
|
+
if (JSON.stringify(from.ids) !== JSON.stringify(to.ids)) return false;
|
|
4181
|
+
const previousRendered = rebuildRenderedAll();
|
|
4182
|
+
restoreHistory(to);
|
|
4183
|
+
renderAllPreviews();
|
|
4184
|
+
renderHandlesLayer();
|
|
4185
|
+
recomputeDirty();
|
|
4186
|
+
emitChange(previousRendered, from.points, from.amplifierPlacements, from.sizeAnchor, from.nonGeometryDirty, from.optionsOverrides, rebuildRenderedAll(), source, "Transform", from.boxAngle);
|
|
4187
|
+
return !ctx.settled && JSON.stringify(captureHistory()) === JSON.stringify(to);
|
|
4188
|
+
}
|
|
4189
|
+
});
|
|
3971
4190
|
const session = {
|
|
4191
|
+
history,
|
|
3972
4192
|
get graphics() {
|
|
3973
4193
|
return members.map((m) => m.measure);
|
|
3974
4194
|
},
|
|
@@ -4044,9 +4264,9 @@ function runGroupTransform(opts) {
|
|
|
4044
4264
|
dragInFlight = true;
|
|
4045
4265
|
driver.setDragPanEnabled(false);
|
|
4046
4266
|
};
|
|
4047
|
-
function paintBoxDragSnapshot(pixel) {
|
|
4267
|
+
function paintBoxDragSnapshot(pixel, constrainRotation = false) {
|
|
4048
4268
|
if (!activeBoxDrag) return;
|
|
4049
|
-
const snapshot = activeBoxDrag.fsm.move(pixel);
|
|
4269
|
+
const snapshot = activeBoxDrag.fsm.move(pixel, { constrainRotation });
|
|
4050
4270
|
activeBoxDrag.lastSnapshot = snapshot;
|
|
4051
4271
|
applyConcatenatedPoints(snapshot.points);
|
|
4052
4272
|
applyAmplifierPlacementsByMember(activeBoxDrag.preDragAmplifierPlacementsByMember.map((placements) => transformAmplifierPlacements(placements, activeBoxDrag.preDragBox, snapshot.box, driver)));
|
|
@@ -4054,11 +4274,11 @@ function runGroupTransform(opts) {
|
|
|
4054
4274
|
renderHandlesLayer();
|
|
4055
4275
|
recomputeDirty();
|
|
4056
4276
|
}
|
|
4057
|
-
const onDriverMove = (pixel) => {
|
|
4277
|
+
const onDriverMove = (pixel, info) => {
|
|
4058
4278
|
if (ctx.settled) return;
|
|
4059
|
-
if (activeBoxDrag) paintBoxDragSnapshot(pixel);
|
|
4279
|
+
if (activeBoxDrag) paintBoxDragSnapshot(pixel, info?.shiftKey);
|
|
4060
4280
|
};
|
|
4061
|
-
const onDriverUp = (pixel) => {
|
|
4281
|
+
const onDriverUp = (pixel, info) => {
|
|
4062
4282
|
if (!activeBoxDrag) return;
|
|
4063
4283
|
if (ctx.settled) {
|
|
4064
4284
|
activeBoxDrag = null;
|
|
@@ -4080,11 +4300,11 @@ function runGroupTransform(opts) {
|
|
|
4080
4300
|
bodyClickPending = true;
|
|
4081
4301
|
return;
|
|
4082
4302
|
}
|
|
4083
|
-
const finalSnapshot = fsm.move(pixel);
|
|
4303
|
+
const finalSnapshot = fsm.move(pixel, { constrainRotation: info?.shiftKey });
|
|
4084
4304
|
applyConcatenatedPoints(preDragPoints);
|
|
4085
4305
|
applyAmplifierPlacementsByMember(preDragAmplifierPlacementsByMember);
|
|
4086
4306
|
if (fsm.kind === "rotate") boxAngle += finalSnapshot.box.angle - preDragBox.angle;
|
|
4087
|
-
commitGesture(finalSnapshot.points, preDragOptionsOverrideByMember, preDragAmplifierPlacementsByMember.map((placements) => transformAmplifierPlacements(placements, preDragBox, finalSnapshot.box, driver)));
|
|
4307
|
+
commitGesture(finalSnapshot.points, preDragOptionsOverrideByMember, preDragAmplifierPlacementsByMember.map((placements) => transformAmplifierPlacements(placements, preDragBox, finalSnapshot.box, driver)), preDragBox.angle);
|
|
4088
4308
|
};
|
|
4089
4309
|
let pickPreview;
|
|
4090
4310
|
let pickHandles;
|
|
@@ -4130,8 +4350,12 @@ function runGroupTransform(opts) {
|
|
|
4130
4350
|
layers: [previewLayer, handlesLayer],
|
|
4131
4351
|
onViewChange: rescaleOnViewChange,
|
|
4132
4352
|
onSettled: (settlement) => {
|
|
4133
|
-
|
|
4134
|
-
|
|
4353
|
+
try {
|
|
4354
|
+
history.settle();
|
|
4355
|
+
} finally {
|
|
4356
|
+
if (onSettled) onSettled();
|
|
4357
|
+
if (settlement.status === "rejected") settleEmitter.emit(settleReasonFromError(settlement.error));
|
|
4358
|
+
}
|
|
4135
4359
|
},
|
|
4136
4360
|
attach(c) {
|
|
4137
4361
|
ctx = c;
|
|
@@ -4245,13 +4469,6 @@ function pointSymbolFeature(symbol, render, resolution) {
|
|
|
4245
4469
|
};
|
|
4246
4470
|
}
|
|
4247
4471
|
//#endregion
|
|
4248
|
-
//#region src/tactical-draw/rotation.ts
|
|
4249
|
-
/** Normalize radians to the canonical clockwise [0, 2π) range. */
|
|
4250
|
-
function normalizeRotation(angle) {
|
|
4251
|
-
const turn = Math.PI * 2;
|
|
4252
|
-
return (angle % turn + turn) % turn;
|
|
4253
|
-
}
|
|
4254
|
-
//#endregion
|
|
4255
4472
|
//#region src/tactical-draw/graphic-transform-controller.ts
|
|
4256
4473
|
const HIT_TOLERANCE_PX$1 = 12;
|
|
4257
4474
|
const CLICK_SLOP_PX$1 = 3;
|
|
@@ -4272,13 +4489,22 @@ function runGraphicTransform(opts) {
|
|
|
4272
4489
|
const commitEmitter = createCommitEmitter("GraphicTransformSession.onCommit listener errors");
|
|
4273
4490
|
const settleEmitter = createSettleEmitter("GraphicTransformSession.onSettled listener errors");
|
|
4274
4491
|
const renderOpts = graphicsStyle ? { graphicsStyle } : void 0;
|
|
4275
|
-
const
|
|
4492
|
+
const snapshotsFor = (source) => source.map((graphic) => isPointSymbol(graphic) ? {
|
|
4276
4493
|
graphic: clonePointSymbol(graphic),
|
|
4277
4494
|
render: capability.render(graphic)
|
|
4278
4495
|
} : {
|
|
4279
4496
|
graphic: cloneControlMeasure(graphic),
|
|
4280
4497
|
render: renderMeasureForAdapter(adapter, graphic, renderOpts)
|
|
4281
4498
|
});
|
|
4499
|
+
const snapshots = () => snapshotsFor(working);
|
|
4500
|
+
const captureHistory = () => ({
|
|
4501
|
+
working: working.map(cloneGraphic),
|
|
4502
|
+
boxAngle
|
|
4503
|
+
});
|
|
4504
|
+
const cloneHistory = (value) => ({
|
|
4505
|
+
working: value.working.map(cloneGraphic),
|
|
4506
|
+
boxAngle: value.boxAngle
|
|
4507
|
+
});
|
|
4282
4508
|
function renderAll() {
|
|
4283
4509
|
if (ctx.settled) return;
|
|
4284
4510
|
const features = [];
|
|
@@ -4345,12 +4571,14 @@ function runGraphicTransform(opts) {
|
|
|
4345
4571
|
return next;
|
|
4346
4572
|
});
|
|
4347
4573
|
}
|
|
4348
|
-
function emit(
|
|
4574
|
+
function emit(previousState, source) {
|
|
4575
|
+
const previous = snapshotsFor(previousState.working);
|
|
4349
4576
|
const current = snapshots();
|
|
4350
4577
|
let reject = false;
|
|
4351
4578
|
let close = false;
|
|
4352
4579
|
let abort = false;
|
|
4353
4580
|
const event = {
|
|
4581
|
+
source,
|
|
4354
4582
|
graphics: current,
|
|
4355
4583
|
previous,
|
|
4356
4584
|
session,
|
|
@@ -4367,14 +4595,30 @@ function runGraphicTransform(opts) {
|
|
|
4367
4595
|
if (abort) ctx.fail(new TacticalDrawAbortError("session"));
|
|
4368
4596
|
else if (close) ctx.commit(snapshots);
|
|
4369
4597
|
else if (reject && !ctx.settled) {
|
|
4370
|
-
working =
|
|
4598
|
+
working = previousState.working.map(cloneGraphic);
|
|
4599
|
+
boxAngle = previousState.boxAngle;
|
|
4371
4600
|
dirty = JSON.stringify(working) !== JSON.stringify(original);
|
|
4372
4601
|
renderAll();
|
|
4373
4602
|
}
|
|
4374
4603
|
if (errors.length === 1) throw errors[0];
|
|
4375
4604
|
if (errors.length > 1) throw new AggregateError(errors, "GraphicTransformSession.onChange errors");
|
|
4376
|
-
|
|
4605
|
+
return !ctx.settled && !reject;
|
|
4606
|
+
}
|
|
4607
|
+
const history = createSessionHistory({
|
|
4608
|
+
initial: captureHistory(),
|
|
4609
|
+
clone: cloneHistory,
|
|
4610
|
+
equals: (a, b) => a.boxAngle === b.boxAngle && JSON.stringify(a.working) === JSON.stringify(b.working),
|
|
4611
|
+
isCurrent: (value) => boxAngle === value.boxAngle && JSON.stringify(working) === JSON.stringify(value.working),
|
|
4612
|
+
apply(from, to, source) {
|
|
4613
|
+
working = to.working.map(cloneGraphic);
|
|
4614
|
+
boxAngle = to.boxAngle;
|
|
4615
|
+
dirty = JSON.stringify(working) !== JSON.stringify(original);
|
|
4616
|
+
renderAll();
|
|
4617
|
+
return emit(from, source);
|
|
4618
|
+
}
|
|
4619
|
+
});
|
|
4377
4620
|
const session = {
|
|
4621
|
+
history,
|
|
4378
4622
|
get graphics() {
|
|
4379
4623
|
return original.map(cloneGraphic);
|
|
4380
4624
|
},
|
|
@@ -4421,9 +4665,9 @@ function runGraphicTransform(opts) {
|
|
|
4421
4665
|
};
|
|
4422
4666
|
driver.setDragPanEnabled(false);
|
|
4423
4667
|
};
|
|
4424
|
-
const dragResult = (pixel) => {
|
|
4668
|
+
const dragResult = (pixel, constrainRotation = false) => {
|
|
4425
4669
|
if (!drag) return null;
|
|
4426
|
-
const result = drag.fsm.move(pixel);
|
|
4670
|
+
const result = drag.fsm.move(pixel, { constrainRotation });
|
|
4427
4671
|
const scale = drag.fsm.kind === "scale" ? Math.max(result.box.halfWidth / drag.startBox.halfWidth, result.box.halfHeight / drag.startBox.halfHeight) : 1;
|
|
4428
4672
|
const rotation = drag.fsm.kind === "rotate" ? result.box.angle - drag.startBox.angle : 0;
|
|
4429
4673
|
return {
|
|
@@ -4431,18 +4675,18 @@ function runGraphicTransform(opts) {
|
|
|
4431
4675
|
graphics: applyPoints(drag.start, result.points, scale, rotation)
|
|
4432
4676
|
};
|
|
4433
4677
|
};
|
|
4434
|
-
const onMove = (pixel) => {
|
|
4678
|
+
const onMove = (pixel, info) => {
|
|
4435
4679
|
const active = drag;
|
|
4436
|
-
const next = dragResult(pixel);
|
|
4680
|
+
const next = dragResult(pixel, info?.shiftKey);
|
|
4437
4681
|
if (!active || !next) return;
|
|
4438
4682
|
working = next.graphics;
|
|
4439
4683
|
active.liveBox = next.result.box;
|
|
4440
4684
|
renderAll();
|
|
4441
4685
|
};
|
|
4442
|
-
const onUp = (pixel) => {
|
|
4686
|
+
const onUp = (pixel, info) => {
|
|
4443
4687
|
if (!drag) return;
|
|
4444
4688
|
const active = drag;
|
|
4445
|
-
const next = dragResult(pixel);
|
|
4689
|
+
const next = dragResult(pixel, info?.shiftKey);
|
|
4446
4690
|
drag = null;
|
|
4447
4691
|
suppressClickAway = true;
|
|
4448
4692
|
driver.setDragPanEnabled(true);
|
|
@@ -4452,13 +4696,15 @@ function runGraphicTransform(opts) {
|
|
|
4452
4696
|
renderAll();
|
|
4453
4697
|
return;
|
|
4454
4698
|
}
|
|
4455
|
-
const
|
|
4456
|
-
const previousGraphics = working.map(cloneGraphic);
|
|
4699
|
+
const previousState = captureHistory();
|
|
4457
4700
|
working = next.graphics;
|
|
4458
4701
|
if (active.fsm.kind === "rotate") boxAngle += next.result.box.angle - active.startBox.angle;
|
|
4459
4702
|
dirty = JSON.stringify(working) !== JSON.stringify(original);
|
|
4460
4703
|
renderAll();
|
|
4461
|
-
emit(
|
|
4704
|
+
if (emit(previousState, "gesture")) {
|
|
4705
|
+
const label = `${active.fsm.kind[0].toUpperCase()}${active.fsm.kind.slice(1)}`;
|
|
4706
|
+
history.record(captureHistory(), label);
|
|
4707
|
+
}
|
|
4462
4708
|
};
|
|
4463
4709
|
let previewPick;
|
|
4464
4710
|
let handlesPick;
|
|
@@ -4481,8 +4727,12 @@ function runGraphicTransform(opts) {
|
|
|
4481
4727
|
layers: [previewLayer, handlesLayer],
|
|
4482
4728
|
onViewChange: renderAll,
|
|
4483
4729
|
onSettled(settlement) {
|
|
4484
|
-
|
|
4485
|
-
|
|
4730
|
+
try {
|
|
4731
|
+
history.settle();
|
|
4732
|
+
} finally {
|
|
4733
|
+
onSettled?.();
|
|
4734
|
+
settleFromInteraction(settlement, commitEmitter, settleEmitter);
|
|
4735
|
+
}
|
|
4486
4736
|
},
|
|
4487
4737
|
attach(interaction) {
|
|
4488
4738
|
ctx = interaction;
|
|
@@ -4632,6 +4882,8 @@ function runPointSymbolEdit(opts) {
|
|
|
4632
4882
|
const listeners = /* @__PURE__ */ new Set();
|
|
4633
4883
|
const commitEmitter = createCommitEmitter("GraphicEditSession.onCommit listener errors");
|
|
4634
4884
|
const settleEmitter = createSettleEmitter("GraphicEditSession.onSettled listener errors");
|
|
4885
|
+
const captureHistory = () => ({ working: clonePointSymbol(working) });
|
|
4886
|
+
const cloneHistory = (value) => ({ working: clonePointSymbol(value.working) });
|
|
4635
4887
|
const snapshot = () => ({
|
|
4636
4888
|
graphic: clonePointSymbol(working),
|
|
4637
4889
|
render: capability.render(working)
|
|
@@ -4649,15 +4901,17 @@ function runPointSymbolEdit(opts) {
|
|
|
4649
4901
|
}
|
|
4650
4902
|
ctx.writeLayer(handlesLayer, handles);
|
|
4651
4903
|
};
|
|
4652
|
-
const
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4904
|
+
const emitChange = (previousState, source) => {
|
|
4905
|
+
const previous = {
|
|
4906
|
+
graphic: clonePointSymbol(previousState.working),
|
|
4907
|
+
render: capability.render(previousState.working)
|
|
4908
|
+
};
|
|
4656
4909
|
const current = snapshot();
|
|
4657
4910
|
let reject = false;
|
|
4658
4911
|
let close = false;
|
|
4659
4912
|
let abort = false;
|
|
4660
4913
|
const event = {
|
|
4914
|
+
source,
|
|
4661
4915
|
graphic: current,
|
|
4662
4916
|
previous,
|
|
4663
4917
|
session,
|
|
@@ -4680,14 +4934,35 @@ function runPointSymbolEdit(opts) {
|
|
|
4680
4934
|
if (abort) ctx.fail(new TacticalDrawAbortError("session"));
|
|
4681
4935
|
else if (close) ctx.commit(snapshot);
|
|
4682
4936
|
else if (reject && !ctx.settled) {
|
|
4683
|
-
working = clonePointSymbol(
|
|
4937
|
+
working = clonePointSymbol(previousState.working);
|
|
4684
4938
|
dirty = JSON.stringify(working) !== JSON.stringify(symbol);
|
|
4685
4939
|
render();
|
|
4686
4940
|
}
|
|
4687
4941
|
if (errors.length === 1) throw errors[0];
|
|
4688
4942
|
if (errors.length > 1) throw new AggregateError(errors, "GraphicEditSession.onChange errors");
|
|
4943
|
+
return !ctx.settled && !reject;
|
|
4944
|
+
};
|
|
4945
|
+
const history = createSessionHistory({
|
|
4946
|
+
initial: captureHistory(),
|
|
4947
|
+
clone: cloneHistory,
|
|
4948
|
+
equals: (a, b) => JSON.stringify(a.working) === JSON.stringify(b.working),
|
|
4949
|
+
isCurrent: (value) => JSON.stringify(working) === JSON.stringify(value.working),
|
|
4950
|
+
apply(from, to, source) {
|
|
4951
|
+
working = clonePointSymbol(to.working);
|
|
4952
|
+
dirty = JSON.stringify(working) !== JSON.stringify(symbol);
|
|
4953
|
+
render();
|
|
4954
|
+
return emitChange(from, source);
|
|
4955
|
+
}
|
|
4956
|
+
});
|
|
4957
|
+
const apply = (next, source = "programmatic", label = "Update graphic") => {
|
|
4958
|
+
const previousState = captureHistory();
|
|
4959
|
+
working = clonePointSymbol(next);
|
|
4960
|
+
dirty = JSON.stringify(working) !== JSON.stringify(symbol);
|
|
4961
|
+
render();
|
|
4962
|
+
if (emitChange(previousState, source)) history.record(captureHistory(), label);
|
|
4689
4963
|
};
|
|
4690
4964
|
const session = {
|
|
4965
|
+
history,
|
|
4691
4966
|
graphic: clonePointSymbol(symbol),
|
|
4692
4967
|
get workingGraphic() {
|
|
4693
4968
|
return clonePointSymbol(working);
|
|
@@ -4711,11 +4986,10 @@ function runPointSymbolEdit(opts) {
|
|
|
4711
4986
|
},
|
|
4712
4987
|
updateGraphic(updater) {
|
|
4713
4988
|
if (ctx.settled) return;
|
|
4714
|
-
const previous = snapshot();
|
|
4715
4989
|
const next = updater(clonePointSymbol(working));
|
|
4716
4990
|
if (next.kind !== "point-symbol" || next.id !== symbol.id) throw new TypeError("updateGraphic must preserve point-symbol kind and id");
|
|
4717
4991
|
capability.render(next);
|
|
4718
|
-
apply(next,
|
|
4992
|
+
apply(next, "programmatic", "Update graphic");
|
|
4719
4993
|
},
|
|
4720
4994
|
onChange(listener) {
|
|
4721
4995
|
if (ctx.settled) return () => {};
|
|
@@ -4747,27 +5021,29 @@ function runPointSymbolEdit(opts) {
|
|
|
4747
5021
|
};
|
|
4748
5022
|
driver.setDragPanEnabled(false);
|
|
4749
5023
|
};
|
|
4750
|
-
const transformed = (pixel) => {
|
|
5024
|
+
const transformed = (pixel, constrainAngle = false) => {
|
|
4751
5025
|
if (!drag) return null;
|
|
4752
5026
|
const next = clonePointSymbol(drag.start);
|
|
4753
5027
|
if (drag.kind === "body") next.position = driver.fromPixel([drag.anchor[0] + pixel[0] - drag.down[0], drag.anchor[1] + pixel[1] - drag.down[1]]);
|
|
4754
|
-
else if (drag.kind === "rotate")
|
|
4755
|
-
|
|
5028
|
+
else if (drag.kind === "rotate") {
|
|
5029
|
+
const rotation = drag.start.rotation + pointerAngle(pixel, drag.anchor) - drag.startPointerAngle;
|
|
5030
|
+
next.rotation = normalizeRotation(constrainAngle ? constrainRotation(rotation) : rotation);
|
|
5031
|
+
} else {
|
|
4756
5032
|
const distance = Math.hypot(pixel[0] - drag.anchor[0], pixel[1] - drag.anchor[1]);
|
|
4757
5033
|
next.size.value = Math.max(.1, drag.start.size.value * (distance / drag.startDistance));
|
|
4758
5034
|
}
|
|
4759
5035
|
return next;
|
|
4760
5036
|
};
|
|
4761
|
-
const onMove = (pixel) => {
|
|
4762
|
-
const next = transformed(pixel);
|
|
5037
|
+
const onMove = (pixel, info) => {
|
|
5038
|
+
const next = transformed(pixel, info?.shiftKey);
|
|
4763
5039
|
if (!next) return;
|
|
4764
5040
|
working = next;
|
|
4765
5041
|
render();
|
|
4766
5042
|
};
|
|
4767
|
-
const onUp = (pixel) => {
|
|
5043
|
+
const onUp = (pixel, info) => {
|
|
4768
5044
|
if (!drag) return;
|
|
4769
5045
|
const currentDrag = drag;
|
|
4770
|
-
const next = transformed(pixel);
|
|
5046
|
+
const next = transformed(pixel, info?.shiftKey);
|
|
4771
5047
|
drag = null;
|
|
4772
5048
|
suppressClickAway = true;
|
|
4773
5049
|
driver.setDragPanEnabled(true);
|
|
@@ -4777,7 +5053,7 @@ function runPointSymbolEdit(opts) {
|
|
|
4777
5053
|
render();
|
|
4778
5054
|
return;
|
|
4779
5055
|
}
|
|
4780
|
-
apply(next);
|
|
5056
|
+
apply(next, "gesture", `${currentDrag.kind[0].toUpperCase()}${currentDrag.kind.slice(1)}`);
|
|
4781
5057
|
};
|
|
4782
5058
|
let previewPick;
|
|
4783
5059
|
let handlesPick;
|
|
@@ -4803,8 +5079,12 @@ function runPointSymbolEdit(opts) {
|
|
|
4803
5079
|
layers: [previewLayer, handlesLayer],
|
|
4804
5080
|
onViewChange: render,
|
|
4805
5081
|
onSettled(settlement) {
|
|
4806
|
-
|
|
4807
|
-
|
|
5082
|
+
try {
|
|
5083
|
+
history.settle();
|
|
5084
|
+
} finally {
|
|
5085
|
+
onSettled?.();
|
|
5086
|
+
settleFromInteraction(settlement, commitEmitter, settleEmitter);
|
|
5087
|
+
}
|
|
4808
5088
|
},
|
|
4809
5089
|
attach(interaction) {
|
|
4810
5090
|
ctx = interaction;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orbat-mapper/tactical-draw",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.22",
|
|
4
4
|
"description": "Engine-agnostic draw/edit core for tactical control measures (MIL-STD-2525 / APP-6). The MapAdapter ABI and bring-your-own-engine entry point for the tactical-draw package family.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Orbat Mapper",
|