@dcg-overseas/number-line 0.1.2 → 0.1.4
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/README.md +255 -0
- package/dist/index.cjs +89 -39
- package/dist/index.d.ts +11 -2
- package/dist/index.js +90 -40
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# @dcg-overseas/number-line
|
|
2
|
+
|
|
3
|
+
交互式数轴组件,支持分组弧线(带箭头终点标签)、自由画笔、橡皮擦、撤销与键盘快捷键。
|
|
4
|
+
|
|
5
|
+
- 容器自适应:`viewBox` 跟随容器实际像素,文字和线条始终保持设计尺寸
|
|
6
|
+
- 笔迹归一化存储(`[0,1]` 坐标 + `vectorEffect="non-scaling-stroke"`),容器 resize 后笔迹位置稳定不漂移
|
|
7
|
+
- Provider/Component 解耦:状态在 `NumberLineProvider` 中管理,工具栏与数轴 SVG 各自独立组装
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 安装
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @dcg-overseas/number-line
|
|
15
|
+
# peer deps
|
|
16
|
+
pnpm add react react-dom
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 快速开始
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { NumberLineProvider, NumberLine } from '@dcg-overseas/number-line'
|
|
25
|
+
|
|
26
|
+
function Demo() {
|
|
27
|
+
return (
|
|
28
|
+
<NumberLineProvider
|
|
29
|
+
min={0}
|
|
30
|
+
max={100}
|
|
31
|
+
groupSize={10}
|
|
32
|
+
groupCount={10}
|
|
33
|
+
tickStep={5}
|
|
34
|
+
>
|
|
35
|
+
<div style={{ width: '100%', height: 200 }}>
|
|
36
|
+
<NumberLine />
|
|
37
|
+
</div>
|
|
38
|
+
</NumberLineProvider>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
> ⚠️ **必须给 `<NumberLine />` 的父容器明确的宽高**(CSS 尺寸或 flex 布局),SVG 默认 `width:100% height:100%`,没有外部尺寸约束会塌陷为 0。
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## API
|
|
48
|
+
|
|
49
|
+
### `<NumberLineProvider>`
|
|
50
|
+
|
|
51
|
+
数轴的状态根。包裹任何需要访问数轴上下文的子组件(`<NumberLine />` 和你自己的工具栏按钮)。
|
|
52
|
+
|
|
53
|
+
| Prop | 类型 | 默认值 | 说明 |
|
|
54
|
+
|------|------|-------|------|
|
|
55
|
+
| `min` | `number` | — | 数轴起点值(含) |
|
|
56
|
+
| `max` | `number` | — | 数轴终点值(含) |
|
|
57
|
+
| `groupSize` | `number` | — | 每组弧线跨度(数轴单位)。例:`groupSize=8` 表示每段弧从 `n` 跳到 `n+8` |
|
|
58
|
+
| `groupCount` | `number` | — | 弧线组数。从 `min` 起依次绘制 `groupCount` 段弧线 |
|
|
59
|
+
| `tickStep` | `number` | — | 主刻度步长(数轴单位)。例:`tickStep=5` 表示 0、5、10、15… 为主刻度 |
|
|
60
|
+
| `arcColors` | `string[]` | 5 色循环 | 弧线颜色数组,按 `groupIndex % length` 轮换。默认值见 [`utils/geometry.ts` ARC_COLORS](src/utils/geometry.ts) |
|
|
61
|
+
| `showGroupTicks` | `boolean` | `true` | 是否显示分组边界刻度(最长,10px) |
|
|
62
|
+
| `showMajorTicks` | `boolean` | `true` | 是否显示主刻度(中等,7px) |
|
|
63
|
+
| `showMinorTicks` | `boolean` | `false` | 是否显示次刻度(最短,4px)。默认隐藏 |
|
|
64
|
+
| `children` | `ReactNode` | — | 子节点(必须包含 `<NumberLine />` 或自定义渲染器) |
|
|
65
|
+
|
|
66
|
+
#### 弧线生成规则
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
弧线 g 的范围:[min + g*groupSize, min + (g+1)*groupSize]
|
|
70
|
+
g ∈ [0, groupCount)
|
|
71
|
+
若 (min + (g+1)*groupSize) > max,弧线提前停止
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### 三档刻度优先级
|
|
75
|
+
|
|
76
|
+
一个位置可能同时是多档刻度(例如 `tickStep=5, groupSize=10` 时,10 既是主刻度也是分组边界)。视觉与可见性按 **分组边界 > 主刻度 > 次刻度** 归类,仅看其最高档对应的 `show*Ticks`。
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
// 默认:只显示主刻度 + 分组边界
|
|
80
|
+
<NumberLineProvider min={0} max={100} groupSize={10} groupCount={10} tickStep={5} />
|
|
81
|
+
|
|
82
|
+
// 显示全部三档刻度
|
|
83
|
+
<NumberLineProvider ... showMinorTicks />
|
|
84
|
+
|
|
85
|
+
// 极简模式:仅分组边界
|
|
86
|
+
<NumberLineProvider ... showMajorTicks={false} />
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
### `<NumberLine>`
|
|
92
|
+
|
|
93
|
+
数轴 SVG 视图。必须放在 `<NumberLineProvider>` 内,自动从 context 读取所有配置和事件处理。
|
|
94
|
+
|
|
95
|
+
| Prop | 类型 | 默认值 | 说明 |
|
|
96
|
+
|------|------|-------|------|
|
|
97
|
+
| `className` | `string?` | — | 追加到 SVG 根节点的 className(基础类名 `nl-svg`) |
|
|
98
|
+
|
|
99
|
+
**渲染层级(从下到上):**
|
|
100
|
+
1. `AxisLayer` — 数轴线 + 刻度 + 标签
|
|
101
|
+
2. `GroupArcsLayer` — 分组弧线 + 弧顶箭头标签 + 选中高亮
|
|
102
|
+
3. `DrawingLayer` — 用户笔迹(normalized `[0,1]` 坐标,scale 还原)
|
|
103
|
+
|
|
104
|
+
**布局参数(内部常量,无需配置):**
|
|
105
|
+
|
|
106
|
+
| 名称 | 值 | 含义 |
|
|
107
|
+
|------|---|------|
|
|
108
|
+
| `PAD_LEFT` | 30 | 数轴左 padding |
|
|
109
|
+
| `PAD_RIGHT` | 40 | 数轴右 padding |
|
|
110
|
+
| `AXIS_BOTTOM_OFFSET` | 50 | 数轴线距容器底部像素(给刻度数字留空间) |
|
|
111
|
+
| `ARC_TOP_PADDING` | 16 | 弧线顶部距容器顶部像素 |
|
|
112
|
+
| `MIN_ARC_HEIGHT` | 24 | 弧线最小高度(容器极小时兜底) |
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
### `useNumberLineContext()`
|
|
117
|
+
|
|
118
|
+
读取 / 操作数轴状态的 hook。**必须在 `<NumberLineProvider>` 子树内调用**,否则抛错。
|
|
119
|
+
|
|
120
|
+
返回 `NumberLineContextValue`:
|
|
121
|
+
|
|
122
|
+
#### 布局
|
|
123
|
+
| 字段 | 类型 | 说明 |
|
|
124
|
+
|------|------|------|
|
|
125
|
+
| `svgRef` | `RefObject<SVGSVGElement>` | SVG 元素 ref |
|
|
126
|
+
| `containerWidth` | `number` | 容器实际像素宽 |
|
|
127
|
+
| `containerHeight` | `number` | 容器实际像素高 |
|
|
128
|
+
| `svgCursor` | `string` | 当前 SVG cursor(自动随 tool 切换) |
|
|
129
|
+
|
|
130
|
+
#### 配置(透传自 Provider props)
|
|
131
|
+
| 字段 | 类型 |
|
|
132
|
+
|------|------|
|
|
133
|
+
| `min` `max` `groupSize` `groupCount` `tickStep` | `number` |
|
|
134
|
+
| `arcColors` | `string[] \| undefined` |
|
|
135
|
+
| `showGroupTicks` `showMajorTicks` `showMinorTicks` | `boolean` |
|
|
136
|
+
|
|
137
|
+
#### 绘图状态
|
|
138
|
+
| 字段 | 类型 | 说明 |
|
|
139
|
+
|------|------|------|
|
|
140
|
+
| `tool` | `'none' \| 'pen' \| 'eraser'` | 当前工具 |
|
|
141
|
+
| `strokes` | `Stroke[]` | 已落笔的所有笔迹 |
|
|
142
|
+
| `liveStroke` | `Stroke \| null` | 正在绘制中的笔迹(半透明预览) |
|
|
143
|
+
| `selectedArcIndices` | `Set<number>` | 当前选中的弧线 group index |
|
|
144
|
+
| `deletedArcIndices` | `Set<number>` | 已被擦除/删除的弧线 group index |
|
|
145
|
+
|
|
146
|
+
#### 事件回调(已绑定到 `<NumberLine>`,自定义渲染时使用)
|
|
147
|
+
| 字段 | 类型 |
|
|
148
|
+
|------|------|
|
|
149
|
+
| `onPointerDown` `onPointerMove` `onPointerUp` | `(e: PointerEvent) => void` |
|
|
150
|
+
| `onStrokeClick` | `(id: string, e: PointerEvent) => void` |
|
|
151
|
+
| `onArcClick` | `(groupIndex: number, e: PointerEvent) => void` |
|
|
152
|
+
|
|
153
|
+
#### 工具栏 API
|
|
154
|
+
| 字段 | 类型 | 说明 |
|
|
155
|
+
|------|------|------|
|
|
156
|
+
| `canDelete` | `boolean` | 是否有可删除的选中项(笔迹或弧线) |
|
|
157
|
+
| `canUndo` | `boolean` | 是否有可撤销的操作 |
|
|
158
|
+
| `onTogglePen` | `() => void` | 切换画笔工具 |
|
|
159
|
+
| `onToggleEraser` | `() => void` | 切换橡皮擦 |
|
|
160
|
+
| `onDelete` | `() => void` | 删除当前选中 |
|
|
161
|
+
| `onReset` | `() => void` | 清空所有笔迹和弧线删除记录 |
|
|
162
|
+
| `onUndo` | `() => void` | 撤销上一步操作 |
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## 自定义工具栏示例
|
|
167
|
+
|
|
168
|
+
`<NumberLine>` 不附带工具栏。把按钮和数轴一起放在 `<NumberLineProvider>` 下,通过 `useNumberLineContext` 接入:
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
import { NumberLineProvider, NumberLine, useNumberLineContext } from '@dcg-overseas/number-line'
|
|
172
|
+
|
|
173
|
+
function Toolbar() {
|
|
174
|
+
const { tool, canDelete, canUndo, onTogglePen, onToggleEraser, onDelete, onReset, onUndo } =
|
|
175
|
+
useNumberLineContext()
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div className="flex gap-2">
|
|
179
|
+
<button data-active={tool === 'pen'} onClick={onTogglePen}>✏️ 画笔</button>
|
|
180
|
+
<button data-active={tool === 'eraser'} onClick={onToggleEraser}>🩹 擦除</button>
|
|
181
|
+
<button disabled={!canDelete} onClick={onDelete}>🗑 删除</button>
|
|
182
|
+
<button onClick={onReset}>♻️ 重置</button>
|
|
183
|
+
<button disabled={!canUndo} onClick={onUndo}>↶ 撤销</button>
|
|
184
|
+
</div>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function App() {
|
|
189
|
+
return (
|
|
190
|
+
<NumberLineProvider min={0} max={80} groupSize={8} groupCount={10} tickStep={1}>
|
|
191
|
+
<Toolbar />
|
|
192
|
+
<div style={{ height: 220 }}>
|
|
193
|
+
<NumberLine />
|
|
194
|
+
</div>
|
|
195
|
+
</NumberLineProvider>
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 键盘快捷键
|
|
203
|
+
|
|
204
|
+
快捷键绑定在 SVG 元素上(`tabIndex={0}`,需先聚焦 SVG),多实例互不干扰,也不会拦截外部输入框。
|
|
205
|
+
|
|
206
|
+
| 按键 | 行为 |
|
|
207
|
+
|------|------|
|
|
208
|
+
| `Esc` | 切回 `'none'` 模式,清空弧线选中 |
|
|
209
|
+
| `Delete` / `Backspace` | 删除当前选中的笔迹和弧线 |
|
|
210
|
+
| `Ctrl+Z` / `Cmd+Z` | 撤销上一步 |
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## 类型导出
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
import type {
|
|
218
|
+
NumberLineProps,
|
|
219
|
+
NumberLineContextValue,
|
|
220
|
+
} from '@dcg-overseas/number-line'
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
完整类型见 [src/types.ts](src/types.ts) 与 [src/context/NumberLineContext.ts](src/context/NumberLineContext.ts)。
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## 设计要点
|
|
228
|
+
|
|
229
|
+
### 自适应坐标系
|
|
230
|
+
|
|
231
|
+
`viewBox` 动态跟随容器实际像素(`viewBox={`0 0 ${w} ${h}`}` + `preserveAspectRatio="none"`),1 SVG 单位 = 1 CSS 像素。这样:
|
|
232
|
+
|
|
233
|
+
- 文字和线条始终是设计尺寸(fontSize=11 永远是 11px),不会因为容器纵横比异常被缩到看不见
|
|
234
|
+
- 数轴自动贴近容器底部,弧线撑满上方空间,无空白浪费
|
|
235
|
+
|
|
236
|
+
### 笔迹归一化(resize 稳定)
|
|
237
|
+
|
|
238
|
+
笔迹存储为 `[0, 1]` 归一化坐标(`M 0.234 0.456 L ...`)。渲染时用 `<g transform={`scale(${w} ${h})`}>` 还原到容器像素,并配合 `vectorEffect="non-scaling-stroke"` 锁定线宽。
|
|
239
|
+
|
|
240
|
+
效果:容器从 `400×200` 变成 `600×300`,所有笔迹的相对位置仍然贴合数轴,stroke 宽度保持 2.5px。
|
|
241
|
+
|
|
242
|
+
### 操作级撤销
|
|
243
|
+
|
|
244
|
+
撤销栈记录的是「操作」而非「状态」(`HistoryOp`),分两类:
|
|
245
|
+
|
|
246
|
+
- `add` — 新增笔迹,撤销 = 移除指定 id
|
|
247
|
+
- `delete` — 删除笔迹/弧线,撤销 = 恢复删除前完整快照 + 还原弧线 group index
|
|
248
|
+
|
|
249
|
+
这样即使一次 `delete` 同时删了多条笔迹和多条弧线,撤销也能精确还原。
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## License
|
|
254
|
+
|
|
255
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -47,16 +47,20 @@ function useDrawing() {
|
|
|
47
47
|
});
|
|
48
48
|
setStrokes((prev) => prev.map((s) => ({ ...s, selected: false })));
|
|
49
49
|
}, []);
|
|
50
|
-
const startStroke = React.useCallback((x, y) => {
|
|
50
|
+
const startStroke = React.useCallback((x, y, w, h) => {
|
|
51
51
|
drawing.current = true;
|
|
52
52
|
pointCount.current = 1;
|
|
53
|
-
|
|
53
|
+
const xN = w > 0 ? x / w : 0;
|
|
54
|
+
const yN = h > 0 ? y / h : 0;
|
|
55
|
+
currentD.current = `M ${xN} ${yN}`;
|
|
54
56
|
currentId.current = `${Date.now()}-${Math.random()}`;
|
|
55
57
|
}, []);
|
|
56
|
-
const continueStroke = React.useCallback((x, y) => {
|
|
58
|
+
const continueStroke = React.useCallback((x, y, w, h) => {
|
|
57
59
|
if (!drawing.current) return null;
|
|
58
60
|
pointCount.current++;
|
|
59
|
-
|
|
61
|
+
const xN = w > 0 ? x / w : 0;
|
|
62
|
+
const yN = h > 0 ? y / h : 0;
|
|
63
|
+
currentD.current += ` L ${xN} ${yN}`;
|
|
60
64
|
return {
|
|
61
65
|
id: currentId.current,
|
|
62
66
|
d: currentD.current,
|
|
@@ -117,21 +121,29 @@ function useDrawing() {
|
|
|
117
121
|
}
|
|
118
122
|
function useContainerSize() {
|
|
119
123
|
const ref = React.useRef(null);
|
|
120
|
-
const [
|
|
124
|
+
const [size, setSize] = React.useState({ width: 600, height: 200 });
|
|
125
|
+
React.useLayoutEffect(() => {
|
|
126
|
+
const el = ref.current;
|
|
127
|
+
if (!el) return;
|
|
128
|
+
const rect = el.getBoundingClientRect();
|
|
129
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
130
|
+
setSize({ width: rect.width, height: rect.height });
|
|
131
|
+
}
|
|
132
|
+
}, []);
|
|
121
133
|
React.useEffect(() => {
|
|
122
134
|
const el = ref.current;
|
|
123
135
|
if (!el) return;
|
|
124
136
|
const ro = new ResizeObserver((entries) => {
|
|
125
137
|
var _a;
|
|
126
|
-
const
|
|
127
|
-
if (
|
|
138
|
+
const rect = (_a = entries[0]) == null ? void 0 : _a.contentRect;
|
|
139
|
+
if (rect && rect.width > 0 && rect.height > 0) {
|
|
140
|
+
setSize({ width: rect.width, height: rect.height });
|
|
141
|
+
}
|
|
128
142
|
});
|
|
129
143
|
ro.observe(el);
|
|
130
|
-
const rect = el.getBoundingClientRect();
|
|
131
|
-
if (rect.width > 0) setWidth(rect.width);
|
|
132
144
|
return () => ro.disconnect();
|
|
133
145
|
}, []);
|
|
134
|
-
return { ref, width };
|
|
146
|
+
return { ref, width: size.width, height: size.height };
|
|
135
147
|
}
|
|
136
148
|
const NumberLineContext = React.createContext(null);
|
|
137
149
|
function useNumberLineContext() {
|
|
@@ -139,8 +151,6 @@ function useNumberLineContext() {
|
|
|
139
151
|
if (!ctx) throw new Error("Must be used inside <NumberLineProvider>");
|
|
140
152
|
return ctx;
|
|
141
153
|
}
|
|
142
|
-
const DESIGN_WIDTH = 1e3;
|
|
143
|
-
const DESIGN_HEIGHT = 200;
|
|
144
154
|
function NumberLineProvider({
|
|
145
155
|
min,
|
|
146
156
|
max,
|
|
@@ -148,11 +158,14 @@ function NumberLineProvider({
|
|
|
148
158
|
groupCount,
|
|
149
159
|
tickStep,
|
|
150
160
|
arcColors,
|
|
161
|
+
showGroupTicks = true,
|
|
162
|
+
showMajorTicks = true,
|
|
163
|
+
showMinorTicks = false,
|
|
151
164
|
children
|
|
152
165
|
}) {
|
|
153
166
|
const [selectedArcIndices, setSelectedArcIndices] = React.useState(/* @__PURE__ */ new Set());
|
|
154
167
|
const [deletedArcIndices, setDeletedArcIndices] = React.useState(/* @__PURE__ */ new Set());
|
|
155
|
-
const { ref: svgRef, width: containerWidth } = useContainerSize();
|
|
168
|
+
const { ref: svgRef, width: containerWidth, height: containerHeight } = useContainerSize();
|
|
156
169
|
const history = useHistory();
|
|
157
170
|
const drawing = useDrawing();
|
|
158
171
|
const [liveStroke, setLiveStroke] = React.useState(null);
|
|
@@ -182,19 +195,19 @@ function NumberLineProvider({
|
|
|
182
195
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
183
196
|
const coords = getLocalCoords(e);
|
|
184
197
|
if (!coords) return;
|
|
185
|
-
drawing.startStroke(
|
|
198
|
+
drawing.startStroke(coords[0], coords[1], containerWidth, containerHeight);
|
|
186
199
|
},
|
|
187
|
-
[drawing, getLocalCoords]
|
|
200
|
+
[drawing, getLocalCoords, containerWidth, containerHeight]
|
|
188
201
|
);
|
|
189
202
|
const onPointerMove = React.useCallback(
|
|
190
203
|
(e) => {
|
|
191
204
|
if (drawing.tool !== "pen") return;
|
|
192
205
|
const coords = getLocalCoords(e);
|
|
193
206
|
if (!coords) return;
|
|
194
|
-
const live = drawing.continueStroke(
|
|
207
|
+
const live = drawing.continueStroke(coords[0], coords[1], containerWidth, containerHeight);
|
|
195
208
|
if (live) setLiveStroke(live);
|
|
196
209
|
},
|
|
197
|
-
[drawing, getLocalCoords]
|
|
210
|
+
[drawing, getLocalCoords, containerWidth, containerHeight]
|
|
198
211
|
);
|
|
199
212
|
const onPointerUp = React.useCallback(() => {
|
|
200
213
|
if (drawing.tool !== "pen") return;
|
|
@@ -314,8 +327,8 @@ function NumberLineProvider({
|
|
|
314
327
|
{
|
|
315
328
|
value: {
|
|
316
329
|
svgRef,
|
|
317
|
-
viewWidth: DESIGN_WIDTH,
|
|
318
330
|
containerWidth,
|
|
331
|
+
containerHeight,
|
|
319
332
|
svgCursor,
|
|
320
333
|
min,
|
|
321
334
|
max,
|
|
@@ -323,6 +336,9 @@ function NumberLineProvider({
|
|
|
323
336
|
groupCount,
|
|
324
337
|
tickStep,
|
|
325
338
|
arcColors,
|
|
339
|
+
showGroupTicks,
|
|
340
|
+
showMajorTicks,
|
|
341
|
+
showMinorTicks,
|
|
326
342
|
tool: drawing.tool,
|
|
327
343
|
strokes: drawing.strokes,
|
|
328
344
|
liveStroke,
|
|
@@ -362,6 +378,12 @@ const ARC_COLORS = [
|
|
|
362
378
|
"#be185d",
|
|
363
379
|
"#16a34a"
|
|
364
380
|
];
|
|
381
|
+
function gcd(a, b) {
|
|
382
|
+
let x = Math.max(1, Math.abs(Math.round(a)));
|
|
383
|
+
let y = Math.max(1, Math.abs(Math.round(b)));
|
|
384
|
+
while (y !== 0) [x, y] = [y, x % y];
|
|
385
|
+
return x;
|
|
386
|
+
}
|
|
365
387
|
const AxisLayer = React.memo(function AxisLayer2({
|
|
366
388
|
min,
|
|
367
389
|
max,
|
|
@@ -371,7 +393,10 @@ const AxisLayer = React.memo(function AxisLayer2({
|
|
|
371
393
|
viewWidth,
|
|
372
394
|
containerWidth,
|
|
373
395
|
groupSize,
|
|
374
|
-
tickStep
|
|
396
|
+
tickStep,
|
|
397
|
+
showGroupTicks,
|
|
398
|
+
showMajorTicks,
|
|
399
|
+
showMinorTicks
|
|
375
400
|
}) {
|
|
376
401
|
const range = max - min;
|
|
377
402
|
if (range <= 0) return null;
|
|
@@ -381,9 +406,11 @@ const AxisLayer = React.memo(function AxisLayer2({
|
|
|
381
406
|
const rawLabelStep = computeLabelStep(containerWidth, range);
|
|
382
407
|
const labelStep = Math.max(step, Math.ceil(rawLabelStep / step) * step);
|
|
383
408
|
const MAX_TICKS = 500;
|
|
384
|
-
const
|
|
409
|
+
const capStep = Math.max(1, Math.ceil(range / MAX_TICKS));
|
|
410
|
+
const visibleStep = groupSize > 0 ? gcd(step, groupSize) : step;
|
|
411
|
+
const iterStep = showMinorTicks ? capStep : Math.max(capStep, visibleStep);
|
|
385
412
|
const allTicks = [];
|
|
386
|
-
for (let v = min; v <= max; v +=
|
|
413
|
+
for (let v = min; v <= max; v += iterStep) allTicks.push(v);
|
|
387
414
|
return /* @__PURE__ */ jsxRuntime.jsxs("g", { className: "nl-axis-layer", children: [
|
|
388
415
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
389
416
|
"line",
|
|
@@ -401,10 +428,14 @@ const AxisLayer = React.memo(function AxisLayer2({
|
|
|
401
428
|
const offset = v - min;
|
|
402
429
|
const isMajor = offset % step === 0;
|
|
403
430
|
const isGroupBoundary = groupSize > 0 && offset % groupSize === 0;
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
const
|
|
407
|
-
const
|
|
431
|
+
const visible = isGroupBoundary && showGroupTicks || isMajor && showMajorTicks || !isGroupBoundary && !isMajor && showMinorTicks;
|
|
432
|
+
if (!visible) return null;
|
|
433
|
+
const renderAsGroup = isGroupBoundary && showGroupTicks;
|
|
434
|
+
const renderAsMajor = !renderAsGroup && isMajor && showMajorTicks;
|
|
435
|
+
const showLabel = (renderAsGroup || renderAsMajor) && offset % labelStep === 0;
|
|
436
|
+
const tickH = renderAsGroup ? 7 : renderAsMajor ? 10 : 4;
|
|
437
|
+
const strokeW = renderAsGroup ? 1.5 : renderAsMajor ? 1 : 0.5;
|
|
438
|
+
const color = renderAsGroup || renderAsMajor ? "#374151" : "#9ca3af";
|
|
408
439
|
return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
|
|
409
440
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
410
441
|
"line",
|
|
@@ -465,9 +496,10 @@ const GroupArcsLayer = React.memo(function GroupArcsLayer2({
|
|
|
465
496
|
const rx = (x2 - x1) / 2;
|
|
466
497
|
const ry = Math.min(rx * 0.7, arcMaxHeight);
|
|
467
498
|
const color = arcColors[g % arcColors.length];
|
|
468
|
-
const textY = axisY - ry - 6;
|
|
469
499
|
const arrowHeadH = 7;
|
|
470
500
|
const arrowHeadHW = 4;
|
|
501
|
+
const MIN_ARROW_SPAN = 18;
|
|
502
|
+
const textY = Math.min(axisY - ry - 6, axisY - MIN_ARROW_SPAN);
|
|
471
503
|
const arcPath = buildArcPath({ x1, x2, y: axisY, rx, ry });
|
|
472
504
|
const isSelected = selectedArcIndices.has(g);
|
|
473
505
|
const hitCursor = tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default";
|
|
@@ -551,8 +583,8 @@ const GroupArcsLayer = React.memo(function GroupArcsLayer2({
|
|
|
551
583
|
}
|
|
552
584
|
return /* @__PURE__ */ jsxRuntime.jsx("g", { className: "nl-arcs-layer", children: arcs });
|
|
553
585
|
});
|
|
554
|
-
function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
555
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("g", { className: "nl-drawing-layer", children: [
|
|
586
|
+
function DrawingLayer({ strokes, liveStroke, tool, width, height, onStrokeClick }) {
|
|
587
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("g", { className: "nl-drawing-layer", transform: `scale(${width} ${height})`, children: [
|
|
556
588
|
strokes.map((s) => /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
|
|
557
589
|
s.selected && /* @__PURE__ */ jsxRuntime.jsx(
|
|
558
590
|
"path",
|
|
@@ -563,6 +595,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
563
595
|
strokeWidth: 18,
|
|
564
596
|
strokeLinecap: "round",
|
|
565
597
|
strokeLinejoin: "round",
|
|
598
|
+
vectorEffect: "non-scaling-stroke",
|
|
566
599
|
pointerEvents: "none"
|
|
567
600
|
}
|
|
568
601
|
),
|
|
@@ -575,6 +608,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
575
608
|
strokeWidth: tool === "eraser" ? 32 : 28,
|
|
576
609
|
strokeLinecap: "round",
|
|
577
610
|
strokeLinejoin: "round",
|
|
611
|
+
vectorEffect: "non-scaling-stroke",
|
|
578
612
|
pointerEvents: "stroke",
|
|
579
613
|
style: { cursor: tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default" },
|
|
580
614
|
onPointerDown: (e) => {
|
|
@@ -594,6 +628,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
594
628
|
strokeWidth: 2.5,
|
|
595
629
|
strokeLinecap: "round",
|
|
596
630
|
strokeLinejoin: "round",
|
|
631
|
+
vectorEffect: "non-scaling-stroke",
|
|
597
632
|
pointerEvents: "none"
|
|
598
633
|
}
|
|
599
634
|
)
|
|
@@ -607,6 +642,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
607
642
|
strokeWidth: 2.5,
|
|
608
643
|
strokeLinecap: "round",
|
|
609
644
|
strokeLinejoin: "round",
|
|
645
|
+
vectorEffect: "non-scaling-stroke",
|
|
610
646
|
pointerEvents: "none",
|
|
611
647
|
opacity: 0.7
|
|
612
648
|
}
|
|
@@ -615,12 +651,14 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
615
651
|
}
|
|
616
652
|
const PAD_LEFT = 30;
|
|
617
653
|
const PAD_RIGHT = 40;
|
|
618
|
-
const
|
|
619
|
-
const
|
|
654
|
+
const AXIS_BOTTOM_OFFSET = 50;
|
|
655
|
+
const ARC_TOP_PADDING = 16;
|
|
656
|
+
const MIN_ARC_HEIGHT = 24;
|
|
620
657
|
function NumberLine({ className }) {
|
|
621
658
|
const {
|
|
622
659
|
svgRef,
|
|
623
660
|
containerWidth,
|
|
661
|
+
containerHeight,
|
|
624
662
|
svgCursor,
|
|
625
663
|
min,
|
|
626
664
|
max,
|
|
@@ -628,6 +666,9 @@ function NumberLine({ className }) {
|
|
|
628
666
|
groupCount,
|
|
629
667
|
tickStep,
|
|
630
668
|
arcColors,
|
|
669
|
+
showGroupTicks,
|
|
670
|
+
showMajorTicks,
|
|
671
|
+
showMinorTicks,
|
|
631
672
|
tool,
|
|
632
673
|
strokes,
|
|
633
674
|
liveStroke,
|
|
@@ -639,14 +680,18 @@ function NumberLine({ className }) {
|
|
|
639
680
|
onStrokeClick,
|
|
640
681
|
onArcClick
|
|
641
682
|
} = useNumberLineContext();
|
|
683
|
+
const w = containerWidth;
|
|
684
|
+
const h = containerHeight;
|
|
685
|
+
const axisY = Math.max(MIN_ARC_HEIGHT + ARC_TOP_PADDING, h - AXIS_BOTTOM_OFFSET);
|
|
686
|
+
const arcMaxHeight = Math.max(MIN_ARC_HEIGHT, axisY - ARC_TOP_PADDING);
|
|
642
687
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
643
688
|
"svg",
|
|
644
689
|
{
|
|
645
690
|
ref: svgRef,
|
|
646
691
|
tabIndex: 0,
|
|
647
692
|
className: className ? `nl-svg ${className}` : "nl-svg",
|
|
648
|
-
viewBox: `0 0 ${
|
|
649
|
-
preserveAspectRatio: "
|
|
693
|
+
viewBox: `0 0 ${w} ${h}`,
|
|
694
|
+
preserveAspectRatio: "none",
|
|
650
695
|
style: { width: "100%", height: "100%", display: "block", outline: "none", cursor: svgCursor, touchAction: "none" },
|
|
651
696
|
onPointerDown,
|
|
652
697
|
onPointerMove,
|
|
@@ -657,13 +702,16 @@ function NumberLine({ className }) {
|
|
|
657
702
|
{
|
|
658
703
|
min,
|
|
659
704
|
max,
|
|
660
|
-
axisY
|
|
705
|
+
axisY,
|
|
661
706
|
padLeft: PAD_LEFT,
|
|
662
707
|
padRight: PAD_RIGHT,
|
|
663
|
-
viewWidth:
|
|
664
|
-
containerWidth,
|
|
708
|
+
viewWidth: w,
|
|
709
|
+
containerWidth: w,
|
|
665
710
|
groupSize,
|
|
666
|
-
tickStep
|
|
711
|
+
tickStep,
|
|
712
|
+
showGroupTicks,
|
|
713
|
+
showMajorTicks,
|
|
714
|
+
showMinorTicks
|
|
667
715
|
}
|
|
668
716
|
),
|
|
669
717
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -673,11 +721,11 @@ function NumberLine({ className }) {
|
|
|
673
721
|
max,
|
|
674
722
|
groupSize,
|
|
675
723
|
groupCount,
|
|
676
|
-
axisY
|
|
724
|
+
axisY,
|
|
677
725
|
padLeft: PAD_LEFT,
|
|
678
726
|
padRight: PAD_RIGHT,
|
|
679
|
-
viewWidth:
|
|
680
|
-
arcMaxHeight
|
|
727
|
+
viewWidth: w,
|
|
728
|
+
arcMaxHeight,
|
|
681
729
|
tool,
|
|
682
730
|
selectedArcIndices,
|
|
683
731
|
deletedArcIndices,
|
|
@@ -691,6 +739,8 @@ function NumberLine({ className }) {
|
|
|
691
739
|
strokes,
|
|
692
740
|
liveStroke,
|
|
693
741
|
tool,
|
|
742
|
+
width: w,
|
|
743
|
+
height: h,
|
|
694
744
|
onStrokeClick
|
|
695
745
|
}
|
|
696
746
|
)
|
package/dist/index.d.ts
CHANGED
|
@@ -7,8 +7,8 @@ export declare function NumberLine({ className }: {
|
|
|
7
7
|
|
|
8
8
|
export declare interface NumberLineContextValue {
|
|
9
9
|
svgRef: default_2.RefObject<SVGSVGElement>;
|
|
10
|
-
viewWidth: number;
|
|
11
10
|
containerWidth: number;
|
|
11
|
+
containerHeight: number;
|
|
12
12
|
svgCursor: string;
|
|
13
13
|
min: number;
|
|
14
14
|
max: number;
|
|
@@ -16,6 +16,9 @@ export declare interface NumberLineContextValue {
|
|
|
16
16
|
groupCount: number;
|
|
17
17
|
tickStep: number;
|
|
18
18
|
arcColors?: string[];
|
|
19
|
+
showGroupTicks: boolean;
|
|
20
|
+
showMajorTicks: boolean;
|
|
21
|
+
showMinorTicks: boolean;
|
|
19
22
|
tool: Tool;
|
|
20
23
|
strokes: Stroke[];
|
|
21
24
|
liveStroke: Stroke | null;
|
|
@@ -42,9 +45,15 @@ export declare interface NumberLineProps {
|
|
|
42
45
|
groupCount: number;
|
|
43
46
|
tickStep: number;
|
|
44
47
|
arcColors?: string[];
|
|
48
|
+
/** show group boundary ticks (tallest). Default: true */
|
|
49
|
+
showGroupTicks?: boolean;
|
|
50
|
+
/** show major ticks. Default: true */
|
|
51
|
+
showMajorTicks?: boolean;
|
|
52
|
+
/** show minor ticks (in-between). Default: false */
|
|
53
|
+
showMinorTicks?: boolean;
|
|
45
54
|
}
|
|
46
55
|
|
|
47
|
-
export declare function NumberLineProvider({ min, max, groupSize, groupCount, tickStep, arcColors, children, }: NumberLineProviderProps): JSX_2.Element;
|
|
56
|
+
export declare function NumberLineProvider({ min, max, groupSize, groupCount, tickStep, arcColors, showGroupTicks, showMajorTicks, showMinorTicks, children, }: NumberLineProviderProps): JSX_2.Element;
|
|
48
57
|
|
|
49
58
|
declare interface NumberLineProviderProps extends NumberLineProps {
|
|
50
59
|
children: default_2.ReactNode;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React, { useRef, useState, useCallback, useEffect, createContext, useContext, memo } from "react";
|
|
2
|
+
import React, { useRef, useState, useCallback, useLayoutEffect, useEffect, createContext, useContext, memo } from "react";
|
|
3
3
|
function useHistory() {
|
|
4
4
|
const stackRef = useRef([]);
|
|
5
5
|
const [stackLen, setStackLen] = useState(0);
|
|
@@ -45,16 +45,20 @@ function useDrawing() {
|
|
|
45
45
|
});
|
|
46
46
|
setStrokes((prev) => prev.map((s) => ({ ...s, selected: false })));
|
|
47
47
|
}, []);
|
|
48
|
-
const startStroke = useCallback((x, y) => {
|
|
48
|
+
const startStroke = useCallback((x, y, w, h) => {
|
|
49
49
|
drawing.current = true;
|
|
50
50
|
pointCount.current = 1;
|
|
51
|
-
|
|
51
|
+
const xN = w > 0 ? x / w : 0;
|
|
52
|
+
const yN = h > 0 ? y / h : 0;
|
|
53
|
+
currentD.current = `M ${xN} ${yN}`;
|
|
52
54
|
currentId.current = `${Date.now()}-${Math.random()}`;
|
|
53
55
|
}, []);
|
|
54
|
-
const continueStroke = useCallback((x, y) => {
|
|
56
|
+
const continueStroke = useCallback((x, y, w, h) => {
|
|
55
57
|
if (!drawing.current) return null;
|
|
56
58
|
pointCount.current++;
|
|
57
|
-
|
|
59
|
+
const xN = w > 0 ? x / w : 0;
|
|
60
|
+
const yN = h > 0 ? y / h : 0;
|
|
61
|
+
currentD.current += ` L ${xN} ${yN}`;
|
|
58
62
|
return {
|
|
59
63
|
id: currentId.current,
|
|
60
64
|
d: currentD.current,
|
|
@@ -115,21 +119,29 @@ function useDrawing() {
|
|
|
115
119
|
}
|
|
116
120
|
function useContainerSize() {
|
|
117
121
|
const ref = useRef(null);
|
|
118
|
-
const [
|
|
122
|
+
const [size, setSize] = useState({ width: 600, height: 200 });
|
|
123
|
+
useLayoutEffect(() => {
|
|
124
|
+
const el = ref.current;
|
|
125
|
+
if (!el) return;
|
|
126
|
+
const rect = el.getBoundingClientRect();
|
|
127
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
128
|
+
setSize({ width: rect.width, height: rect.height });
|
|
129
|
+
}
|
|
130
|
+
}, []);
|
|
119
131
|
useEffect(() => {
|
|
120
132
|
const el = ref.current;
|
|
121
133
|
if (!el) return;
|
|
122
134
|
const ro = new ResizeObserver((entries) => {
|
|
123
135
|
var _a;
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
136
|
+
const rect = (_a = entries[0]) == null ? void 0 : _a.contentRect;
|
|
137
|
+
if (rect && rect.width > 0 && rect.height > 0) {
|
|
138
|
+
setSize({ width: rect.width, height: rect.height });
|
|
139
|
+
}
|
|
126
140
|
});
|
|
127
141
|
ro.observe(el);
|
|
128
|
-
const rect = el.getBoundingClientRect();
|
|
129
|
-
if (rect.width > 0) setWidth(rect.width);
|
|
130
142
|
return () => ro.disconnect();
|
|
131
143
|
}, []);
|
|
132
|
-
return { ref, width };
|
|
144
|
+
return { ref, width: size.width, height: size.height };
|
|
133
145
|
}
|
|
134
146
|
const NumberLineContext = createContext(null);
|
|
135
147
|
function useNumberLineContext() {
|
|
@@ -137,8 +149,6 @@ function useNumberLineContext() {
|
|
|
137
149
|
if (!ctx) throw new Error("Must be used inside <NumberLineProvider>");
|
|
138
150
|
return ctx;
|
|
139
151
|
}
|
|
140
|
-
const DESIGN_WIDTH = 1e3;
|
|
141
|
-
const DESIGN_HEIGHT = 200;
|
|
142
152
|
function NumberLineProvider({
|
|
143
153
|
min,
|
|
144
154
|
max,
|
|
@@ -146,11 +156,14 @@ function NumberLineProvider({
|
|
|
146
156
|
groupCount,
|
|
147
157
|
tickStep,
|
|
148
158
|
arcColors,
|
|
159
|
+
showGroupTicks = true,
|
|
160
|
+
showMajorTicks = true,
|
|
161
|
+
showMinorTicks = false,
|
|
149
162
|
children
|
|
150
163
|
}) {
|
|
151
164
|
const [selectedArcIndices, setSelectedArcIndices] = useState(/* @__PURE__ */ new Set());
|
|
152
165
|
const [deletedArcIndices, setDeletedArcIndices] = useState(/* @__PURE__ */ new Set());
|
|
153
|
-
const { ref: svgRef, width: containerWidth } = useContainerSize();
|
|
166
|
+
const { ref: svgRef, width: containerWidth, height: containerHeight } = useContainerSize();
|
|
154
167
|
const history = useHistory();
|
|
155
168
|
const drawing = useDrawing();
|
|
156
169
|
const [liveStroke, setLiveStroke] = useState(null);
|
|
@@ -180,19 +193,19 @@ function NumberLineProvider({
|
|
|
180
193
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
181
194
|
const coords = getLocalCoords(e);
|
|
182
195
|
if (!coords) return;
|
|
183
|
-
drawing.startStroke(
|
|
196
|
+
drawing.startStroke(coords[0], coords[1], containerWidth, containerHeight);
|
|
184
197
|
},
|
|
185
|
-
[drawing, getLocalCoords]
|
|
198
|
+
[drawing, getLocalCoords, containerWidth, containerHeight]
|
|
186
199
|
);
|
|
187
200
|
const onPointerMove = useCallback(
|
|
188
201
|
(e) => {
|
|
189
202
|
if (drawing.tool !== "pen") return;
|
|
190
203
|
const coords = getLocalCoords(e);
|
|
191
204
|
if (!coords) return;
|
|
192
|
-
const live = drawing.continueStroke(
|
|
205
|
+
const live = drawing.continueStroke(coords[0], coords[1], containerWidth, containerHeight);
|
|
193
206
|
if (live) setLiveStroke(live);
|
|
194
207
|
},
|
|
195
|
-
[drawing, getLocalCoords]
|
|
208
|
+
[drawing, getLocalCoords, containerWidth, containerHeight]
|
|
196
209
|
);
|
|
197
210
|
const onPointerUp = useCallback(() => {
|
|
198
211
|
if (drawing.tool !== "pen") return;
|
|
@@ -312,8 +325,8 @@ function NumberLineProvider({
|
|
|
312
325
|
{
|
|
313
326
|
value: {
|
|
314
327
|
svgRef,
|
|
315
|
-
viewWidth: DESIGN_WIDTH,
|
|
316
328
|
containerWidth,
|
|
329
|
+
containerHeight,
|
|
317
330
|
svgCursor,
|
|
318
331
|
min,
|
|
319
332
|
max,
|
|
@@ -321,6 +334,9 @@ function NumberLineProvider({
|
|
|
321
334
|
groupCount,
|
|
322
335
|
tickStep,
|
|
323
336
|
arcColors,
|
|
337
|
+
showGroupTicks,
|
|
338
|
+
showMajorTicks,
|
|
339
|
+
showMinorTicks,
|
|
324
340
|
tool: drawing.tool,
|
|
325
341
|
strokes: drawing.strokes,
|
|
326
342
|
liveStroke,
|
|
@@ -360,6 +376,12 @@ const ARC_COLORS = [
|
|
|
360
376
|
"#be185d",
|
|
361
377
|
"#16a34a"
|
|
362
378
|
];
|
|
379
|
+
function gcd(a, b) {
|
|
380
|
+
let x = Math.max(1, Math.abs(Math.round(a)));
|
|
381
|
+
let y = Math.max(1, Math.abs(Math.round(b)));
|
|
382
|
+
while (y !== 0) [x, y] = [y, x % y];
|
|
383
|
+
return x;
|
|
384
|
+
}
|
|
363
385
|
const AxisLayer = React.memo(function AxisLayer2({
|
|
364
386
|
min,
|
|
365
387
|
max,
|
|
@@ -369,7 +391,10 @@ const AxisLayer = React.memo(function AxisLayer2({
|
|
|
369
391
|
viewWidth,
|
|
370
392
|
containerWidth,
|
|
371
393
|
groupSize,
|
|
372
|
-
tickStep
|
|
394
|
+
tickStep,
|
|
395
|
+
showGroupTicks,
|
|
396
|
+
showMajorTicks,
|
|
397
|
+
showMinorTicks
|
|
373
398
|
}) {
|
|
374
399
|
const range = max - min;
|
|
375
400
|
if (range <= 0) return null;
|
|
@@ -379,9 +404,11 @@ const AxisLayer = React.memo(function AxisLayer2({
|
|
|
379
404
|
const rawLabelStep = computeLabelStep(containerWidth, range);
|
|
380
405
|
const labelStep = Math.max(step, Math.ceil(rawLabelStep / step) * step);
|
|
381
406
|
const MAX_TICKS = 500;
|
|
382
|
-
const
|
|
407
|
+
const capStep = Math.max(1, Math.ceil(range / MAX_TICKS));
|
|
408
|
+
const visibleStep = groupSize > 0 ? gcd(step, groupSize) : step;
|
|
409
|
+
const iterStep = showMinorTicks ? capStep : Math.max(capStep, visibleStep);
|
|
383
410
|
const allTicks = [];
|
|
384
|
-
for (let v = min; v <= max; v +=
|
|
411
|
+
for (let v = min; v <= max; v += iterStep) allTicks.push(v);
|
|
385
412
|
return /* @__PURE__ */ jsxs("g", { className: "nl-axis-layer", children: [
|
|
386
413
|
/* @__PURE__ */ jsx(
|
|
387
414
|
"line",
|
|
@@ -399,10 +426,14 @@ const AxisLayer = React.memo(function AxisLayer2({
|
|
|
399
426
|
const offset = v - min;
|
|
400
427
|
const isMajor = offset % step === 0;
|
|
401
428
|
const isGroupBoundary = groupSize > 0 && offset % groupSize === 0;
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
const
|
|
405
|
-
const
|
|
429
|
+
const visible = isGroupBoundary && showGroupTicks || isMajor && showMajorTicks || !isGroupBoundary && !isMajor && showMinorTicks;
|
|
430
|
+
if (!visible) return null;
|
|
431
|
+
const renderAsGroup = isGroupBoundary && showGroupTicks;
|
|
432
|
+
const renderAsMajor = !renderAsGroup && isMajor && showMajorTicks;
|
|
433
|
+
const showLabel = (renderAsGroup || renderAsMajor) && offset % labelStep === 0;
|
|
434
|
+
const tickH = renderAsGroup ? 7 : renderAsMajor ? 10 : 4;
|
|
435
|
+
const strokeW = renderAsGroup ? 1.5 : renderAsMajor ? 1 : 0.5;
|
|
436
|
+
const color = renderAsGroup || renderAsMajor ? "#374151" : "#9ca3af";
|
|
406
437
|
return /* @__PURE__ */ jsxs("g", { children: [
|
|
407
438
|
/* @__PURE__ */ jsx(
|
|
408
439
|
"line",
|
|
@@ -463,9 +494,10 @@ const GroupArcsLayer = memo(function GroupArcsLayer2({
|
|
|
463
494
|
const rx = (x2 - x1) / 2;
|
|
464
495
|
const ry = Math.min(rx * 0.7, arcMaxHeight);
|
|
465
496
|
const color = arcColors[g % arcColors.length];
|
|
466
|
-
const textY = axisY - ry - 6;
|
|
467
497
|
const arrowHeadH = 7;
|
|
468
498
|
const arrowHeadHW = 4;
|
|
499
|
+
const MIN_ARROW_SPAN = 18;
|
|
500
|
+
const textY = Math.min(axisY - ry - 6, axisY - MIN_ARROW_SPAN);
|
|
469
501
|
const arcPath = buildArcPath({ x1, x2, y: axisY, rx, ry });
|
|
470
502
|
const isSelected = selectedArcIndices.has(g);
|
|
471
503
|
const hitCursor = tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default";
|
|
@@ -549,8 +581,8 @@ const GroupArcsLayer = memo(function GroupArcsLayer2({
|
|
|
549
581
|
}
|
|
550
582
|
return /* @__PURE__ */ jsx("g", { className: "nl-arcs-layer", children: arcs });
|
|
551
583
|
});
|
|
552
|
-
function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
553
|
-
return /* @__PURE__ */ jsxs("g", { className: "nl-drawing-layer", children: [
|
|
584
|
+
function DrawingLayer({ strokes, liveStroke, tool, width, height, onStrokeClick }) {
|
|
585
|
+
return /* @__PURE__ */ jsxs("g", { className: "nl-drawing-layer", transform: `scale(${width} ${height})`, children: [
|
|
554
586
|
strokes.map((s) => /* @__PURE__ */ jsxs("g", { children: [
|
|
555
587
|
s.selected && /* @__PURE__ */ jsx(
|
|
556
588
|
"path",
|
|
@@ -561,6 +593,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
561
593
|
strokeWidth: 18,
|
|
562
594
|
strokeLinecap: "round",
|
|
563
595
|
strokeLinejoin: "round",
|
|
596
|
+
vectorEffect: "non-scaling-stroke",
|
|
564
597
|
pointerEvents: "none"
|
|
565
598
|
}
|
|
566
599
|
),
|
|
@@ -573,6 +606,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
573
606
|
strokeWidth: tool === "eraser" ? 32 : 28,
|
|
574
607
|
strokeLinecap: "round",
|
|
575
608
|
strokeLinejoin: "round",
|
|
609
|
+
vectorEffect: "non-scaling-stroke",
|
|
576
610
|
pointerEvents: "stroke",
|
|
577
611
|
style: { cursor: tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default" },
|
|
578
612
|
onPointerDown: (e) => {
|
|
@@ -592,6 +626,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
592
626
|
strokeWidth: 2.5,
|
|
593
627
|
strokeLinecap: "round",
|
|
594
628
|
strokeLinejoin: "round",
|
|
629
|
+
vectorEffect: "non-scaling-stroke",
|
|
595
630
|
pointerEvents: "none"
|
|
596
631
|
}
|
|
597
632
|
)
|
|
@@ -605,6 +640,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
605
640
|
strokeWidth: 2.5,
|
|
606
641
|
strokeLinecap: "round",
|
|
607
642
|
strokeLinejoin: "round",
|
|
643
|
+
vectorEffect: "non-scaling-stroke",
|
|
608
644
|
pointerEvents: "none",
|
|
609
645
|
opacity: 0.7
|
|
610
646
|
}
|
|
@@ -613,12 +649,14 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
613
649
|
}
|
|
614
650
|
const PAD_LEFT = 30;
|
|
615
651
|
const PAD_RIGHT = 40;
|
|
616
|
-
const
|
|
617
|
-
const
|
|
652
|
+
const AXIS_BOTTOM_OFFSET = 50;
|
|
653
|
+
const ARC_TOP_PADDING = 16;
|
|
654
|
+
const MIN_ARC_HEIGHT = 24;
|
|
618
655
|
function NumberLine({ className }) {
|
|
619
656
|
const {
|
|
620
657
|
svgRef,
|
|
621
658
|
containerWidth,
|
|
659
|
+
containerHeight,
|
|
622
660
|
svgCursor,
|
|
623
661
|
min,
|
|
624
662
|
max,
|
|
@@ -626,6 +664,9 @@ function NumberLine({ className }) {
|
|
|
626
664
|
groupCount,
|
|
627
665
|
tickStep,
|
|
628
666
|
arcColors,
|
|
667
|
+
showGroupTicks,
|
|
668
|
+
showMajorTicks,
|
|
669
|
+
showMinorTicks,
|
|
629
670
|
tool,
|
|
630
671
|
strokes,
|
|
631
672
|
liveStroke,
|
|
@@ -637,14 +678,18 @@ function NumberLine({ className }) {
|
|
|
637
678
|
onStrokeClick,
|
|
638
679
|
onArcClick
|
|
639
680
|
} = useNumberLineContext();
|
|
681
|
+
const w = containerWidth;
|
|
682
|
+
const h = containerHeight;
|
|
683
|
+
const axisY = Math.max(MIN_ARC_HEIGHT + ARC_TOP_PADDING, h - AXIS_BOTTOM_OFFSET);
|
|
684
|
+
const arcMaxHeight = Math.max(MIN_ARC_HEIGHT, axisY - ARC_TOP_PADDING);
|
|
640
685
|
return /* @__PURE__ */ jsxs(
|
|
641
686
|
"svg",
|
|
642
687
|
{
|
|
643
688
|
ref: svgRef,
|
|
644
689
|
tabIndex: 0,
|
|
645
690
|
className: className ? `nl-svg ${className}` : "nl-svg",
|
|
646
|
-
viewBox: `0 0 ${
|
|
647
|
-
preserveAspectRatio: "
|
|
691
|
+
viewBox: `0 0 ${w} ${h}`,
|
|
692
|
+
preserveAspectRatio: "none",
|
|
648
693
|
style: { width: "100%", height: "100%", display: "block", outline: "none", cursor: svgCursor, touchAction: "none" },
|
|
649
694
|
onPointerDown,
|
|
650
695
|
onPointerMove,
|
|
@@ -655,13 +700,16 @@ function NumberLine({ className }) {
|
|
|
655
700
|
{
|
|
656
701
|
min,
|
|
657
702
|
max,
|
|
658
|
-
axisY
|
|
703
|
+
axisY,
|
|
659
704
|
padLeft: PAD_LEFT,
|
|
660
705
|
padRight: PAD_RIGHT,
|
|
661
|
-
viewWidth:
|
|
662
|
-
containerWidth,
|
|
706
|
+
viewWidth: w,
|
|
707
|
+
containerWidth: w,
|
|
663
708
|
groupSize,
|
|
664
|
-
tickStep
|
|
709
|
+
tickStep,
|
|
710
|
+
showGroupTicks,
|
|
711
|
+
showMajorTicks,
|
|
712
|
+
showMinorTicks
|
|
665
713
|
}
|
|
666
714
|
),
|
|
667
715
|
/* @__PURE__ */ jsx(
|
|
@@ -671,11 +719,11 @@ function NumberLine({ className }) {
|
|
|
671
719
|
max,
|
|
672
720
|
groupSize,
|
|
673
721
|
groupCount,
|
|
674
|
-
axisY
|
|
722
|
+
axisY,
|
|
675
723
|
padLeft: PAD_LEFT,
|
|
676
724
|
padRight: PAD_RIGHT,
|
|
677
|
-
viewWidth:
|
|
678
|
-
arcMaxHeight
|
|
725
|
+
viewWidth: w,
|
|
726
|
+
arcMaxHeight,
|
|
679
727
|
tool,
|
|
680
728
|
selectedArcIndices,
|
|
681
729
|
deletedArcIndices,
|
|
@@ -689,6 +737,8 @@ function NumberLine({ className }) {
|
|
|
689
737
|
strokes,
|
|
690
738
|
liveStroke,
|
|
691
739
|
tool,
|
|
740
|
+
width: w,
|
|
741
|
+
height: h,
|
|
692
742
|
onStrokeClick
|
|
693
743
|
}
|
|
694
744
|
)
|