@classytic/stage 0.1.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 (110) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +7 -3
  3. package/dist/assets/index.mjs +0 -1
  4. package/dist/assets/kit/glyphs.d.mts +5 -5
  5. package/dist/assets/kit/glyphs.mjs +4 -4
  6. package/dist/builder/Palette.mjs +50 -89
  7. package/dist/builder/SceneBuilder.mjs +18 -76
  8. package/dist/builder/editor.d.mts +2 -1
  9. package/dist/builder/editor.mjs +14 -4
  10. package/dist/builder/tools.d.mts +1 -1
  11. package/dist/builder/tools.mjs +1 -1
  12. package/dist/chem/index.d.mts +5 -5
  13. package/dist/chem/index.mjs +4 -4
  14. package/dist/circuit/index.d.mts +92 -0
  15. package/dist/circuit/index.mjs +333 -0
  16. package/dist/core/clock.d.mts +1 -1
  17. package/dist/core/clock.mjs +3 -3
  18. package/dist/core/control.d.mts +1 -1
  19. package/dist/core/control.mjs +2 -2
  20. package/dist/core/coords.d.mts +4 -4
  21. package/dist/core/coords.mjs +1 -1
  22. package/dist/core/index.d.mts +2 -1
  23. package/dist/core/index.mjs +2 -1
  24. package/dist/core/learner.d.mts +1 -1
  25. package/dist/core/learner.mjs +1 -1
  26. package/dist/core/math.d.mts +26 -0
  27. package/dist/core/math.mjs +37 -0
  28. package/dist/core/motion.d.mts +6 -6
  29. package/dist/core/motion.mjs +6 -6
  30. package/dist/core/richText.d.mts +2 -2
  31. package/dist/core/vec.d.mts +1 -1
  32. package/dist/field/index.d.mts +11 -3
  33. package/dist/field/index.mjs +20 -3
  34. package/dist/finance/bizsim.d.mts +93 -0
  35. package/dist/finance/bizsim.mjs +117 -0
  36. package/dist/finance/index.d.mts +118 -0
  37. package/dist/finance/index.mjs +203 -0
  38. package/dist/index.d.mts +6 -3
  39. package/dist/index.mjs +6 -4
  40. package/dist/interaction/MovableDot.d.mts +17 -1
  41. package/dist/interaction/MovableDot.mjs +160 -43
  42. package/dist/interaction/index.d.mts +2 -1
  43. package/dist/interaction/index.mjs +2 -1
  44. package/dist/interaction/useDraggable.d.mts +1 -1
  45. package/dist/interaction/useDraggable.mjs +55 -5
  46. package/dist/interaction/usePressSpring.d.mts +24 -0
  47. package/dist/interaction/usePressSpring.mjs +67 -0
  48. package/dist/logic/ast.d.mts +1 -1
  49. package/dist/logic/index.mjs +8 -3
  50. package/dist/logic/minimize.d.mts +2 -2
  51. package/dist/logic/minimize.mjs +56 -18
  52. package/dist/logic/table.mjs +1 -1
  53. package/dist/math/ast.d.mts +1 -1
  54. package/dist/math/ast.mjs +1 -1
  55. package/dist/math/calculus.d.mts +22 -1
  56. package/dist/math/calculus.mjs +224 -11
  57. package/dist/math/compile.d.mts +8 -0
  58. package/dist/math/compile.mjs +40 -0
  59. package/dist/math/defs.mjs +1 -1
  60. package/dist/math/index.d.mts +5 -4
  61. package/dist/math/index.mjs +5 -4
  62. package/dist/math/latex.mjs +8 -0
  63. package/dist/math/parse.mjs +2 -2
  64. package/dist/math/tokenize.mjs +1 -1
  65. package/dist/primitives/CanvasLayer.mjs +3 -3
  66. package/dist/primitives/Dot.d.mts +2 -15
  67. package/dist/primitives/Dot.mjs +6 -4
  68. package/dist/primitives/Grid.d.mts +41 -14
  69. package/dist/primitives/Grid.mjs +100 -22
  70. package/dist/primitives/Label.d.mts +2 -15
  71. package/dist/primitives/Label.mjs +4 -3
  72. package/dist/primitives/Lines.d.mts +4 -32
  73. package/dist/primitives/Lines.mjs +10 -8
  74. package/dist/primitives/Shapes.d.mts +5 -43
  75. package/dist/primitives/Shapes.mjs +12 -10
  76. package/dist/primitives/Tex.mjs +2 -2
  77. package/dist/primitives/index.d.mts +2 -2
  78. package/dist/primitives/index.mjs +2 -2
  79. package/dist/primitives/props.mjs +31 -0
  80. package/dist/scene/Scene.d.mts +6 -1
  81. package/dist/scene/Scene.mjs +16 -41
  82. package/dist/scene/assets.d.mts +2 -2
  83. package/dist/scene/assets.mjs +2 -2
  84. package/dist/scene/commands.d.mts +10 -1
  85. package/dist/scene/commands.mjs +42 -2
  86. package/dist/scene/evaluators.mjs +2 -2
  87. package/dist/scene/index.d.mts +2 -2
  88. package/dist/scene/index.mjs +2 -2
  89. package/dist/scene/migrate.mjs +1 -1
  90. package/dist/scene/sims.mjs +1 -1
  91. package/dist/scene/types.d.mts +4 -4
  92. package/dist/sim/particles.d.mts +2 -2
  93. package/dist/sim/rate.d.mts +4 -4
  94. package/dist/sim/rate.mjs +1 -1
  95. package/dist/sim/registry.d.mts +1 -1
  96. package/dist/sim/sampler.d.mts +5 -5
  97. package/dist/sim/thermal.d.mts +3 -3
  98. package/dist/sim/thermal.mjs +2 -2
  99. package/dist/sim/types.d.mts +2 -2
  100. package/dist/sim/wave.d.mts +2 -2
  101. package/dist/sim/wave.mjs +2 -2
  102. package/dist/steps/index.d.mts +1 -1
  103. package/dist/steps/index.mjs +3 -3
  104. package/dist/thermo/index.d.mts +1 -1
  105. package/dist/thermo/index.mjs +1 -1
  106. package/dist/view/Stage.mjs +6 -13
  107. package/dist/view/useInView.mjs +1 -1
  108. package/package.json +37 -25
  109. package/styles.css +141 -1
  110. package/dist/assets/kit/index.mjs +0 -4
