@classytic/stage 0.2.0 → 0.3.0

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.
Files changed (41) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +5 -1
  3. package/dist/assets/index.mjs +0 -1
  4. package/dist/builder/Palette.mjs +50 -89
  5. package/dist/builder/SceneBuilder.mjs +15 -73
  6. package/dist/core/index.d.mts +2 -1
  7. package/dist/core/index.mjs +2 -1
  8. package/dist/core/math.d.mts +26 -0
  9. package/dist/core/math.mjs +37 -0
  10. package/dist/finance/bizsim.d.mts +93 -0
  11. package/dist/finance/bizsim.mjs +117 -0
  12. package/dist/finance/index.d.mts +118 -0
  13. package/dist/finance/index.mjs +203 -0
  14. package/dist/index.d.mts +4 -3
  15. package/dist/index.mjs +4 -4
  16. package/dist/interaction/MovableDot.mjs +19 -0
  17. package/dist/interaction/useDraggable.mjs +24 -4
  18. package/dist/math/calculus.d.mts +22 -1
  19. package/dist/math/calculus.mjs +156 -2
  20. package/dist/math/index.d.mts +2 -2
  21. package/dist/math/index.mjs +2 -2
  22. package/dist/math/latex.mjs +8 -0
  23. package/dist/primitives/Dot.d.mts +2 -15
  24. package/dist/primitives/Dot.mjs +6 -4
  25. package/dist/primitives/Grid.d.mts +33 -17
  26. package/dist/primitives/Grid.mjs +89 -17
  27. package/dist/primitives/Label.d.mts +1 -14
  28. package/dist/primitives/Label.mjs +3 -2
  29. package/dist/primitives/Lines.d.mts +4 -32
  30. package/dist/primitives/Lines.mjs +10 -8
  31. package/dist/primitives/Shapes.d.mts +5 -43
  32. package/dist/primitives/Shapes.mjs +12 -10
  33. package/dist/primitives/index.d.mts +2 -2
  34. package/dist/primitives/index.mjs +2 -2
  35. package/dist/primitives/props.mjs +31 -0
  36. package/dist/scene/Scene.d.mts +6 -1
  37. package/dist/scene/Scene.mjs +15 -40
  38. package/dist/view/Stage.mjs +4 -11
  39. package/package.json +30 -22
  40. package/styles.css +125 -8
  41. package/dist/assets/kit/index.mjs +0 -4
@@ -1,5 +1,5 @@
1
1
  import { FN1, FN2 } from "./defs.mjs";
2
- import { add, call, div, mul, neg, num, pow, sub } from "./ast.mjs";
2
+ import { add, call, div, evaluate, mul, neg, num, pow, sub, variable } from "./ast.mjs";
3
3
 
4
4
  //#region src/math/calculus.ts
