@classytic/stage 0.2.0 → 0.4.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 (458) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +5 -1
  3. package/dist/_virtual/_rolldown/runtime.mjs +18 -0
  4. package/dist/assets/index.mjs +0 -1
  5. package/dist/atomic/aufbau.d.mts +144 -0
  6. package/dist/atomic/aufbau.mjs +384 -0
  7. package/dist/atomic/elements.d.mts +67 -0
  8. package/dist/atomic/elements.mjs +785 -0
  9. package/dist/atomic/emission.d.mts +112 -0
  10. package/dist/atomic/emission.mjs +179 -0
  11. package/dist/atomic/index.d.mts +8 -0
  12. package/dist/atomic/index.mjs +9 -0
  13. package/dist/atomic/periodic.d.mts +82 -0
  14. package/dist/atomic/periodic.mjs +231 -0
  15. package/dist/atomic/properties.d.mts +100 -0
  16. package/dist/atomic/properties.mjs +574 -0
  17. package/dist/atomic/shells.d.mts +137 -0
  18. package/dist/atomic/shells.mjs +170 -0
  19. package/dist/atomic/trends.d.mts +148 -0
  20. package/dist/atomic/trends.mjs +225 -0
  21. package/dist/builder/Palette.mjs +50 -89
  22. package/dist/builder/SceneBuilder.mjs +15 -73
  23. package/dist/chem/chromatography.d.mts +66 -0
  24. package/dist/chem/chromatography.mjs +63 -0
  25. package/dist/chem/complex.d.mts +114 -0
  26. package/dist/chem/complex.mjs +234 -0
  27. package/dist/chem/cycle.d.mts +75 -0
  28. package/dist/chem/cycle.mjs +145 -0
  29. package/dist/chem/equation.d.mts +86 -0
  30. package/dist/chem/equation.mjs +168 -0
  31. package/dist/chem/formula.d.mts +90 -0
  32. package/dist/chem/formula.mjs +201 -0
  33. package/dist/chem/functional-groups.d.mts +31 -0
  34. package/dist/chem/functional-groups.mjs +187 -0
  35. package/dist/chem/hazard.d.mts +94 -0
  36. package/dist/chem/hazard.mjs +329 -0
  37. package/dist/chem/index.d.mts +34 -33
  38. package/dist/chem/index.mjs +34 -28
  39. package/dist/chem/ionic.d.mts +98 -0
  40. package/dist/chem/ionic.mjs +156 -0
  41. package/dist/chem/layout.d.mts +46 -0
  42. package/dist/chem/layout.mjs +342 -0
  43. package/dist/chem/lewis.d.mts +79 -0
  44. package/dist/chem/lewis.mjs +163 -0
  45. package/dist/chem/molecular-orbital.d.mts +114 -0
  46. package/dist/chem/molecular-orbital.mjs +201 -0
  47. package/dist/chem/molecule.d.mts +154 -0
  48. package/dist/chem/molecule.mjs +282 -0
  49. package/dist/chem/nmr.d.mts +153 -0
  50. package/dist/chem/nmr.mjs +165 -0
  51. package/dist/chem/polymer.d.mts +65 -0
  52. package/dist/chem/polymer.mjs +179 -0
  53. package/dist/chem/redox.d.mts +138 -0
  54. package/dist/chem/redox.mjs +328 -0
  55. package/dist/chem/relations.d.mts +183 -0
  56. package/dist/chem/relations.mjs +299 -0
  57. package/dist/chem/skeleton.d.mts +51 -0
  58. package/dist/chem/skeleton.mjs +186 -0
  59. package/dist/chem/solution.d.mts +116 -0
  60. package/dist/chem/solution.mjs +186 -0
  61. package/dist/chem/species.d.mts +47 -0
  62. package/dist/chem/species.mjs +128 -0
  63. package/dist/chem/stereo.d.mts +108 -0
  64. package/dist/chem/stereo.mjs +286 -0
  65. package/dist/chem/stoichiometry.d.mts +95 -0
  66. package/dist/chem/stoichiometry.mjs +145 -0
  67. package/dist/chem/synthesis.d.mts +113 -0
  68. package/dist/chem/synthesis.mjs +281 -0
  69. package/dist/chem/vsepr.d.mts +80 -0
  70. package/dist/chem/vsepr.mjs +247 -0
  71. package/dist/circuit/ac.d.mts +163 -0
  72. package/dist/circuit/ac.mjs +452 -0
  73. package/dist/circuit/dc.d.mts +92 -0
  74. package/dist/circuit/dc.mjs +333 -0
  75. package/dist/circuit/index.d.mts +5 -92
  76. package/dist/circuit/index.mjs +5 -332
  77. package/dist/circuit/network.d.mts +99 -0
  78. package/dist/circuit/network.mjs +440 -0
  79. package/dist/circuit/topology.d.mts +102 -0
  80. package/dist/circuit/topology.mjs +143 -0
  81. package/dist/colour/absorption.d.mts +79 -0
  82. package/dist/colour/absorption.mjs +137 -0
  83. package/dist/colour/index.d.mts +6 -0
  84. package/dist/colour/index.mjs +7 -0
  85. package/dist/colour/name.d.mts +14 -0
  86. package/dist/colour/name.mjs +76 -0
  87. package/dist/colour/observer.d.mts +31 -0
  88. package/dist/colour/observer.mjs +46 -0
  89. package/dist/colour/spectrum.d.mts +61 -0
  90. package/dist/colour/spectrum.mjs +108 -0
  91. package/dist/colour/srgb.d.mts +49 -0
  92. package/dist/colour/srgb.mjs +108 -0
  93. package/dist/core/constants.d.mts +138 -0
  94. package/dist/core/constants.mjs +139 -0
  95. package/dist/core/index.d.mts +4 -1
  96. package/dist/core/index.mjs +4 -1
  97. package/dist/core/math.d.mts +26 -0
  98. package/dist/core/math.mjs +37 -0
  99. package/dist/core/vec3.d.mts +94 -0
  100. package/dist/core/vec3.mjs +123 -0
  101. package/dist/core/worked.d.mts +35 -0
  102. package/dist/core/worked.mjs +44 -0
  103. package/dist/electro/cell.d.mts +102 -0
  104. package/dist/electro/cell.mjs +170 -0
  105. package/dist/electro/conductivity.d.mts +157 -0
  106. package/dist/electro/conductivity.mjs +156 -0
  107. package/dist/electro/couples.d.mts +114 -0
  108. package/dist/electro/couples.mjs +334 -0
  109. package/dist/electro/electrolysis.d.mts +96 -0
  110. package/dist/electro/electrolysis.mjs +274 -0
  111. package/dist/electro/faraday.d.mts +150 -0
  112. package/dist/electro/faraday.mjs +118 -0
  113. package/dist/electro/index.d.mts +6 -0
  114. package/dist/electro/index.mjs +7 -0
  115. package/dist/finance/accounting/adjustments.d.mts +17 -0
  116. package/dist/finance/accounting/adjustments.mjs +202 -0
  117. package/dist/finance/accounting/analysis.d.mts +8 -0
  118. package/dist/finance/accounting/analysis.mjs +205 -0
  119. package/dist/finance/accounting/clubs.d.mts +21 -0
  120. package/dist/finance/accounting/clubs.mjs +217 -0
  121. package/dist/finance/accounting/company-statements.d.mts +7 -0
  122. package/dist/finance/accounting/company-statements.mjs +150 -0
  123. package/dist/finance/accounting/harness.d.mts +7 -0
  124. package/dist/finance/accounting/harness.mjs +45 -0
  125. package/dist/finance/accounting/index.d.mts +18 -0
  126. package/dist/finance/accounting/index.mjs +18 -0
  127. package/dist/finance/accounting/journalize.d.mts +16 -0
  128. package/dist/finance/accounting/journalize.mjs +128 -0
  129. package/dist/finance/accounting/manufacturing.d.mts +8 -0
  130. package/dist/finance/accounting/manufacturing.mjs +236 -0
  131. package/dist/finance/accounting/partnership.d.mts +7 -0
  132. package/dist/finance/accounting/partnership.mjs +152 -0
  133. package/dist/finance/accounting/period-case.d.mts +6 -0
  134. package/dist/finance/accounting/period-case.mjs +71 -0
  135. package/dist/finance/accounting/post.d.mts +9 -0
  136. package/dist/finance/accounting/post.mjs +84 -0
  137. package/dist/finance/accounting/precision.d.mts +9 -0
  138. package/dist/finance/accounting/precision.mjs +15 -0
  139. package/dist/finance/accounting/rules.d.mts +21 -0
  140. package/dist/finance/accounting/rules.mjs +37 -0
  141. package/dist/finance/accounting/solve.d.mts +28 -0
  142. package/dist/finance/accounting/solve.mjs +92 -0
  143. package/dist/finance/accounting/statements.d.mts +7 -0
  144. package/dist/finance/accounting/statements.mjs +142 -0
  145. package/dist/finance/accounting/types.d.mts +649 -0
  146. package/dist/finance/accounting/validate.d.mts +7 -0
  147. package/dist/finance/accounting/validate.mjs +67 -0
  148. package/dist/finance/accounting/verification.d.mts +24 -0
  149. package/dist/finance/accounting/verification.mjs +386 -0
  150. package/dist/finance/bizsim.d.mts +75 -0
  151. package/dist/finance/bizsim.mjs +296 -0
  152. package/dist/finance/index.d.mts +95 -0
  153. package/dist/finance/index.mjs +145 -0
  154. package/dist/finance/investment-math.d.mts +7 -0
  155. package/dist/finance/investment-math.mjs +18 -0
  156. package/dist/finance/management-accounting/budgeting.d.mts +7 -0
  157. package/dist/finance/management-accounting/budgeting.mjs +189 -0
  158. package/dist/finance/management-accounting/cash-budget.d.mts +7 -0
  159. package/dist/finance/management-accounting/cash-budget.mjs +154 -0
  160. package/dist/finance/management-accounting/costing.d.mts +8 -0
  161. package/dist/finance/management-accounting/costing.mjs +175 -0
  162. package/dist/finance/management-accounting/decisions.d.mts +8 -0
  163. package/dist/finance/management-accounting/decisions.mjs +164 -0
  164. package/dist/finance/management-accounting/index.d.mts +13 -0
  165. package/dist/finance/management-accounting/index.mjs +13 -0
  166. package/dist/finance/management-accounting/integrated-budget.d.mts +8 -0
  167. package/dist/finance/management-accounting/integrated-budget.mjs +67 -0
  168. package/dist/finance/management-accounting/inventory-costing.d.mts +7 -0
  169. package/dist/finance/management-accounting/inventory-costing.mjs +173 -0
  170. package/dist/finance/management-accounting/investment-appraisal.d.mts +7 -0
  171. package/dist/finance/management-accounting/investment-appraisal.mjs +148 -0
  172. package/dist/finance/management-accounting/management-case.d.mts +8 -0
  173. package/dist/finance/management-accounting/management-case.mjs +91 -0
  174. package/dist/finance/management-accounting/master-budget.d.mts +7 -0
  175. package/dist/finance/management-accounting/master-budget.mjs +137 -0
  176. package/dist/finance/management-accounting/production-costing.d.mts +8 -0
  177. package/dist/finance/management-accounting/production-costing.mjs +214 -0
  178. package/dist/finance/management-accounting/types.d.mts +644 -0
  179. package/dist/finance/management-accounting/variances.d.mts +7 -0
  180. package/dist/finance/management-accounting/variances.mjs +150 -0
  181. package/dist/index.d.mts +9 -8
  182. package/dist/index.mjs +5 -5
  183. package/dist/interaction/MovableDot.mjs +19 -0
  184. package/dist/interaction/useDraggable.mjs +24 -4
  185. package/dist/math/analysis.d.mts +61 -0
  186. package/dist/math/analysis.mjs +187 -0
  187. package/dist/math/answer-check.d.mts +50 -0
  188. package/dist/math/answer-check.mjs +106 -0
  189. package/dist/math/calculus.d.mts +17 -1
  190. package/dist/math/calculus.mjs +365 -2
  191. package/dist/math/complex.d.mts +93 -0
  192. package/dist/math/complex.mjs +239 -0
  193. package/dist/math/expr.d.mts +23 -0
  194. package/dist/math/expr.mjs +36 -0
  195. package/dist/math/index.d.mts +9 -23
  196. package/dist/math/index.mjs +9 -38
  197. package/dist/math/latex.mjs +10 -1
  198. package/dist/math/parse.mjs +1 -1
  199. package/dist/math/poly.d.mts +55 -0
  200. package/dist/math/poly.mjs +273 -0
  201. package/dist/math/rearrange.d.mts +14 -0
  202. package/dist/math/rearrange.mjs +125 -0
  203. package/dist/math/solve.d.mts +21 -0
  204. package/dist/math/solve.mjs +75 -0
  205. package/dist/networking/addressing.d.mts +26 -0
  206. package/dist/networking/addressing.mjs +33 -0
  207. package/dist/networking/cloud/access.d.mts +36 -0
  208. package/dist/networking/cloud/access.mjs +95 -0
  209. package/dist/networking/cloud/containers.d.mts +43 -0
  210. package/dist/networking/cloud/containers.mjs +54 -0
  211. package/dist/networking/cloud/edge.d.mts +17 -0
  212. package/dist/networking/cloud/edge.mjs +31 -0
  213. package/dist/networking/cloud/failure-domains.d.mts +19 -0
  214. package/dist/networking/cloud/failure-domains.mjs +22 -0
  215. package/dist/networking/cloud/geo-routing.d.mts +22 -0
  216. package/dist/networking/cloud/geo-routing.mjs +64 -0
  217. package/dist/networking/cloud/index.d.mts +18 -0
  218. package/dist/networking/cloud/index.mjs +19 -0
  219. package/dist/networking/cloud/inference-reliability.d.mts +30 -0
  220. package/dist/networking/cloud/inference-reliability.mjs +76 -0
  221. package/dist/networking/cloud/inference.d.mts +118 -0
  222. package/dist/networking/cloud/inference.mjs +179 -0
  223. package/dist/networking/cloud/infrastructure.d.mts +45 -0
  224. package/dist/networking/cloud/infrastructure.mjs +96 -0
  225. package/dist/networking/cloud/observability.d.mts +24 -0
  226. package/dist/networking/cloud/observability.mjs +34 -0
  227. package/dist/networking/cloud/orchestration.d.mts +32 -0
  228. package/dist/networking/cloud/orchestration.mjs +38 -0
  229. package/dist/networking/cloud/recovery.d.mts +21 -0
  230. package/dist/networking/cloud/recovery.mjs +27 -0
  231. package/dist/networking/cloud/releases.d.mts +42 -0
  232. package/dist/networking/cloud/releases.mjs +135 -0
  233. package/dist/networking/cloud/scaling.d.mts +25 -0
  234. package/dist/networking/cloud/scaling.mjs +65 -0
  235. package/dist/networking/cloud/secrets.d.mts +39 -0
  236. package/dist/networking/cloud/secrets.mjs +68 -0
  237. package/dist/networking/cloud/server-deployment.d.mts +29 -0
  238. package/dist/networking/cloud/server-deployment.mjs +72 -0
  239. package/dist/networking/cloud/storage.d.mts +16 -0
  240. package/dist/networking/cloud/storage.mjs +58 -0
  241. package/dist/networking/cloud/vpc.d.mts +18 -0
  242. package/dist/networking/cloud/vpc.mjs +100 -0
  243. package/dist/networking/delivery.d.mts +34 -0
  244. package/dist/networking/delivery.mjs +52 -0
  245. package/dist/networking/index.d.mts +47 -0
  246. package/dist/networking/index.mjs +47 -0
  247. package/dist/networking/ipv6.d.mts +28 -0
  248. package/dist/networking/ipv6.mjs +17 -0
  249. package/dist/networking/journeys.d.mts +83 -0
  250. package/dist/networking/journeys.mjs +161 -0
  251. package/dist/networking/models/airtime.d.mts +87 -0
  252. package/dist/networking/models/airtime.mjs +130 -0
  253. package/dist/networking/models/cellular.d.mts +157 -0
  254. package/dist/networking/models/cellular.mjs +252 -0
  255. package/dist/networking/models/encapsulation.d.mts +98 -0
  256. package/dist/networking/models/encapsulation.mjs +132 -0
  257. package/dist/networking/models/handover.d.mts +116 -0
  258. package/dist/networking/models/handover.mjs +209 -0
  259. package/dist/networking/models/handshake.d.mts +90 -0
  260. package/dist/networking/models/handshake.mjs +122 -0
  261. package/dist/networking/models/index.d.mts +14 -0
  262. package/dist/networking/models/index.mjs +15 -0
  263. package/dist/networking/models/media.d.mts +82 -0
  264. package/dist/networking/models/media.mjs +188 -0
  265. package/dist/networking/models/satellite.d.mts +86 -0
  266. package/dist/networking/models/satellite.mjs +121 -0
  267. package/dist/networking/models/spectrum.d.mts +83 -0
  268. package/dist/networking/models/spectrum.mjs +235 -0
  269. package/dist/networking/models/switch.d.mts +80 -0
  270. package/dist/networking/models/switch.mjs +155 -0
  271. package/dist/networking/models/vlan.d.mts +62 -0
  272. package/dist/networking/models/vlan.mjs +100 -0
  273. package/dist/networking/models/vpn.d.mts +66 -0
  274. package/dist/networking/models/vpn.mjs +125 -0
  275. package/dist/networking/models/wifi-privacy.d.mts +82 -0
  276. package/dist/networking/models/wifi-privacy.mjs +130 -0
  277. package/dist/networking/models/wifi.d.mts +240 -0
  278. package/dist/networking/models/wifi.mjs +562 -0
  279. package/dist/networking/nat-traversal.d.mts +24 -0
  280. package/dist/networking/nat-traversal.mjs +38 -0
  281. package/dist/networking/protocols.d.mts +17 -0
  282. package/dist/networking/protocols.mjs +57 -0
  283. package/dist/networking/queue.d.mts +12 -0
  284. package/dist/networking/queue.mjs +18 -0
  285. package/dist/networking/route-learning.d.mts +23 -0
  286. package/dist/networking/route-learning.mjs +43 -0
  287. package/dist/networking/routing.d.mts +11 -0
  288. package/dist/networking/routing.mjs +25 -0
  289. package/dist/networking/signals.d.mts +26 -0
  290. package/dist/networking/signals.mjs +35 -0
  291. package/dist/networking/subnet.d.mts +49 -0
  292. package/dist/networking/subnet.mjs +89 -0
  293. package/dist/networking/tcp-reliability.d.mts +24 -0
  294. package/dist/networking/tcp-reliability.mjs +38 -0
  295. package/dist/networking/topology/contract.d.mts +123 -0
  296. package/dist/networking/topology/index.d.mts +4 -0
  297. package/dist/networking/topology/index.mjs +4 -0
  298. package/dist/networking/topology/presets.d.mts +7 -0
  299. package/dist/networking/topology/presets.mjs +302 -0
  300. package/dist/networking/topology/simulation.d.mts +8 -0
  301. package/dist/networking/topology/simulation.mjs +179 -0
  302. package/dist/networking/wireless.d.mts +11 -0
  303. package/dist/networking/wireless.mjs +17 -0
  304. package/dist/optics/imaging.d.mts +218 -0
  305. package/dist/optics/imaging.mjs +276 -0
  306. package/dist/optics/index.d.mts +7 -0
  307. package/dist/optics/index.mjs +8 -0
  308. package/dist/optics/laws.d.mts +70 -0
  309. package/dist/optics/laws.mjs +195 -0
  310. package/dist/optics/prism.d.mts +148 -0
  311. package/dist/optics/prism.mjs +197 -0
  312. package/dist/optics/refraction.d.mts +201 -0
  313. package/dist/optics/refraction.mjs +270 -0
  314. package/dist/optics/wave-optics.d.mts +171 -0
  315. package/dist/optics/wave-optics.mjs +259 -0
  316. package/dist/physics/ac/index.d.mts +183 -0
  317. package/dist/physics/ac/index.mjs +203 -0
  318. package/dist/physics/dynamics/forces.d.mts +98 -0
  319. package/dist/physics/dynamics/forces.mjs +153 -0
  320. package/dist/physics/dynamics/friction.d.mts +76 -0
  321. package/dist/physics/dynamics/friction.mjs +119 -0
  322. package/dist/physics/dynamics/system.d.mts +68 -0
  323. package/dist/physics/dynamics/system.mjs +125 -0
  324. package/dist/physics/electrostatics/index.d.mts +169 -0
  325. package/dist/physics/electrostatics/index.mjs +249 -0
  326. package/dist/physics/energy/centre-of-mass.d.mts +115 -0
  327. package/dist/physics/energy/centre-of-mass.mjs +180 -0
  328. package/dist/physics/energy/conservation.d.mts +81 -0
  329. package/dist/physics/energy/conservation.mjs +129 -0
  330. package/dist/physics/energy/power.d.mts +90 -0
  331. package/dist/physics/energy/power.mjs +167 -0
  332. package/dist/physics/energy/spring.d.mts +73 -0
  333. package/dist/physics/energy/spring.mjs +129 -0
  334. package/dist/physics/energy/store.d.mts +63 -0
  335. package/dist/physics/energy/store.mjs +108 -0
  336. package/dist/physics/energy/theorem.d.mts +65 -0
  337. package/dist/physics/energy/theorem.mjs +113 -0
  338. package/dist/physics/energy/work.d.mts +114 -0
  339. package/dist/physics/energy/work.mjs +219 -0
  340. package/dist/physics/gravitation/body.d.mts +85 -0
  341. package/dist/physics/gravitation/body.mjs +168 -0
  342. package/dist/physics/gravitation/kepler.d.mts +127 -0
  343. package/dist/physics/gravitation/kepler.mjs +216 -0
  344. package/dist/physics/gravitation/law.d.mts +117 -0
  345. package/dist/physics/gravitation/law.mjs +175 -0
  346. package/dist/physics/gravitation/orbit.d.mts +119 -0
  347. package/dist/physics/gravitation/orbit.mjs +202 -0
  348. package/dist/physics/gravitation/potential.d.mts +114 -0
  349. package/dist/physics/gravitation/potential.mjs +202 -0
  350. package/dist/physics/gravitation/variation.d.mts +97 -0
  351. package/dist/physics/gravitation/variation.mjs +175 -0
  352. package/dist/physics/index.d.mts +35 -0
  353. package/dist/physics/index.mjs +36 -0
  354. package/dist/physics/integral-sweep.d.mts +63 -0
  355. package/dist/physics/integral-sweep.mjs +126 -0
  356. package/dist/physics/kinematics/equations.d.mts +47 -0
  357. package/dist/physics/kinematics/equations.mjs +126 -0
  358. package/dist/physics/kinematics/free-fall.d.mts +53 -0
  359. package/dist/physics/kinematics/free-fall.mjs +71 -0
  360. package/dist/physics/kinematics/given.d.mts +16 -0
  361. package/dist/physics/kinematics/given.mjs +55 -0
  362. package/dist/physics/kinematics/roots.d.mts +41 -0
  363. package/dist/physics/kinematics/roots.mjs +72 -0
  364. package/dist/physics/kinematics/solve.d.mts +52 -0
  365. package/dist/physics/kinematics/solve.mjs +172 -0
  366. package/dist/physics/magnetism/biot-savart.d.mts +161 -0
  367. package/dist/physics/magnetism/biot-savart.mjs +353 -0
  368. package/dist/physics/magnetism/flux.d.mts +94 -0
  369. package/dist/physics/magnetism/flux.mjs +313 -0
  370. package/dist/physics/magnetism/geomagnetism.d.mts +151 -0
  371. package/dist/physics/magnetism/geomagnetism.mjs +184 -0
  372. package/dist/physics/magnetism/index.d.mts +12 -0
  373. package/dist/physics/magnetism/index.mjs +74 -0
  374. package/dist/physics/magnetism/lorentz.d.mts +28 -0
  375. package/dist/physics/magnetism/lorentz.mjs +88 -0
  376. package/dist/physics/magnetism/magnetic-force.d.mts +102 -0
  377. package/dist/physics/magnetism/magnetic-force.mjs +169 -0
  378. package/dist/physics/momentum/collide.d.mts +77 -0
  379. package/dist/physics/momentum/collide.mjs +115 -0
  380. package/dist/physics/momentum/impulse.d.mts +89 -0
  381. package/dist/physics/momentum/impulse.mjs +159 -0
  382. package/dist/physics/momentum/planar.d.mts +60 -0
  383. package/dist/physics/momentum/planar.mjs +110 -0
  384. package/dist/physics/projectile.d.mts +107 -0
  385. package/dist/physics/projectile.mjs +185 -0
  386. package/dist/physics/rotation/angular.d.mts +85 -0
  387. package/dist/physics/rotation/angular.mjs +130 -0
  388. package/dist/physics/rotation/circular.d.mts +79 -0
  389. package/dist/physics/rotation/circular.mjs +131 -0
  390. package/dist/physics/rotation/inertia.d.mts +164 -0
  391. package/dist/physics/rotation/inertia.mjs +332 -0
  392. package/dist/physics/rotation/torque.d.mts +65 -0
  393. package/dist/physics/rotation/torque.mjs +134 -0
  394. package/dist/physics/vectors.d.mts +64 -0
  395. package/dist/physics/vectors.mjs +114 -0
  396. package/dist/primitives/Dot.d.mts +2 -15
  397. package/dist/primitives/Dot.mjs +6 -4
  398. package/dist/primitives/Grid.d.mts +33 -17
  399. package/dist/primitives/Grid.mjs +89 -17
  400. package/dist/primitives/Label.d.mts +1 -14
  401. package/dist/primitives/Label.mjs +3 -2
  402. package/dist/primitives/Lines.d.mts +4 -32
  403. package/dist/primitives/Lines.mjs +10 -8
  404. package/dist/primitives/Shapes.d.mts +5 -43
  405. package/dist/primitives/Shapes.mjs +12 -10
  406. package/dist/primitives/index.d.mts +2 -2
  407. package/dist/primitives/index.mjs +2 -2
  408. package/dist/primitives/props.mjs +31 -0
  409. package/dist/quantity/index.d.mts +119 -0
  410. package/dist/quantity/index.mjs +417 -0
  411. package/dist/relation/derive.d.mts +74 -0
  412. package/dist/relation/derive.mjs +73 -0
  413. package/dist/relation/index.d.mts +56 -0
  414. package/dist/relation/index.mjs +234 -0
  415. package/dist/scene/Scene.d.mts +6 -1
  416. package/dist/scene/Scene.mjs +15 -40
  417. package/dist/scene/evaluators.mjs +1 -1
  418. package/dist/sim/thermal.mjs +45 -20
  419. package/dist/thermo/calorimetry.d.mts +207 -0
  420. package/dist/thermo/calorimetry.mjs +345 -0
  421. package/dist/thermo/cycles.d.mts +142 -0
  422. package/dist/thermo/cycles.mjs +256 -0
  423. package/dist/thermo/entropy.d.mts +87 -0
  424. package/dist/thermo/entropy.mjs +140 -0
  425. package/dist/thermo/expansion.d.mts +130 -0
  426. package/dist/thermo/expansion.mjs +178 -0
  427. package/dist/thermo/gas.d.mts +197 -0
  428. package/dist/thermo/gas.mjs +279 -0
  429. package/dist/thermo/heat-transfer.d.mts +164 -0
  430. package/dist/thermo/heat-transfer.mjs +217 -0
  431. package/dist/thermo/index.d.mts +9 -82
  432. package/dist/thermo/index.mjs +9 -132
  433. package/dist/thermo/processes.d.mts +95 -0
  434. package/dist/thermo/processes.mjs +154 -0
  435. package/dist/thermo/temperature.d.mts +174 -0
  436. package/dist/thermo/temperature.mjs +236 -0
  437. package/dist/view/Stage.mjs +4 -11
  438. package/dist/waves/damping.d.mts +224 -0
  439. package/dist/waves/damping.mjs +317 -0
  440. package/dist/waves/doppler.d.mts +72 -0
  441. package/dist/waves/doppler.mjs +125 -0
  442. package/dist/waves/index.d.mts +10 -0
  443. package/dist/waves/index.mjs +10 -0
  444. package/dist/waves/pendulum.d.mts +130 -0
  445. package/dist/waves/pendulum.mjs +211 -0
  446. package/dist/waves/pitch.d.mts +125 -0
  447. package/dist/waves/pitch.mjs +158 -0
  448. package/dist/waves/shm.d.mts +170 -0
  449. package/dist/waves/shm.mjs +279 -0
  450. package/dist/waves/springs.d.mts +103 -0
  451. package/dist/waves/springs.mjs +176 -0
  452. package/dist/waves/superposition.d.mts +124 -0
  453. package/dist/waves/superposition.mjs +196 -0
  454. package/dist/waves/wave.d.mts +154 -0
  455. package/dist/waves/wave.mjs +178 -0
  456. package/package.json +103 -23
  457. package/styles.css +125 -8
  458. package/dist/assets/kit/index.mjs +0 -4
