@arronqzy/react-view 1.0.38 → 1.0.40

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.
@@ -1,26 +1,63 @@
1
- import React, { useEffect, useMemo, useState } from "react";
1
+ import React, { useEffect, useMemo, useRef, useState } from "react";
2
2
  import Ruler from "@scena/react-ruler";
3
3
  import { PANEL_Z_INDEX } from "../constants/zIndex";
4
4
 
5
- const RULER_WORLD_UNIT = 100;
6
5
  const RULER_SEGMENT = 10;
6
+ /** 主刻度标签在屏幕上的目标间距(px),过密/过疏时动态调 unit */
7
+ const RULER_TARGET_MAJOR_PX = 78;
8
+ /** @scena/react-ruler 内部把刻度文字原点写死为 17px,厚度必须大于该值才看得见数字 */
9
+ const SCENA_TEXT_ORIGIN = 17;
7
10
 
8
11
  export type PanelRulersProps = {
9
12
  zoomX: number;
10
13
  zoomY: number;
11
14
  scrollLeft: number;
12
15
  scrollTop: number;
16
+ /** 固定厚度;省略时按编辑区尺寸动态计算 */
13
17
  size?: number;
14
18
  };
15
19
 
20
+ function clamp(n: number, min: number, max: number) {
21
+ return Math.min(max, Math.max(min, n));
22
+ }
23
+
24
+ /** 1/2/5 × 10^n 好看步长 */
25
+ function niceNumber(raw: number): number {
26
+ if (!Number.isFinite(raw) || raw <= 0) return 1;
27
+ const exp = Math.floor(Math.log10(raw));
28
+ const frac = raw / 10 ** exp;
29
+ const niceFrac = frac <= 1 ? 1 : frac <= 2 ? 2 : frac <= 5 ? 5 : 10;
30
+ return niceFrac * 10 ** exp;
31
+ }
32
+
33
+ /**
34
+ * 按缩放动态主刻度单位:屏幕上主刻度间距 roughly 恒定,
35
+ * 缩小 → unit 变大(标签变少),放大 → unit 变小(标签变密)。
36
+ */
37
+ export function resolveRulerUnit(zoom: number): number {
38
+ const z = Math.max(zoom, 0.0001);
39
+ return Math.max(1, niceNumber(RULER_TARGET_MAJOR_PX / z));
40
+ }
41
+
42
+ /** 按画布可视区域动态计算刻度尺厚度(保证刻度数字可读) */
43
+ export function resolveRulerSize(width: number, height: number): number {
44
+ const edge = Math.min(width || 0, height || 0);
45
+ if (edge <= 0) return 22;
46
+ // 下限 24:文字带与刻度带分离,避免数字压住刻度线
47
+ return Math.round(clamp(edge * 0.032, 24, 30));
48
+ }
49
+
16
50
  export function PanelRulers({
17
51
  zoomX,
18
52
  zoomY,
19
53
  scrollLeft,
20
54
  scrollTop,
21
- size = 32,
55
+ size: sizeProp,
22
56
  }: PanelRulersProps) {
57
+ const rootRef = useRef<HTMLDivElement | null>(null);
58
+ const [autoSize, setAutoSize] = useState(22);
23
59
  const [isDark, setIsDark] = useState(false);
60
+
24
61
  useEffect(() => {
25
62
  const root = document.documentElement;
26
63
  const update = () => setIsDark(root.classList.contains("dark"));
@@ -30,60 +67,150 @@ export function PanelRulers({
30
67
  return () => observer.disconnect();
31
68
  }, []);
32
69
 
33
- const rulerScrollLeft = scrollLeft + size;
34
- const rulerScrollTop = scrollTop + size;
70
+ useEffect(() => {
71
+ if (typeof sizeProp === "number") return;
72
+ const el = rootRef.current;
73
+ if (!el) return;
74
+ const update = () => {
75
+ const rect = el.getBoundingClientRect();
76
+ setAutoSize(resolveRulerSize(rect.width, rect.height));
77
+ };
78
+ update();
79
+ const ro = new ResizeObserver(update);
80
+ ro.observe(el);
81
+ return () => ro.disconnect();
82
+ }, [sizeProp]);
35
83
 
36
- const horizontalProps = useMemo(
37
- () => ({
38
- zoom: zoomX,
39
- unit: RULER_WORLD_UNIT,
40
- segment: RULER_SEGMENT,
41
- backgroundColor: isDark ? "rgba(15,23,42,0.98)" : "rgba(255,255,255,0.98)",
42
- lineColor: isDark ? "rgba(226,232,240,0.4)" : "rgba(15,23,42,0.22)",
43
- textColor: isDark ? "rgba(248,250,252,0.95)" : "rgba(15,23,42,0.75)",
44
- font: "10px system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial",
45
- longLineSize: 8,
46
- shortLineSize: 4,
47
- }),
48
- [isDark, zoomX]
49
- );
84
+ const size = typeof sizeProp === "number" ? sizeProp : autoSize;
85
+ const s = `${size}px`;
86
+ const fontPx = Math.max(10, Math.min(11, Math.round(size * 0.38)));
87
+ const bg = isDark ? "rgba(15,23,42,0.98)" : "rgba(255,255,255,0.98)";
88
+ const lineColor = isDark ? "rgba(226,232,240,0.85)" : "rgba(15,23,42,0.55)";
89
+ const textColor = isDark ? "#f8fafc" : "#0f172a";
90
+ const seam = isDark ? "rgba(226,232,240,0.45)" : "rgba(15,23,42,0.28)";
50
91
 
51
- const verticalProps = useMemo(
92
+ // 刻度线只占内侧约 30%,数字在外侧,互不遮挡
93
+ const longLineSize = Math.max(4, Math.min(Math.round(size * 0.3), size - fontPx - 4));
94
+ const shortLineSize = Math.max(2, Math.round(longLineSize * 0.55));
95
+ // @scena 文字原点写死 17px
96
+ // end(顶/左): baseline=bottom,目标底边 ≈ fontPx+1
97
+ // start(底/右): baseline=top,目标顶边 ≈ size-fontPx-2
98
+ // 横向 offset=[沿尺, 厚度];纵向 offset=[厚度, 沿尺]
99
+ const textBandEnd = fontPx + 1 - (size - SCENA_TEXT_ORIGIN);
100
+ const textBandStart = size - fontPx - 2 - SCENA_TEXT_ORIGIN;
101
+ const textOffsetHEnd: [number, number] = [3, textBandEnd];
102
+ const textOffsetHStart: [number, number] = [3, textBandStart];
103
+ const textOffsetVEnd: [number, number] = [textBandEnd, 3];
104
+ const textOffsetVStart: [number, number] = [textBandStart, 3];
105
+
106
+ const rulerScrollLeft = scrollLeft + size * (1 - 1 / Math.max(zoomX, 0.0001));
107
+ const rulerScrollTop = scrollTop + size * (1 - 1 / Math.max(zoomY, 0.0001));
108
+ const unitX = useMemo(() => resolveRulerUnit(zoomX), [zoomX]);
109
+ const unitY = useMemo(() => resolveRulerUnit(zoomY), [zoomY]);
110
+
111
+ const sharedPaint = useMemo(
52
112
  () => ({
53
- zoom: zoomY,
54
- unit: RULER_WORLD_UNIT,
55
113
  segment: RULER_SEGMENT,
56
- backgroundColor: isDark ? "rgba(15,23,42,0.98)" : "rgba(255,255,255,0.98)",
57
- lineColor: isDark ? "rgba(226,232,240,0.4)" : "rgba(15,23,42,0.22)",
58
- textColor: isDark ? "rgba(248,250,252,0.95)" : "rgba(15,23,42,0.75)",
59
- font: "10px system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial",
60
- longLineSize: 8,
61
- shortLineSize: 4,
114
+ backgroundColor: bg,
115
+ lineColor,
116
+ textColor,
117
+ font: `500 ${fontPx}px system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial`,
118
+ longLineSize,
119
+ shortLineSize,
120
+ mainLineSize: longLineSize + 2,
62
121
  }),
63
- [isDark, zoomY]
122
+ [bg, fontPx, lineColor, longLineSize, shortLineSize, textColor]
64
123
  );
65
124
 
125
+ const clipTop = `polygon(0 0, 100% 0, calc(100% - ${s}) 100%, ${s} 100%)`;
126
+ const clipBottom = `polygon(${s} 0, calc(100% - ${s}) 0, 100% 100%, 0 100%)`;
127
+ const clipLeft = `polygon(0 0, 100% ${s}, 100% calc(100% - ${s}), 0 100%)`;
128
+ const clipRight = `polygon(0 ${s}, 100% 0, 100% 100%, 0 calc(100% - ${s}))`;
129
+
66
130
  return (
67
131
  <div
132
+ ref={rootRef}
68
133
  className="pointer-events-none absolute inset-0 isolate"
69
- style={{ ["--rv-ruler-size" as string]: `${size}px`, zIndex: PANEL_Z_INDEX.ruler }}
134
+ style={{ ["--rv-ruler-size" as string]: s, zIndex: PANEL_Z_INDEX.ruler }}
70
135
  >
71
- <div className="absolute left-[var(--rv-ruler-size)] right-[var(--rv-ruler-size)] top-0 h-[var(--rv-ruler-size)] overflow-visible">
72
- <Ruler type="horizontal" {...horizontalProps} scrollPos={rulerScrollLeft} />
136
+ {/* 顶:刻度朝下(end),数字在上半区 */}
137
+ <div
138
+ className="absolute inset-x-0 top-0 overflow-hidden"
139
+ style={{ height: s, clipPath: clipTop, backgroundColor: bg }}
140
+ >
141
+ <Ruler
142
+ key={`h-end-${unitX}`}
143
+ type="horizontal"
144
+ {...sharedPaint}
145
+ unit={unitX}
146
+ zoom={zoomX}
147
+ direction="end"
148
+ textOffset={textOffsetHEnd}
149
+ scrollPos={rulerScrollLeft}
150
+ />
73
151
  </div>
74
- <div className="absolute left-0 top-[var(--rv-ruler-size)] bottom-[var(--rv-ruler-size)] w-[var(--rv-ruler-size)] overflow-visible">
75
- <Ruler type="vertical" {...verticalProps} scrollPos={rulerScrollTop} />
152
+ {/* 左:刻度朝右(end),数字在左半区 */}
153
+ <div
154
+ className="absolute inset-y-0 left-0 overflow-hidden"
155
+ style={{ width: s, clipPath: clipLeft, backgroundColor: bg }}
156
+ >
157
+ <Ruler
158
+ key={`v-end-${unitY}`}
159
+ type="vertical"
160
+ {...sharedPaint}
161
+ unit={unitY}
162
+ zoom={zoomY}
163
+ direction="end"
164
+ textOffset={textOffsetVEnd}
165
+ scrollPos={rulerScrollTop}
166
+ />
76
167
  </div>
77
- <div className="absolute left-[var(--rv-ruler-size)] right-[var(--rv-ruler-size)] bottom-0 h-[var(--rv-ruler-size)] overflow-visible">
78
- <Ruler type="horizontal" {...horizontalProps} scrollPos={rulerScrollLeft} />
168
+ {/* 底:刻度朝上(start),数字在下半区 */}
169
+ <div
170
+ className="absolute inset-x-0 bottom-0 overflow-hidden"
171
+ style={{ height: s, clipPath: clipBottom, backgroundColor: bg }}
172
+ >
173
+ <Ruler
174
+ key={`h-start-${unitX}`}
175
+ type="horizontal"
176
+ {...sharedPaint}
177
+ unit={unitX}
178
+ zoom={zoomX}
179
+ direction="start"
180
+ textOffset={textOffsetHStart}
181
+ scrollPos={rulerScrollLeft}
182
+ />
79
183
  </div>
80
- <div className="absolute right-0 top-[var(--rv-ruler-size)] bottom-[var(--rv-ruler-size)] w-[var(--rv-ruler-size)] overflow-visible">
81
- <Ruler type="vertical" {...verticalProps} scrollPos={rulerScrollTop} />
184
+ {/* 右:刻度朝左(start),数字在右半区 */}
185
+ <div
186
+ className="absolute inset-y-0 right-0 overflow-hidden"
187
+ style={{ width: s, clipPath: clipRight, backgroundColor: bg }}
188
+ >
189
+ <Ruler
190
+ key={`v-start-${unitY}`}
191
+ type="vertical"
192
+ {...sharedPaint}
193
+ unit={unitY}
194
+ zoom={zoomY}
195
+ direction="start"
196
+ textOffset={textOffsetVStart}
197
+ scrollPos={rulerScrollTop}
198
+ />
82
199
  </div>
83
- <div className="absolute left-0 top-0 h-[var(--rv-ruler-size)] w-[var(--rv-ruler-size)] border-b border-r border-border bg-background" />
84
- <div className="absolute right-0 top-0 h-[var(--rv-ruler-size)] w-[var(--rv-ruler-size)] border-b border-l border-border bg-background" />
85
- <div className="absolute left-0 bottom-0 h-[var(--rv-ruler-size)] w-[var(--rv-ruler-size)] border-r border-t border-border bg-background" />
86
- <div className="absolute right-0 bottom-0 h-[var(--rv-ruler-size)] w-[var(--rv-ruler-size)] border-l border-t border-border bg-background" />
200
+
201
+ {/* 四角斜缝:SVG 明确画外角→内角 */}
202
+ <svg aria-hidden className="pointer-events-none absolute left-0 top-0" width={size} height={size} style={{ zIndex: 1 }}>
203
+ <line x1={0} y1={0} x2={size} y2={size} stroke={seam} strokeWidth={1} />
204
+ </svg>
205
+ <svg aria-hidden className="pointer-events-none absolute right-0 top-0" width={size} height={size} style={{ zIndex: 1 }}>
206
+ <line x1={size} y1={0} x2={0} y2={size} stroke={seam} strokeWidth={1} />
207
+ </svg>
208
+ <svg aria-hidden className="pointer-events-none absolute bottom-0 left-0" width={size} height={size} style={{ zIndex: 1 }}>
209
+ <line x1={0} y1={size} x2={size} y2={0} stroke={seam} strokeWidth={1} />
210
+ </svg>
211
+ <svg aria-hidden className="pointer-events-none absolute bottom-0 right-0" width={size} height={size} style={{ zIndex: 1 }}>
212
+ <line x1={size} y1={size} x2={0} y2={0} stroke={seam} strokeWidth={1} />
213
+ </svg>
87
214
  </div>
88
215
  );
89
216
  }
@@ -44,8 +44,8 @@ export function SelectLayer({
44
44
 
45
45
  return (
46
46
  <Selecto
47
- // container 可以是 transform 过的画布;rootContainer 必须是未 transform 的真实坐标系容器
48
- container={container}
47
+ // 框选层挂在未 scale 的视口上,避免选区宽高被画布 transform 放大、线宽视觉变粗
48
+ container={dragRoot}
49
49
  rootContainer={dragRoot}
50
50
  // 选择框渲染到 body,避免受滚动/缩放影响产生偏移
51
51
  portalContainer={overlayContainer}
@@ -74,7 +74,7 @@ export function WorkspaceProjectNav({
74
74
 
75
75
  return (
76
76
  <>
77
- <div className="flex items-center gap-1.5 border-l border-border pl-2">
77
+ <div className="flex items-center gap-1.5 border-l border-border/60 pl-2.5">
78
78
  <TooltipProvider delayDuration={150}>
79
79
  <Tooltip>
80
80
  <TooltipTrigger asChild>