5
5
  /**
@@ -70,6 +70,134 @@ function differentiate(node, x) {
70
70
  default: return null;
71
71
  }
72
72
  }
73
+ /**
74
+ * Outer antiderivative of a 1-arg function, given its argument node `u`.
75
+ *
76
+ * Mirrors DERIV above. Only the functions whose antiderivative is itself elementary appear:
77
+ * `ln` and the inverse trig functions integrate by parts into forms this table cannot express,
78
+ * so they are absent and `integrate` returns null for them rather than guessing.
79
+ */
80
+ const INTEG = {
81
+ sin: (u) => neg(call("cos", u)),
82
+ cos: (u) => call("sin", u),
83
+ exp: (u) => call("exp", u),
84
+ sinh: (u) => call("cosh", u),
85
+ cosh: (u) => call("sinh", u)
86
+ };
87
+ /** `true` when `node` holds no occurrence of `x`, so it is a constant for this integral. */
88
+ function isConstantIn(node, x) {
89
+ return !freeVarsHas(node, x);
90
+ }
91
+ function freeVarsHas(node, x) {
92
+ switch (node.type) {
93
+ case "num": return false;
94
+ case "var": return node.name === x;
95
+ case "neg": return freeVarsHas(node.arg, x);
96
+ case "binary": return freeVarsHas(node.left, x) || freeVarsHas(node.right, x);
97
+ case "call": return node.args.some((a) => freeVarsHas(a, x));
98
+ default: return true;
99
+ }
100
+ }
101
+ /**
102
+ * The constant `a` when `u` is linear in `x` (that is, `u = a·x + b`), else null.
103
+ *
104
+ * This is what makes the whole linear-substitution family work with one rule instead of a pattern
105
+ * per case: if `du/dx` simplifies to a non-zero NUMBER, then `∫f(u) dx = F(u)/a`. Deriving it from
106
+ * `differentiate` rather than matching `a*x+b` by shape means `(3-2x)^5` and `(-2x+3)^5` and
107
+ * `3-2*x` all work without three separate matchers.
108
+ */
109
+ function linearFactor(u, x) {
110
+ const d = differentiate(u, x);
111
+ if (!d) return null;
112
+ const s = simplify(d);
113
+ return s.type === "num" && s.value !== 0 ? s.value : null;
114
+ }
115
+ /** Divide by a constant, without emitting the noise of `expr / 1`. */
116
+ const overConst = (node, a) => a === 1 ? node : div(node, num(a));
117
+ /**
118
+ * Exact symbolic antiderivative with respect to `x`, or `null` when this engine cannot do it.
119
+ *
120
+ * The constant of integration is NOT included. That is deliberate: `+ c` is the single most
121
+ * commonly dropped mark in the topic, so it belongs in the lesson's own working where a learner
122
+ * has to write it, not silently bolted on by the engine.
123
+ *
124
+ * Covers the Cambridge P1/P3 toolkit: linearity, the power rule including the `1/x → ln|x|`
125
+ * exception, and any of the above composed with a LINEAR inner function. Returns null for
126
+ * integration by parts, by non-linear substitution, and partial fractions, so a caller can fall
127
+ * back to the numerical `integrate` in `core/numeric` and show area rather than a wrong formula.
128
+ */
129
+ function integrate(node, x) {
130
+ if (isConstantIn(node, x)) return mul(node, variable(x));
131
+ switch (node.type) {
132
+ case "var": return div(pow(variable(x), num(2)), num(2));
133
+ case "neg": {
134
+ const inner = integrate(node.arg, x);
135
+ return inner && neg(inner);
136
+ }
137
+ case "binary": {
138
+ const { op, left, right } = node;
139
+ if (op === "+" || op === "-") {
140
+ const l = integrate(left, x);
141
+ const r = integrate(right, x);
142
+ return l && r && (op === "+" ? add(l, r) : sub(l, r));
143
+ }
144
+ if (op === "*") {
145
+ if (isConstantIn(left, x)) {
146
+ const r = integrate(right, x);
147
+ return r && mul(left, r);
148
+ }
149
+ if (isConstantIn(right, x)) {
150
+ const l = integrate(left, x);
151
+ return l && mul(right, l);
152
+ }
153
+ return null;
154
+ }
155
+ if (op === "/") {
156
+ if (isConstantIn(right, x)) {
157
+ const l = integrate(left, x);
158
+ return l && div(l, right);
159
+ }
160
+ if (isConstantIn(left, x)) {
161
+ const a = linearFactor(right, x);
162
+ return a === null ? null : overConst(mul(left, call("ln", call("abs", right))), a);
163
+ }
164
+ return null;
165
+ }
166
+ if (op === "^") {
167
+ const exponent = simplify(right);
168
+ if (exponent.type !== "num") return null;
169
+ const n = exponent.value;
170
+ const a = linearFactor(left, x);
171
+ if (a === null) return null;
172
+ if (n === -1) return overConst(call("ln", call("abs", left)), a);
173
+ return overConst(div(pow(left, num(n + 1)), num(n + 1)), a);
174
+ }
175
+ return null;
176
+ }
177
+ case "call": {
178
+ const u = node.args[0];
179
+ const rule = INTEG[node.fn];
180
+ if (!rule || !u || node.args.length !== 1) return null;
181
+ const a = linearFactor(u, x);
182
+ return a === null ? null : overConst(rule(u), a);
183
+ }
184
+ default: return null;
185
+ }
186
+ }
187
+ /**
188
+ * A definite integral evaluated exactly: `F(b) - F(a)`.
189
+ *
190
+ * Returned as a NUMBER rather than a node, because that is what a lesson compares a learner's
191
+ * answer against. Null when the antiderivative is not elementary, or when either limit lands
192
+ * somewhere the expression is not defined.
193
+ */
194
+ function definiteIntegral(node, x, lower, upper) {
195
+ const F = integrate(node, x);
196
+ if (!F) return null;
197
+ const at = (v) => evaluate(F, { [x]: v });
198
+ const result = at(upper) - at(lower);
199
+ return Number.isFinite(result) ? result : null;
200
+ }
73
201
  const isNum = (n, v) => n.type === "num" && (v === void 0 || n.value === v);
