@dcg-overseas/number-line 0.1.30 → 0.1.31

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/index.cjs CHANGED
@@ -247,6 +247,19 @@ function normalizeDomain(lo, hi) {
247
247
  }
248
248
  return { min: x, max: y };
249
249
  }
250
+ function eraseRadius(pointerType) {
251
+ return pointerType === "touch" ? 28 : 16;
252
+ }
253
+ function strokeWithinRadius(d, px, py, radius, w, h) {
254
+ const nums = d.match(/-?\d*\.?\d+(?:e[-+]?\d+)?/gi);
255
+ if (!nums) return false;
256
+ for (let i = 0; i + 1 < nums.length; i += 2) {
257
+ const x = parseFloat(nums[i]) * w;
258
+ const y = parseFloat(nums[i + 1]) * h;
259
+ if (Math.hypot(x - px, y - py) < radius) return true;
260
+ }
261
+ return false;
262
+ }
250
263
  function NumberLineProvider({
251
264
  min,
252
265
  max,
@@ -285,6 +298,8 @@ function NumberLineProvider({
285
298
  });
286
299
  const [liveStroke, setLiveStroke] = React.useState(null);
287
300
  const svgPoint = React.useRef(null);
301
+ const [eraseHoverIds, setEraseHoverIds] = React.useState(/* @__PURE__ */ new Set());
302
+ const eraseGesture = React.useRef(null);
288
303
  React.useEffect(() => {
289
304
  setDeletedArcIndices(/* @__PURE__ */ new Set());
290
305
  setSelectedArcIndices(/* @__PURE__ */ new Set());
@@ -304,28 +319,87 @@ function NumberLineProvider({
304
319
  },
305
320
  [svgRef]
306
321
  );
322
+ const strokeIdsNear = React.useCallback(
323
+ (x, y, radius) => {
324
+ const ids = [];
325
+ for (const s of drawing.strokes) {
326
+ if (strokeWithinRadius(s.d, x, y, radius, containerWidth, containerHeight)) {
327
+ ids.push(s.id);
328
+ }
329
+ }
330
+ return ids;
331
+ },
332
+ [drawing.strokes, containerWidth, containerHeight]
333
+ );
334
+ const eraseAt = React.useCallback(
335
+ (x, y, radius) => {
336
+ const gesture = eraseGesture.current;
337
+ if (!gesture) return;
338
+ for (const id of strokeIdsNear(x, y, radius)) {
339
+ if (!gesture.removed.has(id)) {
340
+ gesture.removed.add(id);
341
+ drawing.eraseStroke(id);
342
+ }
343
+ }
344
+ },
345
+ [strokeIdsNear, drawing]
346
+ );
307
347
  const onPointerDown = React.useCallback(
308
348
  (e) => {
309
349
  e.currentTarget.focus();
350
+ if (drawing.tool === "eraser") {
351
+ if (eraseGesture.current) return;
352
+ const coords2 = getLocalCoords(e);
353
+ if (!coords2) return;
354
+ e.currentTarget.setPointerCapture(e.pointerId);
355
+ eraseGesture.current = { before: drawing.strokes, removed: /* @__PURE__ */ new Set() };
356
+ setEraseHoverIds((prev) => prev.size === 0 ? prev : /* @__PURE__ */ new Set());
357
+ eraseAt(coords2[0], coords2[1], eraseRadius(e.pointerType));
358
+ return;
359
+ }
310
360
  if (drawing.tool !== "pen") return;
311
361
  e.currentTarget.setPointerCapture(e.pointerId);
312
362
  const coords = getLocalCoords(e);
313
363
  if (!coords) return;
314
364
  drawing.startStroke(coords[0], coords[1], containerWidth, containerHeight);
315
365
  },
316
- [drawing, getLocalCoords, containerWidth, containerHeight]
366
+ [drawing, getLocalCoords, containerWidth, containerHeight, eraseAt]
317
367
  );
318
368
  const onPointerMove = React.useCallback(
319
369
  (e) => {
370
+ if (drawing.tool === "eraser") {
371
+ const coords2 = getLocalCoords(e);
372
+ if (!coords2) return;
373
+ const radius = eraseRadius(e.pointerType);
374
+ if (eraseGesture.current) {
375
+ eraseAt(coords2[0], coords2[1], radius);
376
+ } else {
377
+ const nextIds = strokeIdsNear(coords2[0], coords2[1], radius);
378
+ setEraseHoverIds((prev) => {
379
+ if (nextIds.length === prev.size && nextIds.every((id) => prev.has(id)))
380
+ return prev;
381
+ return new Set(nextIds);
382
+ });
383
+ }
384
+ return;
385
+ }
320
386
  if (drawing.tool !== "pen") return;
321
387
  const coords = getLocalCoords(e);
322
388
  if (!coords) return;
323
389
  const live = drawing.continueStroke(coords[0], coords[1], containerWidth, containerHeight);
324
390
  if (live) setLiveStroke(live);
325
391
  },
326
- [drawing, getLocalCoords, containerWidth, containerHeight]
392
+ [drawing, getLocalCoords, containerWidth, containerHeight, eraseAt, strokeIdsNear]
327
393
  );
328
394
  const onPointerUp = React.useCallback(() => {
395
+ const gesture = eraseGesture.current;
396
+ if (gesture) {
397
+ eraseGesture.current = null;
398
+ if (gesture.removed.size > 0) {
399
+ history.push({ type: "delete", strokesBefore: gesture.before, arcIndices: [] });
400
+ }
401
+ return;
402
+ }
329
403
  if (drawing.tool !== "pen") return;
330
404
  const stroke = drawing.endStroke();
331
405
  setLiveStroke(null);
@@ -334,21 +408,18 @@ function NumberLineProvider({
334
408
  history.push({ type: "add", strokes: [stroke] });
335
409
  }
336
410
  }, [drawing, history]);
411
+ const onPointerLeave = React.useCallback(() => {
412
+ if (eraseGesture.current) return;
413
+ setEraseHoverIds((prev) => prev.size === 0 ? prev : /* @__PURE__ */ new Set());
414
+ }, []);
337
415
  const onStrokeClick = React.useCallback(
338
416
  (id, e) => {
339
417
  e.stopPropagation();
340
- if (drawing.tool === "eraser") {
341
- const target = drawing.strokes.find((s) => s.id === id);
342
- if (target) {
343
- const strokesBefore = drawing.strokes;
344
- drawing.eraseStroke(id);
345
- history.push({ type: "delete", strokesBefore, arcIndices: [] });
346
- }
347
- } else if (drawing.tool === "none") {
418
+ if (drawing.tool === "none") {
348
419
  drawing.toggleSelect(id);
349
420
  }
350
421
  },
351
- [drawing, history]
422
+ [drawing]
352
423
  );
353
424
  const onArcClick = React.useCallback(
354
425
  (groupIndex, e) => {
@@ -417,6 +488,7 @@ function NumberLineProvider({
417
488
  const canDelete = drawing.strokes.some((s) => s.selected) || selectedArcIndices.size > 0;
418
489
  React.useEffect(() => {
419
490
  setSelectedArcIndices(/* @__PURE__ */ new Set());
491
+ setEraseHoverIds((prev) => prev.size === 0 ? prev : /* @__PURE__ */ new Set());
420
492
  }, [drawing.tool]);
421
493
  const keyHandlers = React.useRef({ toggleTool: drawing.toggleTool, clearArcSelection, canDelete, onDelete, onUndo });
422
494
  keyHandlers.current = { toggleTool: drawing.toggleTool, clearArcSelection, canDelete, onDelete, onUndo };
@@ -468,11 +540,13 @@ function NumberLineProvider({
468
540
  tool: drawing.tool,
469
541
  strokes: drawing.strokes,
470
542
  liveStroke,
543
+ eraseHoverIds,
471
544
  selectedArcIndices,
472
545
  deletedArcIndices,
473
546
  onPointerDown,
474
547
  onPointerMove,
475
548
  onPointerUp,
549
+ onPointerLeave,
476
550
  onStrokeClick,
477
551
  onArcClick,
478
552
  canDelete,
@@ -916,11 +990,10 @@ const GroupArcsLayer = React.memo(function GroupArcsLayer2({
916
990
  }
917
991
  return /* @__PURE__ */ jsxRuntime.jsx("g", { className: "nl-arcs-layer", children: arcs });
918
992
  });
919
- function DrawingLayer({ strokes, liveStroke, tool, width, height, onStrokeClick }) {
920
- const [hoveredId, setHoveredId] = React.useState(null);
993
+ function DrawingLayer({ strokes, liveStroke, tool, width, height, eraseHoverIds, onStrokeClick }) {
921
994
  return /* @__PURE__ */ jsxRuntime.jsxs("g", { className: "nl-drawing-layer", transform: `scale(${width} ${height})`, children: [
922
995
  strokes.map((s) => /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
923
- (s.selected || tool === "eraser" && s.id === hoveredId) && /* @__PURE__ */ jsxRuntime.jsx(
996
+ (s.selected || tool === "eraser" && eraseHoverIds.has(s.id)) && /* @__PURE__ */ jsxRuntime.jsx(
924
997
  "path",
925
998
  {
926
999
  d: s.d,
@@ -945,10 +1018,8 @@ function DrawingLayer({ strokes, liveStroke, tool, width, height, onStrokeClick
945
1018
  vectorEffect: "non-scaling-stroke",
946
1019
  pointerEvents: "stroke",
947
1020
  style: { cursor: tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default" },
948
- onPointerEnter: () => setHoveredId(s.id),
949
- onPointerLeave: () => setHoveredId((prev) => prev === s.id ? null : prev),
950
1021
  onClick: (e) => {
951
- if (tool === "eraser" || tool === "none") {
1022
+ if (tool === "none") {
952
1023
  e.stopPropagation();
953
1024
  onStrokeClick(s.id, e);
954
1025
  }
@@ -1015,11 +1086,13 @@ function NumberLine({ className }) {
1015
1086
  tool,
1016
1087
  strokes,
1017
1088
  liveStroke,
1089
+ eraseHoverIds,
1018
1090
  selectedArcIndices,
1019
1091
  deletedArcIndices,
1020
1092
  onPointerDown,
1021
1093
  onPointerMove,
1022
1094
  onPointerUp,
1095
+ onPointerLeave,
1023
1096
  onStrokeClick,
1024
1097
  onArcClick
1025
1098
  } = useNumberLineContext();
@@ -1186,6 +1259,8 @@ function NumberLine({ className }) {
1186
1259
  onPointerDown: handlePointerDown,
1187
1260
  onPointerMove: handlePointerMove,
1188
1261
  onPointerUp: handlePointerUp,
1262
+ onPointerCancel: handlePointerUp,
1263
+ onPointerLeave,
1189
1264
  onWheel: handleWheel,
1190
1265
  onDoubleClick: handleDoubleClick,
1191
1266
  onMouseEnter: () => enableZoom && setIsZoomActive(true),
@@ -1244,6 +1319,7 @@ function NumberLine({ className }) {
1244
1319
  tool,
1245
1320
  width: w,
1246
1321
  height: h,
1322
+ eraseHoverIds,
1247
1323
  onStrokeClick
1248
1324
  }
1249
1325
  )
package/dist/index.d.ts CHANGED
@@ -29,11 +29,14 @@ export declare interface NumberLineContextValue {
29
29
  tool: Tool;
30
30
  strokes: Stroke[];
31
31
  liveStroke: Stroke | null;
32
+ /** Stroke ids the eraser brush is hovering over (preview of what a press would remove). */
33
+ eraseHoverIds: Set<string>;
32
34
  selectedArcIndices: Set<number>;
33
35
  deletedArcIndices: Set<number>;
34
36
  onPointerDown: (e: default_2.PointerEvent<SVGSVGElement>) => void;
35
37
  onPointerMove: (e: default_2.PointerEvent<SVGSVGElement>) => void;
36
38
  onPointerUp: () => void;
39
+ onPointerLeave: () => void;
37
40
  onStrokeClick: (id: string, e: default_2.PointerEvent) => void;
38
41
  onArcClick: (groupIndex: number, e: default_2.PointerEvent) => void;
39
42
  canDelete: boolean;
package/dist/index.js CHANGED
@@ -245,6 +245,19 @@ function normalizeDomain(lo, hi) {
245
245
  }
246
246
  return { min: x, max: y };
247
247
  }
248
+ function eraseRadius(pointerType) {
249
+ return pointerType === "touch" ? 28 : 16;
250
+ }
251
+ function strokeWithinRadius(d, px, py, radius, w, h) {
252
+ const nums = d.match(/-?\d*\.?\d+(?:e[-+]?\d+)?/gi);
253
+ if (!nums) return false;
254
+ for (let i = 0; i + 1 < nums.length; i += 2) {
255
+ const x = parseFloat(nums[i]) * w;
256
+ const y = parseFloat(nums[i + 1]) * h;
257
+ if (Math.hypot(x - px, y - py) < radius) return true;
258
+ }
259
+ return false;
260
+ }
248
261
  function NumberLineProvider({
249
262
  min,
250
263
  max,
@@ -283,6 +296,8 @@ function NumberLineProvider({
283
296
  });
284
297
  const [liveStroke, setLiveStroke] = useState(null);
285
298
  const svgPoint = useRef(null);
299
+ const [eraseHoverIds, setEraseHoverIds] = useState(/* @__PURE__ */ new Set());
300
+ const eraseGesture = useRef(null);
286
301
  useEffect(() => {
287
302
  setDeletedArcIndices(/* @__PURE__ */ new Set());
288
303
  setSelectedArcIndices(/* @__PURE__ */ new Set());
@@ -302,28 +317,87 @@ function NumberLineProvider({
302
317
  },
303
318
  [svgRef]
304
319
  );
320
+ const strokeIdsNear = useCallback(
321
+ (x, y, radius) => {
322
+ const ids = [];
323
+ for (const s of drawing.strokes) {
324
+ if (strokeWithinRadius(s.d, x, y, radius, containerWidth, containerHeight)) {
325
+ ids.push(s.id);
326
+ }
327
+ }
328
+ return ids;
329
+ },
330
+ [drawing.strokes, containerWidth, containerHeight]
331
+ );
332
+ const eraseAt = useCallback(
333
+ (x, y, radius) => {
334
+ const gesture = eraseGesture.current;
335
+ if (!gesture) return;
336
+ for (const id of strokeIdsNear(x, y, radius)) {
337
+ if (!gesture.removed.has(id)) {
338
+ gesture.removed.add(id);
339
+ drawing.eraseStroke(id);
340
+ }
341
+ }
342
+ },
343
+ [strokeIdsNear, drawing]
344
+ );
305
345
  const onPointerDown = useCallback(
306
346
  (e) => {
307
347
  e.currentTarget.focus();
348
+ if (drawing.tool === "eraser") {
349
+ if (eraseGesture.current) return;
350
+ const coords2 = getLocalCoords(e);
351
+ if (!coords2) return;
352
+ e.currentTarget.setPointerCapture(e.pointerId);
353
+ eraseGesture.current = { before: drawing.strokes, removed: /* @__PURE__ */ new Set() };
354
+ setEraseHoverIds((prev) => prev.size === 0 ? prev : /* @__PURE__ */ new Set());
355
+ eraseAt(coords2[0], coords2[1], eraseRadius(e.pointerType));
356
+ return;
357
+ }
308
358
  if (drawing.tool !== "pen") return;
309
359
  e.currentTarget.setPointerCapture(e.pointerId);
310
360
  const coords = getLocalCoords(e);
311
361
  if (!coords) return;
312
362
  drawing.startStroke(coords[0], coords[1], containerWidth, containerHeight);
313
363
  },
314
- [drawing, getLocalCoords, containerWidth, containerHeight]
364
+ [drawing, getLocalCoords, containerWidth, containerHeight, eraseAt]
315
365
  );
316
366
  const onPointerMove = useCallback(
317
367
  (e) => {
368
+ if (drawing.tool === "eraser") {
369
+ const coords2 = getLocalCoords(e);
370
+ if (!coords2) return;
371
+ const radius = eraseRadius(e.pointerType);
372
+ if (eraseGesture.current) {
373
+ eraseAt(coords2[0], coords2[1], radius);
374
+ } else {
375
+ const nextIds = strokeIdsNear(coords2[0], coords2[1], radius);
376
+ setEraseHoverIds((prev) => {
377
+ if (nextIds.length === prev.size && nextIds.every((id) => prev.has(id)))
378
+ return prev;
379
+ return new Set(nextIds);
380
+ });
381
+ }
382
+ return;
383
+ }
318
384
  if (drawing.tool !== "pen") return;
319
385
  const coords = getLocalCoords(e);
320
386
  if (!coords) return;
321
387
  const live = drawing.continueStroke(coords[0], coords[1], containerWidth, containerHeight);
322
388
  if (live) setLiveStroke(live);
323
389
  },
324
- [drawing, getLocalCoords, containerWidth, containerHeight]
390
+ [drawing, getLocalCoords, containerWidth, containerHeight, eraseAt, strokeIdsNear]
325
391
  );
326
392
  const onPointerUp = useCallback(() => {
393
+ const gesture = eraseGesture.current;
394
+ if (gesture) {
395
+ eraseGesture.current = null;
396
+ if (gesture.removed.size > 0) {
397
+ history.push({ type: "delete", strokesBefore: gesture.before, arcIndices: [] });
398
+ }
399
+ return;
400
+ }
327
401
  if (drawing.tool !== "pen") return;
328
402
  const stroke = drawing.endStroke();
329
403
  setLiveStroke(null);
@@ -332,21 +406,18 @@ function NumberLineProvider({
332
406
  history.push({ type: "add", strokes: [stroke] });
333
407
  }
334
408
  }, [drawing, history]);
409
+ const onPointerLeave = useCallback(() => {
410
+ if (eraseGesture.current) return;
411
+ setEraseHoverIds((prev) => prev.size === 0 ? prev : /* @__PURE__ */ new Set());
412
+ }, []);
335
413
  const onStrokeClick = useCallback(
336
414
  (id, e) => {
337
415
  e.stopPropagation();
338
- if (drawing.tool === "eraser") {
339
- const target = drawing.strokes.find((s) => s.id === id);
340
- if (target) {
341
- const strokesBefore = drawing.strokes;
342
- drawing.eraseStroke(id);
343
- history.push({ type: "delete", strokesBefore, arcIndices: [] });
344
- }
345
- } else if (drawing.tool === "none") {
416
+ if (drawing.tool === "none") {
346
417
  drawing.toggleSelect(id);
347
418
  }
348
419
  },
349
- [drawing, history]
420
+ [drawing]
350
421
  );
351
422
  const onArcClick = useCallback(
352
423
  (groupIndex, e) => {
@@ -415,6 +486,7 @@ function NumberLineProvider({
415
486
  const canDelete = drawing.strokes.some((s) => s.selected) || selectedArcIndices.size > 0;
416
487
  useEffect(() => {
417
488
  setSelectedArcIndices(/* @__PURE__ */ new Set());
489
+ setEraseHoverIds((prev) => prev.size === 0 ? prev : /* @__PURE__ */ new Set());
418
490
  }, [drawing.tool]);
419
491
  const keyHandlers = useRef({ toggleTool: drawing.toggleTool, clearArcSelection, canDelete, onDelete, onUndo });
420
492
  keyHandlers.current = { toggleTool: drawing.toggleTool, clearArcSelection, canDelete, onDelete, onUndo };
@@ -466,11 +538,13 @@ function NumberLineProvider({
466
538
  tool: drawing.tool,
467
539
  strokes: drawing.strokes,
468
540
  liveStroke,
541
+ eraseHoverIds,
469
542
  selectedArcIndices,
470
543
  deletedArcIndices,
471
544
  onPointerDown,
472
545
  onPointerMove,
473
546
  onPointerUp,
547
+ onPointerLeave,
474
548
  onStrokeClick,
475
549
  onArcClick,
476
550
  canDelete,
@@ -914,11 +988,10 @@ const GroupArcsLayer = memo(function GroupArcsLayer2({
914
988
  }
915
989
  return /* @__PURE__ */ jsx("g", { className: "nl-arcs-layer", children: arcs });
916
990
  });
917
- function DrawingLayer({ strokes, liveStroke, tool, width, height, onStrokeClick }) {
918
- const [hoveredId, setHoveredId] = useState(null);
991
+ function DrawingLayer({ strokes, liveStroke, tool, width, height, eraseHoverIds, onStrokeClick }) {
919
992
  return /* @__PURE__ */ jsxs("g", { className: "nl-drawing-layer", transform: `scale(${width} ${height})`, children: [
920
993
  strokes.map((s) => /* @__PURE__ */ jsxs("g", { children: [
921
- (s.selected || tool === "eraser" && s.id === hoveredId) && /* @__PURE__ */ jsx(
994
+ (s.selected || tool === "eraser" && eraseHoverIds.has(s.id)) && /* @__PURE__ */ jsx(
922
995
  "path",
923
996
  {
924
997
  d: s.d,
@@ -943,10 +1016,8 @@ function DrawingLayer({ strokes, liveStroke, tool, width, height, onStrokeClick
943
1016
  vectorEffect: "non-scaling-stroke",
944
1017
  pointerEvents: "stroke",
945
1018
  style: { cursor: tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default" },
946
- onPointerEnter: () => setHoveredId(s.id),
947
- onPointerLeave: () => setHoveredId((prev) => prev === s.id ? null : prev),
948
1019
  onClick: (e) => {
949
- if (tool === "eraser" || tool === "none") {
1020
+ if (tool === "none") {
950
1021
  e.stopPropagation();
951
1022
  onStrokeClick(s.id, e);
952
1023
  }
@@ -1013,11 +1084,13 @@ function NumberLine({ className }) {
1013
1084
  tool,
1014
1085
  strokes,
1015
1086
  liveStroke,
1087
+ eraseHoverIds,
1016
1088
  selectedArcIndices,
1017
1089
  deletedArcIndices,
1018
1090
  onPointerDown,
1019
1091
  onPointerMove,
1020
1092
  onPointerUp,
1093
+ onPointerLeave,
1021
1094
  onStrokeClick,
1022
1095
  onArcClick
1023
1096
  } = useNumberLineContext();
@@ -1184,6 +1257,8 @@ function NumberLine({ className }) {
1184
1257
  onPointerDown: handlePointerDown,
1185
1258
  onPointerMove: handlePointerMove,
1186
1259
  onPointerUp: handlePointerUp,
1260
+ onPointerCancel: handlePointerUp,
1261
+ onPointerLeave,
1187
1262
  onWheel: handleWheel,
1188
1263
  onDoubleClick: handleDoubleClick,
1189
1264
  onMouseEnter: () => enableZoom && setIsZoomActive(true),
@@ -1242,6 +1317,7 @@ function NumberLine({ className }) {
1242
1317
  tool,
1243
1318
  width: w,
1244
1319
  height: h,
1320
+ eraseHoverIds,
1245
1321
  onStrokeClick
1246
1322
  }
1247
1323
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcg-overseas/number-line",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
4
4
  "description": "Interactive number line component with group arcs and freehand drawing",
5
5
  "type": "module",
6
6
  "license": "MIT",