@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.cjs
CHANGED
|
@@ -29,6 +29,7 @@ __export(index_exports, {
|
|
|
29
29
|
CreateLayerCommand: () => CreateLayerCommand,
|
|
30
30
|
DEFAULT_FONT_SIZE_PRESETS: () => DEFAULT_FONT_SIZE_PRESETS,
|
|
31
31
|
DEFAULT_NOTE_FONT_SIZE: () => DEFAULT_NOTE_FONT_SIZE,
|
|
32
|
+
DoubleTapDetector: () => DoubleTapDetector,
|
|
32
33
|
ElementRenderer: () => ElementRenderer,
|
|
33
34
|
ElementStore: () => ElementStore,
|
|
34
35
|
EraserTool: () => EraserTool,
|
|
@@ -37,6 +38,7 @@ __export(index_exports, {
|
|
|
37
38
|
HistoryRecorder: () => HistoryRecorder,
|
|
38
39
|
HistoryStack: () => HistoryStack,
|
|
39
40
|
ImageTool: () => ImageTool,
|
|
41
|
+
InputFilter: () => InputFilter,
|
|
40
42
|
InputHandler: () => InputHandler,
|
|
41
43
|
LayerManager: () => LayerManager,
|
|
42
44
|
MeasureTool: () => MeasureTool,
|
|
@@ -82,6 +84,7 @@ __export(index_exports, {
|
|
|
82
84
|
getEdgeIntersection: () => getEdgeIntersection,
|
|
83
85
|
getElementBounds: () => getElementBounds,
|
|
84
86
|
getElementCenter: () => getElementCenter,
|
|
87
|
+
getElementsBoundingBox: () => getElementsBoundingBox,
|
|
85
88
|
getHexCellsInCone: () => getHexCellsInCone,
|
|
86
89
|
getHexCellsInLine: () => getHexCellsInLine,
|
|
87
90
|
getHexCellsInRadius: () => getHexCellsInRadius,
|
|
@@ -391,16 +394,24 @@ function sanitizeAttributes(el, tag) {
|
|
|
391
394
|
|
|
392
395
|
// src/core/state-serializer.ts
|
|
393
396
|
var CURRENT_VERSION = 2;
|
|
394
|
-
function exportState(elements, camera, layers = []) {
|
|
395
|
-
|
|
397
|
+
function exportState(elements, camera, layers = [], activeLayerId) {
|
|
398
|
+
const state = {
|
|
396
399
|
version: CURRENT_VERSION,
|
|
397
400
|
camera: {
|
|
398
401
|
position: { ...camera.position },
|
|
399
402
|
zoom: camera.zoom
|
|
400
403
|
},
|
|
401
|
-
elements: elements.map((el) =>
|
|
404
|
+
elements: elements.map((el) => {
|
|
405
|
+
const clone = structuredClone(el);
|
|
406
|
+
if (clone.type === "arrow") {
|
|
407
|
+
delete clone.cachedControlPoint;
|
|
408
|
+
}
|
|
409
|
+
return clone;
|
|
410
|
+
}),
|
|
402
411
|
layers: layers.map((l) => ({ ...l }))
|
|
403
412
|
};
|
|
413
|
+
if (activeLayerId) state.activeLayerId = activeLayerId;
|
|
414
|
+
return state;
|
|
404
415
|
}
|
|
405
416
|
function parseState(json) {
|
|
406
417
|
const data = JSON.parse(json);
|
|
@@ -558,12 +569,14 @@ var AutoSave = class {
|
|
|
558
569
|
this.key = options.key ?? DEFAULT_KEY;
|
|
559
570
|
this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
560
571
|
this.layerManager = options.layerManager;
|
|
572
|
+
this.onError = options.onError;
|
|
561
573
|
}
|
|
562
574
|
key;
|
|
563
575
|
debounceMs;
|
|
564
576
|
layerManager;
|
|
565
577
|
timerId = null;
|
|
566
578
|
unsubscribers = [];
|
|
579
|
+
onError;
|
|
567
580
|
start() {
|
|
568
581
|
const schedule = () => this.scheduleSave();
|
|
569
582
|
this.unsubscribers = [
|
|
@@ -611,8 +624,9 @@ var AutoSave = class {
|
|
|
611
624
|
const state = exportState(this.store.snapshot(), this.camera, layers);
|
|
612
625
|
try {
|
|
613
626
|
localStorage.setItem(this.key, JSON.stringify(state));
|
|
614
|
-
} catch {
|
|
627
|
+
} catch (e) {
|
|
615
628
|
console.warn("Auto-save failed: storage quota exceeded. State too large for localStorage.");
|
|
629
|
+
this.onError?.(e instanceof Error ? e : new Error(String(e)));
|
|
616
630
|
}
|
|
617
631
|
}
|
|
618
632
|
};
|
|
@@ -681,6 +695,15 @@ var Camera = class {
|
|
|
681
695
|
h: bottomRight.y - topLeft.y
|
|
682
696
|
};
|
|
683
697
|
}
|
|
698
|
+
fitToContent(boundingBox, canvasWidth, canvasHeight, padding = 40) {
|
|
699
|
+
if (boundingBox.w === 0 && boundingBox.h === 0) return;
|
|
700
|
+
const scaleX = canvasWidth / (boundingBox.w + 2 * padding);
|
|
701
|
+
const scaleY = canvasHeight / (boundingBox.h + 2 * padding);
|
|
702
|
+
this.z = Math.min(this.maxZoom, Math.max(this.minZoom, Math.min(scaleX, scaleY)));
|
|
703
|
+
this.x = (canvasWidth - boundingBox.w * this.z) / 2 - boundingBox.x * this.z;
|
|
704
|
+
this.y = (canvasHeight - boundingBox.h * this.z) / 2 - boundingBox.y * this.z;
|
|
705
|
+
this.notifyPanAndZoom();
|
|
706
|
+
}
|
|
684
707
|
toCSSTransform() {
|
|
685
708
|
return `translate3d(${this.x}px, ${this.y}px, 0) scale(${this.z})`;
|
|
686
709
|
}
|
|
@@ -836,6 +859,67 @@ var Background = class {
|
|
|
836
859
|
}
|
|
837
860
|
};
|
|
838
861
|
|
|
862
|
+
// src/canvas/input-filter.ts
|
|
863
|
+
var InputFilter = class _InputFilter {
|
|
864
|
+
activePenId = null;
|
|
865
|
+
pendingTap = null;
|
|
866
|
+
static MIN_MOVE_DISTANCE = 3;
|
|
867
|
+
filterDown(e) {
|
|
868
|
+
if (e.pointerType === "pen") {
|
|
869
|
+
this.activePenId = e.pointerId;
|
|
870
|
+
return { event: e, action: "dispatch" };
|
|
871
|
+
}
|
|
872
|
+
if (e.pointerType === "touch" && this.activePenId !== null) {
|
|
873
|
+
return { event: e, action: "suppress" };
|
|
874
|
+
}
|
|
875
|
+
if (e.pointerType === "touch") {
|
|
876
|
+
this.pendingTap = { pointerId: e.pointerId, x: e.clientX, y: e.clientY };
|
|
877
|
+
return { event: e, action: "defer" };
|
|
878
|
+
}
|
|
879
|
+
return { event: e, action: "dispatch" };
|
|
880
|
+
}
|
|
881
|
+
filterMove(e) {
|
|
882
|
+
if (e.pointerType === "touch" && this.activePenId !== null) {
|
|
883
|
+
return { event: e, action: "suppress" };
|
|
884
|
+
}
|
|
885
|
+
if (this.pendingTap && e.pointerId === this.pendingTap.pointerId) {
|
|
886
|
+
const dx = e.clientX - this.pendingTap.x;
|
|
887
|
+
const dy = e.clientY - this.pendingTap.y;
|
|
888
|
+
if (dx * dx + dy * dy > _InputFilter.MIN_MOVE_DISTANCE * _InputFilter.MIN_MOVE_DISTANCE) {
|
|
889
|
+
this.pendingTap = null;
|
|
890
|
+
return { event: e, action: "dispatch" };
|
|
891
|
+
}
|
|
892
|
+
return { event: e, action: "suppress" };
|
|
893
|
+
}
|
|
894
|
+
return { event: e, action: "dispatch" };
|
|
895
|
+
}
|
|
896
|
+
filterUp(e) {
|
|
897
|
+
if (e.pointerId === this.activePenId) {
|
|
898
|
+
this.activePenId = null;
|
|
899
|
+
return { event: e, action: "dispatch" };
|
|
900
|
+
}
|
|
901
|
+
if (e.pointerType === "touch" && this.activePenId !== null) {
|
|
902
|
+
return { event: e, action: "suppress" };
|
|
903
|
+
}
|
|
904
|
+
if (this.pendingTap && e.pointerId === this.pendingTap.pointerId) {
|
|
905
|
+
const tap = { x: this.pendingTap.x, y: this.pendingTap.y };
|
|
906
|
+
this.pendingTap = null;
|
|
907
|
+
return { event: e, action: "dispatch", pendingTap: tap };
|
|
908
|
+
}
|
|
909
|
+
return { event: e, action: "dispatch" };
|
|
910
|
+
}
|
|
911
|
+
reset() {
|
|
912
|
+
this.activePenId = null;
|
|
913
|
+
this.pendingTap = null;
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
// src/elements/create-id.ts
|
|
918
|
+
var counter = 0;
|
|
919
|
+
function createId(prefix) {
|
|
920
|
+
return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
|
|
921
|
+
}
|
|
922
|
+
|
|
839
923
|
// src/canvas/input-handler.ts
|
|
840
924
|
var ZOOM_SENSITIVITY = 1e-3;
|
|
841
925
|
var MIDDLE_BUTTON = 1;
|
|
@@ -861,13 +945,21 @@ var InputHandler = class {
|
|
|
861
945
|
historyRecorder;
|
|
862
946
|
historyStack;
|
|
863
947
|
isToolActive = false;
|
|
948
|
+
lastPointerEvent = null;
|
|
949
|
+
inputFilter = new InputFilter();
|
|
950
|
+
deferredDown = null;
|
|
864
951
|
abortController = new AbortController();
|
|
952
|
+
clipboard = [];
|
|
953
|
+
pasteCount = 0;
|
|
865
954
|
setToolManager(toolManager, toolContext) {
|
|
866
955
|
this.toolManager = toolManager;
|
|
867
956
|
this.toolContext = toolContext;
|
|
868
957
|
}
|
|
869
958
|
destroy() {
|
|
870
959
|
this.abortController.abort();
|
|
960
|
+
this.inputFilter.reset();
|
|
961
|
+
this.deferredDown = null;
|
|
962
|
+
this.lastPointerEvent = null;
|
|
871
963
|
}
|
|
872
964
|
bind() {
|
|
873
965
|
const opts = { signal: this.abortController.signal };
|
|
@@ -903,11 +995,18 @@ var InputHandler = class {
|
|
|
903
995
|
this.lastPointer = { x: e.clientX, y: e.clientY };
|
|
904
996
|
return;
|
|
905
997
|
}
|
|
906
|
-
if (this.activePointers.size === 1 && e.button === 0) {
|
|
998
|
+
if (this.activePointers.size === 1 && (e.button === 0 || e.pointerType === "touch" || e.pointerType === "pen")) {
|
|
999
|
+
const result = this.inputFilter.filterDown(e);
|
|
1000
|
+
if (result.action === "suppress") return;
|
|
1001
|
+
if (result.action === "defer") {
|
|
1002
|
+
this.deferredDown = e;
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
907
1005
|
this.dispatchToolDown(e);
|
|
908
1006
|
}
|
|
909
1007
|
};
|
|
910
1008
|
onPointerMove = (e) => {
|
|
1009
|
+
this.lastPointerEvent = e;
|
|
911
1010
|
if (this.activePointers.has(e.pointerId)) {
|
|
912
1011
|
this.activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
913
1012
|
}
|
|
@@ -922,13 +1021,26 @@ var InputHandler = class {
|
|
|
922
1021
|
this.camera.pan(dx, dy);
|
|
923
1022
|
return;
|
|
924
1023
|
}
|
|
925
|
-
if (this.
|
|
1024
|
+
if (e.pointerType === "pen" && !this.activePointers.has(e.pointerId)) {
|
|
1025
|
+
this.dispatchToolHover(e);
|
|
1026
|
+
} else if (this.isToolActive) {
|
|
926
1027
|
this.dispatchToolMove(e);
|
|
1028
|
+
} else if (this.deferredDown) {
|
|
1029
|
+
const result = this.inputFilter.filterMove(e);
|
|
1030
|
+
if (result.action === "dispatch") {
|
|
1031
|
+
this.dispatchToolDown(this.deferredDown);
|
|
1032
|
+
this.deferredDown = null;
|
|
1033
|
+
this.dispatchToolMove(e);
|
|
1034
|
+
}
|
|
927
1035
|
} else if (this.activePointers.size === 0) {
|
|
928
1036
|
this.dispatchToolHover(e);
|
|
929
1037
|
}
|
|
930
1038
|
};
|
|
931
1039
|
onPointerUp = (e) => {
|
|
1040
|
+
try {
|
|
1041
|
+
this.element.releasePointerCapture(e.pointerId);
|
|
1042
|
+
} catch {
|
|
1043
|
+
}
|
|
932
1044
|
this.activePointers.delete(e.pointerId);
|
|
933
1045
|
if (this.activePointers.size < 2) {
|
|
934
1046
|
this.lastPinchDistance = 0;
|
|
@@ -936,9 +1048,16 @@ var InputHandler = class {
|
|
|
936
1048
|
if (this.isPanning && this.activePointers.size === 0) {
|
|
937
1049
|
this.isPanning = false;
|
|
938
1050
|
}
|
|
1051
|
+
const upResult = this.inputFilter.filterUp(e);
|
|
939
1052
|
if (this.isToolActive) {
|
|
940
1053
|
this.dispatchToolUp(e);
|
|
941
1054
|
this.isToolActive = false;
|
|
1055
|
+
} else if (this.deferredDown && upResult.pendingTap) {
|
|
1056
|
+
this.dispatchToolDown(this.deferredDown);
|
|
1057
|
+
this.dispatchToolUp(e);
|
|
1058
|
+
this.deferredDown = null;
|
|
1059
|
+
} else {
|
|
1060
|
+
this.deferredDown = null;
|
|
942
1061
|
}
|
|
943
1062
|
};
|
|
944
1063
|
onKeyDown = (e) => {
|
|
@@ -957,13 +1076,38 @@ var InputHandler = class {
|
|
|
957
1076
|
e.preventDefault();
|
|
958
1077
|
this.handleRedo();
|
|
959
1078
|
}
|
|
1079
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "c") {
|
|
1080
|
+
e.preventDefault();
|
|
1081
|
+
this.handleCopy();
|
|
1082
|
+
}
|
|
1083
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "v") {
|
|
1084
|
+
e.preventDefault();
|
|
1085
|
+
this.handlePaste();
|
|
1086
|
+
}
|
|
1087
|
+
if (e.key === "]") {
|
|
1088
|
+
e.preventDefault();
|
|
1089
|
+
this.handleZOrder(e.ctrlKey || e.metaKey ? "front" : "forward");
|
|
1090
|
+
}
|
|
1091
|
+
if (e.key === "[") {
|
|
1092
|
+
e.preventDefault();
|
|
1093
|
+
this.handleZOrder(e.ctrlKey || e.metaKey ? "back" : "backward");
|
|
1094
|
+
}
|
|
960
1095
|
};
|
|
961
1096
|
onKeyUp = (e) => {
|
|
962
1097
|
if (e.key === " ") {
|
|
963
1098
|
this.spaceHeld = false;
|
|
1099
|
+
if (this.activePointers.size === 0) {
|
|
1100
|
+
if (this.lastPointerEvent) {
|
|
1101
|
+
this.dispatchToolHover(this.lastPointerEvent);
|
|
1102
|
+
} else {
|
|
1103
|
+
this.toolContext?.setCursor?.("default");
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
964
1106
|
}
|
|
965
1107
|
};
|
|
966
1108
|
startPinch() {
|
|
1109
|
+
this.inputFilter.reset();
|
|
1110
|
+
this.deferredDown = null;
|
|
967
1111
|
this.isPanning = true;
|
|
968
1112
|
const [a, b] = this.getPinchPoints();
|
|
969
1113
|
this.lastPinchDistance = this.distance(a, b);
|
|
@@ -1003,7 +1147,9 @@ var InputHandler = class {
|
|
|
1003
1147
|
return {
|
|
1004
1148
|
x: e.clientX - rect.left,
|
|
1005
1149
|
y: e.clientY - rect.top,
|
|
1006
|
-
pressure: e.pressure
|
|
1150
|
+
pressure: e.pressure,
|
|
1151
|
+
pointerType: e.pointerType === "touch" || e.pointerType === "pen" ? e.pointerType : "mouse",
|
|
1152
|
+
shiftKey: e.shiftKey
|
|
1007
1153
|
};
|
|
1008
1154
|
}
|
|
1009
1155
|
dispatchToolDown(e) {
|
|
@@ -1056,11 +1202,147 @@ var InputHandler = class {
|
|
|
1056
1202
|
this.historyRecorder?.resume();
|
|
1057
1203
|
this.toolContext.requestRender();
|
|
1058
1204
|
}
|
|
1205
|
+
handleCopy() {
|
|
1206
|
+
if (!this.toolManager || !this.toolContext || this.isToolActive) return;
|
|
1207
|
+
const tool = this.toolManager.activeTool;
|
|
1208
|
+
if (tool?.name !== "select") return;
|
|
1209
|
+
const selectTool = tool;
|
|
1210
|
+
const ids = selectTool.selectedIds;
|
|
1211
|
+
if (ids.length === 0) return;
|
|
1212
|
+
this.clipboard = [];
|
|
1213
|
+
for (const id of ids) {
|
|
1214
|
+
const el = this.toolContext.store.getById(id);
|
|
1215
|
+
if (el) this.clipboard.push(structuredClone(el));
|
|
1216
|
+
}
|
|
1217
|
+
this.pasteCount = 0;
|
|
1218
|
+
}
|
|
1219
|
+
handlePaste() {
|
|
1220
|
+
if (!this.toolManager || !this.toolContext || this.clipboard.length === 0 || this.isToolActive)
|
|
1221
|
+
return;
|
|
1222
|
+
const tool = this.toolManager.activeTool;
|
|
1223
|
+
if (tool?.name !== "select") return;
|
|
1224
|
+
const selectTool = tool;
|
|
1225
|
+
this.pasteCount++;
|
|
1226
|
+
const offset = this.pasteCount * 20;
|
|
1227
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
1228
|
+
for (const el of this.clipboard) {
|
|
1229
|
+
idMap.set(el.id, createId(el.type));
|
|
1230
|
+
}
|
|
1231
|
+
const newIds = [];
|
|
1232
|
+
this.historyRecorder?.begin();
|
|
1233
|
+
for (const el of this.clipboard) {
|
|
1234
|
+
const clone = structuredClone(el);
|
|
1235
|
+
const newId = idMap.get(el.id);
|
|
1236
|
+
if (!newId) continue;
|
|
1237
|
+
clone.id = newId;
|
|
1238
|
+
clone.position = { x: clone.position.x + offset, y: clone.position.y + offset };
|
|
1239
|
+
if (clone.type === "arrow") {
|
|
1240
|
+
const arrow = clone;
|
|
1241
|
+
arrow.from = { x: arrow.from.x + offset, y: arrow.from.y + offset };
|
|
1242
|
+
arrow.to = { x: arrow.to.x + offset, y: arrow.to.y + offset };
|
|
1243
|
+
delete arrow.cachedControlPoint;
|
|
1244
|
+
if (arrow.fromBinding) {
|
|
1245
|
+
const newTarget = idMap.get(arrow.fromBinding.elementId);
|
|
1246
|
+
if (newTarget) {
|
|
1247
|
+
arrow.fromBinding = { elementId: newTarget };
|
|
1248
|
+
} else {
|
|
1249
|
+
delete arrow.fromBinding;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
if (arrow.toBinding) {
|
|
1253
|
+
const newTarget = idMap.get(arrow.toBinding.elementId);
|
|
1254
|
+
if (newTarget) {
|
|
1255
|
+
arrow.toBinding = { elementId: newTarget };
|
|
1256
|
+
} else {
|
|
1257
|
+
delete arrow.toBinding;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
if (this.toolContext.activeLayerId) {
|
|
1262
|
+
clone.layerId = this.toolContext.activeLayerId;
|
|
1263
|
+
}
|
|
1264
|
+
this.toolContext.store.add(clone);
|
|
1265
|
+
newIds.push(clone.id);
|
|
1266
|
+
}
|
|
1267
|
+
this.historyRecorder?.commit();
|
|
1268
|
+
selectTool.setSelection(newIds);
|
|
1269
|
+
this.toolContext.requestRender();
|
|
1270
|
+
}
|
|
1271
|
+
handleZOrder(operation) {
|
|
1272
|
+
if (!this.toolManager || !this.toolContext) return;
|
|
1273
|
+
const tool = this.toolManager.activeTool;
|
|
1274
|
+
if (tool?.name !== "select") return;
|
|
1275
|
+
const selectTool = tool;
|
|
1276
|
+
const ids = selectTool.selectedIds;
|
|
1277
|
+
if (ids.length === 0) return;
|
|
1278
|
+
this.historyRecorder?.begin();
|
|
1279
|
+
for (const id of ids) {
|
|
1280
|
+
switch (operation) {
|
|
1281
|
+
case "forward":
|
|
1282
|
+
this.toolContext.store.bringForward(id);
|
|
1283
|
+
break;
|
|
1284
|
+
case "backward":
|
|
1285
|
+
this.toolContext.store.sendBackward(id);
|
|
1286
|
+
break;
|
|
1287
|
+
case "front":
|
|
1288
|
+
this.toolContext.store.bringToFront(id);
|
|
1289
|
+
break;
|
|
1290
|
+
case "back":
|
|
1291
|
+
this.toolContext.store.sendToBack(id);
|
|
1292
|
+
break;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
this.historyRecorder?.commit();
|
|
1296
|
+
this.toolContext.requestRender();
|
|
1297
|
+
}
|
|
1059
1298
|
cancelToolIfActive(e) {
|
|
1060
1299
|
if (this.isToolActive) {
|
|
1061
1300
|
this.dispatchToolUp(e);
|
|
1062
1301
|
this.isToolActive = false;
|
|
1063
1302
|
}
|
|
1303
|
+
this.deferredDown = null;
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
// src/canvas/double-tap-detector.ts
|
|
1308
|
+
var DEFAULT_TIMEOUT = 300;
|
|
1309
|
+
var DEFAULT_MAX_DISTANCE = 20;
|
|
1310
|
+
var DoubleTapDetector = class {
|
|
1311
|
+
timeout;
|
|
1312
|
+
maxDistance;
|
|
1313
|
+
lastTapTime = 0;
|
|
1314
|
+
lastTapX = 0;
|
|
1315
|
+
lastTapY = 0;
|
|
1316
|
+
hasPendingTap = false;
|
|
1317
|
+
constructor(options) {
|
|
1318
|
+
this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
|
|
1319
|
+
this.maxDistance = options?.maxDistance ?? DEFAULT_MAX_DISTANCE;
|
|
1320
|
+
}
|
|
1321
|
+
feed(e) {
|
|
1322
|
+
const now = Date.now();
|
|
1323
|
+
const x = e.clientX;
|
|
1324
|
+
const y = e.clientY;
|
|
1325
|
+
if (this.hasPendingTap) {
|
|
1326
|
+
const elapsed = now - this.lastTapTime;
|
|
1327
|
+
const dx = x - this.lastTapX;
|
|
1328
|
+
const dy = y - this.lastTapY;
|
|
1329
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
1330
|
+
if (elapsed <= this.timeout && dist <= this.maxDistance) {
|
|
1331
|
+
this.reset();
|
|
1332
|
+
return true;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
this.lastTapTime = now;
|
|
1336
|
+
this.lastTapX = x;
|
|
1337
|
+
this.lastTapY = y;
|
|
1338
|
+
this.hasPendingTap = true;
|
|
1339
|
+
return false;
|
|
1340
|
+
}
|
|
1341
|
+
reset() {
|
|
1342
|
+
this.hasPendingTap = false;
|
|
1343
|
+
this.lastTapTime = 0;
|
|
1344
|
+
this.lastTapX = 0;
|
|
1345
|
+
this.lastTapY = 0;
|
|
1064
1346
|
}
|
|
1065
1347
|
};
|
|
1066
1348
|
|
|
@@ -1308,19 +1590,27 @@ var ElementStore = class {
|
|
|
1308
1590
|
bus = new EventBus();
|
|
1309
1591
|
layerOrderMap = /* @__PURE__ */ new Map();
|
|
1310
1592
|
spatialIndex = new Quadtree({ x: -1e5, y: -1e5, w: 2e5, h: 2e5 });
|
|
1593
|
+
sortedCache = null;
|
|
1594
|
+
_versions = /* @__PURE__ */ new Map();
|
|
1311
1595
|
get count() {
|
|
1312
1596
|
return this.elements.size;
|
|
1313
1597
|
}
|
|
1598
|
+
getVersion(id) {
|
|
1599
|
+
return this._versions.get(id) ?? -1;
|
|
1600
|
+
}
|
|
1314
1601
|
setLayerOrder(order) {
|
|
1315
1602
|
this.layerOrderMap = new Map(order);
|
|
1603
|
+
this.sortedCache = null;
|
|
1316
1604
|
}
|
|
1317
1605
|
getAll() {
|
|
1318
|
-
|
|
1606
|
+
if (this.sortedCache) return this.sortedCache;
|
|
1607
|
+
this.sortedCache = [...this.elements.values()].sort((a, b) => {
|
|
1319
1608
|
const layerA = this.layerOrderMap.get(a.layerId) ?? 0;
|
|
1320
1609
|
const layerB = this.layerOrderMap.get(b.layerId) ?? 0;
|
|
1321
1610
|
if (layerA !== layerB) return layerA - layerB;
|
|
1322
1611
|
return a.zIndex - b.zIndex;
|
|
1323
1612
|
});
|
|
1613
|
+
return this.sortedCache;
|
|
1324
1614
|
}
|
|
1325
1615
|
getById(id) {
|
|
1326
1616
|
return this.elements.get(id);
|
|
@@ -1331,6 +1621,8 @@ var ElementStore = class {
|
|
|
1331
1621
|
);
|
|
1332
1622
|
}
|
|
1333
1623
|
add(element) {
|
|
1624
|
+
this.sortedCache = null;
|
|
1625
|
+
this._versions.set(element.id, 0);
|
|
1334
1626
|
this.elements.set(element.id, element);
|
|
1335
1627
|
const bounds = getElementBounds(element);
|
|
1336
1628
|
if (bounds) this.spatialIndex.insert(element.id, bounds);
|
|
@@ -1339,11 +1631,16 @@ var ElementStore = class {
|
|
|
1339
1631
|
update(id, partial) {
|
|
1340
1632
|
const existing = this.elements.get(id);
|
|
1341
1633
|
if (!existing) return;
|
|
1634
|
+
this.sortedCache = null;
|
|
1635
|
+
this._versions.set(id, (this._versions.get(id) ?? 0) + 1);
|
|
1342
1636
|
const updated = { ...existing, ...partial, id: existing.id, type: existing.type };
|
|
1343
1637
|
if (updated.type === "arrow") {
|
|
1344
1638
|
const arrow = updated;
|
|
1345
1639
|
arrow.cachedControlPoint = getArrowControlPoint(arrow.from, arrow.to, arrow.bend);
|
|
1346
1640
|
}
|
|
1641
|
+
if (updated.type === "note" && "text" in partial) {
|
|
1642
|
+
updated.text = sanitizeNoteHtml(updated.text);
|
|
1643
|
+
}
|
|
1347
1644
|
this.elements.set(id, updated);
|
|
1348
1645
|
const newBounds = getElementBounds(updated);
|
|
1349
1646
|
if (newBounds) {
|
|
@@ -1354,11 +1651,15 @@ var ElementStore = class {
|
|
|
1354
1651
|
remove(id) {
|
|
1355
1652
|
const element = this.elements.get(id);
|
|
1356
1653
|
if (!element) return;
|
|
1654
|
+
this.sortedCache = null;
|
|
1655
|
+
this._versions.delete(id);
|
|
1357
1656
|
this.elements.delete(id);
|
|
1358
1657
|
this.spatialIndex.remove(id);
|
|
1359
1658
|
this.bus.emit("remove", element);
|
|
1360
1659
|
}
|
|
1361
1660
|
clear() {
|
|
1661
|
+
this.sortedCache = null;
|
|
1662
|
+
this._versions.clear();
|
|
1362
1663
|
this.elements.clear();
|
|
1363
1664
|
this.spatialIndex.clear();
|
|
1364
1665
|
this.bus.emit("clear", null);
|
|
@@ -1367,13 +1668,68 @@ var ElementStore = class {
|
|
|
1367
1668
|
return this.getAll().map((el) => ({ ...el }));
|
|
1368
1669
|
}
|
|
1369
1670
|
loadSnapshot(elements) {
|
|
1671
|
+
this.sortedCache = null;
|
|
1672
|
+
this._versions.clear();
|
|
1370
1673
|
this.elements.clear();
|
|
1371
1674
|
this.spatialIndex.clear();
|
|
1372
1675
|
for (const el of elements) {
|
|
1373
1676
|
this.elements.set(el.id, el);
|
|
1677
|
+
this._versions.set(el.id, 0);
|
|
1374
1678
|
const bounds = getElementBounds(el);
|
|
1375
1679
|
if (bounds) this.spatialIndex.insert(el.id, bounds);
|
|
1376
1680
|
}
|
|
1681
|
+
this.bus.emit("clear", null);
|
|
1682
|
+
for (const el of elements) {
|
|
1683
|
+
this.bus.emit("add", el);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
bringToFront(id) {
|
|
1687
|
+
const el = this.elements.get(id);
|
|
1688
|
+
if (!el) return;
|
|
1689
|
+
const siblings = [...this.elements.values()].filter(
|
|
1690
|
+
(e) => e.layerId === el.layerId && e.id !== id
|
|
1691
|
+
);
|
|
1692
|
+
if (siblings.length === 0) return;
|
|
1693
|
+
const maxZ = Math.max(...siblings.map((e) => e.zIndex));
|
|
1694
|
+
if (el.zIndex >= maxZ) return;
|
|
1695
|
+
this.update(id, { zIndex: maxZ + 1 });
|
|
1696
|
+
}
|
|
1697
|
+
sendToBack(id) {
|
|
1698
|
+
const el = this.elements.get(id);
|
|
1699
|
+
if (!el) return;
|
|
1700
|
+
const siblings = [...this.elements.values()].filter(
|
|
1701
|
+
(e) => e.layerId === el.layerId && e.id !== id
|
|
1702
|
+
);
|
|
1703
|
+
if (siblings.length === 0) return;
|
|
1704
|
+
const minZ = Math.min(...siblings.map((e) => e.zIndex));
|
|
1705
|
+
if (el.zIndex <= minZ) return;
|
|
1706
|
+
this.update(id, { zIndex: minZ - 1 });
|
|
1707
|
+
}
|
|
1708
|
+
bringForward(id) {
|
|
1709
|
+
const el = this.elements.get(id);
|
|
1710
|
+
if (!el) return;
|
|
1711
|
+
const sorted = [...this.elements.values()].filter((e) => e.layerId === el.layerId).sort((a, b) => a.zIndex - b.zIndex);
|
|
1712
|
+
const idx = sorted.findIndex((e) => e.id === id);
|
|
1713
|
+
if (idx < 0 || idx >= sorted.length - 1) return;
|
|
1714
|
+
const next = sorted[idx + 1];
|
|
1715
|
+
if (!next) return;
|
|
1716
|
+
const myZ = el.zIndex;
|
|
1717
|
+
const nextZ = next.zIndex;
|
|
1718
|
+
this.update(id, { zIndex: nextZ });
|
|
1719
|
+
this.update(next.id, { zIndex: myZ });
|
|
1720
|
+
}
|
|
1721
|
+
sendBackward(id) {
|
|
1722
|
+
const el = this.elements.get(id);
|
|
1723
|
+
if (!el) return;
|
|
1724
|
+
const sorted = [...this.elements.values()].filter((e) => e.layerId === el.layerId).sort((a, b) => a.zIndex - b.zIndex);
|
|
1725
|
+
const idx = sorted.findIndex((e) => e.id === id);
|
|
1726
|
+
if (idx <= 0) return;
|
|
1727
|
+
const prev = sorted[idx - 1];
|
|
1728
|
+
if (!prev) return;
|
|
1729
|
+
const myZ = el.zIndex;
|
|
1730
|
+
const prevZ = prev.zIndex;
|
|
1731
|
+
this.update(id, { zIndex: prevZ });
|
|
1732
|
+
this.update(prev.id, { zIndex: myZ });
|
|
1377
1733
|
}
|
|
1378
1734
|
queryRect(rect) {
|
|
1379
1735
|
const ids = this.spatialIndex.query(rect);
|
|
@@ -2423,12 +2779,6 @@ var ElementRenderer = class {
|
|
|
2423
2779
|
}
|
|
2424
2780
|
};
|
|
2425
2781
|
|
|
2426
|
-
// src/elements/create-id.ts
|
|
2427
|
-
var counter = 0;
|
|
2428
|
-
function createId(prefix) {
|
|
2429
|
-
return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
|
|
2430
|
-
}
|
|
2431
|
-
|
|
2432
2782
|
// src/elements/element-factory.ts
|
|
2433
2783
|
var DEFAULT_NOTE_FONT_SIZE = 18;
|
|
2434
2784
|
function createStroke(input) {
|
|
@@ -2454,7 +2804,7 @@ function createNote(input) {
|
|
|
2454
2804
|
locked: input.locked ?? false,
|
|
2455
2805
|
layerId: input.layerId ?? "",
|
|
2456
2806
|
size: input.size ?? { w: 200, h: 100 },
|
|
2457
|
-
text: input.text ?? "",
|
|
2807
|
+
text: sanitizeNoteHtml(input.text ?? ""),
|
|
2458
2808
|
backgroundColor: input.backgroundColor ?? "#ffeb3b",
|
|
2459
2809
|
textColor: input.textColor ?? "#000000",
|
|
2460
2810
|
fontSize: input.fontSize ?? DEFAULT_NOTE_FONT_SIZE
|
|
@@ -2503,6 +2853,7 @@ function createHtmlElement(input) {
|
|
|
2503
2853
|
size: input.size
|
|
2504
2854
|
};
|
|
2505
2855
|
if (input.domId) el.domId = input.domId;
|
|
2856
|
+
if (input.interactive) el.interactive = input.interactive;
|
|
2506
2857
|
return el;
|
|
2507
2858
|
}
|
|
2508
2859
|
function createShape(input) {
|
|
@@ -2615,7 +2966,7 @@ function getActiveFormats() {
|
|
|
2615
2966
|
}
|
|
2616
2967
|
|
|
2617
2968
|
// src/elements/note-toolbar.ts
|
|
2618
|
-
var TOOLBAR_HEIGHT =
|
|
2969
|
+
var TOOLBAR_HEIGHT = 52;
|
|
2619
2970
|
var TOOLBAR_GAP = 4;
|
|
2620
2971
|
var FORMAT_BUTTONS = [
|
|
2621
2972
|
{ label: "B", format: "bold", command: "bold" },
|
|
@@ -2702,9 +3053,9 @@ var NoteToolbar = class {
|
|
|
2702
3053
|
fontWeight: config.format === "bold" ? "bold" : "normal",
|
|
2703
3054
|
fontStyle: config.format === "italic" ? "italic" : "normal",
|
|
2704
3055
|
textDecoration: config.format === "underline" ? "underline" : config.format === "strikethrough" ? "line-through" : "none",
|
|
2705
|
-
minWidth: "
|
|
2706
|
-
height: "
|
|
2707
|
-
lineHeight: "
|
|
3056
|
+
minWidth: "44px",
|
|
3057
|
+
height: "44px",
|
|
3058
|
+
lineHeight: "44px"
|
|
2708
3059
|
});
|
|
2709
3060
|
btn.addEventListener("pointerdown", (e) => {
|
|
2710
3061
|
e.preventDefault();
|
|
@@ -2722,7 +3073,7 @@ var NoteToolbar = class {
|
|
|
2722
3073
|
cursor: "pointer",
|
|
2723
3074
|
padding: "2px",
|
|
2724
3075
|
fontSize: "12px",
|
|
2725
|
-
height: "
|
|
3076
|
+
height: "44px",
|
|
2726
3077
|
marginLeft: "4px"
|
|
2727
3078
|
});
|
|
2728
3079
|
for (const preset of this.fontSizePresets) {
|
|
@@ -3642,10 +3993,14 @@ var DomNodeManager = class {
|
|
|
3642
3993
|
domLayer;
|
|
3643
3994
|
onEditRequest;
|
|
3644
3995
|
isEditingElement;
|
|
3996
|
+
getVersion;
|
|
3997
|
+
lastSyncedVersion = /* @__PURE__ */ new Map();
|
|
3998
|
+
lastSyncedZIndex = /* @__PURE__ */ new Map();
|
|
3645
3999
|
constructor(deps) {
|
|
3646
4000
|
this.domLayer = deps.domLayer;
|
|
3647
4001
|
this.onEditRequest = deps.onEditRequest;
|
|
3648
4002
|
this.isEditingElement = deps.isEditingElement;
|
|
4003
|
+
this.getVersion = deps.getVersion ?? null;
|
|
3649
4004
|
}
|
|
3650
4005
|
getNode(id) {
|
|
3651
4006
|
return this.domNodes.get(id);
|
|
@@ -3664,6 +4019,17 @@ var DomNodeManager = class {
|
|
|
3664
4019
|
});
|
|
3665
4020
|
this.domLayer.appendChild(node);
|
|
3666
4021
|
this.domNodes.set(element.id, node);
|
|
4022
|
+
} else if (this.getVersion) {
|
|
4023
|
+
const currentVersion = this.getVersion(element.id);
|
|
4024
|
+
const lastVersion = this.lastSyncedVersion.get(element.id);
|
|
4025
|
+
const lastZ = this.lastSyncedZIndex.get(element.id);
|
|
4026
|
+
if (lastVersion === currentVersion && lastZ === zIndex) {
|
|
4027
|
+
return;
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
if (this.getVersion) {
|
|
4031
|
+
this.lastSyncedVersion.set(element.id, this.getVersion(element.id));
|
|
4032
|
+
this.lastSyncedZIndex.set(element.id, zIndex);
|
|
3667
4033
|
}
|
|
3668
4034
|
const size = "size" in element ? element.size : null;
|
|
3669
4035
|
Object.assign(node.style, {
|
|
@@ -3682,6 +4048,8 @@ var DomNodeManager = class {
|
|
|
3682
4048
|
}
|
|
3683
4049
|
removeDomNode(id) {
|
|
3684
4050
|
this.htmlContent.delete(id);
|
|
4051
|
+
this.lastSyncedVersion.delete(id);
|
|
4052
|
+
this.lastSyncedZIndex.delete(id);
|
|
3685
4053
|
const node = this.domNodes.get(id);
|
|
3686
4054
|
if (node) {
|
|
3687
4055
|
node.remove();
|
|
@@ -3692,6 +4060,8 @@ var DomNodeManager = class {
|
|
|
3692
4060
|
this.domNodes.forEach((node) => node.remove());
|
|
3693
4061
|
this.domNodes.clear();
|
|
3694
4062
|
this.htmlContent.clear();
|
|
4063
|
+
this.lastSyncedVersion.clear();
|
|
4064
|
+
this.lastSyncedZIndex.clear();
|
|
3695
4065
|
}
|
|
3696
4066
|
reattachHtmlContent(store) {
|
|
3697
4067
|
for (const el of store.getElementsByType("html")) {
|
|
@@ -3720,10 +4090,13 @@ var DomNodeManager = class {
|
|
|
3720
4090
|
wordWrap: "break-word"
|
|
3721
4091
|
});
|
|
3722
4092
|
node.innerHTML = element.text || "";
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
4093
|
+
const detector = new DoubleTapDetector();
|
|
4094
|
+
node.addEventListener("pointerup", (e) => {
|
|
4095
|
+
if (detector.feed(e)) {
|
|
4096
|
+
e.stopPropagation();
|
|
4097
|
+
const id = node.dataset["elementId"];
|
|
4098
|
+
if (id) this.onEditRequest(id);
|
|
4099
|
+
}
|
|
3727
4100
|
});
|
|
3728
4101
|
}
|
|
3729
4102
|
if (!this.isEditingElement(element.id)) {
|
|
@@ -3736,15 +4109,19 @@ var DomNodeManager = class {
|
|
|
3736
4109
|
node.style.fontSize = `${element.fontSize ?? DEFAULT_NOTE_FONT_SIZE}px`;
|
|
3737
4110
|
}
|
|
3738
4111
|
}
|
|
3739
|
-
if (element.type === "html"
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
4112
|
+
if (element.type === "html") {
|
|
4113
|
+
if (!node.dataset["initialized"]) {
|
|
4114
|
+
const content = this.htmlContent.get(element.id);
|
|
4115
|
+
if (content) {
|
|
4116
|
+
node.dataset["initialized"] = "true";
|
|
4117
|
+
Object.assign(node.style, {
|
|
4118
|
+
overflow: "hidden",
|
|
4119
|
+
pointerEvents: element.interactive ? "auto" : "none"
|
|
4120
|
+
});
|
|
4121
|
+
node.appendChild(content);
|
|
4122
|
+
}
|
|
4123
|
+
} else {
|
|
4124
|
+
node.style.pointerEvents = element.interactive ? "auto" : "none";
|
|
3748
4125
|
}
|
|
3749
4126
|
}
|
|
3750
4127
|
if (element.type === "text") {
|
|
@@ -3766,10 +4143,13 @@ var DomNodeManager = class {
|
|
|
3766
4143
|
lineHeight: "1.4"
|
|
3767
4144
|
});
|
|
3768
4145
|
node.textContent = element.text || "";
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
4146
|
+
const detector = new DoubleTapDetector();
|
|
4147
|
+
node.addEventListener("pointerup", (e) => {
|
|
4148
|
+
if (detector.feed(e)) {
|
|
4149
|
+
e.stopPropagation();
|
|
4150
|
+
const id = node.dataset["elementId"];
|
|
4151
|
+
if (id) this.onEditRequest(id);
|
|
4152
|
+
}
|
|
3773
4153
|
});
|
|
3774
4154
|
}
|
|
3775
4155
|
if (!this.isEditingElement(element.id)) {
|
|
@@ -4201,7 +4581,8 @@ var Viewport = class {
|
|
|
4201
4581
|
this.domNodeManager = new DomNodeManager({
|
|
4202
4582
|
domLayer: this.domLayer,
|
|
4203
4583
|
onEditRequest: (id) => this.startEditingElement(id),
|
|
4204
|
-
isEditingElement: (id) => this.noteEditor.isEditing && this.noteEditor.editingElementId === id
|
|
4584
|
+
isEditingElement: (id) => this.noteEditor.isEditing && this.noteEditor.editingElementId === id,
|
|
4585
|
+
getVersion: (id) => this.store.getVersion(id)
|
|
4205
4586
|
});
|
|
4206
4587
|
this.interactMode = new InteractMode({
|
|
4207
4588
|
getNode: (id) => this.domNodeManager.getNode(id)
|
|
@@ -4258,7 +4639,8 @@ var Viewport = class {
|
|
|
4258
4639
|
this.toolContext.activeLayerId = this.layerManager.activeLayerId;
|
|
4259
4640
|
this.requestRender();
|
|
4260
4641
|
});
|
|
4261
|
-
this.wrapper.addEventListener("
|
|
4642
|
+
this.wrapper.addEventListener("pointerdown", this.onTapDown);
|
|
4643
|
+
this.wrapper.addEventListener("pointerup", this.onDoubleTap);
|
|
4262
4644
|
this.wrapper.addEventListener("dragover", this.onDragOver);
|
|
4263
4645
|
this.wrapper.addEventListener("drop", this.onDrop);
|
|
4264
4646
|
this.observeResize();
|
|
@@ -4289,6 +4671,9 @@ var Viewport = class {
|
|
|
4289
4671
|
domNodeManager;
|
|
4290
4672
|
interactMode;
|
|
4291
4673
|
gridChangeListeners = /* @__PURE__ */ new Set();
|
|
4674
|
+
doubleTapDetector = new DoubleTapDetector();
|
|
4675
|
+
tapDownX = 0;
|
|
4676
|
+
tapDownY = 0;
|
|
4292
4677
|
get ctx() {
|
|
4293
4678
|
return this.canvasEl.getContext("2d");
|
|
4294
4679
|
}
|
|
@@ -4303,7 +4688,12 @@ var Viewport = class {
|
|
|
4303
4688
|
this.renderLoop.requestRender();
|
|
4304
4689
|
}
|
|
4305
4690
|
exportState() {
|
|
4306
|
-
return exportState(
|
|
4691
|
+
return exportState(
|
|
4692
|
+
this.store.snapshot(),
|
|
4693
|
+
this.camera,
|
|
4694
|
+
this.layerManager.snapshot(),
|
|
4695
|
+
this.layerManager.activeLayerId
|
|
4696
|
+
);
|
|
4307
4697
|
}
|
|
4308
4698
|
exportJSON() {
|
|
4309
4699
|
return JSON.stringify(this.exportState());
|
|
@@ -4319,6 +4709,9 @@ var Viewport = class {
|
|
|
4319
4709
|
if (state.layers && state.layers.length > 0) {
|
|
4320
4710
|
this.layerManager.loadSnapshot(state.layers);
|
|
4321
4711
|
}
|
|
4712
|
+
if (state.activeLayerId) {
|
|
4713
|
+
this.layerManager.setActiveLayer(state.activeLayerId);
|
|
4714
|
+
}
|
|
4322
4715
|
this.domNodeManager.reattachHtmlContent(this.store);
|
|
4323
4716
|
this.history.clear();
|
|
4324
4717
|
this.historyRecorder.resume();
|
|
@@ -4426,7 +4819,8 @@ var Viewport = class {
|
|
|
4426
4819
|
this.interactMode.destroy();
|
|
4427
4820
|
this.noteEditor.destroy(this.store);
|
|
4428
4821
|
this.historyRecorder.destroy();
|
|
4429
|
-
this.wrapper.removeEventListener("
|
|
4822
|
+
this.wrapper.removeEventListener("pointerdown", this.onTapDown);
|
|
4823
|
+
this.wrapper.removeEventListener("pointerup", this.onDoubleTap);
|
|
4430
4824
|
this.wrapper.removeEventListener("dragover", this.onDragOver);
|
|
4431
4825
|
this.wrapper.removeEventListener("drop", this.onDrop);
|
|
4432
4826
|
this.inputHandler.destroy();
|
|
@@ -4464,7 +4858,17 @@ var Viewport = class {
|
|
|
4464
4858
|
}
|
|
4465
4859
|
}
|
|
4466
4860
|
}
|
|
4467
|
-
|
|
4861
|
+
onTapDown = (e) => {
|
|
4862
|
+
this.tapDownX = e.clientX;
|
|
4863
|
+
this.tapDownY = e.clientY;
|
|
4864
|
+
};
|
|
4865
|
+
onDoubleTap = (e) => {
|
|
4866
|
+
const dx = e.clientX - this.tapDownX;
|
|
4867
|
+
const dy = e.clientY - this.tapDownY;
|
|
4868
|
+
const moved = Math.sqrt(dx * dx + dy * dy);
|
|
4869
|
+
if (moved > 10) return;
|
|
4870
|
+
if (!this.doubleTapDetector.feed(e)) return;
|
|
4871
|
+
if (typeof document.elementFromPoint !== "function") return;
|
|
4468
4872
|
const el = document.elementFromPoint(e.clientX, e.clientY);
|
|
4469
4873
|
const nodeEl = el?.closest("[data-element-id]");
|
|
4470
4874
|
if (nodeEl) {
|
|
@@ -4561,7 +4965,10 @@ var Viewport = class {
|
|
|
4561
4965
|
position: "relative",
|
|
4562
4966
|
width: "100%",
|
|
4563
4967
|
height: "100%",
|
|
4564
|
-
overflow: "hidden"
|
|
4968
|
+
overflow: "hidden",
|
|
4969
|
+
overscrollBehavior: "none",
|
|
4970
|
+
userSelect: "none",
|
|
4971
|
+
webkitUserSelect: "none"
|
|
4565
4972
|
});
|
|
4566
4973
|
return el;
|
|
4567
4974
|
}
|
|
@@ -4624,6 +5031,26 @@ var Viewport = class {
|
|
|
4624
5031
|
}
|
|
4625
5032
|
};
|
|
4626
5033
|
|
|
5034
|
+
// src/elements/bounds.ts
|
|
5035
|
+
function getElementsBoundingBox(elements) {
|
|
5036
|
+
let minX = Infinity;
|
|
5037
|
+
let minY = Infinity;
|
|
5038
|
+
let maxX = -Infinity;
|
|
5039
|
+
let maxY = -Infinity;
|
|
5040
|
+
let found = false;
|
|
5041
|
+
for (const el of elements) {
|
|
5042
|
+
const b = getElementBounds(el);
|
|
5043
|
+
if (!b) continue;
|
|
5044
|
+
found = true;
|
|
5045
|
+
if (b.x < minX) minX = b.x;
|
|
5046
|
+
if (b.y < minY) minY = b.y;
|
|
5047
|
+
if (b.x + b.w > maxX) maxX = b.x + b.w;
|
|
5048
|
+
if (b.y + b.h > maxY) maxY = b.y + b.h;
|
|
5049
|
+
}
|
|
5050
|
+
if (!found) return null;
|
|
5051
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
5052
|
+
}
|
|
5053
|
+
|
|
4627
5054
|
// src/tools/hand-tool.ts
|
|
4628
5055
|
var HandTool = class {
|
|
4629
5056
|
name = "hand";
|
|
@@ -4976,9 +5403,16 @@ var SelectTool = class {
|
|
|
4976
5403
|
lastWorld = { x: 0, y: 0 };
|
|
4977
5404
|
currentWorld = { x: 0, y: 0 };
|
|
4978
5405
|
ctx = null;
|
|
5406
|
+
pendingSingleSelectId = null;
|
|
5407
|
+
hasDragged = false;
|
|
5408
|
+
resizeAspectRatio = 0;
|
|
4979
5409
|
get selectedIds() {
|
|
4980
5410
|
return [...this._selectedIds];
|
|
4981
5411
|
}
|
|
5412
|
+
setSelection(ids) {
|
|
5413
|
+
this._selectedIds = ids;
|
|
5414
|
+
this.ctx?.requestRender();
|
|
5415
|
+
}
|
|
4982
5416
|
get isMarqueeActive() {
|
|
4983
5417
|
return this.mode.type === "marquee";
|
|
4984
5418
|
}
|
|
@@ -5017,7 +5451,8 @@ var SelectTool = class {
|
|
|
5017
5451
|
const resizeHit = this.hitTestResizeHandle(world, ctx);
|
|
5018
5452
|
if (resizeHit) {
|
|
5019
5453
|
const el = ctx.store.getById(resizeHit.elementId);
|
|
5020
|
-
if (el) {
|
|
5454
|
+
if (el && "size" in el) {
|
|
5455
|
+
this.resizeAspectRatio = el.size.h > 0 ? el.size.w / el.size.h : 0;
|
|
5021
5456
|
this.mode = {
|
|
5022
5457
|
type: "resizing",
|
|
5023
5458
|
elementId: resizeHit.elementId,
|
|
@@ -5027,13 +5462,27 @@ var SelectTool = class {
|
|
|
5027
5462
|
return;
|
|
5028
5463
|
}
|
|
5029
5464
|
}
|
|
5465
|
+
this.pendingSingleSelectId = null;
|
|
5466
|
+
this.hasDragged = false;
|
|
5030
5467
|
const hit = this.hitTest(world, ctx);
|
|
5031
5468
|
if (hit) {
|
|
5032
5469
|
const alreadySelected = this._selectedIds.includes(hit.id);
|
|
5033
|
-
if (
|
|
5034
|
-
|
|
5470
|
+
if (state.shiftKey) {
|
|
5471
|
+
if (alreadySelected) {
|
|
5472
|
+
this._selectedIds = this._selectedIds.filter((id) => id !== hit.id);
|
|
5473
|
+
this.mode = { type: "idle" };
|
|
5474
|
+
} else {
|
|
5475
|
+
this._selectedIds = [...this._selectedIds, hit.id];
|
|
5476
|
+
this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
|
|
5477
|
+
}
|
|
5478
|
+
} else {
|
|
5479
|
+
if (!alreadySelected) {
|
|
5480
|
+
this._selectedIds = [hit.id];
|
|
5481
|
+
} else if (this._selectedIds.length > 1) {
|
|
5482
|
+
this.pendingSingleSelectId = hit.id;
|
|
5483
|
+
}
|
|
5484
|
+
this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
|
|
5035
5485
|
}
|
|
5036
|
-
this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
|
|
5037
5486
|
} else {
|
|
5038
5487
|
this._selectedIds = [];
|
|
5039
5488
|
this.mode = { type: "marquee", start: world };
|
|
@@ -5055,10 +5504,11 @@ var SelectTool = class {
|
|
|
5055
5504
|
}
|
|
5056
5505
|
if (this.mode.type === "resizing") {
|
|
5057
5506
|
ctx.setCursor?.(HANDLE_CURSORS[this.mode.handle]);
|
|
5058
|
-
this.handleResize(world, ctx);
|
|
5507
|
+
this.handleResize(world, ctx, state.shiftKey);
|
|
5059
5508
|
return;
|
|
5060
5509
|
}
|
|
5061
5510
|
if (this.mode.type === "dragging" && this._selectedIds.length > 0) {
|
|
5511
|
+
this.hasDragged = true;
|
|
5062
5512
|
ctx.setCursor?.("move");
|
|
5063
5513
|
const snapped = this.snap(world, ctx);
|
|
5064
5514
|
const dx = snapped.x - this.lastWorld.x;
|
|
@@ -5127,6 +5577,11 @@ var SelectTool = class {
|
|
|
5127
5577
|
}
|
|
5128
5578
|
ctx.requestRender();
|
|
5129
5579
|
}
|
|
5580
|
+
if (!this.hasDragged && this.pendingSingleSelectId !== null) {
|
|
5581
|
+
this._selectedIds = [this.pendingSingleSelectId];
|
|
5582
|
+
}
|
|
5583
|
+
this.pendingSingleSelectId = null;
|
|
5584
|
+
this.hasDragged = false;
|
|
5130
5585
|
this.mode = { type: "idle" };
|
|
5131
5586
|
ctx.setCursor?.("default");
|
|
5132
5587
|
}
|
|
@@ -5173,7 +5628,7 @@ var SelectTool = class {
|
|
|
5173
5628
|
const hit = this.hitTest(world, ctx);
|
|
5174
5629
|
ctx.setCursor?.(hit ? "move" : "default");
|
|
5175
5630
|
}
|
|
5176
|
-
handleResize(world, ctx) {
|
|
5631
|
+
handleResize(world, ctx, shiftKey = false) {
|
|
5177
5632
|
if (this.mode.type !== "resizing") return;
|
|
5178
5633
|
const el = ctx.store.getById(this.mode.elementId);
|
|
5179
5634
|
if (!el || !("size" in el) || el.locked) return;
|
|
@@ -5204,6 +5659,21 @@ var SelectTool = class {
|
|
|
5204
5659
|
h -= dy;
|
|
5205
5660
|
break;
|
|
5206
5661
|
}
|
|
5662
|
+
if (shiftKey && this.resizeAspectRatio > 0) {
|
|
5663
|
+
const absDw = Math.abs(w - el.size.w);
|
|
5664
|
+
const absDh = Math.abs(h - el.size.h);
|
|
5665
|
+
if (absDw >= absDh) {
|
|
5666
|
+
h = w / this.resizeAspectRatio;
|
|
5667
|
+
} else {
|
|
5668
|
+
w = h * this.resizeAspectRatio;
|
|
5669
|
+
}
|
|
5670
|
+
if (handle === "nw" || handle === "sw") {
|
|
5671
|
+
x = el.position.x + el.size.w - w;
|
|
5672
|
+
}
|
|
5673
|
+
if (handle === "nw" || handle === "ne") {
|
|
5674
|
+
y = el.position.y + el.size.h - h;
|
|
5675
|
+
}
|
|
5676
|
+
}
|
|
5207
5677
|
if (w < MIN_ELEMENT_SIZE) {
|
|
5208
5678
|
if (handle === "nw" || handle === "sw") x = el.position.x + el.size.w - MIN_ELEMENT_SIZE;
|
|
5209
5679
|
w = MIN_ELEMENT_SIZE;
|
|
@@ -6307,7 +6777,7 @@ var UpdateLayerCommand = class {
|
|
|
6307
6777
|
};
|
|
6308
6778
|
|
|
6309
6779
|
// src/index.ts
|
|
6310
|
-
var VERSION = "0.
|
|
6780
|
+
var VERSION = "0.15.0";
|
|
6311
6781
|
// Annotate the CommonJS export names for ESM import in node:
|
|
6312
6782
|
0 && (module.exports = {
|
|
6313
6783
|
AddElementCommand,
|
|
@@ -6319,6 +6789,7 @@ var VERSION = "0.11.0";
|
|
|
6319
6789
|
CreateLayerCommand,
|
|
6320
6790
|
DEFAULT_FONT_SIZE_PRESETS,
|
|
6321
6791
|
DEFAULT_NOTE_FONT_SIZE,
|
|
6792
|
+
DoubleTapDetector,
|
|
6322
6793
|
ElementRenderer,
|
|
6323
6794
|
ElementStore,
|
|
6324
6795
|
EraserTool,
|
|
@@ -6327,6 +6798,7 @@ var VERSION = "0.11.0";
|
|
|
6327
6798
|
HistoryRecorder,
|
|
6328
6799
|
HistoryStack,
|
|
6329
6800
|
ImageTool,
|
|
6801
|
+
InputFilter,
|
|
6330
6802
|
InputHandler,
|
|
6331
6803
|
LayerManager,
|
|
6332
6804
|
MeasureTool,
|
|
@@ -6372,6 +6844,7 @@ var VERSION = "0.11.0";
|
|
|
6372
6844
|
getEdgeIntersection,
|
|
6373
6845
|
getElementBounds,
|
|
6374
6846
|
getElementCenter,
|
|
6847
|
+
getElementsBoundingBox,
|
|
6375
6848
|
getHexCellsInCone,
|
|
6376
6849
|
getHexCellsInLine,
|
|
6377
6850
|
getHexCellsInRadius,
|