@dcg-overseas/number-line 0.1.1 → 0.1.3
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 +107 -38
- package/dist/index.d.ts +11 -2
- package/dist/index.js +108 -39
- package/package.json +1 -1
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",
|
|
@@ -402,6 +429,8 @@ const AxisLayer = React.memo(function AxisLayer2({
|
|
|
402
429
|
const isMajor = offset % step === 0;
|
|
403
430
|
const isGroupBoundary = groupSize > 0 && offset % groupSize === 0;
|
|
404
431
|
const showLabel = isMajor && offset % labelStep === 0;
|
|
432
|
+
const visible = isGroupBoundary ? showGroupTicks : isMajor ? showMajorTicks : showMinorTicks;
|
|
433
|
+
if (!visible) return null;
|
|
405
434
|
const tickH = isGroupBoundary ? 10 : isMajor ? 7 : 4;
|
|
406
435
|
const strokeW = isGroupBoundary ? 1.5 : isMajor ? 1 : 0.5;
|
|
407
436
|
const color = isMajor ? "#374151" : "#9ca3af";
|
|
@@ -465,8 +494,10 @@ const GroupArcsLayer = React.memo(function GroupArcsLayer2({
|
|
|
465
494
|
const rx = (x2 - x1) / 2;
|
|
466
495
|
const ry = Math.min(rx * 0.7, arcMaxHeight);
|
|
467
496
|
const color = arcColors[g % arcColors.length];
|
|
468
|
-
const
|
|
469
|
-
const
|
|
497
|
+
const arrowHeadH = 7;
|
|
498
|
+
const arrowHeadHW = 4;
|
|
499
|
+
const MIN_ARROW_SPAN = 18;
|
|
500
|
+
const textY = Math.min(axisY - ry - 6, axisY - MIN_ARROW_SPAN);
|
|
470
501
|
const arcPath = buildArcPath({ x1, x2, y: axisY, rx, ry });
|
|
471
502
|
const isSelected = selectedArcIndices.has(g);
|
|
472
503
|
const hitCursor = tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default";
|
|
@@ -514,8 +545,8 @@ const GroupArcsLayer = React.memo(function GroupArcsLayer2({
|
|
|
514
545
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
515
546
|
"text",
|
|
516
547
|
{
|
|
517
|
-
x:
|
|
518
|
-
y:
|
|
548
|
+
x: x2,
|
|
549
|
+
y: textY,
|
|
519
550
|
textAnchor: "middle",
|
|
520
551
|
fontSize: 10,
|
|
521
552
|
fill: isSelected ? "#1d4ed8" : color,
|
|
@@ -524,14 +555,34 @@ const GroupArcsLayer = React.memo(function GroupArcsLayer2({
|
|
|
524
555
|
style: { userSelect: "none", WebkitUserSelect: "none" },
|
|
525
556
|
children: end
|
|
526
557
|
}
|
|
558
|
+
),
|
|
559
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
560
|
+
"line",
|
|
561
|
+
{
|
|
562
|
+
x1: x2,
|
|
563
|
+
y1: textY + 4,
|
|
564
|
+
x2,
|
|
565
|
+
y2: axisY - arrowHeadH,
|
|
566
|
+
stroke: isSelected ? "#1d4ed8" : color,
|
|
567
|
+
strokeWidth: 1.5,
|
|
568
|
+
pointerEvents: "none"
|
|
569
|
+
}
|
|
570
|
+
),
|
|
571
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
572
|
+
"polygon",
|
|
573
|
+
{
|
|
574
|
+
points: `${x2},${axisY} ${x2 - arrowHeadHW},${axisY - arrowHeadH} ${x2 + arrowHeadHW},${axisY - arrowHeadH}`,
|
|
575
|
+
fill: isSelected ? "#1d4ed8" : color,
|
|
576
|
+
pointerEvents: "none"
|
|
577
|
+
}
|
|
527
578
|
)
|
|
528
579
|
] }, g)
|
|
529
580
|
);
|
|
530
581
|
}
|
|
531
582
|
return /* @__PURE__ */ jsxRuntime.jsx("g", { className: "nl-arcs-layer", children: arcs });
|
|
532
583
|
});
|
|
533
|
-
function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
534
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("g", { className: "nl-drawing-layer", children: [
|
|
584
|
+
function DrawingLayer({ strokes, liveStroke, tool, width, height, onStrokeClick }) {
|
|
585
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("g", { className: "nl-drawing-layer", transform: `scale(${width} ${height})`, children: [
|
|
535
586
|
strokes.map((s) => /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
|
|
536
587
|
s.selected && /* @__PURE__ */ jsxRuntime.jsx(
|
|
537
588
|
"path",
|
|
@@ -542,6 +593,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
542
593
|
strokeWidth: 18,
|
|
543
594
|
strokeLinecap: "round",
|
|
544
595
|
strokeLinejoin: "round",
|
|
596
|
+
vectorEffect: "non-scaling-stroke",
|
|
545
597
|
pointerEvents: "none"
|
|
546
598
|
}
|
|
547
599
|
),
|
|
@@ -554,6 +606,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
554
606
|
strokeWidth: tool === "eraser" ? 32 : 28,
|
|
555
607
|
strokeLinecap: "round",
|
|
556
608
|
strokeLinejoin: "round",
|
|
609
|
+
vectorEffect: "non-scaling-stroke",
|
|
557
610
|
pointerEvents: "stroke",
|
|
558
611
|
style: { cursor: tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default" },
|
|
559
612
|
onPointerDown: (e) => {
|
|
@@ -573,6 +626,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
573
626
|
strokeWidth: 2.5,
|
|
574
627
|
strokeLinecap: "round",
|
|
575
628
|
strokeLinejoin: "round",
|
|
629
|
+
vectorEffect: "non-scaling-stroke",
|
|
576
630
|
pointerEvents: "none"
|
|
577
631
|
}
|
|
578
632
|
)
|
|
@@ -586,6 +640,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
586
640
|
strokeWidth: 2.5,
|
|
587
641
|
strokeLinecap: "round",
|
|
588
642
|
strokeLinejoin: "round",
|
|
643
|
+
vectorEffect: "non-scaling-stroke",
|
|
589
644
|
pointerEvents: "none",
|
|
590
645
|
opacity: 0.7
|
|
591
646
|
}
|
|
@@ -594,12 +649,14 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
594
649
|
}
|
|
595
650
|
const PAD_LEFT = 30;
|
|
596
651
|
const PAD_RIGHT = 40;
|
|
597
|
-
const
|
|
598
|
-
const
|
|
652
|
+
const AXIS_BOTTOM_OFFSET = 50;
|
|
653
|
+
const ARC_TOP_PADDING = 16;
|
|
654
|
+
const MIN_ARC_HEIGHT = 24;
|
|
599
655
|
function NumberLine({ className }) {
|
|
600
656
|
const {
|
|
601
657
|
svgRef,
|
|
602
658
|
containerWidth,
|
|
659
|
+
containerHeight,
|
|
603
660
|
svgCursor,
|
|
604
661
|
min,
|
|
605
662
|
max,
|
|
@@ -607,6 +664,9 @@ function NumberLine({ className }) {
|
|
|
607
664
|
groupCount,
|
|
608
665
|
tickStep,
|
|
609
666
|
arcColors,
|
|
667
|
+
showGroupTicks,
|
|
668
|
+
showMajorTicks,
|
|
669
|
+
showMinorTicks,
|
|
610
670
|
tool,
|
|
611
671
|
strokes,
|
|
612
672
|
liveStroke,
|
|
@@ -618,14 +678,18 @@ function NumberLine({ className }) {
|
|
|
618
678
|
onStrokeClick,
|
|
619
679
|
onArcClick
|
|
620
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);
|
|
621
685
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
622
686
|
"svg",
|
|
623
687
|
{
|
|
624
688
|
ref: svgRef,
|
|
625
689
|
tabIndex: 0,
|
|
626
690
|
className: className ? `nl-svg ${className}` : "nl-svg",
|
|
627
|
-
viewBox: `0 0 ${
|
|
628
|
-
preserveAspectRatio: "
|
|
691
|
+
viewBox: `0 0 ${w} ${h}`,
|
|
692
|
+
preserveAspectRatio: "none",
|
|
629
693
|
style: { width: "100%", height: "100%", display: "block", outline: "none", cursor: svgCursor, touchAction: "none" },
|
|
630
694
|
onPointerDown,
|
|
631
695
|
onPointerMove,
|
|
@@ -636,13 +700,16 @@ function NumberLine({ className }) {
|
|
|
636
700
|
{
|
|
637
701
|
min,
|
|
638
702
|
max,
|
|
639
|
-
axisY
|
|
703
|
+
axisY,
|
|
640
704
|
padLeft: PAD_LEFT,
|
|
641
705
|
padRight: PAD_RIGHT,
|
|
642
|
-
viewWidth:
|
|
643
|
-
containerWidth,
|
|
706
|
+
viewWidth: w,
|
|
707
|
+
containerWidth: w,
|
|
644
708
|
groupSize,
|
|
645
|
-
tickStep
|
|
709
|
+
tickStep,
|
|
710
|
+
showGroupTicks,
|
|
711
|
+
showMajorTicks,
|
|
712
|
+
showMinorTicks
|
|
646
713
|
}
|
|
647
714
|
),
|
|
648
715
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -652,11 +719,11 @@ function NumberLine({ className }) {
|
|
|
652
719
|
max,
|
|
653
720
|
groupSize,
|
|
654
721
|
groupCount,
|
|
655
|
-
axisY
|
|
722
|
+
axisY,
|
|
656
723
|
padLeft: PAD_LEFT,
|
|
657
724
|
padRight: PAD_RIGHT,
|
|
658
|
-
viewWidth:
|
|
659
|
-
arcMaxHeight
|
|
725
|
+
viewWidth: w,
|
|
726
|
+
arcMaxHeight,
|
|
660
727
|
tool,
|
|
661
728
|
selectedArcIndices,
|
|
662
729
|
deletedArcIndices,
|
|
@@ -670,6 +737,8 @@ function NumberLine({ className }) {
|
|
|
670
737
|
strokes,
|
|
671
738
|
liveStroke,
|
|
672
739
|
tool,
|
|
740
|
+
width: w,
|
|
741
|
+
height: h,
|
|
673
742
|
onStrokeClick
|
|
674
743
|
}
|
|
675
744
|
)
|
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",
|
|
@@ -400,6 +427,8 @@ const AxisLayer = React.memo(function AxisLayer2({
|
|
|
400
427
|
const isMajor = offset % step === 0;
|
|
401
428
|
const isGroupBoundary = groupSize > 0 && offset % groupSize === 0;
|
|
402
429
|
const showLabel = isMajor && offset % labelStep === 0;
|
|
430
|
+
const visible = isGroupBoundary ? showGroupTicks : isMajor ? showMajorTicks : showMinorTicks;
|
|
431
|
+
if (!visible) return null;
|
|
403
432
|
const tickH = isGroupBoundary ? 10 : isMajor ? 7 : 4;
|
|
404
433
|
const strokeW = isGroupBoundary ? 1.5 : isMajor ? 1 : 0.5;
|
|
405
434
|
const color = isMajor ? "#374151" : "#9ca3af";
|
|
@@ -463,8 +492,10 @@ const GroupArcsLayer = memo(function GroupArcsLayer2({
|
|
|
463
492
|
const rx = (x2 - x1) / 2;
|
|
464
493
|
const ry = Math.min(rx * 0.7, arcMaxHeight);
|
|
465
494
|
const color = arcColors[g % arcColors.length];
|
|
466
|
-
const
|
|
467
|
-
const
|
|
495
|
+
const arrowHeadH = 7;
|
|
496
|
+
const arrowHeadHW = 4;
|
|
497
|
+
const MIN_ARROW_SPAN = 18;
|
|
498
|
+
const textY = Math.min(axisY - ry - 6, axisY - MIN_ARROW_SPAN);
|
|
468
499
|
const arcPath = buildArcPath({ x1, x2, y: axisY, rx, ry });
|
|
469
500
|
const isSelected = selectedArcIndices.has(g);
|
|
470
501
|
const hitCursor = tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default";
|
|
@@ -512,8 +543,8 @@ const GroupArcsLayer = memo(function GroupArcsLayer2({
|
|
|
512
543
|
/* @__PURE__ */ jsx(
|
|
513
544
|
"text",
|
|
514
545
|
{
|
|
515
|
-
x:
|
|
516
|
-
y:
|
|
546
|
+
x: x2,
|
|
547
|
+
y: textY,
|
|
517
548
|
textAnchor: "middle",
|
|
518
549
|
fontSize: 10,
|
|
519
550
|
fill: isSelected ? "#1d4ed8" : color,
|
|
@@ -522,14 +553,34 @@ const GroupArcsLayer = memo(function GroupArcsLayer2({
|
|
|
522
553
|
style: { userSelect: "none", WebkitUserSelect: "none" },
|
|
523
554
|
children: end
|
|
524
555
|
}
|
|
556
|
+
),
|
|
557
|
+
/* @__PURE__ */ jsx(
|
|
558
|
+
"line",
|
|
559
|
+
{
|
|
560
|
+
x1: x2,
|
|
561
|
+
y1: textY + 4,
|
|
562
|
+
x2,
|
|
563
|
+
y2: axisY - arrowHeadH,
|
|
564
|
+
stroke: isSelected ? "#1d4ed8" : color,
|
|
565
|
+
strokeWidth: 1.5,
|
|
566
|
+
pointerEvents: "none"
|
|
567
|
+
}
|
|
568
|
+
),
|
|
569
|
+
/* @__PURE__ */ jsx(
|
|
570
|
+
"polygon",
|
|
571
|
+
{
|
|
572
|
+
points: `${x2},${axisY} ${x2 - arrowHeadHW},${axisY - arrowHeadH} ${x2 + arrowHeadHW},${axisY - arrowHeadH}`,
|
|
573
|
+
fill: isSelected ? "#1d4ed8" : color,
|
|
574
|
+
pointerEvents: "none"
|
|
575
|
+
}
|
|
525
576
|
)
|
|
526
577
|
] }, g)
|
|
527
578
|
);
|
|
528
579
|
}
|
|
529
580
|
return /* @__PURE__ */ jsx("g", { className: "nl-arcs-layer", children: arcs });
|
|
530
581
|
});
|
|
531
|
-
function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
532
|
-
return /* @__PURE__ */ jsxs("g", { className: "nl-drawing-layer", children: [
|
|
582
|
+
function DrawingLayer({ strokes, liveStroke, tool, width, height, onStrokeClick }) {
|
|
583
|
+
return /* @__PURE__ */ jsxs("g", { className: "nl-drawing-layer", transform: `scale(${width} ${height})`, children: [
|
|
533
584
|
strokes.map((s) => /* @__PURE__ */ jsxs("g", { children: [
|
|
534
585
|
s.selected && /* @__PURE__ */ jsx(
|
|
535
586
|
"path",
|
|
@@ -540,6 +591,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
540
591
|
strokeWidth: 18,
|
|
541
592
|
strokeLinecap: "round",
|
|
542
593
|
strokeLinejoin: "round",
|
|
594
|
+
vectorEffect: "non-scaling-stroke",
|
|
543
595
|
pointerEvents: "none"
|
|
544
596
|
}
|
|
545
597
|
),
|
|
@@ -552,6 +604,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
552
604
|
strokeWidth: tool === "eraser" ? 32 : 28,
|
|
553
605
|
strokeLinecap: "round",
|
|
554
606
|
strokeLinejoin: "round",
|
|
607
|
+
vectorEffect: "non-scaling-stroke",
|
|
555
608
|
pointerEvents: "stroke",
|
|
556
609
|
style: { cursor: tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default" },
|
|
557
610
|
onPointerDown: (e) => {
|
|
@@ -571,6 +624,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
571
624
|
strokeWidth: 2.5,
|
|
572
625
|
strokeLinecap: "round",
|
|
573
626
|
strokeLinejoin: "round",
|
|
627
|
+
vectorEffect: "non-scaling-stroke",
|
|
574
628
|
pointerEvents: "none"
|
|
575
629
|
}
|
|
576
630
|
)
|
|
@@ -584,6 +638,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
584
638
|
strokeWidth: 2.5,
|
|
585
639
|
strokeLinecap: "round",
|
|
586
640
|
strokeLinejoin: "round",
|
|
641
|
+
vectorEffect: "non-scaling-stroke",
|
|
587
642
|
pointerEvents: "none",
|
|
588
643
|
opacity: 0.7
|
|
589
644
|
}
|
|
@@ -592,12 +647,14 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
592
647
|
}
|
|
593
648
|
const PAD_LEFT = 30;
|
|
594
649
|
const PAD_RIGHT = 40;
|
|
595
|
-
const
|
|
596
|
-
const
|
|
650
|
+
const AXIS_BOTTOM_OFFSET = 50;
|
|
651
|
+
const ARC_TOP_PADDING = 16;
|
|
652
|
+
const MIN_ARC_HEIGHT = 24;
|
|
597
653
|
function NumberLine({ className }) {
|
|
598
654
|
const {
|
|
599
655
|
svgRef,
|
|
600
656
|
containerWidth,
|
|
657
|
+
containerHeight,
|
|
601
658
|
svgCursor,
|
|
602
659
|
min,
|
|
603
660
|
max,
|
|
@@ -605,6 +662,9 @@ function NumberLine({ className }) {
|
|
|
605
662
|
groupCount,
|
|
606
663
|
tickStep,
|
|
607
664
|
arcColors,
|
|
665
|
+
showGroupTicks,
|
|
666
|
+
showMajorTicks,
|
|
667
|
+
showMinorTicks,
|
|
608
668
|
tool,
|
|
609
669
|
strokes,
|
|
610
670
|
liveStroke,
|
|
@@ -616,14 +676,18 @@ function NumberLine({ className }) {
|
|
|
616
676
|
onStrokeClick,
|
|
617
677
|
onArcClick
|
|
618
678
|
} = useNumberLineContext();
|
|
679
|
+
const w = containerWidth;
|
|
680
|
+
const h = containerHeight;
|
|
681
|
+
const axisY = Math.max(MIN_ARC_HEIGHT + ARC_TOP_PADDING, h - AXIS_BOTTOM_OFFSET);
|
|
682
|
+
const arcMaxHeight = Math.max(MIN_ARC_HEIGHT, axisY - ARC_TOP_PADDING);
|
|
619
683
|
return /* @__PURE__ */ jsxs(
|
|
620
684
|
"svg",
|
|
621
685
|
{
|
|
622
686
|
ref: svgRef,
|
|
623
687
|
tabIndex: 0,
|
|
624
688
|
className: className ? `nl-svg ${className}` : "nl-svg",
|
|
625
|
-
viewBox: `0 0 ${
|
|
626
|
-
preserveAspectRatio: "
|
|
689
|
+
viewBox: `0 0 ${w} ${h}`,
|
|
690
|
+
preserveAspectRatio: "none",
|
|
627
691
|
style: { width: "100%", height: "100%", display: "block", outline: "none", cursor: svgCursor, touchAction: "none" },
|
|
628
692
|
onPointerDown,
|
|
629
693
|
onPointerMove,
|
|
@@ -634,13 +698,16 @@ function NumberLine({ className }) {
|
|
|
634
698
|
{
|
|
635
699
|
min,
|
|
636
700
|
max,
|
|
637
|
-
axisY
|
|
701
|
+
axisY,
|
|
638
702
|
padLeft: PAD_LEFT,
|
|
639
703
|
padRight: PAD_RIGHT,
|
|
640
|
-
viewWidth:
|
|
641
|
-
containerWidth,
|
|
704
|
+
viewWidth: w,
|
|
705
|
+
containerWidth: w,
|
|
642
706
|
groupSize,
|
|
643
|
-
tickStep
|
|
707
|
+
tickStep,
|
|
708
|
+
showGroupTicks,
|
|
709
|
+
showMajorTicks,
|
|
710
|
+
showMinorTicks
|
|
644
711
|
}
|
|
645
712
|
),
|
|
646
713
|
/* @__PURE__ */ jsx(
|
|
@@ -650,11 +717,11 @@ function NumberLine({ className }) {
|
|
|
650
717
|
max,
|
|
651
718
|
groupSize,
|
|
652
719
|
groupCount,
|
|
653
|
-
axisY
|
|
720
|
+
axisY,
|
|
654
721
|
padLeft: PAD_LEFT,
|
|
655
722
|
padRight: PAD_RIGHT,
|
|
656
|
-
viewWidth:
|
|
657
|
-
arcMaxHeight
|
|
723
|
+
viewWidth: w,
|
|
724
|
+
arcMaxHeight,
|
|
658
725
|
tool,
|
|
659
726
|
selectedArcIndices,
|
|
660
727
|
deletedArcIndices,
|
|
@@ -668,6 +735,8 @@ function NumberLine({ className }) {
|
|
|
668
735
|
strokes,
|
|
669
736
|
liveStroke,
|
|
670
737
|
tool,
|
|
738
|
+
width: w,
|
|
739
|
+
height: h,
|
|
671
740
|
onStrokeClick
|
|
672
741
|
}
|
|
673
742
|
)
|