@dcg-overseas/number-line 0.1.6 → 0.1.7

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 ADDED
@@ -0,0 +1,1059 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const React = require("react");
5
+ function useHistory() {
6
+ const stackRef = React.useRef([]);
7
+ const [stackLen, setStackLen] = React.useState(0);
8
+ const push = React.useCallback((op) => {
9
+ stackRef.current = [...stackRef.current, op];
10
+ setStackLen(stackRef.current.length);
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];
15
+ stackRef.current = stackRef.current.slice(0, -1);
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
+ }
29
+ }, []);
30
+ const clear = React.useCallback(() => {
31
+ stackRef.current = [];
32
+ setStackLen(0);
33
+ }, []);
34
+ return { push, undo, clear, canUndo: stackLen > 0 };
35
+ }
36
+ function useDrawing() {
37
+ const [strokes, setStrokes] = React.useState([]);
38
+ const [tool, setTool] = React.useState("none");
39
+ const drawing = React.useRef(false);
40
+ const pointCount = React.useRef(0);
41
+ const currentD = React.useRef("");
42
+ const currentId = React.useRef("");
43
+ const toggleTool = React.useCallback((t) => {
44
+ setTool((prev) => {
45
+ if (prev === t) return "none";
46
+ return t;
47
+ });
48
+ setStrokes((prev) => prev.map((s) => ({ ...s, selected: false })));
49
+ }, []);
50
+ const startStroke = React.useCallback((x, y, w, h) => {
51
+ drawing.current = true;
52
+ pointCount.current = 1;
53
+ const xN = w > 0 ? x / w : 0;
54
+ const yN = h > 0 ? y / h : 0;
55
+ currentD.current = `M ${xN} ${yN}`;
56
+ currentId.current = `${Date.now()}-${Math.random()}`;
57
+ }, []);
58
+ const continueStroke = React.useCallback((x, y, w, h) => {
59
+ if (!drawing.current) return null;
60
+ pointCount.current++;
61
+ const xN = w > 0 ? x / w : 0;
62
+ const yN = h > 0 ? y / h : 0;
63
+ currentD.current += ` L ${xN} ${yN}`;
64
+ return {
65
+ id: currentId.current,
66
+ d: currentD.current,
67
+ selected: false
68
+ };
69
+ }, []);
70
+ const endStroke = React.useCallback(() => {
71
+ if (!drawing.current) return null;
72
+ drawing.current = false;
73
+ if (pointCount.current < 2) {
74
+ currentD.current = "";
75
+ pointCount.current = 0;
76
+ return null;
77
+ }
78
+ const stroke = {
79
+ id: currentId.current,
80
+ d: currentD.current,
81
+ selected: false
82
+ };
83
+ currentD.current = "";
84
+ pointCount.current = 0;
85
+ return stroke;
86
+ }, []);
87
+ const toggleSelect = React.useCallback((id) => {
88
+ setStrokes(
89
+ (prev) => prev.map((s) => s.id === id ? { ...s, selected: !s.selected } : s)
90
+ );
91
+ }, []);
92
+ const eraseStroke = React.useCallback((id) => {
93
+ setStrokes((prev) => prev.filter((s) => s.id !== id));
94
+ }, []);
95
+ const addStroke = React.useCallback((stroke) => {
96
+ setStrokes((prev) => [...prev, stroke]);
97
+ }, []);
98
+ const deleteSelected = React.useCallback(() => {
99
+ setStrokes((prev) => prev.filter((s) => !s.selected));
100
+ }, []);
101
+ const resetStrokes = React.useCallback(() => {
102
+ setStrokes([]);
103
+ }, []);
104
+ const restoreStrokes = React.useCallback((restored) => {
105
+ setStrokes(restored);
106
+ }, []);
107
+ return {
108
+ strokes,
109
+ tool,
110
+ toggleTool,
111
+ startStroke,
112
+ continueStroke,
113
+ endStroke,
114
+ toggleSelect,
115
+ eraseStroke,
116
+ addStroke,
117
+ deleteSelected,
118
+ resetStrokes,
119
+ restoreStrokes
120
+ };
121
+ }
122
+ function useContainerSize() {
123
+ const ref = React.useRef(null);
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
+ }, []);
133
+ React.useEffect(() => {
134
+ const el = ref.current;
135
+ if (!el) return;
136
+ const ro = new ResizeObserver((entries) => {
137
+ var _a;
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
+ }
142
+ });
143
+ ro.observe(el);
144
+ return () => ro.disconnect();
145
+ }, []);
146
+ return { ref, width: size.width, height: size.height };
147
+ }
148
+ function useZoom({
149
+ dataMin,
150
+ dataMax,
151
+ minZoom,
152
+ maxZoom,
153
+ controlledMin,
154
+ controlledMax,
155
+ onChange
156
+ }) {
157
+ const isControlled = controlledMin !== void 0 && controlledMax !== void 0;
158
+ const [internal, setInternal] = React.useState([dataMin, dataMax]);
159
+ const lastRange = React.useRef([dataMin, dataMax]);
160
+ React.useEffect(() => {
161
+ const [lm, lM] = lastRange.current;
162
+ if (lm !== dataMin || lM !== dataMax) {
163
+ lastRange.current = [dataMin, dataMax];
164
+ if (!isControlled) setInternal([dataMin, dataMax]);
165
+ }
166
+ }, [dataMin, dataMax, isControlled]);
167
+ const viewMin = isControlled ? controlledMin : internal[0];
168
+ const viewMax = isControlled ? controlledMax : internal[1];
169
+ const viewRef = React.useRef({ viewMin, viewMax });
170
+ viewRef.current = { viewMin, viewMax };
171
+ const dataRange = dataMax - dataMin;
172
+ const minView = dataRange / Math.max(1, maxZoom);
173
+ const maxView = dataRange / Math.max(1, Math.min(minZoom, maxZoom));
174
+ const commit = React.useCallback(
175
+ (next) => {
176
+ const { viewMin: curMin, viewMax: curMax } = viewRef.current;
177
+ let [a, b] = next;
178
+ let width = b - a;
179
+ if (width < minView) {
180
+ const c = (a + b) / 2;
181
+ a = c - minView / 2;
182
+ b = c + minView / 2;
183
+ width = minView;
184
+ }
185
+ if (width > maxView) {
186
+ const c = (a + b) / 2;
187
+ a = c - maxView / 2;
188
+ b = c + maxView / 2;
189
+ width = maxView;
190
+ }
191
+ if (a < dataMin) {
192
+ b += dataMin - a;
193
+ a = dataMin;
194
+ }
195
+ if (b > dataMax) {
196
+ a -= b - dataMax;
197
+ b = dataMax;
198
+ }
199
+ if (a < dataMin) a = dataMin;
200
+ if (b > dataMax) b = dataMax;
201
+ if (a === curMin && b === curMax) return;
202
+ if (!isControlled) setInternal([a, b]);
203
+ onChange == null ? void 0 : onChange(a, b);
204
+ },
205
+ [minView, maxView, dataMin, dataMax, isControlled, onChange]
206
+ );
207
+ const zoomAt = React.useCallback(
208
+ (factor, f) => {
209
+ const width = viewMax - viewMin;
210
+ const anchorVal = viewMin + width * f;
211
+ const newWidth = width / factor;
212
+ const a = anchorVal - newWidth * f;
213
+ const b = anchorVal + newWidth * (1 - f);
214
+ commit([a, b]);
215
+ },
216
+ [viewMin, viewMax, commit]
217
+ );
218
+ const panByFraction = React.useCallback(
219
+ (df) => {
220
+ const width = viewMax - viewMin;
221
+ const dv = width * df;
222
+ commit([viewMin + dv, viewMax + dv]);
223
+ },
224
+ [viewMin, viewMax, commit]
225
+ );
226
+ const resetView = React.useCallback(() => {
227
+ commit([dataMin, dataMax]);
228
+ }, [commit, dataMin, dataMax]);
229
+ return { viewMin, viewMax, zoomAt, panByFraction, resetView };
230
+ }
231
+ const NumberLineContext = React.createContext(null);
232
+ function useNumberLineContext() {
233
+ const ctx = React.useContext(NumberLineContext);
234
+ if (!ctx) throw new Error("Must be used inside <NumberLineProvider>");
235
+ return ctx;
236
+ }
237
+ function normalizeDomain(lo, hi) {
238
+ const a = Number(lo);
239
+ const b = Number(hi);
240
+ const x = Math.min(a, b);
241
+ const y = Math.max(a, b);
242
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
243
+ return { min: 0, max: 1 };
244
+ }
245
+ if (y <= x) {
246
+ return { min: x, max: x + 1 };
247
+ }
248
+ return { min: x, max: y };
249
+ }
250
+ function NumberLineProvider({
251
+ min,
252
+ max,
253
+ groupSize,
254
+ groupCount,
255
+ tickStep,
256
+ arcColors,
257
+ showGroupTicks = true,
258
+ showMajorTicks = true,
259
+ showMinorTicks = false,
260
+ enableZoom = false,
261
+ minZoom = 1,
262
+ maxZoom = 50,
263
+ viewMin: controlledViewMin,
264
+ viewMax: controlledViewMax,
265
+ onViewChange,
266
+ children
267
+ }) {
268
+ const { min: domainMin, max: domainMax } = normalizeDomain(min, max);
269
+ const [selectedArcIndices, setSelectedArcIndices] = React.useState(/* @__PURE__ */ new Set());
270
+ const [deletedArcIndices, setDeletedArcIndices] = React.useState(/* @__PURE__ */ new Set());
271
+ const { ref: svgRef, width: containerWidth, height: containerHeight } = useContainerSize();
272
+ const history = useHistory();
273
+ const drawing = useDrawing();
274
+ const { viewMin, viewMax, zoomAt, panByFraction, resetView } = useZoom({
275
+ dataMin: domainMin,
276
+ dataMax: domainMax,
277
+ minZoom,
278
+ maxZoom,
279
+ controlledMin: controlledViewMin,
280
+ controlledMax: controlledViewMax,
281
+ onChange: onViewChange
282
+ });
283
+ const [liveStroke, setLiveStroke] = React.useState(null);
284
+ const svgPoint = React.useRef(null);
285
+ React.useEffect(() => {
286
+ setDeletedArcIndices(/* @__PURE__ */ new Set());
287
+ setSelectedArcIndices(/* @__PURE__ */ new Set());
288
+ }, [domainMin, domainMax, groupSize, groupCount]);
289
+ const getLocalCoords = React.useCallback(
290
+ (e) => {
291
+ const svg = svgRef.current;
292
+ if (!svg) return null;
293
+ const ctm = svg.getScreenCTM();
294
+ if (!ctm) return null;
295
+ if (!svgPoint.current) svgPoint.current = svg.createSVGPoint();
296
+ svgPoint.current.x = e.clientX;
297
+ svgPoint.current.y = e.clientY;
298
+ const pt = svgPoint.current.matrixTransform(ctm.inverse());
299
+ return [pt.x, pt.y];
300
+ },
301
+ [svgRef]
302
+ );
303
+ const onPointerDown = React.useCallback(
304
+ (e) => {
305
+ e.currentTarget.focus();
306
+ if (drawing.tool !== "pen") return;
307
+ e.currentTarget.setPointerCapture(e.pointerId);
308
+ const coords = getLocalCoords(e);
309
+ if (!coords) return;
310
+ drawing.startStroke(coords[0], coords[1], containerWidth, containerHeight);
311
+ },
312
+ [drawing, getLocalCoords, containerWidth, containerHeight]
313
+ );
314
+ const onPointerMove = React.useCallback(
315
+ (e) => {
316
+ if (drawing.tool !== "pen") return;
317
+ const coords = getLocalCoords(e);
318
+ if (!coords) return;
319
+ const live = drawing.continueStroke(coords[0], coords[1], containerWidth, containerHeight);
320
+ if (live) setLiveStroke(live);
321
+ },
322
+ [drawing, getLocalCoords, containerWidth, containerHeight]
323
+ );
324
+ const onPointerUp = React.useCallback(() => {
325
+ if (drawing.tool !== "pen") return;
326
+ const stroke = drawing.endStroke();
327
+ setLiveStroke(null);
328
+ if (stroke) {
329
+ drawing.addStroke(stroke);
330
+ history.push({ type: "add", strokes: [stroke] });
331
+ }
332
+ }, [drawing, history]);
333
+ const onStrokeClick = React.useCallback(
334
+ (id, e) => {
335
+ e.stopPropagation();
336
+ if (drawing.tool === "eraser") {
337
+ const target = drawing.strokes.find((s) => s.id === id);
338
+ if (target) {
339
+ const strokesBefore = drawing.strokes;
340
+ drawing.eraseStroke(id);
341
+ history.push({ type: "delete", strokesBefore, arcIndices: [] });
342
+ }
343
+ } else if (drawing.tool === "none") {
344
+ drawing.toggleSelect(id);
345
+ }
346
+ },
347
+ [drawing, history]
348
+ );
349
+ const onArcClick = React.useCallback(
350
+ (groupIndex, e) => {
351
+ e.stopPropagation();
352
+ if (drawing.tool === "eraser") {
353
+ const strokesBefore = drawing.strokes;
354
+ setDeletedArcIndices((prev) => /* @__PURE__ */ new Set([...prev, groupIndex]));
355
+ history.push({ type: "delete", strokesBefore, arcIndices: [groupIndex] });
356
+ } else if (drawing.tool === "none") {
357
+ setSelectedArcIndices((prev) => {
358
+ const next = new Set(prev);
359
+ if (next.has(groupIndex)) next.delete(groupIndex);
360
+ else next.add(groupIndex);
361
+ return next;
362
+ });
363
+ }
364
+ },
365
+ [drawing.tool, drawing.strokes, history]
366
+ );
367
+ const clearArcSelection = React.useCallback(() => setSelectedArcIndices(/* @__PURE__ */ new Set()), []);
368
+ const onDelete = React.useCallback(() => {
369
+ const strokesBefore = drawing.strokes;
370
+ const deletedStrokes = drawing.strokes.filter((s) => s.selected);
371
+ const deletedArcs = [...selectedArcIndices];
372
+ if (deletedStrokes.length === 0 && deletedArcs.length === 0) return;
373
+ if (deletedStrokes.length > 0) drawing.deleteSelected();
374
+ if (deletedArcs.length > 0) {
375
+ setDeletedArcIndices((prev) => /* @__PURE__ */ new Set([...prev, ...deletedArcs]));
376
+ setSelectedArcIndices(/* @__PURE__ */ new Set());
377
+ }
378
+ history.push({ type: "delete", strokesBefore, arcIndices: deletedArcs });
379
+ }, [drawing, history, selectedArcIndices]);
380
+ const onReset = React.useCallback(() => {
381
+ drawing.resetStrokes();
382
+ history.clear();
383
+ setDeletedArcIndices(/* @__PURE__ */ new Set());
384
+ setSelectedArcIndices(/* @__PURE__ */ new Set());
385
+ }, [drawing, history]);
386
+ const onUndo = React.useCallback(() => {
387
+ const result = history.undo(drawing.strokes);
388
+ if (result.strokes !== null) drawing.restoreStrokes(result.strokes);
389
+ if (result.arcIndices.length > 0) {
390
+ setDeletedArcIndices((prev) => {
391
+ const next = new Set(prev);
392
+ result.arcIndices.forEach((i) => next.delete(i));
393
+ return next;
394
+ });
395
+ }
396
+ }, [history, drawing]);
397
+ const onTogglePen = React.useCallback(
398
+ () => {
399
+ drawing.toggleTool("pen");
400
+ clearArcSelection();
401
+ },
402
+ [drawing, clearArcSelection]
403
+ );
404
+ const onToggleEraser = React.useCallback(
405
+ () => {
406
+ drawing.toggleTool("eraser");
407
+ clearArcSelection();
408
+ },
409
+ [drawing, clearArcSelection]
410
+ );
411
+ const canDelete = drawing.strokes.some((s) => s.selected) || selectedArcIndices.size > 0;
412
+ React.useEffect(() => {
413
+ setSelectedArcIndices(/* @__PURE__ */ new Set());
414
+ }, [drawing.tool]);
415
+ const keyHandlers = React.useRef({ toggleTool: drawing.toggleTool, clearArcSelection, canDelete, onDelete, onUndo });
416
+ keyHandlers.current = { toggleTool: drawing.toggleTool, clearArcSelection, canDelete, onDelete, onUndo };
417
+ React.useEffect(() => {
418
+ const svg = svgRef.current;
419
+ if (!svg) return;
420
+ const onKey = (e) => {
421
+ const h = keyHandlers.current;
422
+ if (e.key === "Escape") {
423
+ h.toggleTool("none");
424
+ h.clearArcSelection();
425
+ } else if ((e.key === "Delete" || e.key === "Backspace") && h.canDelete) {
426
+ e.preventDefault();
427
+ h.onDelete();
428
+ } else if (e.key === "z" && (e.ctrlKey || e.metaKey)) {
429
+ e.preventDefault();
430
+ h.onUndo();
431
+ }
432
+ };
433
+ svg.addEventListener("keydown", onKey);
434
+ return () => svg.removeEventListener("keydown", onKey);
435
+ }, [svgRef]);
436
+ const svgCursor = drawing.tool === "pen" ? "crosshair" : drawing.tool === "eraser" ? "cell" : "default";
437
+ return /* @__PURE__ */ jsxRuntime.jsx(
438
+ NumberLineContext.Provider,
439
+ {
440
+ value: {
441
+ svgRef,
442
+ containerWidth,
443
+ containerHeight,
444
+ svgCursor,
445
+ min: domainMin,
446
+ max: domainMax,
447
+ groupSize,
448
+ groupCount,
449
+ tickStep,
450
+ arcColors,
451
+ showGroupTicks,
452
+ showMajorTicks,
453
+ showMinorTicks,
454
+ viewMin,
455
+ viewMax,
456
+ enableZoom,
457
+ resetView,
458
+ zoomAt,
459
+ panByFraction,
460
+ tool: drawing.tool,
461
+ strokes: drawing.strokes,
462
+ liveStroke,
463
+ selectedArcIndices,
464
+ deletedArcIndices,
465
+ onPointerDown,
466
+ onPointerMove,
467
+ onPointerUp,
468
+ onStrokeClick,
469
+ onArcClick,
470
+ canDelete,
471
+ canUndo: history.canUndo,
472
+ onTogglePen,
473
+ onToggleEraser,
474
+ onDelete,
475
+ onReset,
476
+ onUndo
477
+ },
478
+ children
479
+ }
480
+ );
481
+ }
482
+ function buildArcPath({ x1, x2, y, rx, ry }) {
483
+ return `M ${x1} ${y} A ${rx} ${ry} 0 0 1 ${x2} ${y}`;
484
+ }
485
+ function computeLabelStep(containerWidth, range) {
486
+ const maxLabels = Math.floor(containerWidth / 40);
487
+ if (maxLabels <= 0) return range;
488
+ const raw = range / maxLabels;
489
+ const candidates = [1, 2, 5, 10, 20, 25, 50, 100, 200, 500];
490
+ return candidates.find((c) => c >= raw) ?? candidates[candidates.length - 1];
491
+ }
492
+ const ARC_COLORS = [
493
+ "#7c3aed",
494
+ "#0891b2",
495
+ "#d97706",
496
+ "#be185d",
497
+ "#16a34a"
498
+ ];
499
+ function gcd(a, b) {
500
+ let x = Math.max(1, Math.abs(Math.round(a)));
501
+ let y = Math.max(1, Math.abs(Math.round(b)));
502
+ while (y !== 0) [x, y] = [y, x % y];
503
+ return x;
504
+ }
505
+ const AxisLayer = React.memo(function AxisLayer2({
506
+ min,
507
+ viewMin,
508
+ viewMax,
509
+ axisY,
510
+ padLeft,
511
+ padRight,
512
+ viewWidth,
513
+ containerWidth,
514
+ groupSize,
515
+ groupCount,
516
+ tickStep,
517
+ showGroupTicks,
518
+ showMajorTicks,
519
+ showMinorTicks
520
+ }) {
521
+ const range = viewMax - viewMin;
522
+ if (range <= 0) return null;
523
+ const usable = viewWidth - padLeft - padRight;
524
+ const toX = (v) => padLeft + (v - viewMin) / range * usable;
525
+ const step = Math.max(1, Math.round(tickStep));
526
+ const rawLabelStep = computeLabelStep(containerWidth, range);
527
+ const labelStep = Math.max(step, Math.ceil(rawLabelStep / step) * step);
528
+ const MAX_TICKS = 500;
529
+ const capStep = Math.max(1, Math.ceil(range / MAX_TICKS));
530
+ const visibleStep = groupSize > 0 ? gcd(step, groupSize) : step;
531
+ const iterStep = showMinorTicks ? capStep : Math.max(capStep, visibleStep);
532
+ const startVal = Math.floor(viewMin / iterStep) * iterStep;
533
+ const allTicks = [];
534
+ for (let v = startVal; v <= viewMax; v += iterStep) {
535
+ if (v >= viewMin && v <= viewMax) allTicks.push(v);
536
+ }
537
+ return /* @__PURE__ */ jsxRuntime.jsxs("g", { className: "nl-axis-layer", children: [
538
+ /* @__PURE__ */ jsxRuntime.jsx(
539
+ "line",
540
+ {
541
+ x1: padLeft - 4,
542
+ y1: axisY,
543
+ x2: viewWidth - padRight,
544
+ y2: axisY,
545
+ stroke: "#374151",
546
+ strokeWidth: 2
547
+ }
548
+ ),
549
+ allTicks.map((v) => {
550
+ const x = toX(v);
551
+ const offset = v - min;
552
+ const isMajor = offset % step === 0;
553
+ const maxGroupBoundary = min + groupCount * groupSize;
554
+ const isGroupBoundary = groupSize > 0 && offset % groupSize === 0 && v <= maxGroupBoundary;
555
+ const visible = isGroupBoundary && showGroupTicks || isMajor && showMajorTicks || !isGroupBoundary && !isMajor && showMinorTicks;
556
+ if (!visible) return null;
557
+ const renderAsGroup = isGroupBoundary && showGroupTicks;
558
+ const renderAsMajor = !renderAsGroup && isMajor && showMajorTicks;
559
+ const showLabel = (renderAsGroup || renderAsMajor) && offset % labelStep === 0;
560
+ const tickH = renderAsGroup ? 7 : renderAsMajor ? 10 : 4;
561
+ const strokeW = renderAsGroup ? 1.5 : renderAsMajor ? 1 : 0.5;
562
+ const color = renderAsGroup || renderAsMajor ? "#374151" : "#9ca3af";
563
+ return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
564
+ /* @__PURE__ */ jsxRuntime.jsx(
565
+ "line",
566
+ {
567
+ x1: x,
568
+ y1: axisY - tickH,
569
+ x2: x,
570
+ y2: axisY + tickH,
571
+ stroke: color,
572
+ strokeWidth: strokeW
573
+ }
574
+ ),
575
+ showLabel && /* @__PURE__ */ jsxRuntime.jsx(
576
+ "text",
577
+ {
578
+ x,
579
+ y: axisY + 22,
580
+ textAnchor: "middle",
581
+ fontSize: 11,
582
+ fill: "#374151",
583
+ pointerEvents: "none",
584
+ style: { userSelect: "none", WebkitUserSelect: "none" },
585
+ children: v
586
+ }
587
+ )
588
+ ] }, v);
589
+ })
590
+ ] });
591
+ });
592
+ const GroupArcsLayer = React.memo(function GroupArcsLayer2({
593
+ min,
594
+ max,
595
+ viewMin,
596
+ viewMax,
597
+ groupSize,
598
+ groupCount,
599
+ axisY,
600
+ padLeft,
601
+ padRight,
602
+ viewWidth,
603
+ arcMaxHeight,
604
+ tool,
605
+ selectedArcIndices,
606
+ deletedArcIndices,
607
+ onArcClick,
608
+ arcColors = ARC_COLORS
609
+ }) {
610
+ if (groupSize <= 0 || groupCount <= 0) return null;
611
+ const range = viewMax - viewMin;
612
+ const usable = viewWidth - padLeft - padRight;
613
+ const toX = (v) => padLeft + (v - viewMin) / range * usable;
614
+ const clipId = React.useId().replace(/:/g, "_") + "-arcs-clip";
615
+ const arcs = [];
616
+ for (let g = 0; g < groupCount; g++) {
617
+ if (deletedArcIndices.has(g)) continue;
618
+ const start = min + g * groupSize;
619
+ const end = start + groupSize;
620
+ if (end > max) break;
621
+ if (end < viewMin || start > viewMax) continue;
622
+ const x1 = toX(start);
623
+ const x2 = toX(end);
624
+ const rx = (x2 - x1) / 2;
625
+ const ry = Math.min(rx * 0.7, arcMaxHeight);
626
+ const color = arcColors[g % arcColors.length];
627
+ const arrowHeadH = 7;
628
+ const arrowHeadHW = 4;
629
+ const MIN_ARROW_SPAN = 18;
630
+ const textY = Math.min(axisY - ry - 6, axisY - MIN_ARROW_SPAN);
631
+ const arcPath = buildArcPath({ x1, x2, y: axisY, rx, ry });
632
+ const isSelected = selectedArcIndices.has(g);
633
+ const hitCursor = tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default";
634
+ arcs.push(
635
+ /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
636
+ isSelected && /* @__PURE__ */ jsxRuntime.jsx(
637
+ "path",
638
+ {
639
+ d: arcPath,
640
+ fill: "none",
641
+ stroke: "rgba(0,0,0,0.15)",
642
+ strokeWidth: 14,
643
+ strokeLinecap: "round",
644
+ pointerEvents: "none"
645
+ }
646
+ ),
647
+ /* @__PURE__ */ jsxRuntime.jsx(
648
+ "path",
649
+ {
650
+ d: arcPath,
651
+ fill: "none",
652
+ stroke: isSelected ? "#1d4ed8" : color,
653
+ strokeWidth: isSelected ? 3 : 2,
654
+ pointerEvents: "none"
655
+ }
656
+ ),
657
+ /* @__PURE__ */ jsxRuntime.jsx(
658
+ "path",
659
+ {
660
+ d: arcPath,
661
+ fill: "none",
662
+ stroke: "transparent",
663
+ strokeWidth: 20,
664
+ strokeLinecap: "round",
665
+ pointerEvents: "stroke",
666
+ style: { cursor: hitCursor },
667
+ onClick: (e) => {
668
+ if (tool === "eraser" || tool === "none") {
669
+ e.stopPropagation();
670
+ onArcClick(g, e);
671
+ }
672
+ }
673
+ }
674
+ ),
675
+ /* @__PURE__ */ jsxRuntime.jsx(
676
+ "text",
677
+ {
678
+ x: x2,
679
+ y: textY,
680
+ textAnchor: "middle",
681
+ fontSize: 10,
682
+ fill: isSelected ? "#1d4ed8" : color,
683
+ fontWeight: "600",
684
+ pointerEvents: "none",
685
+ style: { userSelect: "none", WebkitUserSelect: "none" },
686
+ children: end
687
+ }
688
+ ),
689
+ /* @__PURE__ */ jsxRuntime.jsx(
690
+ "line",
691
+ {
692
+ x1: x2,
693
+ y1: textY + 4,
694
+ x2,
695
+ y2: axisY - arrowHeadH,
696
+ stroke: isSelected ? "#1d4ed8" : color,
697
+ strokeWidth: 1.5,
698
+ pointerEvents: "none"
699
+ }
700
+ ),
701
+ /* @__PURE__ */ jsxRuntime.jsx(
702
+ "polygon",
703
+ {
704
+ points: `${x2},${axisY} ${x2 - arrowHeadHW},${axisY - arrowHeadH} ${x2 + arrowHeadHW},${axisY - arrowHeadH}`,
705
+ fill: isSelected ? "#1d4ed8" : color,
706
+ pointerEvents: "none"
707
+ }
708
+ )
709
+ ] }, g)
710
+ );
711
+ }
712
+ return /* @__PURE__ */ jsxRuntime.jsxs("g", { className: "nl-arcs-layer", children: [
713
+ /* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsx("clipPath", { id: clipId, children: /* @__PURE__ */ jsxRuntime.jsx(
714
+ "rect",
715
+ {
716
+ x: padLeft,
717
+ y: 0,
718
+ width: usable,
719
+ height: axisY + 1
720
+ }
721
+ ) }) }),
722
+ /* @__PURE__ */ jsxRuntime.jsx("g", { clipPath: `url(#${clipId})`, children: arcs })
723
+ ] });
724
+ });
725
+ function DrawingLayer({ strokes, liveStroke, tool, width, height, onStrokeClick }) {
726
+ return /* @__PURE__ */ jsxRuntime.jsxs("g", { className: "nl-drawing-layer", transform: `scale(${width} ${height})`, children: [
727
+ strokes.map((s) => /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
728
+ s.selected && /* @__PURE__ */ jsxRuntime.jsx(
729
+ "path",
730
+ {
731
+ d: s.d,
732
+ fill: "none",
733
+ stroke: "rgba(0,0,0,0.15)",
734
+ strokeWidth: 18,
735
+ strokeLinecap: "round",
736
+ strokeLinejoin: "round",
737
+ vectorEffect: "non-scaling-stroke",
738
+ pointerEvents: "none"
739
+ }
740
+ ),
741
+ /* @__PURE__ */ jsxRuntime.jsx(
742
+ "path",
743
+ {
744
+ d: s.d,
745
+ fill: "none",
746
+ stroke: "transparent",
747
+ strokeWidth: tool === "eraser" ? 32 : 28,
748
+ strokeLinecap: "round",
749
+ strokeLinejoin: "round",
750
+ vectorEffect: "non-scaling-stroke",
751
+ pointerEvents: "stroke",
752
+ style: { cursor: tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default" },
753
+ onClick: (e) => {
754
+ if (tool === "eraser" || tool === "none") {
755
+ e.stopPropagation();
756
+ onStrokeClick(s.id, e);
757
+ }
758
+ }
759
+ }
760
+ ),
761
+ /* @__PURE__ */ jsxRuntime.jsx(
762
+ "path",
763
+ {
764
+ d: s.d,
765
+ fill: "none",
766
+ stroke: "#7c3aed",
767
+ strokeWidth: 2.5,
768
+ strokeLinecap: "round",
769
+ strokeLinejoin: "round",
770
+ vectorEffect: "non-scaling-stroke",
771
+ pointerEvents: "none"
772
+ }
773
+ )
774
+ ] }, s.id)),
775
+ liveStroke && /* @__PURE__ */ jsxRuntime.jsx(
776
+ "path",
777
+ {
778
+ d: liveStroke.d,
779
+ fill: "none",
780
+ stroke: "#7c3aed",
781
+ strokeWidth: 2.5,
782
+ strokeLinecap: "round",
783
+ strokeLinejoin: "round",
784
+ vectorEffect: "non-scaling-stroke",
785
+ pointerEvents: "none",
786
+ opacity: 0.7
787
+ }
788
+ )
789
+ ] });
790
+ }
791
+ const PAD_LEFT_LEGACY = 30;
792
+ const PAD_RIGHT_LEGACY = 40;
793
+ const PAD_X = (PAD_LEFT_LEGACY + PAD_RIGHT_LEGACY) / 2;
794
+ const LABEL_SPACE_BELOW_AXIS = 42;
795
+ const LABEL_ABOVE_AXIS_SLOP = 32;
796
+ const ARC_TOP_PADDING = 16;
797
+ const MIN_ARC_HEIGHT = 24;
798
+ function NumberLine({ className }) {
799
+ const {
800
+ svgRef,
801
+ containerWidth,
802
+ containerHeight,
803
+ svgCursor,
804
+ min,
805
+ max,
806
+ groupSize,
807
+ groupCount,
808
+ tickStep,
809
+ arcColors,
810
+ showGroupTicks,
811
+ showMajorTicks,
812
+ showMinorTicks,
813
+ viewMin,
814
+ viewMax,
815
+ enableZoom,
816
+ resetView,
817
+ zoomAt,
818
+ panByFraction,
819
+ tool,
820
+ strokes,
821
+ liveStroke,
822
+ selectedArcIndices,
823
+ deletedArcIndices,
824
+ onPointerDown,
825
+ onPointerMove,
826
+ onPointerUp,
827
+ onStrokeClick,
828
+ onArcClick
829
+ } = useNumberLineContext();
830
+ const w = containerWidth;
831
+ const h = containerHeight;
832
+ const range = Math.max(viewMax - viewMin, 1e-6);
833
+ const usable = Math.max(w - 2 * PAD_X, 1);
834
+ let ryTypical = MIN_ARC_HEIGHT * 0.5;
835
+ if (groupSize > 0 && groupCount > 0) {
836
+ const rx = usable * groupSize / range / 2;
837
+ ryTypical = Math.min(rx * 0.7, h * 0.48);
838
+ }
839
+ const minAxisY = MIN_ARC_HEIGHT + ARC_TOP_PADDING;
840
+ const maxAxisY = h - LABEL_SPACE_BELOW_AXIS;
841
+ const axisYCentered = (h + ryTypical + LABEL_ABOVE_AXIS_SLOP - LABEL_SPACE_BELOW_AXIS) / 2;
842
+ const axisY = Math.min(maxAxisY, Math.max(minAxisY, axisYCentered));
843
+ const arcMaxHeight = Math.max(MIN_ARC_HEIGHT, axisY - ARC_TOP_PADDING);
844
+ const [isZoomActive, setIsZoomActive] = React.useState(false);
845
+ const [isPanning, setIsPanning] = React.useState(false);
846
+ const panStart = React.useRef(null);
847
+ const activePointers = React.useRef(/* @__PURE__ */ new Map());
848
+ const pinchState = React.useRef(null);
849
+ const handleWheel = React.useCallback(
850
+ (e) => {
851
+ if (!enableZoom || !isZoomActive) return;
852
+ e.preventDefault();
853
+ e.stopPropagation();
854
+ const svg = svgRef.current;
855
+ if (!svg) return;
856
+ const rect = svg.getBoundingClientRect();
857
+ const mouseX = e.clientX - rect.left;
858
+ const fraction = mouseX / w;
859
+ const factor = e.deltaY < 0 ? 1.15 : 1 / 1.15;
860
+ zoomAt(factor, fraction);
861
+ },
862
+ [enableZoom, isZoomActive, svgRef, w, zoomAt]
863
+ );
864
+ const handlePanStart = React.useCallback(
865
+ (e) => {
866
+ if (!enableZoom || tool !== "none") return;
867
+ if (e.button !== 0 && e.button !== 1) return;
868
+ const svg = svgRef.current;
869
+ if (!svg) return;
870
+ const rect = svg.getBoundingClientRect();
871
+ panStart.current = { x: e.clientX - rect.left, pointerId: e.pointerId };
872
+ },
873
+ [enableZoom, tool, svgRef]
874
+ );
875
+ const handlePanMove = React.useCallback(
876
+ (e) => {
877
+ if (!enableZoom || !panStart.current) return;
878
+ if (e.pointerId !== panStart.current.pointerId) return;
879
+ const svg = svgRef.current;
880
+ if (!svg) return;
881
+ const rect = svg.getBoundingClientRect();
882
+ const x = e.clientX - rect.left;
883
+ const dx = x - panStart.current.x;
884
+ if (!isPanning && Math.abs(dx) < 5) return;
885
+ if (!isPanning) {
886
+ setIsPanning(true);
887
+ svg.setPointerCapture(e.pointerId);
888
+ }
889
+ const df = -dx / w;
890
+ panByFraction(df);
891
+ panStart.current.x = x;
892
+ },
893
+ [enableZoom, isPanning, svgRef, w, panByFraction]
894
+ );
895
+ const handlePanEnd = React.useCallback(() => {
896
+ panStart.current = null;
897
+ setIsPanning(false);
898
+ }, []);
899
+ const updatePinch = React.useCallback(() => {
900
+ if (!enableZoom || activePointers.current.size !== 2) return;
901
+ const [p1, p2] = Array.from(activePointers.current.values());
902
+ const dist = Math.hypot(p2.x - p1.x, p2.y - p1.y);
903
+ const midX = (p1.x + p2.x) / 2;
904
+ if (!pinchState.current) {
905
+ pinchState.current = { dist, midX };
906
+ } else {
907
+ const factor = dist / pinchState.current.dist;
908
+ const fraction = midX / w;
909
+ zoomAt(factor, fraction);
910
+ pinchState.current = { dist, midX };
911
+ }
912
+ }, [enableZoom, w, zoomAt]);
913
+ const handlePointerDown = React.useCallback(
914
+ (e) => {
915
+ if (e.pointerType === "touch" && enableZoom) {
916
+ const svg = svgRef.current;
917
+ if (!svg) return;
918
+ const rect = svg.getBoundingClientRect();
919
+ activePointers.current.set(e.pointerId, {
920
+ x: e.clientX - rect.left,
921
+ y: e.clientY - rect.top
922
+ });
923
+ setIsZoomActive(true);
924
+ updatePinch();
925
+ }
926
+ if (tool === "pen") {
927
+ onPointerDown(e);
928
+ } else {
929
+ handlePanStart(e);
930
+ onPointerDown(e);
931
+ }
932
+ },
933
+ [tool, enableZoom, svgRef, onPointerDown, handlePanStart, updatePinch]
934
+ );
935
+ const handlePointerMove = React.useCallback(
936
+ (e) => {
937
+ if (e.pointerType === "touch" && enableZoom && activePointers.current.has(e.pointerId)) {
938
+ const svg = svgRef.current;
939
+ if (!svg) return;
940
+ const rect = svg.getBoundingClientRect();
941
+ activePointers.current.set(e.pointerId, {
942
+ x: e.clientX - rect.left,
943
+ y: e.clientY - rect.top
944
+ });
945
+ updatePinch();
946
+ }
947
+ onPointerMove(e);
948
+ handlePanMove(e);
949
+ },
950
+ [enableZoom, svgRef, onPointerMove, handlePanMove, updatePinch]
951
+ );
952
+ const handlePointerUp = React.useCallback(
953
+ (e) => {
954
+ if (activePointers.current.has(e.pointerId)) {
955
+ activePointers.current.delete(e.pointerId);
956
+ if (activePointers.current.size < 2) {
957
+ pinchState.current = null;
958
+ }
959
+ if (activePointers.current.size === 0 && e.pointerType === "touch") {
960
+ setIsZoomActive(false);
961
+ }
962
+ }
963
+ onPointerUp();
964
+ handlePanEnd();
965
+ },
966
+ [onPointerUp, handlePanEnd]
967
+ );
968
+ const handleDoubleClick = React.useCallback(
969
+ () => {
970
+ if (!enableZoom) return;
971
+ resetView();
972
+ },
973
+ [enableZoom, resetView]
974
+ );
975
+ return /* @__PURE__ */ jsxRuntime.jsxs(
976
+ "svg",
977
+ {
978
+ ref: svgRef,
979
+ tabIndex: 0,
980
+ className: className ? `nl-svg ${className}` : "nl-svg",
981
+ viewBox: `0 0 ${w} ${h}`,
982
+ preserveAspectRatio: "none",
983
+ style: {
984
+ width: "100%",
985
+ height: "100%",
986
+ display: "block",
987
+ outline: enableZoom && isZoomActive ? "2px solid rgba(59, 130, 246, 0.5)" : "none",
988
+ cursor: svgCursor,
989
+ touchAction: "none"
990
+ },
991
+ onPointerDown: handlePointerDown,
992
+ onPointerMove: handlePointerMove,
993
+ onPointerUp: handlePointerUp,
994
+ onWheel: handleWheel,
995
+ onDoubleClick: handleDoubleClick,
996
+ onMouseEnter: () => enableZoom && setIsZoomActive(true),
997
+ onMouseLeave: () => enableZoom && setIsZoomActive(false),
998
+ onFocus: () => enableZoom && setIsZoomActive(true),
999
+ onBlur: () => enableZoom && setIsZoomActive(false),
1000
+ children: [
1001
+ /* @__PURE__ */ jsxRuntime.jsx(
1002
+ AxisLayer,
1003
+ {
1004
+ min,
1005
+ max,
1006
+ viewMin,
1007
+ viewMax,
1008
+ axisY,
1009
+ padLeft: PAD_X,
1010
+ padRight: PAD_X,
1011
+ viewWidth: w,
1012
+ containerWidth: w,
1013
+ groupSize,
1014
+ groupCount,
1015
+ tickStep,
1016
+ showGroupTicks,
1017
+ showMajorTicks,
1018
+ showMinorTicks
1019
+ }
1020
+ ),
1021
+ /* @__PURE__ */ jsxRuntime.jsx(
1022
+ GroupArcsLayer,
1023
+ {
1024
+ min,
1025
+ max,
1026
+ viewMin,
1027
+ viewMax,
1028
+ groupSize,
1029
+ groupCount,
1030
+ axisY,
1031
+ padLeft: PAD_X,
1032
+ padRight: PAD_X,
1033
+ viewWidth: w,
1034
+ arcMaxHeight,
1035
+ tool,
1036
+ selectedArcIndices,
1037
+ deletedArcIndices,
1038
+ onArcClick,
1039
+ arcColors
1040
+ }
1041
+ ),
1042
+ /* @__PURE__ */ jsxRuntime.jsx(
1043
+ DrawingLayer,
1044
+ {
1045
+ strokes,
1046
+ liveStroke,
1047
+ tool,
1048
+ width: w,
1049
+ height: h,
1050
+ onStrokeClick
1051
+ }
1052
+ )
1053
+ ]
1054
+ }
1055
+ );
1056
+ }
1057
+ exports.NumberLine = NumberLine;
1058
+ exports.NumberLineProvider = NumberLineProvider;
1059
+ exports.useNumberLineContext = useNumberLineContext;