canvu-react 0.3.7 → 0.3.9

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/react.cjs CHANGED
@@ -53,93 +53,6 @@ var init_custom_shape = __esm({
53
53
  }
54
54
  });
55
55
 
56
- // src/scene/freehand-path.ts
57
- function dedupeFreehandPoints(points, minDist) {
58
- if (points.length <= 2) {
59
- return points.map((p) => ({ ...p }));
60
- }
61
- const minSq = minDist * minDist;
62
- const first = points[0];
63
- if (!first) return [];
64
- const out = [{ ...first }];
65
- for (let i = 1; i < points.length - 1; i++) {
66
- const p = points[i];
67
- const last = out[out.length - 1];
68
- if (!p || !last) continue;
69
- const dx = p.x - last.x;
70
- const dy = p.y - last.y;
71
- if (dx * dx + dy * dy >= minSq) {
72
- out.push({ ...p });
73
- }
74
- }
75
- const end = points[points.length - 1];
76
- const lastKept = out[out.length - 1];
77
- if (!end || !lastKept) return out;
78
- if ((end.x - lastKept.x) ** 2 + (end.y - lastKept.y) ** 2 > 1e-12) {
79
- out.push({ ...end });
80
- }
81
- return out;
82
- }
83
- function smoothFreehandPointsToPathD(points) {
84
- const n = points.length;
85
- if (n === 0) return "";
86
- if (n === 1) {
87
- const p = points[0];
88
- if (!p) return "";
89
- return `M ${p.x} ${p.y}`;
90
- }
91
- if (n === 2) {
92
- const a = points[0];
93
- const b = points[1];
94
- if (!a || !b) return "";
95
- return `M ${a.x} ${a.y} L ${b.x} ${b.y}`;
96
- }
97
- const p0 = points[0];
98
- if (!p0) return "";
99
- let d = `M ${p0.x} ${p0.y}`;
100
- let i = 1;
101
- for (; i < n - 2; i++) {
102
- const pi = points[i];
103
- const pi1 = points[i + 1];
104
- if (!pi || !pi1) continue;
105
- const xc = (pi.x + pi1.x) / 2;
106
- const yc = (pi.y + pi1.y) / 2;
107
- d += ` Q ${pi.x} ${pi.y} ${xc} ${yc}`;
108
- }
109
- const pLast = points[i];
110
- const pEnd = points[i + 1];
111
- if (!pLast || !pEnd) return d;
112
- d += ` Q ${pLast.x} ${pLast.y} ${pEnd.x} ${pEnd.y}`;
113
- return d;
114
- }
115
- function outlineStrokeToClosedPathD(outline) {
116
- const len = outline.length;
117
- if (len === 0) return "";
118
- const first = outline[0];
119
- if (!first) return "";
120
- if (len < 3) {
121
- let d2 = `M ${first[0]} ${first[1]}`;
122
- for (let i = 1; i < len; i++) {
123
- const pt = outline[i];
124
- if (!pt) continue;
125
- d2 += ` L ${pt[0]} ${pt[1]}`;
126
- }
127
- return `${d2} Z`;
128
- }
129
- let d = `M ${first[0]} ${first[1]} Q`;
130
- for (let i = 0; i < len; i++) {
131
- const p0 = outline[i];
132
- const p1 = outline[(i + 1) % len];
133
- if (!p0 || !p1) continue;
134
- d += ` ${p0[0]} ${p0[1]} ${(p0[0] + p1[0]) / 2} ${(p0[1] + p1[1]) / 2}`;
135
- }
136
- return `${d} Z`;
137
- }
138
- var init_freehand_path = __esm({
139
- "src/scene/freehand-path.ts"() {
140
- }
141
- });
142
-
143
56
  // src/scene/text-svg.ts
144
57
  function escapeSvgTextContent(s) {
145
58
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -325,18 +238,18 @@ function perfectFreehandOptions(toolKind, style, strokeComplete, pressureAware =
325
238
  ...base2,
326
239
  size: Math.max(2, sw * 1.05),
327
240
  thinning: 0.42,
328
- smoothing: 0.56,
329
- streamline: 0.18,
330
- simulatePressure: false
241
+ smoothing: 0.78,
242
+ streamline: 0.62,
243
+ simulatePressure: true
331
244
  };
332
245
  }
333
246
  return {
334
247
  ...base2,
335
248
  size: Math.max(2, sw * 1.18),
336
249
  thinning: 0.12,
337
- smoothing: 0.72,
338
- streamline: 0.42,
339
- simulatePressure: false
250
+ smoothing: 0.85,
251
+ streamline: 0.78,
252
+ simulatePressure: true
340
253
  };
341
254
  }
342
255
  if (toolKind === "brush") {
@@ -354,7 +267,7 @@ function perfectFreehandOptions(toolKind, style, strokeComplete, pressureAware =
354
267
  thinning: 0.08,
355
268
  smoothing: 0.88,
356
269
  streamline: 0.84,
357
- simulatePressure: false
270
+ simulatePressure: true
358
271
  };
359
272
  }
360
273
  function resolveStrokeStyle(item) {
@@ -424,70 +337,41 @@ function computeFreehandSvgPayload(pathPointsLocal, style, toolKind, strokeCompl
424
337
  if (pathPointsLocal.length === 1) {
425
338
  const p = pathPointsLocal[0];
426
339
  if (!p) return null;
427
- const r = Math.max(0.5, style.strokeWidth / 2);
428
- return {
429
- kind: "circle",
430
- cx: p.x,
431
- cy: p.y,
432
- r,
433
- fill: style.stroke,
434
- fillOpacity: style.strokeOpacity
435
- };
436
- }
437
- const minDist = Math.min(0.25, Math.max(0.02, style.strokeWidth * 0.02));
438
- const pts = dedupeFreehandPoints(pathPointsLocal, minDist);
439
- if (pts.length === 0) return null;
440
- if (pts.length === 1) {
441
- const p = pts[0];
442
- if (!p) return null;
443
- const r = Math.max(0.5, style.strokeWidth / 2);
444
340
  return {
445
341
  kind: "circle",
446
342
  cx: p.x,
447
343
  cy: p.y,
448
- r,
344
+ r: Math.max(0.5, style.strokeWidth / 2),
449
345
  fill: style.stroke,
450
346
  fillOpacity: style.strokeOpacity
451
347
  };
452
348
  }
453
- const hasPressure = toolKind === "draw" && pts.some((p) => p.pressure != null && Number.isFinite(p.pressure));
454
- if (toolKind === "draw" && !hasPressure) {
455
- const d2 = smoothFreehandPointsToPathD(pts);
456
- return {
457
- kind: "strokePath",
458
- d: d2,
459
- stroke: style.stroke,
460
- strokeWidth: style.strokeWidth,
461
- strokeOpacity: style.strokeOpacity
462
- };
463
- }
464
- const input = hasPressure ? pts.map(
349
+ const hasPressure = pathPointsLocal.some(
350
+ (p) => p.pressure != null && Number.isFinite(p.pressure)
351
+ );
352
+ const input = hasPressure ? pathPointsLocal.map(
465
353
  (p) => [p.x, p.y, Math.min(1, Math.max(0, p.pressure ?? 0.5))]
466
- ) : pts.map((p) => [p.x, p.y]);
467
- const opts = perfectFreehandOptions(toolKind, style, strokeComplete, hasPressure);
468
- let outline = [];
469
- try {
470
- const raw = getStroke__default.default(input, opts);
471
- outline = raw.map(([x, y]) => [x, y]);
472
- } catch {
473
- outline = [];
474
- }
475
- if (outline.length >= 3) {
476
- const d2 = outlineStrokeToClosedPathD(outline);
477
- return {
478
- kind: "fillPath",
479
- d: d2,
480
- fill: style.stroke,
481
- fillOpacity: style.strokeOpacity
482
- };
354
+ ) : pathPointsLocal.map((p) => [p.x, p.y]);
355
+ const stroke = getStroke__default.default(
356
+ input,
357
+ perfectFreehandOptions(toolKind, style, strokeComplete, hasPressure)
358
+ );
359
+ if (stroke.length < 3) return null;
360
+ const first = stroke[0];
361
+ if (!first) return null;
362
+ let d = `M ${first[0]} ${first[1]} Q`;
363
+ for (let i = 0; i < stroke.length; i++) {
364
+ const a = stroke[i];
365
+ const b = stroke[(i + 1) % stroke.length];
366
+ if (!a || !b) continue;
367
+ d += ` ${a[0]} ${a[1]} ${(a[0] + b[0]) / 2} ${(a[1] + b[1]) / 2}`;
483
368
  }
484
- const d = smoothFreehandPointsToPathD(pts);
369
+ d += " Z";
485
370
  return {
486
- kind: "strokePath",
371
+ kind: "fillPath",
487
372
  d,
488
- stroke: style.stroke,
489
- strokeWidth: style.strokeWidth,
490
- strokeOpacity: style.strokeOpacity
373
+ fill: style.stroke,
374
+ fillOpacity: style.strokeOpacity
491
375
  };
492
376
  }
493
377
  function freehandPayloadToSvgString(payload) {
@@ -817,1118 +701,1369 @@ var init_shape_builders = __esm({
817
701
  "src/scene/shape-builders.ts"() {
818
702
  init_rect();
819
703
  init_custom_shape();
820
- init_freehand_path();
821
704
  init_text_svg();
822
705
  DEFAULT_STROKE_STYLE = {
823
706
  stroke: "#2563eb",
824
707
  strokeWidth: 2
825
708
  };
826
709
  TOOL_FREEHAND_DEFAULTS = {
827
- draw: { strokeWidth: 3 },
710
+ draw: { strokeWidth: 10 },
828
711
  pencil: { strokeWidth: 3 },
829
712
  brush: { strokeWidth: 10 },
830
713
  marker: { stroke: "#fde047", strokeWidth: 16, strokeOpacity: 0.5 }
831
714
  };
832
715
  }
833
716
  });
834
- var CanvuChromeContext = react.createContext(
835
- null
836
- );
837
- function useCanvuChromeContext() {
838
- return react.useContext(CanvuChromeContext);
839
- }
840
717
 
841
- // src/math/item-transform.ts
842
- init_rect();
843
- function getItemRotationRad(item) {
844
- return item.rotation ?? 0;
718
+ // src/image/indexed-db-image-store.ts
719
+ var DB_NAME = "canvu-image-store";
720
+ var DB_VERSION = 1;
721
+ function openDb() {
722
+ return new Promise((resolve, reject) => {
723
+ const req = indexedDB.open(DB_NAME, DB_VERSION);
724
+ req.onupgradeneeded = () => {
725
+ const db = req.result;
726
+ if (!db.objectStoreNames.contains("images")) {
727
+ db.createObjectStore("images");
728
+ }
729
+ if (!db.objectStoreNames.contains("thumbnails")) {
730
+ db.createObjectStore("thumbnails");
731
+ }
732
+ };
733
+ req.onsuccess = () => resolve(req.result);
734
+ req.onerror = () => reject(req.error);
735
+ });
845
736
  }
846
- function itemLocalToWorld(lx, ly, itemX, itemY, w, h, rotationRad) {
847
- const c = { x: w / 2, y: h / 2 };
848
- const dlx = lx - c.x;
849
- const dly = ly - c.y;
850
- const cos = Math.cos(rotationRad);
851
- const sin = Math.sin(rotationRad);
852
- return {
853
- x: itemX + c.x + cos * dlx - sin * dly,
854
- y: itemY + c.y + sin * dlx + cos * dly
855
- };
737
+ function getFromStore(db, storeName, id) {
738
+ return new Promise((resolve, reject) => {
739
+ const tx = db.transaction(storeName, "readonly");
740
+ const req = tx.objectStore(storeName).get(id);
741
+ req.onsuccess = () => resolve(req.result ?? void 0);
742
+ req.onerror = () => reject(req.error);
743
+ });
856
744
  }
857
- function worldToItemLocal(wx, wy, itemX, itemY, w, h, rotationRad) {
858
- const c = { x: w / 2, y: h / 2 };
859
- const vx = wx - itemX;
860
- const vy = wy - itemY;
861
- const dx = vx - c.x;
862
- const dy = vy - c.y;
863
- const cos = Math.cos(-rotationRad);
864
- const sin = Math.sin(-rotationRad);
865
- const lx = cos * dx - sin * dy;
866
- const ly = sin * dx + cos * dy;
867
- return { x: c.x + lx, y: c.y + ly };
745
+ function putInStore(db, storeName, id, blob) {
746
+ return new Promise((resolve, reject) => {
747
+ const tx = db.transaction(storeName, "readwrite");
748
+ const req = tx.objectStore(storeName).put(blob, id);
749
+ req.onsuccess = () => resolve();
750
+ req.onerror = () => reject(req.error);
751
+ });
868
752
  }
869
- function itemPivotWorld(item) {
870
- const r = normalizeRect(item.bounds);
871
- return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
753
+ function deleteFromStore(db, storeName, id) {
754
+ return new Promise((resolve, reject) => {
755
+ const tx = db.transaction(storeName, "readwrite");
756
+ const req = tx.objectStore(storeName).delete(id);
757
+ req.onsuccess = () => resolve();
758
+ req.onerror = () => reject(req.error);
759
+ });
872
760
  }
873
- function boundsAabbForRotatedItem(item) {
874
- const rot = getItemRotationRad(item);
875
- if (Math.abs(rot) < 1e-12 && item.bounds.width >= 0 && item.bounds.height >= 0) {
876
- return item.bounds;
761
+ function generateBlobId() {
762
+ return crypto.randomUUID();
763
+ }
764
+ var IndexedDbImageStore = class {
765
+ dbPromise = null;
766
+ getDb() {
767
+ if (!this.dbPromise) {
768
+ this.dbPromise = openDb();
769
+ }
770
+ return this.dbPromise;
877
771
  }
878
- const r = normalizeRect(item.bounds);
879
- if (Math.abs(rot) < 1e-12) {
880
- return r;
772
+ async storeOriginal(blob) {
773
+ const id = generateBlobId();
774
+ const db = await this.getDb();
775
+ await putInStore(db, "images", id, blob);
776
+ return id;
881
777
  }
882
- const corners = [
883
- [0, 0],
884
- [r.width, 0],
885
- [r.width, r.height],
886
- [0, r.height]
887
- ];
888
- let minX = Infinity;
889
- let minY = Infinity;
890
- let maxX = -Infinity;
891
- let maxY = -Infinity;
892
- for (const [lx, ly] of corners) {
893
- const p = itemLocalToWorld(lx, ly, item.x, item.y, r.width, r.height, rot);
894
- minX = Math.min(minX, p.x);
895
- minY = Math.min(minY, p.y);
896
- maxX = Math.max(maxX, p.x);
897
- maxY = Math.max(maxY, p.y);
778
+ async getOriginal(id) {
779
+ const db = await this.getDb();
780
+ return getFromStore(db, "images", id);
781
+ }
782
+ async deleteOriginal(id) {
783
+ const db = await this.getDb();
784
+ await deleteFromStore(db, "images", id);
785
+ }
786
+ async storeThumbnail(blob) {
787
+ const id = generateBlobId();
788
+ const db = await this.getDb();
789
+ await putInStore(db, "thumbnails", id, blob);
790
+ return id;
791
+ }
792
+ async getThumbnail(id) {
793
+ const db = await this.getDb();
794
+ return getFromStore(db, "thumbnails", id);
795
+ }
796
+ async deleteThumbnail(id) {
797
+ const db = await this.getDb();
798
+ await deleteFromStore(db, "thumbnails", id);
898
799
  }
899
- return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
900
- }
901
-
902
- // src/react/NavMenu.tsx
903
- init_rect();
904
- var shellLook = {
905
- display: "flex",
906
- flexDirection: "column",
907
- gap: 8,
908
- minWidth: 160,
909
- padding: "10px 12px",
910
- borderRadius: 8,
911
- border: "1px solid rgba(0,0,0,0.12)",
912
- background: "rgba(255,255,255,0.96)",
913
- boxShadow: "0 1px 3px rgba(0,0,0,0.08)",
914
- pointerEvents: "auto"
915
- };
916
- var labelStyle = {
917
- display: "flex",
918
- flexDirection: "column",
919
- gap: 4,
920
- fontSize: 11,
921
- fontWeight: 600,
922
- color: "#52525b"
923
800
  };
924
- function normalizeHex(stroke) {
925
- if (/^#[0-9A-Fa-f]{6}$/.test(stroke)) return stroke;
926
- return "#2563eb";
927
- }
928
- function isStylableKind(tk) {
929
- return tk === "rect" || tk === "ellipse" || tk === "line" || tk === "arrow" || tk === "draw" || tk === "pencil" || tk === "brush" || tk === "marker" || tk === "text";
930
- }
931
- function VectorSelectionInspector({
932
- items: itemsProp,
933
- activeToolStyle: activeToolStyleProp,
934
- onChange: onChangeProp,
935
- position = "top-left",
936
- inset = 12,
937
- zIndex = 24,
938
- className,
939
- style
940
- }) {
941
- const ctx = useCanvuChromeContext();
942
- const items = itemsProp ?? ctx?.selectedItems ?? [];
943
- const activeToolStyle = activeToolStyleProp === void 0 ? ctx?.activeToolStyle ?? null : activeToolStyleProp;
944
- const onChange = onChangeProp ?? ctx?.onSelectionStyleChange ?? null;
945
- if (!onChange) return null;
946
- const shell = {
947
- ...getBoardPositionStyle(position, inset, zIndex),
948
- ...shellLook,
949
- ...style
950
- };
951
- if (activeToolStyle) {
952
- const stroke2 = activeToolStyle.stroke;
953
- const strokeWidth2 = activeToolStyle.strokeWidth;
954
- const hex2 = normalizeHex(stroke2);
955
- const showMarkerOpacity2 = activeToolStyle.toolKind === "marker";
956
- const opacityPct2 = Math.round((activeToolStyle.strokeOpacity ?? 1) * 100);
957
- return /* @__PURE__ */ jsxRuntime.jsxs(
958
- "section",
959
- {
960
- "data-slot": "vector-selection-inspector",
961
- "data-position": position,
962
- className,
963
- "aria-label": activeToolStyle.label ?? "Estilo da ferramenta",
964
- style: shell,
965
- children: [
966
- /* @__PURE__ */ jsxRuntime.jsxs("label", { style: labelStyle, children: [
967
- "Cor",
968
- /* @__PURE__ */ jsxRuntime.jsx(
969
- "input",
970
- {
971
- type: "color",
972
- value: hex2,
973
- onChange: (e) => onChange({
974
- stroke: e.target.value,
975
- strokeWidth: strokeWidth2,
976
- ...activeToolStyle.strokeOpacity != null ? { strokeOpacity: activeToolStyle.strokeOpacity } : {}
977
- }),
978
- style: {
979
- width: "100%",
980
- height: 32,
981
- padding: 0,
982
- border: "none",
983
- cursor: "pointer",
984
- background: "transparent"
985
- }
986
- }
987
- )
988
- ] }),
989
- /* @__PURE__ */ jsxRuntime.jsxs("label", { style: labelStyle, children: [
990
- "Grossura",
991
- /* @__PURE__ */ jsxRuntime.jsx(
992
- "input",
993
- {
994
- type: "range",
995
- min: 1,
996
- max: 48,
997
- value: strokeWidth2,
998
- onChange: (e) => onChange({
999
- stroke: hex2,
1000
- strokeWidth: Number(e.target.value),
1001
- ...activeToolStyle.strokeOpacity != null ? { strokeOpacity: activeToolStyle.strokeOpacity } : {}
1002
- })
1003
- }
1004
- ),
1005
- /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontWeight: 500, color: "#71717a" }, children: [
1006
- strokeWidth2,
1007
- "px"
1008
- ] })
1009
- ] }),
1010
- showMarkerOpacity2 && /* @__PURE__ */ jsxRuntime.jsxs("label", { style: labelStyle, children: [
1011
- "Opacidade (marcador)",
1012
- /* @__PURE__ */ jsxRuntime.jsx(
1013
- "input",
1014
- {
1015
- type: "range",
1016
- min: 10,
1017
- max: 100,
1018
- value: opacityPct2,
1019
- onChange: (e) => {
1020
- const v = Number(e.target.value) / 100;
1021
- onChange({
1022
- stroke: hex2,
1023
- strokeWidth: strokeWidth2,
1024
- strokeOpacity: v
1025
- });
1026
- }
1027
- }
1028
- ),
1029
- /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontWeight: 500, color: "#71717a" }, children: [
1030
- opacityPct2,
1031
- "%"
1032
- ] })
1033
- ] })
1034
- ]
801
+ function decodeImageToCanvas(blob, maxDimension) {
802
+ return new Promise((resolve, reject) => {
803
+ const img = new Image();
804
+ img.onload = () => {
805
+ const w0 = img.naturalWidth;
806
+ const h0 = img.naturalHeight;
807
+ if (w0 < 1 || h0 < 1) {
808
+ reject(new Error("Image has no dimensions"));
809
+ return;
810
+ }
811
+ const scale = 1;
812
+ const cw = Math.max(1, Math.round(w0 * scale));
813
+ const ch = Math.max(1, Math.round(h0 * scale));
814
+ const canvas = document.createElement("canvas");
815
+ canvas.width = cw;
816
+ canvas.height = ch;
817
+ const ctx = canvas.getContext("2d");
818
+ if (!ctx) {
819
+ reject(new Error("Canvas 2D context unavailable"));
820
+ return;
1035
821
  }
822
+ ctx.imageSmoothingEnabled = true;
823
+ ctx.imageSmoothingQuality = "high";
824
+ ctx.drawImage(img, 0, 0, cw, ch);
825
+ URL.revokeObjectURL(img.src);
826
+ resolve({ canvas, width: cw, height: ch });
827
+ };
828
+ img.onerror = () => {
829
+ URL.revokeObjectURL(img.src);
830
+ reject(new Error("Could not decode image"));
831
+ };
832
+ img.src = URL.createObjectURL(blob);
833
+ });
834
+ }
835
+ function canvasToBlob(canvas, mime, quality) {
836
+ return new Promise((resolve, reject) => {
837
+ canvas.toBlob(
838
+ (blob) => {
839
+ if (!blob) {
840
+ reject(new Error("Could not encode blob"));
841
+ return;
842
+ }
843
+ resolve(blob);
844
+ },
845
+ mime,
846
+ quality
1036
847
  );
848
+ });
849
+ }
850
+ async function loadImageToStore(file, store) {
851
+ const originalBlob = file;
852
+ const blobId = await store.storeOriginal(originalBlob);
853
+ const { canvas, width, height } = await decodeImageToCanvas(originalBlob);
854
+ const thumbScale = Math.min(1, 256 / Math.max(width, height));
855
+ const tw = Math.max(1, Math.round(width * thumbScale));
856
+ const th = Math.max(1, Math.round(height * thumbScale));
857
+ const thumbCanvas = document.createElement("canvas");
858
+ thumbCanvas.width = tw;
859
+ thumbCanvas.height = th;
860
+ const tCtx = thumbCanvas.getContext("2d");
861
+ if (tCtx) {
862
+ tCtx.imageSmoothingEnabled = true;
863
+ tCtx.imageSmoothingQuality = "high";
864
+ tCtx.drawImage(canvas, 0, 0, tw, th);
1037
865
  }
1038
- const stylable = items.filter(
1039
- (it) => !it.locked && it.toolKind && isStylableKind(it.toolKind)
1040
- );
1041
- if (stylable.length === 0) return null;
1042
- const first = stylable[0];
1043
- if (!first) return null;
1044
- const allSameStroke = stylable.every(
1045
- (it) => (it.stroke ?? "#2563eb") === (first.stroke ?? "#2563eb")
1046
- );
1047
- const allSameWidth = stylable.every(
1048
- (it) => (it.strokeWidth ?? 2) === (first.strokeWidth ?? 2)
1049
- );
1050
- const stroke = first.stroke ?? "#2563eb";
1051
- const strokeWidth = first.strokeWidth ?? 2;
1052
- const hex = normalizeHex(stroke);
1053
- const markers = stylable.filter((it) => it.toolKind === "marker");
1054
- const showMarkerOpacity = markers.length > 0;
1055
- const firstMarker = markers[0];
1056
- const allSameMarkerOpacity = markers.length > 0 && markers.every(
1057
- (it) => (it.strokeOpacity ?? 1) === (firstMarker?.strokeOpacity ?? 1)
1058
- );
1059
- const opacityPct = firstMarker ? Math.round((firstMarker.strokeOpacity ?? 1) * 100) : 100;
1060
- return /* @__PURE__ */ jsxRuntime.jsxs(
1061
- "section",
1062
- {
1063
- "data-slot": "vector-selection-inspector",
1064
- "data-position": position,
1065
- className,
1066
- "aria-label": "Estilo da sele\xE7\xE3o",
1067
- style: shell,
1068
- children: [
1069
- stylable.length > 1 && /* @__PURE__ */ jsxRuntime.jsxs("p", { style: { margin: 0, fontSize: 11, color: "#71717a" }, children: [
1070
- stylable.length,
1071
- " objetos selecionados"
1072
- ] }),
1073
- /* @__PURE__ */ jsxRuntime.jsxs("label", { style: labelStyle, children: [
1074
- "Cor",
1075
- !allSameStroke && /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontWeight: 400, color: "#a1a1aa" }, children: [
1076
- " ",
1077
- "(valores misturados \u2014 novo valor aplica a todos)"
1078
- ] }),
1079
- /* @__PURE__ */ jsxRuntime.jsx(
1080
- "input",
1081
- {
1082
- type: "color",
1083
- value: hex,
1084
- onChange: (e) => onChange({
1085
- stroke: e.target.value,
1086
- strokeWidth
1087
- }),
1088
- style: {
1089
- width: "100%",
1090
- height: 32,
1091
- padding: 0,
1092
- border: "none",
1093
- cursor: "pointer",
1094
- background: "transparent"
1095
- }
1096
- }
1097
- )
1098
- ] }),
1099
- /* @__PURE__ */ jsxRuntime.jsxs("label", { style: labelStyle, children: [
1100
- "Grossura",
1101
- !allSameWidth && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontWeight: 400, color: "#a1a1aa" }, children: " (misturado)" }),
1102
- /* @__PURE__ */ jsxRuntime.jsx(
1103
- "input",
1104
- {
1105
- type: "range",
1106
- min: 1,
1107
- max: 48,
1108
- value: strokeWidth,
1109
- onChange: (e) => onChange({
1110
- stroke: hex,
1111
- strokeWidth: Number(e.target.value)
1112
- })
1113
- }
1114
- ),
1115
- /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontWeight: 500, color: "#71717a" }, children: [
1116
- strokeWidth,
1117
- "px"
1118
- ] })
1119
- ] }),
1120
- showMarkerOpacity && firstMarker && /* @__PURE__ */ jsxRuntime.jsxs("label", { style: labelStyle, children: [
1121
- "Opacidade (marcador)",
1122
- !allSameMarkerOpacity && markers.length > 1 && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontWeight: 400, color: "#a1a1aa" }, children: " (misturado)" }),
1123
- /* @__PURE__ */ jsxRuntime.jsx(
1124
- "input",
1125
- {
1126
- type: "range",
1127
- min: 10,
1128
- max: 100,
1129
- value: opacityPct,
1130
- onChange: (e) => {
1131
- const v = Number(e.target.value) / 100;
1132
- onChange({
1133
- stroke: hex,
1134
- strokeWidth,
1135
- strokeOpacity: v
1136
- });
1137
- }
1138
- }
1139
- ),
1140
- /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontWeight: 500, color: "#71717a" }, children: [
1141
- opacityPct,
1142
- "%"
1143
- ] })
1144
- ] })
1145
- ]
1146
- }
866
+ const thumbBlob = await canvasToBlob(
867
+ thumbCanvas,
868
+ file.type === "image/jpeg" || file.type === "image/jpg" ? "image/jpeg" : "image/png",
869
+ file.type.startsWith("image/jpeg") ? 0.85 : void 0
1147
870
  );
