@classytic/stage 0.1.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 (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -0
  3. package/dist/assets/index.d.mts +3 -0
  4. package/dist/assets/index.mjs +5 -0
  5. package/dist/assets/kit/glyphs.d.mts +61 -0
  6. package/dist/assets/kit/glyphs.mjs +256 -0
  7. package/dist/assets/kit/index.d.mts +2 -0
  8. package/dist/assets/kit/index.mjs +4 -0
  9. package/dist/assets/kit/svg-defs.d.mts +6 -0
  10. package/dist/assets/kit/svg-defs.mjs +137 -0
  11. package/dist/builder/Palette.d.mts +30 -0
  12. package/dist/builder/Palette.mjs +100 -0
  13. package/dist/builder/SceneBuilder.d.mts +26 -0
  14. package/dist/builder/SceneBuilder.mjs +340 -0
  15. package/dist/builder/controls.d.mts +18 -0
  16. package/dist/builder/controls.mjs +29 -0
  17. package/dist/builder/editor.d.mts +46 -0
  18. package/dist/builder/editor.mjs +130 -0
  19. package/dist/builder/icons.d.mts +14 -0
  20. package/dist/builder/icons.mjs +143 -0
  21. package/dist/builder/index.d.mts +6 -0
  22. package/dist/builder/index.mjs +7 -0
  23. package/dist/builder/tools.d.mts +47 -0
  24. package/dist/builder/tools.mjs +303 -0
  25. package/dist/chem/index.d.mts +118 -0
  26. package/dist/chem/index.mjs +131 -0
  27. package/dist/core/clock.d.mts +41 -0
  28. package/dist/core/clock.mjs +62 -0
  29. package/dist/core/context.d.mts +13 -0
  30. package/dist/core/context.mjs +29 -0
  31. package/dist/core/control.d.mts +58 -0
  32. package/dist/core/control.mjs +113 -0
  33. package/dist/core/coords.d.mts +51 -0
  34. package/dist/core/coords.mjs +44 -0
  35. package/dist/core/index.d.mts +8 -0
  36. package/dist/core/index.mjs +9 -0
  37. package/dist/core/learner.d.mts +44 -0
  38. package/dist/core/learner.mjs +23 -0
  39. package/dist/core/motion.d.mts +73 -0
  40. package/dist/core/motion.mjs +103 -0
  41. package/dist/core/richText.d.mts +28 -0
  42. package/dist/core/richText.mjs +52 -0
  43. package/dist/core/vec.d.mts +47 -0
  44. package/dist/core/vec.mjs +144 -0
  45. package/dist/field/index.d.mts +58 -0
  46. package/dist/field/index.mjs +156 -0
  47. package/dist/index.d.mts +54 -0
  48. package/dist/index.mjs +55 -0
  49. package/dist/interaction/MovableDot.d.mts +33 -0
  50. package/dist/interaction/MovableDot.mjs +66 -0
  51. package/dist/interaction/hitTest.d.mts +16 -0
  52. package/dist/interaction/hitTest.mjs +39 -0
  53. package/dist/interaction/index.d.mts +5 -0
  54. package/dist/interaction/index.mjs +6 -0
  55. package/dist/interaction/useDraggable.d.mts +51 -0
  56. package/dist/interaction/useDraggable.mjs +116 -0
  57. package/dist/interaction/useScreenToMath.d.mts +12 -0
  58. package/dist/interaction/useScreenToMath.mjs +25 -0
  59. package/dist/logic/ast.d.mts +29 -0
  60. package/dist/logic/ast.mjs +37 -0
  61. package/dist/logic/index.d.mts +25 -0
  62. package/dist/logic/index.mjs +36 -0
  63. package/dist/logic/latex.d.mts +6 -0
  64. package/dist/logic/latex.mjs +33 -0
  65. package/dist/logic/minimize.d.mts +37 -0
  66. package/dist/logic/minimize.mjs +175 -0
  67. package/dist/logic/parse.d.mts +6 -0
  68. package/dist/logic/parse.mjs +68 -0
  69. package/dist/logic/table.d.mts +24 -0
  70. package/dist/logic/table.mjs +65 -0
  71. package/dist/logic/tokenize.mjs +258 -0
  72. package/dist/math/ast.d.mts +35 -0
  73. package/dist/math/ast.mjs +94 -0
  74. package/dist/math/calculus.d.mts +9 -0
  75. package/dist/math/calculus.mjs +188 -0
  76. package/dist/math/defs.mjs +51 -0
  77. package/dist/math/index.d.mts +26 -0
  78. package/dist/math/index.mjs +42 -0
  79. package/dist/math/latex.d.mts +7 -0
  80. package/dist/math/latex.mjs +115 -0
  81. package/dist/math/parse.d.mts +6 -0
  82. package/dist/math/parse.mjs +91 -0
  83. package/dist/math/tokenize.mjs +112 -0
  84. package/dist/primitives/CanvasLayer.d.mts +26 -0
  85. package/dist/primitives/CanvasLayer.mjs +87 -0
  86. package/dist/primitives/Dot.d.mts +27 -0
  87. package/dist/primitives/Dot.mjs +40 -0
  88. package/dist/primitives/Grid.d.mts +30 -0
  89. package/dist/primitives/Grid.mjs +122 -0
  90. package/dist/primitives/Label.d.mts +33 -0
  91. package/dist/primitives/Label.mjs +45 -0
  92. package/dist/primitives/Lines.d.mts +50 -0
  93. package/dist/primitives/Lines.mjs +82 -0
  94. package/dist/primitives/Plot.d.mts +50 -0
  95. package/dist/primitives/Plot.mjs +87 -0
  96. package/dist/primitives/Shapes.d.mts +76 -0
  97. package/dist/primitives/Shapes.mjs +79 -0
  98. package/dist/primitives/Tex.d.mts +27 -0
  99. package/dist/primitives/Tex.mjs +54 -0
  100. package/dist/primitives/index.d.mts +10 -0
  101. package/dist/primitives/index.mjs +10 -0
  102. package/dist/primitives/props.d.mts +15 -0
  103. package/dist/scene/Scene.d.mts +31 -0
  104. package/dist/scene/Scene.mjs +168 -0
  105. package/dist/scene/assets.d.mts +37 -0
  106. package/dist/scene/assets.mjs +31 -0
  107. package/dist/scene/commands.d.mts +47 -0
  108. package/dist/scene/commands.mjs +162 -0
  109. package/dist/scene/evaluators.d.mts +17 -0
  110. package/dist/scene/evaluators.mjs +210 -0
  111. package/dist/scene/index.d.mts +13 -0
  112. package/dist/scene/index.mjs +13 -0
  113. package/dist/scene/migrate.d.mts +11 -0
  114. package/dist/scene/migrate.mjs +35 -0
  115. package/dist/scene/render.d.mts +14 -0
  116. package/dist/scene/render.mjs +126 -0
  117. package/dist/scene/resolve.d.mts +12 -0
  118. package/dist/scene/resolve.mjs +95 -0
  119. package/dist/scene/schema.d.mts +9 -0
  120. package/dist/scene/schema.mjs +51 -0
  121. package/dist/scene/sims.d.mts +18 -0
  122. package/dist/scene/sims.mjs +54 -0
  123. package/dist/scene/store.d.mts +22 -0
  124. package/dist/scene/store.mjs +37 -0
  125. package/dist/scene/types.d.mts +228 -0
  126. package/dist/scene/types.mjs +27 -0
  127. package/dist/sim/equilibrium.d.mts +36 -0
  128. package/dist/sim/equilibrium.mjs +54 -0
  129. package/dist/sim/index.d.mts +9 -0
  130. package/dist/sim/index.mjs +9 -0
  131. package/dist/sim/particles.d.mts +43 -0
  132. package/dist/sim/particles.mjs +136 -0
  133. package/dist/sim/rate.d.mts +36 -0
  134. package/dist/sim/rate.mjs +39 -0
  135. package/dist/sim/registry.d.mts +10 -0
  136. package/dist/sim/registry.mjs +27 -0
  137. package/dist/sim/sampler.d.mts +47 -0
  138. package/dist/sim/sampler.mjs +79 -0
  139. package/dist/sim/thermal.d.mts +78 -0
  140. package/dist/sim/thermal.mjs +172 -0
  141. package/dist/sim/types.d.mts +24 -0
  142. package/dist/sim/wave.d.mts +34 -0
  143. package/dist/sim/wave.mjs +61 -0
  144. package/dist/steps/index.d.mts +62 -0
  145. package/dist/steps/index.mjs +133 -0
  146. package/dist/thermo/index.d.mts +82 -0
  147. package/dist/thermo/index.mjs +133 -0
  148. package/dist/view/Stage.d.mts +39 -0
  149. package/dist/view/Stage.mjs +91 -0
  150. package/dist/view/index.d.mts +4 -0
  151. package/dist/view/index.mjs +5 -0
  152. package/dist/view/useElementSize.d.mts +13 -0
  153. package/dist/view/useElementSize.mjs +40 -0
  154. package/dist/view/useInView.d.mts +9 -0
  155. package/dist/view/useInView.mjs +33 -0
  156. package/package.json +141 -0
  157. package/styles.css +70 -0
@@ -0,0 +1,112 @@
1
+ import { isFunction } from "./defs.mjs";
2
+
3
+ //#region src/math/tokenize.ts
4
+ /**
5
+ * expr/tokenize — source string → token stream.
6
+ *
7
+ * Handles numbers (incl. `1.5e3`, rejecting malformed `2..3`), identifiers
8
+ * (classified as function vs variable), operators, parens, and commas. Inserts
9
+ * implicit-multiplication operators where two value-producing tokens abut
10
+ * (`2x`, `3sin(x)`, `(x+1)(x-1)`), so the parser stays simple.
11
+ */
12
+ const isDigit = (c) => c >= "0" && c <= "9";
13
+ const isAlpha = (c) => c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c === "_";
14
+ function tokenize(src) {
15
+ const raw = [];
16
+ let i = 0;
17
+ const s = src.trim();
18
+ while (i < s.length) {
19
+ const c = s[i];
20
+ if (c === " " || c === " ") {
21
+ i++;
22
+ continue;
23
+ }
24
+ if (isDigit(c) || c === "." && isDigit(s[i + 1] ?? "")) {
25
+ let j = i;
26
+ let seenDot = false;
27
+ while (j < s.length) {
28
+ const ch = s[j] ?? "";
29
+ if (isDigit(ch)) {
30
+ j++;
31
+ continue;
32
+ }
33
+ if (ch === ".") {
34
+ if (seenDot) throw new Error(`Malformed number near '${s.slice(i, j + 1)}'`);
35
+ seenDot = true;
36
+ j++;
37
+ continue;
38
+ }
39
+ break;
40
+ }
41
+ if (s[j] === "e" || s[j] === "E") {
42
+ let k = j + 1;
43
+ if (s[k] === "+" || s[k] === "-") k++;
44
+ if (!isDigit(s[k] ?? "")) throw new Error(`Incomplete exponent in number '${s.slice(i, k + 1)}'`);
45
+ k++;
46
+ while (isDigit(s[k] ?? "")) k++;
47
+ j = k;
48
+ }
49
+ raw.push({
50
+ t: "num",
51
+ v: Number.parseFloat(s.slice(i, j))
52
+ });
53
+ i = j;
54
+ continue;
55
+ }
56
+ if (isAlpha(c)) {
57
+ let j = i + 1;
58
+ while (j < s.length && (isAlpha(s[j] ?? "") || isDigit(s[j] ?? ""))) j++;
59
+ const orig = s.slice(i, j);
60
+ const lower = orig.toLowerCase();
61
+ raw.push(isFunction(lower) ? {
62
+ t: "fn",
63
+ v: lower
64
+ } : {
65
+ t: "var",
66
+ v: orig
67
+ });
68
+ i = j;
69
+ continue;
70
+ }
71
+ if (c === "(") {
72
+ raw.push({ t: "lp" });
73
+ i++;
74
+ continue;
75
+ }
76
+ if (c === ")") {
77
+ raw.push({ t: "rp" });
78
+ i++;
79
+ continue;
80
+ }
81
+ if (c === ",") {
82
+ raw.push({ t: "comma" });
83
+ i++;
84
+ continue;
85
+ }
86
+ if ("+-*/%^".includes(c)) {
87
+ raw.push({
88
+ t: "op",
89
+ v: c
90
+ });
91
+ i++;
92
+ continue;
93
+ }
94
+ throw new Error(`Unexpected character '${c}'`);
95
+ }
96
+ const out = [];
97
+ for (let k = 0; k < raw.length; k++) {
98
+ const prev = out[out.length - 1];
99
+ const cur = raw[k];
100
+ const prevIsValue = !!prev && (prev.t === "num" || prev.t === "var" || prev.t === "rp");
101
+ const curStartsValue = cur.t === "num" || cur.t === "var" || cur.t === "fn" || cur.t === "lp";
102
+ if (prevIsValue && curStartsValue) out.push({
103
+ t: "op",
104
+ v: "*"
105
+ });
106
+ out.push(cur);
107
+ }
108
+ return out;
109
+ }
110
+
111
+ //#endregion
112
+ export { tokenize };
@@ -0,0 +1,26 @@
1
+ import { CoordinateSystem, CoordsOptions, ViewBox } from "../core/coords.mjs";
2
+ import { ReactNode } from "react";
3
+
4
+ //#region src/primitives/CanvasLayer.d.ts
5
+ interface CanvasLayerProps {
6
+ view: ViewBox;
7
+ height?: number;
8
+ /** Draw into the HiDPI 2D context using the coordinate system (math↔px). */
9
+ draw: (ctx: CanvasRenderingContext2D, coords: CoordinateSystem) => void;
10
+ /** Pointer in MATH coords on press + drag (e.g. to set a start point). */
11
+ onPointerMath?: (m: [number, number]) => void;
12
+ coordsOptions?: CoordsOptions;
13
+ ariaLabel?: string;
14
+ className?: string;
15
+ }
16
+ declare function CanvasLayer({
17
+ view,
18
+ height,
19
+ draw,
20
+ onPointerMath,
21
+ coordsOptions,
22
+ ariaLabel,
23
+ className
24
+ }: CanvasLayerProps): ReactNode;
25
+ //#endregion
26
+ export { CanvasLayer, CanvasLayerProps };
@@ -0,0 +1,87 @@
1
+ 'use client';
2
+
3
+ import { createCoords } from "../core/coords.mjs";
4
+ import { useElementSize } from "../view/useElementSize.mjs";
5
+ import { useEffect, useRef } from "react";
6
+ import { jsx } from "react/jsx-runtime";
7
+
8
+ //#region src/primitives/CanvasLayer.tsx
9
+ /**
10
+ * <CanvasLayer> — the engine's HIGH-ELEMENT-COUNT escape hatch: a zero-dependency
11
+ * HiDPI <canvas> that shares the engine's coordinate system + clock. Use it when
12
+ * a lab needs a heatmap, a particle/gas sim, or thousands of moving dots at 60fps
13
+ * — cases where one retained SVG node per element would drop frames. Declarative,
14
+ * low-count, accessible figures should stay on <Stage> (SVG); this is the
15
+ * deliberate, bundle-free alternative to a WebGL engine (no Pixi/three dependency)
16
+ * until a lab MEASURABLY needs shaders/tens-of-thousands of textured sprites.
17
+ *
18
+ * `draw(ctx, coords)` runs in CSS-pixel space (the context is pre-scaled for dpr)
19
+ * on mount, on resize, and whenever `draw`'s identity changes — so memoize it on
20
+ * your state (or bump it from a useFrameLoop tick) to drive animation. Overlay a
21
+ * sibling <Stage> with the SAME view for accessible SVG handles/labels on top.
22
+ */
23
+ function CanvasLayer({ view, height = 320, draw, onPointerMath, coordsOptions, ariaLabel = "interactive canvas", className }) {
24
+ const { ref: wrapRef, size } = useElementSize();
25
+ const canvasRef = useRef(null);
26
+ const dragging = useRef(false);
27
+ const w = Math.max(1, Math.round(size?.width ?? 640));
28
+ useEffect(() => {
29
+ const cv = canvasRef.current;
30
+ if (!cv) return;
31
+ const dpr = Math.min(typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1, 2);
32
+ cv.width = Math.round(w * dpr);
33
+ cv.height = Math.round(height * dpr);
34
+ const ctx = cv.getContext("2d");
35
+ if (!ctx) return;
36
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
37
+ ctx.clearRect(0, 0, w, height);
38
+ draw(ctx, createCoords(w, height, view, coordsOptions));
39
+ }, [
40
+ w,
41
+ height,
42
+ view,
43
+ draw,
44
+ coordsOptions
45
+ ]);
46
+ const toMath = (e) => {
47
+ const cv = canvasRef.current;
48
+ if (!cv || !onPointerMath) return null;
49
+ const rect = cv.getBoundingClientRect();
50
+ return createCoords(w, height, view, coordsOptions).toMath(e.clientX - rect.left, e.clientY - rect.top);
51
+ };
52
+ const fire = (e) => {
53
+ const m = toMath(e);
54
+ if (m) onPointerMath(m);
55
+ };
56
+ return /* @__PURE__ */ jsx("div", {
57
+ ref: wrapRef,
58
+ className,
59
+ style: { width: "100%" },
60
+ children: /* @__PURE__ */ jsx("canvas", {
61
+ ref: canvasRef,
62
+ role: "img",
63
+ "aria-label": ariaLabel,
64
+ style: {
65
+ display: "block",
66
+ width: "100%",
67
+ height,
68
+ touchAction: "none",
69
+ cursor: onPointerMath ? "crosshair" : "default"
70
+ },
71
+ onPointerDown: onPointerMath ? (e) => {
72
+ dragging.current = true;
73
+ e.currentTarget.setPointerCapture(e.pointerId);
74
+ fire(e);
75
+ } : void 0,
76
+ onPointerMove: onPointerMath ? (e) => {
77
+ if (dragging.current) fire(e);
78
+ } : void 0,
79
+ onPointerUp: onPointerMath ? () => {
80
+ dragging.current = false;
81
+ } : void 0
82
+ })
83
+ });
84
+ }
85
+
86
+ //#endregion
87
+ export { CanvasLayer };
@@ -0,0 +1,27 @@
1
+ import { StyleProps } from "./props.mjs";
2
+ import { ReactNode } from "react";
3
+
4
+ //#region src/primitives/Dot.d.ts
5
+ interface DotProps extends StyleProps {
6
+ x: number;
7
+ y: number;
8
+ /** Pixel radius (never scaled). */
9
+ r?: number;
10
+ }
11
+ declare function Dot({
12
+ x,
13
+ y,
14
+ r,
15
+ color,
16
+ opacity
17
+ }: DotProps): ReactNode;
18
+ /** 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;
26
+ //#endregion
27
+ export { Dot, DotProps, Point };
@@ -0,0 +1,40 @@
1
+ 'use client';
2
+
3
+ import { fmt } from "../core/coords.mjs";
4
+ import { useCoords } from "../core/context.mjs";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+
7
+ //#region src/primitives/Dot.tsx
8
+ function Dot({ x, y, r = 5, color = "var(--stage-accent)", opacity = 1 }) {
9
+ const [px, py] = useCoords().toPx(x, y);
10
+ return /* @__PURE__ */ jsx("circle", {
11
+ cx: fmt(px),
12
+ cy: fmt(py),
13
+ r,
14
+ fill: color,
15
+ opacity
16
+ });
17
+ }
18
+ /** A point with a contrast ring (reads clearly over any background). */
19
+ function Point({ x, y, r = 6, color = "var(--stage-accent)", opacity = 1 }) {
20
+ const [px, py] = useCoords().toPx(x, y);
21
+ return /* @__PURE__ */ jsxs("g", {
22
+ opacity,
23
+ children: [/* @__PURE__ */ jsx("circle", {
24
+ cx: fmt(px),
25
+ cy: fmt(py),
26
+ r,
27
+ fill: color
28
+ }), /* @__PURE__ */ jsx("circle", {
29
+ cx: fmt(px),
30
+ cy: fmt(py),
31
+ r,
32
+ fill: "none",
33
+ stroke: "var(--stage-bg)",
34
+ strokeWidth: 1.5
35
+ })]
36
+ });
37
+ }
38
+
39
+ //#endregion
40
+ export { Dot, Point };
@@ -0,0 +1,30 @@
1
+ import { ReactNode } from "react";
2
+
3
+ //#region src/primitives/Grid.d.ts
4
+ /** A "nice" step (1,2,5 × 10ⁿ) so ~`target` gridlines span the range. */
5
+ declare function niceStep(span: number, target?: number): number;
6
+ interface GridProps {
7
+ step?: number;
8
+ color?: string;
9
+ }
10
+ declare function Grid({
11
+ step,
12
+ color
13
+ }: GridProps): ReactNode;
14
+ interface AxesProps {
15
+ color?: string;
16
+ ticks?: boolean;
17
+ step?: number;
18
+ /** Show the numeric value at each tick (and a "0" at the origin) so learners
19
+ * can READ coordinates off the grid — needed for graphs where the answer is a
20
+ * point (systems, plotting). Default off (most figures want a clean axis). */
21
+ labels?: boolean;
22
+ }
23
+ declare function Axes({
24
+ color,
25
+ ticks,
26
+ step,
27
+ labels
28
+ }: AxesProps): ReactNode;
29
+ //#endregion
30
+ export { Axes, AxesProps, Grid, GridProps, niceStep };
@@ -0,0 +1,122 @@
1
+ 'use client';
2
+
3
+ import { useCoords } from "../core/context.mjs";
4
+ import { jsx } from "react/jsx-runtime";
5
+
6
+ //#region src/primitives/Grid.tsx
7
+ /** A "nice" step (1,2,5 × 10ⁿ) so ~`target` gridlines span the range. */
8
+ function niceStep(span, target = 8) {
9
+ const raw = Math.abs(span) / target || 1;
10
+ const magnitude = Math.pow(10, Math.floor(Math.log10(raw)));
11
+ const n = raw / magnitude;
12
+ return (n < 1.5 ? 1 : n < 3 ? 2 : n < 7 ? 5 : 10) * magnitude;
13
+ }
14
+ function tickValues(min, max, step) {
15
+ const out = [];
16
+ const start = Math.ceil(min / step) * step;
17
+ for (let v = start; v <= max + step * 1e-6; v += step) out.push(Math.round(v / step) * step);
18
+ return out;
19
+ }
20
+ function Grid({ step, color = "var(--stage-grid)" }) {
21
+ const c = useCoords();
22
+ const s = step ?? niceStep(c.view.xMax - c.view.xMin);
23
+ const lines = [];
24
+ for (const x of tickValues(c.view.xMin, c.view.xMax, s)) {
25
+ const a = c.toPx(x, c.view.yMin);
26
+ const b = c.toPx(x, c.view.yMax);
27
+ lines.push(/* @__PURE__ */ jsx("line", {
28
+ x1: a[0],
29
+ y1: a[1],
30
+ x2: b[0],
31
+ y2: b[1],
32
+ stroke: color,
33
+ strokeWidth: 1
34
+ }, `gx${x}`));
35
+ }
36
+ for (const y of tickValues(c.view.yMin, c.view.yMax, s)) {
37
+ const a = c.toPx(c.view.xMin, y);
38
+ const b = c.toPx(c.view.xMax, y);
39
+ lines.push(/* @__PURE__ */ jsx("line", {
40
+ x1: a[0],
41
+ y1: a[1],
42
+ x2: b[0],
43
+ y2: b[1],
44
+ stroke: color,
45
+ strokeWidth: 1
46
+ }, `gy${y}`));
47
+ }
48
+ return /* @__PURE__ */ jsx("g", { children: lines });
49
+ }
50
+ const fmtTick = (v) => Number.isInteger(v) ? String(v) : v.toFixed(1);
51
+ function Axes({ color = "var(--stage-axis)", ticks = true, step, labels = false }) {
52
+ const c = useCoords();
53
+ const s = step ?? niceStep(c.view.xMax - c.view.xMin);
54
+ const xAxisA = c.toPx(c.view.xMin, 0);
55
+ const xAxisB = c.toPx(c.view.xMax, 0);
56
+ const yAxisA = c.toPx(0, c.view.yMin);
57
+ const yAxisB = c.toPx(0, c.view.yMax);
58
+ const labelStyle = {
59
+ paintOrder: "stroke",
60
+ stroke: "var(--stage-bg)",
61
+ strokeWidth: 3,
62
+ strokeLinejoin: "round"
63
+ };
64
+ const num = (key, px, py, text, anchor) => /* @__PURE__ */ jsx("text", {
65
+ x: px,
66
+ y: py,
67
+ fill: "var(--stage-muted)",
68
+ fontSize: 11,
69
+ textAnchor: anchor,
70
+ dominantBaseline: "middle",
71
+ style: labelStyle,
72
+ children: text
73
+ }, key);
74
+ const nodes = [/* @__PURE__ */ jsx("line", {
75
+ x1: xAxisA[0],
76
+ y1: xAxisA[1],
77
+ x2: xAxisB[0],
78
+ y2: xAxisB[1],
79
+ stroke: color,
80
+ strokeWidth: 1.5
81
+ }, "ax"), /* @__PURE__ */ jsx("line", {
82
+ x1: yAxisA[0],
83
+ y1: yAxisA[1],
84
+ x2: yAxisB[0],
85
+ y2: yAxisB[1],
86
+ stroke: color,
87
+ strokeWidth: 1.5
88
+ }, "ay")];
89
+ if (ticks) {
90
+ for (const x of tickValues(c.view.xMin, c.view.xMax, s)) {
91
+ if (Math.abs(x) < 1e-9) continue;
92
+ const p = c.toPx(x, 0);
93
+ nodes.push(/* @__PURE__ */ jsx("line", {
94
+ x1: p[0],
95
+ y1: p[1] - 4,
96
+ x2: p[0],
97
+ y2: p[1] + 4,
98
+ stroke: color,
99
+ strokeWidth: 1.5
100
+ }, `tx${x}`));
101
+ if (labels) nodes.push(num(`lx${x}`, p[0], p[1] + 14, fmtTick(x), "middle"));
102
+ }
103
+ for (const y of tickValues(c.view.yMin, c.view.yMax, s)) {
104
+ if (Math.abs(y) < 1e-9) continue;
105
+ const p = c.toPx(0, y);
106
+ nodes.push(/* @__PURE__ */ jsx("line", {
107
+ x1: p[0] - 4,
108
+ y1: p[1],
109
+ x2: p[0] + 4,
110
+ y2: p[1],
111
+ stroke: color,
112
+ strokeWidth: 1.5
113
+ }, `ty${y}`));
114
+ if (labels) nodes.push(num(`ly${y}`, p[0] - 8, p[1], fmtTick(y), "end"));
115
+ }
116
+ if (labels) nodes.push(num("l0", c.toPx(0, 0)[0] - 8, c.toPx(0, 0)[1] + 12, "0", "end"));
117
+ }
118
+ return /* @__PURE__ */ jsx("g", { children: nodes });
119
+ }
120
+
121
+ //#endregion
122
+ export { Axes, Grid, niceStep };
@@ -0,0 +1,33 @@
1
+ import { ReactNode } from "react";
2
+
3
+ //#region src/primitives/Label.d.ts
4
+ interface LabelProps {
5
+ x: number;
6
+ y: number;
7
+ text: string;
8
+ color?: string;
9
+ size?: number;
10
+ /** Pixel offset from the anchor. */
11
+ dx?: number;
12
+ dy?: number;
13
+ anchor?: 'start' | 'middle' | 'end';
14
+ baseline?: 'auto' | 'middle' | 'hanging';
15
+ weight?: number;
16
+ }
17
+ /** Upright pixel-space text with a background-colored outline for legibility.
18
+ * Renders `_`/`^` as real SVG sub/superscripts via the shared `parseRichText`
19
+ * 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;
32
+ //#endregion
33
+ export { Label, LabelProps };
@@ -0,0 +1,45 @@
1
+ 'use client';
2
+
3
+ import { fmt } from "../core/coords.mjs";
4
+ import { parseRichText } from "../core/richText.mjs";
5
+ import { useCoords } from "../core/context.mjs";
6
+ import { jsx } from "react/jsx-runtime";
7
+
8
+ //#region src/primitives/Label.tsx
9
+ /** Upright pixel-space text with a background-colored outline for legibility.
10
+ * Renders `_`/`^` as real SVG sub/superscripts via the shared `parseRichText`
11
+ * 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 [px, py] = useCoords().toPx(x, y);
14
+ const spans = parseRichText(text);
15
+ const shift = size * .32;
16
+ let prevLevel = 0;
17
+ return /* @__PURE__ */ jsx("text", {
18
+ x: fmt(px + dx),
19
+ y: fmt(py + dy),
20
+ fill: color,
21
+ fontSize: size,
22
+ fontWeight: weight,
23
+ textAnchor: anchor,
24
+ dominantBaseline: baseline,
25
+ style: {
26
+ paintOrder: "stroke",
27
+ stroke: "var(--stage-bg)",
28
+ strokeWidth: 3,
29
+ strokeLinejoin: "round"
30
+ },
31
+ children: spans.length === 1 && spans[0].script === "base" ? text : spans.map((sp, i) => {
32
+ const level = sp.script === "sub" ? 1 : sp.script === "sup" ? -1 : 0;
33
+ const dyPx = (level - prevLevel) * shift;
34
+ prevLevel = level;
35
+ return /* @__PURE__ */ jsx("tspan", {
36
+ dy: fmt(dyPx),
37
+ fontSize: sp.script === "base" ? void 0 : fmt(size * .72),
38
+ children: sp.text
39
+ }, i);
40
+ })
41
+ });
42
+ }
43
+
44
+ //#endregion
45
+ export { Label };
@@ -0,0 +1,50 @@
1
+ import { Vec2 } from "../core/vec.mjs";
2
+ import { StyleProps } from "./props.mjs";
3
+ import { ReactNode } from "react";
4
+
5
+ //#region src/primitives/Lines.d.ts
6
+ interface SegmentProps extends StyleProps {
7
+ from: Vec2;
8
+ to: Vec2;
9
+ }
10
+ declare function Segment({
11
+ from,
12
+ to,
13
+ color,
14
+ weight,
15
+ opacity,
16
+ dashed
17
+ }: SegmentProps): ReactNode;
18
+ interface LineProps extends StyleProps {
19
+ from: Vec2;
20
+ to: Vec2;
21
+ }
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;
38
+ interface VectorProps extends StyleProps {
39
+ tail?: Vec2;
40
+ tip: Vec2;
41
+ }
42
+ declare function Vector({
43
+ tail,
44
+ tip,
45
+ color,
46
+ weight,
47
+ opacity
48
+ }: VectorProps): ReactNode;
49
+ //#endregion
50
+ export { Line, LineProps, Ray, Segment, SegmentProps, Vector, VectorProps };
@@ -0,0 +1,82 @@
1
+ 'use client';
2
+
3
+ import { fmt } from "../core/coords.mjs";
4
+ import { useCoords } from "../core/context.mjs";
5
+ import { jsx } from "react/jsx-runtime";
6
+
7
+ //#region src/primitives/Lines.tsx
8
+ const extend = (c, a, b, back) => {
9
+ const dx = b[0] - a[0];
10
+ const dy = b[1] - a[1];
11
+ const len = Math.hypot(dx, dy) || 1;
12
+ const ux = dx / len;
13
+ const uy = dy / len;
14
+ const big = Math.hypot(c.width, c.height) * 2;
15
+ return [back ? [a[0] - ux * big, a[1] - uy * big] : a, [a[0] + ux * big, a[1] + uy * big]];
16
+ };
17
+ function Segment({ from, to, color = "var(--stage-fg)", weight = 2, opacity = 1, dashed }) {
18
+ const c = useCoords();
19
+ const a = c.toPx(from.x, from.y);
20
+ const b = c.toPx(to.x, to.y);
21
+ return /* @__PURE__ */ jsx("line", {
22
+ x1: fmt(a[0]),
23
+ y1: fmt(a[1]),
24
+ x2: fmt(b[0]),
25
+ y2: fmt(b[1]),
26
+ stroke: color,
27
+ strokeWidth: weight,
28
+ opacity,
29
+ strokeLinecap: "round",
30
+ strokeDasharray: dashed ? "6 6" : void 0
31
+ });
32
+ }
33
+ function Line({ from, to, color = "var(--stage-axis)", weight = 2, opacity = 1, dashed }) {
34
+ const c = useCoords();
35
+ const [p1, p2] = extend(c, c.toPx(from.x, from.y), c.toPx(to.x, to.y), true);
36
+ return /* @__PURE__ */ jsx("line", {
37
+ x1: fmt(p1[0]),
38
+ y1: fmt(p1[1]),
39
+ x2: fmt(p2[0]),
40
+ y2: fmt(p2[1]),
41
+ stroke: color,
42
+ strokeWidth: weight,
43
+ opacity,
44
+ strokeDasharray: dashed ? "6 6" : void 0
45
+ });
46
+ }
47
+ function Ray({ from, to, color = "var(--stage-axis)", weight = 2, opacity = 1, dashed }) {
48
+ const c = useCoords();
49
+ const [p1, p2] = extend(c, c.toPx(from.x, from.y), c.toPx(to.x, to.y), false);
50
+ return /* @__PURE__ */ jsx("line", {
51
+ x1: fmt(p1[0]),
52
+ y1: fmt(p1[1]),
53
+ x2: fmt(p2[0]),
54
+ y2: fmt(p2[1]),
55
+ stroke: color,
56
+ strokeWidth: weight,
57
+ opacity,
58
+ strokeDasharray: dashed ? "6 6" : void 0
59
+ });
60
+ }
61
+ function Vector({ tail = {
62
+ x: 0,
63
+ y: 0
64
+ }, tip, color = "var(--stage-accent-2)", weight = 2.5, opacity = 1 }) {
65
+ const c = useCoords();
66
+ const a = c.toPx(tail.x, tail.y);
67
+ const b = c.toPx(tip.x, tip.y);
68
+ return /* @__PURE__ */ jsx("line", {
69
+ x1: fmt(a[0]),
70
+ y1: fmt(a[1]),
71
+ x2: fmt(b[0]),
72
+ y2: fmt(b[1]),
73
+ stroke: color,
74
+ strokeWidth: weight,
75
+ opacity,
76
+ strokeLinecap: "round",
77
+ markerEnd: "url(#stage-arrow)"
78
+ });
79
+ }
80
+
81
+ //#endregion
82
+ export { Line, Ray, Segment, Vector };