canvu-react 0.4.14 → 0.4.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/native.cjs CHANGED
@@ -1822,13 +1822,224 @@ function NativeShapeRenderer({ item }) {
1822
1822
  }
1823
1823
  return null;
1824
1824
  }
1825
+ var CURSOR_STROKE = "#18181b";
1826
+ var CURSOR_STROKE_WIDTH = 2;
1827
+ var CROSSHAIR_STROKE_WIDTH = 1.5;
1828
+ function NativeDrawCursor() {
1829
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1830
+ /* @__PURE__ */ jsxRuntime.jsx(
1831
+ reactNativeSkia.Path,
1832
+ {
1833
+ path: "M13 21h8",
1834
+ color: CURSOR_STROKE,
1835
+ style: "stroke",
1836
+ strokeWidth: CURSOR_STROKE_WIDTH,
1837
+ strokeCap: "round",
1838
+ strokeJoin: "round",
1839
+ antiAlias: true
1840
+ }
1841
+ ),
1842
+ /* @__PURE__ */ jsxRuntime.jsx(
1843
+ reactNativeSkia.Path,
1844
+ {
1845
+ path: "M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z",
1846
+ color: CURSOR_STROKE,
1847
+ style: "stroke",
1848
+ strokeWidth: CURSOR_STROKE_WIDTH,
1849
+ strokeCap: "round",
1850
+ strokeJoin: "round",
1851
+ antiAlias: true
1852
+ }
1853
+ )
1854
+ ] });
1855
+ }
1856
+ function NativeMarkerCursor() {
1857
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1858
+ /* @__PURE__ */ jsxRuntime.jsx(
1859
+ reactNativeSkia.Path,
1860
+ {
1861
+ path: "m9 11-6 6v3h9l3-3",
1862
+ color: CURSOR_STROKE,
1863
+ style: "stroke",
1864
+ strokeWidth: CURSOR_STROKE_WIDTH,
1865
+ strokeCap: "round",
1866
+ strokeJoin: "round",
1867
+ antiAlias: true
1868
+ }
1869
+ ),
1870
+ /* @__PURE__ */ jsxRuntime.jsx(
1871
+ reactNativeSkia.Path,
1872
+ {
1873
+ path: "m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4",
1874
+ color: CURSOR_STROKE,
1875
+ style: "stroke",
1876
+ strokeWidth: CURSOR_STROKE_WIDTH,
1877
+ strokeCap: "round",
1878
+ strokeJoin: "round",
1879
+ antiAlias: true
1880
+ }
1881
+ )
1882
+ ] });
1883
+ }
1884
+ function NativeEraserCursor() {
1885
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1886
+ /* @__PURE__ */ jsxRuntime.jsx(
1887
+ reactNativeSkia.Path,
1888
+ {
1889
+ path: "M21 21H8a2 2 0 0 1-1.42-.587l-3.994-3.999a2 2 0 0 1 0-2.828l10-10a2 2 0 0 1 2.829 0l5.999 6a2 2 0 0 1 0 2.828L12.834 21",
1890
+ color: CURSOR_STROKE,
1891
+ style: "stroke",
1892
+ strokeWidth: CURSOR_STROKE_WIDTH,
1893
+ strokeCap: "round",
1894
+ strokeJoin: "round",
1895
+ antiAlias: true
1896
+ }
1897
+ ),
1898
+ /* @__PURE__ */ jsxRuntime.jsx(
1899
+ reactNativeSkia.Path,
1900
+ {
1901
+ path: "m5.082 11.09 8.828 8.828",
1902
+ color: CURSOR_STROKE,
1903
+ style: "stroke",
1904
+ strokeWidth: CURSOR_STROKE_WIDTH,
1905
+ strokeCap: "round",
1906
+ strokeJoin: "round",
1907
+ antiAlias: true
1908
+ }
1909
+ )
1910
+ ] });
1911
+ }
1912
+ function NativeCrosshairCursor() {
1913
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1914
+ /* @__PURE__ */ jsxRuntime.jsx(
1915
+ reactNativeSkia.Line,
1916
+ {
1917
+ p1: reactNativeSkia.vec(12, 3),
1918
+ p2: reactNativeSkia.vec(12, 21),
1919
+ color: CURSOR_STROKE,
1920
+ style: "stroke",
1921
+ strokeWidth: CROSSHAIR_STROKE_WIDTH,
1922
+ strokeCap: "round",
1923
+ antiAlias: true
1924
+ }
1925
+ ),
1926
+ /* @__PURE__ */ jsxRuntime.jsx(
1927
+ reactNativeSkia.Line,
1928
+ {
1929
+ p1: reactNativeSkia.vec(3, 12),
1930
+ p2: reactNativeSkia.vec(21, 12),
1931
+ color: CURSOR_STROKE,
1932
+ style: "stroke",
1933
+ strokeWidth: CROSSHAIR_STROKE_WIDTH,
1934
+ strokeCap: "round",
1935
+ antiAlias: true
1936
+ }
1937
+ )
1938
+ ] });
1939
+ }
1940
+ function NativeTextCursor() {
1941
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1942
+ /* @__PURE__ */ jsxRuntime.jsx(
1943
+ reactNativeSkia.Line,
1944
+ {
1945
+ p1: reactNativeSkia.vec(12, 4),
1946
+ p2: reactNativeSkia.vec(12, 20),
1947
+ color: CURSOR_STROKE,
1948
+ style: "stroke",
1949
+ strokeWidth: CURSOR_STROKE_WIDTH,
1950
+ strokeCap: "round",
1951
+ antiAlias: true
1952
+ }
1953
+ ),
1954
+ /* @__PURE__ */ jsxRuntime.jsx(
1955
+ reactNativeSkia.Line,
1956
+ {
1957
+ p1: reactNativeSkia.vec(8, 4),
1958
+ p2: reactNativeSkia.vec(16, 4),
1959
+ color: CURSOR_STROKE,
1960
+ style: "stroke",
1961
+ strokeWidth: CURSOR_STROKE_WIDTH,
1962
+ strokeCap: "round",
1963
+ antiAlias: true
1964
+ }
1965
+ ),
1966
+ /* @__PURE__ */ jsxRuntime.jsx(
1967
+ reactNativeSkia.Line,
1968
+ {
1969
+ p1: reactNativeSkia.vec(8, 20),
1970
+ p2: reactNativeSkia.vec(16, 20),
1971
+ color: CURSOR_STROKE,
1972
+ style: "stroke",
1973
+ strokeWidth: CURSOR_STROKE_WIDTH,
1974
+ strokeCap: "round",
1975
+ antiAlias: true
1976
+ }
1977
+ )
1978
+ ] });
1979
+ }
1980
+ function NativeToolCursorGlyph({
1981
+ cursor
1982
+ }) {
1983
+ if (cursor.kind === "draw") return /* @__PURE__ */ jsxRuntime.jsx(NativeDrawCursor, {});
1984
+ if (cursor.kind === "marker") return /* @__PURE__ */ jsxRuntime.jsx(NativeMarkerCursor, {});
1985
+ if (cursor.kind === "eraser") return /* @__PURE__ */ jsxRuntime.jsx(NativeEraserCursor, {});
1986
+ if (cursor.kind === "text") return /* @__PURE__ */ jsxRuntime.jsx(NativeTextCursor, {});
1987
+ return /* @__PURE__ */ jsxRuntime.jsx(NativeCrosshairCursor, {});
1988
+ }
1989
+ function NativeToolCursorRenderer({
1990
+ cursor,
1991
+ point
1992
+ }) {
1993
+ const x = point.x - cursor.hotspot.x;
1994
+ const y = point.y - cursor.hotspot.y;
1995
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNativeSkia.Group, { transform: [{ translateX: x }, { translateY: y }], children: /* @__PURE__ */ jsxRuntime.jsx(NativeToolCursorGlyph, { cursor }) });
1996
+ }
1997
+
1998
+ // src/native/native-overlay-style.ts
1999
+ var ERASER_TINT = "#cbd5e1";
2000
+ var ERASER_TINT_OPACITY = 0.95;
2001
+ var LASER_TINT = "#f43f5e";
2002
+ var LASER_TINT_OPACITY = 0.9;
2003
+ var ERASER_TRAIL_MAX_AGE_MS = 150;
2004
+ var LASER_TRAIL_MAX_AGE_MS = 650;
2005
+ function colorWithOpacity(hex, alpha) {
2006
+ if (alpha == null || alpha >= 1) return hex;
2007
+ const r = parseInt(hex.slice(1, 3), 16);
2008
+ const g = parseInt(hex.slice(3, 5), 16);
2009
+ const b = parseInt(hex.slice(5, 7), 16);
2010
+ if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return hex;
2011
+ return `rgba(${r},${g},${b},${alpha})`;
2012
+ }
2013
+ function freshTimedTrailPoints(points, now, maxAgeMs) {
2014
+ return points.filter((point) => now - point.t <= maxAgeMs);
2015
+ }
2016
+ function timedTrailHeadOpacity(point, now, maxAgeMs) {
2017
+ if (!point) return 0;
2018
+ return Math.max(0, 1 - (now - point.t) / maxAgeMs);
2019
+ }
2020
+ function resolveNativeStrokePreviewStyle(tool, previewStrokeStyle) {
2021
+ const isLaser = tool === "laser";
2022
+ return {
2023
+ stroke: isLaser ? LASER_TINT : previewStrokeStyle?.stroke ?? "#64748b",
2024
+ strokeWidth: isLaser ? 4 : previewStrokeStyle?.strokeWidth ?? (tool === "marker" ? 16 : 3),
2025
+ ...previewStrokeStyle?.strokeOpacity != null && !isLaser ? { strokeOpacity: previewStrokeStyle.strokeOpacity } : {},
2026
+ ...previewStrokeStyle?.strokeDash != null && !isLaser ? { strokeDash: previewStrokeStyle.strokeDash } : {}
2027
+ };
2028
+ }
1825
2029
  var HANDLE_ORDER = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
