@fieldnotes/core 0.13.0 → 0.15.0
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.cjs +523 -50
- package/dist/index.d.cts +74 -4
- package/dist/index.d.ts +74 -4
- package/dist/index.js +520 -50
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -285,16 +285,24 @@ function sanitizeAttributes(el, tag) {
|
|
|
285
285
|
|
|
286
286
|
// src/core/state-serializer.ts
|
|
287
287
|
var CURRENT_VERSION = 2;
|
|
288
|
-
function exportState(elements, camera, layers = []) {
|
|
289
|
-
|
|
288
|
+
function exportState(elements, camera, layers = [], activeLayerId) {
|
|
289
|
+
const state = {
|
|
290
290
|
version: CURRENT_VERSION,
|
|
291
291
|
camera: {
|
|
292
292
|
position: { ...camera.position },
|
|
293
293
|
zoom: camera.zoom
|
|
294
294
|
},
|
|
295
|
-
elements: elements.map((el) =>
|
|
295
|
+
elements: elements.map((el) => {
|
|
296
|
+
const clone = structuredClone(el);
|
|
297
|
+
if (clone.type === "arrow") {
|
|
298
|
+
delete clone.cachedControlPoint;
|
|
299
|
+
}
|
|
300
|
+
return clone;
|
|
301
|
+
}),
|
|
296
302
|
layers: layers.map((l) => ({ ...l }))
|
|
297
303
|
};
|
|
304
|
+
if (activeLayerId) state.activeLayerId = activeLayerId;
|
|
305
|
+
return state;
|
|
298
306
|
}
|
|
299
307
|
function parseState(json) {
|
|
300
308
|
const data = JSON.parse(json);
|
|
@@ -452,12 +460,14 @@ var AutoSave = class {
|
|
|
452
460
|
this.key = options.key ?? DEFAULT_KEY;
|
|
453
461
|
this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
454
462
|
this.layerManager = options.layerManager;
|
|
463
|
+
this.onError = options.onError;
|
|
455
464
|
}
|
|
456
465
|
key;
|
|
457
466
|
debounceMs;
|
|
458
467
|
layerManager;
|
|
459
468
|
timerId = null;
|
|
460
469
|
unsubscribers = [];
|
|
470
|
+
onError;
|
|
461
471
|
start() {
|
|
462
472
|
const schedule = () => this.scheduleSave();
|
|
463
473
|
this.unsubscribers = [
|
|
@@ -505,8 +515,9 @@ var AutoSave = class {
|
|
|
505
515
|
const state = exportState(this.store.snapshot(), this.camera, layers);
|
|
506
516
|
try {
|
|
507
517
|
localStorage.setItem(this.key, JSON.stringify(state));
|
|
508
|
-
} catch {
|
|
518
|
+
} catch (e) {
|
|
509
519
|
console.warn("Auto-save failed: storage quota exceeded. State too large for localStorage.");
|
|
520
|
+
this.onError?.(e instanceof Error ? e : new Error(String(e)));
|
|
510
521
|
}
|
|
511
522
|
}
|
|
512
523
|
};
|
|
@@ -575,6 +586,15 @@ var Camera = class {
|
|
|
575
586
|
h: bottomRight.y - topLeft.y
|
|
576
587
|
};
|
|
577
588
|
}
|
|
589
|
+
fitToContent(boundingBox, canvasWidth, canvasHeight, padding = 40) {
|
|
590
|
+
if (boundingBox.w === 0 && boundingBox.h === 0) return;
|
|
591
|
+
const scaleX = canvasWidth / (boundingBox.w + 2 * padding);
|
|
592
|
+
const scaleY = canvasHeight / (boundingBox.h + 2 * padding);
|
|
593
|
+
this.z = Math.min(this.maxZoom, Math.max(this.minZoom, Math.min(scaleX, scaleY)));
|
|
594
|
+
this.x = (canvasWidth - boundingBox.w * this.z) / 2 - boundingBox.x * this.z;
|
|
595
|
+
this.y = (canvasHeight - boundingBox.h * this.z) / 2 - boundingBox.y * this.z;
|
|
596
|
+
this.notifyPanAndZoom();
|
|
597
|
+
}
|
|
578
598
|
toCSSTransform() {
|
|
579
599
|
return `translate3d(${this.x}px, ${this.y}px, 0) scale(${this.z})`;
|
|
580
600
|
}
|
|
@@ -730,6 +750,67 @@ var Background = class {
|
|
|
730
750
|
}
|
|
731
751
|
};
|
|
732
752
|
|
|
753
|
+
// src/canvas/input-filter.ts
|
|
754
|
+
var InputFilter = class _InputFilter {
|
|
755
|
+
activePenId = null;
|
|
756
|
+
pendingTap = null;
|
|
757
|
+
static MIN_MOVE_DISTANCE = 3;
|
|
758
|
+
filterDown(e) {
|
|
759
|
+
if (e.pointerType === "pen") {
|
|
760
|
+
this.activePenId = e.pointerId;
|
|
761
|
+
return { event: e, action: "dispatch" };
|
|
762
|
+
}
|
|
763
|
+
if (e.pointerType === "touch" && this.activePenId !== null) {
|
|
764
|
+
return { event: e, action: "suppress" };
|
|
765
|
+
}
|
|
766
|
+
if (e.pointerType === "touch") {
|
|
767
|
+
this.pendingTap = { pointerId: e.pointerId, x: e.clientX, y: e.clientY };
|
|
768
|
+
return { event: e, action: "defer" };
|
|
769
|
+
}
|
|
770
|
+
return { event: e, action: "dispatch" };
|
|
771
|
+
}
|
|
772
|
+
filterMove(e) {
|
|
773
|
+
if (e.pointerType === "touch" && this.activePenId !== null) {
|
|
774
|
+
return { event: e, action: "suppress" };
|
|
775
|
+
}
|
|
776
|
+
if (this.pendingTap && e.pointerId === this.pendingTap.pointerId) {
|
|
777
|
+
const dx = e.clientX - this.pendingTap.x;
|
|
778
|
+
const dy = e.clientY - this.pendingTap.y;
|
|
779
|
+
if (dx * dx + dy * dy > _InputFilter.MIN_MOVE_DISTANCE * _InputFilter.MIN_MOVE_DISTANCE) {
|
|
780
|
+
this.pendingTap = null;
|
|
781
|
+
return { event: e, action: "dispatch" };
|
|
782
|
+
}
|
|
783
|
+
return { event: e, action: "suppress" };
|
|
784
|
+
}
|
|
785
|
+
return { event: e, action: "dispatch" };
|
|
786
|
+
}
|
|
787
|
+
filterUp(e) {
|
|
788
|
+
if (e.pointerId === this.activePenId) {
|
|
789
|
+
this.activePenId = null;
|
|
790
|
+
return { event: e, action: "dispatch" };
|
|
791
|
+
}
|
|
792
|
+
if (e.pointerType === "touch" && this.activePenId !== null) {
|
|
793
|
+
return { event: e, action: "suppress" };
|
|
794
|
+
}
|
|
795
|
+
if (this.pendingTap && e.pointerId === this.pendingTap.pointerId) {
|
|
796
|
+
const tap = { x: this.pendingTap.x, y: this.pendingTap.y };
|
|
797
|
+
this.pendingTap = null;
|
|
798
|
+
return { event: e, action: "dispatch", pendingTap: tap };
|
|
799
|
+
}
|
|
800
|
+
return { event: e, action: "dispatch" };
|
|
801
|
+
}
|
|
802
|
+
reset() {
|
|
803
|
+
this.activePenId = null;
|
|
804
|
+
this.pendingTap = null;
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
// src/elements/create-id.ts
|
|
809
|
+
var counter = 0;
|
|
810
|
+
function createId(prefix) {
|
|
811
|
+
return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
|
|
812
|
+
}
|
|
813
|
+
|
|
733
814
|
// src/canvas/input-handler.ts
|
|
734
815
|
var ZOOM_SENSITIVITY = 1e-3;
|
|
735
816
|
var MIDDLE_BUTTON = 1;
|
|
@@ -755,13 +836,21 @@ var InputHandler = class {
|
|
|
755
836
|
historyRecorder;
|
|
756
837
|
historyStack;
|
|
757
838
|
isToolActive = false;
|
|
839
|
+
lastPointerEvent = null;
|
|
840
|
+
inputFilter = new InputFilter();
|
|
841
|
+
deferredDown = null;
|
|
758
842
|
abortController = new AbortController();
|
|
843
|
+
clipboard = [];
|
|
844
|
+
pasteCount = 0;
|
|
759
845
|
setToolManager(toolManager, toolContext) {
|
|
760
846
|
this.toolManager = toolManager;
|
|
761
847
|
this.toolContext = toolContext;
|
|
762
848
|
}
|
|
763
849
|
destroy() {
|
|
764
850
|
this.abortController.abort();
|
|
851
|
+
this.inputFilter.reset();
|
|
852
|
+
this.deferredDown = null;
|
|
853
|
+
this.lastPointerEvent = null;
|
|
765
854
|
}
|
|
766
855
|
bind() {
|
|
767
856
|
const opts = { signal: this.abortController.signal };
|
|
@@ -797,11 +886,18 @@ var InputHandler = class {
|
|
|
797
886
|
this.lastPointer = { x: e.clientX, y: e.clientY };
|
|
798
887
|
return;
|
|
799
888
|
}
|
|
800
|
-
if (this.activePointers.size === 1 && e.button === 0) {
|
|
889
|
+
if (this.activePointers.size === 1 && (e.button === 0 || e.pointerType === "touch" || e.pointerType === "pen")) {
|
|
890
|
+
const result = this.inputFilter.filterDown(e);
|
|
891
|
+
if (result.action === "suppress") return;
|
|
892
|
+
if (result.action === "defer") {
|
|
893
|
+
this.deferredDown = e;
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
801
896
|
this.dispatchToolDown(e);
|
|
802
897
|
}
|
|
803
898
|
};
|
|
804
899
|
onPointerMove = (e) => {
|
|
900
|
+
this.lastPointerEvent = e;
|
|
805
901
|
if (this.activePointers.has(e.pointerId)) {
|
|
806
902
|
this.activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
807
903
|
}
|
|
@@ -816,13 +912,26 @@ var InputHandler = class {
|
|
|
816
912
|
this.camera.pan(dx, dy);
|
|
817
913
|
return;
|
|
818
914
|
}
|
|
819
|
-
if (this.
|
|
915
|
+
if (e.pointerType === "pen" && !this.activePointers.has(e.pointerId)) {
|
|
916
|
+
this.dispatchToolHover(e);
|
|
917
|
+
} else if (this.isToolActive) {
|
|
820
918
|
this.dispatchToolMove(e);
|
|
919
|
+
} else if (this.deferredDown) {
|
|
920
|
+
const result = this.inputFilter.filterMove(e);
|
|
921
|
+
if (result.action === "dispatch") {
|
|
922
|
+
this.dispatchToolDown(this.deferredDown);
|
|
923
|
+
this.deferredDown = null;
|
|
924
|
+
this.dispatchToolMove(e);
|
|
925
|
+
}
|
|
821
926
|
} else if (this.activePointers.size === 0) {
|
|
822
927
|
this.dispatchToolHover(e);
|
|
823
928
|
}
|
|
824
929
|
};
|
|
825
930
|
onPointerUp = (e) => {
|
|
931
|
+
try {
|
|
932
|
+
this.element.releasePointerCapture(e.pointerId);
|
|
933
|
+
} catch {
|
|
934
|
+
}
|
|
826
935
|
this.activePointers.delete(e.pointerId);
|
|
827
936
|
if (this.activePointers.size < 2) {
|
|
828
937
|
this.lastPinchDistance = 0;
|
|
@@ -830,9 +939,16 @@ var InputHandler = class {
|
|
|
830
939
|
if (this.isPanning && this.activePointers.size === 0) {
|
|
831
940
|
this.isPanning = false;
|
|
832
941
|
}
|
|
942
|
+
const upResult = this.inputFilter.filterUp(e);
|
|
833
943
|
if (this.isToolActive) {
|
|
834
944
|
this.dispatchToolUp(e);
|
|
835
945
|
this.isToolActive = false;
|
|
946
|
+
} else if (this.deferredDown && upResult.pendingTap) {
|
|
947
|
+
this.dispatchToolDown(this.deferredDown);
|
|
948
|
+
this.dispatchToolUp(e);
|
|
949
|
+
this.deferredDown = null;
|
|
950
|
+
} else {
|
|
951
|
+
this.deferredDown = null;
|
|
836
952
|
}
|
|
837
953
|
};
|
|
838
954
|
onKeyDown = (e) => {
|
|
@@ -851,13 +967,38 @@ var InputHandler = class {
|
|
|
851
967
|
e.preventDefault();
|
|
852
968
|
this.handleRedo();
|
|
853
969
|
}
|
|
970
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "c") {
|
|
971
|
+
e.preventDefault();
|
|
972
|
+
this.handleCopy();
|
|
973
|
+
}
|
|
974
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "v") {
|
|
975
|
+
e.preventDefault();
|
|
976
|
+
this.handlePaste();
|
|
977
|
+
}
|
|
978
|
+
if (e.key === "]") {
|
|
979
|
+
e.preventDefault();
|
|
980
|
+
this.handleZOrder(e.ctrlKey || e.metaKey ? "front" : "forward");
|
|
981
|
+
}
|
|
982
|
+
if (e.key === "[") {
|
|
983
|
+
e.preventDefault();
|
|
984
|
+
this.handleZOrder(e.ctrlKey || e.metaKey ? "back" : "backward");
|
|
985
|
+
}
|
|
854
986
|
};
|
|
855
987
|
onKeyUp = (e) => {
|
|
856
988
|
if (e.key === " ") {
|
|
857
989
|
this.spaceHeld = false;
|
|
990
|
+
if (this.activePointers.size === 0) {
|
|
991
|
+
if (this.lastPointerEvent) {
|
|
992
|
+
this.dispatchToolHover(this.lastPointerEvent);
|
|
993
|
+
} else {
|
|
994
|
+
this.toolContext?.setCursor?.("default");
|
|
995
|
+
}
|
|
996
|
+
}
|
|
858
997
|
}
|
|
859
998
|
};
|
|
860
999
|
startPinch() {
|
|
1000
|
+
this.inputFilter.reset();
|
|
1001
|
+
this.deferredDown = null;
|
|
861
1002
|
this.isPanning = true;
|
|
862
1003
|
const [a, b] = this.getPinchPoints();
|
|
863
1004
|
this.lastPinchDistance = this.distance(a, b);
|
|
@@ -897,7 +1038,9 @@ var InputHandler = class {
|
|
|
897
1038
|
return {
|
|
898
1039
|
x: e.clientX - rect.left,
|
|
899
1040
|
y: e.clientY - rect.top,
|
|
900
|
-
pressure: e.pressure
|
|
1041
|
+
pressure: e.pressure,
|
|
1042
|
+
pointerType: e.pointerType === "touch" || e.pointerType === "pen" ? e.pointerType : "mouse",
|
|
1043
|
+
shiftKey: e.shiftKey
|
|
901
1044
|
};
|
|
902
1045
|
}
|
|
903
1046
|
dispatchToolDown(e) {
|
|
@@ -950,11 +1093,147 @@ var InputHandler = class {
|
|
|
950
1093
|
this.historyRecorder?.resume();
|
|
951
1094
|
this.toolContext.requestRender();
|
|
952
1095
|
}
|
|
1096
|
+
handleCopy() {
|
|
1097
|
+
if (!this.toolManager || !this.toolContext || this.isToolActive) return;
|
|
1098
|
+
const tool = this.toolManager.activeTool;
|
|
1099
|
+
if (tool?.name !== "select") return;
|
|
1100
|
+
const selectTool = tool;
|
|
1101
|
+
const ids = selectTool.selectedIds;
|
|
1102
|
+
if (ids.length === 0) return;
|
|
1103
|
+
this.clipboard = [];
|
|
1104
|
+
for (const id of ids) {
|
|
1105
|
+
const el = this.toolContext.store.getById(id);
|
|
1106
|
+
if (el) this.clipboard.push(structuredClone(el));
|
|
1107
|
+
}
|
|
1108
|
+
this.pasteCount = 0;
|
|
1109
|
+
}
|
|
1110
|
+
handlePaste() {
|
|
1111
|
+
if (!this.toolManager || !this.toolContext || this.clipboard.length === 0 || this.isToolActive)
|
|
1112
|
+
return;
|
|
1113
|
+
const tool = this.toolManager.activeTool;
|
|
1114
|
+
if (tool?.name !== "select") return;
|
|
1115
|
+
const selectTool = tool;
|
|
1116
|
+
this.pasteCount++;
|
|
1117
|
+
const offset = this.pasteCount * 20;
|
|
1118
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
1119
|
+
for (const el of this.clipboard) {
|
|
1120
|
+
idMap.set(el.id, createId(el.type));
|
|
1121
|
+
}
|
|
1122
|
+
const newIds = [];
|
|
1123
|
+
this.historyRecorder?.begin();
|
|
1124
|
+
for (const el of this.clipboard) {
|
|
1125
|
+
const clone = structuredClone(el);
|
|
1126
|
+
const newId = idMap.get(el.id);
|
|
1127
|
+
if (!newId) continue;
|
|
1128
|
+
clone.id = newId;
|
|
1129
|
+
clone.position = { x: clone.position.x + offset, y: clone.position.y + offset };
|
|
1130
|
+
if (clone.type === "arrow") {
|
|
1131
|
+
const arrow = clone;
|
|
1132
|
+
arrow.from = { x: arrow.from.x + offset, y: arrow.from.y + offset };
|
|
1133
|
+
arrow.to = { x: arrow.to.x + offset, y: arrow.to.y + offset };
|
|
1134
|
+
delete arrow.cachedControlPoint;
|
|
1135
|
+
if (arrow.fromBinding) {
|
|
1136
|
+
const newTarget = idMap.get(arrow.fromBinding.elementId);
|
|
1137
|
+
if (newTarget) {
|
|
1138
|
+
arrow.fromBinding = { elementId: newTarget };
|
|
1139
|
+
} else {
|
|
1140
|
+
delete arrow.fromBinding;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
if (arrow.toBinding) {
|
|
1144
|
+
const newTarget = idMap.get(arrow.toBinding.elementId);
|
|
1145
|
+
if (newTarget) {
|
|
1146
|
+
arrow.toBinding = { elementId: newTarget };
|
|
1147
|
+
} else {
|
|
1148
|
+
delete arrow.toBinding;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
if (this.toolContext.activeLayerId) {
|
|
1153
|
+
clone.layerId = this.toolContext.activeLayerId;
|
|
1154
|
+
}
|
|
1155
|
+
this.toolContext.store.add(clone);
|
|
1156
|
+
newIds.push(clone.id);
|
|
1157
|
+
}
|
|
1158
|
+
this.historyRecorder?.commit();
|
|
1159
|
+
selectTool.setSelection(newIds);
|
|
1160
|
+
this.toolContext.requestRender();
|
|
1161
|
+
}
|
|
1162
|
+
handleZOrder(operation) {
|
|
1163
|
+
if (!this.toolManager || !this.toolContext) return;
|
|
1164
|
+
const tool = this.toolManager.activeTool;
|
|
1165
|
+
if (tool?.name !== "select") return;
|
|
1166
|
+
const selectTool = tool;
|
|
1167
|
+
const ids = selectTool.selectedIds;
|
|
1168
|
+
if (ids.length === 0) return;
|
|
1169
|
+
this.historyRecorder?.begin();
|
|
1170
|
+
for (const id of ids) {
|
|
1171
|
+
switch (operation) {
|
|
1172
|
+
case "forward":
|
|
1173
|
+
this.toolContext.store.bringForward(id);
|
|
1174
|
+
break;
|
|
1175
|
+
case "backward":
|
|
1176
|
+
this.toolContext.store.sendBackward(id);
|
|
1177
|
+
break;
|
|
1178
|
+
case "front":
|
|
1179
|
+
this.toolContext.store.bringToFront(id);
|
|
1180
|
+
break;
|
|
1181
|
+
case "back":
|
|
1182
|
+
this.toolContext.store.sendToBack(id);
|
|
1183
|
+
break;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
this.historyRecorder?.commit();
|
|
1187
|
+
this.toolContext.requestRender();
|
|
1188
|
+
}
|
|
953
1189
|
cancelToolIfActive(e) {
|
|
954
1190
|
if (this.isToolActive) {
|
|
955
1191
|
this.dispatchToolUp(e);
|
|
956
1192
|
this.isToolActive = false;
|
|
957
1193
|
}
|
|
1194
|
+
this.deferredDown = null;
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
// src/canvas/double-tap-detector.ts
|
|
1199
|
+
var DEFAULT_TIMEOUT = 300;
|
|
1200
|
+
var DEFAULT_MAX_DISTANCE = 20;
|
|
1201
|
+
var DoubleTapDetector = class {
|
|
1202
|
+
timeout;
|
|
1203
|
+
maxDistance;
|
|
1204
|
+
lastTapTime = 0;
|
|
1205
|
+
lastTapX = 0;
|
|
1206
|
+
lastTapY = 0;
|
|
1207
|
+
hasPendingTap = false;
|
|
1208
|
+
constructor(options) {
|
|
1209
|
+
this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
|
|
1210
|
+
this.maxDistance = options?.maxDistance ?? DEFAULT_MAX_DISTANCE;
|
|
1211
|
+
}
|
|
1212
|
+
feed(e) {
|
|
1213
|
+
const now = Date.now();
|
|
1214
|
+
const x = e.clientX;
|
|
1215
|
+
const y = e.clientY;
|
|
1216
|
+
if (this.hasPendingTap) {
|
|
1217
|
+
const elapsed = now - this.lastTapTime;
|
|
1218
|
+
const dx = x - this.lastTapX;
|
|
1219
|
+
const dy = y - this.lastTapY;
|
|
1220
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
1221
|
+
if (elapsed <= this.timeout && dist <= this.maxDistance) {
|
|
1222
|
+
this.reset();
|
|
1223
|
+
return true;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
this.lastTapTime = now;
|
|
1227
|
+
this.lastTapX = x;
|
|
1228
|
+
this.lastTapY = y;
|
|
1229
|
+
this.hasPendingTap = true;
|
|
1230
|
+
return false;
|
|
1231
|
+
}
|
|
1232
|
+
reset() {
|
|
1233
|
+
this.hasPendingTap = false;
|
|
1234
|
+
this.lastTapTime = 0;
|
|
1235
|
+
this.lastTapX = 0;
|
|
1236
|
+
this.lastTapY = 0;
|
|
958
1237
|
}
|
|
959
1238
|
};
|
|
960
1239
|
|
|
@@ -1202,19 +1481,27 @@ var ElementStore = class {
|
|
|
1202
1481
|
bus = new EventBus();
|
|
1203
1482
|
layerOrderMap = /* @__PURE__ */ new Map();
|
|
1204
1483
|
spatialIndex = new Quadtree({ x: -1e5, y: -1e5, w: 2e5, h: 2e5 });
|
|
1484
|
+
sortedCache = null;
|
|
1485
|
+
_versions = /* @__PURE__ */ new Map();
|
|
1205
1486
|
get count() {
|
|
1206
1487
|
return this.elements.size;
|
|
1207
1488
|
}
|
|
1489
|
+
getVersion(id) {
|
|
1490
|
+
return this._versions.get(id) ?? -1;
|
|
1491
|
+
}
|
|
1208
1492
|
setLayerOrder(order) {
|
|
1209
1493
|
this.layerOrderMap = new Map(order);
|
|
1494
|
+
this.sortedCache = null;
|
|
1210
1495
|
}
|
|
1211
1496
|
getAll() {
|
|
1212
|
-
|
|
1497
|
+
if (this.sortedCache) return this.sortedCache;
|
|
1498
|
+
this.sortedCache = [...this.elements.values()].sort((a, b) => {
|
|
1213
1499
|
const layerA = this.layerOrderMap.get(a.layerId) ?? 0;
|
|
1214
1500
|
const layerB = this.layerOrderMap.get(b.layerId) ?? 0;
|
|
1215
1501
|
if (layerA !== layerB) return layerA - layerB;
|
|
1216
1502
|
return a.zIndex - b.zIndex;
|
|
1217
1503
|
});
|
|
1504
|
+
return this.sortedCache;
|
|
1218
1505
|
}
|
|
1219
1506
|
getById(id) {
|
|
1220
1507
|
return this.elements.get(id);
|
|
@@ -1225,6 +1512,8 @@ var ElementStore = class {
|
|
|
1225
1512
|
);
|
|
1226
1513
|
}
|
|
1227
1514
|
add(element) {
|
|
1515
|
+
this.sortedCache = null;
|
|
1516
|
+
this._versions.set(element.id, 0);
|
|
1228
1517
|
this.elements.set(element.id, element);
|
|
1229
1518
|
const bounds = getElementBounds(element);
|
|
1230
1519
|
if (bounds) this.spatialIndex.insert(element.id, bounds);
|
|
@@ -1233,11 +1522,16 @@ var ElementStore = class {
|
|
|
1233
1522
|
update(id, partial) {
|
|
1234
1523
|
const existing = this.elements.get(id);
|
|
1235
1524
|
if (!existing) return;
|
|
1525
|
+
this.sortedCache = null;
|
|
1526
|
+
this._versions.set(id, (this._versions.get(id) ?? 0) + 1);
|
|
1236
1527
|
const updated = { ...existing, ...partial, id: existing.id, type: existing.type };
|
|
1237
1528
|
if (updated.type === "arrow") {
|
|
1238
1529
|
const arrow = updated;
|
|
1239
1530
|
arrow.cachedControlPoint = getArrowControlPoint(arrow.from, arrow.to, arrow.bend);
|
|
1240
1531
|
}
|
|
1532
|
+
if (updated.type === "note" && "text" in partial) {
|
|
1533
|
+
updated.text = sanitizeNoteHtml(updated.text);
|
|
1534
|
+
}
|
|
1241
1535
|
this.elements.set(id, updated);
|
|
1242
1536
|
const newBounds = getElementBounds(updated);
|
|
1243
1537
|
if (newBounds) {
|
|
@@ -1248,11 +1542,15 @@ var ElementStore = class {
|
|
|
1248
1542
|
remove(id) {
|
|
1249
1543
|
const element = this.elements.get(id);
|
|
1250
1544
|
if (!element) return;
|
|
1545
|
+
this.sortedCache = null;
|
|
1546
|
+
this._versions.delete(id);
|
|
1251
1547
|
this.elements.delete(id);
|
|
1252
1548
|
this.spatialIndex.remove(id);
|
|
1253
1549
|
this.bus.emit("remove", element);
|
|
1254
1550
|
}
|
|
1255
1551
|
clear() {
|
|
1552
|
+
this.sortedCache = null;
|
|
1553
|
+
this._versions.clear();
|
|
1256
1554
|
this.elements.clear();
|
|
1257
1555
|
this.spatialIndex.clear();
|
|
1258
1556
|
this.bus.emit("clear", null);
|
|
@@ -1261,13 +1559,68 @@ var ElementStore = class {
|
|
|
1261
1559
|
return this.getAll().map((el) => ({ ...el }));
|
|
1262
1560
|
}
|
|
1263
1561
|
loadSnapshot(elements) {
|
|
1562
|
+
this.sortedCache = null;
|
|
1563
|
+
this._versions.clear();
|
|
1264
1564
|
this.elements.clear();
|
|
1265
1565
|
this.spatialIndex.clear();
|
|
1266
1566
|
for (const el of elements) {
|
|
1267
1567
|
this.elements.set(el.id, el);
|
|
1568
|
+
this._versions.set(el.id, 0);
|
|
1268
1569
|
const bounds = getElementBounds(el);
|
|
1269
1570
|
if (bounds) this.spatialIndex.insert(el.id, bounds);
|
|
1270
1571
|
}
|
|
1572
|
+
this.bus.emit("clear", null);
|
|
1573
|
+
for (const el of elements) {
|
|
1574
|
+
this.bus.emit("add", el);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
bringToFront(id) {
|
|
1578
|
+
const el = this.elements.get(id);
|
|
1579
|
+
if (!el) return;
|
|
1580
|
+
const siblings = [...this.elements.values()].filter(
|
|
1581
|
+
(e) => e.layerId === el.layerId && e.id !== id
|
|
1582
|
+
);
|
|
1583
|
+
if (siblings.length === 0) return;
|
|
1584
|
+
const maxZ = Math.max(...siblings.map((e) => e.zIndex));
|
|
1585
|
+
if (el.zIndex >= maxZ) return;
|
|
1586
|
+
this.update(id, { zIndex: maxZ + 1 });
|
|
1587
|
+
}
|
|
1588
|
+
sendToBack(id) {
|
|
1589
|
+
const el = this.elements.get(id);
|
|
1590
|
+
if (!el) return;
|
|
1591
|
+
const siblings = [...this.elements.values()].filter(
|
|
1592
|
+
(e) => e.layerId === el.layerId && e.id !== id
|
|
1593
|
+
);
|
|
1594
|
+
if (siblings.length === 0) return;
|
|
1595
|
+
const minZ = Math.min(...siblings.map((e) => e.zIndex));
|
|
1596
|
+
if (el.zIndex <= minZ) return;
|
|
1597
|
+
this.update(id, { zIndex: minZ - 1 });
|
|
1598
|
+
}
|
|
1599
|
+
bringForward(id) {
|
|
1600
|
+
const el = this.elements.get(id);
|
|
1601
|
+
if (!el) return;
|
|
1602
|
+
const sorted = [...this.elements.values()].filter((e) => e.layerId === el.layerId).sort((a, b) => a.zIndex - b.zIndex);
|
|
1603
|
+
const idx = sorted.findIndex((e) => e.id === id);
|
|
1604
|
+
if (idx < 0 || idx >= sorted.length - 1) return;
|
|
1605
|
+
const next = sorted[idx + 1];
|
|
1606
|
+
if (!next) return;
|
|
1607
|
+
const myZ = el.zIndex;
|
|
1608
|
+
const nextZ = next.zIndex;
|
|
1609
|
+
this.update(id, { zIndex: nextZ });
|
|
1610
|
+
this.update(next.id, { zIndex: myZ });
|
|
1611
|
+
}
|
|
1612
|
+
sendBackward(id) {
|
|
1613
|
+
const el = this.elements.get(id);
|
|
1614
|
+
if (!el) return;
|
|
1615
|
+
const sorted = [...this.elements.values()].filter((e) => e.layerId === el.layerId).sort((a, b) => a.zIndex - b.zIndex);
|
|
1616
|
+
const idx = sorted.findIndex((e) => e.id === id);
|
|
1617
|
+
if (idx <= 0) return;
|
|
1618
|
+
const prev = sorted[idx - 1];
|
|
1619
|
+
if (!prev) return;
|
|
1620
|
+
const myZ = el.zIndex;
|
|
1621
|
+
const prevZ = prev.zIndex;
|
|
1622
|
+
this.update(id, { zIndex: prevZ });
|
|
1623
|
+
this.update(prev.id, { zIndex: myZ });
|
|
1271
1624
|
}
|
|
1272
1625
|
queryRect(rect) {
|
|
1273
1626
|
const ids = this.spatialIndex.query(rect);
|
|
@@ -2317,12 +2670,6 @@ var ElementRenderer = class {
|
|
|
2317
2670
|
}
|
|
2318
2671
|
};
|
|
2319
2672
|
|
|
2320
|
-
// src/elements/create-id.ts
|
|
2321
|
-
var counter = 0;
|
|
2322
|
-
function createId(prefix) {
|
|
2323
|
-
return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
|
|
2324
|
-
}
|
|
2325
|
-
|
|
2326
2673
|
// src/elements/element-factory.ts
|
|
2327
2674
|
var DEFAULT_NOTE_FONT_SIZE = 18;
|
|
2328
2675
|
function createStroke(input) {
|
|
@@ -2348,7 +2695,7 @@ function createNote(input) {
|
|
|
2348
2695
|
locked: input.locked ?? false,
|
|
2349
2696
|
layerId: input.layerId ?? "",
|
|
2350
2697
|
size: input.size ?? { w: 200, h: 100 },
|
|
2351
|
-
text: input.text ?? "",
|
|
2698
|
+
text: sanitizeNoteHtml(input.text ?? ""),
|
|
2352
2699
|
backgroundColor: input.backgroundColor ?? "#ffeb3b",
|
|
2353
2700
|
textColor: input.textColor ?? "#000000",
|
|
2354
2701
|
fontSize: input.fontSize ?? DEFAULT_NOTE_FONT_SIZE
|
|
@@ -2397,6 +2744,7 @@ function createHtmlElement(input) {
|
|
|
2397
2744
|
size: input.size
|
|
2398
2745
|
};
|
|
2399
2746
|
if (input.domId) el.domId = input.domId;
|
|
2747
|
+
if (input.interactive) el.interactive = input.interactive;
|
|
2400
2748
|
return el;
|
|
2401
2749
|
}
|
|
2402
2750
|
function createShape(input) {
|
|
@@ -2509,7 +2857,7 @@ function getActiveFormats() {
|
|
|
2509
2857
|
}
|
|
2510
2858
|
|
|
2511
2859
|
// src/elements/note-toolbar.ts
|
|
2512
|
-
var TOOLBAR_HEIGHT =
|
|
2860
|
+
var TOOLBAR_HEIGHT = 52;
|
|
2513
2861
|
var TOOLBAR_GAP = 4;
|
|
2514
2862
|
var FORMAT_BUTTONS = [
|
|
2515
2863
|
{ label: "B", format: "bold", command: "bold" },
|
|
@@ -2596,9 +2944,9 @@ var NoteToolbar = class {
|
|
|
2596
2944
|
fontWeight: config.format === "bold" ? "bold" : "normal",
|
|
2597
2945
|
fontStyle: config.format === "italic" ? "italic" : "normal",
|
|
2598
2946
|
textDecoration: config.format === "underline" ? "underline" : config.format === "strikethrough" ? "line-through" : "none",
|
|
2599
|
-
minWidth: "
|
|
2600
|
-
height: "
|
|
2601
|
-
lineHeight: "
|
|
2947
|
+
minWidth: "44px",
|
|
2948
|
+
height: "44px",
|
|
2949
|
+
lineHeight: "44px"
|
|
2602
2950
|
});
|
|
2603
2951
|
btn.addEventListener("pointerdown", (e) => {
|
|
2604
2952
|
e.preventDefault();
|
|
@@ -2616,7 +2964,7 @@ var NoteToolbar = class {
|
|
|
2616
2964
|
cursor: "pointer",
|
|
2617
2965
|
padding: "2px",
|
|
2618
2966
|
fontSize: "12px",
|
|
2619
|
-
height: "
|
|
2967
|
+
height: "44px",
|
|
2620
2968
|
marginLeft: "4px"
|
|
2621
2969
|
});
|
|
2622
2970
|
for (const preset of this.fontSizePresets) {
|
|
@@ -3536,10 +3884,14 @@ var DomNodeManager = class {
|
|
|
3536
3884
|
domLayer;
|
|
3537
3885
|
onEditRequest;
|
|
3538
3886
|
isEditingElement;
|
|
3887
|
+
getVersion;
|
|
3888
|
+
lastSyncedVersion = /* @__PURE__ */ new Map();
|
|
3889
|
+
lastSyncedZIndex = /* @__PURE__ */ new Map();
|
|
3539
3890
|
constructor(deps) {
|
|
3540
3891
|
this.domLayer = deps.domLayer;
|
|
3541
3892
|
this.onEditRequest = deps.onEditRequest;
|
|
3542
3893
|
this.isEditingElement = deps.isEditingElement;
|
|
3894
|
+
this.getVersion = deps.getVersion ?? null;
|
|
3543
3895
|
}
|
|
3544
3896
|
getNode(id) {
|
|
3545
3897
|
return this.domNodes.get(id);
|
|
@@ -3558,6 +3910,17 @@ var DomNodeManager = class {
|
|
|
3558
3910
|
});
|
|
3559
3911
|
this.domLayer.appendChild(node);
|
|
3560
3912
|
this.domNodes.set(element.id, node);
|
|
3913
|
+
} else if (this.getVersion) {
|
|
3914
|
+
const currentVersion = this.getVersion(element.id);
|
|
3915
|
+
const lastVersion = this.lastSyncedVersion.get(element.id);
|
|
3916
|
+
const lastZ = this.lastSyncedZIndex.get(element.id);
|
|
3917
|
+
if (lastVersion === currentVersion && lastZ === zIndex) {
|
|
3918
|
+
return;
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3921
|
+
if (this.getVersion) {
|
|
3922
|
+
this.lastSyncedVersion.set(element.id, this.getVersion(element.id));
|
|
3923
|
+
this.lastSyncedZIndex.set(element.id, zIndex);
|
|
3561
3924
|
}
|
|
3562
3925
|
const size = "size" in element ? element.size : null;
|
|
3563
3926
|
Object.assign(node.style, {
|
|
@@ -3576,6 +3939,8 @@ var DomNodeManager = class {
|
|
|
3576
3939
|
}
|
|
3577
3940
|
removeDomNode(id) {
|
|
3578
3941
|
this.htmlContent.delete(id);
|
|
3942
|
+
this.lastSyncedVersion.delete(id);
|
|
3943
|
+
this.lastSyncedZIndex.delete(id);
|
|
3579
3944
|
const node = this.domNodes.get(id);
|
|
3580
3945
|
if (node) {
|
|
3581
3946
|
node.remove();
|
|
@@ -3586,6 +3951,8 @@ var DomNodeManager = class {
|
|
|
3586
3951
|
this.domNodes.forEach((node) => node.remove());
|
|
3587
3952
|
this.domNodes.clear();
|
|
3588
3953
|
this.htmlContent.clear();
|
|
3954
|
+
this.lastSyncedVersion.clear();
|
|
3955
|
+
this.lastSyncedZIndex.clear();
|
|
3589
3956
|
}
|
|
3590
3957
|
reattachHtmlContent(store) {
|
|
3591
3958
|
for (const el of store.getElementsByType("html")) {
|
|
@@ -3614,10 +3981,13 @@ var DomNodeManager = class {
|
|
|
3614
3981
|
wordWrap: "break-word"
|
|
3615
3982
|
});
|
|
3616
3983
|
node.innerHTML = element.text || "";
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3984
|
+
const detector = new DoubleTapDetector();
|
|
3985
|
+
node.addEventListener("pointerup", (e) => {
|
|
3986
|
+
if (detector.feed(e)) {
|
|
3987
|
+
e.stopPropagation();
|
|
3988
|
+
const id = node.dataset["elementId"];
|
|
3989
|
+
if (id) this.onEditRequest(id);
|
|
3990
|
+
}
|
|
3621
3991
|
});
|
|
3622
3992
|
}
|
|
3623
3993
|
if (!this.isEditingElement(element.id)) {
|
|
@@ -3630,15 +4000,19 @@ var DomNodeManager = class {
|
|
|
3630
4000
|
node.style.fontSize = `${element.fontSize ?? DEFAULT_NOTE_FONT_SIZE}px`;
|
|
3631
4001
|
}
|
|
3632
4002
|
}
|
|
3633
|
-
if (element.type === "html"
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
4003
|
+
if (element.type === "html") {
|
|
4004
|
+
if (!node.dataset["initialized"]) {
|
|
4005
|
+
const content = this.htmlContent.get(element.id);
|
|
4006
|
+
if (content) {
|
|
4007
|
+
node.dataset["initialized"] = "true";
|
|
4008
|
+
Object.assign(node.style, {
|
|
4009
|
+
overflow: "hidden",
|
|
4010
|
+
pointerEvents: element.interactive ? "auto" : "none"
|
|
4011
|
+
});
|
|
4012
|
+
node.appendChild(content);
|
|
4013
|
+
}
|
|
4014
|
+
} else {
|
|
4015
|
+
node.style.pointerEvents = element.interactive ? "auto" : "none";
|
|
3642
4016
|
}
|
|
3643
4017
|
}
|
|
3644
4018
|
if (element.type === "text") {
|
|
@@ -3660,10 +4034,13 @@ var DomNodeManager = class {
|
|
|
3660
4034
|
lineHeight: "1.4"
|
|
3661
4035
|
});
|
|
3662
4036
|
node.textContent = element.text || "";
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
4037
|
+
const detector = new DoubleTapDetector();
|
|
4038
|
+
node.addEventListener("pointerup", (e) => {
|
|
4039
|
+
if (detector.feed(e)) {
|
|
4040
|
+
e.stopPropagation();
|
|
4041
|
+
const id = node.dataset["elementId"];
|
|
4042
|
+
if (id) this.onEditRequest(id);
|
|
4043
|
+
}
|
|
3667
4044
|
});
|
|
3668
4045
|
}
|
|
3669
4046
|
if (!this.isEditingElement(element.id)) {
|
|
@@ -4095,7 +4472,8 @@ var Viewport = class {
|
|
|
4095
4472
|
this.domNodeManager = new DomNodeManager({
|
|
4096
4473
|
domLayer: this.domLayer,
|
|
4097
4474
|
onEditRequest: (id) => this.startEditingElement(id),
|
|
4098
|
-
isEditingElement: (id) => this.noteEditor.isEditing && this.noteEditor.editingElementId === id
|
|
4475
|
+
isEditingElement: (id) => this.noteEditor.isEditing && this.noteEditor.editingElementId === id,
|
|
4476
|
+
getVersion: (id) => this.store.getVersion(id)
|
|
4099
4477
|
});
|
|
4100
4478
|
this.interactMode = new InteractMode({
|
|
4101
4479
|
getNode: (id) => this.domNodeManager.getNode(id)
|
|
@@ -4152,7 +4530,8 @@ var Viewport = class {
|
|
|
4152
4530
|
this.toolContext.activeLayerId = this.layerManager.activeLayerId;
|
|
4153
4531
|
this.requestRender();
|
|
4154
4532
|
});
|
|
4155
|
-
this.wrapper.addEventListener("
|
|
4533
|
+
this.wrapper.addEventListener("pointerdown", this.onTapDown);
|
|
4534
|
+
this.wrapper.addEventListener("pointerup", this.onDoubleTap);
|
|
4156
4535
|
this.wrapper.addEventListener("dragover", this.onDragOver);
|
|
4157
4536
|
this.wrapper.addEventListener("drop", this.onDrop);
|
|
4158
4537
|
this.observeResize();
|
|
@@ -4183,6 +4562,9 @@ var Viewport = class {
|
|
|
4183
4562
|
domNodeManager;
|
|
4184
4563
|
interactMode;
|
|
4185
4564
|
gridChangeListeners = /* @__PURE__ */ new Set();
|
|
4565
|
+
doubleTapDetector = new DoubleTapDetector();
|
|
4566
|
+
tapDownX = 0;
|
|
4567
|
+
tapDownY = 0;
|
|
4186
4568
|
get ctx() {
|
|
4187
4569
|
return this.canvasEl.getContext("2d");
|
|
4188
4570
|
}
|
|
@@ -4197,7 +4579,12 @@ var Viewport = class {
|
|
|
4197
4579
|
this.renderLoop.requestRender();
|
|
4198
4580
|
}
|
|
4199
4581
|
exportState() {
|
|
4200
|
-
return exportState(
|
|
4582
|
+
return exportState(
|
|
4583
|
+
this.store.snapshot(),
|
|
4584
|
+
this.camera,
|
|
4585
|
+
this.layerManager.snapshot(),
|
|
4586
|
+
this.layerManager.activeLayerId
|
|
4587
|
+
);
|
|
4201
4588
|
}
|
|
4202
4589
|
exportJSON() {
|
|
4203
4590
|
return JSON.stringify(this.exportState());
|
|
@@ -4213,6 +4600,9 @@ var Viewport = class {
|
|
|
4213
4600
|
if (state.layers && state.layers.length > 0) {
|
|
4214
4601
|
this.layerManager.loadSnapshot(state.layers);
|
|
4215
4602
|
}
|
|
4603
|
+
if (state.activeLayerId) {
|
|
4604
|
+
this.layerManager.setActiveLayer(state.activeLayerId);
|
|
4605
|
+
}
|
|
4216
4606
|
this.domNodeManager.reattachHtmlContent(this.store);
|
|
4217
4607
|
this.history.clear();
|
|
4218
4608
|
this.historyRecorder.resume();
|
|
@@ -4320,7 +4710,8 @@ var Viewport = class {
|
|
|
4320
4710
|
this.interactMode.destroy();
|
|
4321
4711
|
this.noteEditor.destroy(this.store);
|
|
4322
4712
|
this.historyRecorder.destroy();
|
|
4323
|
-
this.wrapper.removeEventListener("
|
|
4713
|
+
this.wrapper.removeEventListener("pointerdown", this.onTapDown);
|
|
4714
|
+
this.wrapper.removeEventListener("pointerup", this.onDoubleTap);
|
|
4324
4715
|
this.wrapper.removeEventListener("dragover", this.onDragOver);
|
|
4325
4716
|
this.wrapper.removeEventListener("drop", this.onDrop);
|
|
4326
4717
|
this.inputHandler.destroy();
|
|
@@ -4358,7 +4749,17 @@ var Viewport = class {
|
|
|
4358
4749
|
}
|
|
4359
4750
|
}
|
|
4360
4751
|
}
|
|
4361
|
-
|
|
4752
|
+
onTapDown = (e) => {
|
|
4753
|
+
this.tapDownX = e.clientX;
|
|
4754
|
+
this.tapDownY = e.clientY;
|
|
4755
|
+
};
|
|
4756
|
+
onDoubleTap = (e) => {
|
|
4757
|
+
const dx = e.clientX - this.tapDownX;
|
|
4758
|
+
const dy = e.clientY - this.tapDownY;
|
|
4759
|
+
const moved = Math.sqrt(dx * dx + dy * dy);
|
|
4760
|
+
if (moved > 10) return;
|
|
4761
|
+
if (!this.doubleTapDetector.feed(e)) return;
|
|
4762
|
+
if (typeof document.elementFromPoint !== "function") return;
|
|
4362
4763
|
const el = document.elementFromPoint(e.clientX, e.clientY);
|
|
4363
4764
|
const nodeEl = el?.closest("[data-element-id]");
|
|
4364
4765
|
if (nodeEl) {
|
|
@@ -4455,7 +4856,10 @@ var Viewport = class {
|
|
|
4455
4856
|
position: "relative",
|
|
4456
4857
|
width: "100%",
|
|
4457
4858
|
height: "100%",
|
|
4458
|
-
overflow: "hidden"
|
|
4859
|
+
overflow: "hidden",
|
|
4860
|
+
overscrollBehavior: "none",
|
|
4861
|
+
userSelect: "none",
|
|
4862
|
+
webkitUserSelect: "none"
|
|
4459
4863
|
});
|
|
4460
4864
|
return el;
|
|
4461
4865
|
}
|
|
@@ -4518,6 +4922,26 @@ var Viewport = class {
|
|
|
4518
4922
|
}
|
|
4519
4923
|
};
|
|
4520
4924
|
|
|
4925
|
+
// src/elements/bounds.ts
|
|
4926
|
+
function getElementsBoundingBox(elements) {
|
|
4927
|
+
let minX = Infinity;
|
|
4928
|
+
let minY = Infinity;
|
|
4929
|
+
let maxX = -Infinity;
|
|
4930
|
+
let maxY = -Infinity;
|
|
4931
|
+
let found = false;
|
|
4932
|
+
for (const el of elements) {
|
|
4933
|
+
const b = getElementBounds(el);
|
|
4934
|
+
if (!b) continue;
|
|
4935
|
+
found = true;
|
|
4936
|
+
if (b.x < minX) minX = b.x;
|
|
4937
|
+
if (b.y < minY) minY = b.y;
|
|
4938
|
+
if (b.x + b.w > maxX) maxX = b.x + b.w;
|
|
4939
|
+
if (b.y + b.h > maxY) maxY = b.y + b.h;
|
|
4940
|
+
}
|
|
4941
|
+
if (!found) return null;
|
|
4942
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
4943
|
+
}
|
|
4944
|
+
|
|
4521
4945
|
// src/tools/hand-tool.ts
|
|
4522
4946
|
var HandTool = class {
|
|
4523
4947
|
name = "hand";
|
|
@@ -4870,9 +5294,16 @@ var SelectTool = class {
|
|
|
4870
5294
|
lastWorld = { x: 0, y: 0 };
|
|
4871
5295
|
currentWorld = { x: 0, y: 0 };
|
|
4872
5296
|
ctx = null;
|
|
5297
|
+
pendingSingleSelectId = null;
|
|
5298
|
+
hasDragged = false;
|
|
5299
|
+
resizeAspectRatio = 0;
|
|
4873
5300
|
get selectedIds() {
|
|
4874
5301
|
return [...this._selectedIds];
|
|
4875
5302
|
}
|
|
5303
|
+
setSelection(ids) {
|
|
5304
|
+
this._selectedIds = ids;
|
|
5305
|
+
this.ctx?.requestRender();
|
|
5306
|
+
}
|
|
4876
5307
|
get isMarqueeActive() {
|
|
4877
5308
|
return this.mode.type === "marquee";
|
|
4878
5309
|
}
|
|
@@ -4911,7 +5342,8 @@ var SelectTool = class {
|
|
|
4911
5342
|
const resizeHit = this.hitTestResizeHandle(world, ctx);
|
|
4912
5343
|
if (resizeHit) {
|
|
4913
5344
|
const el = ctx.store.getById(resizeHit.elementId);
|
|
4914
|
-
if (el) {
|
|
5345
|
+
if (el && "size" in el) {
|
|
5346
|
+
this.resizeAspectRatio = el.size.h > 0 ? el.size.w / el.size.h : 0;
|
|
4915
5347
|
this.mode = {
|
|
4916
5348
|
type: "resizing",
|
|
4917
5349
|
elementId: resizeHit.elementId,
|
|
@@ -4921,13 +5353,27 @@ var SelectTool = class {
|
|
|
4921
5353
|
return;
|
|
4922
5354
|
}
|
|
4923
5355
|
}
|
|
5356
|
+
this.pendingSingleSelectId = null;
|
|
5357
|
+
this.hasDragged = false;
|
|
4924
5358
|
const hit = this.hitTest(world, ctx);
|
|
4925
5359
|
if (hit) {
|
|
4926
5360
|
const alreadySelected = this._selectedIds.includes(hit.id);
|
|
4927
|
-
if (
|
|
4928
|
-
|
|
5361
|
+
if (state.shiftKey) {
|
|
5362
|
+
if (alreadySelected) {
|
|
5363
|
+
this._selectedIds = this._selectedIds.filter((id) => id !== hit.id);
|
|
5364
|
+
this.mode = { type: "idle" };
|
|
5365
|
+
} else {
|
|
5366
|
+
this._selectedIds = [...this._selectedIds, hit.id];
|
|
5367
|
+
this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
|
|
5368
|
+
}
|
|
5369
|
+
} else {
|
|
5370
|
+
if (!alreadySelected) {
|
|
5371
|
+
this._selectedIds = [hit.id];
|
|
5372
|
+
} else if (this._selectedIds.length > 1) {
|
|
5373
|
+
this.pendingSingleSelectId = hit.id;
|
|
5374
|
+
}
|
|
5375
|
+
this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
|
|
4929
5376
|
}
|
|
4930
|
-
this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
|
|
4931
5377
|
} else {
|
|
4932
5378
|
this._selectedIds = [];
|
|
4933
5379
|
this.mode = { type: "marquee", start: world };
|
|
@@ -4949,10 +5395,11 @@ var SelectTool = class {
|
|
|
4949
5395
|
}
|
|
4950
5396
|
if (this.mode.type === "resizing") {
|
|
4951
5397
|
ctx.setCursor?.(HANDLE_CURSORS[this.mode.handle]);
|
|
4952
|
-
this.handleResize(world, ctx);
|
|
5398
|
+
this.handleResize(world, ctx, state.shiftKey);
|
|
4953
5399
|
return;
|
|
4954
5400
|
}
|
|
4955
5401
|
if (this.mode.type === "dragging" && this._selectedIds.length > 0) {
|
|
5402
|
+
this.hasDragged = true;
|
|
4956
5403
|
ctx.setCursor?.("move");
|
|
4957
5404
|
const snapped = this.snap(world, ctx);
|
|
4958
5405
|
const dx = snapped.x - this.lastWorld.x;
|
|
@@ -5021,6 +5468,11 @@ var SelectTool = class {
|
|
|
5021
5468
|
}
|
|
5022
5469
|
ctx.requestRender();
|
|
5023
5470
|
}
|
|
5471
|
+
if (!this.hasDragged && this.pendingSingleSelectId !== null) {
|
|
5472
|
+
this._selectedIds = [this.pendingSingleSelectId];
|
|
5473
|
+
}
|
|
5474
|
+
this.pendingSingleSelectId = null;
|
|
5475
|
+
this.hasDragged = false;
|
|
5024
5476
|
this.mode = { type: "idle" };
|
|
5025
5477
|
ctx.setCursor?.("default");
|
|
5026
5478
|
}
|
|
@@ -5067,7 +5519,7 @@ var SelectTool = class {
|
|
|
5067
5519
|
const hit = this.hitTest(world, ctx);
|
|
5068
5520
|
ctx.setCursor?.(hit ? "move" : "default");
|
|
5069
5521
|
}
|
|
5070
|
-
handleResize(world, ctx) {
|
|
5522
|
+
handleResize(world, ctx, shiftKey = false) {
|
|
5071
5523
|
if (this.mode.type !== "resizing") return;
|
|
5072
5524
|
const el = ctx.store.getById(this.mode.elementId);
|
|
5073
5525
|
if (!el || !("size" in el) || el.locked) return;
|
|
@@ -5098,6 +5550,21 @@ var SelectTool = class {
|
|
|
5098
5550
|
h -= dy;
|
|
5099
5551
|
break;
|
|
5100
5552
|
}
|
|
5553
|
+
if (shiftKey && this.resizeAspectRatio > 0) {
|
|
5554
|
+
const absDw = Math.abs(w - el.size.w);
|
|
5555
|
+
const absDh = Math.abs(h - el.size.h);
|
|
5556
|
+
if (absDw >= absDh) {
|
|
5557
|
+
h = w / this.resizeAspectRatio;
|
|
5558
|
+
} else {
|
|
5559
|
+
w = h * this.resizeAspectRatio;
|
|
5560
|
+
}
|
|
5561
|
+
if (handle === "nw" || handle === "sw") {
|
|
5562
|
+
x = el.position.x + el.size.w - w;
|
|
5563
|
+
}
|
|
5564
|
+
if (handle === "nw" || handle === "ne") {
|
|
5565
|
+
y = el.position.y + el.size.h - h;
|
|
5566
|
+
}
|
|
5567
|
+
}
|
|
5101
5568
|
if (w < MIN_ELEMENT_SIZE) {
|
|
5102
5569
|
if (handle === "nw" || handle === "sw") x = el.position.x + el.size.w - MIN_ELEMENT_SIZE;
|
|
5103
5570
|
w = MIN_ELEMENT_SIZE;
|
|
@@ -6201,7 +6668,7 @@ var UpdateLayerCommand = class {
|
|
|
6201
6668
|
};
|
|
6202
6669
|
|
|
6203
6670
|
// src/index.ts
|
|
6204
|
-
var VERSION = "0.
|
|
6671
|
+
var VERSION = "0.15.0";
|
|
6205
6672
|
export {
|
|
6206
6673
|
AddElementCommand,
|
|
6207
6674
|
ArrowTool,
|
|
@@ -6212,6 +6679,7 @@ export {
|
|
|
6212
6679
|
CreateLayerCommand,
|
|
6213
6680
|
DEFAULT_FONT_SIZE_PRESETS,
|
|
6214
6681
|
DEFAULT_NOTE_FONT_SIZE,
|
|
6682
|
+
DoubleTapDetector,
|
|
6215
6683
|
ElementRenderer,
|
|
6216
6684
|
ElementStore,
|
|
6217
6685
|
EraserTool,
|
|
@@ -6220,6 +6688,7 @@ export {
|
|
|
6220
6688
|
HistoryRecorder,
|
|
6221
6689
|
HistoryStack,
|
|
6222
6690
|
ImageTool,
|
|
6691
|
+
InputFilter,
|
|
6223
6692
|
InputHandler,
|
|
6224
6693
|
LayerManager,
|
|
6225
6694
|
MeasureTool,
|
|
@@ -6265,6 +6734,7 @@ export {
|
|
|
6265
6734
|
getEdgeIntersection,
|
|
6266
6735
|
getElementBounds,
|
|
6267
6736
|
getElementCenter,
|
|
6737
|
+
getElementsBoundingBox,
|
|
6268
6738
|
getHexCellsInCone,
|
|
6269
6739
|
getHexCellsInLine,
|
|
6270
6740
|
getHexCellsInRadius,
|