@jjlmoya/utils-drones 1.22.0 → 1.24.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 (37) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -0
  3. package/src/entries.ts +4 -0
  4. package/src/tests/locale_completeness.test.ts +2 -2
  5. package/src/tests/tool_validation.test.ts +2 -2
  6. package/src/tool/drone-battery-c-rating-calculator/bibliography.astro +3 -14
  7. package/src/tool/drone-battery-c-rating-calculator/bibliography.ts +3 -1
  8. package/src/tool/fpv-drone-thrust-to-weight-ratio/bibliography.astro +6 -0
  9. package/src/tool/fpv-drone-thrust-to-weight-ratio/bibliography.ts +18 -0
  10. package/src/tool/fpv-drone-thrust-to-weight-ratio/component.astro +203 -0
  11. package/src/tool/fpv-drone-thrust-to-weight-ratio/controller.ts +240 -0
  12. package/src/tool/fpv-drone-thrust-to-weight-ratio/dom-views.ts +148 -0
  13. package/src/tool/fpv-drone-thrust-to-weight-ratio/entry.ts +27 -0
  14. package/src/tool/fpv-drone-thrust-to-weight-ratio/evaluator.ts +30 -0
  15. package/src/tool/fpv-drone-thrust-to-weight-ratio/fpv-drone-thrust-to-weight-ratio.css +404 -0
  16. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/de.ts +208 -0
  17. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/en.ts +208 -0
  18. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/es.ts +208 -0
  19. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/fr.ts +208 -0
  20. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/id.ts +208 -0
  21. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/it.ts +208 -0
  22. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/ja.ts +208 -0
  23. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/ko.ts +208 -0
  24. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/nl.ts +208 -0
  25. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/pl.ts +208 -0
  26. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/pt.ts +208 -0
  27. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/ru.ts +208 -0
  28. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/sv.ts +208 -0
  29. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/tr.ts +208 -0
  30. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/zh.ts +208 -0
  31. package/src/tool/fpv-drone-thrust-to-weight-ratio/index.ts +10 -0
  32. package/src/tool/fpv-drone-thrust-to-weight-ratio/logic.test.ts +93 -0
  33. package/src/tool/fpv-drone-thrust-to-weight-ratio/logic.ts +147 -0
  34. package/src/tool/fpv-drone-thrust-to-weight-ratio/seo.astro +16 -0
  35. package/src/tool/fpv-drone-thrust-to-weight-ratio/storage.ts +19 -0
  36. package/src/tool/fpv-drone-thrust-to-weight-ratio/ui.ts +58 -0
  37. package/src/tools.ts +3 -0
