@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.
- package/LICENSE +1 -1
- package/README.md +5 -1
- package/dist/assets/index.mjs +0 -1
- package/dist/builder/Palette.mjs +50 -89
- package/dist/builder/SceneBuilder.mjs +15 -73
- package/dist/core/index.d.mts +2 -1
- package/dist/core/index.mjs +2 -1
- package/dist/core/math.d.mts +26 -0
- package/dist/core/math.mjs +37 -0
- package/dist/finance/bizsim.d.mts +93 -0
- package/dist/finance/bizsim.mjs +117 -0
- package/dist/finance/index.d.mts +118 -0
- package/dist/finance/index.mjs +203 -0
- package/dist/index.d.mts +4 -3
- package/dist/index.mjs +4 -4
- package/dist/interaction/MovableDot.mjs +19 -0
- package/dist/interaction/useDraggable.mjs +24 -4
- package/dist/math/calculus.d.mts +22 -1
- package/dist/math/calculus.mjs +156 -2
- package/dist/math/index.d.mts +2 -2
- package/dist/math/index.mjs +2 -2
- package/dist/math/latex.mjs +8 -0
- package/dist/primitives/Dot.d.mts +2 -15
- package/dist/primitives/Dot.mjs +6 -4
- package/dist/primitives/Grid.d.mts +33 -17
- package/dist/primitives/Grid.mjs +89 -17
- package/dist/primitives/Label.d.mts +1 -14
- package/dist/primitives/Label.mjs +3 -2
- package/dist/primitives/Lines.d.mts +4 -32
- package/dist/primitives/Lines.mjs +10 -8
- package/dist/primitives/Shapes.d.mts +5 -43
- package/dist/primitives/Shapes.mjs +12 -10
- package/dist/primitives/index.d.mts +2 -2
- package/dist/primitives/index.mjs +2 -2
- package/dist/primitives/props.mjs +31 -0
- package/dist/scene/Scene.d.mts +6 -1
- package/dist/scene/Scene.mjs +15 -40
- package/dist/view/Stage.mjs +4 -11
- package/package.json +30 -22
- package/styles.css +125 -8
- package/dist/assets/kit/index.mjs +0 -4
package/dist/math/calculus.mjs
CHANGED
|
@@ -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 };
|
package/dist/math/index.d.mts
CHANGED
|
@@ -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 };
|
package/dist/math/index.mjs
CHANGED
|
@@ -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 };
|
package/dist/math/latex.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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 };
|
package/dist/primitives/Dot.mjs
CHANGED
|
@@ -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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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 };
|
package/dist/primitives/Grid.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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 (
|
|
180
|
+
if (shown.has(`lx${x}`)) nodes.push(num(`lx${x}`, ...xLabelAt(x), fmtTick(x), "middle"));
|
|
108
181
|
}
|
|
109
|
-
for (const y of
|
|
110
|
-
|
|
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 (
|
|
192
|
+
if (shown.has(`ly${y}`)) nodes.push(num(`ly${y}`, ...yLabelAt(y), fmtTick(y), yAnchor));
|
|
121
193
|
}
|
|
122
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
23
|
-
|
|
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
|
|
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 };
|