871
+ const thumbnailBlobId = await store.storeThumbnail(thumbBlob);
872
+ return { blobId, thumbnailBlobId, width, height };
1148
873
  }
1149
- function getBoardPositionStyle(position, inset = 12, zIndex = 40) {
1150
- const base2 = { position: "absolute", zIndex };
1151
- switch (position) {
1152
- case "fill":
1153
- return { ...base2, inset };
1154
- case "top-left":
1155
- case "left-top":
1156
- return { ...base2, top: inset, left: inset };
1157
- case "top-center":
1158
- return {
1159
- ...base2,
1160
- top: inset,
1161
- left: "50%",
1162
- transform: "translateX(-50%)"
1163
- };
1164
- case "top-right":
1165
- case "right-top":
1166
- return { ...base2, top: inset, right: inset };
1167
- case "bottom-left":
1168
- case "left-bottom":
1169
- return { ...base2, bottom: inset, left: inset };
1170
- case "bottom-center":
1171
- return {
1172
- ...base2,
1173
- bottom: inset,
1174
- left: "50%",
1175
- transform: "translateX(-50%)"
1176
- };
1177
- case "bottom-right":
1178
- case "right-bottom":
1179
- return { ...base2, bottom: inset, right: inset };
1180
- case "left-center":
1181
- return {
1182
- ...base2,
1183
- left: inset,
1184
- top: "50%",
1185
- transform: "translateY(-50%)"
1186
- };
1187
- case "right-center":
1188
- return {
1189
- ...base2,
1190
- right: inset,
1191
- top: "50%",
1192
- transform: "translateY(-50%)"
1193
- };
1194
- case "center":
1195
- return {
1196
- ...base2,
1197
- top: "50%",
1198
- left: "50%",
1199
- transform: "translate(-50%, -50%)"
1200
- };
1201
- default:
1202
- return base2;
874
+ async function createBlobUrlFromStore(store, blobId) {
875
+ const blob = await store.getOriginal(blobId);
876
+ if (!blob) return null;
877
+ return URL.createObjectURL(blob);
878
+ }
879
+ async function createThumbnailBlobUrlFromStore(store, thumbnailBlobId) {
880
+ const blob = await store.getThumbnail(thumbnailBlobId);
881
+ if (!blob) return null;
882
+ return URL.createObjectURL(blob);
883
+ }
884
+
885
+ // src/image/pdf-loader.ts
886
+ var pdfjsPromise = null;
887
+ function getPdfJs() {
888
+ if (!pdfjsPromise) {
889
+ pdfjsPromise = import('pdfjs-dist').then((mod) => {
890
+ const workerSrc = new URL(
891
+ "pdfjs-dist/build/pdf.worker.min.mjs",
892
+ (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('react.cjs', document.baseURI).href))
893
+ ).toString();
894
+ mod.GlobalWorkerOptions.workerSrc = workerSrc;
895
+ return mod;
896
+ });
1203
897
  }
898
+ return pdfjsPromise;
1204
899
  }
1205
- var rootStyle = {
1206
- display: "flex",
1207
- flexDirection: "column",
1208
- height: "100%",
1209
- minHeight: 0,
1210
- width: "100%"
1211
- };
1212
- var headerStyle = {
1213
- flexShrink: 0,
1214
- display: "flex",
1215
- flexWrap: "wrap",
1216
- alignItems: "center",
1217
- gap: "0.75rem",
1218
- padding: "0.75rem 1rem",
1219
- borderBottom: "1px solid #e4e4e7",
1220
- background: "#fff",
1221
- fontSize: "0.875rem"
1222
- };
1223
- var bodyStyle = {
1224
- flex: 1,
1225
- minHeight: 0,
1226
- display: "flex",
1227
- flexDirection: "column"
1228
- };
1229
- var mainStyle = {
1230
- flex: 1,
1231
- minHeight: 0,
1232
- position: "relative",
1233
- display: "flex",
1234
- flexDirection: "column"
1235
- };
1236
- var viewportSurfaceStyle = {
1237
- flex: 1,
1238
- minHeight: 0,
1239
- position: "relative",
1240
- width: "100%",
1241
- alignSelf: "stretch",
1242
- background: "#fff",
1243
- touchAction: "none"
1244
- };
1245
- function mergeStyle(base2, style) {
1246
- return style ? { ...base2, ...style } : base2;
900
+ async function renderPageToCanvas(page, scale) {
901
+ const raw = page.getViewport({ scale: 1 });
902
+ const adjustedScale = Math.round(raw.width * scale) / raw.width;
903
+ const viewport = page.getViewport({ scale: adjustedScale });
904
+ const w = Math.round(viewport.width);
905
+ const h = Math.round(viewport.height);
906
+ const canvas = document.createElement("canvas");
907
+ canvas.width = w;
908
+ canvas.height = h;
909
+ const ctx = canvas.getContext("2d");
910
+ if (!ctx) throw new Error("Canvas 2D context unavailable");
911
+ ctx.imageSmoothingEnabled = true;
912
+ ctx.imageSmoothingQuality = "high";
913
+ await page.render({ canvas, viewport }).promise;
914
+ return { canvas, width: w, height: h };
1247
915
  }
1248
- function vectorCanvasSpaceStyle(position, inset, zIndex) {
1249
- return {
1250
- ...getBoardPositionStyle(position, inset, zIndex),
1251
- pointerEvents: "none"
1252
- };
916
+ function canvasToBlob2(canvas, mime, quality) {
917
+ return new Promise((resolve, reject) => {
918
+ canvas.toBlob(
919
+ (blob) => {
920
+ if (!blob) {
921
+ reject(new Error("Could not encode blob"));
922
+ return;
923
+ }
924
+ resolve(blob);
925
+ },
926
+ mime,
927
+ quality
928
+ );
929
+ });
1253
930
  }
1254
- function VectorCanvasRoot({
1255
- children,
1256
- className,
1257
- style
1258
- }) {
1259
- return /* @__PURE__ */ jsxRuntime.jsx(
1260
- "div",
1261
- {
1262
- "data-slot": "vector-canvas-root",
1263
- className,
1264
- style: mergeStyle(rootStyle, style),
1265
- children
931
+ async function loadPdfToStore(file, store, options) {
932
+ const scale = options?.scale ?? 1.5;
933
+ const pdfjs = await getPdfJs();
934
+ const arrayBuffer = await file.arrayBuffer();
935
+ const pdf = await pdfjs.getDocument({ data: arrayBuffer }).promise;
936
+ const results = [];
937
+ for (let i = 1; i <= pdf.numPages; i++) {
938
+ const page = await pdf.getPage(i);
939
+ const { canvas, width, height } = await renderPageToCanvas(page, scale);
940
+ const mime = "image/png";
941
+ const pageBlob = await canvasToBlob2(canvas, mime);
942
+ const blobId = await store.storeOriginal(pageBlob);
943
+ const thumbScale = Math.min(1, 256 / Math.max(width, height));
944
+ const tw = Math.max(1, Math.round(width * thumbScale));
945
+ const th = Math.max(1, Math.round(height * thumbScale));
946
+ const thumbCanvas = document.createElement("canvas");
947
+ thumbCanvas.width = tw;
948
+ thumbCanvas.height = th;
949
+ const tCtx = thumbCanvas.getContext("2d");
950
+ if (tCtx) {
951
+ tCtx.imageSmoothingEnabled = true;
952
+ tCtx.imageSmoothingQuality = "high";
953
+ tCtx.drawImage(canvas, 0, 0, tw, th);
1266
954
  }
1267
- );
955
+ const thumbBlob = await canvasToBlob2(thumbCanvas, mime);
956
+ const thumbnailBlobId = await store.storeThumbnail(thumbBlob);
957
+ results.push({ blobId, thumbnailBlobId, width, height, pageNumber: i });
958
+ }
959
+ return results;
1268
960
  }
1269
- function VectorCanvasHeader({
1270
- children,
1271
- className,
1272
- style
1273
- }) {
1274
- return /* @__PURE__ */ jsxRuntime.jsx(
1275
- "header",
1276
- {
1277
- "data-slot": "vector-canvas-header",
1278
- className,
1279
- style: mergeStyle(headerStyle, style),
1280
- children
961
+
962
+ // src/react/asset-ingestion.ts
963
+ init_shape_builders();
964
+
965
+ // src/react/asset-store.ts
966
+ function applyAssetUploadResultToItem(item, result) {
967
+ if (!result?.pluginData) return item;
968
+ return {
969
+ ...item,
970
+ pluginData: {
971
+ ...item.pluginData ?? {},
972
+ ...result.pluginData
1281
973
  }
1282
- );
974
+ };
1283
975
  }
1284
- function VectorCanvasBody({
1285
- children,
1286
- className,
1287
- style
1288
- }) {
1289
- return /* @__PURE__ */ jsxRuntime.jsx(
1290
- "div",
1291
- {
1292
- "data-slot": "vector-canvas-body",
1293
- className,
1294
- style: mergeStyle(bodyStyle, style),
1295
- children
1296
- }
1297
- );
976
+
977
+ // src/react/asset-ingestion.ts
978
+ function getAssetKindForFile(file) {
979
+ if (file.type === "application/pdf") return "pdf";
980
+ if (file.type.startsWith("image/")) return "image";
981
+ return null;
1298
982
  }
1299
- function VectorCanvasMain({
1300
- children,
1301
- className,
1302
- style
1303
- }) {
1304
- return /* @__PURE__ */ jsxRuntime.jsx(
1305
- "div",
1306
- {
1307
- "data-slot": "vector-canvas-main",
1308
- className,
1309
- style: mergeStyle(mainStyle, style),
1310
- children
1311
- }
1312
- );
983
+ function finalizeIngestedItem(item, context, uploadResult, decorateItem) {
984
+ const itemWithAssetData = applyAssetUploadResultToItem(item, uploadResult);
985
+ return decorateItem ? decorateItem(itemWithAssetData, context) : itemWithAssetData;
1313
986
  }
1314
- function VectorCanvasViewportSurface({
1315
- children,
1316
- className,
1317
- style
1318
- }) {
1319
- return /* @__PURE__ */ jsxRuntime.jsx(
1320
- "div",
1321
- {
1322
- "data-slot": "vector-canvas-viewport-surface",
1323
- className,
1324
- style: mergeStyle(viewportSurfaceStyle, style),
1325
- children
987
+ async function uploadAssetIfNeeded(assetStore, file, kind) {
988
+ if (!assetStore) return null;
989
+ const result = await assetStore.upload({ file, kind });
990
+ return result ?? null;
991
+ }
992
+ async function ingestAssetFilesToSceneItems(options) {
993
+ const {
994
+ files,
995
+ worldCenter,
996
+ assetStore,
997
+ imageStore = new IndexedDbImageStore(),
998
+ createId = createShapeId,
999
+ gapWorld = 16,
1000
+ stepWorld = 48,
1001
+ pdfScale = 1.5,
1002
+ decorateItem,
1003
+ onError
1004
+ } = options;
1005
+ const items = [];
1006
+ const errors = [];
1007
+ let imagePlacementIndex = 0;
1008
+ let occupiedBottomY = null;
1009
+ let imageYOffsetAdjustment = 0;
1010
+ let hasImagePlacementBase = false;
1011
+ for (const file of files) {
1012
+ const kind = getAssetKindForFile(file);
1013
+ if (!kind) {
1014
+ const error = {
1015
+ file,
1016
+ error: new Error(`Unsupported asset type: ${file.type || "unknown"}`)
1017
+ };
1018
+ errors.push(error);
1019
+ onError?.(error);
1020
+ continue;
1326
1021
  }
1327
- );
1022
+ try {
1023
+ const uploadResult = await uploadAssetIfNeeded(assetStore, file, kind);
1024
+ if (kind === "pdf") {
1025
+ const pages = await loadPdfToStore(file, imageStore, { scale: pdfScale });
1026
+ for (const page of pages) {
1027
+ const fullUrl2 = await createBlobUrlFromStore(imageStore, page.blobId);
1028
+ const naturalTopY2 = worldCenter.y - page.height / 2;
1029
+ const stackedTopY = occupiedBottomY == null ? naturalTopY2 : occupiedBottomY + gapWorld;
1030
+ const bounds2 = {
1031
+ x: worldCenter.x - page.width / 2,
1032
+ y: Math.max(naturalTopY2, stackedTopY),
1033
+ width: page.width,
1034
+ height: page.height
1035
+ };
1036
+ const itemContext2 = {
1037
+ file,
1038
+ kind,
1039
+ itemIndex: items.length,
1040
+ pageNumber: page.pageNumber
1041
+ };
1042
+ const item2 = finalizeIngestedItem(
1043
+ {
1044
+ id: createId(),
1045
+ x: bounds2.x,
1046
+ y: bounds2.y,
1047
+ bounds: { ...bounds2 },
1048
+ toolKind: "image",
1049
+ imageBlobId: page.blobId,
1050
+ imageRasterHref: fullUrl2 ?? void 0,
1051
+ imageIntrinsicSize: {
1052
+ width: page.width,
1053
+ height: page.height
1054
+ },
1055
+ childrenSvg: fullUrl2 ? buildRasterImageChildrenSvg(
1056
+ fullUrl2,
1057
+ { width: page.width, height: page.height },
1058
+ bounds2
1059
+ ) : ""
1060
+ },
1061
+ itemContext2,
1062
+ uploadResult,
1063
+ decorateItem
1064
+ );
1065
+ items.push(item2);
1066
+ occupiedBottomY = bounds2.y + page.height;
1067
+ }
1068
+ hasImagePlacementBase = false;
1069
+ imagePlacementIndex = 0;
1070
+ imageYOffsetAdjustment = 0;
1071
+ continue;
1072
+ }
1073
+ const { blobId, thumbnailBlobId, width, height } = await loadImageToStore(
1074
+ file,
1075
+ imageStore
1076
+ );
1077
+ const fullUrl = await createBlobUrlFromStore(imageStore, blobId);
1078
+ const thumbBlob = await imageStore.getThumbnail(thumbnailBlobId);
1079
+ const thumbnailHref = thumbBlob ? URL.createObjectURL(thumbBlob) : null;
1080
+ const ox = imagePlacementIndex % 8 * stepWorld;
1081
+ const oy = Math.floor(imagePlacementIndex / 8) * stepWorld;
1082
+ const naturalTopY = worldCenter.y - height / 2 + oy;
1083
+ if (!hasImagePlacementBase) {
1084
+ const minimumTopY = occupiedBottomY == null ? naturalTopY : occupiedBottomY + gapWorld;
1085
+ imageYOffsetAdjustment = Math.max(0, minimumTopY - naturalTopY);
1086
+ hasImagePlacementBase = true;
1087
+ }
1088
+ const bounds = {
1089
+ x: worldCenter.x - width / 2 + ox,
1090
+ y: naturalTopY + imageYOffsetAdjustment,
1091
+ width,
1092
+ height
1093
+ };
1094
+ const href = thumbnailHref ?? fullUrl ?? "";
1095
+ const itemContext = {
1096
+ file,
1097
+ kind,
1098
+ itemIndex: items.length
1099
+ };
1100
+ const item = finalizeIngestedItem(
1101
+ {
1102
+ id: createId(),
1103
+ x: bounds.x,
1104
+ y: bounds.y,
1105
+ bounds: { ...bounds },
1106
+ toolKind: "image",
1107
+ imageBlobId: blobId,
1108
+ imageThumbnailBlobId: thumbnailBlobId,
1109
+ imageRasterHref: fullUrl ?? void 0,
1110
+ imageThumbnailHref: thumbnailHref ?? void 0,
1111
+ imageIntrinsicSize: { width, height },
1112
+ childrenSvg: buildRasterImageChildrenSvg(href, { width, height }, bounds)
1113
+ },
1114
+ itemContext,
1115
+ uploadResult,
1116
+ decorateItem
1117
+ );
1118
+ items.push(item);
1119
+ imagePlacementIndex++;
1120
+ occupiedBottomY = occupiedBottomY == null ? bounds.y + height : Math.max(occupiedBottomY, bounds.y + height);
1121
+ } catch (error) {
1122
+ const fileError = {
1123
+ file,
1124
+ kind,
1125
+ error
1126
+ };
1127
+ errors.push(fileError);
1128
+ onError?.(fileError);
1129
+ }
1130
+ }
1131
+ return {
1132
+ items,
1133
+ errors
1134
+ };
1328
1135
  }
1329
- function VectorCanvasToolbar({
1330
- children,
1331
- className,
1332
- style,
1333
- position = "bottom-center",
1334
- inset = 12,
1335
- zIndex = 30
1336
- }) {
1337
- const base2 = {
1338
- ...getBoardPositionStyle(position, inset, zIndex),
1339
- display: "flex",
1340
- justifyContent: "center",
1341
- alignItems: "center",
1342
- maxWidth: "calc(100% - 24px)",
1343
- pointerEvents: "none"
1136
+ var CanvuChromeContext = react.createContext(
1137
+ null
1138
+ );
1139
+ function useCanvuChromeContext() {
1140
+ return react.useContext(CanvuChromeContext);
1141
+ }
1142
+
1143
+ // src/math/item-transform.ts
1144
+ init_rect();
1145
+ function getItemRotationRad(item) {
1146
+ return item.rotation ?? 0;
1147
+ }
1148
+ function itemLocalToWorld(lx, ly, itemX, itemY, w, h, rotationRad) {
1149
+ const c = { x: w / 2, y: h / 2 };
1150
+ const dlx = lx - c.x;
1151
+ const dly = ly - c.y;
1152
+ const cos = Math.cos(rotationRad);
1153
+ const sin = Math.sin(rotationRad);
1154
+ return {
1155
+ x: itemX + c.x + cos * dlx - sin * dly,
1156
+ y: itemY + c.y + sin * dlx + cos * dly
1344
1157
  };
1345
- return /* @__PURE__ */ jsxRuntime.jsx(
1346
- "div",
1347
- {
1348
- "data-slot": "vector-canvas-toolbar",
1349
- "data-position": position,
1350
- className,
1351
- style: mergeStyle(base2, style),
1352
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { pointerEvents: "auto" }, children })
1353
- }
1354
- );
1355
1158
  }
1356
- function VectorCanvasSpace({
1357
- children,
1358
- className,
1359
- style,
1360
- position = "top-right",
1361
- inset = 12,
1362
- zIndex = 40,
1363
- contentPointerEvents = "auto"
1364
- }) {
1365
- return /* @__PURE__ */ jsxRuntime.jsx(
1366
- "div",
1367
- {
1368
- "data-slot": `vector-canvas-space-${position}`,
1369
- className,
1370
- style: mergeStyle(vectorCanvasSpaceStyle(position, inset, zIndex), style),
1371
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { pointerEvents: contentPointerEvents }, children })
1372
- }
1373
- );
1159
+ function worldToItemLocal(wx, wy, itemX, itemY, w, h, rotationRad) {
1160
+ const c = { x: w / 2, y: h / 2 };
1161
+ const vx = wx - itemX;
1162
+ const vy = wy - itemY;
1163
+ const dx = vx - c.x;
1164
+ const dy = vy - c.y;
1165
+ const cos = Math.cos(-rotationRad);
1166
+ const sin = Math.sin(-rotationRad);
1167
+ const lx = cos * dx - sin * dy;
1168
+ const ly = sin * dx + cos * dy;
1169
+ return { x: c.x + lx, y: c.y + ly };
1374
1170
  }
1375
- var VectorCanvas = {
1376
- Root: VectorCanvasRoot,
1377
- Header: VectorCanvasHeader,
1378
- Body: VectorCanvasBody,
1379
- Main: VectorCanvasMain,
1380
- ViewportSurface: VectorCanvasViewportSurface,
1381
- Toolbar: VectorCanvasToolbar,
1382
- Space: VectorCanvasSpace,
1383
- NavMenu,
1384
- SelectionInspector: VectorSelectionInspector
1385
- };
1386
- var MINIMAP_W = 180;
1387
- var MINIMAP_H = 120;
1388
- var PADDING = 12;
1389
- var BORDER_RADIUS = 8;
1390
- var BG = "#ffffff";
1391
- var BORDER_COLOR = "rgba(0,0,0,0.12)";
1392
- var ITEM_FILL = "rgba(0,0,0,0.08)";
1393
- var ITEM_STROKE = "rgba(0,0,0,0.15)";
1394
- var VIEWPORT_FILL = "rgba(59,130,246,0.08)";
1395
- var VIEWPORT_STROKE = "#3b82f6";
1396
- var btnStyle = {
1397
- pointerEvents: "auto",
1398
- width: 36,
1399
- height: 36,
1400
- display: "inline-flex",
1401
- alignItems: "center",
1402
- justifyContent: "center",
1403
- cursor: "pointer",
1404
- fontSize: 18,
1405
- lineHeight: 1,
1406
- color: "#18181b",
1407
- padding: 0,
1408
- border: "none",
1409
- outline: "none",
1410
- background: "none",
1411
- WebkitTapHighlightColor: "transparent"
1412
- };
1413
- var chevronBtnStyle = {
1414
- pointerEvents: "auto",
1415
- width: 24,
1416
- height: 36,
1417
- display: "inline-flex",
1418
- alignItems: "center",
1419
- justifyContent: "center",
1420
- cursor: "pointer",
1421
- fontSize: 12,
1422
- lineHeight: 1,
1423
- color: "#52525b",
1424
- padding: 0,
1425
- border: "none",
1426
- outline: "none",
1427
- background: "none",
1428
- WebkitTapHighlightColor: "transparent"
1429
- };
1430
- var undoBtnStyle = {
1431
- ...chevronBtnStyle,
1432
- width: 30,
1433
- opacity: 1,
1434
- color: "#18181b"
1435
- };
1436
- var labelStyle2 = {
1437
- fontSize: 10,
1438
- fontWeight: 600,
1439
- color: "#52525b",
1440
- textAlign: "center",
1441
- userSelect: "none",
1442
- padding: "2px 0"
1443
- };
1444
- var panelLayoutStyle = {
1171
+ function itemPivotWorld(item) {
1172
+ const r = normalizeRect(item.bounds);
1173
+ return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
1174
+ }
1175
+ function boundsAabbForRotatedItem(item) {
1176
+ const rot = getItemRotationRad(item);
1177
+ if (Math.abs(rot) < 1e-12 && item.bounds.width >= 0 && item.bounds.height >= 0) {
1178
+ return item.bounds;
1179
+ }
1180
+ const r = normalizeRect(item.bounds);
1181
+ if (Math.abs(rot) < 1e-12) {
1182
+ return r;
1183
+ }
1184
+ const corners = [
1185
+ [0, 0],
1186
+ [r.width, 0],
1187
+ [r.width, r.height],
1188
+ [0, r.height]
1189
+ ];
1190
+ let minX = Infinity;
1191
+ let minY = Infinity;
1192
+ let maxX = -Infinity;
1193
+ let maxY = -Infinity;
1194
+ for (const [lx, ly] of corners) {
1195
+ const p = itemLocalToWorld(lx, ly, item.x, item.y, r.width, r.height, rot);
1196
+ minX = Math.min(minX, p.x);
1197
+ minY = Math.min(minY, p.y);
1198
+ maxX = Math.max(maxX, p.x);
1199
+ maxY = Math.max(maxY, p.y);
1200
+ }
1201
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
1202
+ }
1203
+
1204
+ // src/react/NavMenu.tsx
1205
+ init_rect();
1206
+ var shellLook = {
1445
1207
  display: "flex",
1446
1208
  flexDirection: "column",
1447
- alignItems: "flex-start",
1448
- gap: 4,
1449
- touchAction: "none",
1450
- pointerEvents: "none",
1451
- userSelect: "none"
1209
+ gap: 8,
1210
+ minWidth: 160,
1211
+ padding: "10px 12px",
1212
+ borderRadius: 8,
1213
+ border: "1px solid rgba(0,0,0,0.12)",
1214
+ background: "rgba(255,255,255,0.96)",
1215
+ boxShadow: "0 1px 3px rgba(0,0,0,0.08)",
1216
+ pointerEvents: "auto"
1452
1217
  };
1453
- var innerStyle = {
1454
- pointerEvents: "auto",
1218
+ var labelStyle = {
1455
1219
  display: "flex",
1456
- flexDirection: "row",
1457
- alignItems: "center",
1220
+ flexDirection: "column",
1458
1221
  gap: 4,
1459
- background: BG,
1460
- borderRadius: BORDER_RADIUS,
1461
- border: `1px solid ${BORDER_COLOR}`,
1462
- boxShadow: "0 1px 3px rgba(0,0,0,0.08)",
1463
- padding: 4
1222
+ fontSize: 11,
1223
+ fontWeight: 600,
1224
+ color: "#52525b"
1464
1225
  };
1465
- function NavMenu({
1466
- camera: cameraProp,
1467
- viewportWidth: viewportWidthProp,
1468
- viewportHeight: viewportHeightProp,
1226
+ function normalizeHex(stroke) {
1227
+ if (/^#[0-9A-Fa-f]{6}$/.test(stroke)) return stroke;
1228
+ return "#2563eb";
1229
+ }
1230
+ function isStylableKind(tk) {
1231
+ return tk === "rect" || tk === "ellipse" || tk === "line" || tk === "arrow" || tk === "draw" || tk === "pencil" || tk === "brush" || tk === "marker" || tk === "text";
1232
+ }
1233
+ function VectorSelectionInspector({
1469
1234
  items: itemsProp,
1470
- zoomPercent: zoomPercentProp,
1471
- onZoomIn: onZoomInProp,
1472
- onZoomOut: onZoomOutProp,
1473
- onUndo: onUndoProp,
1474
- onRedo: onRedoProp,
1475
- canUndo: canUndoProp,
1476
- canRedo: canRedoProp,
1477
- onRequestRender: onRequestRenderProp,
1478
- position = "bottom-left",
1235
+ activeToolStyle: activeToolStyleProp,
1236
+ onChange: onChangeProp,
1237
+ position = "top-left",
1479
1238
  inset = 12,
1480
- zIndex = 23,
1239
+ zIndex = 24,
1481
1240
  className,
1482
1241
  style
1483
1242
  }) {
1484
1243
  const ctx = useCanvuChromeContext();
1485
- const camera = cameraProp ?? ctx?.camera ?? null;
1486
- const viewportWidth = viewportWidthProp ?? ctx?.viewportWidth ?? 0;
1487
- const viewportHeight = viewportHeightProp ?? ctx?.viewportHeight ?? 0;
1488
- const items = itemsProp ?? ctx?.items ?? [];
1489
- const zoomPercent = zoomPercentProp ?? ctx?.zoomPercent ?? 100;
1490
- const onZoomIn = onZoomInProp ?? ctx?.onZoomIn ?? noop;
1491
- const onZoomOut = onZoomOutProp ?? ctx?.onZoomOut ?? noop;
1492
- const onUndo = onUndoProp ?? ctx?.onUndo ?? noop;
1493
- const onRedo = onRedoProp ?? ctx?.onRedo ?? noop;
1494
- const canUndo = canUndoProp ?? ctx?.canUndo ?? false;
1495
- const canRedo = canRedoProp ?? ctx?.canRedo ?? false;
1496
- const onRequestRender = onRequestRenderProp ?? ctx?.onRequestRender ?? noop;
1497
- const [expanded, setExpanded] = react.useState(false);
1498
- const svgRef = react.useRef(null);
1499
- const [dragging, setDragging] = react.useState(false);
1500
- const [mouseDown, setMouseDown] = react.useState(false);
1501
- const worldBounds = computeWorldBounds(items);
1502
- const isEmpty = worldBounds.width <= 0 || worldBounds.height <= 0;
1503
- const scale = Math.min(
1504
- (MINIMAP_W - PADDING * 2) / Math.max(worldBounds.width, 1),
1505
- (MINIMAP_H - PADDING * 2) / Math.max(worldBounds.height, 1)
1506
- );
1507
- const originX = worldBounds.x;
1508
- const originY = worldBounds.y;
1509
- const toMinimapX = (wx) => (wx - originX) * scale + PADDING;
1510
- const toMinimapY = (wy) => (wy - originY) * scale + PADDING;
1511
- const toMinimapW = (ww) => ww * scale;
1512
- const toMinimapH = (wh) => wh * scale;
1513
- const viewportWorld = camera ? camera.getVisibleWorldRect(viewportWidth, viewportHeight) : { x: 0, y: 0, width: 0, height: 0 };
1514
- const vpMinimap = {
1515
- x: toMinimapX(viewportWorld.x),
1516
- y: toMinimapY(viewportWorld.y),
1517
- width: toMinimapW(viewportWorld.width),
1518
- height: toMinimapH(viewportWorld.height)
1244
+ const items = itemsProp ?? ctx?.selectedItems ?? [];
1245
+ const activeToolStyle = activeToolStyleProp === void 0 ? ctx?.activeToolStyle ?? null : activeToolStyleProp;
1246
+ const onChange = onChangeProp ?? ctx?.onSelectionStyleChange ?? null;
1247
+ if (!onChange) return null;
1248
+ const shell = {
1249
+ ...getBoardPositionStyle(position, inset, zIndex),
1250
+ ...shellLook,
1251
+ ...style
1519
1252
  };
1520
- const worldFromMinimap = react.useCallback(
1521
- (mx, my) => ({
1522
- worldX: (mx - PADDING) / scale + originX,
1523
- worldY: (my - PADDING) / scale + originY
1524
- }),
1525
- [scale, originX, originY]
1253
+ if (activeToolStyle) {
1254
+ const stroke2 = activeToolStyle.stroke;
1255
+ const strokeWidth2 = activeToolStyle.strokeWidth;
1256
+ const hex2 = normalizeHex(stroke2);
1257
+ const showMarkerOpacity2 = activeToolStyle.toolKind === "marker";
1258
+ const opacityPct2 = Math.round((activeToolStyle.strokeOpacity ?? 1) * 100);
1259
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1260
+ "section",
1261
+ {
1262
+ "data-slot": "vector-selection-inspector",
1263
+ "data-position": position,
1264
+ className,
1265
+ "aria-label": activeToolStyle.label ?? "Estilo da ferramenta",
1266
+ style: shell,
1267
+ children: [
1268
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { style: labelStyle, children: [
1269
+ "Cor",
1270
+ /* @__PURE__ */ jsxRuntime.jsx(
1271
+ "input",
1272
+ {
1273
+ type: "color",
1274
+ value: hex2,
1275
+ onChange: (e) => onChange({
1276
+ stroke: e.target.value,
1277
+ strokeWidth: strokeWidth2,
1278
+ ...activeToolStyle.strokeOpacity != null ? { strokeOpacity: activeToolStyle.strokeOpacity } : {}
1279
+ }),
1280
+ style: {
1281
+ width: "100%",
1282
+ height: 32,
1283
+ padding: 0,
1284
+ border: "none",
1285
+ cursor: "pointer",
1286
+ background: "transparent"
1287
+ }
1288
+ }
1289
+ )
1290
+ ] }),
1291
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { style: labelStyle, children: [
1292
+ "Grossura",
1293
+ /* @__PURE__ */ jsxRuntime.jsx(
1294
+ "input",
1295
+ {
1296
+ type: "range",
1297
+ min: 1,
1298
+ max: 48,
1299
+ value: strokeWidth2,
1300
+ onChange: (e) => onChange({
1301
+ stroke: hex2,
1302
+ strokeWidth: Number(e.target.value),
1303
+ ...activeToolStyle.strokeOpacity != null ? { strokeOpacity: activeToolStyle.strokeOpacity } : {}
1304
+ })
1305
+ }
1306
+ ),
1307
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontWeight: 500, color: "#71717a" }, children: [
1308
+ strokeWidth2,
1309
+ "px"
1310
+ ] })
1311
+ ] }),
1312
+ showMarkerOpacity2 && /* @__PURE__ */ jsxRuntime.jsxs("label", { style: labelStyle, children: [
1313
+ "Opacidade (marcador)",
1314
+ /* @__PURE__ */ jsxRuntime.jsx(
1315
+ "input",
1316
+ {
1317
+ type: "range",
1318
+ min: 10,
1319
+ max: 100,
1320
+ value: opacityPct2,
1321
+ onChange: (e) => {
1322
+ const v = Number(e.target.value) / 100;
1323
+ onChange({
1324
+ stroke: hex2,
1325
+ strokeWidth: strokeWidth2,
1326
+ strokeOpacity: v
1327
+ });
1328
+ }
1329
+ }
1330
+ ),
1331
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontWeight: 500, color: "#71717a" }, children: [
1332
+ opacityPct2,
1333
+ "%"
1334
+ ] })
1335
+ ] })
1336
+ ]
1337
+ }
1338
+ );
1339
+ }
1340
+ const stylable = items.filter(
1341
+ (it) => !it.locked && it.toolKind && isStylableKind(it.toolKind)
1526
1342
  );
1527
- const panTo = react.useCallback(
1528
- (mx, my) => {
1529
- if (!camera) return;
1530
- const { worldX, worldY } = worldFromMinimap(mx, my);
1531
- camera.x = viewportWidth / 2 - worldX * camera.zoom;
1532
- camera.y = viewportHeight / 2 - worldY * camera.zoom;
1533
- onRequestRender();
1534
- },
1535
- [worldFromMinimap, camera, viewportWidth, viewportHeight, onRequestRender]
1343
+ if (stylable.length === 0) return null;
1344
+ const first = stylable[0];
1345
+ if (!first) return null;
1346
+ const allSameStroke = stylable.every(
1347
+ (it) => (it.stroke ?? "#2563eb") === (first.stroke ?? "#2563eb")
1536
1348
  );
1537
- const handlePointerDown = react.useCallback(
1538
- (e) => {
1539
- if (isEmpty) return;
1540
- setDragging(true);
1541
- setMouseDown(true);
1542
- const rect = svgRef.current?.getBoundingClientRect();
1543
- if (!rect) return;
1544
- panTo(e.clientX - rect.left, e.clientY - rect.top);
1545
- svgRef.current?.setPointerCapture(e.pointerId);
1546
- },
1547
- [isEmpty, panTo]
1349
+ const allSameWidth = stylable.every(
1350
+ (it) => (it.strokeWidth ?? 2) === (first.strokeWidth ?? 2)
1548
1351
  );
1549
- const handlePointerMove = react.useCallback(
1550
- (e) => {
1551
- if (!mouseDown || isEmpty) return;
1552
- const rect = svgRef.current?.getBoundingClientRect();
1553
- if (!rect) return;
1554
- panTo(e.clientX - rect.left, e.clientY - rect.top);
1555
- },
1556
- [mouseDown, isEmpty, panTo]
1352
+ const stroke = first.stroke ?? "#2563eb";
1353
+ const strokeWidth = first.strokeWidth ?? 2;
1354
+ const hex = normalizeHex(stroke);
1355
+ const markers = stylable.filter((it) => it.toolKind === "marker");
1356
+ const showMarkerOpacity = markers.length > 0;
1357
+ const firstMarker = markers[0];
1358
+ const allSameMarkerOpacity = markers.length > 0 && markers.every(
1359
+ (it) => (it.strokeOpacity ?? 1) === (firstMarker?.strokeOpacity ?? 1)
1557
1360
  );
1558
- const handlePointerUp = react.useCallback(() => {
1559
- setDragging(false);
1560
- setMouseDown(false);
1561
- }, []);
1562
- const toggleExpanded = react.useCallback(() => {
1563
- setExpanded((v) => !v);
1564
- }, []);
1565
- const anchorStyle = getBoardPositionStyle(position, inset, zIndex);
1361
+ const opacityPct = firstMarker ? Math.round((firstMarker.strokeOpacity ?? 1) * 100) : 100;
1566
1362
  return /* @__PURE__ */ jsxRuntime.jsxs(
1567
- "fieldset",
1363
+ "section",
1568
1364
  {
1569
- "data-slot": "canvu-nav-menu",
1365
+ "data-slot": "vector-selection-inspector",
1570
1366
  "data-position": position,
1571
1367
  className,
1572
- style: {
1573
- ...anchorStyle,
1574
- ...panelLayoutStyle,
1575
- border: "none",
1576
- margin: 0,
1577
- padding: 0,
1578
- minWidth: 0,
1579
- ...style
1580
- },
1581
- "aria-label": "Zoom and minimap controls",
1368
+ "aria-label": "Estilo da sele\xE7\xE3o",
1369
+ style: shell,
1582
1370
  children: [
1583
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: innerStyle, children: [
1584
- /* @__PURE__ */ jsxRuntime.jsx(
1585
- "button",
1586
- {
1587
- type: "button",
1588
- style: btnStyle,
1589
- "aria-label": "Zoom out",
1590
- title: "Zoom out",
1591
- onPointerDown: (e) => {
1592
- if (e.pointerType === "mouse") e.preventDefault();
1593
- },
1594
- onClick: onZoomOut,
1595
- children: "\u2212"
1596
- }
1597
- ),
1598
- /* @__PURE__ */ jsxRuntime.jsxs("span", { style: labelStyle2, "aria-hidden": true, children: [
1599
- zoomPercent,
1600
- "%"
1371
+ stylable.length > 1 && /* @__PURE__ */ jsxRuntime.jsxs("p", { style: { margin: 0, fontSize: 11, color: "#71717a" }, children: [
1372
+ stylable.length,
1373
+ " objetos selecionados"
1374
+ ] }),
1375
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { style: labelStyle, children: [
1376
+ "Cor",
1377
+ !allSameStroke && /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontWeight: 400, color: "#a1a1aa" }, children: [
1378
+ " ",
1379
+ "(valores misturados \u2014 novo valor aplica a todos)"
1601
1380
  ] }),
1602
1381
  /* @__PURE__ */ jsxRuntime.jsx(
1603
- "button",
1604
- {
1605
- type: "button",
1606
- style: btnStyle,
1607
- "aria-label": "Zoom in",
1608
- title: "Zoom in",
1609
- onPointerDown: (e) => {
1610
- if (e.pointerType === "mouse") e.preventDefault();
1611
- },
1612
- onClick: onZoomIn,
1613
- children: "+"
1614
- }
1615
- ),
1616
- /* @__PURE__ */ jsxRuntime.jsx(
1617
- "button",
1382
+ "input",
1618
1383
  {
1619
- type: "button",
1620
- style: undoBtnStyle,
1621
- "aria-label": "Undo",
1622
- title: "Undo",
1623
- disabled: !canUndo,
1624
- onPointerDown: (e) => {
1625
- if (e.pointerType === "mouse") e.preventDefault();
1626
- },
1627
- onClick: onUndo,
1628
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Undo2, { size: 16 })
1384
+ type: "color",
1385
+ value: hex,
1386
+ onChange: (e) => onChange({
1387
+ stroke: e.target.value,
1388
+ strokeWidth
1389
+ }),
1390
+ style: {
1391
+ width: "100%",
1392
+ height: 32,
1393
+ padding: 0,
1394
+ border: "none",
1395
+ cursor: "pointer",
1396
+ background: "transparent"
1397
+ }
1629
1398
  }
1630
- ),
1399
+ )
1400
+ ] }),
1401
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { style: labelStyle, children: [
1402
+ "Grossura",
1403
+ !allSameWidth && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontWeight: 400, color: "#a1a1aa" }, children: " (misturado)" }),
1631
1404
  /* @__PURE__ */ jsxRuntime.jsx(
1632
- "button",
1405
+ "input",
1633
1406
  {
1634
- type: "button",
1635
- style: undoBtnStyle,
1636
- "aria-label": "Redo",
1637
- title: "Redo",
1638
- disabled: !canRedo,
1639
- onPointerDown: (e) => {
1640
- if (e.pointerType === "mouse") e.preventDefault();
1641
- },
1642
- onClick: onRedo,
1643
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Redo2, { size: 16 })
1407
+ type: "range",
1408
+ min: 1,
1409
+ max: 48,
1410
+ value: strokeWidth,
1411
+ onChange: (e) => onChange({
1412
+ stroke: hex,
1413
+ strokeWidth: Number(e.target.value)
1414
+ })
1644
1415
  }
1645
1416
  ),
1417
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontWeight: 500, color: "#71717a" }, children: [
1418
+ strokeWidth,
1419
+ "px"
1420
+ ] })
1421
+ ] }),
1422
+ showMarkerOpacity && firstMarker && /* @__PURE__ */ jsxRuntime.jsxs("label", { style: labelStyle, children: [
1423
+ "Opacidade (marcador)",
1424
+ !allSameMarkerOpacity && markers.length > 1 && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontWeight: 400, color: "#a1a1aa" }, children: " (misturado)" }),
1646
1425
  /* @__PURE__ */ jsxRuntime.jsx(
1647
- "button",
1426
+ "input",
1648
1427
  {
1649
- type: "button",
1650
- style: chevronBtnStyle,
1651
- "aria-label": expanded ? "Hide minimap" : "Show minimap",
1652
- title: expanded ? "Hide minimap" : "Show minimap",
1653
- onPointerDown: (e) => {
1654
- if (e.pointerType === "mouse") e.preventDefault();
1655
- },
1656
- onClick: toggleExpanded,
1657
- children: /* @__PURE__ */ jsxRuntime.jsx(
1658
- "svg",
1659
- {
1660
- width: "12",
1661
- height: "12",
1662
- viewBox: "0 0 12 12",
1663
- style: {
1664
- transform: expanded ? "rotate(180deg)" : "rotate(0deg)",
1665
- transition: "transform 0.15s ease"
1666
- },
1667
- "aria-hidden": true,
1668
- children: /* @__PURE__ */ jsxRuntime.jsx(
1669
- "path",
1670
- {
1671
- d: "M2 4 L6 8 L10 4",
1672
- fill: "none",
1673
- stroke: "currentColor",
1674
- strokeWidth: "1.5",
1675
- strokeLinecap: "round",
1676
- strokeLinejoin: "round"
1677
- }
1678
- )
1679
- }
1680
- )
1428
+ type: "range",
1429
+ min: 10,
1430
+ max: 100,
1431
+ value: opacityPct,
1432
+ onChange: (e) => {
1433
+ const v = Number(e.target.value) / 100;
1434
+ onChange({
1435
+ stroke: hex,
1436
+ strokeWidth,
1437
+ strokeOpacity: v
1438
+ });
1439
+ }
1681
1440
  }
1682
- )
1683
- ] }),
1684
- expanded && /* @__PURE__ */ jsxRuntime.jsx("nav", { "aria-label": "Minimap", children: /* @__PURE__ */ jsxRuntime.jsx(
1685
- "svg",
1686
- {
1687
- ref: svgRef,
1688
- style: {
1689
- width: MINIMAP_W,
1690
- height: MINIMAP_H,
1691
- borderRadius: BORDER_RADIUS,
1692
- border: `1px solid ${BORDER_COLOR}`,
1693
- background: BG,
1694
- boxShadow: "0 1px 3px rgba(0,0,0,0.08)",
1695
- cursor: isEmpty ? "default" : dragging ? "grabbing" : "grab",
1696
- display: "block",
1697
- pointerEvents: "auto"
1698
- },
1699
- onPointerDown: handlePointerDown,
1700
- onPointerMove: handlePointerMove,
1701
- onPointerUp: handlePointerUp,
1702
- onPointerCancel: handlePointerUp,
1703
- children: isEmpty ? null : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1704
- items.map((it) => {
1705
- const b = normalizeRect(boundsAabbForRotatedItem(it));
1706
- return /* @__PURE__ */ jsxRuntime.jsx(
1707
- "rect",
1708
- {
1709
- x: toMinimapX(b.x),
1710
- y: toMinimapY(b.y),
1711
- width: toMinimapW(b.width),
1712
- height: toMinimapH(b.height),
1713
- fill: ITEM_FILL,
1714
- stroke: ITEM_STROKE,
1715
- strokeWidth: 0.5,
1716
- rx: 1
1717
- },
1718
- it.id
1719
- );
1720
- }),
1721
- /* @__PURE__ */ jsxRuntime.jsx(
1722
- "rect",
1723
- {
1724
- x: vpMinimap.x,
1725
- y: vpMinimap.y,
1726
- width: vpMinimap.width,
1727
- height: vpMinimap.height,
1728
- fill: VIEWPORT_FILL,
1729
- stroke: VIEWPORT_STROKE,
1730
- strokeWidth: 1.5,
1731
- rx: 2
1732
- }
1733
- )
1734
- ] })
1735
- }
1736
- ) })
1441
+ ),
1442
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontWeight: 500, color: "#71717a" }, children: [
1443
+ opacityPct,
1444
+ "%"
1445
+ ] })
1446
+ ] })
1737
1447
  ]