@@ -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,30 +1,57 @@
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;
6
4
  interface GridProps {
7
5
  step?: number;
6
+ /** Per-axis overrides (win over `step`); keeps a tall-thin or short-wide plot
7
+ * from inheriting the other axis's spacing. */
8
+ stepX?: number;
9
+ stepY?: number;
8
10
  color?: string;
9
11
  }
10
- declare function Grid({
11
- step,
12
- color
13
- }: 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;
14
41
  interface AxesProps {
15
42
  color?: string;
16
43
  ticks?: boolean;
17
44
  step?: number;
45
+ /** Per-axis tick spacing overrides (win over `step`). */
46
+ stepX?: number;
47
+ stepY?: number;
18
48
  /** 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
49
+ * can READ coordinates off the grid, needed for graphs where the answer is a
20
50
  * point (systems, plotting). Default off (most figures want a clean axis). */
21
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[];
22
54
  }
23
- declare function Axes({
24
- color,
25
- ticks,
26
- step,
27
- labels
28
- }: AxesProps): ReactNode;
55
+ declare const Axes: import("react").NamedExoticComponent<AxesProps>;
29
56
  //#endregion
30
- 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,11 +18,12 @@ 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, color = "var(--stage-grid)" }) {
21
+ const Grid = memo(function Grid({ step, stepX, stepY, color = "var(--stage-grid)" }) {
21
22
  const c = useCoords();
22
- const s = step ?? niceStep(c.view.xMax - c.view.xMin);
23
+ const sx = stepX ?? step ?? niceStep(c.view.xMax - c.view.xMin);
24
+ const sy = stepY ?? step ?? niceStep(c.view.yMax - c.view.yMin);
23
25
  const lines = [];
24
- for (const x of tickValues(c.view.xMin, c.view.xMax, s)) {
26
+ for (const x of tickValues(c.view.xMin, c.view.xMax, sx)) {
25
27
  const a = c.toPx(x, c.view.yMin);
26
28
  const b = c.toPx(x, c.view.yMax);
27
29
  lines.push(/* @__PURE__ */ jsx("line", {
@@ -33,7 +35,7 @@ function Grid({ step, color = "var(--stage-grid)" }) {
33
35
  strokeWidth: 1
34
36
  }, `gx${x}`));
35
37
  }
36
- for (const y of tickValues(c.view.yMin, c.view.yMax, s)) {
38
+ for (const y of tickValues(c.view.yMin, c.view.yMax, sy)) {
37
39
  const a = c.toPx(c.view.xMin, y);
38
40
  const b = c.toPx(c.view.xMax, y);
39
41
  lines.push(/* @__PURE__ */ jsx("line", {
@@ -46,15 +48,49 @@ function Grid({ step, color = "var(--stage-grid)" }) {
46
48
  }, `gy${y}`));
47
49
  }
48
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
+ };
49
65
  }
50
- const fmtTick = (v) => Number.isInteger(v) ? String(v) : v.toFixed(1);
51
- function Axes({ color = "var(--stage-axis)", ticks = true, step, labels = false }) {
66
+ const fmtTick = (v) => {
67
+ if (Number.isInteger(v)) return String(v);
68
+ const r = Math.round(v * 100) / 100;
69
+ return String(r);
70
+ };
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 }) {
52
73
  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);
74
+ const sx = stepX ?? step ?? niceStep(c.view.xMax - c.view.xMin);
75
+ const sy = stepY ?? step ?? niceStep(c.view.yMax - c.view.yMin);
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);
58
94
  const labelStyle = {
59
95
  paintOrder: "stroke",
60
96
  stroke: "var(--stage-bg)",
@@ -87,9 +123,52 @@ function Axes({ color = "var(--stage-axis)", ticks = true, step, labels = false
87
123
  strokeWidth: 1.5
88
124
  }, "ay")];
89
125
  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);
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);
93
172
  nodes.push(/* @__PURE__ */ jsx("line", {
94
173
  x1: p[0],
95
174
  y1: p[1] - 4,
@@ -98,11 +177,10 @@ function Axes({ color = "var(--stage-axis)", ticks = true, step, labels = false
98
177
  stroke: color,
99
178
  strokeWidth: 1.5
100
179
  }, `tx${x}`));
101
- 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"));
102
181
  }
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);
182
+ for (const y of ys) {
183
+ const p = c.toPx(axisX, y);
106
184
  nodes.push(/* @__PURE__ */ jsx("line", {
107
185
  x1: p[0] - 4,
108
186
  y1: p[1],
@@ -111,12 +189,12 @@ function Axes({ color = "var(--stage-axis)", ticks = true, step, labels = false
111
189
  stroke: color,
112
190
  strokeWidth: 1.5
113
191
  }, `ty${y}`));
114
- 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));
115
193
  }
116
- 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"));
117
195
  }