@@ -0,0 +1,168 @@
1
+ import { readFormula } from "./species.mjs";
2
+
3
+ //#region src/chem/equation.ts
4
+ /**
5
+ * Strip a trailing state symbol.
6
+ *
7
+ * `(aq)` looks exactly like a bracketed group to the formula reader, which would take the a and the
8
+ * q for elements. It has to come off before the formula is read, and only from the END, because
9
+ * `Fe(OH)3` has a bracketed group in the middle that must survive.
10
+ */
11
+ const STATE_SYMBOL = /\((?:g|l|s|aq)\)\s*$/i;
12
+ const withoutState = (species) => species.replace(STATE_SYMBOL, "").trim();
13
+ const gcd = (a, b) => b === 0 ? Math.abs(a) : gcd(b, a % b);
14
+ function fraction(n, d = 1) {
15
+ if (d === 0) throw new Error("a fraction cannot have a denominator of zero");
16
+ const sign = d < 0 ? -1 : 1;
17
+ const divisor = gcd(n, d) || 1;
18
+ return {
19
+ n: sign * n / divisor,
20
+ d: sign * d / divisor
21
+ };
22
+ }
23
+ const sub = (a, b) => fraction(a.n * b.d - b.n * a.d, a.d * b.d);
24
+ const mul = (a, b) => fraction(a.n * b.n, a.d * b.d);
25
+ const div = (a, b) => {
26
+ if (b.n === 0) throw new Error("cannot divide by zero while balancing");
27
+ return fraction(a.n * b.d, a.d * b.n);
28
+ };
29
+ const isZero = (f) => f.n === 0;
30
+ const ZERO = {
31
+ n: 0,
32
+ d: 1
33
+ };
34
+ const ONE = {
35
+ n: 1,
36
+ d: 1
37
+ };
38
+ /** Split "a + b -> c + d" into its two sides, accepting the arrows people actually type. */
39
+ function splitEquation(input) {
40
+ const parts = input.split(/->|=>|→|⟶|=/);
41
+ if (parts.length !== 2) throw new Error(`"${input}" is not an equation: it needs exactly one arrow, written as -> or = or →`);
42
+ const side = (text) => text.split(/\s+\+\s+/).map((s) => s.trim()).map((s) => s.replace(/^\d+(?=[A-Z([])/, "")).filter((s) => s !== "");
43
+ const left = side(parts[0]);
44
+ const right = side(parts[1]);
45
+ if (left.length === 0 || right.length === 0) throw new Error(`"${input}" has nothing on one side of the arrow`);
46
+ return {
47
+ left,
48
+ right
49
+ };
50
+ }
51
+ /**
52
+ * Balance an equation, returning the smallest whole-number coefficients.
53
+ *
54
+ * `balance('Fe3O4 + CO -> Fe + CO2')`, `balance('C3H8 + O2 -> CO2 + H2O')`.
55
+ *
56
+ * Coefficients already written into the input are ignored: the whole point is to compute them, and
57
+ * accepting them would let a wrong one through unchallenged.
58
+ */
59
+ function balance(input) {
60
+ const { left, right } = splitEquation(input);
61
+ const species = [...left, ...right];
62
+ const read = species.map((formula) => readFormula(withoutState(formula)));
63
+ const compositions = read.map((r) => r.counts);
64
+ const elements = [...new Set(compositions.flatMap((c) => c.map((e) => e.element)))];
65
+ /** Reactants count positive and products negative, so a balance sums to zero. */
66
+ const signed = (column, value) => fraction(column < left.length ? value : -value);
67
+ const rows = elements.map((element) => compositions.map((composition, column) => signed(column, composition.find((e) => e.element === element)?.count ?? 0)));
68
+ if (read.some((r) => r.charge !== 0)) rows.push(read.map((r, column) => signed(column, r.charge)));
69
+ const whole = clearFractions(nullSpaceVector(rows, species.length, input));
70
+ if (whole.some((value) => value <= 0)) throw new Error(`${input} cannot be balanced as written: the only solution needs a coefficient of zero or below, which usually means a species is on the wrong side or one is missing`);
71
+ return {
72
+ reactants: left.map((formula, i) => ({
73
+ formula,
74
+ coefficient: whole[i]
75
+ })),
76
+ products: right.map((formula, i) => ({
77
+ formula,
78
+ coefficient: whole[left.length + i]
79
+ })),
80
+ elements
81
+ };
82
+ }
83
+ /**
84
+ * The one direction the element matrix sends to zero.
85
+ *
86
+ * Gaussian elimination to reduced row echelon form, then the single free column is set to one and
87
+ * the others read off. More than one free column means the equation has more than one independent
88
+ * balance, which is refused with that as the reason rather than by picking one of them.
89
+ */
90
+ function nullSpaceVector(rows, columns, input) {
91
+ const matrix = rows.map((row) => [...row]);
92
+ const pivotOf = [];
93
+ let pivotRow = 0;
94
+ for (let column = 0; column < columns && pivotRow < matrix.length; column += 1) {
95
+ const found = matrix.findIndex((row, i) => i >= pivotRow && !isZero(row[column]));
96
+ if (found === -1) continue;
97
+ [matrix[pivotRow], matrix[found]] = [matrix[found], matrix[pivotRow]];
98
+ const pivot = matrix[pivotRow][column];
99
+ matrix[pivotRow] = matrix[pivotRow].map((value) => div(value, pivot));
100
+ for (let r = 0; r < matrix.length; r += 1) {
101
+ if (r === pivotRow || isZero(matrix[r][column])) continue;
102
+ const factor = matrix[r][column];
103
+ matrix[r] = matrix[r].map((value, c) => sub(value, mul(factor, matrix[pivotRow][c])));
104
+ }
105
+ pivotOf.push(column);
106
+ pivotRow += 1;
107
+ }
108
+ const free = [...Array(columns).keys()].filter((c) => !pivotOf.includes(c));
109
+ if (free.length === 0) throw new Error(`${input} cannot be balanced: no set of coefficients conserves every element, so a species is missing or a formula is wrong`);
110
+ if (free.length > 1) throw new Error(`${input} can be balanced in more than one independent way, which means it describes more than one reaction. Split it into separate equations.`);
111
+ const solution = Array(columns).fill(ZERO);
112
+ solution[free[0]] = ONE;
113
+ pivotOf.forEach((column, row) => {
114
+ solution[column] = mul(matrix[row][free[0]], {
115
+ n: -1,
116
+ d: 1
117
+ });
118
+ });
119
+ return solution;
120
+ }
121
+ /** Scale a set of fractions to the smallest whole numbers with the same ratios. */
122
+ function clearFractions(values) {
123
+ const lcm = (a, b) => Math.abs(a * b) / (gcd(a, b) || 1);
124
+ const denominator = values.reduce((total, f) => lcm(total, f.d), 1);
125
+ const scaled = values.map((f) => f.n * denominator / f.d);
126
+ const divisor = scaled.reduce((total, v) => gcd(total, v), 0) || 1;
127
+ return scaled.map((v) => v / divisor);
128
+ }
129
+ /** Write a balanced equation out, leaving a coefficient of one unwritten. */
130
+ function formatEquation(equation, arrow = "->") {
131
+ const side = (terms) => terms.map((t) => `${t.coefficient === 1 ? "" : t.coefficient}${t.formula}`).join(" + ");
132
+ return `${side(equation.reactants)} ${arrow} ${side(equation.products)}`;
133
+ }
134
+ /**
135
+ * How many atoms of each element sit on each side.
136
+ *
137
+ * For showing the check rather than doing it: the balance is exact by construction, and a learner
138
+ * still has to see both columns match before they believe it.
139
+ */
140
+ function atomTally(equation) {
141
+ const count = (terms, element) => terms.reduce((total, term) => {
142
+ const composition = readFormula(withoutState(term.formula)).counts;
143
+ return total + term.coefficient * (composition.find((e) => e.element === element)?.count ?? 0);
144
+ }, 0);
145
+ return equation.elements.map((element) => ({
146
+ element,
147
+ left: count(equation.reactants, element),
148
+ right: count(equation.products, element)
149
+ }));
150
+ }
151
+ /**
152
+ * Total charge on each side.
153
+ *
154
+ * Separate from the atom tally because it is a different conservation law, and because showing it
155
+ * beside the atoms is what makes an ionic equation make sense: the two sides of a precipitation
156
+ * carry the same charge as well as the same atoms, and a learner who only counts atoms will balance
157
+ * it wrongly and never see why.
158
+ */
159
+ function chargeTally(equation) {
160
+ const total = (terms) => terms.reduce((sum, term) => sum + term.coefficient * readFormula(withoutState(term.formula)).charge, 0);
161
+ return {
162
+ left: total(equation.reactants),
163
+ right: total(equation.products)
164
+ };
165
+ }
166
+
167
+ //#endregion
168
+ export { atomTally, balance, chargeTally, formatEquation, splitEquation };
@@ -0,0 +1,90 @@
1
+ //#region src/chem/formula.d.ts
2
+ /**
3
+ * Chemical formulae: what is in them, what they weigh, and what they weigh per element.
4
+ *
5
+ * ## One parser, because two would disagree
6
+ *
7
+ * A formula turns up in every quantitative question there is, and each of them needs the same three
8
+ * answers: which atoms, how many, and what that comes to. Writing a small reader for each caller is
9
+ * how a kernel ends up telling two different stories about Ca(NO3)2, so everything reads formulae
10
+ * here.
11
+ *
12
+ * What it understands:
13
+ *
14
+ * Ca(NO3)2 groups in brackets, with a multiplier
15
+ * K4[Fe(CN)6] square brackets, nested
16
+ * CuSO4.5H2O a hydrate, written with a dot and a leading count
17
+ * MgCl2.KCl.6H2O a double salt, as many parts as it takes
18
+ *
19
+ * ## Masses come from the element table, and a formula that names nothing is refused
20
+ *
21
+ * Relative atomic masses are the IUPAC values the atomic engine already holds, so a mass computed
22
+ * here cannot disagree with one computed anywhere else. A symbol that is not an element stops the
23
+ * parse: `Xy2O3` is a typo, and a parser that skipped it would return a mass for a substance that
24
+ * does not exist, which is worse than no answer at all.
25
+ *
26
+ * ## The empirical formula is a search, not a division
27
+ *
28
+ * Dividing by the smallest mole count is only the first half. It lands on whole numbers for some
29
+ * compounds and on 1.5 or 1.33 for others, and the answer is to scale until every ratio is close to
30
+ * a whole number. {@link empiricalFormula} tries multipliers in order and takes the first that
31
+ * works, which is exactly the step a learner is told to do "by inspection" and is the step most
32
+ * often fudged.
33
+ *
34
+ * Pure: no React, no pixels.
35
+ */
36
+ /** One element and how many of its atoms the formula contains. */
37
+ interface ElementCount {
38
+ readonly element: string;
39
+ readonly count: number;
40
+ }
41
+ /**
42
+ * The atoms in a formula, in the order they are written.
43
+ *
44
+ * Hydrates and double salts are split on the dot, each part parsed on its own and multiplied by its
45
+ * leading count, then added together. So CuSO4.5H2O has ten hydrogens, nine oxygens, one copper and
46
+ * one sulfur, which is what it weighs as.
47
+ */
48
+ declare function parseFormula(input: string): ElementCount[];
49
+ /**
50
+ * Relative molecular mass, from the IUPAC atomic weights the element table holds.
51
+ *
52
+ * Called relative FORMULA mass for anything ionic, since sodium chloride has no molecules, and the
53
+ * arithmetic is identical either way.
54
+ */
55
+ declare function molarMass(formula: string): number;
56
+ interface MassFraction {
57
+ readonly element: string;
58
+ /** Mass of this element in one formula unit. */
59
+ readonly mass: number;
60
+ /** Its share of the total, as a percentage. */
61
+ readonly percent: number;
62
+ }
63
+ /** What fraction of a compound's mass each element accounts for. */
64
+ declare function percentComposition(formula: string): MassFraction[];
65
+ /**
66
+ * The empirical formula implied by a composition.
67
+ *
68
+ * Takes percentages by mass, or any set of masses in the same unit: only the ratios matter, so the
69
+ * numbers do not have to add to a hundred and a set of measured masses works directly.
70
+ *
71
+ * The scaling step is the one that is usually waved through. Dividing by the smallest mole count
72
+ * can land on 1.5 or 1.33, and the ratios then have to be multiplied up until they are all close to
73
+ * whole numbers. That search is done here rather than left to inspection, and if no multiplier up
74
+ * to eight works the composition is refused rather than rounded into something tidy.
75
+ */
76
+ declare function empiricalFormula(composition: Readonly<Record<string, number>>): ElementCount[];
77
+ /**
78
+ * How many empirical units make up the real molecule.
79
+ *
80
+ * Rounded, because the ratio of a measured molar mass to an exact empirical mass never comes out
81
+ * whole. Refuses when it is not close to one, since a value of 2.4 means the molar mass or the
82
+ * composition is wrong and multiplying by 2 would bury that.
83
+ */
84
+ declare function unitsInMolecule(empirical: readonly ElementCount[], relativeMolecularMass: number): number;
85
+ /** The molecular formula: the empirical formula multiplied up to fit the measured mass. */
86
+ declare const molecularFormulaFor: (empirical: readonly ElementCount[], relativeMolecularMass: number) => ElementCount[];
87
+ /** Write counts back out as a formula, leaving a count of one unwritten. */
88
+ declare const formatFormula: (counts: readonly ElementCount[]) => string;
89
+ //#endregion
90
+ export { ElementCount, MassFraction, empiricalFormula, formatFormula, molarMass, molecularFormulaFor, parseFormula, percentComposition, unitsInMolecule };
@@ -0,0 +1,201 @@
1
+ import { elementBySymbol } from "../atomic/elements.mjs";
2
+
3
+ //#region src/chem/formula.ts
4
+ /**
5
+ * Chemical formulae: what is in them, what they weigh, and what they weigh per element.
6
+ *
7
+ * ## One parser, because two would disagree
8
+ *
9
+ * A formula turns up in every quantitative question there is, and each of them needs the same three
10
+ * answers: which atoms, how many, and what that comes to. Writing a small reader for each caller is
11
+ * how a kernel ends up telling two different stories about Ca(NO3)2, so everything reads formulae
12
+ * here.
13
+ *
14
+ * What it understands:
15
+ *
16
+ * Ca(NO3)2 groups in brackets, with a multiplier
17
+ * K4[Fe(CN)6] square brackets, nested
18
+ * CuSO4.5H2O a hydrate, written with a dot and a leading count
19
+ * MgCl2.KCl.6H2O a double salt, as many parts as it takes
20
+ *
21
+ * ## Masses come from the element table, and a formula that names nothing is refused
22
+ *
23
+ * Relative atomic masses are the IUPAC values the atomic engine already holds, so a mass computed
24
+ * here cannot disagree with one computed anywhere else. A symbol that is not an element stops the
25
+ * parse: `Xy2O3` is a typo, and a parser that skipped it would return a mass for a substance that
26
+ * does not exist, which is worse than no answer at all.
27
+ *
28
+ * ## The empirical formula is a search, not a division
29
+ *
30
+ * Dividing by the smallest mole count is only the first half. It lands on whole numbers for some
31
+ * compounds and on 1.5 or 1.33 for others, and the answer is to scale until every ratio is close to
32
+ * a whole number. {@link empiricalFormula} tries multipliers in order and takes the first that
33
+ * works, which is exactly the step a learner is told to do "by inspection" and is the step most
34
+ * often fudged.
35
+ *
36
+ * Pure: no React, no pixels.
37
+ */
38
+ const isUpper = (c) => c >= "A" && c <= "Z";
39
+ const isLower = (c) => c >= "a" && c <= "z";
40
+ const isDigit = (c) => c >= "0" && c <= "9";
41
+ function readNumber(r) {
42
+ let digits = "";
43
+ while (r.at < r.text.length && isDigit(r.text[r.at])) digits += r.text[r.at++];
44
+ return digits === "" ? 1 : Number(digits);
45
+ }
46
+ const CLOSERS = {
47
+ "(": ")",
48
+ "[": "]"
49
+ };
50
+ /** Parse one group, stopping at a closing bracket or the end of the text. */
51
+ function readGroup(r, closer) {
52
+ const counts = /* @__PURE__ */ new Map();
53
+ const add = (element, n) => void counts.set(element, (counts.get(element) ?? 0) + n);
54
+ while (r.at < r.text.length) {
55
+ const c = r.text[r.at];
56
+ if (closer && c === closer) {
57
+ r.at += 1;
58
+ return counts;
59
+ }
60
+ if (c === "(" || c === "[") {
61
+ r.at += 1;
62
+ const inner = readGroup(r, CLOSERS[c]);
63
+ const multiplier = readNumber(r);
64
+ for (const [element, n] of inner) add(element, n * multiplier);
65
+ continue;
66
+ }
67
+ if (c === ")" || c === "]") throw new Error(`cannot read "${r.text}": a closing bracket at position ${r.at} opens nothing`);
68
+ if (!isUpper(c)) throw new Error(`cannot read "${r.text}": expected an element symbol at position ${r.at}, found "${c}"`);
69
+ let symbol = r.text[r.at++];
70
+ if (r.at < r.text.length && isLower(r.text[r.at])) symbol += r.text[r.at++];
71
+ if (!elementBySymbol(symbol)) throw new Error(`cannot read "${r.text}": "${symbol}" is not an element`);
72
+ add(symbol, readNumber(r));
73
+ }
74
+ if (closer) throw new Error(`cannot read "${r.text}": a bracket is never closed`);
75
+ return counts;
76
+ }
77
+ /**
78
+ * The atoms in a formula, in the order they are written.
79
+ *
80
+ * Hydrates and double salts are split on the dot, each part parsed on its own and multiplied by its
81
+ * leading count, then added together. So CuSO4.5H2O has ten hydrogens, nine oxygens, one copper and
82
+ * one sulfur, which is what it weighs as.
83
+ */
84
+ function parseFormula(input) {
85
+ const text = input.replace(/\s+/g, "");
86
+ if (text === "") throw new Error("an empty formula has no composition");
87
+ const total = /* @__PURE__ */ new Map();
88
+ text.split(".").forEach((part, index) => {
89
+ if (part === "") throw new Error(`cannot read "${input}": an empty part between dots`);
90
+ const r = {
91
+ text: part,
92
+ at: 0
93
+ };
94
+ const multiplier = index === 0 ? 1 : readNumber(r);
95
+ for (const [element, n] of readGroup(r)) total.set(element, (total.get(element) ?? 0) + n * multiplier);
96
+ });
97
+ return [...total].map(([element, count]) => ({
98
+ element,
99
+ count
100
+ }));
101
+ }
102
+ /**
103
+ * Relative molecular mass, from the IUPAC atomic weights the element table holds.
104
+ *
105
+ * Called relative FORMULA mass for anything ionic, since sodium chloride has no molecules, and the
106
+ * arithmetic is identical either way.
107
+ */
108
+ function molarMass(formula) {
109
+ return parseFormula(formula).reduce((total, { element, count }) => total + elementBySymbol(element).atomicWeight * count, 0);
110
+ }
111
+ /** What fraction of a compound's mass each element accounts for. */
112
+ function percentComposition(formula) {
113
+ const total = molarMass(formula);
114
+ return parseFormula(formula).map(({ element, count }) => {
115
+ const mass = elementBySymbol(element).atomicWeight * count;
116
+ return {
117
+ element,
118
+ mass,
119
+ percent: mass / total * 100
120
+ };
121
+ });
122
+ }
123
+ /**
124
+ * How close a scaled ratio has to be to a whole number before it counts as one.
125
+ *
126
+ * Fixed, and deliberately not loosened as the multiplier grows. Scaling the tolerance with the
127
+ * multiplier made every larger multiplier more forgiving than the last, so a composition that fits
128
+ * nothing would always find some multiple that looked close enough, and the refusal below could
129
+ * never fire.
130
+ */
131
+ const WHOLE_TOLERANCE = .1;
132
+ /** Multipliers tried when the first division does not land on whole numbers. */
133
+ const MULTIPLIERS = [
134
+ 1,
135
+ 2,
136
+ 3,
137
+ 4,
138
+ 5,
139
+ 6,
140
+ 8
141
+ ];
142
+ /**
143
+ * The empirical formula implied by a composition.
144
+ *
145
+ * Takes percentages by mass, or any set of masses in the same unit: only the ratios matter, so the
146
+ * numbers do not have to add to a hundred and a set of measured masses works directly.
147
+ *
148
+ * The scaling step is the one that is usually waved through. Dividing by the smallest mole count
149
+ * can land on 1.5 or 1.33, and the ratios then have to be multiplied up until they are all close to
150
+ * whole numbers. That search is done here rather than left to inspection, and if no multiplier up
151
+ * to eight works the composition is refused rather than rounded into something tidy.
152
+ */
153
+ function empiricalFormula(composition) {
154
+ const entries = Object.entries(composition).filter(([, amount]) => amount > 0);
155
+ if (entries.length === 0) throw new Error("a composition with nothing in it has no formula");
156
+ const moles = entries.map(([element, amount]) => {
157
+ const record = elementBySymbol(element);
158
+ if (!record) throw new Error(`"${element}" is not an element`);
159
+ return {
160
+ element,
161
+ moles: amount / record.atomicWeight
162
+ };
163
+ });
164
+ const smallest = Math.min(...moles.map((m) => m.moles));
165
+ const ratios = moles.map((m) => m.moles / smallest);
166
+ for (const multiplier of MULTIPLIERS) {
167
+ const scaled = ratios.map((r) => r * multiplier);
168
+ if (scaled.every((value) => Math.abs(value - Math.round(value)) <= WHOLE_TOLERANCE)) return moles.map((m, i) => ({
169
+ element: m.element,
170
+ count: Math.round(scaled[i])
171
+ }));
172
+ }
173
+ throw new Error(`this composition does not reduce to a whole-number ratio: ${ratios.map((r, i) => `${moles[i].element} ${r.toFixed(3)}`).join(", ")}. Check the percentages add up and that no element is missing.`);
174
+ }
175
+ /**
176
+ * How many empirical units make up the real molecule.
177
+ *
178
+ * Rounded, because the ratio of a measured molar mass to an exact empirical mass never comes out
179
+ * whole. Refuses when it is not close to one, since a value of 2.4 means the molar mass or the
180
+ * composition is wrong and multiplying by 2 would bury that.
181
+ */
182
+ function unitsInMolecule(empirical, relativeMolecularMass) {
183
+ const empiricalMass = empirical.reduce((total, { element, count }) => total + elementBySymbol(element).atomicWeight * count, 0);
184
+ const ratio = relativeMolecularMass / empiricalMass;
185
+ const whole = Math.round(ratio);
186
+ if (whole < 1 || Math.abs(ratio - whole) > .1) throw new Error(`a relative molecular mass of ${relativeMolecularMass} is ${ratio.toFixed(2)} times the empirical mass of ${empiricalMass.toFixed(1)}, which is not a whole number of units`);
187
+ return whole;
188
+ }
189
+ /** The molecular formula: the empirical formula multiplied up to fit the measured mass. */
190
+ const molecularFormulaFor = (empirical, relativeMolecularMass) => {
191
+ const n = unitsInMolecule(empirical, relativeMolecularMass);
192
+ return empirical.map(({ element, count }) => ({
193
+ element,
194
+ count: count * n
195
+ }));
196
+ };
197
+ /** Write counts back out as a formula, leaving a count of one unwritten. */
198
+ const formatFormula = (counts) => counts.map(({ element, count }) => `${element}${count === 1 ? "" : count}`).join("");
199
+
200
+ //#endregion
201
+ export { empiricalFormula, formatFormula, molarMass, molecularFormulaFor, parseFormula, percentComposition, unitsInMolecule };
@@ -0,0 +1,31 @@
1
+ import { Structure } from "./molecule.mjs";
2
+
3
+ //#region src/chem/functional-groups.d.ts
4
+ type GroupKind = 'alkene' | 'alkyne' | 'arene' | 'alcohol' | 'halogenoalkane' | 'aldehyde' | 'ketone' | 'carboxylic-acid' | 'ester' | 'acyl-chloride' | 'amide' | 'amine' | 'nitrile';
5
+ interface FoundGroup {
6
+ readonly kind: GroupKind;
7
+ /** The atoms that make up the group, so a figure can highlight exactly these. */
8
+ readonly atoms: readonly number[];
9
+ /**
10
+ * Primary, secondary or tertiary, where the syllabus distinguishes them.
11
+ *
12
+ * Alcohols and halogenoalkanes, because the class decides the mechanism, and amines, because it
13
+ * decides the basicity.
14
+ */
15
+ readonly klass?: 'primary' | 'secondary' | 'tertiary';
16
+ }
17
+ /**
18
+ * Every functional group in the structure.
19
+ *
20
+ * An atom may appear in more than one group only when the chemistry really has two: an amino acid
21
+ * genuinely has both a carboxylic acid and an amine, and reporting one would lose the point of the
22
+ * molecule. What never happens is the same atoms being reported twice under different names, which
23
+ * is what the carbonyl classification exists to prevent.
24
+ */
25
+ declare function functionalGroups(structure: Structure): FoundGroup[];
26
+ /** Does the structure contain this group? The question a reagent test actually asks. */
27
+ declare const hasGroup: (structure: Structure, kind: GroupKind) => boolean;
28
+ /** Group kinds present, each once, in the order they were found. */
29
+ declare const groupKinds: (structure: Structure) => GroupKind[];
30
+ //#endregion
31
+ export { FoundGroup, GroupKind, functionalGroups, groupKinds, hasGroup };
@@ -0,0 +1,187 @@
1
+ import { bondsAt, implicitHydrogens, otherEnd } from "./molecule.mjs";
2
+
3
+ //#region src/chem/functional-groups.ts
4
+ /**
5
+ * functional-groups, found in a structure rather than declared beside it.
6
+ *
7
+ * WHAT THIS IS FOR. Every lab that highlights a functional group currently does it by listing vertex
8
+ * indices, which means the highlight and the structure are two independent claims and nothing checks
9
+ * that they agree. Worse, it means a learner is shown "here is the carbonyl" rather than being
10
+ * taught what makes a carbonyl a carbonyl, because the recognition was done by the author.
11
+ *
12
+ * Perception closes both. A group is a small pattern in the graph, and finding it is the same work
13
+ * the learner is being asked to do.
14
+ *
15
+ * ## Why the order matters
16
+ *
17
+ * Several groups share a C=O and are told apart only by what else is on that carbon. An ester, an
18
+ * acid, an amide, an acyl chloride, an aldehyde and a ketone all contain one. So the carbonyl is
19
+ * classified ONCE, by looking at its other neighbours, and the more specific answer wins: a
20
+ * structure with a C=O and an O-H on the same carbon is a carboxylic acid and is not reported as a
21
+ * ketone with an alcohol attached.
22
+ *
23
+ * That ordering is not an implementation detail. It is exactly the reasoning an exam question wants,
24
+ * and stating it as a sequence of tests is what makes it teachable.
25
+ *
26
+ * Pure: no React, no pixels.
27
+ */
28
+ const isC = (s, id) => s.atoms[id].element === "C";
29
+ /** Carbons attached to this atom. */
30
+ const carbonNeighbours = (s, id) => bondsAt(s, id).map((b) => otherEnd(b, id)).filter((n) => isC(s, n));
31
+ /** An oxygen bonded to exactly one heavy atom, so its other bond is to hydrogen. */
32
+ const isHydroxyl = (s, id) => s.atoms[id].element === "O" && bondsAt(s, id).length === 1 && implicitHydrogens(s, id) === 1;
33
+ /** Primary, secondary or tertiary, from the carbons attached to the carbon bearing the group. */
34
+ function classify(s, carbon, exclude) {
35
+ const others = carbonNeighbours(s, carbon).filter((n) => n !== exclude).length;
36
+ return others <= 1 ? "primary" : others === 2 ? "secondary" : "tertiary";
37
+ }
38
+ /**
39
+ * Classify one carbonyl carbon by what else is attached to it.
40
+ *
41
+ * The whole of the carbonyl family, in the order an exam question works through it: look for the
42
+ * second oxygen first, because that is what separates the acid and the ester from everything else.
43
+ */
44
+ function carbonylKind(s, carbon, oxygen) {
45
+ const attached = bondsAt(s, carbon).map((b) => otherEnd(b, carbon)).filter((n) => n !== oxygen);
46
+ const hydrogens = implicitHydrogens(s, carbon);
47
+ const hydroxyl = attached.find((n) => isHydroxyl(s, n));
48
+ if (hydroxyl !== void 0) return {
49
+ kind: "carboxylic-acid",
50
+ atoms: [
51
+ carbon,
52
+ oxygen,
53
+ hydroxyl
54
+ ]
55
+ };
56
+ const etherO = attached.find((n) => s.atoms[n].element === "O" && bondsAt(s, n).length === 2);
57
+ if (etherO !== void 0) return {
58
+ kind: "ester",
59
+ atoms: [
60
+ carbon,
61
+ oxygen,
62
+ etherO
63
+ ]
64
+ };
65
+ const chlorine = attached.find((n) => s.atoms[n].element === "Cl");
66
+ if (chlorine !== void 0) return {
67
+ kind: "acyl-chloride",
68
+ atoms: [
69
+ carbon,
70
+ oxygen,
71
+ chlorine
72
+ ]
73
+ };
74
+ const nitrogen = attached.find((n) => s.atoms[n].element === "N");
75
+ if (nitrogen !== void 0) return {
76
+ kind: "amide",
77
+ atoms: [
78
+ carbon,
79
+ oxygen,
80
+ nitrogen
81
+ ]
82
+ };
83
+ const carbons = attached.filter((n) => isC(s, n)).length;
84
+ if (hydrogens >= 1 || carbons <= 1) return {
85
+ kind: "aldehyde",
86
+ atoms: [carbon, oxygen]
87
+ };
88
+ return {
89
+ kind: "ketone",
90
+ atoms: [carbon, oxygen]
91
+ };
92
+ }
93
+ /**
94
+ * Every functional group in the structure.
95
+ *
96
+ * An atom may appear in more than one group only when the chemistry really has two: an amino acid
97
+ * genuinely has both a carboxylic acid and an amine, and reporting one would lose the point of the
98
+ * molecule. What never happens is the same atoms being reported twice under different names, which
99
+ * is what the carbonyl classification exists to prevent.
100
+ */
101
+ function functionalGroups(structure) {
102
+ const found = [];
103
+ const carbonylOxygens = /* @__PURE__ */ new Set();
104
+ const groupedO = /* @__PURE__ */ new Set();
105
+ const groupedN = /* @__PURE__ */ new Set();
106
+ for (const bond of structure.bonds) {
107
+ if (bond.order !== 2 || bond.aromatic) continue;
108
+ const pair = [bond.a, bond.b];
109
+ const carbon = pair.find((n) => isC(structure, n));
110
+ const oxygen = pair.find((n) => structure.atoms[n].element === "O");
111
+ if (carbon === void 0 || oxygen === void 0) continue;
112
+ const group = carbonylKind(structure, carbon, oxygen);
113
+ if (!group) continue;
114
+ found.push(group);
115
+ carbonylOxygens.add(oxygen);
116
+ for (const id of group.atoms) {
117
+ if (structure.atoms[id].element === "O") groupedO.add(id);
118
+ if (structure.atoms[id].element === "N") groupedN.add(id);
119
+ }
120
+ }
121
+ for (const bond of structure.bonds) {
122
+ if (bond.aromatic) continue;
123
+ const bothCarbon = isC(structure, bond.a) && isC(structure, bond.b);
124
+ if (bothCarbon && bond.order === 2) found.push({
125
+ kind: "alkene",
126
+ atoms: [bond.a, bond.b]
127
+ });
128
+ if (bothCarbon && bond.order === 3) found.push({
129
+ kind: "alkyne",
130
+ atoms: [bond.a, bond.b]
131
+ });
132
+ if (bond.order === 3 && !bothCarbon) {
133
+ const carbon = [bond.a, bond.b].find((n) => isC(structure, n));
134
+ const nitrogen = [bond.a, bond.b].find((n) => structure.atoms[n].element === "N");
135
+ if (carbon !== void 0 && nitrogen !== void 0) {
136
+ found.push({
137
+ kind: "nitrile",
138
+ atoms: [carbon, nitrogen]
139
+ });
140
+ groupedN.add(nitrogen);
141
+ }
142
+ }
143
+ }
144
+ if (structure.atoms.some((a) => a.aromatic)) found.push({
145
+ kind: "arene",
146
+ atoms: structure.atoms.filter((a) => a.aromatic).map((a) => a.id)
147
+ });
148
+ for (const atom of structure.atoms) {
149
+ if (atom.element === "O" && !groupedO.has(atom.id) && isHydroxyl(structure, atom.id)) {
150
+ const carbon = carbonNeighbours(structure, atom.id)[0];
151
+ if (carbon !== void 0) found.push({
152
+ kind: "alcohol",
153
+ atoms: [carbon, atom.id],
154
+ klass: classify(structure, carbon, atom.id)
155
+ });
156
+ }
157
+ if ([
158
+ "F",
159
+ "Cl",
160
+ "Br",
161
+ "I"
162
+ ].includes(atom.element)) {
163
+ const carbon = carbonNeighbours(structure, atom.id)[0];
164
+ if (carbon !== void 0 && !found.some((g) => g.kind === "acyl-chloride" && g.atoms.includes(atom.id))) found.push({
165
+ kind: "halogenoalkane",
166
+ atoms: [carbon, atom.id],
167
+ klass: classify(structure, carbon, atom.id)
168
+ });
169
+ }
170
+ if (atom.element === "N" && !groupedN.has(atom.id)) {
171
+ const carbons = carbonNeighbours(structure, atom.id);
172
+ if (carbons.length >= 1) found.push({
173
+ kind: "amine",
174
+ atoms: [atom.id, ...carbons],
175
+ klass: carbons.length === 1 ? "primary" : carbons.length === 2 ? "secondary" : "tertiary"
176
+ });
177
+ }
178
+ }
179
+ return found;
180
+ }
181
+ /** Does the structure contain this group? The question a reagent test actually asks. */
182
+ const hasGroup = (structure, kind) => functionalGroups(structure).some((g) => g.kind === kind);
183
+ /** Group kinds present, each once, in the order they were found. */
184
+ const groupKinds = (structure) => [...new Set(functionalGroups(structure).map((g) => g.kind))];
185
+
186
+ //#endregion
187
+ export { functionalGroups, groupKinds, hasGroup };