1738
1448
  }
1739
1449
  );
1740
1450
  }
1741
- function noop() {
1742
- }
1743
- function computeWorldBounds(items) {
1744
- if (items.length === 0) {
1745
- return { x: 0, y: 0, width: 0, height: 0 };
1746
- }
1747
- let minX = Infinity;
1748
- let minY = Infinity;
1749
- let maxX = -Infinity;
1750
- let maxY = -Infinity;
1751
- for (const it of items) {
1752
- const b = boundsAabbForRotatedItem(it);
1753
- minX = Math.min(minX, b.x);
1754
- minY = Math.min(minY, b.y);
1755
- maxX = Math.max(maxX, b.x + b.width);
1756
- maxY = Math.max(maxY, b.y + b.height);
1451
+ function getBoardPositionStyle(position, inset = 12, zIndex = 40) {
1452
+ const base2 = { position: "absolute", zIndex };
1453
+ switch (position) {
1454
+ case "fill":
1455
+ return { ...base2, inset };
1456
+ case "top-left":
1457
+ case "left-top":
1458
+ return { ...base2, top: inset, left: inset };
1459
+ case "top-center":
1460
+ return {
1461
+ ...base2,
1462
+ top: inset,
1463
+ left: "50%",
1464
+ transform: "translateX(-50%)"
1465
+ };
1466
+ case "top-right":
1467
+ case "right-top":
1468
+ return { ...base2, top: inset, right: inset };
1469
+ case "bottom-left":
1470
+ case "left-bottom":
1471
+ return { ...base2, bottom: inset, left: inset };
1472
+ case "bottom-center":
1473
+ return {
1474
+ ...base2,
1475
+ bottom: inset,
1476
+ left: "50%",
1477
+ transform: "translateX(-50%)"
1478
+ };
1479
+ case "bottom-right":
1480
+ case "right-bottom":
1481
+ return { ...base2, bottom: inset, right: inset };
1482
+ case "left-center":
1483
+ return {
1484
+ ...base2,
1485
+ left: inset,
1486
+ top: "50%",
1487
+ transform: "translateY(-50%)"
1488
+ };
1489
+ case "right-center":
1490
+ return {
1491
+ ...base2,
1492
+ right: inset,
1493
+ top: "50%",
1494
+ transform: "translateY(-50%)"
1495
+ };
1496
+ case "center":
1497
+ return {
1498
+ ...base2,
1499
+ top: "50%",
1500
+ left: "50%",
1501
+ transform: "translate(-50%, -50%)"
1502
+ };
1503
+ default:
1504
+ return base2;
1757
1505
  }
1758
- const pad = Math.max((maxX - minX) * 0.1, (maxY - minY) * 0.1, 40);
1759
- return {
1760
- x: minX - pad,
1761
- y: minY - pad,
1762
- width: maxX - minX + pad * 2,
1763
- height: maxY - minY + pad * 2
1764
- };
1765
1506
  }
1766
-
1767
- // src/image/indexed-db-image-store.ts
1768
- var DB_NAME = "canvu-image-store";
1769
- var DB_VERSION = 1;
1770
- function openDb() {
1771
- return new Promise((resolve, reject) => {
1772
- const req = indexedDB.open(DB_NAME, DB_VERSION);
1773
- req.onupgradeneeded = () => {
1774
- const db = req.result;
1775
- if (!db.objectStoreNames.contains("images")) {
1776
- db.createObjectStore("images");
1777
- }
1778
- if (!db.objectStoreNames.contains("thumbnails")) {
1779
- db.createObjectStore("thumbnails");
1780
- }
1781
- };
1782
- req.onsuccess = () => resolve(req.result);
1783
- req.onerror = () => reject(req.error);
1784
- });
1507
+ var rootStyle = {
1508
+ display: "flex",
1509
+ flexDirection: "column",
1510
+ height: "100%",
1511
+ minHeight: 0,
1512
+ width: "100%"
1513
+ };
1514
+ var headerStyle = {
1515
+ flexShrink: 0,
1516
+ display: "flex",
1517
+ flexWrap: "wrap",
1518
+ alignItems: "center",
1519
+ gap: "0.75rem",
1520
+ padding: "0.75rem 1rem",
1521
+ borderBottom: "1px solid #e4e4e7",
1522
+ background: "#fff",
1523
+ fontSize: "0.875rem"
1524
+ };
1525
+ var bodyStyle = {
1526
+ flex: 1,
1527
+ minHeight: 0,
1528
+ display: "flex",
1529
+ flexDirection: "column"
1530
+ };
1531
+ var mainStyle = {
1532
+ flex: 1,
1533
+ minHeight: 0,
1534
+ position: "relative",
1535
+ display: "flex",
1536
+ flexDirection: "column"
1537
+ };
1538
+ var viewportSurfaceStyle = {
1539
+ flex: 1,
1540
+ minHeight: 0,
1541
+ position: "relative",
1542
+ width: "100%",
1543
+ alignSelf: "stretch",
1544
+ background: "#fff",
1545
+ touchAction: "none"
1546
+ };
1547
+ function mergeStyle(base2, style) {
1548
+ return style ? { ...base2, ...style } : base2;
1785
1549
  }
1786
- function getFromStore(db, storeName, id) {
1787
- return new Promise((resolve, reject) => {
1788
- const tx = db.transaction(storeName, "readonly");
1789
- const req = tx.objectStore(storeName).get(id);
1790
- req.onsuccess = () => resolve(req.result ?? void 0);
1791
- req.onerror = () => reject(req.error);
1792
- });
1550
+ function vectorCanvasSpaceStyle(position, inset, zIndex) {
1551
+ return {
1552
+ ...getBoardPositionStyle(position, inset, zIndex),
1553
+ pointerEvents: "none"
1554
+ };
1793
1555
  }
1794
- function putInStore(db, storeName, id, blob) {
1795
- return new Promise((resolve, reject) => {
1796
- const tx = db.transaction(storeName, "readwrite");
1797
- const req = tx.objectStore(storeName).put(blob, id);
1798
- req.onsuccess = () => resolve();
1799
- req.onerror = () => reject(req.error);
1800
- });
1556
+ function VectorCanvasRoot({
1557
+ children,
1558
+ className,
1559
+ style
1560
+ }) {
1561
+ return /* @__PURE__ */ jsxRuntime.jsx(
1562
+ "div",
1563
+ {
1564
+ "data-slot": "vector-canvas-root",
1565
+ className,
1566
+ style: mergeStyle(rootStyle, style),
1567
+ children
1568
+ }
1569
+ );
1801
1570
  }
1802
- function deleteFromStore(db, storeName, id) {
1803
- return new Promise((resolve, reject) => {
1804
- const tx = db.transaction(storeName, "readwrite");
1805
- const req = tx.objectStore(storeName).delete(id);
1806
- req.onsuccess = () => resolve();
1807
- req.onerror = () => reject(req.error);
1808
- });
1571
+ function VectorCanvasHeader({
1572
+ children,
1573
+ className,
1574
+ style
1575
+ }) {
1576
+ return /* @__PURE__ */ jsxRuntime.jsx(
1577
+ "header",
1578
+ {
1579
+ "data-slot": "vector-canvas-header",
1580
+ className,
1581
+ style: mergeStyle(headerStyle, style),
1582
+ children
1583
+ }
1584
+ );
1809
1585
  }
1810
- function generateBlobId() {
1811
- return crypto.randomUUID();
1586
+ function VectorCanvasBody({
1587
+ children,
1588
+ className,
1589
+ style
1590
+ }) {
1591
+ return /* @__PURE__ */ jsxRuntime.jsx(
1592
+ "div",
1593
+ {
1594
+ "data-slot": "vector-canvas-body",
1595
+ className,
1596
+ style: mergeStyle(bodyStyle, style),
1597
+ children
1598
+ }
1599
+ );
1812
1600
  }
1813
- var IndexedDbImageStore = class {
1814
- dbPromise = null;
1815
- getDb() {
1816
- if (!this.dbPromise) {
1817
- this.dbPromise = openDb();
1601
+ function VectorCanvasMain({
1602
+ children,
1603
+ className,
1604
+ style
1605
+ }) {
1606
+ return /* @__PURE__ */ jsxRuntime.jsx(
1607
+ "div",
1608
+ {
1609
+ "data-slot": "vector-canvas-main",
1610
+ className,
1611
+ style: mergeStyle(mainStyle, style),
1612
+ children
1818
1613
  }
1819
- return this.dbPromise;
1820
- }
1821
- async storeOriginal(blob) {
1822
- const id = generateBlobId();
1823
- const db = await this.getDb();
1824
- await putInStore(db, "images", id, blob);
1825
- return id;
1826
- }
1827
- async getOriginal(id) {
1828
- const db = await this.getDb();
1829
- return getFromStore(db, "images", id);
1830
- }
1831
- async deleteOriginal(id) {
1832
- const db = await this.getDb();
1833
- await deleteFromStore(db, "images", id);
1834
- }
1835
- async storeThumbnail(blob) {
1836
- const id = generateBlobId();
1837
- const db = await this.getDb();
1838
- await putInStore(db, "thumbnails", id, blob);
1839
- return id;
1840
- }
1841
- async getThumbnail(id) {
1842
- const db = await this.getDb();
1843
- return getFromStore(db, "thumbnails", id);
1844
- }
1845
- async deleteThumbnail(id) {
1846
- const db = await this.getDb();
1847
- await deleteFromStore(db, "thumbnails", id);
1848
- }
1849
- };
1850
- function decodeImageToCanvas(blob, maxDimension) {
1851
- return new Promise((resolve, reject) => {
1852
- const img = new Image();
1853
- img.onload = () => {
1854
- const w0 = img.naturalWidth;
1855
- const h0 = img.naturalHeight;
1856
- if (w0 < 1 || h0 < 1) {
1857
- reject(new Error("Image has no dimensions"));
1858
- return;
1859
- }
1860
- const scale = 1;
1861
- const cw = Math.max(1, Math.round(w0 * scale));
1862
- const ch = Math.max(1, Math.round(h0 * scale));
1863
- const canvas = document.createElement("canvas");
1864
- canvas.width = cw;
1865
- canvas.height = ch;
1866
- const ctx = canvas.getContext("2d");
1867
- if (!ctx) {
1868
- reject(new Error("Canvas 2D context unavailable"));
1869
- return;
1870
- }
1871
- ctx.imageSmoothingEnabled = true;
1872
- ctx.imageSmoothingQuality = "high";
1873
- ctx.drawImage(img, 0, 0, cw, ch);
1874
- URL.revokeObjectURL(img.src);
1875
- resolve({ canvas, width: cw, height: ch });
1876
- };
1877
- img.onerror = () => {
1878
- URL.revokeObjectURL(img.src);
1879
- reject(new Error("Could not decode image"));
1880
- };
1881
- img.src = URL.createObjectURL(blob);
1882
- });
1614
+ );
1883
1615
  }
1884
- function canvasToBlob(canvas, mime, quality) {
1885
- return new Promise((resolve, reject) => {
1886
- canvas.toBlob(
1887
- (blob) => {
1888
- if (!blob) {
1889
- reject(new Error("Could not encode blob"));
1890
- return;
1891
- }
1892
- resolve(blob);
1893
- },
1894
- mime,
1895
- quality
1896
- );
1897
- });
1616
+ function VectorCanvasViewportSurface({
1617
+ children,
1618
+ className,
1619
+ style
1620
+ }) {
1621
+ return /* @__PURE__ */ jsxRuntime.jsx(
1622
+ "div",
1623
+ {
1624
+ "data-slot": "vector-canvas-viewport-surface",
1625
+ className,
1626
+ style: mergeStyle(viewportSurfaceStyle, style),
1627
+ children
1628
+ }
1629
+ );
1898
1630
  }
1899
- async function loadImageToStore(file, store) {
1900
- const originalBlob = file;
1901
- const blobId = await store.storeOriginal(originalBlob);
1902
- const { canvas, width, height } = await decodeImageToCanvas(originalBlob);
1903
- const thumbScale = Math.min(1, 256 / Math.max(width, height));
1904
- const tw = Math.max(1, Math.round(width * thumbScale));
1905
- const th = Math.max(1, Math.round(height * thumbScale));
1906
- const thumbCanvas = document.createElement("canvas");
1907
- thumbCanvas.width = tw;
1908
- thumbCanvas.height = th;
1909
- const tCtx = thumbCanvas.getContext("2d");
1910
- if (tCtx) {
1911
- tCtx.imageSmoothingEnabled = true;
1912
- tCtx.imageSmoothingQuality = "high";
1913
- tCtx.drawImage(canvas, 0, 0, tw, th);
1914
- }
1915
- const thumbBlob = await canvasToBlob(
1916
- thumbCanvas,
1917
- file.type === "image/jpeg" || file.type === "image/jpg" ? "image/jpeg" : "image/png",
1918
- file.type.startsWith("image/jpeg") ? 0.85 : void 0
1631
+ function VectorCanvasToolbar({
1632
+ children,
1633
+ className,
1634
+ style,
1635
+ position = "bottom-center",
1636
+ inset = 12,
1637
+ zIndex = 30
1638
+ }) {
1639
+ const base2 = {
1640
+ ...getBoardPositionStyle(position, inset, zIndex),
1641
+ display: "flex",
1642
+ justifyContent: "center",
1643
+ alignItems: "center",
1644
+ maxWidth: "calc(100% - 24px)",
1645
+ pointerEvents: "none"
1646
+ };
1647
+ return /* @__PURE__ */ jsxRuntime.jsx(
1648
+ "div",
1649
+ {
1650
+ "data-slot": "vector-canvas-toolbar",
1651
+ "data-position": position,
1652
+ className,
1653
+ style: mergeStyle(base2, style),
1654
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { pointerEvents: "auto" }, children })
1655
+ }
1656
+ );
1657
+ }
1658
+ function VectorCanvasSpace({
1659
+ children,
1660
+ className,
1661
+ style,
1662
+ position = "top-right",
1663
+ inset = 12,
1664
+ zIndex = 40,
1665
+ contentPointerEvents = "auto"
1666
+ }) {
1667
+ return /* @__PURE__ */ jsxRuntime.jsx(
1668
+ "div",
1669
+ {
1670
+ "data-slot": `vector-canvas-space-${position}`,
1671
+ className,
1672
+ style: mergeStyle(vectorCanvasSpaceStyle(position, inset, zIndex), style),
1673
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { pointerEvents: contentPointerEvents }, children })
1674
+ }
1675
+ );
1676
+ }
1677
+ var VectorCanvas = {
1678
+ Root: VectorCanvasRoot,
1679
+ Header: VectorCanvasHeader,
1680
+ Body: VectorCanvasBody,
1681
+ Main: VectorCanvasMain,
1682
+ ViewportSurface: VectorCanvasViewportSurface,
1683
+ Toolbar: VectorCanvasToolbar,
1684
+ Space: VectorCanvasSpace,
1685
+ NavMenu,
1686
+ SelectionInspector: VectorSelectionInspector
1687
+ };
1688
+ var MINIMAP_W = 180;
1689
+ var MINIMAP_H = 120;
1690
+ var PADDING = 12;
1691
+ var BORDER_RADIUS = 8;
1692
+ var BG = "#ffffff";
1693
+ var BORDER_COLOR = "rgba(0,0,0,0.12)";
1694
+ var ITEM_FILL = "rgba(0,0,0,0.08)";
1695
+ var ITEM_STROKE = "rgba(0,0,0,0.15)";
1696
+ var VIEWPORT_FILL = "rgba(59,130,246,0.08)";
1697
+ var VIEWPORT_STROKE = "#3b82f6";
1698
+ var btnStyle = {
1699
+ pointerEvents: "auto",
1700
+ width: 36,
1701
+ height: 36,
1702
+ display: "inline-flex",
1703
+ alignItems: "center",
1704
+ justifyContent: "center",
1705
+ cursor: "pointer",
1706
+ fontSize: 18,
1707
+ lineHeight: 1,
1708
+ color: "#18181b",
1709
+ padding: 0,
1710
+ border: "none",
1711
+ outline: "none",
1712
+ background: "none",
1713
+ WebkitTapHighlightColor: "transparent"
1714
+ };
1715
+ var chevronBtnStyle = {
1716
+ pointerEvents: "auto",
1717
+ width: 24,
1718
+ height: 36,
1719
+ display: "inline-flex",
1720
+ alignItems: "center",
1721
+ justifyContent: "center",
1722
+ cursor: "pointer",
1723
+ fontSize: 12,
1724
+ lineHeight: 1,
1725
+ color: "#52525b",
1726
+ padding: 0,
1727
+ border: "none",
1728
+ outline: "none",
1729
+ background: "none",
1730
+ WebkitTapHighlightColor: "transparent"
1731
+ };
1732
+ var undoBtnStyle = {
1733
+ ...chevronBtnStyle,
1734
+ width: 30,
1735
+ opacity: 1,
1736
+ color: "#18181b"
1737
+ };
1738
+ var labelStyle2 = {
1739
+ fontSize: 10,
1740
+ fontWeight: 600,
1741
+ color: "#52525b",
1742
+ textAlign: "center",
1743
+ userSelect: "none",
1744
+ padding: "2px 0"
1745
+ };
1746
+ var panelLayoutStyle = {
1747
+ display: "flex",
1748
+ flexDirection: "column",
1749
+ alignItems: "flex-start",
1750
+ gap: 4,
1751
+ touchAction: "none",
1752
+ pointerEvents: "none",
1753
+ userSelect: "none"
1754
+ };
1755
+ var innerStyle = {
1756
+ pointerEvents: "auto",
1757
+ display: "flex",
1758
+ flexDirection: "row",
1759
+ alignItems: "center",
1760
+ gap: 4,
1761
+ background: BG,
1762
+ borderRadius: BORDER_RADIUS,
1763
+ border: `1px solid ${BORDER_COLOR}`,
1764
+ boxShadow: "0 1px 3px rgba(0,0,0,0.08)",
1765
+ padding: 4
1766
+ };
1767
+ function NavMenu({
1768
+ camera: cameraProp,
1769
+ viewportWidth: viewportWidthProp,
1770
+ viewportHeight: viewportHeightProp,
1771
+ items: itemsProp,
1772
+ zoomPercent: zoomPercentProp,
1773
+ onZoomIn: onZoomInProp,
1774
+ onZoomOut: onZoomOutProp,
1775
+ onUndo: onUndoProp,
1776
+ onRedo: onRedoProp,
1777
+ canUndo: canUndoProp,
1778
+ canRedo: canRedoProp,
1779
+ onRequestRender: onRequestRenderProp,
1780
+ position = "bottom-left",
1781
+ inset = 12,
1782
+ zIndex = 23,
1783
+ className,
1784
+ style
1785
+ }) {
1786
+ const ctx = useCanvuChromeContext();
1787
+ const camera = cameraProp ?? ctx?.camera ?? null;
1788
+ const viewportWidth = viewportWidthProp ?? ctx?.viewportWidth ?? 0;
1789
+ const viewportHeight = viewportHeightProp ?? ctx?.viewportHeight ?? 0;
1790
+ const items = itemsProp ?? ctx?.items ?? [];
1791
+ const zoomPercent = zoomPercentProp ?? ctx?.zoomPercent ?? 100;
1792
+ const onZoomIn = onZoomInProp ?? ctx?.onZoomIn ?? noop;
1793
+ const onZoomOut = onZoomOutProp ?? ctx?.onZoomOut ?? noop;
1794
+ const onUndo = onUndoProp ?? ctx?.onUndo ?? noop;
1795
+ const onRedo = onRedoProp ?? ctx?.onRedo ?? noop;
1796
+ const canUndo = canUndoProp ?? ctx?.canUndo ?? false;
1797
+ const canRedo = canRedoProp ?? ctx?.canRedo ?? false;
1798
+ const onRequestRender = onRequestRenderProp ?? ctx?.onRequestRender ?? noop;
1799
+ const [expanded, setExpanded] = react.useState(false);
1800
+ const svgRef = react.useRef(null);
1801
+ const [dragging, setDragging] = react.useState(false);
1802
+ const [mouseDown, setMouseDown] = react.useState(false);
1803
+ const worldBounds = computeWorldBounds(items);
1804
+ const isEmpty = worldBounds.width <= 0 || worldBounds.height <= 0;
1805
+ const scale = Math.min(
1806
+ (MINIMAP_W - PADDING * 2) / Math.max(worldBounds.width, 1),
1807
+ (MINIMAP_H - PADDING * 2) / Math.max(worldBounds.height, 1)
1808
+ );
1809
+ const originX = worldBounds.x;
1810
+ const originY = worldBounds.y;
1811
+ const toMinimapX = (wx) => (wx - originX) * scale + PADDING;
1812
+ const toMinimapY = (wy) => (wy - originY) * scale + PADDING;
1813
+ const toMinimapW = (ww) => ww * scale;
1814
+ const toMinimapH = (wh) => wh * scale;
1815
+ const viewportWorld = camera ? camera.getVisibleWorldRect(viewportWidth, viewportHeight) : { x: 0, y: 0, width: 0, height: 0 };
1816
+ const vpMinimap = {
1817
+ x: toMinimapX(viewportWorld.x),
1818
+ y: toMinimapY(viewportWorld.y),
1819
+ width: toMinimapW(viewportWorld.width),
1820
+ height: toMinimapH(viewportWorld.height)
1821
+ };
1822
+ const worldFromMinimap = react.useCallback(
1823
+ (mx, my) => ({
1824
+ worldX: (mx - PADDING) / scale + originX,
1825
+ worldY: (my - PADDING) / scale + originY
1826
+ }),
1827
+ [scale, originX, originY]
1828
+ );
1829
+ const panTo = react.useCallback(
1830
+ (mx, my) => {
1831
+ if (!camera) return;
1832
+ const { worldX, worldY } = worldFromMinimap(mx, my);
1833
+ camera.x = viewportWidth / 2 - worldX * camera.zoom;
1834
+ camera.y = viewportHeight / 2 - worldY * camera.zoom;
1835
+ onRequestRender();
1836
+ },
1837
+ [worldFromMinimap, camera, viewportWidth, viewportHeight, onRequestRender]
1838
+ );
1839
+ const handlePointerDown = react.useCallback(
1840
+ (e) => {
1841
+ if (isEmpty) return;
1842
+ setDragging(true);
1843
+ setMouseDown(true);
1844
+ const rect = svgRef.current?.getBoundingClientRect();
1845
+ if (!rect) return;
1846
+ panTo(e.clientX - rect.left, e.clientY - rect.top);
1847
+ svgRef.current?.setPointerCapture(e.pointerId);
1848
+ },
1849
+ [isEmpty, panTo]
1850
+ );
1851
+ const handlePointerMove = react.useCallback(
1852
+ (e) => {
1853
+ if (!mouseDown || isEmpty) return;
1854
+ const rect = svgRef.current?.getBoundingClientRect();
1855
+ if (!rect) return;
1856
+ panTo(e.clientX - rect.left, e.clientY - rect.top);
1857
+ },
1858
+ [mouseDown, isEmpty, panTo]
1859
+ );
1860
+ const handlePointerUp = react.useCallback(() => {
1861
+ setDragging(false);
1862
+ setMouseDown(false);
1863
+ }, []);
1864
+ const toggleExpanded = react.useCallback(() => {
1865
+ setExpanded((v) => !v);
1866
+ }, []);
1867
+ const anchorStyle = getBoardPositionStyle(position, inset, zIndex);
1868
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1869
+ "fieldset",
1870
+ {
1871
+ "data-slot": "canvu-nav-menu",
1872
+ "data-position": position,
1873
+ className,
1874
+ style: {
1875
+ ...anchorStyle,
1876
+ ...panelLayoutStyle,
1877
+ border: "none",
1878
+ margin: 0,
1879
+ padding: 0,
1880
+ minWidth: 0,
1881
+ ...style
1882
+ },
1883
+ "aria-label": "Zoom and minimap controls",
1884
+ children: [
1885
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: innerStyle, children: [
1886
+ /* @__PURE__ */ jsxRuntime.jsx(
1887
+ "button",
1888
+ {
1889
+ type: "button",
1890
+ style: btnStyle,
1891
+ "aria-label": "Zoom out",
1892
+ title: "Zoom out",
1893
+ onPointerDown: (e) => {
1894
+ if (e.pointerType === "mouse") e.preventDefault();
1895
+ },
1896
+ onClick: onZoomOut,
1897
+ children: "\u2212"
1898
+ }
1899
+ ),
1900
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: labelStyle2, "aria-hidden": true, children: [
1901
+ zoomPercent,
1902
+ "%"
1903
+ ] }),
1904
+ /* @__PURE__ */ jsxRuntime.jsx(
1905
+ "button",
1906
+ {
1907
+ type: "button",
1908
+ style: btnStyle,
1909
+ "aria-label": "Zoom in",
1910
+ title: "Zoom in",
1911
+ onPointerDown: (e) => {
1912
+ if (e.pointerType === "mouse") e.preventDefault();
1913
+ },
1914
+ onClick: onZoomIn,
1915
+ children: "+"
1916
+ }
1917
+ ),
1918
+ /* @__PURE__ */ jsxRuntime.jsx(
1919
+ "button",
1920
+ {
1921
+ type: "button",
1922
+ style: undoBtnStyle,
1923
+ "aria-label": "Undo",
1924
+ title: "Undo",
1925
+ disabled: !canUndo,
1926
+ onPointerDown: (e) => {
1927
+ if (e.pointerType === "mouse") e.preventDefault();
1928
+ },
1929
+ onClick: onUndo,
1930
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Undo2, { size: 16 })
1931
+ }
1932
+ ),
1933
+ /* @__PURE__ */ jsxRuntime.jsx(
1934
+ "button",
1935
+ {
1936
+ type: "button",
1937
+ style: undoBtnStyle,
1938
+ "aria-label": "Redo",
1939
+ title: "Redo",
1940
+ disabled: !canRedo,
1941
+ onPointerDown: (e) => {
1942
+ if (e.pointerType === "mouse") e.preventDefault();
1943
+ },
1944
+ onClick: onRedo,
1945
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Redo2, { size: 16 })
1946
+ }
1947
+ ),
1948
+ /* @__PURE__ */ jsxRuntime.jsx(
1949
+ "button",
1950
+ {
1951
+ type: "button",
1952
+ style: chevronBtnStyle,
1953
+ "aria-label": expanded ? "Hide minimap" : "Show minimap",
1954
+ title: expanded ? "Hide minimap" : "Show minimap",
1955
+ onPointerDown: (e) => {
1956
+ if (e.pointerType === "mouse") e.preventDefault();
1957
+ },
1958
+ onClick: toggleExpanded,
1959
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1960
+ "svg",
1961
+ {
1962
+ width: "12",
1963
+ height: "12",
1964
+ viewBox: "0 0 12 12",
1965
+ style: {
1966
+ transform: expanded ? "rotate(180deg)" : "rotate(0deg)",
1967
+ transition: "transform 0.15s ease"
1968
+ },
1969
+ "aria-hidden": true,
1970
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1971
+ "path",
1972
+ {
1973
+ d: "M2 4 L6 8 L10 4",
1974
+ fill: "none",
1975
+ stroke: "currentColor",
1976
+ strokeWidth: "1.5",
1977
+ strokeLinecap: "round",
1978
+ strokeLinejoin: "round"
1979
+ }
1980
+ )
1981
+ }
1982
+ )
1983
+ }
1984
+ )
1985
+ ] }),
1986
+ expanded && /* @__PURE__ */ jsxRuntime.jsx("nav", { "aria-label": "Minimap", children: /* @__PURE__ */ jsxRuntime.jsx(
1987
+ "svg",
1988
+ {
1989
+ ref: svgRef,
1990
+ style: {
1991
+ width: MINIMAP_W,
1992
+ height: MINIMAP_H,
1993
+ borderRadius: BORDER_RADIUS,
1994
+ border: `1px solid ${BORDER_COLOR}`,
1995
+ background: BG,
1996
+ boxShadow: "0 1px 3px rgba(0,0,0,0.08)",
1997
+ cursor: isEmpty ? "default" : dragging ? "grabbing" : "grab",
1998
+ display: "block",
1999
+ pointerEvents: "auto"
2000
+ },
2001
+ onPointerDown: handlePointerDown,
2002
+ onPointerMove: handlePointerMove,
2003
+ onPointerUp: handlePointerUp,
2004
+ onPointerCancel: handlePointerUp,
2005
+ children: isEmpty ? null : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2006
+ items.map((it) => {
2007
+ const b = normalizeRect(boundsAabbForRotatedItem(it));
2008
+ return /* @__PURE__ */ jsxRuntime.jsx(
2009
+ "rect",
2010
+ {
2011
+ x: toMinimapX(b.x),
2012
+ y: toMinimapY(b.y),
2013
+ width: toMinimapW(b.width),
2014
+ height: toMinimapH(b.height),
2015
+ fill: ITEM_FILL,
2016
+ stroke: ITEM_STROKE,
2017
+ strokeWidth: 0.5,
2018
+ rx: 1
2019
+ },
2020
+ it.id
2021
+ );
2022
+ }),
2023
+ /* @__PURE__ */ jsxRuntime.jsx(
2024
+ "rect",
2025
+ {
2026
+ x: vpMinimap.x,
2027
+ y: vpMinimap.y,
2028
+ width: vpMinimap.width,
2029
+ height: vpMinimap.height,
2030
+ fill: VIEWPORT_FILL,
2031
+ stroke: VIEWPORT_STROKE,
2032
+ strokeWidth: 1.5,
2033
+ rx: 2
2034
+ }
2035
+ )
2036
+ ] })
2037
+ }
2038
+ ) })
2039
+ ]
2040
+ }
1919
2041
  );
