@dcg-overseas/number-line 1.0.0-beta.1 → 1.0.0-beta.2

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 CHANGED
@@ -210,12 +210,12 @@ function App() {
210
210
  | `canDelete` | `boolean` | 是否有可删除的选中项(笔迹或弧线) |
211
211
  | `canUndo` | `boolean` | 是否有可撤销的操作 |
212
212
  | `onTogglePen` | `() => void` | 切换画笔工具 |
213
- | `onToggleEraser` | `() => void` | 切换橡皮擦 |
214
- | `onDelete` | `() => void` | 删除当前选中 |
215
- | `clearAll` | `() => void` | 可撤销地清空全部笔迹、弧线、选择和分组边界粗刻度;连续清空不会增加空历史项 |
216
- | `onReset` | `() => void` | `clearAll` 的兼容别名,已弃用;新代码请使用 `clearAll` |
213
+ | `onToggleEraser` | `() => void` | 切换橡皮擦 |
214
+ | `onDelete` | `() => void` | 删除当前选中 |
215
+ | `clearAll` | `() => void` | 可撤销地清空全部笔迹、弧线、选择和分组边界粗刻度;连续清空不会增加空历史项 |
216
+ | `onReset` | `() => void` | `clearAll` 的兼容别名,已弃用;新代码请使用 `clearAll` |
217
217
  | `onUndo` | `() => void` | 撤销上一步操作 |
218
- | `showAllArcs` | `() => void` | 重新显示全部跳数弧线**及分组边界粗刻度**(清空删除记录)。即使参数未变、或刚 `clearAll` 隐藏后,也能强制把跳数显示出来,且不影响笔迹 |
218
+ | `showAllArcs` | `() => void` | 重新显示全部跳数弧线**及分组边界粗刻度**(清空删除记录)。即使参数未变、或刚 `clearAll` 隐藏后,也能强制把跳数显示出来,且不影响笔迹 |
219
219
 
220
220
  #### 缩放 API(仅 `enableZoom=true` 时有效)
221
221
  | 字段 | 类型 | 说明 |
@@ -235,7 +235,7 @@ function App() {
235
235
  import { NumberLineProvider, NumberLine, useNumberLineContext } from '@dcg-overseas/number-line'
236
236
 
237
237
  function Toolbar() {
238
- const { tool, canDelete, canUndo, onTogglePen, onToggleEraser, onDelete, clearAll, onUndo } =
238
+ const { tool, canDelete, canUndo, onTogglePen, onToggleEraser, onDelete, clearAll, onUndo } =
239
239
  useNumberLineContext()
240
240
 
241
241
  return (
@@ -243,7 +243,7 @@ function Toolbar() {
243
243
  <button data-active={tool === 'pen'} onClick={onTogglePen}>✏️ 画笔</button>
244
244
  <button data-active={tool === 'eraser'} onClick={onToggleEraser}>🩹 擦除</button>
245
245
  <button disabled={!canDelete} onClick={onDelete}>🗑 删除</button>
246
- <button onClick={clearAll}>♻️ 全部清除</button>
246
+ <button onClick={clearAll}>♻️ 全部清除</button>
247
247
  <button disabled={!canUndo} onClick={onUndo}>↶ 撤销</button>
248
248
  </div>
249
249
  )
@@ -305,9 +305,9 @@ import type {
305
305
 
306
306
  ### 操作级撤销
307
307
 
308
- 撤销栈记录每次操作发生前的完整状态快照,包括笔迹、已删除弧线、已选弧线和粗分组刻度状态。
309
- 画笔、橡皮擦、删除选中及 `clearAll` 都复用同一恢复流程,因此一次清空或批量删除可以完整还原。
310
- `min`、`max`、`groupSize`、`groupCount` 或 `tickStep` 改变时会执行不可撤销初始化并清空历史,防止把上一道题的数据恢复到新坐标范围。
308
+ 撤销栈记录每次操作发生前的完整状态快照,包括笔迹、已删除弧线、已选弧线和粗分组刻度状态。
309
+ 画笔、橡皮擦、删除选中及 `clearAll` 都复用同一恢复流程,因此一次清空或批量删除可以完整还原。
310
+ `min`、`max`、`groupSize`、`groupCount` 或 `tickStep` 改变时会执行不可撤销初始化并清空历史,防止把上一道题的数据恢复到新坐标范围。
311
311
 
312
312
  ---
313
313
 
package/dist/index.cjs CHANGED
@@ -357,7 +357,6 @@ function NumberLineProvider({
357
357
  );
358
358
  const onPointerDown = React.useCallback(
359
359
  (e) => {
360
- e.currentTarget.focus();
361
360
  if (drawing.tool === "eraser") {
362
361
  if (eraseGesture.current) return;
363
362
  const coords2 = getLocalCoords(e);
@@ -731,14 +730,15 @@ const AxisLayer = React.memo(function AxisLayer2({
731
730
  const renderAsMajor = !renderAsGroup && majorActive;
732
731
  const renderAsHalf = !renderAsGroup && !renderAsMajor && halfTickOffset > 0 && Math.abs(offset % step - halfTickOffset) < 1e-9;
733
732
  const showLabel = (renderAsGroup || renderAsMajor) && offset % labelStep === 0;
734
- const currentShowLabelStep = typeof getShowLabelStep !== "function" ? false : getShowLabelStep == null ? void 0 : getShowLabelStep({
733
+ const currentShowLabelStep = typeof getShowLabelStep === "function" ? getShowLabelStep({
735
734
  viewMin,
736
735
  viewMax,
737
736
  offset,
738
737
  labelStep,
739
738
  length,
740
739
  v
741
- });
740
+ }) : void 0;
741
+ const shouldShowLabel = currentShowLabelStep ?? (showLabel || showLabelStep);
742
742
  const tickH = renderAsGroup ? 7 : renderAsMajor ? 10 : renderAsHalf ? 8 : 6;
743
743
  const strokeW = renderAsGroup ? 4 : renderAsMajor ? 2 : renderAsHalf ? 2 : 1;
744
744
  const arcIndex = renderAsGroup ? offset > 0 ? offset / groupSize - 1 : offset === 0 ? 0 : -1 : -1;
@@ -756,7 +756,7 @@ const AxisLayer = React.memo(function AxisLayer2({
756
756
  strokeWidth: strokeW
757
757
  }
758
758
  ),
759
- (showLabel || showLabelStep || currentShowLabelStep) && /* @__PURE__ */ jsxRuntime.jsx(
759
+ shouldShowLabel && /* @__PURE__ */ jsxRuntime.jsx(
760
760
  "text",
761
761
  {
762
762
  x,
@@ -790,6 +790,7 @@ function AnimatedArc({
790
790
  max,
791
791
  hitCursor,
792
792
  shouldAnimate,
793
+ showLabel,
793
794
  onArcClick
794
795
  }) {
795
796
  const measureRef = React.useRef(null);
@@ -872,7 +873,7 @@ function AnimatedArc({
872
873
  pointerEvents: "none"
873
874
  }
874
875
  ),
875
- arcVisible && /* @__PURE__ */ jsxRuntime.jsx(
876
+ arcVisible && showLabel && /* @__PURE__ */ jsxRuntime.jsx(
876
877
  "path",
877
878
  {
878
879
  d: arcPath,
@@ -969,6 +970,8 @@ const GroupArcsLayer = React.memo(function GroupArcsLayer2({
969
970
  if (groupSize <= 0 || groupCount <= 0) return null;
970
971
  const shouldAnimate = groupSize > 0 && groupCount > 0;
971
972
  const toX = createValueToX(viewMin, viewMax, viewWidth, horizontalLayout);
973
+ const pixelsPerGroup = Math.abs(toX(min + groupSize) - toX(min));
974
+ const arcLabelInterval = Math.max(1, Math.ceil(36 / Math.max(1, pixelsPerGroup)));
972
975
  const arcs = [];
973
976
  for (let g = 0; g < groupCount; g++) {
974
977
  if (deletedArcIndices.has(g)) continue;
@@ -988,6 +991,7 @@ const GroupArcsLayer = React.memo(function GroupArcsLayer2({
988
991
  );
989
992
  const arcPath = buildArcPath({ x1, x2, y: axisY, rx, ry });
990
993
  const isSelected = selectedArcIndices.has(g);
994
+ const showLabel = arcLabelInterval === 1 || (g + 1) % arcLabelInterval === 0 || g === groupCount - 1;
991
995
  const hitCursor = tool === "none" ? "pointer" : "default";
992
996
  arcs.push(
993
997
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -1005,6 +1009,7 @@ const GroupArcsLayer = React.memo(function GroupArcsLayer2({
1005
1009
  max,
1006
1010
  hitCursor,
1007
1011
  shouldAnimate,
1012
+ showLabel,
1008
1013
  onArcClick: (index, e) => {
1009
1014
  if (tool === "none") {
1010
1015
  e.stopPropagation();
@@ -1281,7 +1286,11 @@ function NumberLine({ className }) {
1281
1286
  display: "block",
1282
1287
  cursor: svgCursor,
1283
1288
  touchAction: "none",
1284
- overflow: "visible"
1289
+ overflow: "visible",
1290
+ // 数轴自身带有键盘快捷键,所以 SVG 需要保留 tabIndex;但各浏览器会在鼠标、
1291
+ // 触摸和键盘聚焦时都绘制一圈与教学内容无关的边框。该工具使用按钮状态表达
1292
+ // 可操作性,因此统一移除原生 outline,避免它被误认为数轴的选中标记。
1293
+ outline: "none"
1285
1294
  },
1286
1295
  onPointerDown: handlePointerDown,
1287
1296
  onPointerMove: handlePointerMove,
package/dist/index.d.ts CHANGED
@@ -80,6 +80,10 @@ export declare interface NumberLineProps {
80
80
  viewMax?: number;
81
81
  /** fired whenever internal viewport changes (uncontrolled mode) */
82
82
  onViewChange?: (viewMin: number, viewMax: number) => void;
83
+ /**
84
+ * 自定义主刻度标签可见性。提供该回调后,其返回值会覆盖组件的自动标签密度,
85
+ * 适用于小窗等需要按实际可用宽度降低标签数量的场景。
86
+ */
83
87
  getShowLabelStep?: (params: unknown) => boolean;
84
88
  }
85
89
 
package/dist/index.js CHANGED
@@ -355,7 +355,6 @@ function NumberLineProvider({
355
355
  );
356
356
  const onPointerDown = useCallback(
357
357
  (e) => {
358
- e.currentTarget.focus();
359
358
  if (drawing.tool === "eraser") {
360
359
  if (eraseGesture.current) return;
361
360
  const coords2 = getLocalCoords(e);
@@ -729,14 +728,15 @@ const AxisLayer = React.memo(function AxisLayer2({
729
728
  const renderAsMajor = !renderAsGroup && majorActive;
730
729
  const renderAsHalf = !renderAsGroup && !renderAsMajor && halfTickOffset > 0 && Math.abs(offset % step - halfTickOffset) < 1e-9;
731
730
  const showLabel = (renderAsGroup || renderAsMajor) && offset % labelStep === 0;
732
- const currentShowLabelStep = typeof getShowLabelStep !== "function" ? false : getShowLabelStep == null ? void 0 : getShowLabelStep({
731
+ const currentShowLabelStep = typeof getShowLabelStep === "function" ? getShowLabelStep({
733
732
  viewMin,
734
733
  viewMax,
735
734
  offset,
736
735
  labelStep,
737
736
  length,
738
737
  v
739
- });
738
+ }) : void 0;
739
+ const shouldShowLabel = currentShowLabelStep ?? (showLabel || showLabelStep);
740
740
  const tickH = renderAsGroup ? 7 : renderAsMajor ? 10 : renderAsHalf ? 8 : 6;
741
741
  const strokeW = renderAsGroup ? 4 : renderAsMajor ? 2 : renderAsHalf ? 2 : 1;
742
742
  const arcIndex = renderAsGroup ? offset > 0 ? offset / groupSize - 1 : offset === 0 ? 0 : -1 : -1;
@@ -754,7 +754,7 @@ const AxisLayer = React.memo(function AxisLayer2({
754
754
  strokeWidth: strokeW
755
755
  }
756
756
  ),
757
- (showLabel || showLabelStep || currentShowLabelStep) && /* @__PURE__ */ jsx(
757
+ shouldShowLabel && /* @__PURE__ */ jsx(
758
758
  "text",
759
759
  {
760
760
  x,
@@ -788,6 +788,7 @@ function AnimatedArc({
788
788
  max,
789
789
  hitCursor,
790
790
  shouldAnimate,
791
+ showLabel,
791
792
  onArcClick
792
793
  }) {
793
794
  const measureRef = useRef(null);
@@ -870,7 +871,7 @@ function AnimatedArc({
870
871
  pointerEvents: "none"
871
872
  }
872
873
  ),
873
- arcVisible && /* @__PURE__ */ jsx(
874
+ arcVisible && showLabel && /* @__PURE__ */ jsx(
874
875
  "path",
875
876
  {
876
877
  d: arcPath,
@@ -967,6 +968,8 @@ const GroupArcsLayer = memo(function GroupArcsLayer2({
967
968
  if (groupSize <= 0 || groupCount <= 0) return null;
968
969
  const shouldAnimate = groupSize > 0 && groupCount > 0;
969
970
  const toX = createValueToX(viewMin, viewMax, viewWidth, horizontalLayout);
971
+ const pixelsPerGroup = Math.abs(toX(min + groupSize) - toX(min));
972
+ const arcLabelInterval = Math.max(1, Math.ceil(36 / Math.max(1, pixelsPerGroup)));
970
973
  const arcs = [];
971
974
  for (let g = 0; g < groupCount; g++) {
972
975
  if (deletedArcIndices.has(g)) continue;
@@ -986,6 +989,7 @@ const GroupArcsLayer = memo(function GroupArcsLayer2({
986
989
  );
987
990
  const arcPath = buildArcPath({ x1, x2, y: axisY, rx, ry });
988
991
  const isSelected = selectedArcIndices.has(g);
992
+ const showLabel = arcLabelInterval === 1 || (g + 1) % arcLabelInterval === 0 || g === groupCount - 1;
989
993
  const hitCursor = tool === "none" ? "pointer" : "default";
990
994
  arcs.push(
991
995
  /* @__PURE__ */ jsx(
@@ -1003,6 +1007,7 @@ const GroupArcsLayer = memo(function GroupArcsLayer2({
1003
1007
  max,
1004
1008
  hitCursor,
1005
1009
  shouldAnimate,
1010
+ showLabel,
1006
1011
  onArcClick: (index, e) => {
1007
1012
  if (tool === "none") {
1008
1013
  e.stopPropagation();
@@ -1279,7 +1284,11 @@ function NumberLine({ className }) {
1279
1284
  display: "block",
1280
1285
  cursor: svgCursor,
1281
1286
  touchAction: "none",
1282
- overflow: "visible"
1287
+ overflow: "visible",
1288
+ // 数轴自身带有键盘快捷键,所以 SVG 需要保留 tabIndex;但各浏览器会在鼠标、
1289
+ // 触摸和键盘聚焦时都绘制一圈与教学内容无关的边框。该工具使用按钮状态表达
1290
+ // 可操作性,因此统一移除原生 outline,避免它被误认为数轴的选中标记。
1291
+ outline: "none"
1283
1292
  },
1284
1293
  onPointerDown: handlePointerDown,
1285
1294
  onPointerMove: handlePointerMove,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcg-overseas/number-line",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.2",
4
4
  "description": "Interactive number line component with group arcs and freehand drawing",
5
5
  "type": "module",
6
6
  "license": "MIT",