1826
2030
  var ERASER_PREVIEW_OPACITY = 0.3;
2031
+ var OVERLAY_STROKE_PX = 1.25;
2032
+ var MARQUEE_DASH_PX = 4;
1827
2033
  function pointsToSmoothPathD(points) {
1828
2034
  if (points.length < 2) return null;
1829
2035
  const d = smoothFreehandPointsToPathD(points);
1830
2036
  return d || null;
1831
2037
  }
2038
+ function dashIntervalsFromStrokeDasharray(strokeDasharray) {
2039
+ if (!strokeDasharray) return null;
2040
+ const intervals = strokeDasharray.split(/\s+/).map((part) => Number(part)).filter((part) => Number.isFinite(part) && part > 0);
2041
+ return intervals.length > 0 ? intervals : null;
2042
+ }
1832
2043
  function NativeInteractionOverlay({
1833
2044
  camera,
1834
2045
  width,
@@ -1838,13 +2049,19 @@ function NativeInteractionOverlay({
1838
2049
  placementPreview,
1839
2050
  eraserTrail,
1840
2051
  laserTrail,
1841
- eraserPreviewItems = []
2052
+ eraserPreviewItems = [],
2053
+ previewStrokeStyle,
2054
+ toolCursor
1842
2055
  }) {
1843
2056
  const z = camera.zoom;
1844
2057
  const camTransform = skiaCameraTransform(z, camera.x, camera.y);
1845
2058
  const handleR = 5 / z;
1846
- const overlayStrokePx = 1.25;
2059
+ const overlayStrokeWorld = OVERLAY_STROKE_PX / z;
2060
+ const marqueeDashWorld = MARQUEE_DASH_PX / z;
1847
2061
  const rotateOffsetWorld = 24 / z;
2062
+ const rotateIconWorld = 16 / z;
2063
+ const rotateIconScale = rotateIconWorld / 24;
2064
+ const rotateIconStroke = OVERLAY_STROKE_PX / (z * rotateIconScale);
1848
2065
  const selectionElements = react.useMemo(() => {
1849
2066
  if (selectedItems.length === 0) return null;
1850
2067
  const single = selectedItems.length === 1 ? selectedItems[0] : void 0;
@@ -1866,7 +2083,7 @@ function NativeInteractionOverlay({
1866
2083
  height: b.height,
1867
2084
  color: "#3b82f6",
1868
2085
  style: "stroke",
1869
- strokeWidth: overlayStrokePx,
2086
+ strokeWidth: overlayStrokeWorld,
1870
2087
  antiAlias: true
1871
2088
  }
1872
2089
  ) }, it.id);
@@ -1874,83 +2091,167 @@ function NativeInteractionOverlay({
1874
2091
  showResizeHandles && bSingle && single && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1875
2092
  HANDLE_ORDER.map((hid) => {
1876
2093
  const p = getHandleWorldPositionRotated(bSingle, hid, rotSingle);
1877
- return /* @__PURE__ */ jsxRuntime.jsx(
1878
- reactNativeSkia.Circle,
1879
- {
1880
- cx: p.x,
1881
- cy: p.y,
1882
- r: handleR,
1883
- color: "#ffffff",
1884
- style: "fill",
1885
- antiAlias: true
1886
- },
1887
- hid
1888
- );
2094
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNativeSkia.Group, { children: [
2095
+ /* @__PURE__ */ jsxRuntime.jsx(
2096
+ reactNativeSkia.Circle,
2097
+ {
2098
+ cx: p.x,
2099
+ cy: p.y,
2100
+ r: handleR,
2101
+ color: "#ffffff",
2102
+ style: "fill",
2103
+ antiAlias: true
2104
+ }
2105
+ ),
2106
+ /* @__PURE__ */ jsxRuntime.jsx(
2107
+ reactNativeSkia.Circle,
2108
+ {
2109
+ cx: p.x,
2110
+ cy: p.y,
2111
+ r: handleR,
2112
+ color: "#3b82f6",
2113
+ style: "stroke",
2114
+ strokeWidth: overlayStrokeWorld,
2115
+ antiAlias: true
2116
+ }
2117
+ )
2118
+ ] }, hid);
1889
2119
  }),
1890
- rotHandlePos && /* @__PURE__ */ jsxRuntime.jsx(
1891
- reactNativeSkia.Circle,
2120
+ rotHandlePos && /* @__PURE__ */ jsxRuntime.jsxs(
2121
+ reactNativeSkia.Group,
1892
2122
  {
1893
- cx: rotHandlePos.x,
1894
- cy: rotHandlePos.y,
1895
- r: handleR * 1.5,
1896
- color: "#3b82f6",
1897
- style: "stroke",
1898
- strokeWidth: overlayStrokePx * 1.2,
1899
- antiAlias: true
2123
+ transform: [
2124
+ { translateX: rotHandlePos.x },
2125
+ { translateY: rotHandlePos.y },
2126
+ { rotate: rotSingle },
2127
+ { translateX: -rotateIconWorld / 2 },
2128
+ { translateY: -rotateIconWorld / 2 },
2129
+ { scale: rotateIconScale }
2130
+ ],
2131
+ children: [
2132
+ /* @__PURE__ */ jsxRuntime.jsx(
2133
+ reactNativeSkia.Path,
2134
+ {
2135
+ path: "M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8",
2136
+ color: "#3b82f6",
2137
+ style: "stroke",
2138
+ strokeWidth: rotateIconStroke,
2139
+ strokeCap: "round",
2140
+ strokeJoin: "round",
2141
+ antiAlias: true
2142
+ }
2143
+ ),
2144
+ /* @__PURE__ */ jsxRuntime.jsx(
2145
+ reactNativeSkia.Path,
2146
+ {
2147
+ path: "M21 3v5h-5",
2148
+ color: "#3b82f6",
2149
+ style: "stroke",
2150
+ strokeWidth: rotateIconStroke,
2151
+ strokeCap: "round",
2152
+ strokeJoin: "round",
2153
+ antiAlias: true
2154
+ }
2155
+ )
2156
+ ]
1900
2157
  }
1901
2158
  )
1902
2159
  ] })
1903
2160
  ] });
1904
- }, [selectedItems, showResizeHandles, rotateOffsetWorld, handleR]);
2161
+ }, [
2162
+ selectedItems,
2163
+ showResizeHandles,
2164
+ rotateOffsetWorld,
2165
+ handleR,
2166
+ overlayStrokeWorld,
2167
+ rotateIconWorld,
2168
+ rotateIconScale,
2169
+ rotateIconStroke
2170
+ ]);
1905
2171
  const previewElements = react.useMemo(() => {
1906
2172
  if (!placementPreview) return null;
1907
2173
  const p = placementPreview;
2174
+ const shapeStroke = previewStrokeStyle?.stroke ?? "#1d1d1d";
2175
+ const shapeWidth = previewStrokeStyle?.strokeWidth ?? 2;
2176
+ const shapeColor = colorWithOpacity(
2177
+ shapeStroke,
2178
+ previewStrokeStyle?.strokeOpacity
2179
+ );
1908
2180
  if (p.kind === "rect" || p.kind === "ellipse" || p.kind === "architectural-cloud") {
1909
2181
  const r = normalizeRect(p.rect);
1910
- return p.kind === "rect" || p.kind === "architectural-cloud" ? /* @__PURE__ */ jsxRuntime.jsx(
1911
- reactNativeSkia.Rect,
2182
+ return p.kind === "rect" ? /* @__PURE__ */ jsxRuntime.jsx(
2183
+ reactNativeSkia.RoundedRect,
1912
2184
  {
1913
2185
  x: r.x,
1914
2186
  y: r.y,
1915
2187
  width: r.width,
1916
2188
  height: r.height,
1917
- color: "#64748b",
1918
- style: "stroke",
1919
- strokeWidth: overlayStrokePx,
1920
- antiAlias: true
1921
- }
1922
- ) : /* @__PURE__ */ jsxRuntime.jsx(
1923
- reactNativeSkia.Circle,
1924
- {
1925
- cx: r.x + r.width / 2,
1926
- cy: r.y + r.height / 2,
1927
- r: Math.max(0, r.width / 2),
1928
- color: "#64748b",
2189
+ r: 4,
2190
+ color: shapeColor,
1929
2191
  style: "stroke",
1930
- strokeWidth: overlayStrokePx,
2192
+ strokeWidth: shapeWidth,
1931
2193
  antiAlias: true
1932
2194
  }
1933
- );
1934
- }
1935
- if (p.kind === "marquee") {
1936
- const r = normalizeRect(p.rect);
1937
- return /* @__PURE__ */ jsxRuntime.jsx(
1938
- reactNativeSkia.Rect,
2195
+ ) : p.kind === "ellipse" ? /* @__PURE__ */ jsxRuntime.jsx(
2196
+ reactNativeSkia.Oval,
1939
2197
  {
1940
2198
  x: r.x,
1941
2199
  y: r.y,
1942
2200
  width: r.width,
1943
2201
  height: r.height,
1944
- color: "rgba(59, 130, 246, 0.12)",
1945
- style: "fill",
2202
+ color: shapeColor,
2203
+ style: "stroke",
2204
+ strokeWidth: shapeWidth,
1946
2205
  antiAlias: true
1947
2206
  }
1948
- );
2207
+ ) : /* @__PURE__ */ jsxRuntime.jsx(reactNativeSkia.Group, { transform: [{ translateX: r.x }, { translateY: r.y }], children: /* @__PURE__ */ jsxRuntime.jsx(
2208
+ reactNativeSkia.Path,
2209
+ {
2210
+ path: buildArchitecturalCloudPathD(r.width, r.height, shapeWidth),
2211
+ color: shapeColor,
2212
+ style: "stroke",
2213
+ strokeWidth: shapeWidth,
2214
+ strokeCap: "round",
2215
+ strokeJoin: "round",
2216
+ antiAlias: true
2217
+ }
2218
+ ) });
2219
+ }
2220
+ if (p.kind === "marquee") {
2221
+ const r = normalizeRect(p.rect);
2222
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2223
+ /* @__PURE__ */ jsxRuntime.jsx(
2224
+ reactNativeSkia.Rect,
2225
+ {
2226
+ x: r.x,
2227
+ y: r.y,
2228
+ width: r.width,
2229
+ height: r.height,
2230
+ color: "rgba(59, 130, 246, 0.12)",
2231
+ style: "fill",
2232
+ antiAlias: true
2233
+ }
2234
+ ),
2235
+ /* @__PURE__ */ jsxRuntime.jsx(
2236
+ reactNativeSkia.Rect,
2237
+ {
2238
+ x: r.x,
2239
+ y: r.y,
2240
+ width: r.width,
2241
+ height: r.height,
2242
+ color: "#3b82f6",
2243
+ style: "stroke",
2244
+ strokeWidth: overlayStrokeWorld,
2245
+ antiAlias: true,
2246
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNativeSkia.DashPathEffect, { intervals: [marqueeDashWorld, marqueeDashWorld] })
2247
+ }
2248
+ )
2249
+ ] });
1949
2250
  }
