@fieldnotes/core 0.13.0 → 0.14.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 +389 -46
- package/dist/index.d.cts +66 -4
- package/dist/index.d.ts +66 -4
- package/dist/index.js +386 -46
- 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,30 @@ 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
|
+
}
|
|
854
978
|
};
|
|
855
979
|
onKeyUp = (e) => {
|
|
856
980
|
if (e.key === " ") {
|
|
857
981
|
this.spaceHeld = false;
|
|
982
|
+
if (this.activePointers.size === 0) {
|
|
983
|
+
if (this.lastPointerEvent) {
|
|
984
|
+
this.dispatchToolHover(this.lastPointerEvent);
|
|
985
|
+
} else {
|
|
986
|
+
this.toolContext?.setCursor?.("default");
|
|
987
|
+
}
|
|
988
|
+
}
|
|
858
989
|
}
|
|
859
990
|
};
|
|
860
991
|
startPinch() {
|
|
992
|
+
this.inputFilter.reset();
|
|
993
|
+
this.deferredDown = null;
|
|
861
994
|
this.isPanning = true;
|
|
862
995
|
const [a, b] = this.getPinchPoints();
|
|
863
996
|
this.lastPinchDistance = this.distance(a, b);
|
|
@@ -897,7 +1030,9 @@ var InputHandler = class {
|
|
|
897
1030
|
return {
|
|
898
1031
|
x: e.clientX - rect.left,
|
|
899
1032
|
y: e.clientY - rect.top,
|
|
900
|
-
pressure: e.pressure
|
|
1033
|
+
pressure: e.pressure,
|
|
1034
|
+
pointerType: e.pointerType === "touch" || e.pointerType === "pen" ? e.pointerType : "mouse",
|
|
1035
|
+
shiftKey: e.shiftKey
|
|
901
1036
|
};
|
|
902
1037
|
}
|
|
903
1038
|
dispatchToolDown(e) {
|
|
@@ -950,11 +1085,120 @@ var InputHandler = class {
|
|
|
950
1085
|
this.historyRecorder?.resume();
|
|
951
1086
|
this.toolContext.requestRender();
|
|
952
1087
|
}
|
|
1088
|
+
handleCopy() {
|
|
1089
|
+
if (!this.toolManager || !this.toolContext || this.isToolActive) return;
|
|
1090
|
+
const tool = this.toolManager.activeTool;
|
|
1091
|
+
if (tool?.name !== "select") return;
|
|
1092
|
+
const selectTool = tool;
|
|
1093
|
+
const ids = selectTool.selectedIds;
|
|
1094
|
+
if (ids.length === 0) return;
|
|
1095
|
+
this.clipboard = [];
|
|
1096
|
+
for (const id of ids) {
|
|
1097
|
+
const el = this.toolContext.store.getById(id);
|
|
1098
|
+
if (el) this.clipboard.push(structuredClone(el));
|
|
1099
|
+
}
|
|
1100
|
+
this.pasteCount = 0;
|
|
1101
|
+
}
|
|
1102
|
+
handlePaste() {
|
|
1103
|
+
if (!this.toolManager || !this.toolContext || this.clipboard.length === 0 || this.isToolActive)
|
|
1104
|
+
return;
|
|
1105
|
+
const tool = this.toolManager.activeTool;
|
|
1106
|
+
if (tool?.name !== "select") return;
|
|
1107
|
+
const selectTool = tool;
|
|
1108
|
+
this.pasteCount++;
|
|
1109
|
+
const offset = this.pasteCount * 20;
|
|
1110
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
1111
|
+
for (const el of this.clipboard) {
|
|
1112
|
+
idMap.set(el.id, createId(el.type));
|
|
1113
|
+
}
|
|
1114
|
+
const newIds = [];
|
|
1115
|
+
this.historyRecorder?.begin();
|
|
1116
|
+
for (const el of this.clipboard) {
|
|
1117
|
+
const clone = structuredClone(el);
|
|
1118
|
+
const newId = idMap.get(el.id);
|
|
1119
|
+
if (!newId) continue;
|
|
1120
|
+
clone.id = newId;
|
|
1121
|
+
clone.position = { x: clone.position.x + offset, y: clone.position.y + offset };
|
|
1122
|
+
if (clone.type === "arrow") {
|
|
1123
|
+
const arrow = clone;
|
|
1124
|
+
arrow.from = { x: arrow.from.x + offset, y: arrow.from.y + offset };
|
|
1125
|
+
arrow.to = { x: arrow.to.x + offset, y: arrow.to.y + offset };
|
|
1126
|
+
delete arrow.cachedControlPoint;
|
|
1127
|
+
if (arrow.fromBinding) {
|
|
1128
|
+
const newTarget = idMap.get(arrow.fromBinding.elementId);
|
|
1129
|
+
if (newTarget) {
|
|
1130
|
+
arrow.fromBinding = { elementId: newTarget };
|
|
1131
|
+
} else {
|
|
1132
|
+
delete arrow.fromBinding;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
if (arrow.toBinding) {
|
|
1136
|
+
const newTarget = idMap.get(arrow.toBinding.elementId);
|
|
1137
|
+
if (newTarget) {
|
|
1138
|
+
arrow.toBinding = { elementId: newTarget };
|
|
1139
|
+
} else {
|
|
1140
|
+
delete arrow.toBinding;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
if (this.toolContext.activeLayerId) {
|
|
1145
|
+
clone.layerId = this.toolContext.activeLayerId;
|
|
1146
|
+
}
|
|
1147
|
+
this.toolContext.store.add(clone);
|
|
1148
|
+
newIds.push(clone.id);
|
|
1149
|
+
}
|
|
1150
|
+
this.historyRecorder?.commit();
|
|
1151
|
+
selectTool.setSelection(newIds);
|
|
1152
|
+
this.toolContext.requestRender();
|
|
1153
|
+
}
|
|
953
1154
|
cancelToolIfActive(e) {
|
|
954
1155
|
if (this.isToolActive) {
|
|
955
1156
|
this.dispatchToolUp(e);
|
|
956
1157
|
this.isToolActive = false;
|
|
957
1158
|
}
|
|
1159
|
+
this.deferredDown = null;
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
|
|
1163
|
+
// src/canvas/double-tap-detector.ts
|
|
1164
|
+
var DEFAULT_TIMEOUT = 300;
|
|
1165
|
+
var DEFAULT_MAX_DISTANCE = 20;
|
|
1166
|
+
var DoubleTapDetector = class {
|
|
1167
|
+
timeout;
|
|
1168
|
+
maxDistance;
|
|
1169
|
+
lastTapTime = 0;
|
|
1170
|
+
lastTapX = 0;
|
|
1171
|
+
lastTapY = 0;
|
|
1172
|
+
hasPendingTap = false;
|
|
1173
|
+
constructor(options) {
|
|
1174
|
+
this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
|
|
1175
|
+
this.maxDistance = options?.maxDistance ?? DEFAULT_MAX_DISTANCE;
|
|
1176
|
+
}
|
|
1177
|
+
feed(e) {
|
|
1178
|
+
const now = Date.now();
|
|
1179
|
+
const x = e.clientX;
|
|
1180
|
+
const y = e.clientY;
|
|
1181
|
+
if (this.hasPendingTap) {
|
|
1182
|
+
const elapsed = now - this.lastTapTime;
|
|
1183
|
+
const dx = x - this.lastTapX;
|
|
1184
|
+
const dy = y - this.lastTapY;
|
|
1185
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
1186
|
+
if (elapsed <= this.timeout && dist <= this.maxDistance) {
|
|
1187
|
+
this.reset();
|
|
1188
|
+
return true;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
this.lastTapTime = now;
|
|
1192
|
+
this.lastTapX = x;
|
|
1193
|
+
this.lastTapY = y;
|
|
1194
|
+
this.hasPendingTap = true;
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1197
|
+
reset() {
|
|
1198
|
+
this.hasPendingTap = false;
|
|
1199
|
+
this.lastTapTime = 0;
|
|
1200
|
+
this.lastTapX = 0;
|
|
1201
|
+
this.lastTapY = 0;
|
|
958
1202
|
}
|
|
959
1203
|
};
|
|
960
1204
|
|
|
@@ -1202,19 +1446,23 @@ var ElementStore = class {
|
|
|
1202
1446
|
bus = new EventBus();
|
|
1203
1447
|
layerOrderMap = /* @__PURE__ */ new Map();
|
|
1204
1448
|
spatialIndex = new Quadtree({ x: -1e5, y: -1e5, w: 2e5, h: 2e5 });
|
|
1449
|
+
sortedCache = null;
|
|
1205
1450
|
get count() {
|
|
1206
1451
|
return this.elements.size;
|
|
1207
1452
|
}
|
|
1208
1453
|
setLayerOrder(order) {
|
|
1209
1454
|
this.layerOrderMap = new Map(order);
|
|
1455
|
+
this.sortedCache = null;
|
|
1210
1456
|
}
|
|
1211
1457
|
getAll() {
|
|
1212
|
-
|
|
1458
|
+
if (this.sortedCache) return this.sortedCache;
|
|
1459
|
+
this.sortedCache = [...this.elements.values()].sort((a, b) => {
|
|
1213
1460
|
const layerA = this.layerOrderMap.get(a.layerId) ?? 0;
|
|
1214
1461
|
const layerB = this.layerOrderMap.get(b.layerId) ?? 0;
|
|
1215
1462
|
if (layerA !== layerB) return layerA - layerB;
|
|
1216
1463
|
return a.zIndex - b.zIndex;
|
|
1217
1464
|
});
|
|
1465
|
+
return this.sortedCache;
|
|
1218
1466
|
}
|
|
1219
1467
|
getById(id) {
|
|
1220
1468
|
return this.elements.get(id);
|
|
@@ -1225,6 +1473,7 @@ var ElementStore = class {
|
|
|
1225
1473
|
);
|
|
1226
1474
|
}
|
|
1227
1475
|
add(element) {
|
|
1476
|
+
this.sortedCache = null;
|
|
1228
1477
|
this.elements.set(element.id, element);
|
|
1229
1478
|
const bounds = getElementBounds(element);
|
|
1230
1479
|
if (bounds) this.spatialIndex.insert(element.id, bounds);
|
|
@@ -1233,11 +1482,15 @@ var ElementStore = class {
|
|
|
1233
1482
|
update(id, partial) {
|
|
1234
1483
|
const existing = this.elements.get(id);
|
|
1235
1484
|
if (!existing) return;
|
|
1485
|
+
this.sortedCache = null;
|
|
1236
1486
|
const updated = { ...existing, ...partial, id: existing.id, type: existing.type };
|
|
1237
1487
|
if (updated.type === "arrow") {
|
|
1238
1488
|
const arrow = updated;
|
|
1239
1489
|
arrow.cachedControlPoint = getArrowControlPoint(arrow.from, arrow.to, arrow.bend);
|
|
1240
1490
|
}
|
|
1491
|
+
if (updated.type === "note" && "text" in partial) {
|
|
1492
|
+
updated.text = sanitizeNoteHtml(updated.text);
|
|
1493
|
+
}
|
|
1241
1494
|
this.elements.set(id, updated);
|
|
1242
1495
|
const newBounds = getElementBounds(updated);
|
|
1243
1496
|
if (newBounds) {
|
|
@@ -1248,11 +1501,13 @@ var ElementStore = class {
|
|
|
1248
1501
|
remove(id) {
|
|
1249
1502
|
const element = this.elements.get(id);
|
|
1250
1503
|
if (!element) return;
|
|
1504
|
+
this.sortedCache = null;
|
|
1251
1505
|
this.elements.delete(id);
|
|
1252
1506
|
this.spatialIndex.remove(id);
|
|
1253
1507
|
this.bus.emit("remove", element);
|
|
1254
1508
|
}
|
|
1255
1509
|
clear() {
|
|
1510
|
+
this.sortedCache = null;
|
|
1256
1511
|
this.elements.clear();
|
|
1257
1512
|
this.spatialIndex.clear();
|
|
1258
1513
|
this.bus.emit("clear", null);
|
|
@@ -1261,6 +1516,7 @@ var ElementStore = class {
|
|
|
1261
1516
|
return this.getAll().map((el) => ({ ...el }));
|
|
1262
1517
|
}
|
|
1263
1518
|
loadSnapshot(elements) {
|
|
1519
|
+
this.sortedCache = null;
|
|
1264
1520
|
this.elements.clear();
|
|
1265
1521
|
this.spatialIndex.clear();
|
|
1266
1522
|
for (const el of elements) {
|
|
@@ -1268,6 +1524,10 @@ var ElementStore = class {
|
|
|
1268
1524
|
const bounds = getElementBounds(el);
|
|
1269
1525
|
if (bounds) this.spatialIndex.insert(el.id, bounds);
|
|
1270
1526
|
}
|
|
1527
|
+
this.bus.emit("clear", null);
|
|
1528
|
+
for (const el of elements) {
|
|
1529
|
+
this.bus.emit("add", el);
|
|
1530
|
+
}
|
|
1271
1531
|
}
|
|
1272
1532
|
queryRect(rect) {
|
|
1273
1533
|
const ids = this.spatialIndex.query(rect);
|
|
@@ -2317,12 +2577,6 @@ var ElementRenderer = class {
|
|
|
2317
2577
|
}
|
|
2318
2578
|
};
|
|
2319
2579
|
|
|
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
2580
|
// src/elements/element-factory.ts
|
|
2327
2581
|
var DEFAULT_NOTE_FONT_SIZE = 18;
|
|
2328
2582
|
function createStroke(input) {
|
|
@@ -2348,7 +2602,7 @@ function createNote(input) {
|
|
|
2348
2602
|
locked: input.locked ?? false,
|
|
2349
2603
|
layerId: input.layerId ?? "",
|
|
2350
2604
|
size: input.size ?? { w: 200, h: 100 },
|
|
2351
|
-
text: input.text ?? "",
|
|
2605
|
+
text: sanitizeNoteHtml(input.text ?? ""),
|
|
2352
2606
|
backgroundColor: input.backgroundColor ?? "#ffeb3b",
|
|
2353
2607
|
textColor: input.textColor ?? "#000000",
|
|
2354
2608
|
fontSize: input.fontSize ?? DEFAULT_NOTE_FONT_SIZE
|
|
@@ -2397,6 +2651,7 @@ function createHtmlElement(input) {
|
|
|
2397
2651
|
size: input.size
|
|
2398
2652
|
};
|
|
2399
2653
|
if (input.domId) el.domId = input.domId;
|
|
2654
|
+
if (input.interactive) el.interactive = input.interactive;
|
|
2400
2655
|
return el;
|
|
2401
2656
|
}
|
|
2402
2657
|
function createShape(input) {
|
|
@@ -2509,7 +2764,7 @@ function getActiveFormats() {
|
|
|
2509
2764
|
}
|
|
2510
2765
|
|
|
2511
2766
|
// src/elements/note-toolbar.ts
|
|
2512
|
-
var TOOLBAR_HEIGHT =
|
|
2767
|
+
var TOOLBAR_HEIGHT = 52;
|
|
2513
2768
|
var TOOLBAR_GAP = 4;
|
|
2514
2769
|
var FORMAT_BUTTONS = [
|
|
2515
2770
|
{ label: "B", format: "bold", command: "bold" },
|
|
@@ -2596,9 +2851,9 @@ var NoteToolbar = class {
|
|
|
2596
2851
|
fontWeight: config.format === "bold" ? "bold" : "normal",
|
|
2597
2852
|
fontStyle: config.format === "italic" ? "italic" : "normal",
|
|
2598
2853
|
textDecoration: config.format === "underline" ? "underline" : config.format === "strikethrough" ? "line-through" : "none",
|
|
2599
|
-
minWidth: "
|
|
2600
|
-
height: "
|
|
2601
|
-
lineHeight: "
|
|
2854
|
+
minWidth: "44px",
|
|
2855
|
+
height: "44px",
|
|
2856
|
+
lineHeight: "44px"
|
|
2602
2857
|
});
|
|
2603
2858
|
btn.addEventListener("pointerdown", (e) => {
|
|
2604
2859
|
e.preventDefault();
|
|
@@ -2616,7 +2871,7 @@ var NoteToolbar = class {
|
|
|
2616
2871
|
cursor: "pointer",
|
|
2617
2872
|
padding: "2px",
|
|
2618
2873
|
fontSize: "12px",
|
|
2619
|
-
height: "
|
|
2874
|
+
height: "44px",
|
|
2620
2875
|
marginLeft: "4px"
|
|
2621
2876
|
});
|
|
2622
2877
|
for (const preset of this.fontSizePresets) {
|
|
@@ -3614,10 +3869,13 @@ var DomNodeManager = class {
|
|
|
3614
3869
|
wordWrap: "break-word"
|
|
3615
3870
|
});
|
|
3616
3871
|
node.innerHTML = element.text || "";
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3872
|
+
const detector = new DoubleTapDetector();
|
|
3873
|
+
node.addEventListener("pointerup", (e) => {
|
|
3874
|
+
if (detector.feed(e)) {
|
|
3875
|
+
e.stopPropagation();
|
|
3876
|
+
const id = node.dataset["elementId"];
|
|
3877
|
+
if (id) this.onEditRequest(id);
|
|
3878
|
+
}
|
|
3621
3879
|
});
|
|
3622
3880
|
}
|
|
3623
3881
|
if (!this.isEditingElement(element.id)) {
|
|
@@ -3630,15 +3888,19 @@ var DomNodeManager = class {
|
|
|
3630
3888
|
node.style.fontSize = `${element.fontSize ?? DEFAULT_NOTE_FONT_SIZE}px`;
|
|
3631
3889
|
}
|
|
3632
3890
|
}
|
|
3633
|
-
if (element.type === "html"
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3891
|
+
if (element.type === "html") {
|
|
3892
|
+
if (!node.dataset["initialized"]) {
|
|
3893
|
+
const content = this.htmlContent.get(element.id);
|
|
3894
|
+
if (content) {
|
|
3895
|
+
node.dataset["initialized"] = "true";
|
|
3896
|
+
Object.assign(node.style, {
|
|
3897
|
+
overflow: "hidden",
|
|
3898
|
+
pointerEvents: element.interactive ? "auto" : "none"
|
|
3899
|
+
});
|
|
3900
|
+
node.appendChild(content);
|
|
3901
|
+
}
|
|
3902
|
+
} else {
|
|
3903
|
+
node.style.pointerEvents = element.interactive ? "auto" : "none";
|
|
3642
3904
|
}
|
|
3643
3905
|
}
|
|
3644
3906
|
if (element.type === "text") {
|
|
@@ -3660,10 +3922,13 @@ var DomNodeManager = class {
|
|
|
3660
3922
|
lineHeight: "1.4"
|
|
3661
3923
|
});
|
|
3662
3924
|
node.textContent = element.text || "";
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3925
|
+
const detector = new DoubleTapDetector();
|
|
3926
|
+
node.addEventListener("pointerup", (e) => {
|
|
3927
|
+
if (detector.feed(e)) {
|
|
3928
|
+
e.stopPropagation();
|
|
3929
|
+
const id = node.dataset["elementId"];
|
|
3930
|
+
if (id) this.onEditRequest(id);
|
|
3931
|
+
}
|
|
3667
3932
|
});
|
|
3668
3933
|
}
|
|
3669
3934
|
if (!this.isEditingElement(element.id)) {
|
|
@@ -4152,7 +4417,8 @@ var Viewport = class {
|
|
|
4152
4417
|
this.toolContext.activeLayerId = this.layerManager.activeLayerId;
|
|
4153
4418
|
this.requestRender();
|
|
4154
4419
|
});
|
|
4155
|
-
this.wrapper.addEventListener("
|
|
4420
|
+
this.wrapper.addEventListener("pointerdown", this.onTapDown);
|
|
4421
|
+
this.wrapper.addEventListener("pointerup", this.onDoubleTap);
|
|
4156
4422
|
this.wrapper.addEventListener("dragover", this.onDragOver);
|
|
4157
4423
|
this.wrapper.addEventListener("drop", this.onDrop);
|
|
4158
4424
|
this.observeResize();
|
|
@@ -4183,6 +4449,9 @@ var Viewport = class {
|
|
|
4183
4449
|
domNodeManager;
|
|
4184
4450
|
interactMode;
|
|
4185
4451
|
gridChangeListeners = /* @__PURE__ */ new Set();
|
|
4452
|
+
doubleTapDetector = new DoubleTapDetector();
|
|
4453
|
+
tapDownX = 0;
|
|
4454
|
+
tapDownY = 0;
|
|
4186
4455
|
get ctx() {
|
|
4187
4456
|
return this.canvasEl.getContext("2d");
|
|
4188
4457
|
}
|
|
@@ -4197,7 +4466,12 @@ var Viewport = class {
|
|
|
4197
4466
|
this.renderLoop.requestRender();
|
|
4198
4467
|
}
|
|
4199
4468
|
exportState() {
|
|
4200
|
-
return exportState(
|
|
4469
|
+
return exportState(
|
|
4470
|
+
this.store.snapshot(),
|
|
4471
|
+
this.camera,
|
|
4472
|
+
this.layerManager.snapshot(),
|
|
4473
|
+
this.layerManager.activeLayerId
|
|
4474
|
+
);
|
|
4201
4475
|
}
|
|
4202
4476
|
exportJSON() {
|
|
4203
4477
|
return JSON.stringify(this.exportState());
|
|
@@ -4213,6 +4487,9 @@ var Viewport = class {
|
|
|
4213
4487
|
if (state.layers && state.layers.length > 0) {
|
|
4214
4488
|
this.layerManager.loadSnapshot(state.layers);
|
|
4215
4489
|
}
|
|
4490
|
+
if (state.activeLayerId) {
|
|
4491
|
+
this.layerManager.setActiveLayer(state.activeLayerId);
|
|
4492
|
+
}
|
|
4216
4493
|
this.domNodeManager.reattachHtmlContent(this.store);
|
|
4217
4494
|
this.history.clear();
|
|
4218
4495
|
this.historyRecorder.resume();
|
|
@@ -4320,7 +4597,8 @@ var Viewport = class {
|
|
|
4320
4597
|
this.interactMode.destroy();
|
|
4321
4598
|
this.noteEditor.destroy(this.store);
|
|
4322
4599
|
this.historyRecorder.destroy();
|
|
4323
|
-
this.wrapper.removeEventListener("
|
|
4600
|
+
this.wrapper.removeEventListener("pointerdown", this.onTapDown);
|
|
4601
|
+
this.wrapper.removeEventListener("pointerup", this.onDoubleTap);
|
|
4324
4602
|
this.wrapper.removeEventListener("dragover", this.onDragOver);
|
|
4325
4603
|
this.wrapper.removeEventListener("drop", this.onDrop);
|
|
4326
4604
|
this.inputHandler.destroy();
|
|
@@ -4358,7 +4636,17 @@ var Viewport = class {
|
|
|
4358
4636
|
}
|
|
4359
4637
|
}
|
|
4360
4638
|
}
|
|
4361
|
-
|
|
4639
|
+
onTapDown = (e) => {
|
|
4640
|
+
this.tapDownX = e.clientX;
|
|
4641
|
+
this.tapDownY = e.clientY;
|
|
4642
|
+
};
|
|
4643
|
+
onDoubleTap = (e) => {
|
|
4644
|
+
const dx = e.clientX - this.tapDownX;
|
|
4645
|
+
const dy = e.clientY - this.tapDownY;
|
|
4646
|
+
const moved = Math.sqrt(dx * dx + dy * dy);
|
|
4647
|
+
if (moved > 10) return;
|
|
4648
|
+
if (!this.doubleTapDetector.feed(e)) return;
|
|
4649
|
+
if (typeof document.elementFromPoint !== "function") return;
|
|
4362
4650
|
const el = document.elementFromPoint(e.clientX, e.clientY);
|
|
4363
4651
|
const nodeEl = el?.closest("[data-element-id]");
|
|
4364
4652
|
if (nodeEl) {
|
|
@@ -4455,7 +4743,10 @@ var Viewport = class {
|
|
|
4455
4743
|
position: "relative",
|
|
4456
4744
|
width: "100%",
|
|
4457
4745
|
height: "100%",
|
|
4458
|
-
overflow: "hidden"
|
|
4746
|
+
overflow: "hidden",
|
|
4747
|
+
overscrollBehavior: "none",
|
|
4748
|
+
userSelect: "none",
|
|
4749
|
+
webkitUserSelect: "none"
|
|
4459
4750
|
});
|
|
4460
4751
|
return el;
|
|
4461
4752
|
}
|
|
@@ -4518,6 +4809,26 @@ var Viewport = class {
|
|
|
4518
4809
|
}
|
|
4519
4810
|
};
|
|
4520
4811
|
|
|
4812
|
+
// src/elements/bounds.ts
|
|
4813
|
+
function getElementsBoundingBox(elements) {
|
|
4814
|
+
let minX = Infinity;
|
|
4815
|
+
let minY = Infinity;
|
|
4816
|
+
let maxX = -Infinity;
|
|
4817
|
+
let maxY = -Infinity;
|
|
4818
|
+
let found = false;
|
|
4819
|
+
for (const el of elements) {
|
|
4820
|
+
const b = getElementBounds(el);
|
|
4821
|
+
if (!b) continue;
|
|
4822
|
+
found = true;
|
|
4823
|
+
if (b.x < minX) minX = b.x;
|
|
4824
|
+
if (b.y < minY) minY = b.y;
|
|
4825
|
+
if (b.x + b.w > maxX) maxX = b.x + b.w;
|
|
4826
|
+
if (b.y + b.h > maxY) maxY = b.y + b.h;
|
|
4827
|
+
}
|
|
4828
|
+
if (!found) return null;
|
|
4829
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
4830
|
+
}
|
|
4831
|
+
|
|
4521
4832
|
// src/tools/hand-tool.ts
|
|
4522
4833
|
var HandTool = class {
|
|
4523
4834
|
name = "hand";
|
|
@@ -4870,9 +5181,15 @@ var SelectTool = class {
|
|
|
4870
5181
|
lastWorld = { x: 0, y: 0 };
|
|
4871
5182
|
currentWorld = { x: 0, y: 0 };
|
|
4872
5183
|
ctx = null;
|
|
5184
|
+
pendingSingleSelectId = null;
|
|
5185
|
+
hasDragged = false;
|
|
4873
5186
|
get selectedIds() {
|
|
4874
5187
|
return [...this._selectedIds];
|
|
4875
5188
|
}
|
|
5189
|
+
setSelection(ids) {
|
|
5190
|
+
this._selectedIds = ids;
|
|
5191
|
+
this.ctx?.requestRender();
|
|
5192
|
+
}
|
|
4876
5193
|
get isMarqueeActive() {
|
|
4877
5194
|
return this.mode.type === "marquee";
|
|
4878
5195
|
}
|
|
@@ -4921,13 +5238,27 @@ var SelectTool = class {
|
|
|
4921
5238
|
return;
|
|
4922
5239
|
}
|
|
4923
5240
|
}
|
|
5241
|
+
this.pendingSingleSelectId = null;
|
|
5242
|
+
this.hasDragged = false;
|
|
4924
5243
|
const hit = this.hitTest(world, ctx);
|
|
4925
5244
|
if (hit) {
|
|
4926
5245
|
const alreadySelected = this._selectedIds.includes(hit.id);
|
|
4927
|
-
if (
|
|
4928
|
-
|
|
5246
|
+
if (state.shiftKey) {
|
|
5247
|
+
if (alreadySelected) {
|
|
5248
|
+
this._selectedIds = this._selectedIds.filter((id) => id !== hit.id);
|
|
5249
|
+
this.mode = { type: "idle" };
|
|
5250
|
+
} else {
|
|
5251
|
+
this._selectedIds = [...this._selectedIds, hit.id];
|
|
5252
|
+
this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
|
|
5253
|
+
}
|
|
5254
|
+
} else {
|
|
5255
|
+
if (!alreadySelected) {
|
|
5256
|
+
this._selectedIds = [hit.id];
|
|
5257
|
+
} else if (this._selectedIds.length > 1) {
|
|
5258
|
+
this.pendingSingleSelectId = hit.id;
|
|
5259
|
+
}
|
|
5260
|
+
this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
|
|
4929
5261
|
}
|
|
4930
|
-
this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
|
|
4931
5262
|
} else {
|
|
4932
5263
|
this._selectedIds = [];
|
|
4933
5264
|
this.mode = { type: "marquee", start: world };
|
|
@@ -4953,6 +5284,7 @@ var SelectTool = class {
|
|
|
4953
5284
|
return;
|
|
4954
5285
|
}
|
|
4955
5286
|
if (this.mode.type === "dragging" && this._selectedIds.length > 0) {
|
|
5287
|
+
this.hasDragged = true;
|
|
4956
5288
|
ctx.setCursor?.("move");
|
|
4957
5289
|
const snapped = this.snap(world, ctx);
|
|
4958
5290
|
const dx = snapped.x - this.lastWorld.x;
|
|
@@ -5021,6 +5353,11 @@ var SelectTool = class {
|
|
|
5021
5353
|
}
|
|
5022
5354
|
ctx.requestRender();
|
|
5023
5355
|
}
|
|
5356
|
+
if (!this.hasDragged && this.pendingSingleSelectId !== null) {
|
|
5357
|
+
this._selectedIds = [this.pendingSingleSelectId];
|
|
5358
|
+
}
|
|
5359
|
+
this.pendingSingleSelectId = null;
|
|
5360
|
+
this.hasDragged = false;
|
|
5024
5361
|
this.mode = { type: "idle" };
|
|
5025
5362
|
ctx.setCursor?.("default");
|
|
5026
5363
|
}
|
|
@@ -6201,7 +6538,7 @@ var UpdateLayerCommand = class {
|
|
|
6201
6538
|
};
|
|
6202
6539
|
|
|
6203
6540
|
// src/index.ts
|
|
6204
|
-
var VERSION = "0.
|
|
6541
|
+
var VERSION = "0.14.0";
|
|
6205
6542
|
export {
|
|
6206
6543
|
AddElementCommand,
|
|
6207
6544
|
ArrowTool,
|
|
@@ -6212,6 +6549,7 @@ export {
|
|
|
6212
6549
|
CreateLayerCommand,
|
|
6213
6550
|
DEFAULT_FONT_SIZE_PRESETS,
|
|
6214
6551
|
DEFAULT_NOTE_FONT_SIZE,
|
|
6552
|
+
DoubleTapDetector,
|
|
6215
6553
|
ElementRenderer,
|
|
6216
6554
|
ElementStore,
|
|
6217
6555
|
EraserTool,
|
|
@@ -6220,6 +6558,7 @@ export {
|
|
|
6220
6558
|
HistoryRecorder,
|
|
6221
6559
|
HistoryStack,
|
|
6222
6560
|
ImageTool,
|
|
6561
|
+
InputFilter,
|
|
6223
6562
|
InputHandler,
|
|
6224
6563
|
LayerManager,
|
|
6225
6564
|
MeasureTool,
|
|
@@ -6265,6 +6604,7 @@ export {
|
|
|
6265
6604
|
getEdgeIntersection,
|
|
6266
6605
|
getElementBounds,
|
|
6267
6606
|
getElementCenter,
|
|
6607
|
+
getElementsBoundingBox,
|
|
6268
6608
|
getHexCellsInCone,
|
|
6269
6609
|
getHexCellsInLine,
|
|
6270
6610
|
getHexCellsInRadius,
|