@jjlmoya/utils-science 1.39.0 → 1.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/category/index.ts +2 -1
- package/src/entries.ts +3 -1
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/crystal-lattice-structure-finder/bibliography.astro +14 -0
- package/src/tool/crystal-lattice-structure-finder/bibliography.ts +20 -0
- package/src/tool/crystal-lattice-structure-finder/component.astro +366 -0
- package/src/tool/crystal-lattice-structure-finder/crystal-lattice-structure-finder.css +387 -0
- package/src/tool/crystal-lattice-structure-finder/data.ts +220 -0
- package/src/tool/crystal-lattice-structure-finder/entry.ts +26 -0
- package/src/tool/crystal-lattice-structure-finder/i18n/de.ts +218 -0
- package/src/tool/crystal-lattice-structure-finder/i18n/en.ts +218 -0
- package/src/tool/crystal-lattice-structure-finder/i18n/es.ts +218 -0
- package/src/tool/crystal-lattice-structure-finder/i18n/fr.ts +218 -0
- package/src/tool/crystal-lattice-structure-finder/i18n/id.ts +218 -0
- package/src/tool/crystal-lattice-structure-finder/i18n/it.ts +218 -0
- package/src/tool/crystal-lattice-structure-finder/i18n/ja.ts +218 -0
- package/src/tool/crystal-lattice-structure-finder/i18n/ko.ts +218 -0
- package/src/tool/crystal-lattice-structure-finder/i18n/nl.ts +218 -0
- package/src/tool/crystal-lattice-structure-finder/i18n/pl.ts +218 -0
- package/src/tool/crystal-lattice-structure-finder/i18n/pt.ts +218 -0
- package/src/tool/crystal-lattice-structure-finder/i18n/ru.ts +218 -0
- package/src/tool/crystal-lattice-structure-finder/i18n/sv.ts +218 -0
- package/src/tool/crystal-lattice-structure-finder/i18n/tr.ts +218 -0
- package/src/tool/crystal-lattice-structure-finder/i18n/zh.ts +218 -0
- package/src/tool/crystal-lattice-structure-finder/index.ts +11 -0
- package/src/tool/crystal-lattice-structure-finder/logic.ts +39 -0
- package/src/tool/crystal-lattice-structure-finder/seo.astro +15 -0
- package/src/tools.ts +2 -0
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -21,10 +21,11 @@ import { rocheLimitSatelliteDisruption } from '../tool/roche-limit-satellite-dis
|
|
|
21
21
|
import { dysonSphereEnergyCapture } from '../tool/dyson-sphere-energy-capture/index';
|
|
22
22
|
import { globalAlbedoSnowballSimulator } from '../tool/global-albedo-snowball-simulator/index';
|
|
23
23
|
import { conwayLifeRuleLab } from '../tool/conway-life-rule-lab/index';
|
|
24
|
+
import { crystalLatticeStructureFinder } from '../tool/crystal-lattice-structure-finder/index';
|
|
24
25
|
|
|
25
26
|
export const scienceCategory: ScienceCategoryEntry = {
|
|
26
27
|
icon: 'mdi:flask',
|
|
27
|
-
tools: [colonyCounter, asteroidImpact, microwaveDetector, simulationProbability, cellularRenewal, cosmicInflation, temperatureTimeline, lorenzAttractor, stellarHabitabilityZone, radioactiveDecay, naturalSelectionDrift, entropySecondLaw, doubleSlitDecoherence, phaseDiagramCriticalPoints, twinParadoxVisualizer, mandelbrotFractal, planetAtmosphereSurvival, threeBodyProblem, rocheLimitSatelliteDisruption, dysonSphereEnergyCapture, globalAlbedoSnowballSimulator, conwayLifeRuleLab],
|
|
28
|
+
tools: [colonyCounter, asteroidImpact, microwaveDetector, simulationProbability, cellularRenewal, cosmicInflation, temperatureTimeline, lorenzAttractor, stellarHabitabilityZone, radioactiveDecay, naturalSelectionDrift, entropySecondLaw, doubleSlitDecoherence, phaseDiagramCriticalPoints, twinParadoxVisualizer, mandelbrotFractal, planetAtmosphereSurvival, threeBodyProblem, rocheLimitSatelliteDisruption, dysonSphereEnergyCapture, globalAlbedoSnowballSimulator, conwayLifeRuleLab, crystalLatticeStructureFinder],
|
|
28
29
|
i18n: {
|
|
29
30
|
es: () => import('./i18n/es').then((m) => m.content),
|
|
30
31
|
en: () => import('./i18n/en').then((m) => m.content),
|
package/src/entries.ts
CHANGED
|
@@ -21,6 +21,7 @@ export { rocheLimitSatelliteDisruption } from './tool/roche-limit-satellite-disr
|
|
|
21
21
|
export { dysonSphereEnergyCapture } from './tool/dyson-sphere-energy-capture/entry';
|
|
22
22
|
export { globalAlbedoSnowballSimulator } from './tool/global-albedo-snowball-simulator/entry';
|
|
23
23
|
export { conwayLifeRuleLab } from './tool/conway-life-rule-lab/entry';
|
|
24
|
+
export { crystalLatticeStructureFinder } from './tool/crystal-lattice-structure-finder/entry';
|
|
24
25
|
export { scienceCategory } from './category';
|
|
25
26
|
import { asteroidImpact } from './tool/asteroid-impact/entry';
|
|
26
27
|
import { cellularRenewal } from './tool/cellular-renewal/entry';
|
|
@@ -44,4 +45,5 @@ import { rocheLimitSatelliteDisruption } from './tool/roche-limit-satellite-disr
|
|
|
44
45
|
import { dysonSphereEnergyCapture } from './tool/dyson-sphere-energy-capture/entry';
|
|
45
46
|
import { globalAlbedoSnowballSimulator } from './tool/global-albedo-snowball-simulator/entry';
|
|
46
47
|
import { conwayLifeRuleLab } from './tool/conway-life-rule-lab/entry';
|
|
47
|
-
|
|
48
|
+
import { crystalLatticeStructureFinder } from './tool/crystal-lattice-structure-finder/entry';
|
|
49
|
+
export const ALL_ENTRIES = [asteroidImpact, cellularRenewal, colonyCounter, microwaveDetector, simulationProbability, cosmicInflation, temperatureTimeline, lorenzAttractor, stellarHabitabilityZone, radioactiveDecay, naturalSelectionDrift, entropySecondLaw, doubleSlitDecoherence, phaseDiagramCriticalPoints, twinParadoxVisualizer, mandelbrotFractal, planetAtmosphereSurvival, threeBodyProblem, rocheLimitSatelliteDisruption, dysonSphereEnergyCapture, globalAlbedoSnowballSimulator, conwayLifeRuleLab, crystalLatticeStructureFinder];
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ export { ROCHE_LIMIT_SATELLITE_DISRUPTION_TOOL } from './tool/roche-limit-satell
|
|
|
22
22
|
export { DYSON_SPHERE_ENERGY_CAPTURE_TOOL } from './tool/dyson-sphere-energy-capture/index';
|
|
23
23
|
export { GLOBAL_ALBEDO_SNOWBALL_SIMULATOR_TOOL } from './tool/global-albedo-snowball-simulator/index';
|
|
24
24
|
export { CONWAY_LIFE_RULE_LAB_TOOL } from './tool/conway-life-rule-lab/index';
|
|
25
|
+
export { CRYSTAL_LATTICE_STRUCTURE_FINDER_TOOL } from './tool/crystal-lattice-structure-finder/index';
|
|
25
26
|
|
|
26
27
|
export type {
|
|
27
28
|
KnownLocale,
|
|
@@ -4,8 +4,8 @@ import { scienceCategory } from '../data';
|
|
|
4
4
|
|
|
5
5
|
describe('Tool Validation Suite', () => {
|
|
6
6
|
describe('Library Registration', () => {
|
|
7
|
-
it('should have
|
|
8
|
-
expect(ALL_TOOLS.length).toBe(
|
|
7
|
+
it('should have 23 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(23);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('scienceCategory should be defined', () => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { crystalLatticeStructureFinder } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'en' } = Astro.props;
|
|
11
|
+
const content = await crystalLatticeStructureFinder.i18n[locale]?.();
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{content && <SharedBibliography links={content.bibliography} />}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
|
+
{
|
|
5
|
+
name: 'NIST CODATA, Avogadro constant',
|
|
6
|
+
url: 'https://physics.nist.gov/cgi-bin/cuu/Value?na',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'OpenStax Chemistry 2e, Unit Cells and Crystal Structures',
|
|
10
|
+
url: 'https://openstax.org/books/chemistry-2e/pages/10-6-lattice-structures-in-crystalline-solids',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'Cambridge University DoITPoMS, Crystal structures and packing',
|
|
14
|
+
url: 'https://www.doitpoms.ac.uk/tlplib/atomic-scale-structure/index.php',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'International Union of Crystallography, Online Dictionary of Crystallography',
|
|
18
|
+
url: 'https://dictionary.iucr.org/',
|
|
19
|
+
},
|
|
20
|
+
];
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { LatticeId } from './logic';
|
|
3
|
+
import './crystal-lattice-structure-finder.css';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
ui: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { ui } = Astro.props;
|
|
10
|
+
|
|
11
|
+
const latticeOptions: Array<{ id: LatticeId; uiKey: string; shortKey: string }> = [
|
|
12
|
+
{ id: 'simple-cubic', uiKey: 'latticeSimpleCubic', shortKey: 'shortSc' },
|
|
13
|
+
{ id: 'face-centered-cubic', uiKey: 'latticeFcc', shortKey: 'shortFcc' },
|
|
14
|
+
{ id: 'hexagonal-close-packed', uiKey: 'latticeHcp', shortKey: 'shortHcp' },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const materialOptions: Array<{ id: string; uiKey: string; noteKey: string }> = [
|
|
18
|
+
{ id: 'copper', uiKey: 'materialCopper', noteKey: 'materialCopperNote' },
|
|
19
|
+
{ id: 'aluminum', uiKey: 'materialAluminum', noteKey: 'materialAluminumNote' },
|
|
20
|
+
{ id: 'polonium', uiKey: 'materialPolonium', noteKey: 'materialPoloniumNote' },
|
|
21
|
+
{ id: 'magnesium', uiKey: 'materialMagnesium', noteKey: 'materialMagnesiumNote' },
|
|
22
|
+
{ id: 'titanium', uiKey: 'materialTitanium', noteKey: 'materialTitaniumNote' },
|
|
23
|
+
{ id: 'halite', uiKey: 'materialHalite', noteKey: 'materialHaliteNote' },
|
|
24
|
+
];
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
<div class="lattice-lab" id="lattice-lab">
|
|
28
|
+
<section class="lattice-viewer" aria-label={ui.viewerLabel}>
|
|
29
|
+
<div class="lattice-orbit">
|
|
30
|
+
<svg class="lattice-svg" id="lattice-svg" viewBox="0 0 720 560" role="img" aria-label={ui.viewerLabel}>
|
|
31
|
+
<defs>
|
|
32
|
+
<radialGradient id="lattice-atom-corner" cx="35%" cy="30%" r="70%">
|
|
33
|
+
<stop offset="0%" stop-color="#fff7d1"></stop>
|
|
34
|
+
<stop offset="45%" stop-color="#f6b44b"></stop>
|
|
35
|
+
<stop offset="100%" stop-color="#9f4f23"></stop>
|
|
36
|
+
</radialGradient>
|
|
37
|
+
<radialGradient id="lattice-atom-face" cx="35%" cy="30%" r="70%">
|
|
38
|
+
<stop offset="0%" stop-color="#e8fff8"></stop>
|
|
39
|
+
<stop offset="48%" stop-color="#54d6b0"></stop>
|
|
40
|
+
<stop offset="100%" stop-color="#167c78"></stop>
|
|
41
|
+
</radialGradient>
|
|
42
|
+
<radialGradient id="lattice-atom-interior" cx="35%" cy="30%" r="70%">
|
|
43
|
+
<stop offset="0%" stop-color="#f2f0ff"></stop>
|
|
44
|
+
<stop offset="48%" stop-color="#8f82ff"></stop>
|
|
45
|
+
<stop offset="100%" stop-color="#473caa"></stop>
|
|
46
|
+
</radialGradient>
|
|
47
|
+
</defs>
|
|
48
|
+
<g id="lattice-grid"></g>
|
|
49
|
+
<g id="lattice-atoms"></g>
|
|
50
|
+
</svg>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="lattice-legend">
|
|
54
|
+
<span><i class="lattice-swatch lattice-swatch-corner"></i>{ui.cornerAtoms}</span>
|
|
55
|
+
<span><i class="lattice-swatch lattice-swatch-face"></i>{ui.faceAtoms}</span>
|
|
56
|
+
<span><i class="lattice-swatch lattice-swatch-interior"></i>{ui.interiorAtoms}</span>
|
|
57
|
+
</div>
|
|
58
|
+
</section>
|
|
59
|
+
|
|
60
|
+
<section class="lattice-console" aria-label="Crystal lattice controls">
|
|
61
|
+
<div class="lattice-summary">
|
|
62
|
+
<div>
|
|
63
|
+
<span id="lattice-short-name">FCC</span>
|
|
64
|
+
<strong id="lattice-density">-</strong>
|
|
65
|
+
</div>
|
|
66
|
+
<p>{ui.density}</p>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<label class="lattice-field lattice-field-select" for="lattice-material">
|
|
70
|
+
<span>{ui.material}</span>
|
|
71
|
+
<select id="lattice-material">
|
|
72
|
+
{materialOptions.map((opt) => (
|
|
73
|
+
<option value={opt.id} data-note={ui[opt.noteKey]}>{ui[opt.uiKey]}</option>
|
|
74
|
+
))}
|
|
75
|
+
</select>
|
|
76
|
+
</label>
|
|
77
|
+
|
|
78
|
+
<div class="lattice-note">
|
|
79
|
+
<span id="lattice-formula">Cu</span>
|
|
80
|
+
<p id="lattice-material-note">{ui.materialNote}</p>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<label class="lattice-field lattice-field-select" for="lattice-structure">
|
|
84
|
+
<span>{ui.lattice}</span>
|
|
85
|
+
<select id="lattice-structure">
|
|
86
|
+
{latticeOptions.map((opt) => (
|
|
87
|
+
<option value={opt.id} data-short={ui[opt.shortKey]}>{ui[opt.uiKey]}</option>
|
|
88
|
+
))}
|
|
89
|
+
</select>
|
|
90
|
+
</label>
|
|
91
|
+
|
|
92
|
+
<div class="lattice-slider-grid">
|
|
93
|
+
<label class="lattice-field" for="lattice-rotation">
|
|
94
|
+
<span>{ui.rotation}</span>
|
|
95
|
+
<output id="lattice-rotation-output">38 deg</output>
|
|
96
|
+
<input id="lattice-rotation" type="range" min="-70" max="70" step="1" value="38" />
|
|
97
|
+
</label>
|
|
98
|
+
|
|
99
|
+
<label class="lattice-field" for="lattice-zoom">
|
|
100
|
+
<span>{ui.zoom}</span>
|
|
101
|
+
<output id="lattice-zoom-output">100%</output>
|
|
102
|
+
<input id="lattice-zoom" type="range" min="75" max="130" step="1" value="100" />
|
|
103
|
+
</label>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div class="lattice-input-grid">
|
|
107
|
+
<label class="lattice-number" for="lattice-a">
|
|
108
|
+
<span>{ui.latticeA} (Å)</span>
|
|
109
|
+
<input id="lattice-a" type="text" inputmode="decimal" pattern="[0-9.]*" value="3.615" />
|
|
110
|
+
</label>
|
|
111
|
+
<label class="lattice-number" for="lattice-c-ratio">
|
|
112
|
+
<span>{ui.cRatio}</span>
|
|
113
|
+
<input id="lattice-c-ratio" type="text" inputmode="decimal" pattern="[0-9.]*" value="1" />
|
|
114
|
+
</label>
|
|
115
|
+
<label class="lattice-number" for="lattice-mass">
|
|
116
|
+
<span>{ui.molarMass} (g/mol)</span>
|
|
117
|
+
<input id="lattice-mass" type="text" inputmode="decimal" pattern="[0-9.]*" value="63.546" />
|
|
118
|
+
</label>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<button class="lattice-reset" id="lattice-reset" type="button">{ui.resetView}</button>
|
|
122
|
+
</section>
|
|
123
|
+
|
|
124
|
+
<section class="lattice-results" aria-label="Crystal lattice results">
|
|
125
|
+
<div class="lattice-result-group">
|
|
126
|
+
<h2>{ui.crystalProperties}</h2>
|
|
127
|
+
<div class="lattice-metric-row">
|
|
128
|
+
<span>{ui.atomsPerCell}</span>
|
|
129
|
+
<strong id="lattice-atoms-cell">-</strong>
|
|
130
|
+
</div>
|
|
131
|
+
<div class="lattice-metric-row">
|
|
132
|
+
<span>{ui.coordination}</span>
|
|
133
|
+
<strong id="lattice-coordination">-</strong>
|
|
134
|
+
</div>
|
|
135
|
+
<div class="lattice-metric-row">
|
|
136
|
+
<span>{ui.packingFactor}</span>
|
|
137
|
+
<strong id="lattice-packing">-</strong>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div class="lattice-result-group">
|
|
142
|
+
<h2>{ui.physicalMetrics}</h2>
|
|
143
|
+
<div class="lattice-metric-row">
|
|
144
|
+
<span>{ui.cellVolume}</span>
|
|
145
|
+
<strong id="lattice-volume">-</strong>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="lattice-metric-row">
|
|
148
|
+
<span>{ui.cellMass}</span>
|
|
149
|
+
<strong id="lattice-cell-mass">-</strong>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="lattice-metric-row">
|
|
152
|
+
<span>{ui.radius}</span>
|
|
153
|
+
<strong id="lattice-radius">-</strong>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</section>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<script>
|
|
160
|
+
import {
|
|
161
|
+
MATERIAL_PRESETS,
|
|
162
|
+
calculatePackingPercent,
|
|
163
|
+
calculateTheoreticalDensity,
|
|
164
|
+
getLattice,
|
|
165
|
+
getMaterial,
|
|
166
|
+
} from './logic';
|
|
167
|
+
import type { AtomSite, DensityResult, LatticeId, LatticeStructure, MaterialPreset } from './logic';
|
|
168
|
+
|
|
169
|
+
const svgNamespace = 'http://www.w3.org/2000/svg';
|
|
170
|
+
const materialSelect = document.getElementById('lattice-material') as HTMLSelectElement | null;
|
|
171
|
+
const structureSelect = document.getElementById('lattice-structure') as HTMLSelectElement | null;
|
|
172
|
+
const rotationInput = document.getElementById('lattice-rotation') as HTMLInputElement | null;
|
|
173
|
+
const zoomInput = document.getElementById('lattice-zoom') as HTMLInputElement | null;
|
|
174
|
+
const aInput = document.getElementById('lattice-a') as HTMLInputElement | null;
|
|
175
|
+
const cRatioInput = document.getElementById('lattice-c-ratio') as HTMLInputElement | null;
|
|
176
|
+
const massInput = document.getElementById('lattice-mass') as HTMLInputElement | null;
|
|
177
|
+
const gridLayer = document.getElementById('lattice-grid');
|
|
178
|
+
const atomLayer = document.getElementById('lattice-atoms');
|
|
179
|
+
const resetButton = document.getElementById('lattice-reset');
|
|
180
|
+
|
|
181
|
+
const labels = {
|
|
182
|
+
formula: document.getElementById('lattice-formula'),
|
|
183
|
+
note: document.getElementById('lattice-material-note'),
|
|
184
|
+
rotation: document.getElementById('lattice-rotation-output'),
|
|
185
|
+
zoom: document.getElementById('lattice-zoom-output'),
|
|
186
|
+
shortName: document.getElementById('lattice-short-name'),
|
|
187
|
+
density: document.getElementById('lattice-density'),
|
|
188
|
+
packing: document.getElementById('lattice-packing'),
|
|
189
|
+
atomsCell: document.getElementById('lattice-atoms-cell'),
|
|
190
|
+
coordination: document.getElementById('lattice-coordination'),
|
|
191
|
+
volume: document.getElementById('lattice-volume'),
|
|
192
|
+
cellMass: document.getElementById('lattice-cell-mass'),
|
|
193
|
+
radius: document.getElementById('lattice-radius'),
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
function formatNumber(value: number, digits = 3): string {
|
|
197
|
+
return new Intl.NumberFormat('en', {
|
|
198
|
+
maximumFractionDigits: digits,
|
|
199
|
+
minimumFractionDigits: value < 10 ? Math.min(2, digits) : 0,
|
|
200
|
+
}).format(value);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function activeMaterial(): MaterialPreset {
|
|
204
|
+
return getMaterial(materialSelect?.value ?? MATERIAL_PRESETS[0].id);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function decimalValue(input: HTMLInputElement | null, fallback: number): number {
|
|
208
|
+
if (!input) return fallback;
|
|
209
|
+
|
|
210
|
+
const normalized = input.value.replace(/,/g, '.').replace(/[^0-9.]/g, '');
|
|
211
|
+
if (normalized !== input.value) input.value = normalized;
|
|
212
|
+
|
|
213
|
+
const value = Number(normalized);
|
|
214
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function projectPoint(point: [number, number, number], rotation: number, zoom: number, hex = false) {
|
|
218
|
+
const angle = (rotation * Math.PI) / 180;
|
|
219
|
+
const x0 = hex ? point[0] : point[0] - 0.5;
|
|
220
|
+
const y0 = hex ? point[1] : point[1] - 0.5;
|
|
221
|
+
const z0 = point[2] - 0.5;
|
|
222
|
+
const x = x0 * Math.cos(angle) - y0 * Math.sin(angle);
|
|
223
|
+
const y = x0 * Math.sin(angle) + y0 * Math.cos(angle);
|
|
224
|
+
const scale = zoom * (hex ? 136 : 210);
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
x: 360 + (x - y) * scale * 0.58,
|
|
228
|
+
y: 288 + (x + y) * scale * 0.26 - z0 * scale * 0.78,
|
|
229
|
+
depth: x + y + z0,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function clearLayer(layer: Element | null) {
|
|
234
|
+
while (layer?.firstChild) {
|
|
235
|
+
layer.firstChild.remove();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function drawLine(from: { x: number; y: number }, to: { x: number; y: number }) {
|
|
240
|
+
const line = document.createElementNS(svgNamespace, 'line');
|
|
241
|
+
line.setAttribute('x1', from.x.toFixed(2));
|
|
242
|
+
line.setAttribute('y1', from.y.toFixed(2));
|
|
243
|
+
line.setAttribute('x2', to.x.toFixed(2));
|
|
244
|
+
line.setAttribute('y2', to.y.toFixed(2));
|
|
245
|
+
line.classList.add('lattice-edge');
|
|
246
|
+
gridLayer?.append(line);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const ATOM_RADIUS_MAP: Record<AtomSite['role'], number> = { corner: 18, face: 24, interior: 28 };
|
|
250
|
+
|
|
251
|
+
function atomRadius(role: AtomSite['role'], zoom: number) {
|
|
252
|
+
return ATOM_RADIUS_MAP[role] * (zoom / 100);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function drawAtom(site: AtomSite, position: { x: number; y: number; depth: number }, zoom: number) {
|
|
256
|
+
const circle = document.createElementNS(svgNamespace, 'circle');
|
|
257
|
+
circle.setAttribute('cx', position.x.toFixed(2));
|
|
258
|
+
circle.setAttribute('cy', position.y.toFixed(2));
|
|
259
|
+
circle.setAttribute('r', atomRadius(site.role, zoom).toFixed(2));
|
|
260
|
+
circle.setAttribute('data-depth', position.depth.toFixed(3));
|
|
261
|
+
circle.classList.add('lattice-atom', `lattice-atom-${site.role}`);
|
|
262
|
+
atomLayer?.append(circle);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function renderCell() {
|
|
266
|
+
if (!structureSelect || !rotationInput || !zoomInput) return;
|
|
267
|
+
|
|
268
|
+
const lattice = getLattice(structureSelect.value as LatticeId);
|
|
269
|
+
const rotation = Number(rotationInput.value);
|
|
270
|
+
const zoom = Number(zoomInput.value);
|
|
271
|
+
const isHex = lattice.id === 'hexagonal-close-packed';
|
|
272
|
+
const projected = lattice.vertices.map((vertex) => projectPoint(vertex, rotation, zoom / 100, isHex));
|
|
273
|
+
|
|
274
|
+
clearLayer(gridLayer);
|
|
275
|
+
clearLayer(atomLayer);
|
|
276
|
+
|
|
277
|
+
lattice.edges.forEach(([from, to]) => drawLine(projected[from], projected[to]));
|
|
278
|
+
|
|
279
|
+
const atoms = lattice.sites
|
|
280
|
+
.map((site) => {
|
|
281
|
+
const source: [number, number, number] = isHex
|
|
282
|
+
? [site.x * 2 - 1, site.y * 1.732 - 0.866, site.z]
|
|
283
|
+
: [site.x, site.y, site.z];
|
|
284
|
+
return { site, position: projectPoint(source, rotation, zoom / 100, isHex) };
|
|
285
|
+
})
|
|
286
|
+
.sort((a, b) => a.position.depth - b.position.depth);
|
|
287
|
+
|
|
288
|
+
atoms.forEach(({ site, position }) => drawAtom(site, position, zoom));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function currentCustomMaterial(): MaterialPreset {
|
|
292
|
+
const material = activeMaterial();
|
|
293
|
+
return {
|
|
294
|
+
...material,
|
|
295
|
+
latticeId: (structureSelect?.value as LatticeId) ?? material.latticeId,
|
|
296
|
+
latticeA: decimalValue(aInput, material.latticeA),
|
|
297
|
+
cToA: decimalValue(cRatioInput, material.cToA ?? getLattice(material.latticeId).cToA ?? 1),
|
|
298
|
+
atomicMass: decimalValue(massInput, material.atomicMass),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function updateResults() {
|
|
303
|
+
if (!structureSelect || !rotationInput || !zoomInput || !aInput || !cRatioInput || !massInput) return;
|
|
304
|
+
|
|
305
|
+
const material = currentCustomMaterial();
|
|
306
|
+
const lattice = getLattice(material.latticeId);
|
|
307
|
+
const result = calculateTheoreticalDensity(material);
|
|
308
|
+
const selectedStructure = structureSelect.options[structureSelect.selectedIndex];
|
|
309
|
+
|
|
310
|
+
renderLabels(lattice, material, result, selectedStructure);
|
|
311
|
+
renderCell();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function renderLabels(
|
|
315
|
+
lattice: LatticeStructure,
|
|
316
|
+
material: MaterialPreset,
|
|
317
|
+
result: DensityResult,
|
|
318
|
+
selectedOption: HTMLOptionElement | undefined,
|
|
319
|
+
) {
|
|
320
|
+
const radius = material.latticeA * lattice.radiusToA;
|
|
321
|
+
labels.rotation!.textContent = `${rotationInput!.value} deg`;
|
|
322
|
+
labels.zoom!.textContent = `${zoomInput!.value}%`;
|
|
323
|
+
labels.shortName!.textContent = selectedOption?.getAttribute('data-short') ?? '';
|
|
324
|
+
labels.density!.textContent = `${formatNumber(result.density, 3)} g/cm³`;
|
|
325
|
+
labels.packing!.textContent = `${formatNumber(calculatePackingPercent(lattice), 2)}%`;
|
|
326
|
+
labels.atomsCell!.textContent = formatNumber(result.atomsPerCell, 2);
|
|
327
|
+
labels.coordination!.textContent = `${lattice.coordinationNumber}`;
|
|
328
|
+
labels.volume!.textContent = `${formatNumber(result.cellVolumeCm3 * 1e24, 3)} ų`;
|
|
329
|
+
labels.cellMass!.textContent = `${result.cellMassG.toExponential(3)} g`;
|
|
330
|
+
labels.radius!.textContent = `${formatNumber(radius, 3)} Å`;
|
|
331
|
+
resetButton?.classList.toggle('lattice-reset-active', rotationInput!.value !== '38' || zoomInput!.value !== '100');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function loadMaterial(materialId: string) {
|
|
335
|
+
const material = getMaterial(materialId);
|
|
336
|
+
const lattice = getLattice(material.latticeId);
|
|
337
|
+
|
|
338
|
+
populateInputs(material, lattice);
|
|
339
|
+
labels.formula!.textContent = material.formula;
|
|
340
|
+
const selectedMaterial = materialSelect?.options[materialSelect.selectedIndex ?? -1];
|
|
341
|
+
labels.note!.textContent = selectedMaterial?.getAttribute('data-note') ?? '';
|
|
342
|
+
updateResults();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function populateInputs(material: MaterialPreset, lattice: LatticeStructure) {
|
|
346
|
+
if (structureSelect) structureSelect.value = material.latticeId;
|
|
347
|
+
if (aInput) aInput.value = `${material.latticeA}`;
|
|
348
|
+
if (cRatioInput) cRatioInput.value = `${material.cToA ?? lattice.cToA ?? 1}`;
|
|
349
|
+
if (massInput) massInput.value = `${material.atomicMass}`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
materialSelect?.addEventListener('change', () => loadMaterial(materialSelect.value));
|
|
353
|
+
structureSelect?.addEventListener('change', updateResults);
|
|
354
|
+
rotationInput?.addEventListener('input', updateResults);
|
|
355
|
+
zoomInput?.addEventListener('input', updateResults);
|
|
356
|
+
aInput?.addEventListener('input', updateResults);
|
|
357
|
+
cRatioInput?.addEventListener('input', updateResults);
|
|
358
|
+
massInput?.addEventListener('input', updateResults);
|
|
359
|
+
resetButton?.addEventListener('click', () => {
|
|
360
|
+
if (rotationInput) rotationInput.value = '38';
|
|
361
|
+
if (zoomInput) zoomInput.value = '100';
|
|
362
|
+
updateResults();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
loadMaterial(materialSelect?.value ?? MATERIAL_PRESETS[0].id);
|
|
366
|
+
</script>
|