74
202
  /** A stable structural key, so identical cores group (x and x; x² and x²). */
75
203
  function nodeKey(n) {
@@ -192,10 +320,36 @@ function simplify(node) {
192
320
  right: l.left
193
321
  };
194
322
  }
323
+ /**
324
+ * Cancel a constant against a fraction's denominator: c·(a/b) → (c/b)·a.
325
+ *
326
+ * `∫3x² dx` is the plainest case. The constant comes out of the integral and the power
327
+ * rule divides by 3, so the honest answer is `3·(x³/3)`. A learner expects `x³`, and
328
+ * only the fully cancelled form is worth showing as the ANSWER.
329
+ */
330
+ if (l.type === "num" && r.type === "binary" && r.op === "/" && r.right.type === "num") {
331
+ const k = l.value / r.right.value;
332
+ if (Number.isInteger(k)) return k === 1 ? r.left : simplify(mul(num(k), r.left));
333
+ }
334
+ if (r.type === "num" && l.type === "binary" && l.op === "/" && l.right.type === "num") {
335
+ const k = r.value / l.right.value;
336
+ if (Number.isInteger(k)) return k === 1 ? l.left : simplify(mul(num(k), l.left));
337
+ }
195
338
  break;
196
339
  case "/":
197
340
  if (isNum(r, 1)) return l;
198
341
  if (isNum(l, 0)) return num(0);
342
+ /**
343
+ * Collapse a nested fraction: (a/b)/c → a/(bc).
344
+ *
345
+ * Integration produces these constantly, because the power rule divides by n+1 and the
346
+ * linear-substitution rule then divides by a. Left alone, ∫(2x+1)³dx renders as a
347
+ * fraction inside a fraction, which is correct and unreadable, and unreadable is the
348
+ * complaint this engine exists to answer.
349
+ */
350
+ if (r.type === "num" && l.type === "binary" && l.op === "/" && l.right.type === "num") return simplify(div(l.left, num(l.right.value * r.value)));
351
+ if (r.type === "num" && r.value < 0) return simplify(neg(div(l, num(-r.value))));
352
+ if (l.type === "neg") return neg(simplify(div(l.arg, r)));
199
353
  break;
200
354
  case "^":
201
355
  if (isNum(r, 1)) return l;
@@ -244,4 +398,4 @@ function fold(op, a, b) {
244
398
  }
245
399
 
246
400
  //#endregion
247
- export { differentiate, simplify };
401
+ export { definiteIntegral, differentiate, integrate, simplify };
@@ -1,6 +1,6 @@
1
1
  import { BinOp, Node, compileNode, evaluate, freeVars } from "./ast.mjs";
2
2
  import { parse } from "./parse.mjs";
3
- import { differentiate, simplify } from "./calculus.mjs";
3
+ import { definiteIntegral, differentiate, integrate, simplify } from "./calculus.mjs";
4
4
  import { CompiledFn, compile } from "./compile.mjs";
5
5
  import { toLatex } from "./latex.mjs";
6
6
 
@@ -24,4 +24,4 @@ type ExprResult = CompiledExpr | ExprError;
24
24
  /** Compile a formula string. Returns `{ fn, vars, ast }` or `{ error }`, never throws. */
25
25
  declare function compileExpr(src: string): ExprResult;
26
26
  //#endregion
27
- export { type BinOp, CompiledExpr, type CompiledFn, ExprError, ExprResult, type Node, compile, compileExpr, compileNode, differentiate, evaluate, freeVars, parse, simplify, toLatex };
27
+ export { type BinOp, CompiledExpr, type CompiledFn, ExprError, ExprResult, type Node, compile, compileExpr, compileNode, definiteIntegral, differentiate, evaluate, freeVars, integrate, parse, simplify, toLatex };
@@ -1,6 +1,6 @@
1
1
  import { compileNode, evaluate, freeVars } from "./ast.mjs";