1950
2251
  if (p.kind === "line" || p.kind === "arrow") {
1951
2252
  const geometry = p.kind === "arrow" ? computeStraightArrowGeometry(
1952
2253
  { x1: p.start.x, y1: p.start.y, x2: p.end.x, y2: p.end.y },
1953
- overlayStrokePx
2254
+ shapeWidth
1954
2255
  ) : null;
1955
2256
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1956
2257
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -1958,9 +2259,9 @@ function NativeInteractionOverlay({
1958
2259
  {
1959
2260
  p1: reactNativeSkia.vec(p.start.x, p.start.y),
1960
2261
  p2: reactNativeSkia.vec(geometry?.shaftEndX ?? p.end.x, geometry?.shaftEndY ?? p.end.y),
1961
- color: "#64748b",
2262
+ color: shapeColor,
1962
2263
  style: "stroke",
1963
- strokeWidth: overlayStrokePx,
2264
+ strokeWidth: shapeWidth,
1964
2265
  strokeCap: "round",
1965
2266
  antiAlias: true
1966
2267
  }
@@ -1969,9 +2270,9 @@ function NativeInteractionOverlay({
1969
2270
  reactNativeSkia.Path,
1970
2271
  {
1971
2272
  path: `M ${geometry.headLeftX} ${geometry.headLeftY} L ${geometry.headTipX} ${geometry.headTipY} L ${geometry.headRightX} ${geometry.headRightY}`,
1972
- color: "#64748b",
2273
+ color: shapeColor,
1973
2274
  style: "stroke",
1974
- strokeWidth: overlayStrokePx,
2275
+ strokeWidth: shapeWidth,
1975
2276
  strokeCap: "round",
1976
2277
  strokeJoin: "round",
1977
2278
  antiAlias: true
@@ -1980,10 +2281,15 @@ function NativeInteractionOverlay({
1980
2281
  ] });
1981
2282
  }
1982
2283
  if (p.kind === "stroke" && p.points.length >= 1) {
2284
+ const isLaser = p.tool === "laser";
2285
+ const style = resolveNativeStrokePreviewStyle(
2286
+ p.tool,
2287
+ p.style ?? previewStrokeStyle
2288
+ );
1983
2289
  const payload = computeFreehandSvgPayload(
1984
2290
  p.points,
1985
- { stroke: "#64748b", strokeWidth: 3 },
1986
- "draw",
2291
+ style,
2292
+ isLaser ? "draw" : p.tool,
1987
2293
  p.points.length === 2
1988
2294
  );
1989
2295
  if (!payload) return null;
@@ -1994,33 +2300,47 @@ function NativeInteractionOverlay({
1994
2300
  cx: payload.cx,
1995
2301
  cy: payload.cy,
1996
2302
  r: payload.r,
1997
- color: payload.fill,
2303
+ color: colorWithOpacity(payload.fill, payload.fillOpacity),
1998
2304
  style: "fill",
1999
2305
  antiAlias: true
2000
2306
  }
2001
2307
  );
2002
2308
  }
2003
2309
  if (payload.kind === "fillPath") {
2004
- return /* @__PURE__ */ jsxRuntime.jsx(reactNativeSkia.Path, { path: payload.d, color: payload.fill, style: "fill", antiAlias: true });
2310
+ return /* @__PURE__ */ jsxRuntime.jsx(
2311
+ reactNativeSkia.Path,
2312
+ {
2313
+ path: payload.d,
2314
+ color: colorWithOpacity(payload.fill, payload.fillOpacity),
2315
+ style: "fill",
2316
+ fillType: "winding",
2317
+ antiAlias: true
2318
+ }
2319
+ );
2005
2320
  }
2006
2321
  if (payload.kind === "strokePath") {
2322
+ const intervals = dashIntervalsFromStrokeDasharray(payload.strokeDasharray);
2007
2323
  return /* @__PURE__ */ jsxRuntime.jsx(
2008
2324
  reactNativeSkia.Path,
2009
2325
  {
2010
2326
  path: payload.d,
2011
- color: "#64748b",
2327
+ color: colorWithOpacity(
2328
+ payload.stroke,
2329
+ isLaser ? 0.85 : payload.strokeOpacity
2330
+ ),
2012
2331
  style: "stroke",
2013
- strokeWidth: 3,
2332
+ strokeWidth: payload.strokeWidth,
2014
2333
  strokeCap: "round",
2015
2334
  strokeJoin: "round",
2016
- antiAlias: true
2335
+ antiAlias: true,
2336
+ children: intervals ? /* @__PURE__ */ jsxRuntime.jsx(reactNativeSkia.DashPathEffect, { intervals }) : null
2017
2337
  }
2018
2338
  );
2019
2339
  }
2020
2340
  return null;
2021
2341
  }
2022
2342
  return null;
2023
- }, [placementPreview]);
2343
+ }, [placementPreview, previewStrokeStyle, overlayStrokeWorld, marqueeDashWorld]);
2024
2344
  const eraserPreviewElements = react.useMemo(() => {
2025
2345
  if (eraserPreviewItems.length === 0) return null;
2026
2346
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: eraserPreviewItems.map((it) => {
@@ -2041,62 +2361,74 @@ function NativeInteractionOverlay({
2041
2361
  }, [eraserPreviewItems]);
2042
2362
  const eraserTrailElements = react.useMemo(() => {
2043
2363
  if (!eraserTrail || eraserTrail.length < 1) return null;
2044
- const d = pointsToSmoothPathD(eraserTrail);
2045
- if (!d)
2046
- return eraserTrail[0] ? /* @__PURE__ */ jsxRuntime.jsx(
2364
+ const now = Date.now();
2365
+ const alive = freshTimedTrailPoints(eraserTrail, now, ERASER_TRAIL_MAX_AGE_MS);
2366
+ if (alive.length === 0) return null;
2367
+ const d = pointsToSmoothPathD(alive);
2368
+ const newest = alive[alive.length - 1];
2369
+ const headOpacity = timedTrailHeadOpacity(newest, now, ERASER_TRAIL_MAX_AGE_MS);
2370
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2371
+ alive.length >= 2 && d ? /* @__PURE__ */ jsxRuntime.jsx(
2372
+ reactNativeSkia.Path,
2373
+ {
2374
+ path: d,
2375
+ color: colorWithOpacity(ERASER_TINT, ERASER_TINT_OPACITY),
2376
+ style: "stroke",
2377
+ strokeWidth: Math.max(3.5, OVERLAY_STROKE_PX) / z,
2378
+ strokeCap: "round",
2379
+ strokeJoin: "round",
2380
+ antiAlias: true
2381
+ }
2382
+ ) : null,
2383
+ /* @__PURE__ */ jsxRuntime.jsx(
2047
2384
  reactNativeSkia.Circle,
2048
2385
  {
2049
- cx: eraserTrail[0].x,
2050
- cy: eraserTrail[0].y,
2386
+ cx: newest?.x ?? alive[0]?.x ?? 0,
2387
+ cy: newest?.y ?? alive[0]?.y ?? 0,
2051
2388
  r: Math.max(5 / z, 3),
2052
- color: "#cbd5e1",
2389
+ color: colorWithOpacity(ERASER_TINT, headOpacity),
2053
2390
  style: "fill",
2054
2391
  antiAlias: true
2055
2392
  }
2056
- ) : null;
2057
- return /* @__PURE__ */ jsxRuntime.jsx(
2058
- reactNativeSkia.Path,
2059
- {
2060
- path: d,
2061
- color: "#cbd5e1",
2062
- style: "stroke",
2063
- strokeWidth: Math.max(3.5 / z, overlayStrokePx),
2064
- strokeCap: "round",
2065
- strokeJoin: "round",
2066
- antiAlias: true
2067
- }
2068
- );
2393
+ )
2394
+ ] });
2069
2395
  }, [eraserTrail, z]);
2070
2396
  const laserTrailElements = react.useMemo(() => {
2071
2397
  if (!laserTrail || laserTrail.length < 1) return null;
2072
- const d = pointsToSmoothPathD(laserTrail);
2073
- if (!d)
2074
- return laserTrail[0] ? /* @__PURE__ */ jsxRuntime.jsx(
2398
+ const now = Date.now();
2399
+ const alive = freshTimedTrailPoints(laserTrail, now, LASER_TRAIL_MAX_AGE_MS);
2400
+ if (alive.length === 0) return null;
2401
+ const d = pointsToSmoothPathD(alive);
2402
+ const newest = alive[alive.length - 1];
2403
+ const headOpacity = timedTrailHeadOpacity(newest, now, LASER_TRAIL_MAX_AGE_MS);
2404
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2405
+ alive.length >= 2 && d ? /* @__PURE__ */ jsxRuntime.jsx(
2406
+ reactNativeSkia.Path,
2407
+ {
2408
+ path: d,
2409
+ color: colorWithOpacity(LASER_TINT, LASER_TINT_OPACITY),
2410
+ style: "stroke",
2411
+ strokeWidth: Math.max(4, OVERLAY_STROKE_PX) / z,
2412
+ strokeCap: "round",
2413
+ strokeJoin: "round",
2414
+ antiAlias: true
2415
+ }
2416
+ ) : null,
2417
+ /* @__PURE__ */ jsxRuntime.jsx(
2075
2418
  reactNativeSkia.Circle,
2076
2419
  {
2077
- cx: laserTrail[0].x,
2078
- cy: laserTrail[0].y,
2420
+ cx: newest?.x ?? alive[0]?.x ?? 0,
2421
+ cy: newest?.y ?? alive[0]?.y ?? 0,
2079
2422
  r: Math.max(5 / z, 3),
2080
- color: "#f43f5e",
2423
+ color: colorWithOpacity(LASER_TINT, headOpacity),
2081
2424
  style: "fill",
2082
2425
  antiAlias: true
2083
2426
  }
2084
- ) : null;
2085
- return /* @__PURE__ */ jsxRuntime.jsx(
2086
- reactNativeSkia.Path,
2087
- {
2088
- path: d,
2089
- color: "#f43f5e",
2090
- style: "stroke",
2091
- strokeWidth: Math.max(4 / z, overlayStrokePx),
2092
- strokeCap: "round",
2093
- strokeJoin: "round",
2094
- antiAlias: true
2095
- }
2096
- );
2427
+ )
2428
+ ] });
2097
2429
  }, [laserTrail, z]);
