canvu-react 0.3.9 → 0.3.11

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/native.js CHANGED
@@ -697,592 +697,142 @@ function skiaItemPlacementTransform(x, y, cx, cy, rotationRad) {
697
697
  result.push({ translateX: x }, { translateY: y });
698
698
  return result;
699
699
  }
700
- var HANDLE_ORDER = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
701
- function pointsToSmoothPathD(points) {
702
- if (points.length < 2) return null;
703
- const d = smoothFreehandPointsToPathD(points);
704
- return d || null;
700
+ function rgbaFromHexAndOpacity(hex, opacity) {
701
+ if (!hex) return hex;
702
+ if (opacity == null || opacity >= 1) return hex;
703
+ const r = parseInt(hex.slice(1, 3), 16);
704
+ const g = parseInt(hex.slice(3, 5), 16);
705
+ const b = parseInt(hex.slice(5, 7), 16);
706
+ if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return hex;
707
+ return `rgba(${r},${g},${b},${opacity})`;
705
708
  }
706
- function renderEraserSkeleton(item, overlayStrokePx) {
707
- const b = normalizeRect(item.bounds);
708
- const sw = Math.max(item.strokeWidth ?? 2, overlayStrokePx);
709
- const common = {
710
- color: "#cbd5e1",
711
- style: "stroke",
712
- strokeWidth: sw,
713
- strokeCap: "round",
714
- strokeJoin: "round",
715
- antiAlias: true
716
- };
717
- const k = item.toolKind;
718
- if (k === "rect") {
719
- return /* @__PURE__ */ jsx(Rect, { x: 0, y: 0, width: b.width, height: b.height, ...common });
720
- }
721
- if (k === "ellipse") {
722
- const rx = Math.max(0, b.width / 2);
723
- const ry = Math.max(0, b.height / 2);
724
- if (Math.abs(rx - ry) < 1e-9) {
725
- return /* @__PURE__ */ jsx(Circle, { cx: rx, cy: ry, r: rx, ...common });
726
- }
727
- return /* @__PURE__ */ jsx(Circle, { cx: rx, cy: ry, r: Math.min(rx, ry), ...common });
728
- }
729
- if ((k === "line" || k === "arrow") && item.line) {
730
- const ln = item.line;
731
- const geometry = k === "arrow" ? computeStraightArrowGeometry(
732
- ln,
733
- Math.max(item.strokeWidth ?? 2, overlayStrokePx)
734
- ) : null;
735
- return /* @__PURE__ */ jsxs(Fragment, { children: [
736
- /* @__PURE__ */ jsx(
737
- Line,
738
- {
739
- p1: vec(ln.x1, ln.y1),
740
- p2: vec(geometry?.shaftEndX ?? ln.x2, geometry?.shaftEndY ?? ln.y2),
741
- ...common
742
- }
743
- ),
744
- k === "arrow" && geometry ? /* @__PURE__ */ jsx(
745
- Path,
746
- {
747
- path: `M ${geometry.headLeftX} ${geometry.headLeftY} L ${geometry.headTipX} ${geometry.headTipY} L ${geometry.headRightX} ${geometry.headRightY}`,
748
- ...common
749
- }
750
- ) : null
751
- ] });
752
- }
753
- if ((k === "draw" || k === "marker" || k === "pencil" || k === "brush") && item.pathPointsLocal) {
754
- const pts = item.pathPointsLocal;
755
- if (pts.length === 1 && pts[0]) {
756
- const p = pts[0];
757
- const dotR = Math.max((item.strokeWidth ?? 2) / 2, 2);
758
- return /* @__PURE__ */ jsx(
759
- Circle,
760
- {
761
- cx: p.x,
762
- cy: p.y,
763
- r: dotR,
764
- color: "#cbd5e1",
765
- style: "fill",
766
- antiAlias: true
767
- }
768
- );
769
- }
770
- const d = pointsToSmoothPathD(pts);
771
- if (d) {
772
- return /* @__PURE__ */ jsx(Path, { path: d, ...common });
773
- }
774
- }
775
- return /* @__PURE__ */ jsx(Path, { path: `M0 0 h${b.width} v${b.height} h${-b.width} Z`, ...common });
709
+ function toNum(v) {
710
+ return typeof v === "number" ? v : Number(v) || 0;
776
711
  }
777
- function NativeInteractionOverlay({
778
- camera,
779
- width,
780
- height,
781
- selectedItems,
782
- showResizeHandles,
783
- placementPreview,
784
- eraserTrail,
785
- laserTrail,
786
- eraserPreviewItems = []
787
- }) {
788
- const z = camera.zoom;
789
- const camTransform = skiaCameraTransform(z, camera.x, camera.y);
790
- const handleR = 5 / z;
791
- const overlayStrokePx = 1.25;
792
- const rotateOffsetWorld = 24 / z;
793
- const selectionElements = useMemo(() => {
794
- if (selectedItems.length === 0) return null;
795
- const single = selectedItems.length === 1 ? selectedItems[0] : void 0;
796
- const bSingle = single ? normalizeRect(single.bounds) : null;
797
- const rotSingle = single?.rotation ?? 0;
798
- const rotHandlePos = showResizeHandles && bSingle && single ? getRotationHandleWorldPosition(bSingle, rotSingle, rotateOffsetWorld) : null;
799
- return /* @__PURE__ */ jsxs(Fragment, { children: [
800
- selectedItems.map((it) => {
801
- const b = normalizeRect(it.bounds);
802
- const cx = b.width / 2;
803
- const cy = b.height / 2;
804
- const t = skiaItemPlacementTransform(it.x, it.y, cx, cy, it.rotation ?? 0);
805
- return /* @__PURE__ */ jsx(Group, { transform: t, children: /* @__PURE__ */ jsx(
806
- Rect,
712
+ function SvgNodeRenderer({ nodes }) {
713
+ return /* @__PURE__ */ jsx(Fragment, { children: nodes.map((node, i) => /* @__PURE__ */ jsx(SvgNodeItem, { node }, i)) });
714
+ }
715
+ function SvgNodeItem({ node }) {
716
+ const isFill = node.fill !== "none";
717
+ const hasStroke = node.stroke != null && node.stroke !== "none";
718
+ switch (node.kind) {
719
+ case "rect": {
720
+ const stroke = rgbaFromHexAndOpacity(node.stroke, node.strokeOpacity);
721
+ const fill = isFill ? rgbaFromHexAndOpacity(node.fill, node.fillOpacity) ?? node.fill : void 0;
722
+ const r = node.rx != null ? toNum(node.rx) : 0;
723
+ const w = toNum(node.width);
724
+ const h = toNum(node.height);
725
+ if (r > 0) {
726
+ return /* @__PURE__ */ jsx(
727
+ RoundedRect,
807
728
  {
808
729
  x: 0,
809
730
  y: 0,
810
- width: b.width,
811
- height: b.height,
812
- color: "#3b82f6",
813
- style: "stroke",
814
- strokeWidth: overlayStrokePx,
731
+ width: w,
732
+ height: h,
733
+ r,
734
+ color: fill ?? stroke,
735
+ style: fill ? "fill" : hasStroke ? "stroke" : "fill",
736
+ strokeWidth: hasStroke ? toNum(node.strokeWidth ?? "1") : void 0,
815
737
  antiAlias: true
816
738
  }
817
- ) }, it.id);
818
- }),
819
- showResizeHandles && bSingle && single && /* @__PURE__ */ jsxs(Fragment, { children: [
820
- HANDLE_ORDER.map((hid) => {
821
- const p = getHandleWorldPositionRotated(bSingle, hid, rotSingle);
822
- return /* @__PURE__ */ jsx(
823
- Circle,
824
- {
825
- cx: p.x,
826
- cy: p.y,
827
- r: handleR,
828
- color: "#ffffff",
829
- style: "fill",
830
- antiAlias: true
831
- },
832
- hid
833
- );
834
- }),
835
- rotHandlePos && /* @__PURE__ */ jsx(
739
+ );
740
+ }
741
+ return /* @__PURE__ */ jsx(
742
+ Rect,
743
+ {
744
+ x: 0,
745
+ y: 0,
746
+ width: w,
747
+ height: h,
748
+ color: fill ?? stroke,
749
+ style: fill ? "fill" : hasStroke ? "stroke" : "fill",
750
+ strokeWidth: hasStroke ? toNum(node.strokeWidth ?? "1") : void 0,
751
+ antiAlias: true
752
+ }
753
+ );
754
+ }
755
+ case "ellipse": {
756
+ const stroke = rgbaFromHexAndOpacity(node.stroke, node.strokeOpacity);
757
+ const fill = isFill ? rgbaFromHexAndOpacity(node.fill, node.fillOpacity) ?? node.fill : void 0;
758
+ const cx = toNum(node.cx);
759
+ const cy = toNum(node.cy);
760
+ const rx = toNum(node.rx);
761
+ const ry = toNum(node.ry);
762
+ if (Math.abs(rx - ry) < 1e-6) {
763
+ return /* @__PURE__ */ jsx(
836
764
  Circle,
837
765
  {
838
- cx: rotHandlePos.x,
839
- cy: rotHandlePos.y,
840
- r: handleR * 1.5,
841
- color: "#3b82f6",
842
- style: "stroke",
843
- strokeWidth: overlayStrokePx * 1.2,
766
+ cx,
767
+ cy,
768
+ r: rx,
769
+ color: fill ?? stroke,
770
+ style: fill ? "fill" : hasStroke ? "stroke" : "fill",
771
+ strokeWidth: hasStroke ? toNum(node.strokeWidth ?? "1") : void 0,
844
772
  antiAlias: true
845
773
  }
846
- )
847
- ] })
848
- ] });
849
- }, [selectedItems, showResizeHandles, rotateOffsetWorld, handleR]);
850
- const previewElements = useMemo(() => {
851
- if (!placementPreview) return null;
852
- const p = placementPreview;
853
- if (p.kind === "rect" || p.kind === "ellipse") {
854
- const r = normalizeRect(p.rect);
855
- return p.kind === "rect" ? /* @__PURE__ */ jsx(
856
- Rect,
774
+ );
775
+ }
776
+ return /* @__PURE__ */ jsx(
777
+ Oval,
857
778
  {
858
- x: r.x,
859
- y: r.y,
860
- width: r.width,
861
- height: r.height,
862
- color: "#64748b",
863
- style: "stroke",
864
- strokeWidth: overlayStrokePx,
779
+ x: cx - rx,
780
+ y: cy - ry,
781
+ width: rx * 2,
782
+ height: ry * 2,
783
+ color: fill ?? stroke,
784
+ style: fill ? "fill" : hasStroke ? "stroke" : "fill",
785
+ strokeWidth: hasStroke ? toNum(node.strokeWidth ?? "1") : void 0,
865
786
  antiAlias: true
866
787
  }
867
- ) : /* @__PURE__ */ jsx(
788
+ );
789
+ }
790
+ case "circle": {
791
+ const fill = rgbaFromHexAndOpacity(node.fill, node.fillOpacity) ?? node.fill;
792
+ const isCircleFill = fill && fill !== "none";
793
+ const strokeColor = rgbaFromHexAndOpacity(node.stroke, node.strokeOpacity);
794
+ return /* @__PURE__ */ jsx(
868
795
  Circle,
869
796
  {
870
- cx: r.x + r.width / 2,
871
- cy: r.y + r.height / 2,
872
- r: Math.max(0, r.width / 2),
873
- color: "#64748b",
797
+ cx: toNum(node.cx),
798
+ cy: toNum(node.cy),
799
+ r: toNum(node.r),
800
+ color: isCircleFill ? fill : strokeColor ?? "black",
801
+ style: isCircleFill ? "fill" : "stroke",
802
+ strokeWidth: !isCircleFill && hasStroke ? toNum(node.strokeWidth ?? "1") : void 0,
803
+ antiAlias: true
804
+ }
805
+ );
806
+ }
807
+ case "line": {
808
+ const stroke = rgbaFromHexAndOpacity(node.stroke, node.strokeOpacity);
809
+ return /* @__PURE__ */ jsx(
810
+ Line,
811
+ {
812
+ p1: vec(toNum(node.x1), toNum(node.y1)),
813
+ p2: vec(toNum(node.x2), toNum(node.y2)),
814
+ color: stroke ?? "black",
874
815
  style: "stroke",
875
- strokeWidth: overlayStrokePx,
816
+ strokeWidth: toNum(node.strokeWidth ?? "1"),
817
+ strokeCap: node.strokeLinecap === "round" ? "round" : "butt",
876
818
  antiAlias: true
877
819
  }
878
820
  );
879
821
  }
880
- if (p.kind === "marquee") {
881
- const r = normalizeRect(p.rect);
822
+ case "path": {
823
+ const stroke = rgbaFromHexAndOpacity(node.stroke, node.strokeOpacity);
824
+ const fill = isFill ? rgbaFromHexAndOpacity(node.fill, node.fillOpacity) ?? node.fill : void 0;
825
+ const style = fill && fill !== "none" ? "fill" : "stroke";
882
826
  return /* @__PURE__ */ jsx(
883
- Rect,
827
+ Path,
884
828
  {
885
- x: r.x,
886
- y: r.y,
887
- width: r.width,
888
- height: r.height,
889
- color: "rgba(59, 130, 246, 0.12)",
890
- style: "fill",
891
- antiAlias: true
892
- }
893
- );
894
- }
895
- if (p.kind === "line" || p.kind === "arrow") {
896
- const geometry = p.kind === "arrow" ? computeStraightArrowGeometry(
897
- { x1: p.start.x, y1: p.start.y, x2: p.end.x, y2: p.end.y },
898
- overlayStrokePx
899
- ) : null;
900
- return /* @__PURE__ */ jsxs(Fragment, { children: [
901
- /* @__PURE__ */ jsx(
902
- Line,
903
- {
904
- p1: vec(p.start.x, p.start.y),
905
- p2: vec(geometry?.shaftEndX ?? p.end.x, geometry?.shaftEndY ?? p.end.y),
906
- color: "#64748b",
907
- style: "stroke",
908
- strokeWidth: overlayStrokePx,
909
- strokeCap: "round",
910
- antiAlias: true
911
- }
912
- ),
913
- p.kind === "arrow" && geometry ? /* @__PURE__ */ jsx(
914
- Path,
915
- {
916
- path: `M ${geometry.headLeftX} ${geometry.headLeftY} L ${geometry.headTipX} ${geometry.headTipY} L ${geometry.headRightX} ${geometry.headRightY}`,
917
- color: "#64748b",
918
- style: "stroke",
919
- strokeWidth: overlayStrokePx,
920
- strokeCap: "round",
921
- strokeJoin: "round",
922
- antiAlias: true
923
- }
924
- ) : null
925
- ] });
926
- }
927
- if (p.kind === "stroke" && p.points.length >= 1) {
928
- const payload = computeFreehandSvgPayload(
929
- p.points,
930
- { stroke: "#64748b", strokeWidth: 3 },
931
- "draw",
932
- false
933
- );
934
- if (!payload) return null;
935
- if (payload.kind === "circle") {
936
- return /* @__PURE__ */ jsx(
937
- Circle,
938
- {
939
- cx: payload.cx,
940
- cy: payload.cy,
941
- r: payload.r,
942
- color: payload.fill,
943
- style: "fill",
944
- antiAlias: true
945
- }
946
- );
947
- }
948
- if (payload.kind === "fillPath") {
949
- return /* @__PURE__ */ jsx(Path, { path: payload.d, color: payload.fill, style: "fill", antiAlias: true });
950
- }
951
- if (payload.kind === "strokePath") {
952
- return /* @__PURE__ */ jsx(
953
- Path,
954
- {
955
- path: payload.d,
956
- color: "#64748b",
957
- style: "stroke",
958
- strokeWidth: 3,
959
- strokeCap: "round",
960
- strokeJoin: "round",
961
- antiAlias: true
962
- }
963
- );
964
- }
965
- return null;
966
- }
967
- return null;
968
- }, [placementPreview]);
969
- const eraserPreviewElements = useMemo(() => {
970
- if (eraserPreviewItems.length === 0) return null;
971
- return /* @__PURE__ */ jsx(Fragment, { children: eraserPreviewItems.map((it) => {
972
- const b = normalizeRect(it.bounds);
973
- const cx = b.width / 2;
974
- const cy = b.height / 2;
975
- const t = skiaItemPlacementTransform(it.x, it.y, cx, cy, it.rotation ?? 0);
976
- return /* @__PURE__ */ jsx(Group, { transform: t, children: renderEraserSkeleton(it, overlayStrokePx) }, `erase-${it.id}`);
977
- }) });
978
- }, [eraserPreviewItems]);
979
- const eraserTrailElements = useMemo(() => {
980
- if (!eraserTrail || eraserTrail.length < 1) return null;
981
- const d = pointsToSmoothPathD(eraserTrail);
982
- if (!d)
983
- return eraserTrail[0] ? /* @__PURE__ */ jsx(
984
- Circle,
985
- {
986
- cx: eraserTrail[0].x,
987
- cy: eraserTrail[0].y,
988
- r: Math.max(5 / z, 3),
989
- color: "#cbd5e1",
990
- style: "fill",
991
- antiAlias: true
992
- }
993
- ) : null;
994
- return /* @__PURE__ */ jsx(
995
- Path,
996
- {
997
- path: d,
998
- color: "#cbd5e1",
999
- style: "stroke",
1000
- strokeWidth: Math.max(3.5 / z, overlayStrokePx),
1001
- strokeCap: "round",
1002
- strokeJoin: "round",
1003
- antiAlias: true
1004
- }
1005
- );
1006
- }, [eraserTrail, z]);
1007
- const laserTrailElements = useMemo(() => {
1008
- if (!laserTrail || laserTrail.length < 1) return null;
1009
- const d = pointsToSmoothPathD(laserTrail);
1010
- if (!d)
1011
- return laserTrail[0] ? /* @__PURE__ */ jsx(
1012
- Circle,
1013
- {
1014
- cx: laserTrail[0].x,
1015
- cy: laserTrail[0].y,
1016
- r: Math.max(5 / z, 3),
1017
- color: "#f43f5e",
1018
- style: "fill",
1019
- antiAlias: true
1020
- }
1021
- ) : null;
1022
- return /* @__PURE__ */ jsx(
1023
- Path,
1024
- {
1025
- path: d,
1026
- color: "#f43f5e",
1027
- style: "stroke",
1028
- strokeWidth: Math.max(4 / z, overlayStrokePx),
1029
- strokeCap: "round",
1030
- strokeJoin: "round",
1031
- antiAlias: true
1032
- }
1033
- );
1034
- }, [laserTrail, z]);
1035
- if (width <= 0 || height <= 0) return null;
1036
- return /* @__PURE__ */ jsx(
1037
- Canvas,
1038
- {
1039
- style: {
1040
- position: "absolute",
1041
- top: 0,
1042
- left: 0,
1043
- width,
1044
- height
1045
- },
1046
- pointerEvents: "none",
1047
- children: /* @__PURE__ */ jsxs(Group, { transform: camTransform, children: [
1048
- previewElements,
1049
- laserTrailElements,
1050
- eraserTrailElements,
1051
- eraserPreviewElements,
1052
- selectionElements
1053
- ] })
1054
- }
1055
- );
1056
- }
1057
-
1058
- // src/scene/spatial-cull.ts
1059
- var spatialIndexCache = /* @__PURE__ */ new WeakMap();
1060
- function cellKey(ix, iy) {
1061
- return `${ix},${iy}`;
1062
- }
1063
- function buildSpatialIndex(items, cellSize) {
1064
- const cached = spatialIndexCache.get(items);
1065
- if (cached && cached.cellSize === cellSize) {
1066
- return cached;
1067
- }
1068
- const aabbs = new Array(items.length);
1069
- const buckets = /* @__PURE__ */ new Map();
1070
- for (let index = 0; index < items.length; index += 1) {
1071
- const item = items[index];
1072
- if (!item) continue;
1073
- const aabb = boundsAabbForRotatedItem(item);
1074
- aabbs[index] = aabb;
1075
- const { minIx, maxIx, minIy, maxIy } = cellRangeForRect(aabb, cellSize);
1076
- for (let ix = minIx; ix <= maxIx; ix += 1) {
1077
- for (let iy = minIy; iy <= maxIy; iy += 1) {
1078
- const key = cellKey(ix, iy);
1079
- let bucket = buckets.get(key);
1080
- if (!bucket) {
1081
- bucket = [];
1082
- buckets.set(key, bucket);
1083
- }
1084
- bucket.push(index);
1085
- }
1086
- }
1087
- }
1088
- const next = {
1089
- cellSize,
1090
- aabbs,
1091
- buckets
1092
- };
1093
- spatialIndexCache.set(items, next);
1094
- return next;
1095
- }
1096
- function cellRangeForRect(r, cellSize) {
1097
- const n = normalizeRect(r);
1098
- const x1 = n.x + n.width;
1099
- const y1 = n.y + n.height;
1100
- const minIx = Math.floor(n.x / cellSize);
1101
- const maxIx = Math.max(minIx, Math.ceil(x1 / cellSize) - 1);
1102
- const minIy = Math.floor(n.y / cellSize);
1103
- const maxIy = Math.max(minIy, Math.ceil(y1 / cellSize) - 1);
1104
- return { minIx, maxIx, minIy, maxIy };
1105
- }
1106
- function cullItemsByViewportSpatial(items, visibleWorld, cellSize) {
1107
- const { aabbs, buckets } = buildSpatialIndex(items, cellSize);
1108
- const vr = cellRangeForRect(visibleWorld, cellSize);
1109
- const seen = /* @__PURE__ */ new Set();
1110
- const outIndices = [];
1111
- for (let ix = vr.minIx; ix <= vr.maxIx; ix++) {
1112
- for (let iy = vr.minIy; iy <= vr.maxIy; iy++) {
1113
- const bucket = buckets.get(cellKey(ix, iy));
1114
- if (!bucket) {
1115
- continue;
1116
- }
1117
- for (const index of bucket) {
1118
- if (seen.has(index)) {
1119
- continue;
1120
- }
1121
- seen.add(index);
1122
- const aabb = aabbs[index];
1123
- if (aabb && rectsIntersect(aabb, visibleWorld)) {
1124
- outIndices.push(index);
1125
- }
1126
- }
1127
- }
1128
- }
1129
- outIndices.sort((a, b) => a - b);
1130
- return outIndices.map((index) => items[index]).filter((item) => item != null);
1131
- }
1132
-
1133
- // src/scene/cull.ts
1134
- var SPATIAL_MIN_ITEMS = 400;
1135
- var SPATIAL_CELL_SIZE = 256;
1136
- function cullItemsLinear(items, visibleWorld) {
1137
- return items.filter(
1138
- (item) => rectsIntersect(boundsAabbForCull(item), visibleWorld)
1139
- );
1140
- }
1141
- function boundsAabbForCull(item) {
1142
- return boundsAabbForRotatedItem(item);
1143
- }
1144
- function cullItemsByViewport(items, visibleWorld) {
1145
- if (items.length < SPATIAL_MIN_ITEMS) {
1146
- return cullItemsLinear(items, visibleWorld);
1147
- }
1148
- return cullItemsByViewportSpatial(items, visibleWorld, SPATIAL_CELL_SIZE);
1149
- }
1150
- function rgbaFromHexAndOpacity(hex, opacity) {
1151
- if (!hex) return hex;
1152
- if (opacity == null || opacity >= 1) return hex;
1153
- const r = parseInt(hex.slice(1, 3), 16);
1154
- const g = parseInt(hex.slice(3, 5), 16);
1155
- const b = parseInt(hex.slice(5, 7), 16);
1156
- if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return hex;
1157
- return `rgba(${r},${g},${b},${opacity})`;
1158
- }
1159
- function toNum(v) {
1160
- return typeof v === "number" ? v : Number(v) || 0;
1161
- }
1162
- function SvgNodeRenderer({ nodes }) {
1163
- return /* @__PURE__ */ jsx(Fragment, { children: nodes.map((node, i) => /* @__PURE__ */ jsx(SvgNodeItem, { node }, i)) });
1164
- }
1165
- function SvgNodeItem({ node }) {
1166
- const isFill = node.fill !== "none";
1167
- const hasStroke = node.stroke != null && node.stroke !== "none";
1168
- switch (node.kind) {
1169
- case "rect": {
1170
- const stroke = rgbaFromHexAndOpacity(node.stroke, node.strokeOpacity);
1171
- const fill = isFill ? rgbaFromHexAndOpacity(node.fill, node.fillOpacity) ?? node.fill : void 0;
1172
- const r = node.rx != null ? toNum(node.rx) : 0;
1173
- const w = toNum(node.width);
1174
- const h = toNum(node.height);
1175
- if (r > 0) {
1176
- return /* @__PURE__ */ jsx(
1177
- RoundedRect,
1178
- {
1179
- x: 0,
1180
- y: 0,
1181
- width: w,
1182
- height: h,
1183
- r,
1184
- color: fill ?? stroke,
1185
- style: fill ? "fill" : hasStroke ? "stroke" : "fill",
1186
- strokeWidth: hasStroke ? toNum(node.strokeWidth ?? "1") : void 0,
1187
- antiAlias: true
1188
- }
1189
- );
1190
- }
1191
- return /* @__PURE__ */ jsx(
1192
- Rect,
1193
- {
1194
- x: 0,
1195
- y: 0,
1196
- width: w,
1197
- height: h,
1198
- color: fill ?? stroke,
1199
- style: fill ? "fill" : hasStroke ? "stroke" : "fill",
1200
- strokeWidth: hasStroke ? toNum(node.strokeWidth ?? "1") : void 0,
1201
- antiAlias: true
1202
- }
1203
- );
1204
- }
1205
- case "ellipse": {
1206
- const stroke = rgbaFromHexAndOpacity(node.stroke, node.strokeOpacity);
1207
- const fill = isFill ? rgbaFromHexAndOpacity(node.fill, node.fillOpacity) ?? node.fill : void 0;
1208
- const cx = toNum(node.cx);
1209
- const cy = toNum(node.cy);
1210
- const rx = toNum(node.rx);
1211
- const ry = toNum(node.ry);
1212
- if (Math.abs(rx - ry) < 1e-6) {
1213
- return /* @__PURE__ */ jsx(
1214
- Circle,
1215
- {
1216
- cx,
1217
- cy,
1218
- r: rx,
1219
- color: fill ?? stroke,
1220
- style: fill ? "fill" : hasStroke ? "stroke" : "fill",
1221
- strokeWidth: hasStroke ? toNum(node.strokeWidth ?? "1") : void 0,
1222
- antiAlias: true
1223
- }
1224
- );
1225
- }
1226
- return /* @__PURE__ */ jsx(
1227
- Oval,
1228
- {
1229
- x: cx - rx,
1230
- y: cy - ry,
1231
- width: rx * 2,
1232
- height: ry * 2,
1233
- color: fill ?? stroke,
1234
- style: fill ? "fill" : hasStroke ? "stroke" : "fill",
1235
- strokeWidth: hasStroke ? toNum(node.strokeWidth ?? "1") : void 0,
1236
- antiAlias: true
1237
- }
1238
- );
1239
- }
1240
- case "circle": {
1241
- const fill = rgbaFromHexAndOpacity(node.fill, node.fillOpacity) ?? node.fill;
1242
- const isCircleFill = fill && fill !== "none";
1243
- const strokeColor = rgbaFromHexAndOpacity(node.stroke, node.strokeOpacity);
1244
- return /* @__PURE__ */ jsx(
1245
- Circle,
1246
- {
1247
- cx: toNum(node.cx),
1248
- cy: toNum(node.cy),
1249
- r: toNum(node.r),
1250
- color: isCircleFill ? fill : strokeColor ?? "black",
1251
- style: isCircleFill ? "fill" : "stroke",
1252
- strokeWidth: !isCircleFill && hasStroke ? toNum(node.strokeWidth ?? "1") : void 0,
1253
- antiAlias: true
1254
- }
1255
- );
1256
- }
1257
- case "line": {
1258
- const stroke = rgbaFromHexAndOpacity(node.stroke, node.strokeOpacity);
1259
- return /* @__PURE__ */ jsx(
1260
- Line,
1261
- {
1262
- p1: vec(toNum(node.x1), toNum(node.y1)),
1263
- p2: vec(toNum(node.x2), toNum(node.y2)),
1264
- color: stroke ?? "black",
1265
- style: "stroke",
1266
- strokeWidth: toNum(node.strokeWidth ?? "1"),
1267
- strokeCap: node.strokeLinecap === "round" ? "round" : "butt",
1268
- antiAlias: true
1269
- }
1270
- );
1271
- }
1272
- case "path": {
1273
- const stroke = rgbaFromHexAndOpacity(node.stroke, node.strokeOpacity);
1274
- const fill = isFill ? rgbaFromHexAndOpacity(node.fill, node.fillOpacity) ?? node.fill : void 0;
1275
- const style = fill && fill !== "none" ? "fill" : "stroke";
1276
- return /* @__PURE__ */ jsx(
1277
- Path,
1278
- {
1279
- path: node.d,
1280
- color: style === "fill" ? fill ?? stroke : stroke ?? "black",
1281
- style,
1282
- strokeWidth: style === "stroke" ? toNum(node.strokeWidth ?? "1") : void 0,
1283
- strokeCap: node.strokeLinecap === "round" ? "round" : "butt",
1284
- strokeJoin: node.strokeLinejoin === "round" ? "round" : "miter",
1285
- fillType: node.fillRule === "evenodd" ? "evenOdd" : "winding",
829
+ path: node.d,
830
+ color: style === "fill" ? fill ?? stroke : stroke ?? "black",
831
+ style,
832
+ strokeWidth: style === "stroke" ? toNum(node.strokeWidth ?? "1") : void 0,
833
+ strokeCap: node.strokeLinecap === "round" ? "round" : "butt",
834
+ strokeJoin: node.strokeLinejoin === "round" ? "round" : "miter",
835
+ fillType: node.fillRule === "evenodd" ? "evenOdd" : "winding",
1286
836
  antiAlias: true
1287
837
  }
1288
838
  );
@@ -1628,188 +1178,576 @@ function NativeShapeRenderer({ item }) {
1628
1178
  const ry = Math.max(0, h / 2);
1629
1179
  if (Math.abs(rx - ry) < 1e-9) {
1630
1180
  return /* @__PURE__ */ jsx(
1631
- Circle,
1181
+ Circle,
1182
+ {
1183
+ cx: rx,
1184
+ cy: ry,
1185
+ r: rx,
1186
+ color: style.stroke,
1187
+ style: "stroke",
1188
+ strokeWidth: style.strokeWidth,
1189
+ antiAlias: true
1190
+ }
1191
+ );
1192
+ }
1193
+ return /* @__PURE__ */ jsx(
1194
+ Circle,
1195
+ {
1196
+ cx: rx,
1197
+ cy: ry,
1198
+ r: Math.min(rx, ry),
1199
+ color: style.stroke,
1200
+ style: "stroke",
1201
+ strokeWidth: style.strokeWidth,
1202
+ antiAlias: true
1203
+ }
1204
+ );
1205
+ }
1206
+ if ((k === "line" || k === "arrow") && item.line) {
1207
+ const ln = item.line;
1208
+ const color = rgba(style.stroke, style.strokeOpacity);
1209
+ if (k === "line") {
1210
+ return /* @__PURE__ */ jsx(
1211
+ Line,
1212
+ {
1213
+ p1: vec(ln.x1, ln.y1),
1214
+ p2: vec(ln.x2, ln.y2),
1215
+ color,
1216
+ style: "stroke",
1217
+ strokeWidth: style.strokeWidth,
1218
+ strokeCap: "round",
1219
+ antiAlias: true
1220
+ }
1221
+ );
1222
+ }
1223
+ const dx = ln.x2 - ln.x1;
1224
+ const dy = ln.y2 - ln.y1;
1225
+ const len = Math.hypot(dx, dy);
1226
+ if (len < 1e-6) {
1227
+ return /* @__PURE__ */ jsx(
1228
+ Line,
1229
+ {
1230
+ p1: vec(ln.x1, ln.y1),
1231
+ p2: vec(ln.x2, ln.y2),
1232
+ color,
1233
+ style: "stroke",
1234
+ strokeWidth: style.strokeWidth,
1235
+ strokeCap: "round",
1236
+ antiAlias: true
1237
+ }
1238
+ );
1239
+ }
1240
+ const geometry = computeStraightArrowGeometry(ln, style.strokeWidth);
1241
+ if (!geometry) {
1242
+ return /* @__PURE__ */ jsx(
1243
+ Line,
1244
+ {
1245
+ p1: vec(ln.x1, ln.y1),
1246
+ p2: vec(ln.x2, ln.y2),
1247
+ color,
1248
+ style: "stroke",
1249
+ strokeWidth: style.strokeWidth,
1250
+ strokeCap: "round",
1251
+ antiAlias: true
1252
+ }
1253
+ );
1254
+ }
1255
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1256
+ /* @__PURE__ */ jsx(
1257
+ Line,
1258
+ {
1259
+ p1: vec(ln.x1, ln.y1),
1260
+ p2: vec(geometry.shaftEndX, geometry.shaftEndY),
1261
+ color,
1262
+ style: "stroke",
1263
+ strokeWidth: style.strokeWidth,
1264
+ strokeCap: "round",
1265
+ antiAlias: true
1266
+ }
1267
+ ),
1268
+ /* @__PURE__ */ jsx(
1269
+ Path,
1270
+ {
1271
+ path: `M ${geometry.headLeftX} ${geometry.headLeftY} L ${geometry.headTipX} ${geometry.headTipY} L ${geometry.headRightX} ${geometry.headRightY}`,
1272
+ color,
1273
+ style: "stroke",
1274
+ strokeWidth: style.strokeWidth,
1275
+ strokeCap: "round",
1276
+ strokeJoin: "round",
1277
+ antiAlias: true
1278
+ }
1279
+ )
1280
+ ] });
1281
+ }
1282
+ if (k === "text" && item.text !== void 0) {
1283
+ const fs = item.textFontSize ?? 16;
1284
+ const color = rgba(style.stroke, style.strokeOpacity);
1285
+ const lines = item.text.split("\n");
1286
+ const font = matchFont({ fontSize: fs });
1287
+ return /* @__PURE__ */ jsx(Fragment, { children: lines.map((line, i) => /* @__PURE__ */ jsx(
1288
+ Text,
1289
+ {
1290
+ x: 0,
1291
+ y: fs + i * fs * 1.2,
1292
+ text: line,
1293
+ color,
1294
+ font
1295
+ },
1296
+ i
1297
+ )) });
1298
+ }
1299
+ if ((k === "draw" || k === "pencil" || k === "brush" || k === "marker") && item.pathPointsLocal && item.pathPointsLocal.length > 0) {
1300
+ const payload = computeFreehandSvgPayload(item.pathPointsLocal, style, k);
1301
+ if (!payload) return null;
1302
+ const color = rgba(style.stroke, style.strokeOpacity);
1303
+ if (payload.kind === "circle") {
1304
+ return /* @__PURE__ */ jsx(
1305
+ Circle,
1306
+ {
1307
+ cx: payload.cx,
1308
+ cy: payload.cy,
1309
+ r: payload.r,
1310
+ color: rgba(payload.fill, payload.fillOpacity),
1311
+ style: "fill",
1312
+ antiAlias: true
1313
+ }
1314
+ );
1315
+ }
1316
+ if (payload.kind === "fillPath") {
1317
+ return /* @__PURE__ */ jsx(
1318
+ Path,
1632
1319
  {
1633
- cx: rx,
1634
- cy: ry,
1635
- r: rx,
1636
- color: style.stroke,
1637
- style: "stroke",
1638
- strokeWidth: style.strokeWidth,
1320
+ path: payload.d,
1321
+ color: rgba(payload.fill, payload.fillOpacity),
1322
+ style: "fill",
1323
+ fillType: "winding",
1639
1324
  antiAlias: true
1640
1325
  }
1641
1326
  );
1642
1327
  }
1643
1328
  return /* @__PURE__ */ jsx(
1644
- Circle,
1329
+ Path,
1645
1330
  {
1646
- cx: rx,
1647
- cy: ry,
1648
- r: Math.min(rx, ry),
1649
- color: style.stroke,
1331
+ path: payload.d,
1332
+ color,
1650
1333
  style: "stroke",
1651
- strokeWidth: style.strokeWidth,
1334
+ strokeWidth: payload.strokeWidth,
1335
+ strokeCap: "round",
1336
+ strokeJoin: "round",
1652
1337
  antiAlias: true
1653
1338
  }
1654
1339
  );
1655
1340
  }
1656
- if ((k === "line" || k === "arrow") && item.line) {
1657
- const ln = item.line;
1658
- const color = rgba(style.stroke, style.strokeOpacity);
1659
- if (k === "line") {
1660
- return /* @__PURE__ */ jsx(
1661
- Line,
1341
+ if (k === "draw") {
1342
+ const { w, h } = localBounds(item.bounds);
1343
+ const r = Math.min(w, h) / 2;
1344
+ return /* @__PURE__ */ jsx(
1345
+ Circle,
1346
+ {
1347
+ cx: Math.max(0, w) / 2,
1348
+ cy: Math.max(0, h) / 2,
1349
+ r: Math.max(0, r),
1350
+ color: rgba(style.stroke, style.strokeOpacity),
1351
+ style: "fill",
1352
+ antiAlias: true
1353
+ }
1354
+ );
1355
+ }
1356
+ if (k === "image" || k === "custom" || item.childrenSvg) {
1357
+ const nodes = parseSvgFragment(item.childrenSvg);
1358
+ if (nodes.length > 0) {
1359
+ return /* @__PURE__ */ jsx(SvgNodeRenderer, { nodes });
1360
+ }
1361
+ }
1362
+ return null;
1363
+ }
1364
+ var HANDLE_ORDER = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
1365
+ var ERASER_PREVIEW_OPACITY = 0.3;
1366
+ function pointsToSmoothPathD(points) {
1367
+ if (points.length < 2) return null;
1368
+ const d = smoothFreehandPointsToPathD(points);
1369
+ return d || null;
1370
+ }
1371
+ function NativeInteractionOverlay({
1372
+ camera,
1373
+ width,
1374
+ height,
1375
+ selectedItems,
1376
+ showResizeHandles,
1377
+ placementPreview,
1378
+ eraserTrail,
1379
+ laserTrail,
1380
+ eraserPreviewItems = []
1381
+ }) {
1382
+ const z = camera.zoom;
1383
+ const camTransform = skiaCameraTransform(z, camera.x, camera.y);
1384
+ const handleR = 5 / z;
1385
+ const overlayStrokePx = 1.25;
1386
+ const rotateOffsetWorld = 24 / z;
1387
+ const selectionElements = useMemo(() => {
1388
+ if (selectedItems.length === 0) return null;
1389
+ const single = selectedItems.length === 1 ? selectedItems[0] : void 0;
1390
+ const bSingle = single ? normalizeRect(single.bounds) : null;
1391
+ const rotSingle = single?.rotation ?? 0;
1392
+ const rotHandlePos = showResizeHandles && bSingle && single ? getRotationHandleWorldPosition(bSingle, rotSingle, rotateOffsetWorld) : null;
1393
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1394
+ selectedItems.map((it) => {
1395
+ const b = normalizeRect(it.bounds);
1396
+ const cx = b.width / 2;
1397
+ const cy = b.height / 2;
1398
+ const t = skiaItemPlacementTransform(it.x, it.y, cx, cy, it.rotation ?? 0);
1399
+ return /* @__PURE__ */ jsx(Group, { transform: t, children: /* @__PURE__ */ jsx(
1400
+ Rect,
1401
+ {
1402
+ x: 0,
1403
+ y: 0,
1404
+ width: b.width,
1405
+ height: b.height,
1406
+ color: "#3b82f6",
1407
+ style: "stroke",
1408
+ strokeWidth: overlayStrokePx,
1409
+ antiAlias: true
1410
+ }
1411
+ ) }, it.id);
1412
+ }),
1413
+ showResizeHandles && bSingle && single && /* @__PURE__ */ jsxs(Fragment, { children: [
1414
+ HANDLE_ORDER.map((hid) => {
1415
+ const p = getHandleWorldPositionRotated(bSingle, hid, rotSingle);
1416
+ return /* @__PURE__ */ jsx(
1417
+ Circle,
1418
+ {
1419
+ cx: p.x,
1420
+ cy: p.y,
1421
+ r: handleR,
1422
+ color: "#ffffff",
1423
+ style: "fill",
1424
+ antiAlias: true
1425
+ },
1426
+ hid
1427
+ );
1428
+ }),
1429
+ rotHandlePos && /* @__PURE__ */ jsx(
1430
+ Circle,
1431
+ {
1432
+ cx: rotHandlePos.x,
1433
+ cy: rotHandlePos.y,
1434
+ r: handleR * 1.5,
1435
+ color: "#3b82f6",
1436
+ style: "stroke",
1437
+ strokeWidth: overlayStrokePx * 1.2,
1438
+ antiAlias: true
1439
+ }
1440
+ )
1441
+ ] })
1442
+ ] });
1443
+ }, [selectedItems, showResizeHandles, rotateOffsetWorld, handleR]);
1444
+ const previewElements = useMemo(() => {
1445
+ if (!placementPreview) return null;
1446
+ const p = placementPreview;
1447
+ if (p.kind === "rect" || p.kind === "ellipse") {
1448
+ const r = normalizeRect(p.rect);
1449
+ return p.kind === "rect" ? /* @__PURE__ */ jsx(
1450
+ Rect,
1662
1451
  {
1663
- p1: vec(ln.x1, ln.y1),
1664
- p2: vec(ln.x2, ln.y2),
1665
- color,
1452
+ x: r.x,
1453
+ y: r.y,
1454
+ width: r.width,
1455
+ height: r.height,
1456
+ color: "#64748b",
1666
1457
  style: "stroke",
1667
- strokeWidth: style.strokeWidth,
1668
- strokeCap: "round",
1458
+ strokeWidth: overlayStrokePx,
1459
+ antiAlias: true
1460
+ }
1461
+ ) : /* @__PURE__ */ jsx(
1462
+ Circle,
1463
+ {
1464
+ cx: r.x + r.width / 2,
1465
+ cy: r.y + r.height / 2,
1466
+ r: Math.max(0, r.width / 2),
1467
+ color: "#64748b",
1468
+ style: "stroke",
1469
+ strokeWidth: overlayStrokePx,
1669
1470
  antiAlias: true
1670
1471
  }
1671
1472
  );
1672
1473
  }
1673
- const dx = ln.x2 - ln.x1;
1674
- const dy = ln.y2 - ln.y1;
1675
- const len = Math.hypot(dx, dy);
1676
- if (len < 1e-6) {
1474
+ if (p.kind === "marquee") {
1475
+ const r = normalizeRect(p.rect);
1677
1476
  return /* @__PURE__ */ jsx(
1678
- Line,
1477
+ Rect,
1679
1478
  {
1680
- p1: vec(ln.x1, ln.y1),
1681
- p2: vec(ln.x2, ln.y2),
1682
- color,
1683
- style: "stroke",
1684
- strokeWidth: style.strokeWidth,
1685
- strokeCap: "round",
1479
+ x: r.x,
1480
+ y: r.y,
1481
+ width: r.width,
1482
+ height: r.height,
1483
+ color: "rgba(59, 130, 246, 0.12)",
1484
+ style: "fill",
1686
1485
  antiAlias: true
1687
1486
  }
1688
1487
  );
1689
1488
  }
1690
- const geometry = computeStraightArrowGeometry(ln, style.strokeWidth);
1691
- if (!geometry) {
1489
+ if (p.kind === "line" || p.kind === "arrow") {
1490
+ const geometry = p.kind === "arrow" ? computeStraightArrowGeometry(
1491
+ { x1: p.start.x, y1: p.start.y, x2: p.end.x, y2: p.end.y },
1492
+ overlayStrokePx
1493
+ ) : null;
1494
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1495
+ /* @__PURE__ */ jsx(
1496
+ Line,
1497
+ {
1498
+ p1: vec(p.start.x, p.start.y),
1499
+ p2: vec(geometry?.shaftEndX ?? p.end.x, geometry?.shaftEndY ?? p.end.y),
1500
+ color: "#64748b",
1501
+ style: "stroke",
1502
+ strokeWidth: overlayStrokePx,
1503
+ strokeCap: "round",
1504
+ antiAlias: true
1505
+ }
1506
+ ),
1507
+ p.kind === "arrow" && geometry ? /* @__PURE__ */ jsx(
1508
+ Path,
1509
+ {
1510
+ path: `M ${geometry.headLeftX} ${geometry.headLeftY} L ${geometry.headTipX} ${geometry.headTipY} L ${geometry.headRightX} ${geometry.headRightY}`,
1511
+ color: "#64748b",
1512
+ style: "stroke",
1513
+ strokeWidth: overlayStrokePx,
1514
+ strokeCap: "round",
1515
+ strokeJoin: "round",
1516
+ antiAlias: true
1517
+ }
1518
+ ) : null
1519
+ ] });
1520
+ }
1521
+ if (p.kind === "stroke" && p.points.length >= 1) {
1522
+ const payload = computeFreehandSvgPayload(
1523
+ p.points,
1524
+ { stroke: "#64748b", strokeWidth: 3 },
1525
+ "draw",
1526
+ false
1527
+ );
1528
+ if (!payload) return null;
1529
+ if (payload.kind === "circle") {
1530
+ return /* @__PURE__ */ jsx(
1531
+ Circle,
1532
+ {
1533
+ cx: payload.cx,
1534
+ cy: payload.cy,
1535
+ r: payload.r,
1536
+ color: payload.fill,
1537
+ style: "fill",
1538
+ antiAlias: true
1539
+ }
1540
+ );
1541
+ }
1542
+ if (payload.kind === "fillPath") {
1543
+ return /* @__PURE__ */ jsx(Path, { path: payload.d, color: payload.fill, style: "fill", antiAlias: true });
1544
+ }
1545
+ if (payload.kind === "strokePath") {
1546
+ return /* @__PURE__ */ jsx(
1547
+ Path,
1548
+ {
1549
+ path: payload.d,
1550
+ color: "#64748b",
1551
+ style: "stroke",
1552
+ strokeWidth: 3,
1553
+ strokeCap: "round",
1554
+ strokeJoin: "round",
1555
+ antiAlias: true
1556
+ }
1557
+ );
1558
+ }
1559
+ return null;
1560
+ }
1561
+ return null;
1562
+ }, [placementPreview]);
1563
+ const eraserPreviewElements = useMemo(() => {
1564
+ if (eraserPreviewItems.length === 0) return null;
1565
+ return /* @__PURE__ */ jsx(Fragment, { children: eraserPreviewItems.map((it) => {
1566
+ const b = normalizeRect(it.bounds);
1567
+ const cx = b.width / 2;
1568
+ const cy = b.height / 2;
1569
+ const t = skiaItemPlacementTransform(it.x, it.y, cx, cy, it.rotation ?? 0);
1692
1570
  return /* @__PURE__ */ jsx(
1693
- Line,
1571
+ Group,
1694
1572
  {
1695
- p1: vec(ln.x1, ln.y1),
1696
- p2: vec(ln.x2, ln.y2),
1697
- color,
1698
- style: "stroke",
1699
- strokeWidth: style.strokeWidth,
1700
- strokeCap: "round",
1701
- antiAlias: true
1702
- }
1573
+ transform: t,
1574
+ opacity: ERASER_PREVIEW_OPACITY,
1575
+ children: /* @__PURE__ */ jsx(NativeShapeRenderer, { item: it })
1576
+ },
1577
+ `erase-${it.id}`
1703
1578
  );
1704
- }
1705
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1706
- /* @__PURE__ */ jsx(
1707
- Line,
1708
- {
1709
- p1: vec(ln.x1, ln.y1),
1710
- p2: vec(geometry.shaftEndX, geometry.shaftEndY),
1711
- color,
1712
- style: "stroke",
1713
- strokeWidth: style.strokeWidth,
1714
- strokeCap: "round",
1715
- antiAlias: true
1716
- }
1717
- ),
1718
- /* @__PURE__ */ jsx(
1719
- Path,
1720
- {
1721
- path: `M ${geometry.headLeftX} ${geometry.headLeftY} L ${geometry.headTipX} ${geometry.headTipY} L ${geometry.headRightX} ${geometry.headRightY}`,
1722
- color,
1723
- style: "stroke",
1724
- strokeWidth: style.strokeWidth,
1725
- strokeCap: "round",
1726
- strokeJoin: "round",
1727
- antiAlias: true
1728
- }
1729
- )
1730
- ] });
1731
- }
1732
- if (k === "text" && item.text !== void 0) {
1733
- const fs = item.textFontSize ?? 16;
1734
- const color = rgba(style.stroke, style.strokeOpacity);
1735
- const lines = item.text.split("\n");
1736
- const font = matchFont({ fontSize: fs });
1737
- return /* @__PURE__ */ jsx(Fragment, { children: lines.map((line, i) => /* @__PURE__ */ jsx(
1738
- Text,
1739
- {
1740
- x: 0,
1741
- y: fs + i * fs * 1.2,
1742
- text: line,
1743
- color,
1744
- font
1745
- },
1746
- i
1747
- )) });
1748
- }
1749
- if ((k === "draw" || k === "pencil" || k === "brush" || k === "marker") && item.pathPointsLocal && item.pathPointsLocal.length > 0) {
1750
- const payload = computeFreehandSvgPayload(item.pathPointsLocal, style, k);
1751
- if (!payload) return null;
1752
- const color = rgba(style.stroke, style.strokeOpacity);
1753
- if (payload.kind === "circle") {
1754
- return /* @__PURE__ */ jsx(
1579
+ }) });
1580
+ }, [eraserPreviewItems]);
1581
+ const eraserTrailElements = useMemo(() => {
1582
+ if (!eraserTrail || eraserTrail.length < 1) return null;
1583
+ const d = pointsToSmoothPathD(eraserTrail);
1584
+ if (!d)
1585
+ return eraserTrail[0] ? /* @__PURE__ */ jsx(
1755
1586
  Circle,
1756
1587
  {
1757
- cx: payload.cx,
1758
- cy: payload.cy,
1759
- r: payload.r,
1760
- color: rgba(payload.fill, payload.fillOpacity),
1588
+ cx: eraserTrail[0].x,
1589
+ cy: eraserTrail[0].y,
1590
+ r: Math.max(5 / z, 3),
1591
+ color: "#cbd5e1",
1761
1592
  style: "fill",
1762
1593
  antiAlias: true
1763
1594
  }
1764
- );
1765
- }
1766
- if (payload.kind === "fillPath") {
1767
- return /* @__PURE__ */ jsx(
1768
- Path,
1595
+ ) : null;
1596
+ return /* @__PURE__ */ jsx(
1597
+ Path,
1598
+ {
1599
+ path: d,
1600
+ color: "#cbd5e1",
1601
+ style: "stroke",
1602
+ strokeWidth: Math.max(3.5 / z, overlayStrokePx),
1603
+ strokeCap: "round",
1604
+ strokeJoin: "round",
1605
+ antiAlias: true
1606
+ }
1607
+ );
1608
+ }, [eraserTrail, z]);
1609
+ const laserTrailElements = useMemo(() => {
1610
+ if (!laserTrail || laserTrail.length < 1) return null;
1611
+ const d = pointsToSmoothPathD(laserTrail);
1612
+ if (!d)
1613
+ return laserTrail[0] ? /* @__PURE__ */ jsx(
1614
+ Circle,
1769
1615
  {
1770
- path: payload.d,
1771
- color: rgba(payload.fill, payload.fillOpacity),
1616
+ cx: laserTrail[0].x,
1617
+ cy: laserTrail[0].y,
1618
+ r: Math.max(5 / z, 3),
1619
+ color: "#f43f5e",
1772
1620
  style: "fill",
1773
- fillType: "winding",
1774
1621
  antiAlias: true
1775
1622
  }
1776
- );
1777
- }
1623
+ ) : null;
1778
1624
  return /* @__PURE__ */ jsx(
1779
1625
  Path,
1780
1626
  {
1781
- path: payload.d,
1782
- color,
1627
+ path: d,
1628
+ color: "#f43f5e",
1783
1629
  style: "stroke",
1784
- strokeWidth: payload.strokeWidth,
1630
+ strokeWidth: Math.max(4 / z, overlayStrokePx),
1785
1631
  strokeCap: "round",
1786
1632
  strokeJoin: "round",
1787
1633
  antiAlias: true
1788
1634
  }
1789
1635
  );
1636
+ }, [laserTrail, z]);
1637
+ if (width <= 0 || height <= 0) return null;
1638
+ return /* @__PURE__ */ jsx(
1639
+ Canvas,
1640
+ {
1641
+ style: {
1642
+ position: "absolute",
1643
+ top: 0,
1644
+ left: 0,
1645
+ width,
1646
+ height
1647
+ },
1648
+ pointerEvents: "none",
1649
+ children: /* @__PURE__ */ jsxs(Group, { transform: camTransform, children: [
1650
+ previewElements,
1651
+ laserTrailElements,
1652
+ eraserTrailElements,
1653
+ eraserPreviewElements,
1654
+ selectionElements
1655
+ ] })
1656
+ }
1657
+ );
1658
+ }
1659
+
1660
+ // src/scene/spatial-cull.ts
1661
+ var spatialIndexCache = /* @__PURE__ */ new WeakMap();
1662
+ function cellKey(ix, iy) {
1663
+ return `${ix},${iy}`;
1664
+ }
1665
+ function buildSpatialIndex(items, cellSize) {
1666
+ const cached = spatialIndexCache.get(items);
1667
+ if (cached && cached.cellSize === cellSize) {
1668
+ return cached;
1790
1669
  }
1791
- if (k === "draw") {
1792
- const { w, h } = localBounds(item.bounds);
1793
- const r = Math.min(w, h) / 2;
1794
- return /* @__PURE__ */ jsx(
1795
- Circle,
1796
- {
1797
- cx: Math.max(0, w) / 2,
1798
- cy: Math.max(0, h) / 2,
1799
- r: Math.max(0, r),
1800
- color: rgba(style.stroke, style.strokeOpacity),
1801
- style: "fill",
1802
- antiAlias: true
1670
+ const aabbs = new Array(items.length);
1671
+ const buckets = /* @__PURE__ */ new Map();
1672
+ for (let index = 0; index < items.length; index += 1) {
1673
+ const item = items[index];
1674
+ if (!item) continue;
1675
+ const aabb = boundsAabbForRotatedItem(item);
1676
+ aabbs[index] = aabb;
1677
+ const { minIx, maxIx, minIy, maxIy } = cellRangeForRect(aabb, cellSize);
1678
+ for (let ix = minIx; ix <= maxIx; ix += 1) {
1679
+ for (let iy = minIy; iy <= maxIy; iy += 1) {
1680
+ const key = cellKey(ix, iy);
1681
+ let bucket = buckets.get(key);
1682
+ if (!bucket) {
1683
+ bucket = [];
1684
+ buckets.set(key, bucket);
1685
+ }
1686
+ bucket.push(index);
1803
1687
  }
1804
- );
1688
+ }
1805
1689
  }
1806
- if (k === "image" || k === "custom" || item.childrenSvg) {
1807
- const nodes = parseSvgFragment(item.childrenSvg);
1808
- if (nodes.length > 0) {
1809
- return /* @__PURE__ */ jsx(SvgNodeRenderer, { nodes });
1690
+ const next = {
1691
+ cellSize,
1692
+ aabbs,
1693
+ buckets
1694
+ };
1695
+ spatialIndexCache.set(items, next);
1696
+ return next;
1697
+ }
1698
+ function cellRangeForRect(r, cellSize) {
1699
+ const n = normalizeRect(r);
1700
+ const x1 = n.x + n.width;
1701
+ const y1 = n.y + n.height;
1702
+ const minIx = Math.floor(n.x / cellSize);
1703
+ const maxIx = Math.max(minIx, Math.ceil(x1 / cellSize) - 1);
1704
+ const minIy = Math.floor(n.y / cellSize);
1705
+ const maxIy = Math.max(minIy, Math.ceil(y1 / cellSize) - 1);
1706
+ return { minIx, maxIx, minIy, maxIy };
1707
+ }
1708
+ function cullItemsByViewportSpatial(items, visibleWorld, cellSize) {
1709
+ const { aabbs, buckets } = buildSpatialIndex(items, cellSize);
1710
+ const vr = cellRangeForRect(visibleWorld, cellSize);
1711
+ const seen = /* @__PURE__ */ new Set();
1712
+ const outIndices = [];
1713
+ for (let ix = vr.minIx; ix <= vr.maxIx; ix++) {
1714
+ for (let iy = vr.minIy; iy <= vr.maxIy; iy++) {
1715
+ const bucket = buckets.get(cellKey(ix, iy));
1716
+ if (!bucket) {
1717
+ continue;
1718
+ }
1719
+ for (const index of bucket) {
1720
+ if (seen.has(index)) {
1721
+ continue;
1722
+ }
1723
+ seen.add(index);
1724
+ const aabb = aabbs[index];
1725
+ if (aabb && rectsIntersect(aabb, visibleWorld)) {
1726
+ outIndices.push(index);
1727
+ }
1728
+ }
1810
1729
  }
1811
1730
  }
1812
- return null;
1731
+ outIndices.sort((a, b) => a - b);
1732
+ return outIndices.map((index) => items[index]).filter((item) => item != null);
1733
+ }
1734
+
1735
+ // src/scene/cull.ts
1736
+ var SPATIAL_MIN_ITEMS = 400;
1737
+ var SPATIAL_CELL_SIZE = 256;
1738
+ function cullItemsLinear(items, visibleWorld) {
1739
+ return items.filter(
1740
+ (item) => rectsIntersect(boundsAabbForCull(item), visibleWorld)
1741
+ );
1742
+ }
1743
+ function boundsAabbForCull(item) {
1744
+ return boundsAabbForRotatedItem(item);
1745
+ }
1746
+ function cullItemsByViewport(items, visibleWorld) {
1747
+ if (items.length < SPATIAL_MIN_ITEMS) {
1748
+ return cullItemsLinear(items, visibleWorld);
1749
+ }
1750
+ return cullItemsByViewportSpatial(items, visibleWorld, SPATIAL_CELL_SIZE);
1813
1751
  }
1814
1752
  var MemoShape = memo(function MemoShape2({
1815
1753
  item
@@ -1984,16 +1922,14 @@ function itemHitTestWorldPoint(item, worldX, worldY, options) {
1984
1922
  return true;
1985
1923
  }
1986
1924
  }
1925
+ return false;
1987
1926
  }
1988
- if (pts?.length === 1) {
1927
+ if (pts && pts.length === 1) {
1989
1928
  const p = pts[0];
1990
- if (p) {
1991
- const cw = itemLocalToWorld(p.x, p.y, item.x, item.y, w, h, rot);
1992
- const dsq = (worldX - cw.x) ** 2 + (worldY - cw.y) ** 2;
1993
- if (dsq <= tolSq) {
1994
- return true;
1995
- }
1996
- }
1929
+ if (!p) return false;
1930
+ const cw = itemLocalToWorld(p.x, p.y, item.x, item.y, w, h, rot);
1931
+ const dsq = (worldX - cw.x) ** 2 + (worldY - cw.y) ** 2;
1932
+ return dsq <= tolSq;
1997
1933
  }
1998
1934
  return hitTestFilledShape(item, worldX, worldY);
1999
1935
  }
@@ -2112,6 +2048,11 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
2112
2048
  () => items.filter((it) => selectedIds.includes(it.id)),
2113
2049
  [items, selectedIds]
2114
2050
  );
2051
+ const sceneItems = useMemo(() => {
2052
+ if (eraserPreviewIds.length === 0) return items;
2053
+ const hidden = new Set(eraserPreviewIds);
2054
+ return items.filter((it) => !hidden.has(it.id));
2055
+ }, [items, eraserPreviewIds]);
2115
2056
  const showResizeHandles = interactive && selectedItems.length === 1 && !selectedItems[0]?.locked;
2116
2057
  const lastPinchDist = useRef(null);
2117
2058
  const lastPanPoint = useRef(null);
@@ -2437,7 +2378,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
2437
2378
  /* @__PURE__ */ jsx(
2438
2379
  NativeSceneRenderer,
2439
2380
  {
2440
- items,
2381
+ items: sceneItems,
2441
2382
  camera,
2442
2383
  width: size.width,
2443
2384
  height: size.height