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