2098
2430
  if (width <= 0 || height <= 0) return null;
2099
- return /* @__PURE__ */ jsxRuntime.jsx(
2431
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2100
2432
  reactNativeSkia.Canvas,
2101
2433
  {
2102
2434
  style: {
@@ -2107,13 +2439,22 @@ function NativeInteractionOverlay({
2107
2439
  height
2108
2440
  },
2109
2441
  pointerEvents: "none",
2110
- children: /* @__PURE__ */ jsxRuntime.jsxs(reactNativeSkia.Group, { transform: camTransform, children: [
2111
- previewElements,
2112
- laserTrailElements,
2113
- eraserTrailElements,
2114
- eraserPreviewElements,
2115
- selectionElements
2116
- ] })
2442
+ children: [
2443
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNativeSkia.Group, { transform: camTransform, children: [
2444
+ previewElements,
2445
+ laserTrailElements,
2446
+ eraserTrailElements,
2447
+ eraserPreviewElements,
2448
+ selectionElements
2449
+ ] }),
2450
+ toolCursor ? /* @__PURE__ */ jsxRuntime.jsx(
2451
+ NativeToolCursorRenderer,
2452
+ {
2453
+ cursor: toolCursor.cursor,
2454
+ point: toolCursor.point
2455
+ }
2456
+ ) : null
2457
+ ]
2117
2458
  }
2118
2459
  );
2119
2460
  }