@@ -0,0 +1,93 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ calculateTotalMaxThrust,
4
+ calculateTwr,
5
+ calculateHoverThrottle,
6
+ calculateCurrentThrust,
7
+ calculateZeroToHundred,
8
+ calculateRecommendedCamAngle,
9
+ calculateWindResistance,
10
+ calculateMaxPitchAngle,
11
+ evaluateAgilityTier,
12
+ evaluateBetaflightTune,
13
+ calculateTwrMetrics,
14
+ type DroneTwrInputs,
15
+ } from './logic';
16
+
17
+ describe('FPV Drone Flight Dynamics Logic', () => {
18
+ it('calculates total max thrust', () => {
19
+ expect(calculateTotalMaxThrust(4, 1950)).toBe(7800);
20
+ expect(calculateTotalMaxThrust(0, 1950)).toBe(0);
21
+ });
22
+
23
+ it('calculates TWR ratio accurately', () => {
24
+ expect(calculateTwr(7800, 680)).toBeCloseTo(11.47, 2);
25
+ });
26
+
27
+ it('calculates realistic exponential hover throttle', () => {
28
+ const hover5in = calculateHoverThrottle(11.47);
29
+ expect(hover5in).toBeGreaterThan(20);
30
+ expect(hover5in).toBeLessThan(35);
31
+ });
32
+
33
+ it('calculates dynamic thrust from stick position', () => {
34
+ expect(calculateCurrentThrust(7800, 0)).toBe(0);
35
+ expect(calculateCurrentThrust(7800, 100)).toBe(7800);
36
+ expect(calculateCurrentThrust(7800, 50)).toBeCloseTo(2239, -1);
37
+ });
38
+
39
+ it('calculates 0-100 km/h acceleration time', () => {
40
+ const time5in = calculateZeroToHundred(11.47);
41
+ expect(time5in).toBeGreaterThan(0.2);
42
+ expect(time5in).toBeLessThan(0.4);
43
+ });
44
+
45
+ it('calculates recommended FPV camera angle', () => {
46
+ const camAngle = calculateRecommendedCamAngle(11.47);
47
+ expect(camAngle).toBeGreaterThan(30);
48
+ expect(camAngle).toBeLessThanOrEqual(55);
49
+ });
50
+
51
+ it('calculates wind resistance rating', () => {
52
+ const wind = calculateWindResistance(11.47);
53
+ expect(wind).toBeGreaterThan(80);
54
+ expect(wind).toBeLessThan(120);
55
+ });
56
+
57
+ it('calculates max pitch angle', () => {
58
+ expect(calculateMaxPitchAngle(11.47)).toBeGreaterThan(80);
59
+ expect(calculateMaxPitchAngle(1.0)).toBe(0);
60
+ });
61
+
62
+ it('evaluates flight agility tier', () => {
63
+ expect(evaluateAgilityTier(1.5)).toBe('underpowered');
64
+ expect(evaluateAgilityTier(3.0)).toBe('cinematic');
65
+ expect(evaluateAgilityTier(6.5)).toBe('freestyle');
66
+ expect(evaluateAgilityTier(10.0)).toBe('acro_pro');
67
+ expect(evaluateAgilityTier(15.0)).toBe('racing_extreme');
68
+ });
69
+
70
+ it('evaluates Betaflight tune advice', () => {
71
+ const tune5in = evaluateBetaflightTune(11.47, 680);
72
+ expect(tune5in.tpaSetting).toContain('0.65');
73
+ expect(tune5in.dynamicIdleSetting).toContain('5.5%');
74
+ });
75
+
76
+ it('computes full aggregate metrics on live inputs', () => {
77
+ const inputs: DroneTwrInputs = {
78
+ auwGrams: 680,
79
+ motorCount: 4,
80
+ thrustPerMotorGrams: 1950,
81
+ propellerSizeInches: 5.1,
82
+ propellerPitchInches: 4.3,
83
+ bladeCount: 3,
84
+ throttleStickPercent: 75,
85
+ };
86
+ const results = calculateTwrMetrics(inputs);
87
+ expect(results.twrRatio).toBeCloseTo(11.47, 2);
88
+ expect(results.currentThrustGrams).toBeGreaterThan(4000);
89
+ expect(results.instantGForce).toBeGreaterThan(6);
90
+ expect(results.agilityTier).toBe('acro_pro');
91
+ expect(results.tuneAdvice.tpaSetting).toBeDefined();
92
+ });
93
+ });
@@ -0,0 +1,147 @@
1
+ export type AgilityTier = 'underpowered' | 'cinematic' | 'freestyle' | 'acro_pro' | 'racing_extreme';
2
+
3
+ export interface DroneTwrInputs {
4
+ auwGrams: number;
5
+ motorCount: number;
6
+ thrustPerMotorGrams: number;
7
+ propellerSizeInches: number;
8
+ propellerPitchInches: number;
9
+ bladeCount: number;
10
+ throttleStickPercent: number;
11
+ }
12
+
13
+ export interface BetaflightTuneAdvice {
14
+ tpaSetting: string;
15
+ dynamicIdleSetting: string;
16
+ propwashRisk: string;
17
+ }
18
+
19
+ export interface DroneTwrResults {
20
+ totalMaxThrustGrams: number;
21
+ twrRatio: number;
22
+ hoverThrottlePercent: number;
23
+ currentThrottlePercent: number;
24
+ currentThrustGrams: number;
25
+ instantGForce: number;
26
+ zeroToHundredTimeSec: number;
27
+ recommendedCamAngleDeg: number;
28
+ windResistanceKmh: number;
29
+ maxPitchAngleDeg: number;
30
+ agilityTier: AgilityTier;
31
+ tuneAdvice: BetaflightTuneAdvice;
32
+ }
33
+
34
+ export function calculateTotalMaxThrust(motorCount: number, thrustPerMotorGrams: number): number {
35
+ if (motorCount <= 0 || thrustPerMotorGrams <= 0) return 0;
36
+ return motorCount * thrustPerMotorGrams;
37
+ }
38
+
39
+ export function calculateTwr(totalThrustGrams: number, auwGrams: number): number {
40
+ if (totalThrustGrams <= 0 || auwGrams <= 0) return 0;
41
+ return totalThrustGrams / auwGrams;
42
+ }
43
+
44
+ export function calculateHoverThrottle(twr: number): number {
45
+ if (twr <= 1) return 100;
46
+ const linearFraction = 1 / twr;
47
+ const throttlePercent = Math.pow(linearFraction, 1 / 1.8) * 100;
48
+ return Math.min(100, Math.max(5, Math.round(throttlePercent * 10) / 10));
49
+ }
50
+
51
+ export function calculateCurrentThrust(maxThrust: number, throttlePercent: number): number {
52
+ if (maxThrust <= 0 || throttlePercent <= 0) return 0;
53
+ const normalized = Math.min(100, Math.max(0, throttlePercent)) / 100;
54
+ const factor = Math.pow(normalized, 1.8);
55
+ return Math.round(maxThrust * factor);
56
+ }
57
+
58
+ export function calculateZeroToHundred(twr: number): number {
59
+ if (twr <= 1.05) return 0;
60
+ const netAccelMps2 = (twr - 1) * 9.81;
61
+ const targetVelocityMps = 100 / 3.6;
62
+ const timeSec = targetVelocityMps / netAccelMps2;
63
+ return Math.max(0.2, Math.round(timeSec * 100) / 100);
64
+ }
65
+
66
+ export function calculateRecommendedCamAngle(twr: number): number {
67
+ if (twr <= 1.2) return 15;
68
+ const cruiseTwr = Math.max(1.05, twr * 0.35);
69
+ const radians = Math.acos(1 / cruiseTwr);
70
+ const degrees = Math.round(radians * (180 / Math.PI));
71
+ return Math.min(55, Math.max(15, degrees));
72
+ }
73
+
74
+ export function calculateWindResistance(twr: number): number {
75
+ if (twr <= 0) return 0;
76
+ const baseKmh = Math.sqrt(Math.max(0, twr - 0.5)) * 28;
77
+ return Math.min(160, Math.round(baseKmh));
78
+ }
79
+
80
+ export function calculateMaxPitchAngle(twr: number): number {
81
+ if (twr <= 1.05) return 0;
82
+ const radians = Math.acos(1 / twr);
83
+ return Math.min(85, Math.round(radians * (180 / Math.PI)));
84
+ }
85
+
86
+ export function evaluateAgilityTier(twr: number): AgilityTier {
87
+ if (twr < 2.0) return 'underpowered';
88
+ if (twr < 4.0) return 'cinematic';
89
+ if (twr < 8.0) return 'freestyle';
90
+ if (twr < 13.0) return 'acro_pro';
91
+ return 'racing_extreme';
92
+ }
93
+
94
+ function getTpaSetting(twr: number): string {
95
+ if (twr >= 12) return 'TPA 0.75 @ 1250';
96
+ if (twr >= 8) return 'TPA 0.65 @ 1350';
97
+ return 'TPA 0.45 @ 1500';
98
+ }
99
+
100
+ function getDynamicIdleSetting(auwGrams: number): string {
101
+ if (auwGrams < 100) return 'Idle 4.0% (Micro)';
102
+ if (auwGrams > 1000) return 'Idle 6.0% (Heavy)';
103
+ return 'Idle 5.5% (Standard 5")';
104
+ }
105
+
106
+ function getPropwashRisk(twr: number): string {
107
+ if (twr >= 10) return 'High Authority (Low Wash)';
108
+ if (twr >= 5) return 'Moderate (Tune D-Term)';
109
+ return 'High Wash Sensitivity';
110
+ }
111
+
112
+ export function evaluateBetaflightTune(twr: number, auwGrams: number): BetaflightTuneAdvice {
113
+ return {
114
+ tpaSetting: getTpaSetting(twr),
115
+ dynamicIdleSetting: getDynamicIdleSetting(auwGrams),
116
+ propwashRisk: getPropwashRisk(twr),
117
+ };
118
+ }
119
+
120
+ export function calculateTwrMetrics(inputs: DroneTwrInputs): DroneTwrResults {
121
+ const totalMaxThrustGrams = calculateTotalMaxThrust(inputs.motorCount, inputs.thrustPerMotorGrams);
122
+ const twrRatio = calculateTwr(totalMaxThrustGrams, inputs.auwGrams);
123
+ const hoverThrottlePercent = calculateHoverThrottle(twrRatio);
124
+ const currentThrustGrams = calculateCurrentThrust(totalMaxThrustGrams, inputs.throttleStickPercent);
125
+ const instantGForce = inputs.auwGrams > 0 ? Math.round((currentThrustGrams / inputs.auwGrams) * 100) / 100 : 0;
126
+ const zeroToHundredTimeSec = calculateZeroToHundred(twrRatio);
127
+ const recommendedCamAngleDeg = calculateRecommendedCamAngle(twrRatio);
128
+ const windResistanceKmh = calculateWindResistance(twrRatio);
129
+ const maxPitchAngleDeg = calculateMaxPitchAngle(twrRatio);
130
+ const agilityTier = evaluateAgilityTier(twrRatio);
131
+ const tuneAdvice = evaluateBetaflightTune(twrRatio, inputs.auwGrams);
132
+
133
+ return {
134
+ totalMaxThrustGrams,
135
+ twrRatio,
136
+ hoverThrottlePercent,
137
+ currentThrottlePercent: inputs.throttleStickPercent,
138
+ currentThrustGrams,
139
+ instantGForce,
140
+ zeroToHundredTimeSec,
141
+ recommendedCamAngleDeg,
142
+ windResistanceKmh,
143
+ maxPitchAngleDeg,
144
+ agilityTier,
145
+ tuneAdvice,
146
+ };
147
+ }
@@ -0,0 +1,16 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { fpvDroneThrustToWeightRatio } from './entry';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props;
11
+ const loader = fpvDroneThrustToWeightRatio.i18n[locale] || fpvDroneThrustToWeightRatio.i18n.en;
12
+ const content = await loader?.();
13
+ if (!content) return null;
14
+ ---
15
+
16
+ {content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,19 @@
1
+ import type { DroneTwrInputs } from './logic';
2
+
3
+ const STORAGE_KEY = 'jjl_drone_twr_inputs';
4
+
5
+ export function loadSavedInputs(): Partial<DroneTwrInputs> {
6
+ try {
7
+ const raw = localStorage.getItem(STORAGE_KEY);
8
+ if (!raw) return {};
9
+ return JSON.parse(raw) as Partial<DroneTwrInputs>;
10
+ } catch {
11
+ return {};
12
+ }
13
+ }
14
+
15
+ export function saveInputs(inputs: DroneTwrInputs): void {
16
+ try {
17
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(inputs));
18
+ } catch {}
19
+ }
@@ -0,0 +1,58 @@
1
+ export interface FpvDroneThrustToWeightRatioUI {
2
+ [key: string]: string;
3
+ title: string;
4
+ subtitle: string;
5
+ presetsHeader: string;
6
+ customPreset: string;
7
+ whoop1sPreset: string;
8
+ freestyle35Preset: string;
9
+ freestyle5Preset: string;
10
+ longrange7Preset: string;
11
+ cinelifter8Preset: string;
12
+ specsHeader: string;
13
+ auwGramsLabel: string;
14
+ motorCountLabel: string;
15
+ thrustPerMotorLabel: string;
16
+ propellerSizeLabel: string;
17
+ propellerPitchLabel: string;
18
+ bladeCountLabel: string;
19
+ blade2Option: string;
20
+ blade3Option: string;
21
+ blade4Option: string;
22
+ throttleStickHeader: string;
23
+ throttleStickLabel: string;
24
+ snapIdleLabel: string;
25
+ snapHoverLabel: string;
26
+ snapCruiseLabel: string;
27
+ snapPunchLabel: string;
28
+ telemetryHeader: string;
29
+ twrRatioLabel: string;
30
+ hoverThrottleLabel: string;
31
+ currentThrustLabel: string;
32
+ instantGForceLabel: string;
33
+ zeroToHundredLabel: string;
34
+ recommendedCamAngleLabel: string;
35
+ windResistanceLabel: string;
36
+ totalMaxThrustLabel: string;
37
+ maxPitchAngleLabel: string;
38
+ tuningHeader: string;
39
+ tpaRecommendationLabel: string;
40
+ dynamicIdleLabel: string;
41
+ propwashRiskLabel: string;
42
+ tierUnderpoweredTitle: string;
43
+ tierUnderpoweredDesc: string;
44
+ tierCinematicTitle: string;
45
+ tierCinematicDesc: string;
46
+ tierFreestyleTitle: string;
47
+ tierFreestyleDesc: string;
48
+ tierAcroProTitle: string;
49
+ tierAcroProDesc: string;
50
+ tierRacingExtremeTitle: string;
51
+ tierRacingExtremeDesc: string;
52
+ hudThrustCurveTitle: string;
53
+ hudHoverMarker: string;
54
+ hudCurrentStickMarker: string;
55
+ hudGForceLabel: string;
56
+ hudTiltAngleLabel: string;
57
+ hudVectorPowerLabel: string;
58
+ }
package/src/tools.ts CHANGED
@@ -5,6 +5,7 @@ import { GPS_COORDINATES_CONVERTER_TOOL } from './tool/gps-coordinates-converter
5
5
  import { DRONE_POWER_ANALYZER_TOOL } from './tool/drone-power-analyzer/index';