2
2
  import { parse } from "./parse.mjs";
3
- import { differentiate, simplify } from "./calculus.mjs";
3
+ import { definiteIntegral, differentiate, integrate, simplify } from "./calculus.mjs";
4
4
  import { compile } from "./compile.mjs";
5
5
  import { toLatex } from "./latex.mjs";
6
6
 
@@ -40,4 +40,4 @@ function compileExpr(src) {
40
40
  }
41
41
 
42
42
  //#endregion
43
- export { compile, compileExpr, compileNode, differentiate, evaluate, freeVars, parse, simplify, toLatex };
43
+ export { compile, compileExpr, compileNode, definiteIntegral, differentiate, evaluate, freeVars, integrate, parse, simplify, toLatex };
@@ -103,6 +103,14 @@ function texCall(node) {
103
103
  if (node.fn === "cbrt") return `\\sqrt[3]{${wrap(a0, 0)}}`;
104
104
  if (node.fn === "abs") return `\\left|${wrap(a0, 0)}\\right|`;
105
105
  if (node.fn === "exp") return `e^{${wrap(a0, 0)}}`;
106
+ /**
107
+ * `ln|x|`, not `ln(|x|)`.
108
+ *
109
+ * The modulus bars already delimit the argument, so the usual parentheses double up. Every
110
+ * textbook and mark scheme writes the integral of 1/x as ln|x|, and that is the form a learner
111
+ * has to reproduce, so it is the form to show.
112
+ */
113
+ if ((node.fn === "ln" || node.fn === "log") && a0.type === "call" && a0.fn === "abs" && a0.args[0]) return `\\${node.fn}\\left|${wrap(a0.args[0], 0)}\\right|`;
106
114
  const args = node.args.map((a) => wrap(a, 0)).join(", ");
107
115
  return `${NAMED_FN.has(node.fn) ? `\\${node.fn}` : `\\operatorname{${node.fn}}`}\\left(${args}\\right)`;
108
116
  }
@@ -1,5 +1,4 @@
1
1
  import { StyleProps } from "./props.mjs";
2
- import { ReactNode } from "react";
3
2
 
4
3
  //#region src/primitives/Dot.d.ts
5
4
  interface DotProps extends StyleProps {
@@ -8,20 +7,8 @@ interface DotProps extends StyleProps {
8
7
  /** Pixel radius (never scaled). */
9
8
  r?: number;
10
9
  }
11
- declare function Dot({
12
- x,
13
- y,
14
- r,
15
- color,
16
- opacity
17
- }: DotProps): ReactNode;
10
+ declare const Dot: import("react").NamedExoticComponent<DotProps>;
18
11
  /** A point with a contrast ring (reads clearly over any background). */
19
- declare function Point({
20
- x,
21
- y,
22
- r,
23
- color,
24
- opacity
25
- }: DotProps): ReactNode;
12
+ declare const Point: import("react").NamedExoticComponent<DotProps>;
26
13
  //#endregion
27
14
  export { Dot, DotProps, Point };
@@ -2,10 +2,12 @@
2
2
 
3
3
  import { fmt } from "../core/coords.mjs";
4
4
  import { useCoords } from "../core/context.mjs";
5
+ import { geomPropsEqual } from "./props.mjs";
6
+ import { memo } from "react";
5
7
  import { jsx, jsxs } from "react/jsx-runtime";
6
8
 
7
9
  //#region src/primitives/Dot.tsx
8
- function Dot({ x, y, r = 5, color = "var(--stage-accent)", opacity = 1 }) {
10
+ const Dot = memo(function Dot({ x, y, r = 5, color = "var(--stage-accent)", opacity = 1 }) {
9
11
  const [px, py] = useCoords().toPx(x, y);
10
12
  return /* @__PURE__ */ jsx("circle", {
11
13
  cx: fmt(px),
@@ -14,9 +16,9 @@ function Dot({ x, y, r = 5, color = "var(--stage-accent)", opacity = 1 }) {
14
16
  fill: color,
15
17
  opacity
16
18
  });
17
- }
19
+ }, geomPropsEqual);
18
20
  /** A point with a contrast ring (reads clearly over any background). */
19
- function Point({ x, y, r = 6, color = "var(--stage-accent)", opacity = 1 }) {
21
+ const Point = memo(function Point({ x, y, r = 6, color = "var(--stage-accent)", opacity = 1 }) {
20
22
  const [px, py] = useCoords().toPx(x, y);
21
23
  return /* @__PURE__ */ jsxs("g", {
22
24
  opacity,
@@ -34,7 +36,7 @@ function Point({ x, y, r = 6, color = "var(--stage-accent)", opacity = 1 }) {
34
36
  strokeWidth: 1.5
35
37
  })]
36
38
  });
37
- }
39
+ }, geomPropsEqual);
38
40
 