@@ -2239,6 +2580,383 @@ function NativeSceneRenderer({
2239
2580
  if (width <= 0 || height <= 0) return null;
2240
2581
  return /* @__PURE__ */ jsxRuntime.jsx(reactNativeSkia.Canvas, { style: { width, height }, children: /* @__PURE__ */ jsxRuntime.jsx(reactNativeSkia.Group, { transform: cameraTransform, children: visible.map((item) => /* @__PURE__ */ jsxRuntime.jsx(MemoShape, { item }, item.id)) }) });
2241
2582
  }
2583
+
2584
+ // src/native/native-style-inspector-values.ts
2585
+ var NATIVE_STYLE_PALETTE = [
2586
+ { name: "black", hex: "#1d1d1d" },
2587
+ { name: "grey", hex: "#9fa8b2" },
2588
+ { name: "light-violet", hex: "#e085f4" },
2589
+ { name: "violet", hex: "#ae3ec9" },
2590
+ { name: "blue", hex: "#4263eb" },
2591
+ { name: "light-blue", hex: "#4dabf7" },
2592
+ { name: "yellow", hex: "#ffc078" },
2593
+ { name: "orange", hex: "#f76707" },
2594
+ { name: "green", hex: "#099268" },
2595
+ { name: "light-green", hex: "#40c057" },
2596
+ { name: "light-red", hex: "#ff8787" },
2597
+ { name: "red", hex: "#e03131" }
2598
+ ];
2599
+ function normalizeNativeStyleHex(stroke) {
2600
+ if (stroke && /^#[0-9A-Fa-f]{6}$/.test(stroke)) return stroke;
2601
+ return "#1d1d1d";
2602
+ }
2603
+ function nativeStyleColorWithOpacity(hex, opacity) {
2604
+ const alpha = opacity ?? 1;
2605
+ if (alpha >= 1) return hex;
2606
+ const r = Number.parseInt(hex.slice(1, 3), 16);
2607
+ const g = Number.parseInt(hex.slice(3, 5), 16);
2608
+ const b = Number.parseInt(hex.slice(5, 7), 16);
2609
+ return `rgba(${r},${g},${b},${alpha})`;
2610
+ }
2611
+ function hexesEqual(a, b) {
2612
+ return a.toLowerCase() === b.toLowerCase();
2613
+ }
2614
+ function DashPreview({
2615
+ color,
2616
+ width,
2617
+ dashed = false
2618
+ }) {
2619
+ const strokeHeight = Math.max(3, Math.min(6, width));
2620
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.dashPreviewTrack, children: [
2621
+ /* @__PURE__ */ jsxRuntime.jsx(
2622
+ reactNative.View,
2623
+ {
2624
+ style: [
2625
+ styles.dashPreviewStroke,
2626
+ {
2627
+ width: dashed ? 11 : 26,
2628
+ height: strokeHeight,
2629
+ borderRadius: strokeHeight / 2,
2630
+ backgroundColor: color
2631
+ }
2632
+ ]
2633
+ }
2634
+ ),
2635
+ dashed ? /* @__PURE__ */ jsxRuntime.jsx(
2636
+ reactNative.View,
2637
+ {
2638
+ style: [
2639
+ styles.dashPreviewStroke,
2640
+ {
2641
+ width: 11,
2642
+ height: strokeHeight,
2643
+ borderRadius: strokeHeight / 2,
2644
+ backgroundColor: color
2645
+ }
2646
+ ]
2647
+ }
2648
+ ) : null
2649
+ ] });
2650
+ }
2651
+ function clampValue(value, min, max) {
2652
+ return Math.min(max, Math.max(min, value));
2653
+ }
2654
+ function RangeControl({
2655
+ value,
2656
+ min,
2657
+ max,
2658
+ onChange,
2659
+ valueLabel
2660
+ }) {
2661
+ const [trackWidth, setTrackWidth] = react.useState(1);
2662
+ const normalized = (clampValue(value, min, max) - min) / (max - min);
2663
+ const updateFromX = react.useCallback(
2664
+ (x) => {
2665
+ const next = min + clampValue(x / trackWidth, 0, 1) * (max - min);
2666
+ onChange(Math.round(next));
2667
+ },
2668
+ [max, min, onChange, trackWidth]
2669
+ );
2670
+ const panResponder = react.useMemo(
2671
+ () => reactNative.PanResponder.create({
2672
+ onMoveShouldSetPanResponder: () => true,
2673
+ onStartShouldSetPanResponder: () => true,
2674
+ onPanResponderGrant: (event) => {
2675
+ updateFromX(event.nativeEvent.locationX);
2676
+ },
2677
+ onPanResponderMove: (event) => {
2678
+ updateFromX(event.nativeEvent.locationX);
2679
+ }
2680
+ }),
2681
+ [updateFromX]
2682
+ );
2683
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.rangeRow, children: [
2684
+ /* @__PURE__ */ jsxRuntime.jsxs(
2685
+ reactNative.View,
2686
+ {
2687
+ style: styles.rangeTrack,
2688
+ onLayout: (event) => setTrackWidth(event.nativeEvent.layout.width),
2689
+ ...panResponder.panHandlers,
2690
+ children: [
2691
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles.rangeBase }),
2692
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles.rangeFill, { width: `${normalized * 100}%` }] }),
2693
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles.rangeThumb, { left: `${normalized * 100}%` }] })
2694
+ ]
2695
+ }
2696
+ ),
2697
+ valueLabel ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.rangeValue, children: valueLabel }) : null
2698
+ ] });
2699
+ }
2700
+ function InspectorSection({
2701
+ label,
2702
+ children
2703
+ }) {
2704
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.section, children: [
2705
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.sectionLabel, children: label }),
2706
+ children
2707
+ ] });
2708
+ }
2709
+ function SegmentControl({
2710
+ segments,
2711
+ value,
2712
+ onChange
2713
+ }) {
2714
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles.segmentGroup, children: segments.map((segment) => {
2715
+ const selected = segment.value === value;
2716
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2717
+ reactNative.Pressable,
2718
+ {
2719
+ accessibilityRole: "button",
2720
+ accessibilityState: { selected },
2721
+ accessibilityLabel: segment.label,
2722
+ onPress: () => onChange(segment.value),
2723
+ style: [styles.segment, selected ? styles.segmentSelected : null],
2724
+ children: [
2725
+ segment.preview,
2726
+ /* @__PURE__ */ jsxRuntime.jsx(
2727
+ reactNative.Text,
2728
+ {
2729
+ style: [
2730
+ styles.segmentLabel,
2731
+ selected ? styles.segmentLabelSelected : null
2732
+ ],
2733
+ children: segment.label
2734
+ }
2735
+ )
2736
+ ]
2737
+ },
2738
+ String(segment.value)
2739
+ );
2740
+ }) });
2741
+ }
2742
+ function NativeVectorStyleInspector({
2743
+ toolId,
2744
+ value,
2745
+ onChange,
2746
+ style
2747
+ }) {
2748
+ const hex = normalizeNativeStyleHex(value.stroke);
2749
+ const opacity = toolId === "marker" ? value.strokeOpacity ?? 0.5 : void 0;
2750
+ const dashSegments = [
2751
+ {
2752
+ value: "solid",
2753
+ label: "Cont\xEDnuo",
2754
+ preview: /* @__PURE__ */ jsxRuntime.jsx(DashPreview, { color: "#18181b", width: 4 })
2755
+ },
2756
+ {
2757
+ value: "dashed",
2758
+ label: "Tracejado",
2759
+ preview: /* @__PURE__ */ jsxRuntime.jsx(DashPreview, { color: "#18181b", width: 4, dashed: true })
2760
+ }
2761
+ ];
2762
+ const opacityPercent = Math.round((opacity ?? 1) * 100);
2763
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2764
+ reactNative.View,
2765
+ {
2766
+ pointerEvents: "auto",
2767
+ style: [styles.shell, style],
2768
+ accessibilityRole: "summary",
2769
+ accessibilityLabel: toolId === "marker" ? "Configura\xE7\xF5es do marcador" : "Configura\xE7\xF5es da caneta",
2770
+ children: [
2771
+ /* @__PURE__ */ jsxRuntime.jsx(InspectorSection, { label: "Cor", children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles.palette, children: NATIVE_STYLE_PALETTE.map((color) => {
2772
+ const selected = hexesEqual(color.hex, hex);
2773
+ return /* @__PURE__ */ jsxRuntime.jsx(
2774
+ reactNative.Pressable,
2775
+ {
2776
+ accessibilityRole: "button",
2777
+ accessibilityState: { selected },
2778
+ accessibilityLabel: color.name,
2779
+ onPress: () => onChange({ stroke: color.hex }),
2780
+ style: [
2781
+ styles.swatch,
2782
+ {
2783
+ backgroundColor: color.hex,
2784
+ borderWidth: selected ? 2 : 1,
2785
+ transform: [{ scale: selected ? 1.08 : 1 }],
2786
+ shadowOpacity: selected ? 0.08 : 0
2787
+ }
2788
+ ]
2789
+ },
2790
+ color.name
2791
+ );
2792
+ }) }) }),
2793
+ /* @__PURE__ */ jsxRuntime.jsx(InspectorSection, { label: "Grossura", children: /* @__PURE__ */ jsxRuntime.jsx(
2794
+ RangeControl,
2795
+ {
2796
+ value: value.strokeWidth,
2797
+ min: 1,
2798
+ max: 48,
2799
+ onChange: (strokeWidth) => onChange({ strokeWidth })
2800
+ }
2801
+ ) }),
2802
+ toolId === "draw" ? /* @__PURE__ */ jsxRuntime.jsx(InspectorSection, { label: "Tra\xE7o", children: /* @__PURE__ */ jsxRuntime.jsx(
2803
+ SegmentControl,
2804
+ {
2805
+ segments: dashSegments,
2806
+ value: value.strokeDash === "dashed" ? "dashed" : "solid",
2807
+ onChange: (strokeDash) => onChange({ strokeDash })
2808
+ }
2809
+ ) }) : /* @__PURE__ */ jsxRuntime.jsx(InspectorSection, { label: "Opacidade", children: /* @__PURE__ */ jsxRuntime.jsx(
2810
+ RangeControl,
2811
+ {
2812
+ value: opacityPercent,
2813
+ min: 10,
2814
+ max: 100,
2815
+ valueLabel: `${opacityPercent}%`,
2816
+ onChange: (nextOpacity) => onChange({ strokeOpacity: nextOpacity / 100 })
2817
+ }
2818
+ ) })
2819
+ ]
2820
+ }
2821
+ );
2822
+ }
2823
+ var styles = reactNative.StyleSheet.create({
2824
+ shell: {
2825
+ minWidth: 240,
2826
+ paddingHorizontal: 14,
2827
+ paddingVertical: 12,
2828
+ borderRadius: 10,
2829
+ borderWidth: 1,
2830
+ borderColor: "rgba(0,0,0,0.1)",
2831
+ backgroundColor: "rgba(255,255,255,0.97)",
2832
+ shadowColor: "#000000",
2833
+ shadowOpacity: 0.06,
2834
+ shadowRadius: 16,
2835
+ shadowOffset: { width: 0, height: 4 },
2836
+ gap: 10
2837
+ },
2838
+ section: {
2839
+ gap: 6
2840
+ },
2841
+ sectionLabel: {
2842
+ fontSize: 11,
2843
+ fontWeight: "600",
2844
+ color: "#52525b",
2845
+ textTransform: "uppercase",
2846
+ letterSpacing: 0.22
2847
+ },
2848
+ palette: {
2849
+ flexDirection: "row",
2850
+ flexWrap: "wrap",
2851
+ columnGap: 6,
2852
+ rowGap: 6,
2853
+ maxWidth: 174
2854
+ },
2855
+ swatch: {
2856
+ width: 24,
2857
+ height: 24,
2858
+ borderRadius: 12,
2859
+ borderColor: "rgba(0,0,0,0.12)",
2860
+ shadowColor: "#000000",
2861
+ shadowRadius: 2,
2862
+ shadowOffset: { width: 0, height: 1 }
2863
+ },
2864
+ rangeRow: {
2865
+ minHeight: 28,
2866
+ flexDirection: "row",
2867
+ alignItems: "center",
2868
+ gap: 8
2869
+ },
2870
+ rangeTrack: {
2871
+ position: "relative",
2872
+ flex: 1,
2873
+ height: 28,
2874
+ justifyContent: "center"
2875
+ },
2876
+ rangeBase: {
2877
+ position: "absolute",
2878
+ left: 0,
2879
+ right: 0,
2880
+ height: 3,
2881
+ borderRadius: 2,
2882
+ backgroundColor: "rgba(24,24,27,0.12)"
2883
+ },
2884
+ rangeFill: {
2885
+ position: "absolute",
2886
+ left: 0,
2887
+ height: 3,
2888
+ borderRadius: 2,
2889
+ backgroundColor: "#18181b"
2890
+ },
2891
+ rangeThumb: {
2892
+ position: "absolute",
2893
+ width: 18,
2894
+ height: 18,
2895
+ marginLeft: -9,
2896
+ borderRadius: 9,
2897
+ borderWidth: 2,
2898
+ borderColor: "#18181b",
2899
+ backgroundColor: "#ffffff",
2900
+ shadowColor: "#000000",
2901
+ shadowOpacity: 0.12,
2902
+ shadowRadius: 3,
2903
+ shadowOffset: { width: 0, height: 1 }
2904
+ },
2905
+ rangeValue: {
2906
+ minWidth: 34,
2907
+ textAlign: "right",
2908
+ fontSize: 11,
2909
+ fontWeight: "500",
2910
+ color: "#71717a"
2911
+ },
2912
+ segmentGroup: {
2913
+ flexDirection: "row",
2914
+ alignItems: "center",
2915
+ gap: 3,
2916
+ padding: 3,
2917
+ borderRadius: 8,
2918
+ backgroundColor: "rgba(24,24,27,0.06)"
2919
+ },
2920
+ segment: {
2921
+ flex: 1,
2922
+ minWidth: 96,
2923
+ minHeight: 32,
2924
+ flexDirection: "row",
2925
+ alignItems: "center",
2926
+ justifyContent: "center",
2927
+ gap: 6,
2928
+ paddingHorizontal: 8,
2929
+ paddingVertical: 5,
2930
+ borderRadius: 6
2931
+ },
2932
+ segmentSelected: {
2933
+ backgroundColor: "#ffffff",
2934
+ shadowColor: "#000000",
2935
+ shadowOpacity: 0.1,
2936
+ shadowRadius: 2,
2937
+ shadowOffset: { width: 0, height: 1 }
2938
+ },
2939
+ segmentLabel: {
2940
+ fontSize: 11,
2941
+ fontWeight: "600",
2942
+ color: "#71717a",
2943
+ flexShrink: 0
2944
+ },
2945
+ segmentLabelSelected: {
2946
+ color: "#18181b"
2947
+ },
2948
+ dashPreviewTrack: {
2949
+ width: 26,
2950
+ height: 6,
2951
+ flexDirection: "row",
2952
+ alignItems: "center",
2953
+ justifyContent: "center",
2954
+ gap: 3
2955
+ },
2956
+ dashPreviewStroke: {
2957
+ flexShrink: 0
2958
+ }
2959
+ });
2242
2960
  var DEFAULT_NATIVE_OVERFLOW_TOOL_IDS = [
2243
2961
  "rect",
2244
2962
  "ellipse",
@@ -2370,7 +3088,7 @@ function NativeVectorToolbar({
2370
3088
  reactNative.View,
2371
3089
  {
2372
3090
  accessibilityLabel,
2373
- style: [styles.shell, style],
3091
+ style: [styles2.shell, style],
2374
3092
  pointerEvents: "box-none",
2375
3093
  children: [
2376
3094
  /* @__PURE__ */ jsxRuntime.jsxs(
@@ -2379,8 +3097,8 @@ function NativeVectorToolbar({
2379
3097
  horizontal: true,
2380
3098
  showsHorizontalScrollIndicator: false,
2381
3099
  contentContainerStyle: [
2382
- styles.content,
2383
- density === "comfortable" ? styles.comfortableContent : void 0,
3100
+ styles2.content,
3101
+ density === "comfortable" ? styles2.comfortableContent : void 0,
2384
3102
  contentContainerStyle
2385
3103
  ],
2386
3104
  children: [
@@ -2397,21 +3115,21 @@ function NativeVectorToolbar({
2397
3115
  disabled: toolLockDisabled,
2398
3116
  onPress: toggleToolLock,
2399
3117
  style: ({ pressed }) => [
2400
- styles.toolButton,
2401
- density === "comfortable" ? styles.comfortableToolButton : void 0,
2402
- toolLocked ? styles.activeToolButton : void 0,
2403
- pressed && !toolLockDisabled ? styles.pressedToolButton : void 0,
2404
- toolLockDisabled ? styles.disabledToolButton : void 0
3118
+ styles2.toolButton,
3119
+ density === "comfortable" ? styles2.comfortableToolButton : void 0,
3120
+ toolLocked ? styles2.activeToolButton : void 0,
3121
+ pressed && !toolLockDisabled ? styles2.pressedToolButton : void 0,
3122
+ toolLockDisabled ? styles2.disabledToolButton : void 0
2405
3123
  ],
2406
3124
  children: renderToolLockIcon?.({
2407
3125
  locked: toolLocked,
2408
3126
  disabled: toolLockDisabled,
2409
3127
  foregroundColor: "#18181b",
2410
3128
  onToggle: toggleToolLock
2411
- }) ?? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.lockGlyph, children: toolLocked ? "L" : "U" })
3129
+ }) ?? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.lockGlyph, children: toolLocked ? "L" : "U" })
2412
3130
  }
2413
3131
  ),
2414
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles.toolLockDivider })
3132
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles2.toolLockDivider })
2415
3133
  ] }) : null,