6
6
  import { GSD_FLIGHT_PLANNER_TOOL } from './tool/gsd-flight-planner/index';
7
7
  import { DRONE_BATTERY_C_RATING_CALCULATOR_TOOL } from './tool/drone-battery-c-rating-calculator/index';
8
+ import { FPV_DRONE_THRUST_TO_WEIGHT_RATIO_TOOL } from './tool/fpv-drone-thrust-to-weight-ratio/index';
8
9
  import type { ToolDefinition } from './types';
9
10
 
10
11
  export const ALL_TOOLS: ToolDefinition[] = [
@@ -14,6 +15,7 @@ export const ALL_TOOLS: ToolDefinition[] = [
14
15
  DRONE_POWER_ANALYZER_TOOL,
15
16
  GSD_FLIGHT_PLANNER_TOOL,
16
17
  DRONE_BATTERY_C_RATING_CALCULATOR_TOOL,
18
+ FPV_DRONE_THRUST_TO_WEIGHT_RATIO_TOOL,
17
19
  ];
18
20
 
19
21
  export {
@@ -23,4 +25,5 @@ export {
23
25
  DRONE_POWER_ANALYZER_TOOL,
24
26
  GSD_FLIGHT_PLANNER_TOOL,
25
27
  DRONE_BATTERY_C_RATING_CALCULATOR_TOOL,
28
+ FPV_DRONE_THRUST_TO_WEIGHT_RATIO_TOOL,
26
29
  };