1920
- const thumbnailBlobId = await store.storeThumbnail(thumbBlob);
1921
- return { blobId, thumbnailBlobId, width, height };
1922
2042
  }
1923
- async function createBlobUrlFromStore(store, blobId) {
1924
- const blob = await store.getOriginal(blobId);
1925
- if (!blob) return null;
1926
- return URL.createObjectURL(blob);
2043
+ function noop() {
1927
2044
  }
1928
- async function createThumbnailBlobUrlFromStore(store, thumbnailBlobId) {
1929
- const blob = await store.getThumbnail(thumbnailBlobId);
1930
- if (!blob) return null;
1931
- return URL.createObjectURL(blob);
2045
+ function computeWorldBounds(items) {
2046
+ if (items.length === 0) {
2047
+ return { x: 0, y: 0, width: 0, height: 0 };
2048
+ }
2049
+ let minX = Infinity;
2050
+ let minY = Infinity;
2051
+ let maxX = -Infinity;
2052
+ let maxY = -Infinity;
2053
+ for (const it of items) {
2054
+ const b = boundsAabbForRotatedItem(it);
2055
+ minX = Math.min(minX, b.x);
2056
+ minY = Math.min(minY, b.y);
2057
+ maxX = Math.max(maxX, b.x + b.width);
2058
+ maxY = Math.max(maxY, b.y + b.height);
2059
+ }
2060
+ const pad = Math.max((maxX - minX) * 0.1, (maxY - minY) * 0.1, 40);
2061
+ return {
2062
+ x: minX - pad,
2063
+ y: minY - pad,
2064
+ width: maxX - minX + pad * 2,
2065
+ height: maxY - minY + pad * 2
2066
+ };
1932
2067
  }
1933
2068
 
1934
2069
  // src/react/persistence/indexed-db-adapter.ts
@@ -3260,83 +3395,6 @@ var Camera2D = class {
3260
3395
  }
3261
3396
  };
3262
3397
 
3263
- // src/image/pdf-loader.ts
3264
- var pdfjsPromise = null;
3265
- function getPdfJs() {
3266
- if (!pdfjsPromise) {
3267
- pdfjsPromise = import('pdfjs-dist').then((mod) => {
3268
- const workerSrc = new URL(
3269
- "pdfjs-dist/build/pdf.worker.min.mjs",
3270
- (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('react.cjs', document.baseURI).href))
3271
- ).toString();
3272
- mod.GlobalWorkerOptions.workerSrc = workerSrc;
3273
- return mod;
3274
- });
3275
- }
3276
- return pdfjsPromise;
3277
- }
3278
- async function renderPageToCanvas(page, scale) {
3279
- const raw = page.getViewport({ scale: 1 });
3280
- const adjustedScale = Math.round(raw.width * scale) / raw.width;
3281
- const viewport = page.getViewport({ scale: adjustedScale });
3282
- const w = Math.round(viewport.width);
3283
- const h = Math.round(viewport.height);
3284
- const canvas = document.createElement("canvas");
3285
- canvas.width = w;
3286
- canvas.height = h;
3287
- const ctx = canvas.getContext("2d");
3288
- if (!ctx) throw new Error("Canvas 2D context unavailable");
3289
- ctx.imageSmoothingEnabled = true;
3290
- ctx.imageSmoothingQuality = "high";
3291
- await page.render({ canvas, viewport }).promise;
3292
- return { canvas, width: w, height: h };
3293
- }
3294
- function canvasToBlob2(canvas, mime, quality) {
3295
- return new Promise((resolve, reject) => {
3296
- canvas.toBlob(
3297
- (blob) => {
3298
- if (!blob) {
3299
- reject(new Error("Could not encode blob"));
3300
- return;
3301
- }
3302
- resolve(blob);
3303
- },
3304
- mime,
3305
- quality
3306
- );
3307
- });
3308
- }
3309
- async function loadPdfToStore(file, store, options) {
3310
- const scale = 1.5;
3311
- const pdfjs = await getPdfJs();
3312
- const arrayBuffer = await file.arrayBuffer();
3313
- const pdf = await pdfjs.getDocument({ data: arrayBuffer }).promise;
3314
- const results = [];
3315
- for (let i = 1; i <= pdf.numPages; i++) {
3316
- const page = await pdf.getPage(i);
3317
- const { canvas, width, height } = await renderPageToCanvas(page, scale);
3318
- const mime = "image/png";
3319
- const pageBlob = await canvasToBlob2(canvas, mime);
3320
- const blobId = await store.storeOriginal(pageBlob);
3321
- const thumbScale = Math.min(1, 256 / Math.max(width, height));
3322
- const tw = Math.max(1, Math.round(width * thumbScale));
3323
- const th = Math.max(1, Math.round(height * thumbScale));
3324
- const thumbCanvas = document.createElement("canvas");
3325
- thumbCanvas.width = tw;
3326
- thumbCanvas.height = th;
3327
- const tCtx = thumbCanvas.getContext("2d");
3328
- if (tCtx) {
3329
- tCtx.imageSmoothingEnabled = true;
3330
- tCtx.imageSmoothingQuality = "high";
3331
- tCtx.drawImage(canvas, 0, 0, tw, th);
3332
- }
3333
- const thumbBlob = await canvasToBlob2(thumbCanvas, mime);
3334
- const thumbnailBlobId = await store.storeThumbnail(thumbBlob);
3335
- results.push({ blobId, thumbnailBlobId, width, height, pageNumber: i });
3336
- }
3337
- return results;
3338
- }
3339
-
3340
3398
  // src/input/apple-pencil-navigation.ts
