@dcg-overseas/number-line 0.1.31 → 1.0.0-beta.0

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,11 +210,12 @@ function App() {
210
210
  | `canDelete` | `boolean` | 是否有可删除的选中项(笔迹或弧线) |
211
211
  | `canUndo` | `boolean` | 是否有可撤销的操作 |
212
212
  | `onTogglePen` | `() => void` | 切换画笔工具 |
213
- | `onToggleEraser` | `() => void` | 切换橡皮擦 |
214
- | `onDelete` | `() => void` | 删除当前选中 |
215
- | `onReset` | `() => void` | 清空所有笔迹与选中,并隐藏全部跳数弧线**及分组边界粗刻度**(重新设置 `groupSize/groupCount/min/max` 后恢复) |
213
+ | `onToggleEraser` | `() => void` | 切换橡皮擦 |
214
+ | `onDelete` | `() => void` | 删除当前选中 |
215
+ | `clearAll` | `() => void` | 可撤销地清空全部笔迹、弧线、选择和分组边界粗刻度;连续清空不会增加空历史项 |
216
+ | `onReset` | `() => void` | `clearAll` 的兼容别名,已弃用;新代码请使用 `clearAll` |
216
217
  | `onUndo` | `() => void` | 撤销上一步操作 |
217
- | `showAllArcs` | `() => void` | 重新显示全部跳数弧线**及分组边界粗刻度**(清空删除记录)。即使参数未变、或刚 `onReset` 隐藏后,也能强制把跳数显示出来,且不影响笔迹 |
218
+ | `showAllArcs` | `() => void` | 重新显示全部跳数弧线**及分组边界粗刻度**(清空删除记录)。即使参数未变、或刚 `clearAll` 隐藏后,也能强制把跳数显示出来,且不影响笔迹 |
218
219
 
219
220
  #### 缩放 API(仅 `enableZoom=true` 时有效)
220
221
  | 字段 | 类型 | 说明 |
@@ -234,7 +235,7 @@ function App() {
234
235
  import { NumberLineProvider, NumberLine, useNumberLineContext } from '@dcg-overseas/number-line'
235
236
 
236
237
  function Toolbar() {
237
- const { tool, canDelete, canUndo, onTogglePen, onToggleEraser, onDelete, onReset, onUndo } =
238
+ const { tool, canDelete, canUndo, onTogglePen, onToggleEraser, onDelete, clearAll, onUndo } =
238
239
  useNumberLineContext()
239
240
 
240
241
  return (
@@ -242,7 +243,7 @@ function Toolbar() {
242
243
  <button data-active={tool === 'pen'} onClick={onTogglePen}>✏️ 画笔</button>
243
244
  <button data-active={tool === 'eraser'} onClick={onToggleEraser}>🩹 擦除</button>
244
245
  <button disabled={!canDelete} onClick={onDelete}>🗑 删除</button>
245
- <button onClick={onReset}>♻️ 重置</button>
246
+ <button onClick={clearAll}>♻️ 全部清除</button>
246
247
  <button disabled={!canUndo} onClick={onUndo}>↶ 撤销</button>
247
248
  </div>
248
249
  )
@@ -304,12 +305,9 @@ import type {
304
305
 
305
306
  ### 操作级撤销
306
307
 
307
- 撤销栈记录的是「操作」而非「状态」(`HistoryOp`),分两类:
308
-
309
- - `add` 新增笔迹,撤销 = 移除指定 id
310
- - `delete` — 删除笔迹/弧线,撤销 = 恢复删除前完整快照 + 还原弧线 group index
311
-
312
- 这样即使一次 `delete` 同时删了多条笔迹和多条弧线,撤销也能精确还原。
308
+ 撤销栈记录每次操作发生前的完整状态快照,包括笔迹、已删除弧线、已选弧线和粗分组刻度状态。
309
+ 画笔、橡皮擦、删除选中及 `clearAll` 都复用同一恢复流程,因此一次清空或批量删除可以完整还原。
310
+ `min`、`max`、`groupSize`、`groupCount` `tickStep` 改变时会执行不可撤销初始化并清空历史,防止把上一道题的数据恢复到新坐标范围。
313
311
 
314
312
  ---
315
313
 
package/dist/index.cjs CHANGED
@@ -5,27 +5,16 @@ const React = require("react");
5
5
  function useHistory() {
6
6
  const stackRef = React.useRef([]);
7
7
  const [stackLen, setStackLen] = React.useState(0);
8
- const push = React.useCallback((op) => {
9
- stackRef.current = [...stackRef.current, op];
8
+ const push = React.useCallback((snapshot) => {
9
+ stackRef.current = [...stackRef.current, snapshot];
10
10
  setStackLen(stackRef.current.length);
11
11
  }, []);
12
- const undo = React.useCallback((strokes) => {
13
- if (stackRef.current.length === 0) return { strokes: null, arcIndices: [] };
14
- const op = stackRef.current[stackRef.current.length - 1];
12
+ const undo = React.useCallback(() => {
13
+ if (stackRef.current.length === 0) return null;
14
+ const snapshot = stackRef.current[stackRef.current.length - 1];
15
15
  stackRef.current = stackRef.current.slice(0, -1);
16
16
  setStackLen(stackRef.current.length);
17
- switch (op.type) {
18
- case "add":
19
- return {
20
- strokes: strokes.filter((s) => !op.strokes.some((os) => os.id === s.id)),
21
- arcIndices: []
22
- };
23
- case "delete":
24
- return {
25
- strokes: op.strokesBefore,
26
- arcIndices: op.arcIndices
27
- };
28
- }
17
+ return snapshot;
29
18
  }, []);
30
19
  const clear = React.useCallback(() => {
31
20
  stackRef.current = [];
@@ -300,11 +289,33 @@ function NumberLineProvider({
300
289
  const svgPoint = React.useRef(null);
301
290
  const [eraseHoverIds, setEraseHoverIds] = React.useState(/* @__PURE__ */ new Set());
302
291
  const eraseGesture = React.useRef(null);
292
+ const createHistorySnapshot = React.useCallback(
293
+ (strokesOverride) => ({
294
+ strokes: (strokesOverride ?? drawing.strokes).map((stroke) => ({ ...stroke })),
295
+ deletedArcIndices: [...deletedArcIndices],
296
+ selectedArcIndices: [...selectedArcIndices],
297
+ groupTicksHidden
298
+ }),
299
+ [drawing.strokes, deletedArcIndices, selectedArcIndices, groupTicksHidden]
300
+ );
303
301
  React.useEffect(() => {
302
+ drawing.resetStrokes();
303
+ history.clear();
304
+ setLiveStroke(null);
305
+ eraseGesture.current = null;
306
+ setEraseHoverIds(/* @__PURE__ */ new Set());
304
307
  setDeletedArcIndices(/* @__PURE__ */ new Set());
305
308
  setSelectedArcIndices(/* @__PURE__ */ new Set());
306
309
  setGroupTicksHidden(false);
307
- }, [domainMin, domainMax, groupSize, groupCount]);
310
+ }, [
311
+ domainMin,
312
+ domainMax,
313
+ groupSize,
314
+ groupCount,
315
+ tickStep,
316
+ drawing.resetStrokes,
317
+ history.clear
318
+ ]);
308
319
  const getLocalCoords = React.useCallback(
309
320
  (e) => {
310
321
  const svg = svgRef.current;
@@ -396,7 +407,7 @@ function NumberLineProvider({
396
407
  if (gesture) {
397
408
  eraseGesture.current = null;
398
409
  if (gesture.removed.size > 0) {
399
- history.push({ type: "delete", strokesBefore: gesture.before, arcIndices: [] });
410
+ history.push(createHistorySnapshot(gesture.before));
400
411
  }
401
412
  return;
402
413
  }
@@ -404,10 +415,10 @@ function NumberLineProvider({
404
415
  const stroke = drawing.endStroke();
405
416
  setLiveStroke(null);
406
417
  if (stroke) {
418
+ history.push(createHistorySnapshot());
407
419
  drawing.addStroke(stroke);
408
- history.push({ type: "add", strokes: [stroke] });
409
420
  }
410
- }, [drawing, history]);
421
+ }, [drawing, history, createHistorySnapshot]);
411
422
  const onPointerLeave = React.useCallback(() => {
412
423
  if (eraseGesture.current) return;
413
424
  setEraseHoverIds((prev) => prev.size === 0 ? prev : /* @__PURE__ */ new Set());
@@ -437,39 +448,55 @@ function NumberLineProvider({
437
448
  );
438
449
  const clearArcSelection = React.useCallback(() => setSelectedArcIndices(/* @__PURE__ */ new Set()), []);
439
450
  const onDelete = React.useCallback(() => {
440
- const strokesBefore = drawing.strokes;
441
451
  const deletedStrokes = drawing.strokes.filter((s) => s.selected);
442
452
  const deletedArcs = [...selectedArcIndices];
443
453
  if (deletedStrokes.length === 0 && deletedArcs.length === 0) return;
454
+ history.push(createHistorySnapshot());
444
455
  if (deletedStrokes.length > 0) drawing.deleteSelected();
445
456
  if (deletedArcs.length > 0) {
446
457
  setDeletedArcIndices((prev) => /* @__PURE__ */ new Set([...prev, ...deletedArcs]));
447
458
  setSelectedArcIndices(/* @__PURE__ */ new Set());
448
459
  }
449
- history.push({ type: "delete", strokesBefore, arcIndices: deletedArcs });
450
- }, [drawing, history, selectedArcIndices]);
451
- const onReset = React.useCallback(() => {
452
- drawing.resetStrokes();
453
- history.clear();
460
+ }, [drawing, history, selectedArcIndices, createHistorySnapshot]);
461
+ const clearAll = React.useCallback(() => {
454
462
  const arcCount = Math.max(0, Math.ceil(Number(groupCount) || 0));
463
+ const allArcsDeleted = deletedArcIndices.size >= arcCount && Array.from({ length: arcCount }, (_, index) => index).every(
464
+ (index) => deletedArcIndices.has(index)
465
+ );
466
+ const alreadyClear = drawing.strokes.length === 0 && selectedArcIndices.size === 0 && allArcsDeleted && groupTicksHidden;
467
+ if (alreadyClear) return;
468
+ history.push(createHistorySnapshot());
469
+ drawing.resetStrokes();
470
+ setLiveStroke(null);
471
+ eraseGesture.current = null;
472
+ setEraseHoverIds(/* @__PURE__ */ new Set());
455
473
  setDeletedArcIndices(new Set(Array.from({ length: arcCount }, (_, i) => i)));
456
474
  setSelectedArcIndices(/* @__PURE__ */ new Set());
457
475
  setGroupTicksHidden(true);
458
- }, [drawing, history, groupCount]);
476
+ }, [
477
+ drawing,
478
+ history,
479
+ groupCount,
480
+ deletedArcIndices,
481
+ selectedArcIndices,
482
+ groupTicksHidden,
483
+ createHistorySnapshot
484
+ ]);
485
+ const onReset = clearAll;
459
486
  const showAllArcs = React.useCallback(() => {
460
487
  setDeletedArcIndices((prev) => prev.size === 0 ? prev : /* @__PURE__ */ new Set());
461
488
  setGroupTicksHidden(false);
462
489
  }, []);
463
490
  const onUndo = React.useCallback(() => {
464
- const result = history.undo(drawing.strokes);
465
- if (result.strokes !== null) drawing.restoreStrokes(result.strokes);
466
- if (result.arcIndices.length > 0) {
467
- setDeletedArcIndices((prev) => {
468
- const next = new Set(prev);
469
- result.arcIndices.forEach((i) => next.delete(i));
470
- return next;
471
- });
472
- }
491
+ const snapshot = history.undo();
492
+ if (!snapshot) return;
493
+ drawing.restoreStrokes(snapshot.strokes);
494
+ setDeletedArcIndices(new Set(snapshot.deletedArcIndices));
495
+ setSelectedArcIndices(new Set(snapshot.selectedArcIndices));
496
+ setGroupTicksHidden(snapshot.groupTicksHidden);
497
+ setLiveStroke(null);
498
+ eraseGesture.current = null;
499
+ setEraseHoverIds(/* @__PURE__ */ new Set());
473
500
  }, [history, drawing]);
474
501
  const onTogglePen = React.useCallback(
475
502
  () => {
@@ -554,6 +581,7 @@ function NumberLineProvider({
554
581
  onTogglePen,
555
582
  onToggleEraser,
556
583
  onDelete,
584
+ clearAll,
557
585
  onReset,
558
586
  onUndo,
559
587
  showAllArcs
package/dist/index.d.ts CHANGED
@@ -44,6 +44,9 @@ export declare interface NumberLineContextValue {
44
44
  onTogglePen: () => void;
45
45
  onToggleEraser: () => void;
46
46
  onDelete: () => void;
47
+ /** 可撤销地清空笔迹、弧线、选择和粗分组刻度;已为空时不创建历史项。 */
48
+ clearAll: () => void;
49
+ /** @deprecated 请使用语义明确且行为相同的 `clearAll`。 */
47
50
  onReset: () => void;
48
51
  onUndo: () => void;
49
52
  /** restore (re-show) every arc by clearing the deleted-arc record */
package/dist/index.js CHANGED
@@ -3,27 +3,16 @@ import React, { useRef, useState, useCallback, useLayoutEffect, useEffect, creat
3
3
  function useHistory() {
4
4
  const stackRef = useRef([]);
5
5
  const [stackLen, setStackLen] = useState(0);
6
- const push = useCallback((op) => {
7
- stackRef.current = [...stackRef.current, op];
6
+ const push = useCallback((snapshot) => {
7
+ stackRef.current = [...stackRef.current, snapshot];
8
8
  setStackLen(stackRef.current.length);
9
9
  }, []);
10
- const undo = useCallback((strokes) => {
11
- if (stackRef.current.length === 0) return { strokes: null, arcIndices: [] };
12
- const op = stackRef.current[stackRef.current.length - 1];
10
+ const undo = useCallback(() => {
11
+ if (stackRef.current.length === 0) return null;
12
+ const snapshot = stackRef.current[stackRef.current.length - 1];
13
13
  stackRef.current = stackRef.current.slice(0, -1);
14
14
  setStackLen(stackRef.current.length);
15
- switch (op.type) {
16
- case "add":
17
- return {
18
- strokes: strokes.filter((s) => !op.strokes.some((os) => os.id === s.id)),
19
- arcIndices: []
20
- };
21
- case "delete":
22
- return {
23
- strokes: op.strokesBefore,
24
- arcIndices: op.arcIndices
25
- };
26
- }
15
+ return snapshot;
27
16
  }, []);
28
17
  const clear = useCallback(() => {
29
18
  stackRef.current = [];
@@ -298,11 +287,33 @@ function NumberLineProvider({
298
287
  const svgPoint = useRef(null);
299
288
  const [eraseHoverIds, setEraseHoverIds] = useState(/* @__PURE__ */ new Set());
300
289
  const eraseGesture = useRef(null);
290
+ const createHistorySnapshot = useCallback(
291
+ (strokesOverride) => ({
292
+ strokes: (strokesOverride ?? drawing.strokes).map((stroke) => ({ ...stroke })),
293
+ deletedArcIndices: [...deletedArcIndices],
294
+ selectedArcIndices: [...selectedArcIndices],
295
+ groupTicksHidden
296
+ }),
297
+ [drawing.strokes, deletedArcIndices, selectedArcIndices, groupTicksHidden]
298
+ );
301
299
  useEffect(() => {
300
+ drawing.resetStrokes();
301
+ history.clear();
302
+ setLiveStroke(null);
303
+ eraseGesture.current = null;
304
+ setEraseHoverIds(/* @__PURE__ */ new Set());
302
305
  setDeletedArcIndices(/* @__PURE__ */ new Set());
303
306
  setSelectedArcIndices(/* @__PURE__ */ new Set());
304
307
  setGroupTicksHidden(false);
305
- }, [domainMin, domainMax, groupSize, groupCount]);
308
+ }, [
309
+ domainMin,
310
+ domainMax,
311
+ groupSize,
312
+ groupCount,
313
+ tickStep,
314
+ drawing.resetStrokes,
315
+ history.clear
316
+ ]);
306
317
  const getLocalCoords = useCallback(
307
318
  (e) => {
308
319
  const svg = svgRef.current;
@@ -394,7 +405,7 @@ function NumberLineProvider({
394
405
  if (gesture) {
395
406
  eraseGesture.current = null;
396
407
  if (gesture.removed.size > 0) {
397
- history.push({ type: "delete", strokesBefore: gesture.before, arcIndices: [] });
408
+ history.push(createHistorySnapshot(gesture.before));
398
409
  }
399
410
  return;
400
411
  }
@@ -402,10 +413,10 @@ function NumberLineProvider({
402
413
  const stroke = drawing.endStroke();
403
414
  setLiveStroke(null);
404
415
  if (stroke) {
416
+ history.push(createHistorySnapshot());
405
417
  drawing.addStroke(stroke);
406
- history.push({ type: "add", strokes: [stroke] });
407
418
  }
408
- }, [drawing, history]);
419
+ }, [drawing, history, createHistorySnapshot]);
409
420
  const onPointerLeave = useCallback(() => {
410
421
  if (eraseGesture.current) return;
411
422
  setEraseHoverIds((prev) => prev.size === 0 ? prev : /* @__PURE__ */ new Set());
@@ -435,39 +446,55 @@ function NumberLineProvider({
435
446
  );
436
447
  const clearArcSelection = useCallback(() => setSelectedArcIndices(/* @__PURE__ */ new Set()), []);
437
448
  const onDelete = useCallback(() => {
438
- const strokesBefore = drawing.strokes;
439
449
  const deletedStrokes = drawing.strokes.filter((s) => s.selected);
440
450
  const deletedArcs = [...selectedArcIndices];
441
451
  if (deletedStrokes.length === 0 && deletedArcs.length === 0) return;
452
+ history.push(createHistorySnapshot());
442
453
  if (deletedStrokes.length > 0) drawing.deleteSelected();
443
454
  if (deletedArcs.length > 0) {
444
455
  setDeletedArcIndices((prev) => /* @__PURE__ */ new Set([...prev, ...deletedArcs]));
445
456
  setSelectedArcIndices(/* @__PURE__ */ new Set());
446
457
  }
447
- history.push({ type: "delete", strokesBefore, arcIndices: deletedArcs });
448
- }, [drawing, history, selectedArcIndices]);
449
- const onReset = useCallback(() => {
450
- drawing.resetStrokes();
451
- history.clear();
458
+ }, [drawing, history, selectedArcIndices, createHistorySnapshot]);
459
+ const clearAll = useCallback(() => {
452
460
  const arcCount = Math.max(0, Math.ceil(Number(groupCount) || 0));
461
+ const allArcsDeleted = deletedArcIndices.size >= arcCount && Array.from({ length: arcCount }, (_, index) => index).every(
462
+ (index) => deletedArcIndices.has(index)
463
+ );
464
+ const alreadyClear = drawing.strokes.length === 0 && selectedArcIndices.size === 0 && allArcsDeleted && groupTicksHidden;
465
+ if (alreadyClear) return;
466
+ history.push(createHistorySnapshot());
467
+ drawing.resetStrokes();
468
+ setLiveStroke(null);
469
+ eraseGesture.current = null;
470
+ setEraseHoverIds(/* @__PURE__ */ new Set());
453
471
  setDeletedArcIndices(new Set(Array.from({ length: arcCount }, (_, i) => i)));
454
472
  setSelectedArcIndices(/* @__PURE__ */ new Set());
455
473
  setGroupTicksHidden(true);
456
- }, [drawing, history, groupCount]);
474
+ }, [
475
+ drawing,
476
+ history,
477
+ groupCount,
478
+ deletedArcIndices,
479
+ selectedArcIndices,
480
+ groupTicksHidden,
481
+ createHistorySnapshot
482
+ ]);
483
+ const onReset = clearAll;
457
484
  const showAllArcs = useCallback(() => {
458
485
  setDeletedArcIndices((prev) => prev.size === 0 ? prev : /* @__PURE__ */ new Set());
459
486
  setGroupTicksHidden(false);
460
487
  }, []);
461
488
  const onUndo = useCallback(() => {
462
- const result = history.undo(drawing.strokes);
463
- if (result.strokes !== null) drawing.restoreStrokes(result.strokes);
464
- if (result.arcIndices.length > 0) {
465
- setDeletedArcIndices((prev) => {
466
- const next = new Set(prev);
467
- result.arcIndices.forEach((i) => next.delete(i));
468
- return next;
469
- });
470
- }
489
+ const snapshot = history.undo();
490
+ if (!snapshot) return;
491
+ drawing.restoreStrokes(snapshot.strokes);
492
+ setDeletedArcIndices(new Set(snapshot.deletedArcIndices));
493
+ setSelectedArcIndices(new Set(snapshot.selectedArcIndices));
494
+ setGroupTicksHidden(snapshot.groupTicksHidden);
495
+ setLiveStroke(null);
496
+ eraseGesture.current = null;
497
+ setEraseHoverIds(/* @__PURE__ */ new Set());
471
498
  }, [history, drawing]);
472
499
  const onTogglePen = useCallback(
473
500
  () => {
@@ -552,6 +579,7 @@ function NumberLineProvider({
552
579
  onTogglePen,
553
580
  onToggleEraser,
554
581
  onDelete,
582
+ clearAll,
555
583
  onReset,
556
584
  onUndo,
557
585
  showAllArcs
package/package.json CHANGED
@@ -1,47 +1,47 @@
1
- {
2
- "name": "@dcg-overseas/number-line",
3
- "version": "0.1.31",
4
- "description": "Interactive number line component with group arcs and freehand drawing",
5
- "type": "module",
6
- "license": "MIT",
7
- "main": "./dist/index.cjs",
8
- "module": "./dist/index.js",
9
- "types": "./dist/index.d.ts",
10
- "exports": {
11
- ".": {
12
- "import": {
13
- "types": "./dist/index.d.ts",
14
- "default": "./dist/index.js"
15
- },
16
- "require": {
17
- "types": "./dist/index.d.cts",
18
- "default": "./dist/index.cjs"
19
- }
20
- }
21
- },
22
- "files": [
23
- "dist"
24
- ],
25
- "sideEffects": false,
26
- "scripts": {
27
- "build": "vite build",
28
- "build:watch": "vite build --watch",
29
- "typecheck": "tsc --noEmit",
30
- "clean": "rm -rf dist *.tsbuildinfo"
31
- },
32
- "peerDependencies": {
33
- "react": "^18.0.0",
34
- "react-dom": "^18.0.0"
35
- },
36
- "devDependencies": {
37
- "@types/react": "^18.3.3",
38
- "@types/react-dom": "^18.3.0",
39
- "@vitejs/plugin-react": "^4.3.1",
40
- "vite": "^5.3.1",
41
- "vite-plugin-dts": "^3.9.1"
42
- },
43
- "publishConfig": {
44
- "access": "public",
45
- "registry": "https://registry.npmjs.org/"
46
- }
47
- }
1
+ {
2
+ "name": "@dcg-overseas/number-line",
3
+ "version": "1.0.0-beta.0",
4
+ "description": "Interactive number line component with group arcs and freehand drawing",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "require": {
17
+ "types": "./dist/index.d.cts",
18
+ "default": "./dist/index.cjs"
19
+ }
20
+ }
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "sideEffects": false,
26
+ "scripts": {
27
+ "build": "vite build",
28
+ "build:watch": "vite build --watch",
29
+ "typecheck": "tsc --noEmit",
30
+ "clean": "rm -rf dist *.tsbuildinfo"
31
+ },
32
+ "peerDependencies": {
33
+ "react": "^18.0.0",
34
+ "react-dom": "^18.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/react": "^18.3.3",
38
+ "@types/react-dom": "^18.3.0",
39
+ "@vitejs/plugin-react": "^4.3.1",
40
+ "vite": "^5.3.1",
41
+ "vite-plugin-dts": "^3.9.1"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public",
45
+ "registry": "https://registry.npmjs.org/"
46
+ }
47
+ }