39
41
  //#endregion
40
42
  export { Dot, Point };
@@ -1,5 +1,3 @@
1
- import { ReactNode } from "react";
2
-
3
1
  //#region src/primitives/Grid.d.ts
4
2
  /** A "nice" step (1,2,5 × 10ⁿ) so ~`target` gridlines span the range. */
5
3
  declare function niceStep(span: number, target?: number): number;
@@ -11,12 +9,35 @@ interface GridProps {
11
9
  stepY?: number;
12
10
  color?: string;
13
11
  }
14
- declare function Grid({
15
- step,
16
- stepX,
17
- stepY,
18
- color
19
- }: GridProps): ReactNode;
12
+ declare const Grid: import("react").NamedExoticComponent<GridProps>;
13
+ /**
14
+ * A spot the axis numbers must leave clear: a point in math coordinates, widened by a half-size and
15
+ * shifted by an offset, both in px. A dot drawn on an axis sits exactly where that axis prints its
16
+ * numbers (an intercept on the y-axis lands on the y tick labels, a focus on the x-axis on the x
17
+ * ones), so without this the number is half-covered by the dot and both read badly.
18
+ */
19
+ interface ClearZone {
20
+ x: number;
21
+ y: number;
22
+ /** Half-width in px (default 10, a dot plus a margin). */
23
+ rx?: number;
24
+ /** Half-height in px (default 10). */
25
+ ry?: number;
26
+ dx?: number;
27
+ dy?: number;
28
+ }
29
+ /** The zone a `<Label>` occupies, from the same props, so a figure can keep the axis numbers off it. */
30
+ declare function clearOfLabel(x: number, y: number, text: string, {
31
+ size,
32
+ anchor,
33
+ dx,
34
+ dy
35
+ }?: {
36
+ size?: number;
37
+ anchor?: 'start' | 'middle' | 'end';
38
+ dx?: number;
39
+ dy?: number;
40
+ }): ClearZone;
20
41
  interface AxesProps {
21
42
  color?: string;
22
43
  ticks?: boolean;
@@ -28,14 +49,9 @@ interface AxesProps {
28
49
  * can READ coordinates off the grid, needed for graphs where the answer is a
29
50
  * point (systems, plotting). Default off (most figures want a clean axis). */
30
51
  labels?: boolean;
52
+ /** Places the numbers must leave clear, such as the dots and labels the figure draws on an axis. */
53
+ keepClear?: readonly ClearZone[];
31
54
  }
32
- declare function Axes({
33
- color,
34
- ticks,
35
- step,
36
- stepX,
37
- stepY,
38
- labels
39
- }: AxesProps): ReactNode;
55
+ declare const Axes: import("react").NamedExoticComponent<AxesProps>;
40
56
  //#endregion
41
- export { Axes, AxesProps, Grid, GridProps, niceStep };
57
+ export { Axes, AxesProps, ClearZone, Grid, GridProps, clearOfLabel, niceStep };
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useCoords } from "../core/context.mjs";
4
+ import { memo } from "react";
4
5
  import { jsx } from "react/jsx-runtime";
5
6
 
6
7
  //#region src/primitives/Grid.tsx
@@ -17,7 +18,7 @@ function tickValues(min, max, step) {
17
18
  for (let v = start; v <= max + step * 1e-6; v += step) out.push(Math.round(v / step) * step);
18
19
  return out;
19
20
  }
20
- function Grid({ step, stepX, stepY, color = "var(--stage-grid)" }) {
21
+ const Grid = memo(function Grid({ step, stepX, stepY, color = "var(--stage-grid)" }) {
21
22
  const c = useCoords();
22
23
  const sx = stepX ?? step ?? niceStep(c.view.xMax - c.view.xMin);
23
24
  const sy = stepY ?? step ?? niceStep(c.view.yMax - c.view.yMin);
@@ -47,20 +48,49 @@ function Grid({ step, stepX, stepY, color = "var(--stage-grid)" }) {
47
48
  }, `gy${y}`));
48
49
  }
