@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,34 @@
1
+ import { WaveShape } from "../core/motion.mjs";
2
+ import { SimCore } from "./types.mjs";
3
+
4
+ //#region src/sim/wave.d.ts
5
+ type WaveMode = 'ac' | 'dc';
6
+ interface WaveParams {
7
+ mode?: WaveMode;
8
+ /** Peak voltage (V). Default 9. */
9
+ volts?: number;
10
+ /** AC frequency (Hz). Ignored for DC. Default 1. */
11
+ freqHz?: number;
12
+ shape?: WaveShape;
13
+ /** Trace ring-buffer capacity (samples). Default 240 (~4s @ 60fps). */
14
+ trace?: number;
15
+ }
16
+ interface WaveState {
17
+ mode: WaveMode;
18
+ volts: number;
19
+ freqHz: number;
20
+ shape: WaveShape;
21
+ /** Seconds elapsed on this core's timeline. */
22
+ tSec: number;
23
+ /** Instantaneous signed voltage; sign = current direction. */
24
+ v: number;
25
+ /** ∫v·dt — accumulated charge displacement; what flow position (electrons / water) reads. */
26
+ charge: number;
27
+ /** Rolling history of `v`, oldest-first, length ≤ `cap` — for the live trace. */
28
+ samples: number[];
29
+ /** Fixed capacity of `samples`. */
30
+ cap: number;
31
+ }
32
+ declare const WaveCore: SimCore<WaveState, WaveParams>;
33
+ //#endregion
34
+ export { WaveCore, WaveMode, WaveParams, WaveState };
@@ -0,0 +1,61 @@
1
+ import { oscillate } from "../core/motion.mjs";
2
+
3
+ //#region src/sim/wave.ts
4
+ /**
5
+ * `wave` — the AC/DC signal source. One core drives a whole scene: a DC source
6
+ * holds a steady voltage; an AC source oscillates. Either way it exposes the
7
+ * instantaneous voltage `v` (signed — sign IS current direction) plus a rolling
8
+ * `samples` buffer for a live scrolling trace. The SAME `v` can skin many ways at
9
+ * once (a lamp's brightness, electrons in a wire, water sloshing in a pipe), which
10
+ * is the point: sim ≠ render, so one signal feeds every visual metaphor in sync.
11
+ *
12
+ * `mode` / `volts` / `freqHz` live in state (not just params) so a control can
13
+ * write them between steps and a binding can read them by name.
14
+ */
15
+ function signal(s) {
16
+ if (s.mode === "dc") return s.volts;
17
+ return oscillate(s.tSec, {
18
+ amp: s.volts,
19
+ freq: s.freqHz,
20
+ shape: s.shape
21
+ });
22
+ }
23
+ const WaveCore = {
24
+ reset(params = {}) {
25
+ const cap = Math.max(2, Math.round(params.trace ?? 240));
26
+ const base = {
27
+ mode: params.mode ?? "dc",
28
+ volts: params.volts ?? 9,
29
+ freqHz: params.freqHz ?? 1,
30
+ shape: params.shape ?? "sine",
31
+ tSec: 0
32
+ };
33
+ return {
34
+ ...base,
35
+ v: signal(base),
36
+ charge: 0,
37
+ samples: [signal(base)],
38
+ cap
39
+ };
40
+ },
41
+ step(state, dtSec) {
42
+ const dt = Math.max(0, dtSec);
43
+ const tSec = state.tSec + dt;
44
+ const v = signal({
45
+ ...state,
46
+ tSec
47
+ });
48
+ const charge = state.charge + v * dt;
49
+ const samples = state.samples.length >= state.cap ? [...state.samples.slice(state.samples.length - state.cap + 1), v] : [...state.samples, v];
50
+ return {
51
+ ...state,
52
+ tSec,
53
+ v,
54
+ charge,
55
+ samples
56
+ };
57
+ }
58
+ };
59
+
60
+ //#endregion
61
+ export { WaveCore };
@@ -0,0 +1,62 @@
1
+ import { ReactNode } from "react";
2
+
3
+ //#region src/steps/index.d.ts
4
+ /** One step in the sequence — identity + gating only. Its TITLE, PROSE, and the
5
+ * interactive itself live in the authored Section block, not here. */
6
+ interface StepSpec {
7
+ id: string;
8
+ /** Unlock when ALL these step ids are completed. Default: the previous step
9
+ * (linear). Empty array → always unlocked. */
10
+ gate?: {
11
+ after: string[];
12
+ };
13
+ /** If false, the step never blocks downstream gating (informational/explainer). */
14
+ required?: boolean;
15
+ }
16
+ interface StepProgress {
17
+ completed: boolean;
18
+ score?: {
19
+ raw: number;
20
+ max: number;
21
+ };
22
+ attempts: number;
23
+ }
24
+ interface StepState {
25
+ spec: StepSpec;
26
+ index: number;
27
+ completed: boolean;
28
+ unlocked: boolean;
29
+ /** The first unlocked-and-unfinished step (where the learner "is"). */
30
+ current: boolean;
31
+ progress: StepProgress;
32
+ }
33
+ /** Owns the progress store for one stepped lesson/unit. Wrap its steps in this.
34
+ * `id` namespaces the forwarded xAPI activity (e.g. the lesson id). */
35
+ declare function StepProgressProvider({
36
+ id,
37
+ children
38
+ }: {
39
+ id?: string;
40
+ children: ReactNode;
41
+ }): ReactNode;
42
+ /**
43
+ * Scope a step's interactive so its `useLearner().report(...)` routes into the
44
+ * step progress store (keyed by step id) AND forwards to the host's learner
45
+ * (namespaced) for xAPI. Wrap each step's interactive content with this.
46
+ */
47
+ declare function StepScope({
48
+ id,
49
+ children
50
+ }: {
51
+ id: string;
52
+ children: ReactNode;
53
+ }): ReactNode;
54
+ /** Reactive step state for an ordered sequence: per-step completion + gating +
55
+ * the current step. Pass the `StepSpec[]` derived from the authored sections. */
56
+ declare function useStepProgress(steps: StepSpec[]): {
57
+ steps: StepState[];
58
+ allComplete: boolean;
59
+ reset: () => void;
60
+ };
61
+ //#endregion
62
+ export { StepProgress, StepProgressProvider, StepScope, StepSpec, StepState, useStepProgress };
@@ -0,0 +1,133 @@
1
+ 'use client';
2
+
3
+ import { LearnerProvider, useLearner } from "../core/learner.mjs";
4
+ import { createContext, createElement, useContext, useMemo, useRef, useSyncExternalStore } from "react";
5
+
6
+ //#region src/steps/index.ts
7
+ /**
8
+ * Step progress + gating — the runtime that turns an ordered sequence of lesson
9
+ * STEPS into "unlock the next when this one is solved" + aggregated completion.
10
+ *
11
+ * ONE SOURCE OF TRUTH for the step sequence is the authored content itself — the
12
+ * @classytic/cms-ui `Section`/`Step` blocks a creator orders in the editor
13
+ * (serialized in the MDX). This engine deliberately does NOT model an authored
14
+ * "LessonDoc": that would be a SECOND, competing definition of the same steps
15
+ * (and creators never hand-write one — they build Section blocks). The host's
16
+ * Section player derives a `StepSpec[]` from the section order, wraps each
17
+ * section's interactive in `<StepScope id>`, and reads `useStepProgress` to gate
18
+ * reveal. Each lab's `useLearner().report` flows into step completion AND
19
+ * forwards (namespaced) to the host's xAPI learner.
20
+ *
21
+ * Headless on purpose: progress is one external store read via
22
+ * `useSyncExternalStore` (one store, many subscribers, no concurrent-React
23
+ * tearing). The host renders the prose + resolves each step's interactive.
24
+ */
25
+ const EMPTY = {};
26
+ const NOOP_SUBSCRIBE = () => () => {};
27
+ var StepStore = class {
28
+ map = /* @__PURE__ */ new Map();
29
+ snap = EMPTY;
30
+ listeners = /* @__PURE__ */ new Set();
31
+ subscribe = (l) => {
32
+ this.listeners.add(l);
33
+ return () => {
34
+ this.listeners.delete(l);
35
+ };
36
+ };
37
+ getSnapshot = () => this.snap;
38
+ report(stepId, r) {
39
+ const prev = this.map.get(stepId) ?? {
40
+ completed: false,
41
+ attempts: 0
42
+ };
43
+ this.map.set(stepId, {
44
+ completed: prev.completed || r.completion === true,
45
+ score: r.score ?? prev.score,
46
+ attempts: prev.attempts + 1
47
+ });
48
+ this.snap = Object.fromEntries(this.map);
49
+ this.listeners.forEach((l) => l());
50
+ }
51
+ reset() {
52
+ this.map.clear();
53
+ this.snap = EMPTY;
54
+ this.listeners.forEach((l) => l());
55
+ }
56
+ };
57
+ const StepCtx = createContext(null);
58
+ /** Owns the progress store for one stepped lesson/unit. Wrap its steps in this.
59
+ * `id` namespaces the forwarded xAPI activity (e.g. the lesson id). */
60
+ function StepProgressProvider({ id, children }) {
61
+ const ref = useRef(null);
62
+ if (!ref.current || ref.current.groupId !== id) ref.current = {
63
+ store: ref.current?.store ?? new StepStore(),
64
+ groupId: id
65
+ };
66
+ return createElement(StepCtx.Provider, { value: ref.current }, children);
67
+ }
68
+ /**
69
+ * Scope a step's interactive so its `useLearner().report(...)` routes into the
70
+ * step progress store (keyed by step id) AND forwards to the host's learner
71
+ * (namespaced) for xAPI. Wrap each step's interactive content with this.
72
+ */
73
+ function StepScope({ id, children }) {
74
+ const ctx = useContext(StepCtx);
75
+ const host = useLearner();
76
+ return createElement(LearnerProvider, {
77
+ learner: useMemo(() => ({
78
+ report: (r) => {
79
+ ctx?.store.report(id, r);
80
+ host?.report({
81
+ ...r,
82
+ activity: ctx?.groupId ? `${ctx.groupId}.${id}.${r.activity}` : `${id}.${r.activity}`
83
+ });
84
+ },
85
+ ...host?.getPrior ? { getPrior: (a) => host.getPrior(a) } : {}
86
+ }), [
87
+ ctx,
88
+ host,
89
+ id
90
+ ]),
91
+ children
92
+ });
93
+ }
94
+ /** Reactive step state for an ordered sequence: per-step completion + gating +
95
+ * the current step. Pass the `StepSpec[]` derived from the authored sections. */
96
+ function useStepProgress(steps) {
97
+ const ctx = useContext(StepCtx);
98
+ const subscribe = ctx?.store.subscribe ?? NOOP_SUBSCRIBE;
99
+ const getSnapshot = ctx?.store.getSnapshot ?? (() => EMPTY);
100
+ const snap = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
101
+ return useMemo(() => {
102
+ const out = steps.map((spec, i) => {
103
+ const progress = snap[spec.id] ?? {
104
+ completed: false,
105
+ attempts: 0
106
+ };
107
+ const prev = steps[i - 1];
108
+ const unlocked = (spec.gate?.after ?? (prev ? [prev.id] : [])).every((gid) => snap[gid]?.completed === true);
109
+ return {
110
+ spec,
111
+ index: i,
112
+ completed: progress.completed,
113
+ unlocked,
114
+ current: false,
115
+ progress
116
+ };
117
+ });
118
+ const cur = out.find((s) => s.unlocked && !s.completed) ?? out[out.length - 1];
119
+ if (cur) cur.current = true;
120
+ return {
121
+ steps: out,
122
+ allComplete: out.every((s) => s.completed || s.spec.required === false),
123
+ reset: () => ctx?.store.reset()
124
+ };
125
+ }, [
126
+ steps,
127
+ snap,
128
+ ctx
129
+ ]);
130
+ }
131
+
132
+ //#endregion
133
+ export { StepProgressProvider, StepScope, useStepProgress };
@@ -0,0 +1,82 @@
1
+ //#region src/thermo/index.d.ts
2
+ /**
3
+ * `thermo` — ideal-gas thermodynamic processes, as a PURE kernel (like `field`:
4
+ * no React, no pixels, no time-stepping). A lab drives it with a slider/progress
5
+ * fraction; the kernel returns the full P–V path of a quasi-static process plus the
6
+ * first-law bookkeeping along it (work, heat, internal energy, entropy).
7
+ *
8
+ * Everything is one ideal gas, PV = nRT, with a degrees-of-freedom ratio γ = Cp/Cv
9
+ * (monatomic 5/3, diatomic 7/5). The four canonical processes and the Carnot cycle
10
+ * all compose from `runProcess`:
11
+ *
12
+ * isothermal T const PV = const W = nRT·ln(V₂/V₁), Q = W, ΔU = 0
13
+ * adiabatic Q = 0 PVᵞ = const W = (P₁V₁−P₂V₂)/(γ−1) = −ΔU, ΔS = 0
14
+ * isobaric P const V/T = const W = PΔV, Q = nCpΔT, ΔU = nCvΔT
15
+ * isochoric V const P/T = const W = 0, Q = ΔU = nCvΔT
16
+ *
17
+ * SI units throughout: P in pascals, V in m³, T in kelvin, energy in joules,
18
+ * entropy in J/K. (Labs convert to kPa / litres for display.)
19
+ */
20
+ /** Universal gas constant, J/(mol·K). */
21
+ declare const R = 8.314462618;
22
+ type ProcessKind = 'isothermal' | 'adiabatic' | 'isobaric' | 'isochoric';
23
+ interface GasState {
24
+ /** Pressure, Pa. */
25
+ P: number;
26
+ /** Volume, m³. */
27
+ V: number;
28
+ /** Temperature, K. */
29
+ T: number;
30
+ /** Amount, mol. */
31
+ n: number;
32
+ }
33
+ interface ProcessResult {
34
+ kind: ProcessKind;
35
+ start: GasState;
36
+ end: GasState;
37
+ /** Sampled states from start→end (for drawing the P–V curve). */
38
+ path: GasState[];
39
+ /** Work done BY the gas, J ( = ∫P dV, the area under the P–V curve). */
40
+ W: number;
41
+ /** Heat added TO the gas, J. */
42
+ Q: number;
43
+ /** Change in internal energy, J ( = nCvΔT). */
44
+ dU: number;
45
+ /** Change in entropy, J/K. */
46
+ dS: number;
47
+ }
48
+ /**
49
+ * Run one quasi-static process from `start` to a target. Give exactly one target
50
+ * coordinate that the process can reach (e.g. a target volume for an isothermal or
51
+ * adiabatic expansion, a target temperature for isobaric/isochoric heating).
52
+ */
53
+ declare function runProcess(start: GasState, kind: ProcessKind, target: {
54
+ V?: number;
55
+ P?: number;
56
+ T?: number;
57
+ }, gamma?: number, samples?: number): ProcessResult;
58
+ interface CarnotResult {
59
+ /** The four legs in order: hot isothermal expansion, adiabatic expansion,
60
+ * cold isothermal compression, adiabatic compression (back to start). */
61
+ legs: ProcessResult[];
62
+ Th: number;
63
+ Tc: number;
64
+ /** Heat absorbed from the hot reservoir, J (>0). */
65
+ Qh: number;
66
+ /** Heat rejected to the cold reservoir, J (>0). */
67
+ Qc: number;
68
+ /** Net work done by the engine over the cycle, J ( = Qh − Qc = enclosed P–V area). */
69
+ Wnet: number;
70
+ /** Thermal efficiency = 1 − Tc/Th = Wnet/Qh. */
71
+ efficiency: number;
72
+ }
73
+ /**
74
+ * A Carnot cycle starting at `(V1, Th)`: isothermal expansion V1→V2 at Th, then
75
+ * adiabatic expansion to Tc, isothermal compression at Tc, and adiabatic
76
+ * compression back to the start. V3/V4 are fixed by the closure condition.
77
+ */
78
+ declare function carnotCycle(n: number, Th: number, Tc: number, V1: number, V2: number, gamma?: number): CarnotResult;
79
+ declare const gammaMonatomic: number;
80
+ declare const gammaDiatomic: number;
81
+ //#endregion
82
+ export { CarnotResult, GasState, ProcessKind, ProcessResult, R, carnotCycle, gammaDiatomic, gammaMonatomic, runProcess };
@@ -0,0 +1,133 @@
1
+ //#region src/thermo/index.ts
2
+ /**
3
+ * `thermo` — ideal-gas thermodynamic processes, as a PURE kernel (like `field`:
4
+ * no React, no pixels, no time-stepping). A lab drives it with a slider/progress
5
+ * fraction; the kernel returns the full P–V path of a quasi-static process plus the
6
+ * first-law bookkeeping along it (work, heat, internal energy, entropy).
7
+ *
8
+ * Everything is one ideal gas, PV = nRT, with a degrees-of-freedom ratio γ = Cp/Cv
9
+ * (monatomic 5/3, diatomic 7/5). The four canonical processes and the Carnot cycle
10
+ * all compose from `runProcess`:
11
+ *
12
+ * isothermal T const PV = const W = nRT·ln(V₂/V₁), Q = W, ΔU = 0
13
+ * adiabatic Q = 0 PVᵞ = const W = (P₁V₁−P₂V₂)/(γ−1) = −ΔU, ΔS = 0
14
+ * isobaric P const V/T = const W = PΔV, Q = nCpΔT, ΔU = nCvΔT
15
+ * isochoric V const P/T = const W = 0, Q = ΔU = nCvΔT
16
+ *
17
+ * SI units throughout: P in pascals, V in m³, T in kelvin, energy in joules,
18
+ * entropy in J/K. (Labs convert to kPa / litres for display.)
19
+ */
20
+ /** Universal gas constant, J/(mol·K). */
21
+ const R = 8.314462618;
22
+ const cv = (gamma) => R / (gamma - 1);
23
+ const cp = (gamma) => gamma * R / (gamma - 1);
24
+ /** State completed from any two of (P,V,T) via PV = nRT. */
25
+ const fromPV = (P, V, n) => ({
26
+ P,
27
+ V,
28
+ n,
29
+ T: P * V / (n * R)
30
+ });
31
+ /**
32
+ * Run one quasi-static process from `start` to a target. Give exactly one target
33
+ * coordinate that the process can reach (e.g. a target volume for an isothermal or
34
+ * adiabatic expansion, a target temperature for isobaric/isochoric heating).
35
+ */
36
+ function runProcess(start, kind, target, gamma = 5 / 3, samples = 64) {
37
+ const { n } = start;
38
+ const Cv = cv(gamma), Cp = cp(gamma);
39
+ let end;
40
+ if (kind === "isothermal") {
41
+ const T = start.T;
42
+ const V2 = target.V ?? (target.P != null ? n * 8.314462618 * T / target.P : start.V);
43
+ end = fromPV(n * R * T / V2, V2, n);
44
+ } else if (kind === "adiabatic") {
45
+ const K = start.P * start.V ** gamma;
46
+ const V2 = target.V ?? (target.T != null ? start.V * (start.T / target.T) ** (1 / (gamma - 1)) : start.V);
47
+ end = fromPV(K / V2 ** gamma, V2, n);
48
+ } else if (kind === "isobaric") {
49
+ const P = start.P;
50
+ end = fromPV(P, target.V ?? (target.T != null ? n * 8.314462618 * target.T / P : start.V), n);
51
+ } else {
52
+ const V = start.V;
53
+ end = fromPV(target.P ?? (target.T != null ? n * 8.314462618 * target.T / V : start.P), V, n);
54
+ }
55
+ const path = [];
56
+ for (let i = 0; i <= samples; i++) {
57
+ const u = i / samples;
58
+ let s;
59
+ if (kind === "isochoric") s = fromPV(start.P + (end.P - start.P) * u, start.V, n);
60
+ else {
61
+ const V = start.V + (end.V - start.V) * u;
62
+ if (kind === "isothermal") s = fromPV(n * R * start.T / V, V, n);
63
+ else if (kind === "adiabatic") s = fromPV(start.P * start.V ** gamma / V ** gamma, V, n);
64
+ else s = fromPV(start.P, V, n);
65
+ }
66
+ path.push(s);
67
+ }
68
+ const dU = n * Cv * (end.T - start.T);
69
+ let W, Q, dS;
70
+ if (kind === "isothermal") {
71
+ W = n * R * start.T * Math.log(end.V / start.V);
72
+ Q = W;
73
+ dS = n * R * Math.log(end.V / start.V);
74
+ } else if (kind === "adiabatic") {
75
+ W = (start.P * start.V - end.P * end.V) / (gamma - 1);
76
+ Q = 0;
77
+ dS = 0;
78
+ } else if (kind === "isobaric") {
79
+ W = start.P * (end.V - start.V);
80
+ Q = n * Cp * (end.T - start.T);
81
+ dS = n * Cp * Math.log(end.T / start.T);
82
+ } else {
83
+ W = 0;
84
+ Q = dU;
85
+ dS = n * Cv * Math.log(end.T / start.T);
86
+ }
87
+ return {
88
+ kind,
89
+ start,
90
+ end,
91
+ path,
92
+ W,
93
+ Q,
94
+ dU,
95
+ dS
96
+ };
97
+ }
98
+ /**
99
+ * A Carnot cycle starting at `(V1, Th)`: isothermal expansion V1→V2 at Th, then
100
+ * adiabatic expansion to Tc, isothermal compression at Tc, and adiabatic
101
+ * compression back to the start. V3/V4 are fixed by the closure condition.
102
+ */
103
+ function carnotCycle(n, Th, Tc, V1, V2, gamma = 5 / 3) {
104
+ const ratio = (Th / Tc) ** (1 / (gamma - 1));
105
+ const V3 = V2 * ratio;
106
+ const V4 = V1 * ratio;
107
+ const legHot = runProcess(fromPV(n * R * Th / V1, V1, n), "isothermal", { V: V2 }, gamma);
108
+ const legExp = runProcess(legHot.end, "adiabatic", { V: V3 }, gamma);
109
+ const legCold = runProcess(legExp.end, "isothermal", { V: V4 }, gamma);
110
+ const legComp = runProcess(legCold.end, "adiabatic", { V: V1 }, gamma);
111
+ const Qh = legHot.Q;
112
+ const Qc = -legCold.Q;
113
+ const Wnet = Qh - Qc;
114
+ return {
115
+ legs: [
116
+ legHot,
117
+ legExp,
118
+ legCold,
119
+ legComp
120
+ ],
121
+ Th,
122
+ Tc,
123
+ Qh,
124
+ Qc,
125
+ Wnet,
126
+ efficiency: 1 - Tc / Th
127
+ };
128
+ }
129
+ const gammaMonatomic = 5 / 3;
130
+ const gammaDiatomic = 7 / 5;
131
+
132
+ //#endregion
133
+ export { R, carnotCycle, gammaDiatomic, gammaMonatomic, runProcess };
@@ -0,0 +1,39 @@
1
+ import { ViewBox } from "../core/coords.mjs";
2
+ import { PointerEvent, ReactNode } from "react";
3
+
4
+ //#region src/view/Stage.d.ts
5
+ interface StageProps {
6
+ /** Visible math range. Defaults to [-8,8]×[-5,5]. */
7
+ view?: ViewBox;
8
+ /** Fixed pixel width (otherwise fills container + measures). */
9
+ width?: number;
10
+ /** Fixed pixel height (otherwise derived from aspectRatio or width). */
11
+ height?: number;
12
+ /** width / height, used to derive the missing dimension. */
13
+ aspectRatio?: number;
14
+ /** Inner padding in px. */
15
+ pad?: number;
16
+ /** Preserve aspect ratio (uniform scale). Default true. */
17
+ preserveAspect?: boolean;
18
+ ariaLabel?: string;
19
+ className?: string;
20
+ background?: string;
21
+ /** Pointer → math coordinate callback (tools/builder use this). */
22
+ onPointerMath?: (p: [number, number], e: PointerEvent<SVGSVGElement>) => void;
23
+ children?: ReactNode;
24
+ }
25
+ declare function Stage({
26
+ view,
27
+ width,
28
+ height,
29
+ aspectRatio,
30
+ pad,
31
+ preserveAspect,
32
+ ariaLabel,
33
+ className,
34
+ background,
35
+ onPointerMath,
36
+ children
37
+ }: StageProps): ReactNode;
38
+ //#endregion
39
+ export { Stage, StageProps };
@@ -0,0 +1,91 @@
1
+ 'use client';
2
+
3
+ import { createCoords } from "../core/coords.mjs";
4
+ import { CoordsContext, StageRefContext } from "../core/context.mjs";
5
+ import { useElementSize } from "./useElementSize.mjs";
6
+ import { useMemo, useRef } from "react";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+
9
+ //#region src/view/Stage.tsx
10
+ /**
11
+ * <Stage> — the SVG root. SSR-safe: builds a CoordinateSystem from explicit
12
+ * width/height/aspectRatio (or a default) so children render on first paint;
13
+ * after mount a ResizeObserver swaps in the measured width. There is ONE content
14
+ * group with no transform — every primitive self-positions via coords.toPx().
15
+ * A pixel-space overlay group hosts selection halos/handles (builder phase).
16
+ */
17
+ const DEFAULT_VIEW = {
18
+ xMin: -8,
19
+ xMax: 8,
20
+ yMin: -5,
21
+ yMax: 5
22
+ };
23
+ function Stage({ view = DEFAULT_VIEW, width, height, aspectRatio, pad = 12, preserveAspect = true, ariaLabel = "Interactive figure", className, background = "var(--stage-bg)", onPointerMath, children }) {
24
+ const svgRef = useRef(null);
25
+ const { ref: wrapRef, size } = useElementSize();
26
+ const w = Math.max(1, Math.round(size?.width ?? width ?? 640));
27
+ const h = Math.max(1, Math.round(height ?? (aspectRatio ? w / aspectRatio : Math.max(160, w * .6))));
28
+ const coords = useMemo(() => createCoords(w, h, view, {
29
+ pad,
30
+ preserveAspect
31
+ }), [
32
+ w,
33
+ h,
34
+ view,
35
+ pad,
36
+ preserveAspect
37
+ ]);
38
+ const handlePointer = onPointerMath ? (e) => {
39
+ const ctm = svgRef.current?.getScreenCTM();
40
+ if (!ctm) return;
41
+ const pt = new DOMPoint(e.clientX, e.clientY).matrixTransform(ctm.inverse());
42
+ onPointerMath(coords.toMath(pt.x, pt.y), e);
43
+ } : void 0;
44
+ return /* @__PURE__ */ jsx("div", {
45
+ ref: wrapRef,
46
+ className,
47
+ style: { width: "100%" },
48
+ children: /* @__PURE__ */ jsxs("svg", {
49
+ ref: svgRef,
50
+ width: w,
51
+ height: h,
52
+ viewBox: `0 0 ${w} ${h}`,
53
+ role: "group",
54
+ "aria-label": ariaLabel,
55
+ onPointerDown: handlePointer,
56
+ style: {
57
+ display: "block",
58
+ background,
59
+ borderRadius: 12,
60
+ border: "1px solid var(--stage-grid)",
61
+ userSelect: "none",
62
+ touchAction: "none"
63
+ },
64
+ children: [/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("marker", {
65
+ id: "stage-arrow",
66
+ viewBox: "0 0 10 10",
67
+ refX: "8",
68
+ refY: "5",
69
+ markerWidth: "7",
70
+ markerHeight: "7",
71
+ orient: "auto-start-reverse",
72
+ children: /* @__PURE__ */ jsx("path", {
73
+ d: "M0,0 L10,5 L0,10 z",
74
+ fill: "context-stroke"
75
+ })
76
+ }) }), /* @__PURE__ */ jsx(CoordsContext.Provider, {
77
+ value: coords,
78
+ children: /* @__PURE__ */ jsxs(StageRefContext.Provider, {
79
+ value: svgRef,
80
+ children: [/* @__PURE__ */ jsx("g", {
81
+ "data-stage-content": true,
82
+ children
83
+ }), /* @__PURE__ */ jsx("g", { "data-stage-overlay": true })]
84
+ })
85
+ })]
86
+ })
87
+ });
88
+ }
89
+
90
+ //#endregion
91
+ export { Stage };
@@ -0,0 +1,4 @@
1
+ import { Stage, StageProps } from "./Stage.mjs";
2
+ import { Size, useElementSize } from "./useElementSize.mjs";
3
+ import { useInView } from "./useInView.mjs";
4
+ export { type Size, Stage, type StageProps, useElementSize, useInView };
@@ -0,0 +1,5 @@
1
+ import { useElementSize } from "./useElementSize.mjs";
2
+ import { Stage } from "./Stage.mjs";
3
+ import { useInView } from "./useInView.mjs";
4
+
5
+ export { Stage, useElementSize, useInView };
@@ -0,0 +1,13 @@
1
+ import { RefObject } from "react";
2
+
3
+ //#region src/view/useElementSize.d.ts
4
+ interface Size {
5
+ width: number;
6
+ height: number;
7
+ }
8
+ declare function useElementSize<T extends HTMLElement>(): {
9
+ ref: RefObject<T | null>;
10
+ size: Size | null;
11
+ };
12
+ //#endregion
13
+ export { Size, useElementSize };