@dcg-overseas/number-line 0.1.2 → 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 +83 -35
- package/dist/index.d.ts +11 -2
- package/dist/index.js +84 -36
- 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,9 +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 textY = axisY - ry - 6;
|
|
469
497
|
const arrowHeadH = 7;
|
|
470
498
|
const arrowHeadHW = 4;
|
|
499
|
+
const MIN_ARROW_SPAN = 18;
|
|
500
|
+
const textY = Math.min(axisY - ry - 6, axisY - MIN_ARROW_SPAN);
|
|
471
501
|
const arcPath = buildArcPath({ x1, x2, y: axisY, rx, ry });
|
|
472
502
|
const isSelected = selectedArcIndices.has(g);
|
|
473
503
|
const hitCursor = tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default";
|
|
@@ -551,8 +581,8 @@ const GroupArcsLayer = React.memo(function GroupArcsLayer2({
|
|
|
551
581
|
}
|
|
552
582
|
return /* @__PURE__ */ jsxRuntime.jsx("g", { className: "nl-arcs-layer", children: arcs });
|
|
553
583
|
});
|
|
554
|
-
function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
555
|
-
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: [
|
|
556
586
|
strokes.map((s) => /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
|
|
557
587
|
s.selected && /* @__PURE__ */ jsxRuntime.jsx(
|
|
558
588
|
"path",
|
|
@@ -563,6 +593,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
563
593
|
strokeWidth: 18,
|
|
564
594
|
strokeLinecap: "round",
|
|
565
595
|
strokeLinejoin: "round",
|
|
596
|
+
vectorEffect: "non-scaling-stroke",
|
|
566
597
|
pointerEvents: "none"
|
|
567
598
|
}
|
|
568
599
|
),
|
|
@@ -575,6 +606,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
575
606
|
strokeWidth: tool === "eraser" ? 32 : 28,
|
|
576
607
|
strokeLinecap: "round",
|
|
577
608
|
strokeLinejoin: "round",
|
|
609
|
+
vectorEffect: "non-scaling-stroke",
|
|
578
610
|
pointerEvents: "stroke",
|
|
579
611
|
style: { cursor: tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default" },
|
|
580
612
|
onPointerDown: (e) => {
|
|
@@ -594,6 +626,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
594
626
|
strokeWidth: 2.5,
|
|
595
627
|
strokeLinecap: "round",
|
|
596
628
|
strokeLinejoin: "round",
|
|
629
|
+
vectorEffect: "non-scaling-stroke",
|
|
597
630
|
pointerEvents: "none"
|
|
598
631
|
}
|
|
599
632
|
)
|
|
@@ -607,6 +640,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
607
640
|
strokeWidth: 2.5,
|
|
608
641
|
strokeLinecap: "round",
|
|
609
642
|
strokeLinejoin: "round",
|
|
643
|
+
vectorEffect: "non-scaling-stroke",
|
|
610
644
|
pointerEvents: "none",
|
|
611
645
|
opacity: 0.7
|
|
612
646
|
}
|
|
@@ -615,12 +649,14 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
615
649
|
}
|
|
616
650
|
const PAD_LEFT = 30;
|
|
617
651
|
const PAD_RIGHT = 40;
|
|
618
|
-
const
|
|
619
|
-
const
|
|
652
|
+
const AXIS_BOTTOM_OFFSET = 50;
|
|
653
|
+
const ARC_TOP_PADDING = 16;
|
|
654
|
+
const MIN_ARC_HEIGHT = 24;
|
|
620
655
|
function NumberLine({ className }) {
|
|
621
656
|
const {
|
|
622
657
|
svgRef,
|
|
623
658
|
containerWidth,
|
|
659
|
+
containerHeight,
|
|
624
660
|
svgCursor,
|
|
625
661
|
min,
|
|
626
662
|
max,
|
|
@@ -628,6 +664,9 @@ function NumberLine({ className }) {
|
|
|
628
664
|
groupCount,
|
|
629
665
|
tickStep,
|
|
630
666
|
arcColors,
|
|
667
|
+
showGroupTicks,
|
|
668
|
+
showMajorTicks,
|
|
669
|
+
showMinorTicks,
|
|
631
670
|
tool,
|
|
632
671
|
strokes,
|
|
633
672
|
liveStroke,
|
|
@@ -639,14 +678,18 @@ function NumberLine({ className }) {
|
|
|
639
678
|
onStrokeClick,
|
|
640
679
|
onArcClick
|
|
641
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);
|
|
642
685
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
643
686
|
"svg",
|
|
644
687
|
{
|
|
645
688
|
ref: svgRef,
|
|
646
689
|
tabIndex: 0,
|
|
647
690
|
className: className ? `nl-svg ${className}` : "nl-svg",
|
|
648
|
-
viewBox: `0 0 ${
|
|
649
|
-
preserveAspectRatio: "
|
|
691
|
+
viewBox: `0 0 ${w} ${h}`,
|
|
692
|
+
preserveAspectRatio: "none",
|
|
650
693
|
style: { width: "100%", height: "100%", display: "block", outline: "none", cursor: svgCursor, touchAction: "none" },
|
|
651
694
|
onPointerDown,
|
|
652
695
|
onPointerMove,
|
|
@@ -657,13 +700,16 @@ function NumberLine({ className }) {
|
|
|
657
700
|
{
|
|
658
701
|
min,
|
|
659
702
|
max,
|
|
660
|
-
axisY
|
|
703
|
+
axisY,
|
|
661
704
|
padLeft: PAD_LEFT,
|
|
662
705
|
padRight: PAD_RIGHT,
|
|
663
|
-
viewWidth:
|
|
664
|
-
containerWidth,
|
|
706
|
+
viewWidth: w,
|
|
707
|
+
containerWidth: w,
|
|
665
708
|
groupSize,
|
|
666
|
-
tickStep
|
|
709
|
+
tickStep,
|
|
710
|
+
showGroupTicks,
|
|
711
|
+
showMajorTicks,
|
|
712
|
+
showMinorTicks
|
|
667
713
|
}
|
|
668
714
|
),
|
|
669
715
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -673,11 +719,11 @@ function NumberLine({ className }) {
|
|
|
673
719
|
max,
|
|
674
720
|
groupSize,
|
|
675
721
|
groupCount,
|
|
676
|
-
axisY
|
|
722
|
+
axisY,
|
|
677
723
|
padLeft: PAD_LEFT,
|
|
678
724
|
padRight: PAD_RIGHT,
|
|
679
|
-
viewWidth:
|
|
680
|
-
arcMaxHeight
|
|
725
|
+
viewWidth: w,
|
|
726
|
+
arcMaxHeight,
|
|
681
727
|
tool,
|
|
682
728
|
selectedArcIndices,
|
|
683
729
|
deletedArcIndices,
|
|
@@ -691,6 +737,8 @@ function NumberLine({ className }) {
|
|
|
691
737
|
strokes,
|
|
692
738
|
liveStroke,
|
|
693
739
|
tool,
|
|
740
|
+
width: w,
|
|
741
|
+
height: h,
|
|
694
742
|
onStrokeClick
|
|
695
743
|
}
|
|
696
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,9 +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 textY = axisY - ry - 6;
|
|
467
495
|
const arrowHeadH = 7;
|
|
468
496
|
const arrowHeadHW = 4;
|
|
497
|
+
const MIN_ARROW_SPAN = 18;
|
|
498
|
+
const textY = Math.min(axisY - ry - 6, axisY - MIN_ARROW_SPAN);
|
|
469
499
|
const arcPath = buildArcPath({ x1, x2, y: axisY, rx, ry });
|
|
470
500
|
const isSelected = selectedArcIndices.has(g);
|
|
471
501
|
const hitCursor = tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default";
|
|
@@ -549,8 +579,8 @@ const GroupArcsLayer = memo(function GroupArcsLayer2({
|
|
|
549
579
|
}
|
|
550
580
|
return /* @__PURE__ */ jsx("g", { className: "nl-arcs-layer", children: arcs });
|
|
551
581
|
});
|
|
552
|
-
function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
553
|
-
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: [
|
|
554
584
|
strokes.map((s) => /* @__PURE__ */ jsxs("g", { children: [
|
|
555
585
|
s.selected && /* @__PURE__ */ jsx(
|
|
556
586
|
"path",
|
|
@@ -561,6 +591,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
561
591
|
strokeWidth: 18,
|
|
562
592
|
strokeLinecap: "round",
|
|
563
593
|
strokeLinejoin: "round",
|
|
594
|
+
vectorEffect: "non-scaling-stroke",
|
|
564
595
|
pointerEvents: "none"
|
|
565
596
|
}
|
|
566
597
|
),
|
|
@@ -573,6 +604,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
573
604
|
strokeWidth: tool === "eraser" ? 32 : 28,
|
|
574
605
|
strokeLinecap: "round",
|
|
575
606
|
strokeLinejoin: "round",
|
|
607
|
+
vectorEffect: "non-scaling-stroke",
|
|
576
608
|
pointerEvents: "stroke",
|
|
577
609
|
style: { cursor: tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default" },
|
|
578
610
|
onPointerDown: (e) => {
|
|
@@ -592,6 +624,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
592
624
|
strokeWidth: 2.5,
|
|
593
625
|
strokeLinecap: "round",
|
|
594
626
|
strokeLinejoin: "round",
|
|
627
|
+
vectorEffect: "non-scaling-stroke",
|
|
595
628
|
pointerEvents: "none"
|
|
596
629
|
}
|
|
597
630
|
)
|
|
@@ -605,6 +638,7 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
605
638
|
strokeWidth: 2.5,
|
|
606
639
|
strokeLinecap: "round",
|
|
607
640
|
strokeLinejoin: "round",
|
|
641
|
+
vectorEffect: "non-scaling-stroke",
|
|
608
642
|
pointerEvents: "none",
|
|
609
643
|
opacity: 0.7
|
|
610
644
|
}
|
|
@@ -613,12 +647,14 @@ function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
|
613
647
|
}
|
|
614
648
|
const PAD_LEFT = 30;
|
|
615
649
|
const PAD_RIGHT = 40;
|
|
616
|
-
const
|
|
617
|
-
const
|
|
650
|
+
const AXIS_BOTTOM_OFFSET = 50;
|
|
651
|
+
const ARC_TOP_PADDING = 16;
|
|
652
|
+
const MIN_ARC_HEIGHT = 24;
|
|
618
653
|
function NumberLine({ className }) {
|
|
619
654
|
const {
|
|
620
655
|
svgRef,
|
|
621
656
|
containerWidth,
|
|
657
|
+
containerHeight,
|
|
622
658
|
svgCursor,
|
|
623
659
|
min,
|
|
624
660
|
max,
|
|
@@ -626,6 +662,9 @@ function NumberLine({ className }) {
|
|
|
626
662
|
groupCount,
|
|
627
663
|
tickStep,
|
|
628
664
|
arcColors,
|
|
665
|
+
showGroupTicks,
|
|
666
|
+
showMajorTicks,
|
|
667
|
+
showMinorTicks,
|
|
629
668
|
tool,
|
|
630
669
|
strokes,
|
|
631
670
|
liveStroke,
|
|
@@ -637,14 +676,18 @@ function NumberLine({ className }) {
|
|
|
637
676
|
onStrokeClick,
|
|
638
677
|
onArcClick
|
|
639
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);
|
|
640
683
|
return /* @__PURE__ */ jsxs(
|
|
641
684
|
"svg",
|
|
642
685
|
{
|
|
643
686
|
ref: svgRef,
|
|
644
687
|
tabIndex: 0,
|
|
645
688
|
className: className ? `nl-svg ${className}` : "nl-svg",
|
|
646
|
-
viewBox: `0 0 ${
|
|
647
|
-
preserveAspectRatio: "
|
|
689
|
+
viewBox: `0 0 ${w} ${h}`,
|
|
690
|
+
preserveAspectRatio: "none",
|
|
648
691
|
style: { width: "100%", height: "100%", display: "block", outline: "none", cursor: svgCursor, touchAction: "none" },
|
|
649
692
|
onPointerDown,
|
|
650
693
|
onPointerMove,
|
|
@@ -655,13 +698,16 @@ function NumberLine({ className }) {
|
|
|
655
698
|
{
|
|
656
699
|
min,
|
|
657
700
|
max,
|
|
658
|
-
axisY
|
|
701
|
+
axisY,
|
|
659
702
|
padLeft: PAD_LEFT,
|
|
660
703
|
padRight: PAD_RIGHT,
|
|
661
|
-
viewWidth:
|
|
662
|
-
containerWidth,
|
|
704
|
+
viewWidth: w,
|
|
705
|
+
containerWidth: w,
|
|
663
706
|
groupSize,
|
|
664
|
-
tickStep
|
|
707
|
+
tickStep,
|
|
708
|
+
showGroupTicks,
|
|
709
|
+
showMajorTicks,
|
|
710
|
+
showMinorTicks
|
|
665
711
|
}
|
|
666
712
|
),
|
|
667
713
|
/* @__PURE__ */ jsx(
|
|
@@ -671,11 +717,11 @@ function NumberLine({ className }) {
|
|
|
671
717
|
max,
|
|
672
718
|
groupSize,
|
|
673
719
|
groupCount,
|
|
674
|
-
axisY
|
|
720
|
+
axisY,
|
|
675
721
|
padLeft: PAD_LEFT,
|
|
676
722
|
padRight: PAD_RIGHT,
|
|
677
|
-
viewWidth:
|
|
678
|
-
arcMaxHeight
|
|
723
|
+
viewWidth: w,
|
|
724
|
+
arcMaxHeight,
|
|
679
725
|
tool,
|
|
680
726
|
selectedArcIndices,
|
|
681
727
|
deletedArcIndices,
|
|
@@ -689,6 +735,8 @@ function NumberLine({ className }) {
|
|
|
689
735
|
strokes,
|
|
690
736
|
liveStroke,
|
|
691
737
|
tool,
|
|
738
|
+
width: w,
|
|
739
|
+
height: h,
|
|
692
740
|
onStrokeClick
|
|
693
741
|
}
|
|
694
742
|
)
|