3341
3399
  var DRAWING_LIKE_TOOLS = /* @__PURE__ */ new Set([
3342
3400
  "draw",
@@ -3535,7 +3593,7 @@ function attachViewportInput(options) {
3535
3593
  if (e.ctrlKey || e.metaKey) {
3536
3594
  e.preventDefault();
3537
3595
  const dy = wheelDeltaYPixels(e);
3538
- const normDy = dy < 20 ? dy * 12 : dy;
3596
+ const normDy = Math.abs(dy) < 20 ? dy * 12 : dy;
3539
3597
  const factor = Math.exp(-normDy * wheelZoomSensitivity);
3540
3598
  const rect = element.getBoundingClientRect();
3541
3599
  camera.setZoom(camera.zoom * factor, {
@@ -4689,7 +4747,42 @@ init_shape_builders();
4689
4747
 
4690
4748
  // src/react/InteractionOverlay.tsx
4691
4749
  init_rect();
4692
- init_freehand_path();
4750
+
4751
+ // src/scene/freehand-path.ts
4752
+ function smoothFreehandPointsToPathD(points) {
4753
+ const n = points.length;
4754
+ if (n === 0) return "";
4755
+ if (n === 1) {
4756
+ const p = points[0];
4757
+ if (!p) return "";
4758
+ return `M ${p.x} ${p.y}`;
4759
+ }
4760
+ if (n === 2) {
4761
+ const a = points[0];
4762
+ const b = points[1];
4763
+ if (!a || !b) return "";
4764
+ return `M ${a.x} ${a.y} L ${b.x} ${b.y}`;
4765
+ }
4766
+ const p0 = points[0];
4767
+ if (!p0) return "";
4768
+ let d = `M ${p0.x} ${p0.y}`;
4769
+ let i = 1;
4770
+ for (; i < n - 2; i++) {
4771
+ const pi = points[i];
4772
+ const pi1 = points[i + 1];
4773
+ if (!pi || !pi1) continue;
4774
+ const xc = (pi.x + pi1.x) / 2;
4775
+ const yc = (pi.y + pi1.y) / 2;
4776
+ d += ` Q ${pi.x} ${pi.y} ${xc} ${yc}`;
4777
+ }
4778
+ const pLast = points[i];
4779
+ const pEnd = points[i + 1];
4780
+ if (!pLast || !pEnd) return d;
4781
+ d += ` Q ${pLast.x} ${pLast.y} ${pEnd.x} ${pEnd.y}`;
4782
+ return d;
4783
+ }
4784
+
4785
+ // src/react/InteractionOverlay.tsx
4693
4786
  init_shape_builders();
4694
4787
  var HANDLE_ORDER = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
4695
4788
  var ERASER_TINT = "#cbd5e1";
@@ -4798,6 +4891,7 @@ function InteractionOverlay({
4798
4891
  eraserTrail = [],
4799
4892
  laserTrail = [],
4800
4893
  eraserPreviewItems = [],
4894
+ marqueeCandidateItems = [],
4801
4895
  previewStrokeStyle
4802
4896
  }) {
4803
4897
  const z = camera.zoom;
@@ -5046,6 +5140,33 @@ function InteractionOverlay({
5046
5140
  );
5047
5141
  }) });
5048
5142
  }
5143
+ let marqueeCandidates = null;
5144
+ if (marqueeCandidateItems.length > 0) {
5145
+ marqueeCandidates = /* @__PURE__ */ jsxRuntime.jsx("g", { children: marqueeCandidateItems.map((it) => {
5146
+ const b = normalizeRect(it.bounds);
5147
+ return /* @__PURE__ */ jsxRuntime.jsx(
5148
+ "g",
5149
+ {
5150
+ transform: formatItemPlacementTransform(it),
5151
+ children: /* @__PURE__ */ jsxRuntime.jsx(
5152
+ "rect",
5153
+ {
5154
+ x: 0,
5155
+ y: 0,
5156
+ width: b.width,
5157
+ height: b.height,
5158
+ fill: "rgba(59, 130, 246, 0.10)",
5159
+ stroke: "#3b82f6",
5160
+ strokeWidth: overlayStrokePx,
5161
+ strokeDasharray: dashPattern,
5162
+ vectorEffect: "non-scaling-stroke"
5163
+ }
5164
+ )
5165
+ },
5166
+ `marquee-cand-${it.id}`
5167
+ );
5168
+ }) });
5169
+ }
5049
5170
  let eraserTrailOverlay = null;
5050
5171
  if (eraserTrail.length >= 1) {
5051
5172
  const now = Date.now();
@@ -5146,6 +5267,7 @@ function InteractionOverlay({
5146
5267
  width: "100%",
5147
5268
  height: "100%",
5148
5269
  children: /* @__PURE__ */ jsxRuntime.jsxs("g", { transform: rootTransform, children: [
5270
+ marqueeCandidates,
5149
5271
  preview,
5150
5272
  laserTrailOverlay,
5151
5273
  eraserTrailOverlay,
@@ -5155,7 +5277,6 @@ function InteractionOverlay({
5155
5277
  }
5156
5278
  );
5157
5279
  }
5158
- init_freehand_path();
5159
5280
 
5160
5281
  // src/react/presence/peer-color.ts
5161
5282
  function defaultPresenceColorForId(id) {
@@ -5305,6 +5426,17 @@ function PresenceRemoteLayer({
5305
5426
  );
5306
5427
  }
5307
5428
 
5429
+ // src/react/stable-selection.ts
5430
+ function shallowEqualStringArray(a, b) {
5431
+ if (a === b) return true;
5432
+ if (!a || !b) return a === b;
5433
+ if (a.length !== b.length) return false;
5434
+ for (let i = 0; i < a.length; i++) {
5435
+ if (a[i] !== b[i]) return false;
5436
+ }
5437
+ return true;
5438
+ }
5439
+
5308
5440
  // src/react/TextEditOverlay.tsx
5309
5441
  init_rect();
5310
5442
  init_text_svg();
@@ -5474,7 +5606,8 @@ var visuallyHidden = {
5474
5606
  whiteSpace: "nowrap",
5475
5607
  border: 0
5476
5608
  };
5477
- var VIEWPORT_ZOOM_STEP = 1.15;
5609
+ var VIEWPORT_ZOOM_IN_STEP = 1.15;
5610
+ var VIEWPORT_ZOOM_OUT_STEP = 1 / 1.15;
5478
5611
  function replaceItem(items, id, next) {
5479
5612
  return items.map((it) => it.id === id ? next : it);
5480
5613
  }
@@ -5678,22 +5811,8 @@ function appendInterpolatedPoints(points, next, maxStepWorld) {
5678
5811
  if (!last) return [...points, next];
5679
5812
  const dx = next.x - last.x;
5680
5813
  const dy = next.y - last.y;
5681
- const dist = Math.hypot(dx, dy);
5682
- if (dist < 1e-6) return points;
5683
- const safeStep = Math.max(maxStepWorld, 1e-4);
5684
- const steps = Math.max(1, Math.ceil(dist / safeStep));
5685
- if (steps === 1) return [...points, next];
5686
- const out = [...points];
5687
- for (let i = 1; i <= steps; i++) {
5688
- const t = i / steps;
5689
- const pressure = last.pressure != null || next.pressure != null ? (last.pressure ?? next.pressure ?? 0.5) + ((next.pressure ?? last.pressure ?? 0.5) - (last.pressure ?? next.pressure ?? 0.5)) * t : void 0;
5690
- out.push({
5691
- x: last.x + dx * t,
5692
- y: last.y + dy * t,
5693
- ...pressure != null ? { pressure } : {}
5694
- });
5695
- }
5696
- return out;
5814
+ if (dx * dx + dy * dy < 1e-12) return points;
5815
+ return [...points, next];
5697
5816
  }
5698
5817
  function pointerSampleToWorldPoint(screenToWorldFn, clientX, clientY, pressure) {
5699
5818
  const { worldX, worldY } = screenToWorldFn(clientX, clientY);
@@ -5725,7 +5844,7 @@ function isLikelyStylusTouch(touch) {
5725
5844
  }
5726
5845
  function appendPointerEventSamplesToStrokePoints(points, ev, zoom, screenToWorldFn) {
5727
5846
  let interpolated = points;
5728
- const maxStepWorld = ev.pointerType === "pen" ? 0.35 / zoom : 1 / zoom;
5847
+ ev.pointerType === "pen" ? 0.35 / zoom : 1 / zoom;
5729
5848
  for (const sample of pointerEventSamples(ev)) {
5730
5849
  const nextPoint = pointerSampleToWorldPoint(
5731
5850
  screenToWorldFn,
@@ -5733,7 +5852,7 @@ function appendPointerEventSamplesToStrokePoints(points, ev, zoom, screenToWorld
5733
5852
  sample.clientY,
5734
5853
  sample.pointerType === "pen" ? sample.pressure : void 0
5735
5854
  );
5736
- interpolated = appendInterpolatedPoints(interpolated, nextPoint, maxStepWorld);
5855
+ interpolated = appendInterpolatedPoints(interpolated, nextPoint);
5737
5856
  }
5738
5857
  return interpolated;
5739
5858
  }
@@ -5744,7 +5863,7 @@ function appendTouchToStrokePoints(points, touch, zoom, screenToWorldFn) {
5744
5863
  touch.clientY,
5745
5864
  touchPressure(touch)
5746
5865
  );
5747
- return appendInterpolatedPoints(points, nextPoint, 0.35 / zoom);
5866
+ return appendInterpolatedPoints(points, nextPoint);
5748
5867
  }
5749
5868
  var VectorViewport = react.forwardRef(
5750
5869
  function VectorViewport2({
@@ -5766,6 +5885,7 @@ var VectorViewport = react.forwardRef(
5766
5885
  onCameraChange: consumerOnCameraChange,
5767
5886
  customPlacement: consumerCustomPlacement,
5768
5887
  customPlacements: consumerCustomPlacements,
5888
+ assetStore,
5769
5889
  toolLocked = false,
5770
5890
  autoResetToolTo = "select",
5771
5891
  onToolChangeRequest,
@@ -5936,6 +6056,8 @@ var VectorViewport = react.forwardRef(
5936
6056
  const itemsRef = react.useRef(items);
5937
6057
  const onWorldPointerDownRef = react.useRef(onWorldPointerDown);
5938
6058
  const onItemsChangeRef = react.useRef(onItemsChange);
6059
+ const assetStoreRef = react.useRef(assetStore);
6060
+ assetStoreRef.current = assetStore;
5939
6061
  const customPlacementRef = react.useRef(customPlacement);
5940
6062
  customPlacementRef.current = customPlacement;
5941
6063
  const dragStateRef = react.useRef({ kind: "idle" });
@@ -6038,6 +6160,9 @@ var VectorViewport = react.forwardRef(
6038
6160
  const [laserTrail, setLaserTrail] = react.useState([]);
6039
6161
  const [eraserPreviewIds, setEraserPreviewIds] = react.useState([]);
6040
6162
  const eraserPreviewIdsRef = react.useRef(/* @__PURE__ */ new Set());
6163
+ const [marqueeCandidateIds, setMarqueeCandidateIds] = react.useState([]);
6164
+ const marqueeCandidateIdsRef = react.useRef([]);
6165
+ marqueeCandidateIdsRef.current = marqueeCandidateIds;
6041
6166
  const resolvedSceneItems = react.useMemo(() => {
6042
6167
  if (eraserPreviewIds.length === 0) return resolvedItems;
6043
6168
  const hiddenIds = new Set(eraserPreviewIds);
@@ -6552,14 +6677,14 @@ var VectorViewport = react.forwardRef(
6552
6677
  handled = true;
6553
6678
  } else if (e.key === "+" || e.key === "=") {
6554
6679
  const rect = el.getBoundingClientRect();
6555
- camera.setZoom(camera.zoom * VIEWPORT_ZOOM_STEP, {
6680
+ camera.setZoom(camera.zoom * VIEWPORT_ZOOM_IN_STEP, {
6556
6681
  x: rect.width / 2,
6557
6682
  y: rect.height / 2
6558
6683
  });
6559
6684
  handled = true;
6560
6685
  } else if (e.key === "-" || e.key === "_") {
6561
6686
  const rect = el.getBoundingClientRect();
6562
- camera.setZoom(camera.zoom / VIEWPORT_ZOOM_STEP, {
6687
+ camera.setZoom(camera.zoom * VIEWPORT_ZOOM_OUT_STEP, {
6563
6688
  x: rect.width / 2,
6564
6689
  y: rect.height / 2
6565
6690
  });
@@ -6828,7 +6953,7 @@ var VectorViewport = react.forwardRef(
6828
6953
  const el = sceneContainerRef.current;
6829
6954
  if (!camera || !el) return;
6830
6955
  const rect = el.getBoundingClientRect();
6831
- camera.setZoom(camera.zoom * VIEWPORT_ZOOM_STEP, {
6956
+ camera.setZoom(camera.zoom * VIEWPORT_ZOOM_IN_STEP, {
6832
6957
  x: rect.width / 2,
6833
6958
  y: rect.height / 2
6834
6959
  });
@@ -6839,7 +6964,7 @@ var VectorViewport = react.forwardRef(
6839
6964
  const el = sceneContainerRef.current;
6840
6965
  if (!camera || !el) return;
6841
6966
  const rect = el.getBoundingClientRect();
6842
- camera.setZoom(camera.zoom / VIEWPORT_ZOOM_STEP, {
6967
+ camera.setZoom(camera.zoom * VIEWPORT_ZOOM_OUT_STEP, {
6843
6968
  x: rect.width / 2,
6844
6969
  y: rect.height / 2
6845
6970
  });
@@ -6857,13 +6982,13 @@ var VectorViewport = react.forwardRef(
6857
6982
  if (isDefaultMarkerToolStyle(current)) {
6858
6983
  setCurrentStrokeStyle({
6859
6984
  stroke: DEFAULT_STROKE_STYLE.stroke,
6860
- strokeWidth: toolId === "draw" ? 3 : DEFAULT_STROKE_STYLE.strokeWidth
6985
+ strokeWidth: toolId === "draw" ? 10 : DEFAULT_STROKE_STYLE.strokeWidth
6861
6986
  });
6862
6987
  return;
6863
6988
  }
6864
6989
  setCurrentStrokeStyle({
6865
6990
  stroke: current.stroke,
6866
- strokeWidth: toolId === "draw" ? 3 : current.strokeWidth
6991
+ strokeWidth: toolId === "draw" ? 10 : current.strokeWidth
6867
6992
  });
6868
6993
  }, [setCurrentStrokeStyle, toolId]);
6869
6994
  react.useEffect(() => {
@@ -6965,97 +7090,49 @@ var VectorViewport = react.forwardRef(
6965
7090
  if (!change || files.length === 0) return;
6966
7091
  const store = imageStoreRef.current;
6967
7092
  if (!store) return;
6968
- const { buildRasterImageChildrenSvg: buildRasterImageChildrenSvg2 } = await Promise.resolve().then(() => (init_shape_builders(), shape_builders_exports));
6969
- const newItems = [];
6970
- const gapWorld = 16;
6971
- const stepWorld = 48;
6972
- let placed = 0;
6973
- for (const file of files) {
6974
- try {
6975
- if (file.type === "application/pdf") {
6976
- const estW = 1241;
6977
- const estH = 1754;
6978
- const estPages = 10;
6979
- const skels = [];
6980
- const skelIds = [];
6981
- for (let i = 0; i < estPages; i++) {
7093
+ try {
7094
+ const pdfFiles = files.filter((file) => file.type === "application/pdf");
7095
+ if (pdfFiles.length > 0) {
7096
+ const gapWorld = 16;
7097
+ const estW = 1241;
7098
+ const estH = 1754;
7099
+ const estPages = 10;
7100
+ const skels = [];
7101
+ for (const [fileIndex] of pdfFiles.entries()) {
7102
+ for (let pageIndex = 0; pageIndex < estPages; pageIndex++) {
6982
7103
  const id = `skeleton-${skeletonSeqRef.current++}`;
6983
- skelIds.push(id);
7104
+ const offsetIndex = fileIndex * estPages + pageIndex;
6984
7105
  skels.push({
6985
7106
  id,
6986
7107
  x: worldX - estW / 2,
6987
- y: worldY - estH / 2 + i * (estH + gapWorld),
7108
+ y: worldY - estH / 2 + offsetIndex * (estH + gapWorld),
6988
7109
  width: estW,
6989
7110
  height: estH
6990
7111
  });
6991
7112
  }
6992
- setLoadingSkeletons(skels);
6993
- const pages = await loadPdfToStore(file, store);
6994
- setLoadingSkeletons([]);
6995
- for (const page of pages) {
6996
- const { blobId, width, height } = page;
6997
- const fullUrl = await createBlobUrlFromStore(store, blobId);
6998
- const id = createShapeId();
6999
- const bounds = {
7000
- x: worldX - width / 2,
7001
- y: worldY - height / 2 + placed * (height + gapWorld),
7002
- width,
7003
- height
7004
- };
7005
- newItems.push({
7006
- id,
7007
- x: bounds.x,
7008
- y: bounds.y,
7009
- bounds: { ...bounds },
7010
- toolKind: "image",
7011
- imageBlobId: blobId,
7012
- imageRasterHref: fullUrl ?? void 0,
7013
- imageIntrinsicSize: { width, height },
7014
- childrenSvg: fullUrl ? buildRasterImageChildrenSvg2(fullUrl, { width, height }, bounds) : ""
7015
- });
7016
- placed++;
7017
- }
7018
- } else if (file.type.startsWith("image/")) {
7019
- const { blobId, thumbnailBlobId, width, height } = await loadImageToStore(file, store);
7020
- const fullUrl = await createBlobUrlFromStore(store, blobId);
7021
- const thumbBlob = await store.getThumbnail(thumbnailBlobId);
7022
- const thumbnailHref = thumbBlob ? URL.createObjectURL(thumbBlob) : null;
7023
- const id = createShapeId();
7024
- const ox = placed % 8 * stepWorld;
7025
- const oy = Math.floor(placed / 8) * stepWorld;
7026
- const bounds = {
7027
- x: worldX - width / 2 + ox,
7028
- y: worldY - height / 2 + oy,
7029
- width,
7030
- height
7031
- };
7032
- const href = thumbnailHref ?? fullUrl;
7033
- newItems.push({
7034
- id,
7035
- x: bounds.x,
7036
- y: bounds.y,
7037
- bounds: { ...bounds },
7038
- toolKind: "image",
7039
- imageBlobId: blobId,
7040
- imageThumbnailBlobId: thumbnailBlobId,
7041
- imageRasterHref: fullUrl ?? void 0,
7042
- imageThumbnailHref: thumbnailHref ?? void 0,
7043
- imageIntrinsicSize: { width, height },
7044
- childrenSvg: buildRasterImageChildrenSvg2(
7045
- href ?? "",
7046
- { width, height },
7047
- bounds
7048
- )
7049
- });
7050
- placed++;
7051
7113
  }
7052
- } catch (err) {
7053
- console.error("Failed to add file:", err);
7114
+ setLoadingSkeletons(skels);
7115
+ }
7116
+ const result = await ingestAssetFilesToSceneItems({
7117
+ files,
7118
+ worldCenter: {
7119
+ x: worldX,
7120
+ y: worldY
7121
+ },
7122
+ imageStore: store,
7123
+ assetStore: assetStoreRef.current ?? void 0
7124
+ });
7125
+ if (result.errors.length > 0) {
7126
+ for (const error of result.errors) {
7127
+ console.error("Failed to add file:", error.error);
7128
+ }
7054
7129
  }
7130
+ if (result.items.length === 0) return;
7131
+ change([...itemsRef.current, ...result.items]);
7132
+ setEffectiveSelectedIdsRef.current(result.items.map((item) => item.id));
7133
+ } finally {
7134
+ setLoadingSkeletons([]);
7055
7135
  }
7056
- if (newItems.length === 0) return;
7057
- change([...itemsRef.current, ...newItems]);
7058
- setEffectiveSelectedIdsRef.current(newItems.map((it) => it.id));
7059
7136
  },
7060
7137
  []
7061
7138
  );
@@ -7726,6 +7803,14 @@ var VectorViewport = react.forwardRef(
7726
7803
  const { worldX: worldX2, worldY: worldY2 } = screenToWorld(ev.clientX, ev.clientY);
7727
7804
  const raw = rectFromCorners(st.startWorld, { x: worldX2, y: worldY2 });
7728
7805
  setPlacementPreview({ kind: "marquee", rect: raw });
7806
+ const nextCand = collectItemIdsInRect(
7807
+ resolvedItemsRef.current,
7808
+ normalizeRect(raw)
7809
+ );
7810
+ if (!shallowEqualStringArray(nextCand, marqueeCandidateIdsRef.current)) {
7811
+ marqueeCandidateIdsRef.current = nextCand;
7812
+ setMarqueeCandidateIds(nextCand);
7813
+ }
7729
7814
  return;
7730
7815
  }
7731
7816
  if (st.kind === "stroke") {
@@ -7905,6 +7990,8 @@ var VectorViewport = react.forwardRef(
7905
7990
  dragStateRef.current = { kind: "idle" };
7906
7991
  releaseInteractionPointer();
7907
7992
  setPlacementPreview(null);
7993
+ marqueeCandidateIdsRef.current = [];
7994
+ setMarqueeCandidateIds([]);
7908
7995
  return;
7909
7996
  }
7910
7997
  if (st.kind === "move" || st.kind === "resize" || st.kind === "rotate") {
@@ -7916,6 +8003,8 @@ var VectorViewport = react.forwardRef(
7916
8003
  dragStateRef.current = { kind: "idle" };
7917
8004
  releaseInteractionPointer();
7918
8005
  setPlacementPreview(null);
8006
+ marqueeCandidateIdsRef.current = [];
8007
+ setMarqueeCandidateIds([]);
7919
8008
  const { worldX, worldY } = screenToWorld(ev.clientX, ev.clientY);
7920
8009
  const raw = rectFromCorners(st.startWorld, { x: worldX, y: worldY });
7921
8010
  const br = normalizeRect(raw);
@@ -8146,6 +8235,11 @@ var VectorViewport = react.forwardRef(
8146
8235
  const eraserPreviewItemsForOverlay = react.useMemo(() => {
8147
8236
  return eraserPreviewIds.map((id) => resolvedItems.find((i) => i.id === id)).filter((i) => i != null);
8148
8237
  }, [eraserPreviewIds, resolvedItems]);
8238
+ const marqueeCandidateItemsForOverlay = react.useMemo(() => {
8239
+ if (marqueeCandidateIds.length === 0) return [];
8240
+ const selected = new Set(effectiveSelectedIds);
8241
+ return marqueeCandidateIds.filter((id) => !selected.has(id)).map((id) => resolvedItems.find((i) => i.id === id)).filter((i) => i != null);
8242
+ }, [marqueeCandidateIds, effectiveSelectedIds, resolvedItems]);
8149
8243
  const presenceLayer = react.useMemo(() => {
8150
8244
  if (!cameraForOverlay) return null;
8151
8245
  if (presenceOverlay) {
@@ -8291,6 +8385,7 @@ var VectorViewport = react.forwardRef(
8291
8385
  eraserTrail,
8292
8386
  laserTrail,
8293
8387
  eraserPreviewItems: eraserPreviewItemsForOverlay,
8388
+ marqueeCandidateItems: marqueeCandidateItemsForOverlay,
8294
8389
  previewStrokeStyle: strokeStyleRef.current
8295
8390
  }
8296
8391
  ),
@@ -8542,6 +8637,7 @@ exports.createNoopPersistenceAdapter = createNoopPersistenceAdapter;
8542
8637
  exports.createToolPlugin = createToolPlugin;
8543
8638
  exports.cursorForVectorToolId = cursorForVectorToolId;
8544
8639
  exports.getBoardPositionStyle = getBoardPositionStyle;
8640
+ exports.ingestAssetFilesToSceneItems = ingestAssetFilesToSceneItems;
8545
8641
  exports.useCanvuChromeContext = useCanvuChromeContext;
8546
8642
  exports.useCanvuDocumentContext = useCanvuDocumentContext;
8547
8643
  exports.useCanvuPluginContext = useCanvuPluginContext;