49
50
  return /* @__PURE__ */ jsx("g", { children: lines });
51
+ });
52
+ /** Roughly how wide a label renders, in px: digits and letters average a little over half an em. */
53
+ const textWidth = (text, size) => text.length * size * .58;
54
+ /** The zone a `<Label>` occupies, from the same props, so a figure can keep the axis numbers off it. */
55
+ function clearOfLabel(x, y, text, { size = 14, anchor = "middle", dx = 0, dy = 0 } = {}) {
56
+ const w = textWidth(text, size);
57
+ return {
58
+ x,
59
+ y,
60
+ dx: dx + (anchor === "start" ? w / 2 : anchor === "end" ? -w / 2 : 0),
61
+ dy,
62
+ rx: w / 2 + 3,
63
+ ry: size * .5 + 2
64
+ };
50
65
  }
51
66
  const fmtTick = (v) => {
52
67
  if (Number.isInteger(v)) return String(v);
53
68
  const r = Math.round(v * 100) / 100;
54
69
  return String(r);
55
70
  };
56
- function Axes({ color = "var(--stage-axis)", ticks = true, step, stepX, stepY, labels = false }) {
71
+ const hits = (a, b) => a.l < b.r && b.l < a.r && a.t < b.b && b.t < a.b;
72
+ const Axes = memo(function Axes({ color = "var(--stage-axis)", ticks = true, step, stepX, stepY, labels = false, keepClear }) {
57
73
  const c = useCoords();
58
74
  const sx = stepX ?? step ?? niceStep(c.view.xMax - c.view.xMin);
59
75
  const sy = stepY ?? step ?? niceStep(c.view.yMax - c.view.yMin);
60
- const xAxisA = c.toPx(c.view.xMin, 0);
61
- const xAxisB = c.toPx(c.view.xMax, 0);
62
- const yAxisA = c.toPx(0, c.view.yMin);
63
- const yAxisB = c.toPx(0, c.view.yMax);
76
+ /**
77
+ * Where the axes sit when the origin is off-screen.
78
+ *
79
+ * Drawing them at coordinate 0 unconditionally means that a view which does not contain 0 puts
80
+ * the axis line, every tick and every number outside the canvas, so the reader loses the scale
81
+ * entirely and cannot tell what they are looking at. Pin each axis to the nearest edge instead,
82
+ * which is what a plotting library does: the ticks and numbers stay readable, and the axis is
83
+ * still in the right place whenever the origin IS in view.
84
+ */
85
+ const axisY = Math.min(c.view.yMax, Math.max(c.view.yMin, 0));
86
+ const axisX = Math.min(c.view.xMax, Math.max(c.view.xMin, 0));
87
+ const originInView = axisX === 0 && axisY === 0;
88
+ const yLabelsRight = axisX <= c.view.xMin;
89
+ const xLabelsAbove = axisY <= c.view.yMin;
90
+ const xAxisA = c.toPx(c.view.xMin, axisY);
91
+ const xAxisB = c.toPx(c.view.xMax, axisY);
92
+ const yAxisA = c.toPx(axisX, c.view.yMin);
93
+ const yAxisB = c.toPx(axisX, c.view.yMax);
64
94
  const labelStyle = {
65
95
  paintOrder: "stroke",
66
96
  stroke: "var(--stage-bg)",
@@ -93,9 +123,52 @@ function Axes({ color = "var(--stage-axis)", ticks = true, step, stepX, stepY, l
93
123
  strokeWidth: 1.5
94
124
  }, "ay")];
95
125
  if (ticks) {
96
- for (const x of tickValues(c.view.xMin, c.view.xMax, sx)) {
97
- if (Math.abs(x) < 1e-9) continue;
98
- const p = c.toPx(x, 0);
126
+ const xs = tickValues(c.view.xMin, c.view.xMax, sx).filter((x) => !(originInView && Math.abs(x) < 1e-9));
127
+ const ys = tickValues(c.view.yMin, c.view.yMax, sy).filter((y) => !(originInView && Math.abs(y) < 1e-9));
128
+ const origin = c.toPx(0, 0);
129
+ const xLabelAt = (x) => {
130
+ const p = c.toPx(x, axisY);
131
+ return [p[0], p[1] + (xLabelsAbove ? -12 : 14)];
132
+ };
133
+ const yLabelAt = (y) => {
134
+ const p = c.toPx(axisX, y);
135
+ return [p[0] + (yLabelsRight ? 8 : -8), p[1]];
136
+ };
137
+ const yAnchor = yLabelsRight ? "start" : "end";
138
+ const shown = /* @__PURE__ */ new Set();
139
+ if (labels) {
140
+ const taken = (keepClear ?? []).map((z) => {
141
+ const [zx, zy] = c.toPx(z.x, z.y);
142
+ const px = zx + (z.dx ?? 0);
143
+ const py = zy + (z.dy ?? 0);
144
+ const rx = z.rx ?? 10;
145
+ const ry = z.ry ?? 10;
146
+ return {
147
+ l: px - rx,
148
+ r: px + rx,
149
+ t: py - ry,
150
+ b: py + ry
151
+ };
152
+ });
153
+ const place = (key, [px, py], text, anchor) => {
154
+ const w = textWidth(text, 11);
155
+ const l = anchor === "middle" ? px - w / 2 : anchor === "end" ? px - w : px;
156
+ const box = {
157
+ l,
158
+ r: l + w,
159
+ t: py - 5,
160
+ b: py + 5
161
+ };
162
+ if (taken.some((t) => hits(box, t))) return;
163
+ taken.push(box);
164
+ shown.add(key);
165
+ };
166
+ if (originInView) place("l0", [origin[0] - 8, origin[1] + 12], "0", "end");
167
+ for (const x of xs) place(`lx${x}`, xLabelAt(x), fmtTick(x), "middle");
168
+ for (const y of ys) place(`ly${y}`, yLabelAt(y), fmtTick(y), yAnchor);
169
+ }
170
+ for (const x of xs) {
171
+ const p = c.toPx(x, axisY);
99
172
  nodes.push(/* @__PURE__ */ jsx("line", {
100
173
  x1: p[0],
101
174
  y1: p[1] - 4,
@@ -104,11 +177,10 @@ function Axes({ color = "var(--stage-axis)", ticks = true, step, stepX, stepY, l
104
177
  stroke: color,
105
178
  strokeWidth: 1.5
106
179
  }, `tx${x}`));
107
- if (labels) nodes.push(num(`lx${x}`, p[0], p[1] + 14, fmtTick(x), "middle"));
180
+ if (shown.has(`lx${x}`)) nodes.push(num(`lx${x}`, ...xLabelAt(x), fmtTick(x), "middle"));
108
181
  }
109
- for (const y of tickValues(c.view.yMin, c.view.yMax, sy)) {
110
- if (Math.abs(y) < 1e-9) continue;
111
- const p = c.toPx(0, y);
182
+ for (const y of ys) {
183
+ const p = c.toPx(axisX, y);
112
184
  nodes.push(/* @__PURE__ */ jsx("line", {
113
185
  x1: p[0] - 4,
114
186
  y1: p[1],
@@ -117,12 +189,12 @@ function Axes({ color = "var(--stage-axis)", ticks = true, step, stepX, stepY, l
117
189
  stroke: color,
118
190
  strokeWidth: 1.5
119
191
  }, `ty${y}`));
120
- if (labels) nodes.push(num(`ly${y}`, p[0] - 8, p[1], fmtTick(y), "end"));
192
+ if (shown.has(`ly${y}`)) nodes.push(num(`ly${y}`, ...yLabelAt(y), fmtTick(y), yAnchor));
121
193
  }
122
- if (labels) nodes.push(num("l0", c.toPx(0, 0)[0] - 8, c.toPx(0, 0)[1] + 12, "0", "end"));
194
+ if (shown.has("l0")) nodes.push(num("l0", origin[0] - 8, origin[1] + 12, "0", "end"));
123
195
  }
124
196
  return /* @__PURE__ */ jsx("g", { children: nodes });
125
- }
197
+ });
126
198
 
127
199
  //#endregion
128
- export { Axes, Grid, niceStep };
200
+ export { Axes, Grid, clearOfLabel, niceStep };
@@ -1,5 +1,3 @@
1
- import { ReactNode } from "react";
2
-
3
1
  //#region src/primitives/Label.d.ts
4
2
  interface LabelProps {
5
3
  x: number;
@@ -17,17 +15,6 @@ interface LabelProps {
17
15
  /** Upright pixel-space text with a background-colored outline for legibility.
18
16
  * Renders `_`/`^` as real SVG sub/superscripts via the shared `parseRichText`
19
17
  * grammar (the same one labs' HTML `<RichText>` uses, one source of truth). */
20
- declare function Label({
21
- x,
22
- y,
23
- text,
24
- color,
25
- size,
26
- dx,
27
- dy,
28
- anchor,
29
- baseline,
30
- weight
31
- }: LabelProps): ReactNode;
18
+ declare const Label: import("react").NamedExoticComponent<LabelProps>;
32
19
  //#endregion
33
20
  export { Label, LabelProps };
@@ -3,13 +3,14 @@
3
3
  import { fmt } from "../core/coords.mjs";
4
4
  import { parseRichText } from "../core/richText.mjs";
5
5
  import { useCoords } from "../core/context.mjs";
6
+ import { memo } from "react";
6
7
  import { jsx } from "react/jsx-runtime";
7
8
 
8
9
  //#region src/primitives/Label.tsx
9
10
  /** Upright pixel-space text with a background-colored outline for legibility.
10
11
  * Renders `_`/`^` as real SVG sub/superscripts via the shared `parseRichText`
11
12
  * grammar (the same one labs' HTML `<RichText>` uses, one source of truth). */
12
- function Label({ x, y, text, color = "var(--stage-fg)", size = 14, dx = 0, dy = 0, anchor = "middle", baseline = "middle", weight = 600 }) {
13
+ const Label = memo(function Label({ x, y, text, color = "var(--stage-fg)", size = 14, dx = 0, dy = 0, anchor = "middle", baseline = "middle", weight = 600 }) {
13
14
  const [px, py] = useCoords().toPx(x, y);
14
15
  const spans = parseRichText(text);
15
16
  const shift = size * .32;
@@ -39,7 +40,7 @@ function Label({ x, y, text, color = "var(--stage-fg)", size = 14, dx = 0, dy =
39
40
  }, i);
40
41
  })
41
42
  });
42
- }
43
+ });
43
44
 
44
45
  //#endregion
45
46
  export { Label };
@@ -1,50 +1,22 @@
1
1
  import { Vec2 } from "../core/vec.mjs";
2
2
  import { StyleProps } from "./props.mjs";
3
- import { ReactNode } from "react";
4
3
 
5
4
  //#region src/primitives/Lines.d.ts
6
5
  interface SegmentProps extends StyleProps {
7
6
  from: Vec2;
8
7
  to: Vec2;
9
8
  }
10
- declare function Segment({
11
- from,
12
- to,
13
- color,
14
- weight,
15
- opacity,
16
- dashed
17
- }: SegmentProps): ReactNode;
9
+ declare const Segment: import("react").NamedExoticComponent<SegmentProps>;
18
10
  interface LineProps extends StyleProps {
19
11
  from: Vec2;
20
12
  to: Vec2;
21
13
  }
22
- declare function Line({
23
- from,
24
- to,
25
- color,
26
- weight,
27
- opacity,
28
- dashed
29
- }: LineProps): ReactNode;
30
- declare function Ray({
31
- from,
32
- to,
33
- color,
34
- weight,
35
- opacity,
36
- dashed
37
- }: LineProps): ReactNode;
14
+ declare const Line: import("react").NamedExoticComponent<LineProps>;
15
+ declare const Ray: import("react").NamedExoticComponent<LineProps>;
38
16
  interface VectorProps extends StyleProps {
39
17
  tail?: Vec2;
40
18
  tip: Vec2;
41
19
  }
42
- declare function Vector({
43
- tail,
44
- tip,
45
- color,
46
- weight,
47
- opacity
48
- }: VectorProps): ReactNode;
20
+ declare const Vector: import("react").NamedExoticComponent<VectorProps>;
49
21
  //#endregion
50
22
  export { Line, LineProps, Ray, Segment, SegmentProps, Vector, VectorProps };