@dcg-overseas/number-line 0.1.0 → 0.1.1
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 +682 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +682 -0
- package/package.json +1 -1
- package/dist/.tsbuildinfo +0 -1
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,682 @@
|
|
|
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) => {
|
|
51
|
+
drawing.current = true;
|
|
52
|
+
pointCount.current = 1;
|
|
53
|
+
currentD.current = `M ${x} ${y}`;
|
|
54
|
+
currentId.current = `${Date.now()}-${Math.random()}`;
|
|
55
|
+
}, []);
|
|
56
|
+
const continueStroke = React.useCallback((x, y) => {
|
|
57
|
+
if (!drawing.current) return null;
|
|
58
|
+
pointCount.current++;
|
|
59
|
+
currentD.current += ` L ${x} ${y}`;
|
|
60
|
+
return {
|
|
61
|
+
id: currentId.current,
|
|
62
|
+
d: currentD.current,
|
|
63
|
+
selected: false
|
|
64
|
+
};
|
|
65
|
+
}, []);
|
|
66
|
+
const endStroke = React.useCallback(() => {
|
|
67
|
+
if (!drawing.current) return null;
|
|
68
|
+
drawing.current = false;
|
|
69
|
+
if (pointCount.current < 2) {
|
|
70
|
+
currentD.current = "";
|
|
71
|
+
pointCount.current = 0;
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const stroke = {
|
|
75
|
+
id: currentId.current,
|
|
76
|
+
d: currentD.current,
|
|
77
|
+
selected: false
|
|
78
|
+
};
|
|
79
|
+
currentD.current = "";
|
|
80
|
+
pointCount.current = 0;
|
|
81
|
+
return stroke;
|
|
82
|
+
}, []);
|
|
83
|
+
const toggleSelect = React.useCallback((id) => {
|
|
84
|
+
setStrokes(
|
|
85
|
+
(prev) => prev.map((s) => s.id === id ? { ...s, selected: !s.selected } : s)
|
|
86
|
+
);
|
|
87
|
+
}, []);
|
|
88
|
+
const eraseStroke = React.useCallback((id) => {
|
|
89
|
+
setStrokes((prev) => prev.filter((s) => s.id !== id));
|
|
90
|
+
}, []);
|
|
91
|
+
const addStroke = React.useCallback((stroke) => {
|
|
92
|
+
setStrokes((prev) => [...prev, stroke]);
|
|
93
|
+
}, []);
|
|
94
|
+
const deleteSelected = React.useCallback(() => {
|
|
95
|
+
setStrokes((prev) => prev.filter((s) => !s.selected));
|
|
96
|
+
}, []);
|
|
97
|
+
const resetStrokes = React.useCallback(() => {
|
|
98
|
+
setStrokes([]);
|
|
99
|
+
}, []);
|
|
100
|
+
const restoreStrokes = React.useCallback((restored) => {
|
|
101
|
+
setStrokes(restored);
|
|
102
|
+
}, []);
|
|
103
|
+
return {
|
|
104
|
+
strokes,
|
|
105
|
+
tool,
|
|
106
|
+
toggleTool,
|
|
107
|
+
startStroke,
|
|
108
|
+
continueStroke,
|
|
109
|
+
endStroke,
|
|
110
|
+
toggleSelect,
|
|
111
|
+
eraseStroke,
|
|
112
|
+
addStroke,
|
|
113
|
+
deleteSelected,
|
|
114
|
+
resetStrokes,
|
|
115
|
+
restoreStrokes
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function useContainerSize() {
|
|
119
|
+
const ref = React.useRef(null);
|
|
120
|
+
const [width, setWidth] = React.useState(600);
|
|
121
|
+
React.useEffect(() => {
|
|
122
|
+
const el = ref.current;
|
|
123
|
+
if (!el) return;
|
|
124
|
+
const ro = new ResizeObserver((entries) => {
|
|
125
|
+
var _a;
|
|
126
|
+
const w = (_a = entries[0]) == null ? void 0 : _a.contentRect.width;
|
|
127
|
+
if (w && w > 0) setWidth(w);
|
|
128
|
+
});
|
|
129
|
+
ro.observe(el);
|
|
130
|
+
const rect = el.getBoundingClientRect();
|
|
131
|
+
if (rect.width > 0) setWidth(rect.width);
|
|
132
|
+
return () => ro.disconnect();
|
|
133
|
+
}, []);
|
|
134
|
+
return { ref, width };
|
|
135
|
+
}
|
|
136
|
+
const NumberLineContext = React.createContext(null);
|
|
137
|
+
function useNumberLineContext() {
|
|
138
|
+
const ctx = React.useContext(NumberLineContext);
|
|
139
|
+
if (!ctx) throw new Error("Must be used inside <NumberLineProvider>");
|
|
140
|
+
return ctx;
|
|
141
|
+
}
|
|
142
|
+
const DESIGN_WIDTH = 1e3;
|
|
143
|
+
const DESIGN_HEIGHT = 200;
|
|
144
|
+
function NumberLineProvider({
|
|
145
|
+
min,
|
|
146
|
+
max,
|
|
147
|
+
groupSize,
|
|
148
|
+
groupCount,
|
|
149
|
+
tickStep,
|
|
150
|
+
arcColors,
|
|
151
|
+
children
|
|
152
|
+
}) {
|
|
153
|
+
const [selectedArcIndices, setSelectedArcIndices] = React.useState(/* @__PURE__ */ new Set());
|
|
154
|
+
const [deletedArcIndices, setDeletedArcIndices] = React.useState(/* @__PURE__ */ new Set());
|
|
155
|
+
const { ref: svgRef, width: containerWidth } = useContainerSize();
|
|
156
|
+
const history = useHistory();
|
|
157
|
+
const drawing = useDrawing();
|
|
158
|
+
const [liveStroke, setLiveStroke] = React.useState(null);
|
|
159
|
+
const svgPoint = React.useRef(null);
|
|
160
|
+
React.useEffect(() => {
|
|
161
|
+
setDeletedArcIndices(/* @__PURE__ */ new Set());
|
|
162
|
+
setSelectedArcIndices(/* @__PURE__ */ new Set());
|
|
163
|
+
}, [min, max, groupSize, groupCount]);
|
|
164
|
+
const getLocalCoords = React.useCallback(
|
|
165
|
+
(e) => {
|
|
166
|
+
const svg = svgRef.current;
|
|
167
|
+
if (!svg) return null;
|
|
168
|
+
const ctm = svg.getScreenCTM();
|
|
169
|
+
if (!ctm) return null;
|
|
170
|
+
if (!svgPoint.current) svgPoint.current = svg.createSVGPoint();
|
|
171
|
+
svgPoint.current.x = e.clientX;
|
|
172
|
+
svgPoint.current.y = e.clientY;
|
|
173
|
+
const pt = svgPoint.current.matrixTransform(ctm.inverse());
|
|
174
|
+
return [pt.x, pt.y];
|
|
175
|
+
},
|
|
176
|
+
[svgRef]
|
|
177
|
+
);
|
|
178
|
+
const onPointerDown = React.useCallback(
|
|
179
|
+
(e) => {
|
|
180
|
+
e.currentTarget.focus();
|
|
181
|
+
if (drawing.tool !== "pen") return;
|
|
182
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
183
|
+
const coords = getLocalCoords(e);
|
|
184
|
+
if (!coords) return;
|
|
185
|
+
drawing.startStroke(...coords);
|
|
186
|
+
},
|
|
187
|
+
[drawing, getLocalCoords]
|
|
188
|
+
);
|
|
189
|
+
const onPointerMove = React.useCallback(
|
|
190
|
+
(e) => {
|
|
191
|
+
if (drawing.tool !== "pen") return;
|
|
192
|
+
const coords = getLocalCoords(e);
|
|
193
|
+
if (!coords) return;
|
|
194
|
+
const live = drawing.continueStroke(...coords);
|
|
195
|
+
if (live) setLiveStroke(live);
|
|
196
|
+
},
|
|
197
|
+
[drawing, getLocalCoords]
|
|
198
|
+
);
|
|
199
|
+
const onPointerUp = React.useCallback(() => {
|
|
200
|
+
if (drawing.tool !== "pen") return;
|
|
201
|
+
const stroke = drawing.endStroke();
|
|
202
|
+
setLiveStroke(null);
|
|
203
|
+
if (stroke) {
|
|
204
|
+
drawing.addStroke(stroke);
|
|
205
|
+
history.push({ type: "add", strokes: [stroke] });
|
|
206
|
+
}
|
|
207
|
+
}, [drawing, history]);
|
|
208
|
+
const onStrokeClick = React.useCallback(
|
|
209
|
+
(id, e) => {
|
|
210
|
+
e.stopPropagation();
|
|
211
|
+
if (drawing.tool === "eraser") {
|
|
212
|
+
const target = drawing.strokes.find((s) => s.id === id);
|
|
213
|
+
if (target) {
|
|
214
|
+
const strokesBefore = drawing.strokes;
|
|
215
|
+
drawing.eraseStroke(id);
|
|
216
|
+
history.push({ type: "delete", strokesBefore, arcIndices: [] });
|
|
217
|
+
}
|
|
218
|
+
} else if (drawing.tool === "none") {
|
|
219
|
+
drawing.toggleSelect(id);
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
[drawing, history]
|
|
223
|
+
);
|
|
224
|
+
const onArcClick = React.useCallback(
|
|
225
|
+
(groupIndex, e) => {
|
|
226
|
+
e.stopPropagation();
|
|
227
|
+
if (drawing.tool === "eraser") {
|
|
228
|
+
const strokesBefore = drawing.strokes;
|
|
229
|
+
setDeletedArcIndices((prev) => /* @__PURE__ */ new Set([...prev, groupIndex]));
|
|
230
|
+
history.push({ type: "delete", strokesBefore, arcIndices: [groupIndex] });
|
|
231
|
+
} else if (drawing.tool === "none") {
|
|
232
|
+
setSelectedArcIndices((prev) => {
|
|
233
|
+
const next = new Set(prev);
|
|
234
|
+
if (next.has(groupIndex)) next.delete(groupIndex);
|
|
235
|
+
else next.add(groupIndex);
|
|
236
|
+
return next;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
[drawing.tool, drawing.strokes, history]
|
|
241
|
+
);
|
|
242
|
+
const clearArcSelection = React.useCallback(() => setSelectedArcIndices(/* @__PURE__ */ new Set()), []);
|
|
243
|
+
const onDelete = React.useCallback(() => {
|
|
244
|
+
const strokesBefore = drawing.strokes;
|
|
245
|
+
const deletedStrokes = drawing.strokes.filter((s) => s.selected);
|
|
246
|
+
const deletedArcs = [...selectedArcIndices];
|
|
247
|
+
if (deletedStrokes.length === 0 && deletedArcs.length === 0) return;
|
|
248
|
+
if (deletedStrokes.length > 0) drawing.deleteSelected();
|
|
249
|
+
if (deletedArcs.length > 0) {
|
|
250
|
+
setDeletedArcIndices((prev) => /* @__PURE__ */ new Set([...prev, ...deletedArcs]));
|
|
251
|
+
setSelectedArcIndices(/* @__PURE__ */ new Set());
|
|
252
|
+
}
|
|
253
|
+
history.push({ type: "delete", strokesBefore, arcIndices: deletedArcs });
|
|
254
|
+
}, [drawing, history, selectedArcIndices]);
|
|
255
|
+
const onReset = React.useCallback(() => {
|
|
256
|
+
drawing.resetStrokes();
|
|
257
|
+
history.clear();
|
|
258
|
+
setDeletedArcIndices(/* @__PURE__ */ new Set());
|
|
259
|
+
setSelectedArcIndices(/* @__PURE__ */ new Set());
|
|
260
|
+
}, [drawing, history]);
|
|
261
|
+
const onUndo = React.useCallback(() => {
|
|
262
|
+
const result = history.undo(drawing.strokes);
|
|
263
|
+
if (result.strokes !== null) drawing.restoreStrokes(result.strokes);
|
|
264
|
+
if (result.arcIndices.length > 0) {
|
|
265
|
+
setDeletedArcIndices((prev) => {
|
|
266
|
+
const next = new Set(prev);
|
|
267
|
+
result.arcIndices.forEach((i) => next.delete(i));
|
|
268
|
+
return next;
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}, [history, drawing]);
|
|
272
|
+
const onTogglePen = React.useCallback(
|
|
273
|
+
() => {
|
|
274
|
+
drawing.toggleTool("pen");
|
|
275
|
+
clearArcSelection();
|
|
276
|
+
},
|
|
277
|
+
[drawing, clearArcSelection]
|
|
278
|
+
);
|
|
279
|
+
const onToggleEraser = React.useCallback(
|
|
280
|
+
() => {
|
|
281
|
+
drawing.toggleTool("eraser");
|
|
282
|
+
clearArcSelection();
|
|
283
|
+
},
|
|
284
|
+
[drawing, clearArcSelection]
|
|
285
|
+
);
|
|
286
|
+
const canDelete = drawing.strokes.some((s) => s.selected) || selectedArcIndices.size > 0;
|
|
287
|
+
React.useEffect(() => {
|
|
288
|
+
setSelectedArcIndices(/* @__PURE__ */ new Set());
|
|
289
|
+
}, [drawing.tool]);
|
|
290
|
+
const keyHandlers = React.useRef({ toggleTool: drawing.toggleTool, clearArcSelection, canDelete, onDelete, onUndo });
|
|
291
|
+
keyHandlers.current = { toggleTool: drawing.toggleTool, clearArcSelection, canDelete, onDelete, onUndo };
|
|
292
|
+
React.useEffect(() => {
|
|
293
|
+
const svg = svgRef.current;
|
|
294
|
+
if (!svg) return;
|
|
295
|
+
const onKey = (e) => {
|
|
296
|
+
const h = keyHandlers.current;
|
|
297
|
+
if (e.key === "Escape") {
|
|
298
|
+
h.toggleTool("none");
|
|
299
|
+
h.clearArcSelection();
|
|
300
|
+
} else if ((e.key === "Delete" || e.key === "Backspace") && h.canDelete) {
|
|
301
|
+
e.preventDefault();
|
|
302
|
+
h.onDelete();
|
|
303
|
+
} else if (e.key === "z" && (e.ctrlKey || e.metaKey)) {
|
|
304
|
+
e.preventDefault();
|
|
305
|
+
h.onUndo();
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
svg.addEventListener("keydown", onKey);
|
|
309
|
+
return () => svg.removeEventListener("keydown", onKey);
|
|
310
|
+
}, [svgRef]);
|
|
311
|
+
const svgCursor = drawing.tool === "pen" ? "crosshair" : drawing.tool === "eraser" ? "cell" : "default";
|
|
312
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
313
|
+
NumberLineContext.Provider,
|
|
314
|
+
{
|
|
315
|
+
value: {
|
|
316
|
+
svgRef,
|
|
317
|
+
viewWidth: DESIGN_WIDTH,
|
|
318
|
+
containerWidth,
|
|
319
|
+
svgCursor,
|
|
320
|
+
min,
|
|
321
|
+
max,
|
|
322
|
+
groupSize,
|
|
323
|
+
groupCount,
|
|
324
|
+
tickStep,
|
|
325
|
+
arcColors,
|
|
326
|
+
tool: drawing.tool,
|
|
327
|
+
strokes: drawing.strokes,
|
|
328
|
+
liveStroke,
|
|
329
|
+
selectedArcIndices,
|
|
330
|
+
deletedArcIndices,
|
|
331
|
+
onPointerDown,
|
|
332
|
+
onPointerMove,
|
|
333
|
+
onPointerUp,
|
|
334
|
+
onStrokeClick,
|
|
335
|
+
onArcClick,
|
|
336
|
+
canDelete,
|
|
337
|
+
canUndo: history.canUndo,
|
|
338
|
+
onTogglePen,
|
|
339
|
+
onToggleEraser,
|
|
340
|
+
onDelete,
|
|
341
|
+
onReset,
|
|
342
|
+
onUndo
|
|
343
|
+
},
|
|
344
|
+
children
|
|
345
|
+
}
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
function buildArcPath({ x1, x2, y, rx, ry }) {
|
|
349
|
+
return `M ${x1} ${y} A ${rx} ${ry} 0 0 1 ${x2} ${y}`;
|
|
350
|
+
}
|
|
351
|
+
function computeLabelStep(containerWidth, range) {
|
|
352
|
+
const maxLabels = Math.floor(containerWidth / 40);
|
|
353
|
+
if (maxLabels <= 0) return range;
|
|
354
|
+
const raw = range / maxLabels;
|
|
355
|
+
const candidates = [1, 2, 5, 10, 20, 25, 50, 100, 200, 500];
|
|
356
|
+
return candidates.find((c) => c >= raw) ?? candidates[candidates.length - 1];
|
|
357
|
+
}
|
|
358
|
+
const ARC_COLORS = [
|
|
359
|
+
"#7c3aed",
|
|
360
|
+
"#0891b2",
|
|
361
|
+
"#d97706",
|
|
362
|
+
"#be185d",
|
|
363
|
+
"#16a34a"
|
|
364
|
+
];
|
|
365
|
+
const AxisLayer = React.memo(function AxisLayer2({
|
|
366
|
+
min,
|
|
367
|
+
max,
|
|
368
|
+
axisY,
|
|
369
|
+
padLeft,
|
|
370
|
+
padRight,
|
|
371
|
+
viewWidth,
|
|
372
|
+
containerWidth,
|
|
373
|
+
groupSize,
|
|
374
|
+
tickStep
|
|
375
|
+
}) {
|
|
376
|
+
const range = max - min;
|
|
377
|
+
if (range <= 0) return null;
|
|
378
|
+
const usable = viewWidth - padLeft - padRight;
|
|
379
|
+
const toX = (v) => padLeft + (v - min) / range * usable;
|
|
380
|
+
const step = Math.max(1, Math.round(tickStep));
|
|
381
|
+
const rawLabelStep = computeLabelStep(containerWidth, range);
|
|
382
|
+
const labelStep = Math.max(step, Math.ceil(rawLabelStep / step) * step);
|
|
383
|
+
const MAX_TICKS = 500;
|
|
384
|
+
const minorStep = Math.max(1, Math.ceil(range / MAX_TICKS));
|
|
385
|
+
const allTicks = [];
|
|
386
|
+
for (let v = min; v <= max; v += minorStep) allTicks.push(v);
|
|
387
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("g", { className: "nl-axis-layer", children: [
|
|
388
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
389
|
+
"line",
|
|
390
|
+
{
|
|
391
|
+
x1: padLeft - 4,
|
|
392
|
+
y1: axisY,
|
|
393
|
+
x2: viewWidth - padRight,
|
|
394
|
+
y2: axisY,
|
|
395
|
+
stroke: "#374151",
|
|
396
|
+
strokeWidth: 2
|
|
397
|
+
}
|
|
398
|
+
),
|
|
399
|
+
allTicks.map((v) => {
|
|
400
|
+
const x = toX(v);
|
|
401
|
+
const offset = v - min;
|
|
402
|
+
const isMajor = offset % step === 0;
|
|
403
|
+
const isGroupBoundary = groupSize > 0 && offset % groupSize === 0;
|
|
404
|
+
const showLabel = isMajor && offset % labelStep === 0;
|
|
405
|
+
const tickH = isGroupBoundary ? 10 : isMajor ? 7 : 4;
|
|
406
|
+
const strokeW = isGroupBoundary ? 1.5 : isMajor ? 1 : 0.5;
|
|
407
|
+
const color = isMajor ? "#374151" : "#9ca3af";
|
|
408
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
|
|
409
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
410
|
+
"line",
|
|
411
|
+
{
|
|
412
|
+
x1: x,
|
|
413
|
+
y1: axisY - tickH,
|
|
414
|
+
x2: x,
|
|
415
|
+
y2: axisY + tickH,
|
|
416
|
+
stroke: color,
|
|
417
|
+
strokeWidth: strokeW
|
|
418
|
+
}
|
|
419
|
+
),
|
|
420
|
+
showLabel && /* @__PURE__ */ jsxRuntime.jsx(
|
|
421
|
+
"text",
|
|
422
|
+
{
|
|
423
|
+
x,
|
|
424
|
+
y: axisY + 22,
|
|
425
|
+
textAnchor: "middle",
|
|
426
|
+
fontSize: 11,
|
|
427
|
+
fill: "#374151",
|
|
428
|
+
pointerEvents: "none",
|
|
429
|
+
style: { userSelect: "none", WebkitUserSelect: "none" },
|
|
430
|
+
children: v
|
|
431
|
+
}
|
|
432
|
+
)
|
|
433
|
+
] }, v);
|
|
434
|
+
})
|
|
435
|
+
] });
|
|
436
|
+
});
|
|
437
|
+
const GroupArcsLayer = React.memo(function GroupArcsLayer2({
|
|
438
|
+
min,
|
|
439
|
+
max,
|
|
440
|
+
groupSize,
|
|
441
|
+
groupCount,
|
|
442
|
+
axisY,
|
|
443
|
+
padLeft,
|
|
444
|
+
padRight,
|
|
445
|
+
viewWidth,
|
|
446
|
+
arcMaxHeight,
|
|
447
|
+
tool,
|
|
448
|
+
selectedArcIndices,
|
|
449
|
+
deletedArcIndices,
|
|
450
|
+
onArcClick,
|
|
451
|
+
arcColors = ARC_COLORS
|
|
452
|
+
}) {
|
|
453
|
+
if (groupSize <= 0 || groupCount <= 0) return null;
|
|
454
|
+
const range = max - min;
|
|
455
|
+
const usable = viewWidth - padLeft - padRight;
|
|
456
|
+
const toX = (v) => padLeft + (v - min) / range * usable;
|
|
457
|
+
const arcs = [];
|
|
458
|
+
for (let g = 0; g < groupCount; g++) {
|
|
459
|
+
if (deletedArcIndices.has(g)) continue;
|
|
460
|
+
const start = min + g * groupSize;
|
|
461
|
+
const end = start + groupSize;
|
|
462
|
+
if (end > max) break;
|
|
463
|
+
const x1 = toX(start);
|
|
464
|
+
const x2 = toX(end);
|
|
465
|
+
const rx = (x2 - x1) / 2;
|
|
466
|
+
const ry = Math.min(rx * 0.7, arcMaxHeight);
|
|
467
|
+
const color = arcColors[g % arcColors.length];
|
|
468
|
+
const midX = (x1 + x2) / 2;
|
|
469
|
+
const labelY = axisY - ry - 6;
|
|
470
|
+
const arcPath = buildArcPath({ x1, x2, y: axisY, rx, ry });
|
|
471
|
+
const isSelected = selectedArcIndices.has(g);
|
|
472
|
+
const hitCursor = tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default";
|
|
473
|
+
arcs.push(
|
|
474
|
+
/* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
|
|
475
|
+
isSelected && /* @__PURE__ */ jsxRuntime.jsx(
|
|
476
|
+
"path",
|
|
477
|
+
{
|
|
478
|
+
d: arcPath,
|
|
479
|
+
fill: "none",
|
|
480
|
+
stroke: "rgba(0,0,0,0.15)",
|
|
481
|
+
strokeWidth: 14,
|
|
482
|
+
strokeLinecap: "round",
|
|
483
|
+
pointerEvents: "none"
|
|
484
|
+
}
|
|
485
|
+
),
|
|
486
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
487
|
+
"path",
|
|
488
|
+
{
|
|
489
|
+
d: arcPath,
|
|
490
|
+
fill: "none",
|
|
491
|
+
stroke: isSelected ? "#1d4ed8" : color,
|
|
492
|
+
strokeWidth: isSelected ? 3 : 2,
|
|
493
|
+
pointerEvents: "none"
|
|
494
|
+
}
|
|
495
|
+
),
|
|
496
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
497
|
+
"path",
|
|
498
|
+
{
|
|
499
|
+
d: arcPath,
|
|
500
|
+
fill: "none",
|
|
501
|
+
stroke: "transparent",
|
|
502
|
+
strokeWidth: 20,
|
|
503
|
+
strokeLinecap: "round",
|
|
504
|
+
pointerEvents: "stroke",
|
|
505
|
+
style: { cursor: hitCursor },
|
|
506
|
+
onPointerDown: (e) => {
|
|
507
|
+
if (tool === "eraser" || tool === "none") {
|
|
508
|
+
e.stopPropagation();
|
|
509
|
+
onArcClick(g, e);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
),
|
|
514
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
515
|
+
"text",
|
|
516
|
+
{
|
|
517
|
+
x: midX,
|
|
518
|
+
y: labelY,
|
|
519
|
+
textAnchor: "middle",
|
|
520
|
+
fontSize: 10,
|
|
521
|
+
fill: isSelected ? "#1d4ed8" : color,
|
|
522
|
+
fontWeight: "600",
|
|
523
|
+
pointerEvents: "none",
|
|
524
|
+
style: { userSelect: "none", WebkitUserSelect: "none" },
|
|
525
|
+
children: end
|
|
526
|
+
}
|
|
527
|
+
)
|
|
528
|
+
] }, g)
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
return /* @__PURE__ */ jsxRuntime.jsx("g", { className: "nl-arcs-layer", children: arcs });
|
|
532
|
+
});
|
|
533
|
+
function DrawingLayer({ strokes, liveStroke, tool, onStrokeClick }) {
|
|
534
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("g", { className: "nl-drawing-layer", children: [
|
|
535
|
+
strokes.map((s) => /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
|
|
536
|
+
s.selected && /* @__PURE__ */ jsxRuntime.jsx(
|
|
537
|
+
"path",
|
|
538
|
+
{
|
|
539
|
+
d: s.d,
|
|
540
|
+
fill: "none",
|
|
541
|
+
stroke: "rgba(0,0,0,0.15)",
|
|
542
|
+
strokeWidth: 18,
|
|
543
|
+
strokeLinecap: "round",
|
|
544
|
+
strokeLinejoin: "round",
|
|
545
|
+
pointerEvents: "none"
|
|
546
|
+
}
|
|
547
|
+
),
|
|
548
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
549
|
+
"path",
|
|
550
|
+
{
|
|
551
|
+
d: s.d,
|
|
552
|
+
fill: "none",
|
|
553
|
+
stroke: "transparent",
|
|
554
|
+
strokeWidth: tool === "eraser" ? 32 : 28,
|
|
555
|
+
strokeLinecap: "round",
|
|
556
|
+
strokeLinejoin: "round",
|
|
557
|
+
pointerEvents: "stroke",
|
|
558
|
+
style: { cursor: tool === "eraser" ? "cell" : tool === "none" ? "pointer" : "default" },
|
|
559
|
+
onPointerDown: (e) => {
|
|
560
|
+
if (tool === "eraser" || tool === "none") {
|
|
561
|
+
e.stopPropagation();
|
|
562
|
+
onStrokeClick(s.id, e);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
),
|
|
567
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
568
|
+
"path",
|
|
569
|
+
{
|
|
570
|
+
d: s.d,
|
|
571
|
+
fill: "none",
|
|
572
|
+
stroke: "#7c3aed",
|
|
573
|
+
strokeWidth: 2.5,
|
|
574
|
+
strokeLinecap: "round",
|
|
575
|
+
strokeLinejoin: "round",
|
|
576
|
+
pointerEvents: "none"
|
|
577
|
+
}
|
|
578
|
+
)
|
|
579
|
+
] }, s.id)),
|
|
580
|
+
liveStroke && /* @__PURE__ */ jsxRuntime.jsx(
|
|
581
|
+
"path",
|
|
582
|
+
{
|
|
583
|
+
d: liveStroke.d,
|
|
584
|
+
fill: "none",
|
|
585
|
+
stroke: "#7c3aed",
|
|
586
|
+
strokeWidth: 2.5,
|
|
587
|
+
strokeLinecap: "round",
|
|
588
|
+
strokeLinejoin: "round",
|
|
589
|
+
pointerEvents: "none",
|
|
590
|
+
opacity: 0.7
|
|
591
|
+
}
|
|
592
|
+
)
|
|
593
|
+
] });
|
|
594
|
+
}
|
|
595
|
+
const PAD_LEFT = 30;
|
|
596
|
+
const PAD_RIGHT = 40;
|
|
597
|
+
const AXIS_Y = 130;
|
|
598
|
+
const ARC_MAX_HEIGHT = 80;
|
|
599
|
+
function NumberLine({ className }) {
|
|
600
|
+
const {
|
|
601
|
+
svgRef,
|
|
602
|
+
containerWidth,
|
|
603
|
+
svgCursor,
|
|
604
|
+
min,
|
|
605
|
+
max,
|
|
606
|
+
groupSize,
|
|
607
|
+
groupCount,
|
|
608
|
+
tickStep,
|
|
609
|
+
arcColors,
|
|
610
|
+
tool,
|
|
611
|
+
strokes,
|
|
612
|
+
liveStroke,
|
|
613
|
+
selectedArcIndices,
|
|
614
|
+
deletedArcIndices,
|
|
615
|
+
onPointerDown,
|
|
616
|
+
onPointerMove,
|
|
617
|
+
onPointerUp,
|
|
618
|
+
onStrokeClick,
|
|
619
|
+
onArcClick
|
|
620
|
+
} = useNumberLineContext();
|
|
621
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
622
|
+
"svg",
|
|
623
|
+
{
|
|
624
|
+
ref: svgRef,
|
|
625
|
+
tabIndex: 0,
|
|
626
|
+
className: className ? `nl-svg ${className}` : "nl-svg",
|
|
627
|
+
viewBox: `0 0 ${DESIGN_WIDTH} ${DESIGN_HEIGHT}`,
|
|
628
|
+
preserveAspectRatio: "xMidYMid meet",
|
|
629
|
+
style: { width: "100%", height: "100%", display: "block", outline: "none", cursor: svgCursor, touchAction: "none" },
|
|
630
|
+
onPointerDown,
|
|
631
|
+
onPointerMove,
|
|
632
|
+
onPointerUp,
|
|
633
|
+
children: [
|
|
634
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
635
|
+
AxisLayer,
|
|
636
|
+
{
|
|
637
|
+
min,
|
|
638
|
+
max,
|
|
639
|
+
axisY: AXIS_Y,
|
|
640
|
+
padLeft: PAD_LEFT,
|
|
641
|
+
padRight: PAD_RIGHT,
|
|
642
|
+
viewWidth: DESIGN_WIDTH,
|
|
643
|
+
containerWidth,
|
|
644
|
+
groupSize,
|
|
645
|
+
tickStep
|
|
646
|
+
}
|
|
647
|
+
),
|
|
648
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
649
|
+
GroupArcsLayer,
|
|
650
|
+
{
|
|
651
|
+
min,
|
|
652
|
+
max,
|
|
653
|
+
groupSize,
|
|
654
|
+
groupCount,
|
|
655
|
+
axisY: AXIS_Y,
|
|
656
|
+
padLeft: PAD_LEFT,
|
|
657
|
+
padRight: PAD_RIGHT,
|
|
658
|
+
viewWidth: DESIGN_WIDTH,
|
|
659
|
+
arcMaxHeight: ARC_MAX_HEIGHT,
|
|
660
|
+
tool,
|
|
661
|
+
selectedArcIndices,
|
|
662
|
+
deletedArcIndices,
|
|
663
|
+
onArcClick,
|
|
664
|
+
arcColors
|
|
665
|
+
}
|
|
666
|
+
),
|
|
667
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
668
|
+
DrawingLayer,
|
|
669
|
+
{
|
|
670
|
+
strokes,
|
|
671
|
+
liveStroke,
|
|
672
|
+
tool,
|
|
673
|
+
onStrokeClick
|
|
674
|
+
}
|
|
675
|
+
)
|
|
676
|
+
]
|
|
677
|
+
}
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
exports.NumberLine = NumberLine;
|
|
681
|
+
exports.NumberLineProvider = NumberLineProvider;
|
|
682
|
+
exports.useNumberLineContext = useNumberLineContext;
|