118
196
  return /* @__PURE__ */ jsx("g", { children: nodes });
119
- }
197
+ });
120
198
 
121
199
  //#endregion
122
- 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;
@@ -16,18 +14,7 @@ interface LabelProps {
16
14
  }
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
- * 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;
17
+ * grammar (the same one labs' HTML `<RichText>` uses, one source of truth). */
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
- * 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 }) {
12
+ * grammar (the same one labs' HTML `<RichText>` uses, one source of truth). */
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 };
@@ -2,6 +2,8 @@
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 } from "react/jsx-runtime";
6
8
 
7
9
  //#region src/primitives/Lines.tsx
@@ -14,7 +16,7 @@ const extend = (c, a, b, back) => {
14
16
  const big = Math.hypot(c.width, c.height) * 2;
15
17
  return [back ? [a[0] - ux * big, a[1] - uy * big] : a, [a[0] + ux * big, a[1] + uy * big]];
16
18
  };
17
- function Segment({ from, to, color = "var(--stage-fg)", weight = 2, opacity = 1, dashed }) {
19
+ const Segment = memo(function Segment({ from, to, color = "var(--stage-fg)", weight = 2, opacity = 1, dashed }) {
18
20
  const c = useCoords();
19
21
  const a = c.toPx(from.x, from.y);
20
22
  const b = c.toPx(to.x, to.y);
@@ -29,8 +31,8 @@ function Segment({ from, to, color = "var(--stage-fg)", weight = 2, opacity = 1,
29
31
  strokeLinecap: "round",
30
32
  strokeDasharray: dashed ? "6 6" : void 0
31
33
  });
32
- }
33
- function Line({ from, to, color = "var(--stage-axis)", weight = 2, opacity = 1, dashed }) {
34
+ }, geomPropsEqual);
35
+ const Line = memo(function Line({ from, to, color = "var(--stage-axis)", weight = 2, opacity = 1, dashed }) {
34
36
  const c = useCoords();
35
37
  const [p1, p2] = extend(c, c.toPx(from.x, from.y), c.toPx(to.x, to.y), true);
36
38
  return /* @__PURE__ */ jsx("line", {
@@ -43,8 +45,8 @@ function Line({ from, to, color = "var(--stage-axis)", weight = 2, opacity = 1,
43
45
  opacity,
44
46
  strokeDasharray: dashed ? "6 6" : void 0
45
47
  });
46
- }
47
- function Ray({ from, to, color = "var(--stage-axis)", weight = 2, opacity = 1, dashed }) {
48
+ }, geomPropsEqual);
49
+ const Ray = memo(function Ray({ from, to, color = "var(--stage-axis)", weight = 2, opacity = 1, dashed }) {
48
50
  const c = useCoords();
49
51
  const [p1, p2] = extend(c, c.toPx(from.x, from.y), c.toPx(to.x, to.y), false);
50
52
  return /* @__PURE__ */ jsx("line", {
@@ -57,8 +59,8 @@ function Ray({ from, to, color = "var(--stage-axis)", weight = 2, opacity = 1, d
57
59
  opacity,
58
60
  strokeDasharray: dashed ? "6 6" : void 0
59
61
  });
60
- }
61
- function Vector({ tail = {
62
+ }, geomPropsEqual);
63
+ const Vector = memo(function Vector({ tail = {
62
64
  x: 0,
63
65
  y: 0
64
66
  }, tip, color = "var(--stage-accent-2)", weight = 2.5, opacity = 1 }) {
@@ -76,7 +78,7 @@ function Vector({ tail = {
76
78
  strokeLinecap: "round",
77
79
  markerEnd: "url(#stage-arrow)"
78
80
  });
79
- }
81
+ }, geomPropsEqual);
80
82
 
81
83
  //#endregion
82
84
  export { Line, Ray, Segment, Vector };
@@ -1,6 +1,5 @@
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/Shapes.d.ts
6
5
  interface CircleProps extends StyleProps {
@@ -9,15 +8,7 @@ interface CircleProps extends StyleProps {
9
8
  fill?: string;
10
9
  fillOpacity?: number;
11
10
  }
12
- declare function Circle({
13
- center,
14
- r,
15
- color,
16
- fill,
17
- fillOpacity,
18
- weight,
19
- opacity
20
- }: CircleProps): ReactNode;
11
+ declare const Circle: import("react").NamedExoticComponent<CircleProps>;
21
12
  interface EllipseProps extends StyleProps {
22
13
  center: Vec2;
23
14
  rx: number;
@@ -25,52 +16,23 @@ interface EllipseProps extends StyleProps {
25
16
  fill?: string;
26
17
  fillOpacity?: number;
27
18
  }
28
- declare function Ellipse({
29
- center,
30
- rx,
31
- ry,
32
- color,
33
- fill,
34
- fillOpacity,
35
- weight,
36
- opacity
37
- }: EllipseProps): ReactNode;
19
+ declare const Ellipse: import("react").NamedExoticComponent<EllipseProps>;
38
20
  interface PolygonProps extends StyleProps {
39
21
  points: Vec2[];
40
22
  fill?: string;
41
23
  fillOpacity?: number;
42
24
  }
43
- declare function Polygon({
44
- points,
45
- color,
46
- fill,
47
- fillOpacity,
48
- weight,
49
- opacity
50
- }: PolygonProps): ReactNode;
25
+ declare const Polygon: import("react").NamedExoticComponent<PolygonProps>;
51
26
  interface PolylineProps extends StyleProps {
52
27
  points: Vec2[];
53
28
  }
54
- declare function Polyline({
55
- points,
56
- color,
57
- weight,
58
- opacity,
59
- dashed
60
- }: PolylineProps): ReactNode;
29
+ declare const Polyline: import("react").NamedExoticComponent<PolylineProps>;
61
30
  /** Low-level pixel-space path (d is already in pixels). */
62
31
  interface PathProps extends StyleProps {
63
32
  d: string;
64
33
  fill?: string;
65
34
  fillOpacity?: number;
66
35
  }
67
- declare function Path({
68
- d,
69
- color,
70
- fill,
71
- fillOpacity,
72
- weight,
73
- opacity
74
- }: PathProps): ReactNode;
36
+ declare const Path: import("react").NamedExoticComponent<PathProps>;
75
37
  //#endregion
76
38
  export { Circle, CircleProps, Ellipse, EllipseProps, Path, PathProps, Polygon, PolygonProps, Polyline, PolylineProps };
@@ -2,6 +2,8 @@
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 } from "react/jsx-runtime";
6
8
 
7
9
  //#region src/primitives/Shapes.tsx
@@ -9,7 +11,7 @@ const toPoints = (c, points) => points.map((p) => {
9
11
  const [x, y] = c.toPx(p.x, p.y);
10
12
  return `${fmt(x)},${fmt(y)}`;
11
13
  }).join(" ");
12
- function Circle({ center, r, color = "var(--stage-accent)", fill = "none", fillOpacity = .12, weight = 2, opacity = 1 }) {
14
+ const Circle = memo(function Circle({ center, r, color = "var(--stage-accent)", fill = "none", fillOpacity = .12, weight = 2, opacity = 1 }) {
13
15
  const c = useCoords();
14
16
  const [cx, cy] = c.toPx(center.x, center.y);
15
17
  return /* @__PURE__ */ jsx("ellipse", {
@@ -23,8 +25,8 @@ function Circle({ center, r, color = "var(--stage-accent)", fill = "none", fillO
23
25
  fillOpacity: fill === "none" ? void 0 : fillOpacity,
24
26
  opacity
25
27
  });
26
- }
27
- function Ellipse({ center, rx, ry, color = "var(--stage-accent)", fill = "none", fillOpacity = .12, weight = 2, opacity = 1 }) {
28
+ }, geomPropsEqual);
29
+ const Ellipse = memo(function Ellipse({ center, rx, ry, color = "var(--stage-accent)", fill = "none", fillOpacity = .12, weight = 2, opacity = 1 }) {
28
30
  const c = useCoords();
29
31
  const [cx, cy] = c.toPx(center.x, center.y);
30
32
  return /* @__PURE__ */ jsx("ellipse", {
@@ -38,8 +40,8 @@ function Ellipse({ center, rx, ry, color = "var(--stage-accent)", fill = "none",
38
40
  fillOpacity: fill === "none" ? void 0 : fillOpacity,
39
41
  opacity
40
42
  });
41
- }
42
- function Polygon({ points, color = "var(--stage-accent)", fill = "var(--stage-accent)", fillOpacity = .12, weight = 2, opacity = 1 }) {
43
+ }, geomPropsEqual);
44
+ const Polygon = memo(function Polygon({ points, color = "var(--stage-accent)", fill = "var(--stage-accent)", fillOpacity = .12, weight = 2, opacity = 1 }) {
43
45
  return /* @__PURE__ */ jsx("polygon", {
44
46
  points: toPoints(useCoords(), points),
45
47
  stroke: color,
@@ -49,8 +51,8 @@ function Polygon({ points, color = "var(--stage-accent)", fill = "var(--stage-ac
49
51
  opacity,
50
52
  strokeLinejoin: "round"
51
53
  });
52
- }
53
- function Polyline({ points, color = "var(--stage-fg)", weight = 2, opacity = 1, dashed }) {
54
+ }, geomPropsEqual);
55
+ const Polyline = memo(function Polyline({ points, color = "var(--stage-fg)", weight = 2, opacity = 1, dashed }) {
54
56
  return /* @__PURE__ */ jsx("polyline", {
55
57
  points: toPoints(useCoords(), points),
56
58
  fill: "none",
@@ -61,8 +63,8 @@ function Polyline({ points, color = "var(--stage-fg)", weight = 2, opacity = 1,
61
63
  strokeLinecap: "round",
62
64
  strokeDasharray: dashed ? "6 6" : void 0
63
65
  });
64
- }
65
- function Path({ d, color = "var(--stage-fg)", fill = "none", fillOpacity = 1, weight = 2, opacity = 1 }) {
66
+ }, geomPropsEqual);
67
+ const Path = memo(function Path({ d, color = "var(--stage-fg)", fill = "none", fillOpacity = 1, weight = 2, opacity = 1 }) {
66
68
  return /* @__PURE__ */ jsx("path", {
67
69
  d,
68
70
  stroke: color,
@@ -73,7 +75,7 @@ function Path({ d, color = "var(--stage-fg)", fill = "none", fillOpacity = 1, we
73
75
  strokeLinejoin: "round",
74
76
  strokeLinecap: "round"
75
77
  });
76
- }
78
+ }, geomPropsEqual);
77
79
 
78
80
  //#endregion
79
81
  export { Circle, Ellipse, Path, Polygon, Polyline };
@@ -8,11 +8,11 @@ import katex from "katex";
8
8
 
9
9
  //#region src/primitives/Tex.tsx
10
10
  /**
11
- * Tex KaTeX maths in a <foreignObject>, rendered AT RENDER TIME (not in an
11
+ * Tex, KaTeX maths in a <foreignObject>, rendered AT RENDER TIME (not in an
12
12
  * effect) so the markup is identical on the server and the client: SSR/static
13
13
  * exports show real maths (not raw `\left…\right`), there's no raw-then-hydrate
14
14
  * flash, and hydration matches. KaTeX is a static import kept in its OWN module so
15
- * only Tex's importers pull it (stage's neverBundle leaves `katex` external the
15
+ * only Tex's importers pull it (stage's neverBundle leaves `katex` external, the
16
16
  * consumer resolves the peer; non-Tex consumers never bundle it).
17
17
  *
18
18
  * Needs the consumer to load `katex/dist/katex.min.css` for proper glyphs.
@@ -4,7 +4,7 @@ import { Line, LineProps, Ray, Segment, SegmentProps, Vector, VectorProps } from
4
4
  import { Circle, CircleProps, Ellipse, EllipseProps, Path, PathProps, Polygon, PolygonProps, Polyline, PolylineProps } from "./Shapes.mjs";
5
5
  import { Label, LabelProps } from "./Label.mjs";
6
6
  import { Tex, TexProps } from "./Tex.mjs";
7
- import { Axes, AxesProps, Grid, GridProps, niceStep } from "./Grid.mjs";
7
+ import { Axes, AxesProps, ClearZone, Grid, GridProps, clearOfLabel, niceStep } from "./Grid.mjs";
8
8
  import { OfXProps, OfYProps, ParametricProps, Plot } from "./Plot.mjs";
9
9
  import { CanvasLayer, CanvasLayerProps } from "./CanvasLayer.mjs";
10
- export { type A11yProps, Axes, type AxesProps, CanvasLayer, type CanvasLayerProps, Circle, type CircleProps, Dot, type DotProps, Ellipse, type EllipseProps, Grid, type GridProps, Label, type LabelProps, Line, type LineProps, type OfXProps, type OfYProps, type ParametricProps, Path, type PathProps, Plot, Point, Polygon, type PolygonProps, Polyline, type PolylineProps, Ray, Segment, type SegmentProps, type StyleProps, Tex, type TexProps, Vector, type VectorProps, niceStep };
10
+ export { type A11yProps, Axes, type AxesProps, CanvasLayer, type CanvasLayerProps, Circle, type CircleProps, type ClearZone, Dot, type DotProps, Ellipse, type EllipseProps, Grid, type GridProps, Label, type LabelProps, Line, type LineProps, type OfXProps, type OfYProps, type ParametricProps, Path, type PathProps, Plot, Point, Polygon, type PolygonProps, Polyline, type PolylineProps, Ray, Segment, type SegmentProps, type StyleProps, Tex, type TexProps, Vector, type VectorProps, clearOfLabel, niceStep };
@@ -3,8 +3,8 @@ import { Line, Ray, Segment, Vector } from "./Lines.mjs";
3
3
  import { Circle, Ellipse, Path, Polygon, Polyline } from "./Shapes.mjs";
4
4
  import { Label } from "./Label.mjs";
5
5
  import { Tex } from "./Tex.mjs";
6
- import { Axes, Grid, niceStep } from "./Grid.mjs";
6
+ import { Axes, Grid, clearOfLabel, niceStep } from "./Grid.mjs";
7
7
  import { Plot } from "./Plot.mjs";
8
8
  import { CanvasLayer } from "./CanvasLayer.mjs";
9
9
 
10
- export { Axes, CanvasLayer, Circle, Dot, Ellipse, Grid, Label, Line, Path, Plot, Point, Polygon, Polyline, Ray, Segment, Tex, Vector, niceStep };
10
+ export { Axes, CanvasLayer, Circle, Dot, Ellipse, Grid, Label, Line, Path, Plot, Point, Polygon, Polyline, Ray, Segment, Tex, Vector, clearOfLabel, niceStep };
@@ -0,0 +1,31 @@
1
+ //#region src/primitives/props.ts
2
+ const MAX_DEPTH = 4;
3
+ const valEq = (a, b, depth) => {
4
+ if (Object.is(a, b)) return true;
5
+ if (depth <= 0) return false;
6
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
7
+ if ("$$typeof" in a || "$$typeof" in b) return false;
8
+ if (Array.isArray(a) || Array.isArray(b)) {
9
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
10
+ for (let i = 0; i < a.length; i++) if (!valEq(a[i], b[i], depth - 1)) return false;
11
+ return true;
12
+ }
13
+ const ka = Object.keys(a);
14
+ if (ka.length !== Object.keys(b).length) return false;
15
+ for (const k of ka) {
16
+ if (!(k in b)) return false;
17
+ if (!valEq(a[k], b[k], depth - 1)) return false;
18
+ }
19
+ return true;
20
+ };
21
+ /**
22
+ * Value-based props comparator for `React.memo` on geometry primitives.
23
+ * Labs recreate `Vec2`/`points` objects every render (often 60×/s under a frame
24
+ * tick), so identity-based memo never hits; comparing coordinate VALUES lets
25
+ * static shapes skip reconciliation while anything that actually moved still
26
+ * re-renders. Functions and React elements compare by reference (safe fallback).
27
+ */
28
+ const geomPropsEqual = (prev, next) => valEq(prev, next, MAX_DEPTH);
29
+
30
+ //#endregion
31
+ export { geomPropsEqual };