2416
3134
  toolbarTools.map(
2417
3135
  (tool) => renderNativeToolButton({
@@ -2429,7 +3147,7 @@ function NativeVectorToolbar({
2429
3147
  renderToolButton
2430
3148
  })
2431
3149
  ),
2432
- showOverflowMenu ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles.overflowSpacer }) : null,
3150
+ showOverflowMenu ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles2.overflowSpacer }) : null,
2433
3151
  showOverflowMenu ? /* @__PURE__ */ jsxRuntime.jsxs(
2434
3152
  reactNative.Pressable,
2435
3153
  {
@@ -2443,27 +3161,27 @@ function NativeVectorToolbar({
2443
3161
  disabled,
2444
3162
  onPress: toggleOverflow,
2445
3163
  style: ({ pressed }) => [
2446
- styles.overflowTrigger,
2447
- overflowOpen || activeOverflowTool ? styles.activeToolButton : void 0,
2448
- pressed && !disabled ? styles.pressedToolButton : void 0,
2449
- disabled ? styles.disabledToolButton : void 0
3164
+ styles2.overflowTrigger,
3165
+ overflowOpen || activeOverflowTool ? styles2.activeToolButton : void 0,
3166
+ pressed && !disabled ? styles2.pressedToolButton : void 0,
3167
+ disabled ? styles2.disabledToolButton : void 0
2450
3168
  ],
2451
3169
  children: [
2452
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles.iconSlot, children: activeOverflowTool ? renderToolIcon?.({
3170
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles2.iconSlot, children: activeOverflowTool ? renderToolIcon?.({
2453
3171
  tool: activeOverflowTool,
2454
3172
  selected: true,
2455
3173
  disabled,
2456
3174
  foregroundColor: "#18181b",
2457
3175
  onSelect: () => onChange(activeOverflowTool.id)
2458
- }) ?? renderNativeToolFallback(activeOverflowTool, "#18181b") : renderOverflowIcon?.(overflowRenderInput) ?? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.shapesGlyph, children: "S" }) }),
2459
- renderOverflowChevronIcon?.(overflowRenderInput) ?? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.chevronGlyph, children: overflowOpen ? "^" : "v" })
3176
+ }) ?? renderNativeToolFallback(activeOverflowTool, "#18181b") : renderOverflowIcon?.(overflowRenderInput) ?? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.shapesGlyph, children: "S" }) }),
3177
+ renderOverflowChevronIcon?.(overflowRenderInput) ?? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles2.chevronGlyph, children: overflowOpen ? "^" : "v" })
2460
3178
  ]
2461
3179
  }
2462
3180
  ) : null
2463
3181
  ]
2464
3182
  }
2465
3183
  ),
2466
- overflowOpen && showOverflowMenu ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles.overflowPanel, overflowPanelStyle], children: remainingOverflowTools.map(
3184
+ overflowOpen && showOverflowMenu ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles2.overflowPanel, overflowPanelStyle], children: remainingOverflowTools.map(
2467
3185
  (tool) => renderNativeToolButton({
2468
3186
  tool,
2469
3187
  value,
@@ -2474,7 +3192,7 @@ function NativeVectorToolbar({
2474
3192
  disabled,
2475
3193
  disabledIds,
2476
3194
  density: "compact",
2477
- toolButtonStyle: styles.overflowToolButton,
3195
+ toolButtonStyle: styles2.overflowToolButton,
2478
3196
  activeToolButtonStyle,
2479
3197
  toolLabelStyle,
2480
3198
  activeToolLabelStyle,
@@ -2515,22 +3233,22 @@ function renderNativeToolButton(input) {
2515
3233
  disabled: toolDisabled,
2516
3234
  onPress: onSelect,
2517
3235
  style: ({ pressed }) => [
2518
- styles.toolButton,
2519
- input.density === "comfortable" ? styles.comfortableToolButton : void 0,
3236
+ styles2.toolButton,
3237
+ input.density === "comfortable" ? styles2.comfortableToolButton : void 0,
2520
3238
  input.toolButtonStyle,
2521
- selected ? styles.activeToolButton : void 0,
3239
+ selected ? styles2.activeToolButton : void 0,
2522
3240
  selected ? input.activeToolButtonStyle : void 0,
2523
- pressed && !toolDisabled ? styles.pressedToolButton : void 0,
2524
- toolDisabled ? styles.disabledToolButton : void 0
3241
+ pressed && !toolDisabled ? styles2.pressedToolButton : void 0,
3242
+ toolDisabled ? styles2.disabledToolButton : void 0
2525
3243
  ],
2526
3244
  children: [
2527
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles.iconSlot, children: icon }),
3245
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles2.iconSlot, children: icon }),
2528
3246
  input.density === "comfortable" ? /* @__PURE__ */ jsxRuntime.jsx(
2529
3247
  reactNative.Text,
2530
3248
  {
2531
3249
  numberOfLines: 1,
2532
3250
  style: [
2533
- styles.toolLabel,
3251
+ styles2.toolLabel,
2534
3252
  { color: foregroundColor },
2535
3253
  input.toolLabelStyle,
2536
3254
  selected ? input.activeToolLabelStyle : void 0
@@ -2544,9 +3262,9 @@ function renderNativeToolButton(input) {
2544
3262
  );
2545
3263
  }
2546
3264
  function renderNativeToolFallback(tool, foregroundColor, toolLabelStyle) {
2547
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.shortLabel, { color: foregroundColor }, toolLabelStyle], children: tool.shortLabel ?? tool.label.slice(0, 1).toUpperCase() });
3265
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.shortLabel, { color: foregroundColor }, toolLabelStyle], children: tool.shortLabel ?? tool.label.slice(0, 1).toUpperCase() });
2548
3266
  }
