@jjlmoya/utils-science 1.34.0 → 1.35.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 (34) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +3 -1
  4. package/src/index.ts +1 -0
  5. package/src/tests/locale_completeness.test.ts +2 -2
  6. package/src/tests/tool_validation.test.ts +2 -2
  7. package/src/tool/roche-limit-satellite-disruption/bibliography.astro +14 -0
  8. package/src/tool/roche-limit-satellite-disruption/bibliography.ts +16 -0
  9. package/src/tool/roche-limit-satellite-disruption/component.astro +97 -0
  10. package/src/tool/roche-limit-satellite-disruption/entry.ts +28 -0
  11. package/src/tool/roche-limit-satellite-disruption/i18n/de.ts +229 -0
  12. package/src/tool/roche-limit-satellite-disruption/i18n/en.ts +229 -0
  13. package/src/tool/roche-limit-satellite-disruption/i18n/es.ts +229 -0
  14. package/src/tool/roche-limit-satellite-disruption/i18n/fr.ts +229 -0
  15. package/src/tool/roche-limit-satellite-disruption/i18n/id.ts +229 -0
  16. package/src/tool/roche-limit-satellite-disruption/i18n/it.ts +229 -0
  17. package/src/tool/roche-limit-satellite-disruption/i18n/ja.ts +229 -0
  18. package/src/tool/roche-limit-satellite-disruption/i18n/ko.ts +229 -0
  19. package/src/tool/roche-limit-satellite-disruption/i18n/nl.ts +229 -0
  20. package/src/tool/roche-limit-satellite-disruption/i18n/pl.ts +229 -0
  21. package/src/tool/roche-limit-satellite-disruption/i18n/pt.ts +229 -0
  22. package/src/tool/roche-limit-satellite-disruption/i18n/ru.ts +229 -0
  23. package/src/tool/roche-limit-satellite-disruption/i18n/sv.ts +229 -0
  24. package/src/tool/roche-limit-satellite-disruption/i18n/tr.ts +229 -0
  25. package/src/tool/roche-limit-satellite-disruption/i18n/zh.ts +229 -0
  26. package/src/tool/roche-limit-satellite-disruption/index.ts +11 -0
  27. package/src/tool/roche-limit-satellite-disruption/logic.ts +102 -0
  28. package/src/tool/roche-limit-satellite-disruption/particle-system.ts +66 -0
  29. package/src/tool/roche-limit-satellite-disruption/roche-limit-satellite-disruption-calculator.css +568 -0
  30. package/src/tool/roche-limit-satellite-disruption/script.ts +274 -0
  31. package/src/tool/roche-limit-satellite-disruption/seo.astro +15 -0
  32. package/src/tool/roche-limit-satellite-disruption/storage.ts +28 -0
  33. package/src/tool/roche-limit-satellite-disruption/visual-data.ts +16 -0
  34. package/src/tools.ts +2 -0
