canvu-react 0.3.6 → 0.3.8

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";
@@ -5155,7 +5248,6 @@ function InteractionOverlay({
5155
5248
  }
5156
5249
  );
5157
5250
  }
5158
- init_freehand_path();
5159
5251
 
5160
5252
  // src/react/presence/peer-color.ts
5161
5253
  function defaultPresenceColorForId(id) {
@@ -5474,7 +5566,8 @@ var visuallyHidden = {
5474
5566
  whiteSpace: "nowrap",
5475
5567
  border: 0
5476
5568
  };
5477
- var VIEWPORT_ZOOM_STEP = 1.15;
5569
+ var VIEWPORT_ZOOM_IN_STEP = 1.15;
5570
+ var VIEWPORT_ZOOM_OUT_STEP = 1 / 1.15;
5478
5571
  function replaceItem(items, id, next) {
5479
5572
  return items.map((it) => it.id === id ? next : it);
5480
5573
  }
@@ -5678,22 +5771,8 @@ function appendInterpolatedPoints(points, next, maxStepWorld) {
5678
5771
  if (!last) return [...points, next];
5679
5772
  const dx = next.x - last.x;
5680
5773
  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;
5774
+ if (dx * dx + dy * dy < 1e-12) return points;
5775
+ return [...points, next];
5697
5776
  }
5698
5777
  function pointerSampleToWorldPoint(screenToWorldFn, clientX, clientY, pressure) {
5699
5778
  const { worldX, worldY } = screenToWorldFn(clientX, clientY);
@@ -5725,7 +5804,7 @@ function isLikelyStylusTouch(touch) {
5725
5804
  }
5726
5805
  function appendPointerEventSamplesToStrokePoints(points, ev, zoom, screenToWorldFn) {
5727
5806
  let interpolated = points;
5728
- const maxStepWorld = ev.pointerType === "pen" ? 0.35 / zoom : 1 / zoom;
5807
+ ev.pointerType === "pen" ? 0.35 / zoom : 1 / zoom;
5729
5808
  for (const sample of pointerEventSamples(ev)) {
5730
5809
  const nextPoint = pointerSampleToWorldPoint(
5731
5810
  screenToWorldFn,
@@ -5733,7 +5812,7 @@ function appendPointerEventSamplesToStrokePoints(points, ev, zoom, screenToWorld
5733
5812
  sample.clientY,
5734
5813
  sample.pointerType === "pen" ? sample.pressure : void 0
5735
5814
  );
5736
- interpolated = appendInterpolatedPoints(interpolated, nextPoint, maxStepWorld);
5815
+ interpolated = appendInterpolatedPoints(interpolated, nextPoint);
5737
5816
  }
5738
5817
  return interpolated;
5739
5818
  }
@@ -5744,7 +5823,7 @@ function appendTouchToStrokePoints(points, touch, zoom, screenToWorldFn) {
5744
5823
  touch.clientY,
5745
5824
  touchPressure(touch)
5746
5825
  );
5747
- return appendInterpolatedPoints(points, nextPoint, 0.35 / zoom);
5826
+ return appendInterpolatedPoints(points, nextPoint);
5748
5827
  }
5749
5828
  var VectorViewport = react.forwardRef(
5750
5829
  function VectorViewport2({
@@ -5757,14 +5836,16 @@ var VectorViewport = react.forwardRef(
5757
5836
  selectedIds: selectedIdsProp,
5758
5837
  onSelectionChange,
5759
5838
  onItemsChange: consumerOnItemsChange,
5760
- onWorldPointerDown,
5839
+ onWorldPointerDown: consumerOnWorldPointerDown,
5761
5840
  toolbar,
5762
5841
  navMenu,
5763
5842
  selectionInspector,
5843
+ selectionInspectorProps,
5764
5844
  plugins = [],
5765
5845
  onCameraChange: consumerOnCameraChange,
5766
5846
  customPlacement: consumerCustomPlacement,
5767
5847
  customPlacements: consumerCustomPlacements,
5848
+ assetStore,
5768
5849
  toolLocked = false,
5769
5850
  autoResetToolTo = "select",
5770
5851
  onToolChangeRequest,
@@ -5847,6 +5928,12 @@ var VectorViewport = react.forwardRef(
5847
5928
  );
5848
5929
  const remotePresence = [...orderedPluginContributions].reverse().map((contribution) => contribution.viewportProps?.remotePresence).find((value) => value !== void 0) ?? consumerRemotePresence;
5849
5930
  const presenceOverlay = [...orderedPluginContributions].reverse().map((contribution) => contribution.viewportProps?.presenceOverlay).find((value) => value !== void 0) ?? consumerPresenceOverlay;
5931
+ const onWorldPointerDown = composePluginEvent(
5932
+ consumerOnWorldPointerDown,
5933
+ orderedPluginContributions.map(
5934
+ (contribution) => contribution.callbacks?.onWorldPointerDown
5935
+ )
5936
+ );
5850
5937
  const onWorldPointerMove = composePluginEvent(
5851
5938
  consumerOnWorldPointerMove,
5852
5939
  orderedPluginContributions.map(
@@ -5929,6 +6016,8 @@ var VectorViewport = react.forwardRef(
5929
6016
  const itemsRef = react.useRef(items);
5930
6017
  const onWorldPointerDownRef = react.useRef(onWorldPointerDown);
5931
6018
  const onItemsChangeRef = react.useRef(onItemsChange);
6019
+ const assetStoreRef = react.useRef(assetStore);
6020
+ assetStoreRef.current = assetStore;
5932
6021
  const customPlacementRef = react.useRef(customPlacement);
5933
6022
  customPlacementRef.current = customPlacement;
5934
6023
  const dragStateRef = react.useRef({ kind: "idle" });
@@ -6545,14 +6634,14 @@ var VectorViewport = react.forwardRef(
6545
6634
  handled = true;
6546
6635
  } else if (e.key === "+" || e.key === "=") {
6547
6636
  const rect = el.getBoundingClientRect();
6548
- camera.setZoom(camera.zoom * VIEWPORT_ZOOM_STEP, {
6637
+ camera.setZoom(camera.zoom * VIEWPORT_ZOOM_IN_STEP, {
6549
6638
  x: rect.width / 2,
6550
6639
  y: rect.height / 2
6551
6640
  });
6552
6641
  handled = true;
6553
6642
  } else if (e.key === "-" || e.key === "_") {
6554
6643
  const rect = el.getBoundingClientRect();
6555
- camera.setZoom(camera.zoom / VIEWPORT_ZOOM_STEP, {
6644
+ camera.setZoom(camera.zoom * VIEWPORT_ZOOM_OUT_STEP, {
6556
6645
  x: rect.width / 2,
6557
6646
  y: rect.height / 2
6558
6647
  });
@@ -6821,7 +6910,7 @@ var VectorViewport = react.forwardRef(
6821
6910
  const el = sceneContainerRef.current;
6822
6911
  if (!camera || !el) return;
6823
6912
  const rect = el.getBoundingClientRect();
6824
- camera.setZoom(camera.zoom * VIEWPORT_ZOOM_STEP, {
6913
+ camera.setZoom(camera.zoom * VIEWPORT_ZOOM_IN_STEP, {
6825
6914
  x: rect.width / 2,
6826
6915
  y: rect.height / 2
6827
6916
  });
@@ -6832,7 +6921,7 @@ var VectorViewport = react.forwardRef(
6832
6921
  const el = sceneContainerRef.current;
6833
6922
  if (!camera || !el) return;
6834
6923
  const rect = el.getBoundingClientRect();
6835
- camera.setZoom(camera.zoom / VIEWPORT_ZOOM_STEP, {
6924
+ camera.setZoom(camera.zoom * VIEWPORT_ZOOM_OUT_STEP, {
6836
6925
  x: rect.width / 2,
6837
6926
  y: rect.height / 2
6838
6927
  });
@@ -6850,13 +6939,13 @@ var VectorViewport = react.forwardRef(
6850
6939
  if (isDefaultMarkerToolStyle(current)) {
6851
6940
  setCurrentStrokeStyle({
6852
6941
  stroke: DEFAULT_STROKE_STYLE.stroke,
6853
- strokeWidth: toolId === "draw" ? 3 : DEFAULT_STROKE_STYLE.strokeWidth
6942
+ strokeWidth: toolId === "draw" ? 10 : DEFAULT_STROKE_STYLE.strokeWidth
6854
6943
  });
6855
6944
  return;
6856
6945
  }
6857
6946
  setCurrentStrokeStyle({
6858
6947
  stroke: current.stroke,
6859
- strokeWidth: toolId === "draw" ? 3 : current.strokeWidth
6948
+ strokeWidth: toolId === "draw" ? 10 : current.strokeWidth
6860
6949
  });
6861
6950
  }, [setCurrentStrokeStyle, toolId]);
6862
6951
  react.useEffect(() => {
@@ -6958,97 +7047,49 @@ var VectorViewport = react.forwardRef(
6958
7047
  if (!change || files.length === 0) return;
6959
7048
  const store = imageStoreRef.current;
6960
7049
  if (!store) return;
6961
- const { buildRasterImageChildrenSvg: buildRasterImageChildrenSvg2 } = await Promise.resolve().then(() => (init_shape_builders(), shape_builders_exports));
6962
- const newItems = [];
6963
- const gapWorld = 16;
6964
- const stepWorld = 48;
6965
- let placed = 0;
6966
- for (const file of files) {
6967
- try {
6968
- if (file.type === "application/pdf") {
6969
- const estW = 1241;
6970
- const estH = 1754;
6971
- const estPages = 10;
6972
- const skels = [];
6973
- const skelIds = [];
6974
- for (let i = 0; i < estPages; i++) {
7050
+ try {
7051
+ const pdfFiles = files.filter((file) => file.type === "application/pdf");
7052
+ if (pdfFiles.length > 0) {
7053
+ const gapWorld = 16;
7054
+ const estW = 1241;
7055
+ const estH = 1754;
7056
+ const estPages = 10;
7057
+ const skels = [];
7058
+ for (const [fileIndex] of pdfFiles.entries()) {
7059
+ for (let pageIndex = 0; pageIndex < estPages; pageIndex++) {
6975
7060
  const id = `skeleton-${skeletonSeqRef.current++}`;
6976
- skelIds.push(id);
7061
+ const offsetIndex = fileIndex * estPages + pageIndex;
6977
7062
  skels.push({
6978
7063
  id,
6979
7064
  x: worldX - estW / 2,
6980
- y: worldY - estH / 2 + i * (estH + gapWorld),
7065
+ y: worldY - estH / 2 + offsetIndex * (estH + gapWorld),
6981
7066
  width: estW,
6982
7067
  height: estH
6983
7068
  });
6984
7069
  }
6985
- setLoadingSkeletons(skels);
6986
- const pages = await loadPdfToStore(file, store);
6987
- setLoadingSkeletons([]);
6988
- for (const page of pages) {
6989
- const { blobId, width, height } = page;
6990
- const fullUrl = await createBlobUrlFromStore(store, blobId);
6991
- const id = createShapeId();
6992
- const bounds = {
6993
- x: worldX - width / 2,
6994
- y: worldY - height / 2 + placed * (height + gapWorld),
6995
- width,
6996
- height
6997
- };
6998
- newItems.push({
6999
- id,
7000
- x: bounds.x,
7001
- y: bounds.y,
7002
- bounds: { ...bounds },
7003
- toolKind: "image",
7004
- imageBlobId: blobId,
7005
- imageRasterHref: fullUrl ?? void 0,
7006
- imageIntrinsicSize: { width, height },
7007
- childrenSvg: fullUrl ? buildRasterImageChildrenSvg2(fullUrl, { width, height }, bounds) : ""
7008
- });
7009
- placed++;
7010
- }
7011
- } else if (file.type.startsWith("image/")) {
7012
- const { blobId, thumbnailBlobId, width, height } = await loadImageToStore(file, store);
7013
- const fullUrl = await createBlobUrlFromStore(store, blobId);
7014
- const thumbBlob = await store.getThumbnail(thumbnailBlobId);
7015
- const thumbnailHref = thumbBlob ? URL.createObjectURL(thumbBlob) : null;
7016
- const id = createShapeId();
7017
- const ox = placed % 8 * stepWorld;
7018
- const oy = Math.floor(placed / 8) * stepWorld;
7019
- const bounds = {
7020
- x: worldX - width / 2 + ox,
7021
- y: worldY - height / 2 + oy,
7022
- width,
7023
- height
7024
- };
7025
- const href = thumbnailHref ?? fullUrl;
7026
- newItems.push({
7027
- id,
7028
- x: bounds.x,
7029
- y: bounds.y,
7030
- bounds: { ...bounds },
7031
- toolKind: "image",
7032
- imageBlobId: blobId,
7033
- imageThumbnailBlobId: thumbnailBlobId,
7034
- imageRasterHref: fullUrl ?? void 0,
7035
- imageThumbnailHref: thumbnailHref ?? void 0,
7036
- imageIntrinsicSize: { width, height },
7037
- childrenSvg: buildRasterImageChildrenSvg2(
7038
- href ?? "",
7039
- { width, height },
7040
- bounds
7041
- )
7042
- });
7043
- placed++;
7044
7070
  }
7045
- } catch (err) {
7046
- console.error("Failed to add file:", err);
7071
+ setLoadingSkeletons(skels);
7072
+ }
7073
+ const result = await ingestAssetFilesToSceneItems({
7074
+ files,
7075
+ worldCenter: {
7076
+ x: worldX,
7077
+ y: worldY
7078
+ },
7079
+ imageStore: store,
7080
+ assetStore: assetStoreRef.current ?? void 0
7081
+ });
7082
+ if (result.errors.length > 0) {
7083
+ for (const error of result.errors) {
7084
+ console.error("Failed to add file:", error.error);
7085
+ }
7047
7086
  }
7087
+ if (result.items.length === 0) return;
7088
+ change([...itemsRef.current, ...result.items]);
7089
+ setEffectiveSelectedIdsRef.current(result.items.map((item) => item.id));
7090
+ } finally {
7091
+ setLoadingSkeletons([]);
7048
7092
  }
7049
- if (newItems.length === 0) return;
7050
- change([...itemsRef.current, ...newItems]);
7051
- setEffectiveSelectedIdsRef.current(newItems.map((it) => it.id));
7052
7093
  },
7053
7094
  []
7054
7095
  );
@@ -8195,7 +8236,13 @@ var VectorViewport = react.forwardRef(
8195
8236
  canRedo,
8196
8237
  onRequestRender: renderFrame
8197
8238
  };
8198
- const defaultSelectionInspector = cameraForOverlay && canEditSelection ? /* @__PURE__ */ jsxRuntime.jsx(VectorSelectionInspector, {}) : null;
8239
+ const defaultSelectionInspector = cameraForOverlay && canEditSelection ? /* @__PURE__ */ jsxRuntime.jsx(
8240
+ VectorSelectionInspector,
8241
+ {
8242
+ className: selectionInspectorProps?.className,
8243
+ style: selectionInspectorProps?.style
8244
+ }
8245
+ ) : null;
8199
8246
  const defaultNavMenu = cameraForOverlay ? /* @__PURE__ */ jsxRuntime.jsx(NavMenu, {}) : null;
8200
8247
  const resolvedSelectionInspector = selectionInspector === void 0 ? defaultSelectionInspector : selectionInspector;
8201
8248
  const resolvedNavMenu = navMenu === void 0 ? defaultNavMenu : navMenu;
@@ -8529,6 +8576,7 @@ exports.createNoopPersistenceAdapter = createNoopPersistenceAdapter;
8529
8576
  exports.createToolPlugin = createToolPlugin;
8530
8577
  exports.cursorForVectorToolId = cursorForVectorToolId;
8531
8578
  exports.getBoardPositionStyle = getBoardPositionStyle;
8579
+ exports.ingestAssetFilesToSceneItems = ingestAssetFilesToSceneItems;
8532
8580
  exports.useCanvuChromeContext = useCanvuChromeContext;
8533
8581
  exports.useCanvuDocumentContext = useCanvuDocumentContext;
8534
8582
  exports.useCanvuPluginContext = useCanvuPluginContext;