2549
- var styles = reactNative.StyleSheet.create({
3267
+ var styles2 = reactNative.StyleSheet.create({
2550
3268
  shell: {
2551
3269
  borderRadius: 8,
2552
3270
  borderWidth: reactNative.StyleSheet.hairlineWidth,
@@ -2868,12 +3586,56 @@ function collectEraserTargetsAtWorldPoint(items, worldX, worldY, options) {
2868
3586
  }
2869
3587
  return ids;
2870
3588
  }
3589
+
3590
+ // src/native/native-tool-cursors.ts
3591
+ var ICON_SIZE = 24;
3592
+ var CENTER_HOTSPOT = { x: 12, y: 12 };
3593
+ function nativeCursorForVectorToolId(toolId) {
3594
+ switch (toolId) {
3595
+ case "rect":
3596
+ case "ellipse":
3597
+ case "architectural-cloud":
3598
+ case "line":
3599
+ case "arrow":
3600
+ case "laser":
3601
+ case "image":
3602
+ return { kind: "crosshair", size: ICON_SIZE, hotspot: CENTER_HOTSPOT };
3603
+ case "draw":
3604
+ return { kind: "draw", size: ICON_SIZE, hotspot: { x: 4, y: 18 } };
3605
+ case "marker":
3606
+ return { kind: "marker", size: ICON_SIZE, hotspot: { x: 11, y: 14 } };
3607
+ case "eraser":
3608
+ return { kind: "eraser", size: ICON_SIZE, hotspot: { x: 12, y: 14 } };
3609
+ case "text":
3610
+ return { kind: "text", size: ICON_SIZE, hotspot: CENTER_HOTSPOT };
3611
+ default:
3612
+ return null;
3613
+ }
3614
+ }
2871
3615
  var MIN_PLACE_SIZE = 8;
2872
3616
  var MIN_ARROW_DRAG_PX = 8;
2873
3617
  var TAP_PX = 20;
3618
+ var MARKER_TOOL_STYLE = {
3619
+ stroke: "#fde047",
3620
+ strokeWidth: 16,
3621
+ strokeOpacity: 0.5
3622
+ };
2874
3623
  function isPlacementTool(toolId) {
2875
3624
  return toolId === "rect" || toolId === "ellipse" || toolId === "architectural-cloud" || toolId === "line" || toolId === "arrow";
2876
3625
  }
3626
+ function isDefaultMarkerToolStyle(style) {
3627
+ return style.stroke === MARKER_TOOL_STYLE.stroke && style.strokeWidth === MARKER_TOOL_STYLE.strokeWidth && style.strokeOpacity === MARKER_TOOL_STYLE.strokeOpacity;
3628
+ }
3629
+ function supportsNativeResizeHandles(item) {
3630
+ const k = item?.toolKind;
3631
+ if (k === "rect" || k === "ellipse" || k === "architectural-cloud" || k === "line" || k === "arrow" || k === "image" || k === "text") {
3632
+ return true;
3633
+ }
3634
+ if ((k === "draw" || k === "pencil" || k === "brush" || k === "marker") && item?.pathPointsLocal && item.pathPointsLocal.length > 0) {
3635
+ return true;
3636
+ }
3637
+ return k === "custom" && !!item?.customIntrinsicSize && !!item?.customInnerSvg;
3638
+ }
2877
3639
  function placementPreviewForTool(toolId, start, end) {
2878
3640
  if (toolId === "rect" || toolId === "ellipse" || toolId === "architectural-cloud") {
2879
3641
  return { kind: toolId, rect: rectFromCorners(start, end) };
@@ -2910,6 +3672,12 @@ function collectIdsInRect(items, marquee) {
2910
3672
  }
2911
3673
  return out;
2912
3674
  }
3675
+ function screenPointFromPointerEvent(event) {
3676
+ const nativeEvent = event.nativeEvent;
3677
+ const x = Number.isFinite(nativeEvent.offsetX) ? nativeEvent.offsetX : nativeEvent.x;
3678
+ const y = Number.isFinite(nativeEvent.offsetY) ? nativeEvent.offsetY : nativeEvent.y;
3679
+ return { x, y };
3680
+ }
2913
3681
  function fitCameraToWorldRect(camera, viewportW, viewportH, worldRect, padding) {
2914
3682
  const r = normalizeRect(worldRect);
2915
3683
  if (r.width <= 0 || r.height <= 0 || viewportW <= 0 || viewportH <= 0) return;
@@ -2936,7 +3704,9 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
2936
3704
  onItemsChange,
2937
3705
  onToolChangeRequest,
2938
3706
  onCameraChange,
2939
- toolbar
3707
+ toolbar,
3708
+ showStyleInspector = false,
3709
+ styleInspectorPlacement = "bottom"
2940
3710
  }, ref) {
2941
3711
  const [size, setSize] = react.useState({ width: 0, height: 0 });
2942
3712
  const cameraRef = react.useRef(null);
@@ -2962,7 +3732,15 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
2962
3732
  );
2963
3733
  const [eraserTrail, setEraserTrail] = react.useState([]);
2964
3734
  const [laserTrail, setLaserTrail] = react.useState([]);
3735
+ const [toolCursorPoint, setToolCursorPoint] = react.useState(null);
2965
3736
  const laserClearTimerRef = react.useRef(null);
3737
+ const strokeStyleRef = react.useRef({ ...DEFAULT_STROKE_STYLE });
3738
+ const markerStrokeStyleRef = react.useRef({ ...MARKER_TOOL_STYLE });
3739
+ const styleBeforeMarkerRef = react.useRef({ ...DEFAULT_STROKE_STYLE });
3740
+ const lastToolIdRef = react.useRef(toolId);
3741
+ const [strokeStyleState, setStrokeStyleState] = react.useState({
3742
+ ...DEFAULT_STROKE_STYLE
3743
+ });
2966
3744
  react.useEffect(
2967
3745
  () => () => {
2968
3746
  if (laserClearTimerRef.current) {
@@ -2971,6 +3749,60 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
2971
3749
  },
2972
3750
  []
2973
3751
  );
3752
+ react.useEffect(() => {
3753
+ if (eraserTrail.length === 0 && laserTrail.length === 0) return;
3754
+ const timer = setInterval(() => {
3755
+ const now = Date.now();
3756
+ setEraserTrail(
3757
+ (prev) => freshTimedTrailPoints(prev, now, ERASER_TRAIL_MAX_AGE_MS)
3758
+ );
3759
+ setLaserTrail(
3760
+ (prev) => freshTimedTrailPoints(prev, now, LASER_TRAIL_MAX_AGE_MS)
3761
+ );
3762
+ }, 50);
3763
+ return () => clearInterval(timer);
3764
+ }, [eraserTrail.length, laserTrail.length]);
3765
+ react.useEffect(() => {
3766
+ const previousToolId = lastToolIdRef.current;
3767
+ const current = strokeStyleRef.current;
3768
+ let next = current;
3769
+ if (toolId === "marker") {
3770
+ if (previousToolId !== "marker") {
3771
+ styleBeforeMarkerRef.current = current;
3772
+ }
3773
+ next = markerStrokeStyleRef.current;
3774
+ } else if (previousToolId === "marker") {
3775
+ markerStrokeStyleRef.current = current;
3776
+ const restored = styleBeforeMarkerRef.current;
3777
+ next = {
3778
+ stroke: restored.stroke,
3779
+ strokeWidth: toolId === "draw" && restored.strokeWidth === DEFAULT_STROKE_STYLE.strokeWidth ? 10 : restored.strokeWidth,
3780
+ ...restored.strokeDash != null ? { strokeDash: restored.strokeDash } : {}
3781
+ };
3782
+ } else if (toolId === "draw") {
3783
+ next = {
3784
+ ...current,
3785
+ strokeWidth: current.strokeWidth === DEFAULT_STROKE_STYLE.strokeWidth ? 10 : current.strokeWidth
3786
+ };
3787
+ } else if (isDefaultMarkerToolStyle(current)) {
3788
+ next = {
3789
+ stroke: DEFAULT_STROKE_STYLE.stroke,
3790
+ strokeWidth: DEFAULT_STROKE_STYLE.strokeWidth,
3791
+ ...current.strokeDash != null ? { strokeDash: current.strokeDash } : {}
3792
+ };
3793
+ }
3794
+ strokeStyleRef.current = next;
3795
+ setStrokeStyleState(next);
3796
+ lastToolIdRef.current = toolId;
3797
+ }, [toolId]);
3798
+ const patchCurrentStrokeStyle = react.useCallback((patch) => {
3799
+ const next = { ...strokeStyleRef.current, ...patch };
3800
+ strokeStyleRef.current = next;
3801
+ if (toolIdRef.current === "marker") {
3802
+ markerStrokeStyleRef.current = next;
3803
+ }
3804
+ setStrokeStyleState(next);
3805
+ }, []);
2974
3806
  const [eraserPreviewIds, setEraserPreviewIds] = react.useState([]);
2975
3807
  const eraserPreviewIdSetRef = react.useRef(/* @__PURE__ */ new Set());
2976
3808
  const requestSelectToolAfterUse = react.useCallback(() => {
@@ -2999,6 +3831,23 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
2999
3831
  const { width, height } = e.nativeEvent.layout;
3000
3832
  setSize({ width, height });
3001
3833
  }, []);
3834
+ const updateToolCursorPoint = react.useCallback(
3835
+ (point) => {
3836
+ if (!interactive) return;
3837
+ if (!nativeCursorForVectorToolId(toolIdRef.current)) return;
3838
+ setToolCursorPoint(point);
3839
+ },
3840
+ [interactive]
3841
+ );
3842
+ const hideToolCursor = react.useCallback(() => {
3843
+ setToolCursorPoint(null);
3844
+ }, []);
3845
+ const handlePointerMove = react.useCallback(
3846
+ (event) => {
3847
+ updateToolCursorPoint(screenPointFromPointerEvent(event));
3848
+ },
3849
+ [updateToolCursorPoint]
3850
+ );
3002
3851
  const selectedItems = react.useMemo(
3003
3852
  () => items.filter((it) => selectedIds.includes(it.id)),
3004
3853
  [items, selectedIds]
@@ -3008,7 +3857,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3008
3857
  const hidden = new Set(eraserPreviewIds);
3009
3858
  return items.filter((it) => !hidden.has(it.id));
3010
3859
  }, [items, eraserPreviewIds]);
3011
- const showResizeHandles = interactive && selectedItems.length === 1 && !selectedItems[0]?.locked;
3860
+ const showResizeHandles = interactive && selectedItems.length === 1 && !selectedItems[0]?.locked && supportsNativeResizeHandles(selectedItems[0]);
3012
3861
  const lastPinchDist = react.useRef(null);
3013
3862
  const lastPanPoint = react.useRef(null);
3014
3863
  const panResponder = react.useMemo(
@@ -3022,9 +3871,11 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3022
3871
  const sx = evt.nativeEvent.locationX;
3023
3872
  const sy = evt.nativeEvent.locationY;
3024
3873
  if (touches && touches.length >= 2) {
3874
+ hideToolCursor();
3025
3875
  dragStateRef.current = { kind: "pan" };
3026
3876
  return;
3027
3877
  }
3878
+ updateToolCursorPoint({ x: sx, y: sy });
3028
3879
  if (!interactive) {
3029
3880
  dragStateRef.current = { kind: "pan" };
3030
3881
  return;
@@ -3083,7 +3934,14 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3083
3934
  clearTimeout(laserClearTimerRef.current);
3084
3935
  laserClearTimerRef.current = null;
3085
3936
  }
3086
- setLaserTrail([{ x: worldX, y: worldY }]);
3937
+ setLaserTrail([{ x: worldX, y: worldY, t: Date.now() }]);
3938
+ } else {
3939
+ setPlacementPreview({
3940
+ kind: "stroke",
3941
+ tool,
3942
+ points: [{ x: worldX, y: worldY }],
3943
+ style: { ...strokeStyleRef.current }
3944
+ });
3087
3945
  }
3088
3946
  return;
3089
3947
  }
@@ -3091,7 +3949,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3091
3949
  dragStateRef.current = { kind: "erase" };
3092
3950
  eraserPreviewIdSetRef.current = /* @__PURE__ */ new Set();
3093
3951
  setEraserPreviewIds([]);
3094
- setEraserTrail([{ x: worldX, y: worldY }]);
3952
+ setEraserTrail([{ x: worldX, y: worldY, t: Date.now() }]);
3095
3953
  const toErase = collectEraserTargetsAtWorldPoint(
3096
3954
  itemsRef.current,
3097
3955
  worldX,
@@ -3143,6 +4001,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3143
4001
  const pageX = evt.nativeEvent.pageX;
3144
4002
  const pageY = evt.nativeEvent.pageY;
3145
4003
  if (touches && touches.length >= 2) {
4004
+ hideToolCursor();
3146
4005
  const t0 = touches[0];
3147
4006
  const t1 = touches[1];
3148
4007
  if (t0 && t1) {
@@ -3162,6 +4021,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3162
4021
  return;
3163
4022
  }
3164
4023
  lastPinchDist.current = null;
4024
+ updateToolCursorPoint({ x: sx, y: sy });
3165
4025
  const { worldX, worldY } = screenToWorld(sx, sy);
3166
4026
  const st = dragStateRef.current;
3167
4027
  if (st.kind === "pan") {
@@ -3182,18 +4042,25 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3182
4042
  const last = pts[pts.length - 1];
3183
4043
  const dx = worldX - (last?.x ?? worldX);
3184
4044
  const dy = worldY - (last?.y ?? worldY);
3185
- if (Math.hypot(dx, dy) > 0.5 / cam.zoom) {
4045
+ const shouldAppendPoint = Math.hypot(dx, dy) > 0.5 / cam.zoom;
4046
+ if (shouldAppendPoint) {
3186
4047
  pts.push({ x: worldX, y: worldY });
3187
4048
  }
3188
4049
  if (st.tool === "laser") {
3189
- setLaserTrail([...pts]);
3190
- } else {
3191
- setPlacementPreview({
3192
- kind: "stroke",
3193
- tool: st.tool,
3194
- points: [...pts]
3195
- });
4050
+ if (shouldAppendPoint) {
4051
+ setLaserTrail((prev) => [
4052
+ ...prev,
4053
+ { x: worldX, y: worldY, t: Date.now() }
4054
+ ]);
4055
+ }
4056
+ return;
3196
4057
  }
4058
+ setPlacementPreview({
4059
+ kind: "stroke",
4060
+ tool: st.tool,
4061
+ points: [...pts],
4062
+ style: { ...strokeStyleRef.current }
4063
+ });
3197
4064
  return;
3198
4065
  }
3199
4066
  if (st.kind === "move") {
@@ -3231,7 +4098,10 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3231
4098
  return;
3232
4099
  }
3233
4100
  if (st.kind === "erase") {
3234
- setEraserTrail((prev) => [...prev, { x: worldX, y: worldY }]);
4101
+ setEraserTrail((prev) => [
4102
+ ...prev,
4103
+ { x: worldX, y: worldY, t: Date.now() }
4104
+ ]);
3235
4105
  const toErase = collectEraserTargetsAtWorldPoint(
3236
4106
  itemsRef.current,
3237
4107
  worldX,
@@ -3257,6 +4127,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3257
4127
  onPanResponderRelease: (evt) => {
3258
4128
  lastPinchDist.current = null;
3259
4129
  lastPanPoint.current = null;
4130
+ hideToolCursor();
3260
4131
  const st = dragStateRef.current;
3261
4132
  if (st.kind === "draw") {
3262
4133
  dragStateRef.current = { kind: "idle" };
@@ -3276,7 +4147,12 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3276
4147
  const change = onItemsChangeRef.current;
3277
4148
  if (!change) return;
3278
4149
  const id = createShapeId();
3279
- const item = createFreehandStrokeItem(id, st.points, st.tool);
4150
+ const item = createFreehandStrokeItem(
4151
+ id,
4152
+ st.points,
4153
+ st.tool,
4154
+ strokeStyleRef.current
4155
+ );
3280
4156
  if (item) {
3281
4157
  change([...itemsRef.current, item]);
3282
4158
  }
@@ -3353,26 +4229,33 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3353
4229
  }
3354
4230
  }
3355
4231
  const id = createShapeId();
4232
+ const style = strokeStyleRef.current;
3356
4233
  if (st.tool === "rect") {
3357
- change([...itemsRef.current, createRectangleItem(id, raw)]);
4234
+ change([...itemsRef.current, createRectangleItem(id, raw, style)]);
3358
4235
  onSelectionChangeRef.current?.([id]);
3359
4236
  requestSelectToolAfterUse();
3360
4237
  return;
3361
4238
  }
3362
4239
  if (st.tool === "ellipse") {
3363
- change([...itemsRef.current, createEllipseItem(id, raw)]);
4240
+ change([...itemsRef.current, createEllipseItem(id, raw, style)]);
3364
4241
  onSelectionChangeRef.current?.([id]);
3365
4242
  requestSelectToolAfterUse();
3366
4243
  return;
3367
4244
  }
3368
4245
  if (st.tool === "architectural-cloud") {
3369
- change([...itemsRef.current, createArchitecturalCloudItem(id, raw)]);
4246
+ change([
4247
+ ...itemsRef.current,
4248
+ createArchitecturalCloudItem(id, raw, style)
4249
+ ]);
3370
4250
  onSelectionChangeRef.current?.([id]);
3371
4251
  requestSelectToolAfterUse();
3372
4252
  return;
3373
4253
  }
3374
4254
  const line = lineEndpointsToLocal(br, lineStart, lineEnd);
3375
- change([...itemsRef.current, createLineItem(id, br, line, st.tool)]);
4255
+ change([
4256
+ ...itemsRef.current,
4257
+ createLineItem(id, br, line, st.tool, style)
4258
+ ]);
3376
4259
  onSelectionChangeRef.current?.([id]);
3377
4260
  requestSelectToolAfterUse();
3378
4261
  return;
@@ -3395,7 +4278,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3395
4278
  height: 26
3396
4279
  },
3397
4280
  "Text",
3398
- void 0,
4281
+ strokeStyleRef.current,
3399
4282
  18
3400
4283
  );
3401
4284
  change([...itemsRef.current, item]);
@@ -3428,12 +4311,23 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3428
4311
  onPanResponderTerminate: () => {
3429
4312
  lastPinchDist.current = null;
3430
4313
  lastPanPoint.current = null;
4314
+ hideToolCursor();
3431
4315
  dragStateRef.current = { kind: "idle" };
3432
4316
  setPlacementPreview(null);
3433
4317
  setLaserTrail([]);
4318
+ setEraserTrail([]);
4319
+ setEraserPreviewIds([]);
4320
+ eraserPreviewIdSetRef.current.clear();
3434
4321
  }
3435
4322
  }),
3436
- [screenToWorld, requestRender, requestSelectToolAfterUse, interactive]
4323
+ [
4324
+ screenToWorld,
4325
+ requestRender,
4326
+ requestSelectToolAfterUse,
4327
+ interactive,
4328
+ updateToolCursorPoint,
4329
+ hideToolCursor
4330
+ ]
3437
4331
  );
3438
4332
  react.useImperativeHandle(
3439
4333
  ref,
@@ -3456,11 +4350,17 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3456
4350
  }),
3457
4351
  [requestRender, size]
3458
4352
  );
4353
+ const activeStyleToolId = toolId === "draw" || toolId === "marker" ? toolId : null;
4354
+ const activeToolCursor = nativeCursorForVectorToolId(toolId);
4355
+ const toolCursor = activeToolCursor && toolCursorPoint ? { cursor: activeToolCursor, point: toolCursorPoint } : null;
3459
4356
  return /* @__PURE__ */ jsxRuntime.jsx(
3460
4357
  reactNative.View,
3461
4358
  {
3462
4359
  style: { flex: 1, overflow: "hidden" },
3463
4360
  onLayout,
4361
+ onPointerMove: handlePointerMove,
4362
+ onPointerEnter: handlePointerMove,
4363
+ onPointerLeave: hideToolCursor,
3464
4364
  ...panResponder.panHandlers,
3465
4365
  children: size.width > 0 && size.height > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3466
4366
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -3485,9 +4385,37 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3485
4385
  eraserTrail,
3486
4386
  eraserPreviewItems: items.filter(
3487
4387
  (it) => eraserPreviewIds.includes(it.id)
3488
- )
4388
+ ),
4389
+ previewStrokeStyle: strokeStyleState,
4390
+ toolCursor
3489
4391
  }
3490
4392
  ),