@@ -0,0 +1,274 @@
1
+ import { PRIMARY_BODIES, SATELLITE_BODIES, calculateRocheLimit } from './logic';
2
+ import type { PrimaryId, RocheResult, SatelliteId } from './logic';
3
+ import { animateParticles, resizeParticleCanvas } from './particle-system';
4
+ import type { VisualState } from './particle-system';
5
+ import { clearState, restoreState, saveState } from './storage';
6
+ import { planetPalettes, satelliteStyles } from './visual-data';
7
+
8
+ const root = document.getElementById('roche-console') as HTMLElement;
9
+ const ui = JSON.parse(root.dataset.ui ?? '{}');
10
+ const $ = <T extends HTMLElement | SVGElement>(id: string) => document.getElementById(id) as T;
11
+ const refs = {
12
+ primaryName: $('roche-primary-name'),
13
+ primaryDensity: $('roche-primary-density'),
14
+ satelliteName: $('roche-satellite-name'),
15
+ satelliteDensity: $('roche-satellite-density'),
16
+ primaryPicker: $('roche-primary-picker'),
17
+ satellitePicker: $('roche-satellite-picker'),
18
+ primaryTrigger: $('roche-primary-trigger'),
19
+ satelliteTrigger: $('roche-satellite-trigger'),
20
+ primaryMenu: $('roche-primary-menu'),
21
+ satelliteMenu: $('roche-satellite-menu'),
22
+ distanceInput: $('roche-distance') as HTMLInputElement,
23
+ distanceOutput: $('roche-distance-output'),
24
+ verdict: $('roche-verdict'),
25
+ particleCanvas: $('roche-particle-canvas') as HTMLCanvasElement,
26
+ moon: $('roche-moon') as unknown as SVGGElement,
27
+ moonBody: $('roche-moon-body') as unknown as SVGEllipseElement,
28
+ moonScar: $('roche-moon-scar') as unknown as SVGPathElement,
29
+ planet: $('roche-planet') as unknown as SVGCircleElement,
30
+ planetStops: [$('roche-planet-stop-a'), $('roche-planet-stop-b'), $('roche-planet-stop-c')] as SVGElement[],
31
+ orbit: $('roche-orbit') as unknown as SVGCircleElement,
32
+ boundary: $('roche-boundary') as unknown as SVGCircleElement,
33
+ fragments: $('roche-fragments') as unknown as SVGGElement,
34
+ activeLimit: $('roche-active-limit'),
35
+ safetyRatio: $('roche-safety-ratio'),
36
+ period: $('roche-period'),
37
+ ringProgress: $('roche-ring-progress'),
38
+ fluidLimit: $('roche-fluid-limit'),
39
+ rigidLimit: $('roche-rigid-limit'),
40
+ fluidBar: $('roche-fluid-bar'),
41
+ rigidBar: $('roche-rigid-bar'),
42
+ density: $('roche-density'),
43
+ cohesion: $('roche-cohesion'),
44
+ radius: $('roche-radius'),
45
+ mapLabel: $('roche-map-label') as unknown as SVGTextElement,
46
+ closePass: $('roche-close-pass'),
47
+ reset: $('roche-reset'),
48
+ };
49
+
50
+ let selectedPrimary: PrimaryId = 'saturn';
51
+ let selectedSatellite: SatelliteId = 'icy-moon';
52
+ let latestMoon = { x: 0, y: 0, orbitRadius: 160, progress: 0 };
53
+ let latestResult = calculateRocheLimit({ primaryId: selectedPrimary, satelliteId: selectedSatellite, orbitDistanceKm: 140000 });
54
+ let orbitPhase = -35;
55
+ let visualState: VisualState = 'orbiting';
56
+ const verdictLabels: Record<RocheResult['verdict'], string> = {
57
+ stable: ui.stable,
58
+ grazing: ui.grazing,
59
+ fragmenting: ui.fragmenting,
60
+ ring: ui.ring,
61
+ };
62
+
63
+ function formatKm(value: number): string {
64
+ return `${Math.round(value).toLocaleString('en')} ${ui.km}`;
65
+ }
66
+
67
+ function formatValueUnit(value: string, unit: string): string {
68
+ return `<span>${value}</span><small>${unit}</small>`;
69
+ }
70
+
71
+ function setDistanceRange(): void {
72
+ const primary = PRIMARY_BODIES.find((item) => item.id === selectedPrimary) ?? PRIMARY_BODIES[0];
73
+ refs.distanceInput.min = `${Math.round(primary.radiusKm * 1.08)}`;
74
+ refs.distanceInput.max = `${Math.round(primary.radiusKm * 6.2)}`;
75
+ const value = Number(refs.distanceInput.value);
76
+ if (value < Number(refs.distanceInput.min) || value > Number(refs.distanceInput.max)) refs.distanceInput.value = `${Math.round(primary.radiusKm * 3.8)}`;
77
+ }
78
+
79
+ function updateBodyVisuals(): void {
80
+ const palette = planetPalettes[selectedPrimary];
81
+ const satelliteStyle = satelliteStyles[selectedSatellite];
82
+ refs.planetStops.forEach((stop, index) => stop.setAttribute('stop-color', palette.stops[index] ?? palette.stops[0]));
83
+ refs.moonBody.style.fill = satelliteStyle.fill;
84
+ refs.moonBody.style.stroke = satelliteStyle.stroke;
85
+ refs.moonScar.style.stroke = satelliteStyle.scar;
86
+ root.style.setProperty('--planet-line-rgb', palette.line);
87
+ }
88
+
89
+ function visualStateFor(result: RocheResult, stretch: number): VisualState {
90
+ if (result.ringProgress >= 0.6) return 'ringFormed';
91
+ if (result.ringProgress > 0.18) return 'disrupting';
92
+ if (stretch > 0.2) return 'deforming';
93
+ return 'orbiting';
94
+ }
95
+
96
+ function drawFragments(progress: number, orbitRadius: number): void {
97
+ refs.fragments.textContent = '';
98
+ for (let index = 0; index < 18 + Math.round(progress * 54); index += 1) {
99
+ const angle = index * 137.5 + progress * 80;
100
+ const lane = (index % 7) - 3;
101
+ const point = fragmentPoint(angle, orbitRadius, lane, progress);
102
+ const dot = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
103
+ dot.setAttribute('cx', `${point.x.toFixed(2)}`);
104
+ dot.setAttribute('cy', `${point.y.toFixed(2)}`);
105
+ dot.setAttribute('r', `${(1.6 + (index % 4) * 0.7 + progress * 2.4).toFixed(2)}`);
106
+ dot.classList.add('roche-fragment');
107
+ dot.style.setProperty('--delay', `${index * -80}ms`);
108
+ refs.fragments.append(dot);
109
+ }
110
+ }
111
+
112
+ function fragmentPoint(angle: number, orbitRadius: number, lane: number, progress: number): { x: number; y: number } {
113
+ const radians = angle * Math.PI / 180;
114
+ const spread = progress * (18 + Math.abs(lane) * 6);
115
+ return {
116
+ x: 280 + Math.cos(radians) * (orbitRadius + lane * spread),
117
+ y: 280 + Math.sin(radians) * (orbitRadius * 0.31 + lane * spread * 0.22),
118
+ };
119
+ }
120
+
121
+ function updateMoon(result: RocheResult, orbitRadius: number, stretch: number): void {
122
+ const radians = orbitPhase * Math.PI / 180;
123
+ const moonX = 280 + Math.cos(radians) * orbitRadius;
124
+ const moonY = 280 + Math.sin(radians) * orbitRadius * 0.35;
125
+ const moonRx = 22 + stretch * 19;
126
+ const moonRy = 22 - stretch * 8;
127
+ const scale = visualState === 'ringFormed' ? 0.01 : satelliteStyles[selectedSatellite].size * Math.max(0.42, 1 - result.ringProgress * 0.65);
128
+ refs.moon.setAttribute('transform', `translate(${moonX.toFixed(2)} ${moonY.toFixed(2)}) rotate(${(orbitPhase + stretch * 3).toFixed(2)}) scale(${scale.toFixed(2)})`);
129
+ refs.moonBody.setAttribute('rx', `${moonRx.toFixed(2)}`);
130
+ refs.moonBody.setAttribute('ry', `${moonRy.toFixed(2)}`);
131
+ refs.moonScar.setAttribute('d', `M ${(-moonRx * 0.42).toFixed(1)} ${(-moonRy * 0.62).toFixed(1)} C ${(moonRx * 0.2).toFixed(1)} ${(-moonRy * 0.28).toFixed(1)} ${(moonRx * 0.42).toFixed(1)} ${(moonRy * 0.18).toFixed(1)} ${(moonRx * 0.18).toFixed(1)} ${(moonRy * 0.78).toFixed(1)}`);
132
+ setMoonDepth(radians, result.ringProgress);
133
+ latestMoon = { x: moonX, y: moonY, orbitRadius, progress: result.ringProgress };
134
+ }
135
+
136
+ function setMoonDepth(radians: number, ringProgress: number): void {
137
+ const rearOrbit = Math.sin(radians) < 0;
138
+ if (rearOrbit) refs.planet.parentNode?.insertBefore(refs.moon, refs.planet);
139
+ if (!rearOrbit) refs.mapLabel.parentNode?.insertBefore(refs.moon, refs.mapLabel);
140
+ refs.moon.classList.toggle('is-rear', rearOrbit);
141
+ refs.moon.style.opacity = visualState === 'ringFormed' ? '0' : `${Math.max(rearOrbit ? 0.1 : 0.18, (rearOrbit ? 0.38 : 1) - ringProgress * 0.82)}`;
142
+ }
143
+
144
+ function updateReadouts(result: RocheResult): void {
145
+ refs.distanceOutput.textContent = formatKm(Number(refs.distanceInput.value));
146
+ refs.verdict.textContent = verdictLabels[result.verdict];
147
+ refs.activeLimit.innerHTML = formatValueUnit(Math.round(result.selectedLimitKm).toLocaleString('en'), ui.km);
148
+ refs.safetyRatio.innerHTML = formatValueUnit(result.safetyRatio.toFixed(2), 'x');
149
+ refs.period.innerHTML = formatValueUnit(result.orbitalPeriodHours.toFixed(1), ui.hours);
150
+ refs.ringProgress.innerHTML = formatValueUnit(`${Math.round(result.ringProgress * 100)}`, '%');
151
+ refs.fluidLimit.textContent = formatKm(result.fluidLimitKm);
152
+ refs.rigidLimit.textContent = formatKm(result.rigidLimitKm);
153
+ refs.density.textContent = `${ui.density}: ${result.satellite.densityGcm3} g/cm3`;
154
+ refs.cohesion.textContent = `${ui.cohesion}: ${result.satellite.cohesion}`;
155
+ refs.radius.textContent = `${ui.planetRadius}: ${formatKm(result.primary.radiusKm)}`;
156
+ }
157
+
158
+ function update(persist = true): void {
159
+ setDistanceRange();
160
+ const result = calculateRocheLimit({ primaryId: selectedPrimary, satelliteId: selectedSatellite, orbitDistanceKm: Number(refs.distanceInput.value) });
161
+ const orbitRadius = Math.max(104, Math.min(238, Number(refs.distanceInput.value) * (205 / Number(refs.distanceInput.max))));
162
+ const stretch = Math.min(1, Math.max(0, (1.14 - result.safetyRatio) / 0.42));
163
+ visualState = visualStateFor(result, stretch);
164
+ latestResult = result;
165
+ root.dataset.verdict = result.verdict;
166
+ refs.orbit.setAttribute('r', `${orbitRadius.toFixed(1)}`);
167
+ refs.boundary.setAttribute('r', `${Math.max(96, Math.min(236, result.selectedLimitKm * (205 / Number(refs.distanceInput.max)))).toFixed(1)}`);
168
+ refs.fluidBar.style.width = `${Math.max(8, result.fluidLimitKm / Math.max(result.fluidLimitKm, result.rigidLimitKm) * 100)}%`;
169
+ refs.rigidBar.style.width = `${Math.max(8, result.rigidLimitKm / Math.max(result.fluidLimitKm, result.rigidLimitKm) * 100)}%`;
170
+ refs.mapLabel.textContent = result.ringProgress > 0.35 ? ui.debrisTrack : ui.moonTrack;
171
+ root.style.setProperty('--ring-opacity', `${0.12 + result.ringProgress * 0.78}`);
172
+ root.style.setProperty('--stress', `${Math.min(1, result.tidalStressIndex / 1.4)}`);
173
+ updateBodyVisuals();
174
+ updateMoon(result, orbitRadius, stretch);
175
+ updateReadouts(result);
176
+ drawFragments(result.ringProgress, orbitRadius);
177
+ if (persist) saveState(selectedPrimary, selectedSatellite, Number(refs.distanceInput.value));
178
+ }
179
+
180
+ function syncPickers(): void {
181
+ const primary = PRIMARY_BODIES.find((body) => body.id === selectedPrimary) ?? PRIMARY_BODIES[0];
182
+ const satellite = SATELLITE_BODIES.find((body) => body.id === selectedSatellite) ?? SATELLITE_BODIES[0];
183
+ refs.primaryName.textContent = primary.name;
184
+ refs.primaryDensity.textContent = `${primary.densityGcm3} g/cm3`;
185
+ refs.satelliteName.textContent = satellite.name;
186
+ refs.satelliteDensity.textContent = `${satellite.densityGcm3} g/cm3`;
187
+ refs.primaryMenu.querySelectorAll<HTMLButtonElement>('button').forEach((button) => button.classList.toggle('is-selected', button.dataset.value === selectedPrimary));
188
+ refs.satelliteMenu.querySelectorAll<HTMLButtonElement>('button').forEach((button) => button.classList.toggle('is-selected', button.dataset.value === selectedSatellite));
189
+ }
190
+
191
+ function setPickerOpen(picker: HTMLElement, trigger: HTMLElement, open: boolean): void {
192
+ picker.classList.toggle('is-open', open);
193
+ trigger.setAttribute('aria-expanded', `${open}`);
194
+ }
195
+
196
+ function closePickers(): void {
197
+ setPickerOpen(refs.primaryPicker, refs.primaryTrigger, false);
198
+ setPickerOpen(refs.satellitePicker, refs.satelliteTrigger, false);
199
+ }
200
+
201
+ function fillMenus(): void {
202
+ PRIMARY_BODIES.forEach((body) => appendOption({ menu: refs.primaryMenu, value: body.id, name: body.name, meta: `${body.densityGcm3} g/cm3`, select: () => {
203
+ selectedPrimary = body.id;
204
+ } }));
205
+ SATELLITE_BODIES.forEach((body) => appendOption({ menu: refs.satelliteMenu, value: body.id, name: body.name, meta: `${body.densityGcm3} g/cm3`, select: () => {
206
+ selectedSatellite = body.id;
207
+ } }));
208
+ }
209
+
210
+ function appendOption(option: { menu: HTMLElement; value: string; name: string; meta: string; select: () => void }): void {
211
+ const button = document.createElement('button');
212
+ button.type = 'button';
213
+ button.dataset.value = option.value;
214
+ button.innerHTML = `<strong>${option.name}</strong><small>${option.meta}</small>`;
215
+ button.addEventListener('click', () => {
216
+ option.select();
217
+ closePickers();
218
+ syncPickers();
219
+ update();
220
+ });
221
+ option.menu.append(button);
222
+ }
223
+
224
+ function bindEvents(): void {
225
+ refs.distanceInput.addEventListener('input', () => update());
226
+ window.addEventListener('resize', () => resizeParticleCanvas(refs.particleCanvas));
227
+ refs.primaryTrigger.addEventListener('click', () => togglePicker(refs.primaryPicker, refs.primaryTrigger));
228
+ refs.satelliteTrigger.addEventListener('click', () => togglePicker(refs.satellitePicker, refs.satelliteTrigger));
229
+ document.addEventListener('click', (event) => {
230
+ if (!root.contains(event.target as Node)) closePickers();
231
+ });
232
+ refs.closePass.addEventListener('click', closePass);
233
+ refs.reset.addEventListener('click', reset);
234
+ }
235
+
236
+ function togglePicker(picker: HTMLElement, trigger: HTMLElement): void {
237
+ const willOpen = !picker.classList.contains('is-open');
238
+ closePickers();
239
+ setPickerOpen(picker, trigger, willOpen);
240
+ }
241
+
242
+ function closePass(): void {
243
+ refs.distanceInput.value = `${Math.round(latestResult.selectedLimitKm * 0.86)}`;
244
+ update();
245
+ }
246
+
247
+ function reset(): void {
248
+ selectedPrimary = 'saturn';
249
+ selectedSatellite = 'icy-moon';
250
+ refs.distanceInput.value = '140000';
251
+ clearState();
252
+ syncPickers();
253
+ update();
254
+ }
255
+
256
+ function animateOrbit(): void {
257
+ const speed = visualState === 'ringFormed' ? 0 : 0.14 + Math.min(0.34, Math.pow(Math.max(0.2, 1 / latestResult.safetyRatio), 1.5) * 0.08);
258
+ orbitPhase = (orbitPhase + speed) % 360;
259
+ update(false);
260
+ window.requestAnimationFrame(animateOrbit);
261
+ }
262
+
263
+ fillMenus(); refs.distanceInput.value = '140000';
264
+ const restoredState = restoreState();
265
+ if (restoredState.primaryId) selectedPrimary = restoredState.primaryId;
266
+ if (restoredState.satelliteId) selectedSatellite = restoredState.satelliteId;
267
+ if (restoredState.orbitDistanceKm) refs.distanceInput.value = `${restoredState.orbitDistanceKm}`;
268
+ setDistanceRange();
269
+ bindEvents();
270
+ syncPickers();
271
+ resizeParticleCanvas(refs.particleCanvas);
272
+ update();
273
+ window.requestAnimationFrame(() => animateParticles(refs.particleCanvas, () => ({ moon: latestMoon, visualState })));
274
+ window.requestAnimationFrame(animateOrbit);
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { rocheLimitSatelliteDisruption } 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 rocheLimitSatelliteDisruption.i18n[locale]?.();
12
+ if (!content) return null;
13
+ ---
14
+
15
+ {content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,28 @@
1
+ import { PRIMARY_BODIES, SATELLITE_BODIES } from './logic';
2
+ import type { PrimaryId, SatelliteId } from './logic';
3
+
4
+ const storageKey = 'roche-limit-satellite-disruption-state';
5
+
6
+ export function saveState(primaryId: PrimaryId, satelliteId: SatelliteId, orbitDistanceKm: number): void {
7
+ window.localStorage.setItem(storageKey, JSON.stringify({ primaryId, satelliteId, orbitDistanceKm }));
8
+ }
9
+
10
+ export function clearState(): void {
11
+ window.localStorage.removeItem(storageKey);
12
+ }
13
+
14
+ export function restoreState(): { primaryId?: PrimaryId; satelliteId?: SatelliteId; orbitDistanceKm?: number } {
15
+ const rawState = window.localStorage.getItem(storageKey);
16
+ if (!rawState) return {};
17
+ try {
18
+ const state = JSON.parse(rawState);
19
+ return {
20
+ primaryId: PRIMARY_BODIES.some((body) => body.id === state.primaryId) ? state.primaryId : undefined,
21
+ satelliteId: SATELLITE_BODIES.some((body) => body.id === state.satelliteId) ? state.satelliteId : undefined,
22
+ orbitDistanceKm: Number.isFinite(state.orbitDistanceKm) ? state.orbitDistanceKm : undefined,
23
+ };
24
+ } catch {
25
+ clearState();
26
+ return {};
27
+ }
28
+ }
@@ -0,0 +1,16 @@
1
+ import type { PrimaryId, SatelliteId } from './logic';
2
+
3
+ export const planetPalettes: Record<PrimaryId, { stops: [string, string, string]; line: string }> = {
4
+ earth: { stops: ['#e6fbff', '#3c91b8', '#233a74'], line: '60 145 184' },
5
+ mars: { stops: ['#ffe0ba', '#c9653f', '#4a2621'], line: '201 101 63' },
6
+ jupiter: { stops: ['#fff4d6', '#d29b62', '#6d5148'], line: '210 155 98' },
7
+ saturn: { stops: ['#f7efe2', '#d19a66', '#63425f'], line: '209 154 102' },
8
+ neptune: { stops: ['#d8f4ff', '#4f8fcf', '#16295f'], line: '79 143 207' },
9
+ };
10
+
11
+ export const satelliteStyles: Record<SatelliteId, { fill: string; stroke: string; scar: string; size: number }> = {
12
+ 'icy-moon': { fill: '#dbefff', stroke: '#7aa9c7', scar: '#9ec9df', size: 0.92 },
13
+ 'rocky-moon': { fill: '#c9beb2', stroke: '#75695f', scar: '#93867a', size: 1.08 },
14
+ 'rubble-pile': { fill: '#b9aa95', stroke: '#6f6253', scar: '#8e806e', size: 0.62 },
15
+ 'iron-core': { fill: '#b9bec8', stroke: '#656d7a', scar: '#818a98', size: 0.98 },
16
+ };
package/src/tools.ts CHANGED
@@ -17,6 +17,7 @@ import { TWIN_PARADOX_VISUALIZER_TOOL } from './tool/twin-paradox-visualizer/ind
17
17
  import { MANDELBROT_FRACTAL_TOOL } from './tool/mandelbrot-fractal/index';
18
18
  import { PLANET_ATMOSPHERE_SURVIVAL_TOOL } from './tool/planet-atmosphere-survival/index';
19
19
  import { THREE_BODY_PROBLEM_TOOL } from './tool/three-body-problem/index';
20
+ import { ROCHE_LIMIT_SATELLITE_DISRUPTION_TOOL } from './tool/roche-limit-satellite-disruption/index';
20
21
 
21
22
  export const ALL_TOOLS: ToolDefinition[] = [
22
23
  COLONY_COUNTER_TOOL,
@@ -36,4 +37,5 @@ export const ALL_TOOLS: ToolDefinition[] = [
36
37
  MANDELBROT_FRACTAL_TOOL,
37
38
  PLANET_ATMOSPHERE_SURVIVAL_TOOL,
38
39
  THREE_BODY_PROBLEM_TOOL,
40
+ ROCHE_LIMIT_SATELLITE_DISRUPTION_TOOL,
39
41
  ];