4393
+ interactive && showStyleInspector && activeStyleToolId ? /* @__PURE__ */ jsxRuntime.jsx(
4394
+ reactNative.View,
4395
+ {
4396
+ pointerEvents: "box-none",
4397
+ style: styleInspectorPlacement === "top-left" ? {
4398
+ position: "absolute",
4399
+ left: 16,
4400
+ top: 104,
4401
+ alignItems: "flex-start"
4402
+ } : {
4403
+ position: "absolute",
4404
+ left: 16,
4405
+ right: 16,
4406
+ bottom: 84,
4407
+ alignItems: "center"
4408
+ },
4409
+ children: /* @__PURE__ */ jsxRuntime.jsx(
4410
+ NativeVectorStyleInspector,
4411
+ {
4412
+ toolId: activeStyleToolId,
4413
+ value: strokeStyleState,
4414
+ onChange: patchCurrentStrokeStyle
4415
+ }
4416
+ )
4417
+ }
4418
+ ) : null,
3491
4419
  toolbar && /* @__PURE__ */ jsxRuntime.jsx(
3492
4420
  reactNative.View,
3493
4421
  {
@@ -3511,14 +4439,18 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3511
4439
 
3512
4440
  exports.DEFAULT_NATIVE_OVERFLOW_TOOL_IDS = DEFAULT_NATIVE_OVERFLOW_TOOL_IDS;
3513
4441
  exports.DEFAULT_NATIVE_VECTOR_TOOLS = DEFAULT_NATIVE_VECTOR_TOOLS;
4442
+ exports.NATIVE_STYLE_PALETTE = NATIVE_STYLE_PALETTE;
3514
4443
  exports.NativeInteractionOverlay = NativeInteractionOverlay;
3515
4444
  exports.NativeSceneRenderer = NativeSceneRenderer;
3516
4445
  exports.NativeShapeRenderer = NativeShapeRenderer;
4446
+ exports.NativeVectorStyleInspector = NativeVectorStyleInspector;
3517
4447
  exports.NativeVectorToolbar = NativeVectorToolbar;
3518
4448
  exports.NativeVectorViewport = NativeVectorViewport;
3519
4449
  exports.createFreehandStrokeItem = createFreehandStrokeItem;
3520
4450
  exports.createImageItem = createImageItem;
3521
4451
  exports.createShapeId = createShapeId;
4452
+ exports.nativeStyleColorWithOpacity = nativeStyleColorWithOpacity;
4453
+ exports.normalizeNativeStyleHex = normalizeNativeStyleHex;
3522
4454
  exports.parseSvgFragment = parseSvgFragment;
3523
4455
  //# sourceMappingURL=native.cjs.map
3524
4456
  //# sourceMappingURL=native.cjs.map