@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.
- package/package.json +1 -1
- package/src/category/index.ts +2 -0
- package/src/entries.ts +4 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/drone-battery-c-rating-calculator/bibliography.astro +3 -14
- package/src/tool/drone-battery-c-rating-calculator/bibliography.ts +3 -1
- package/src/tool/fpv-drone-thrust-to-weight-ratio/bibliography.astro +6 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/bibliography.ts +18 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/component.astro +203 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/controller.ts +240 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/dom-views.ts +148 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/entry.ts +27 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/evaluator.ts +30 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/fpv-drone-thrust-to-weight-ratio.css +404 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/de.ts +208 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/en.ts +208 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/es.ts +208 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/fr.ts +208 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/id.ts +208 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/it.ts +208 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/ja.ts +208 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/ko.ts +208 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/nl.ts +208 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/pl.ts +208 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/pt.ts +208 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/ru.ts +208 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/sv.ts +208 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/tr.ts +208 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/zh.ts +208 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/index.ts +10 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/logic.test.ts +93 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/logic.ts +147 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/seo.astro +16 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/storage.ts +19 -0
- package/src/tool/fpv-drone-thrust-to-weight-ratio/ui.ts +58 -0
- package/src/tools.ts +3 -0
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { gpsCoordinatesConverter } from '../tool/gps-coordinates-converter/entry
|
|
|
5
5
|
import { dronePowerAnalyzer } from '../tool/drone-power-analyzer/entry';
|
|
6
6
|
import { gsdFlightPlanner } from '../tool/gsd-flight-planner/entry';
|
|
7
7
|
import { droneBatteryCRatingCalculator } from '../tool/drone-battery-c-rating-calculator/entry';
|
|
8
|
+
import { fpvDroneThrustToWeightRatio } from '../tool/fpv-drone-thrust-to-weight-ratio/entry';
|
|
8
9
|
|
|
9
10
|
export const dronesCategory: DronesCategoryEntry = {
|
|
10
11
|
icon: 'mdi:drone',
|
|
@@ -15,6 +16,7 @@ export const dronesCategory: DronesCategoryEntry = {
|
|
|
15
16
|
dronePowerAnalyzer,
|
|
16
17
|
gsdFlightPlanner,
|
|
17
18
|
droneBatteryCRatingCalculator,
|
|
19
|
+
fpvDroneThrustToWeightRatio,
|
|
18
20
|
],
|
|
19
21
|
i18n: {
|
|
20
22
|
es: () => import('./i18n/es').then((m) => m.content),
|
package/src/entries.ts
CHANGED
|
@@ -10,6 +10,8 @@ export { gsdFlightPlanner } from './tool/gsd-flight-planner/entry';
|
|
|
10
10
|
export type { GsdFlightPlannerUI, GsdFlightPlannerLocaleContent } from './tool/gsd-flight-planner/entry';
|
|
11
11
|
export { droneBatteryCRatingCalculator } from './tool/drone-battery-c-rating-calculator/entry';
|
|
12
12
|
export type { DroneBatteryCRatingCalculatorUI, DroneBatteryCRatingCalculatorLocaleContent } from './tool/drone-battery-c-rating-calculator/entry';
|
|
13
|
+
export { fpvDroneThrustToWeightRatio } from './tool/fpv-drone-thrust-to-weight-ratio/entry';
|
|
14
|
+
export type { FpvDroneThrustToWeightRatioUI, FpvDroneThrustToWeightRatioLocaleContent } from './tool/fpv-drone-thrust-to-weight-ratio/entry';
|
|
13
15
|
export { dronesCategory } from './category';
|
|
14
16
|
|
|
15
17
|
import { antennaLengthCalculator } from './tool/antenna-length-calculator/entry';
|
|
@@ -18,6 +20,7 @@ import { gpsCoordinatesConverter } from './tool/gps-coordinates-converter/entry'
|
|
|
18
20
|
import { dronePowerAnalyzer } from './tool/drone-power-analyzer/entry';
|
|
19
21
|
import { gsdFlightPlanner } from './tool/gsd-flight-planner/entry';
|
|
20
22
|
import { droneBatteryCRatingCalculator } from './tool/drone-battery-c-rating-calculator/entry';
|
|
23
|
+
import { fpvDroneThrustToWeightRatio } from './tool/fpv-drone-thrust-to-weight-ratio/entry';
|
|
21
24
|
|
|
22
25
|
export const ALL_ENTRIES = [
|
|
23
26
|
antennaLengthCalculator,
|
|
@@ -26,4 +29,5 @@ export const ALL_ENTRIES = [
|
|
|
26
29
|
dronePowerAnalyzer,
|
|
27
30
|
gsdFlightPlanner,
|
|
28
31
|
droneBatteryCRatingCalculator,
|
|
32
|
+
fpvDroneThrustToWeightRatio,
|
|
29
33
|
];
|
|
@@ -4,8 +4,8 @@ import { dronesCategory } 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 7 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(7);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('dronesCategory should be defined', () => {
|
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
import {
|
|
2
|
+
import { Bibliography as BibliographyComponent } from "@jjlmoya/utils-shared";
|
|
3
|
+
import { bibliography } from "./bibliography";
|
|
3
4
|
---
|
|
4
5
|
|
|
5
|
-
<
|
|
6
|
-
<ul>
|
|
7
|
-
{
|
|
8
|
-
BIBLIOGRAPHY_ITEMS.map((item) => (
|
|
9
|
-
<li>
|
|
10
|
-
<a href={item.url} target="_blank" rel="noopener noreferrer">
|
|
11
|
-
{item.name}
|
|
12
|
-
</a>
|
|
13
|
-
</li>
|
|
14
|
-
))
|
|
15
|
-
}
|
|
16
|
-
</ul>
|
|
17
|
-
</div>
|
|
6
|
+
<BibliographyComponent links={bibliography} />
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BibliographyEntry } from '../../types';
|
|
2
2
|
|
|
3
|
-
export const
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
4
|
{
|
|
5
5
|
name: 'Battery University - BU-409: Charging Lithium-ion',
|
|
6
6
|
url: 'http://www.batteryuniversity.com/article/bu-409-charging-lithium-ion/',
|
|
@@ -14,3 +14,5 @@ export const BIBLIOGRAPHY_ITEMS: BibliographyEntry[] = [
|
|
|
14
14
|
url: 'https://betaflight.com/docs/wiki/guides/current/Battery',
|
|
15
15
|
},
|
|
16
16
|
];
|
|
17
|
+
|
|
18
|
+
export const BIBLIOGRAPHY_ITEMS = bibliography;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
|
+
{
|
|
5
|
+
name: 'Betaflight Firmware Documentation - Multirotor Flight Dynamics and Thrust Tuning',
|
|
6
|
+
url: 'https://betaflight.com/docs/wiki/tuning/4-3-Tuning-Notes',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'IEEE Xplore - Dynamic Thrust and Power Modeling of Brushless Multirotor Propellers',
|
|
10
|
+
url: 'https://ieeexplore.ieee.org/document/7036341/',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'NASA Technical Reports Server - Aerodynamic Performance of Small Multirotor Propellers in Hover',
|
|
14
|
+
url: 'https://ntrs.nasa.gov/api/citations/20250009190/downloads/NASA-TM-20250009190.pdf?attachment=true',
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export const BIBLIOGRAPHY_ITEMS = bibliography;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { FpvDroneThrustToWeightRatioUI } from './ui';
|
|
3
|
+
import { calculateTwrMetrics, type DroneTwrInputs } from './logic';
|
|
4
|
+
import { renderTwrSVG } from './dom-views';
|
|
5
|
+
import './fpv-drone-thrust-to-weight-ratio.css';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
ui: FpvDroneThrustToWeightRatioUI;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { ui } = Astro.props;
|
|
12
|
+
|
|
13
|
+
const defaultInputs: DroneTwrInputs = {
|
|
14
|
+
auwGrams: 680,
|
|
15
|
+
motorCount: 4,
|
|
16
|
+
thrustPerMotorGrams: 1950,
|
|
17
|
+
propellerSizeInches: 5.1,
|
|
18
|
+
propellerPitchInches: 4.3,
|
|
19
|
+
bladeCount: 3,
|
|
20
|
+
throttleStickPercent: 70,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const initialResults = calculateTwrMetrics(defaultInputs);
|
|
24
|
+
const initialSvg = renderTwrSVG(defaultInputs, initialResults, ui);
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
<div class="sc-fpv-cockpit" id="fpv-drone-thrust-to-weight-ratio-root">
|
|
28
|
+
<div class="sc-cockpit-sidebar">
|
|
29
|
+
<div>
|
|
30
|
+
<div class="sc-cockpit-title">{ui.presetsHeader}</div>
|
|
31
|
+
<div class="sc-preset-group">
|
|
32
|
+
<button type="button" class="sc-preset-chip active" data-preset="freestyle5">{ui.freestyle5Preset}</button>
|
|
33
|
+
<button type="button" class="sc-preset-chip" data-preset="freestyle35">{ui.freestyle35Preset}</button>
|
|
34
|
+
<button type="button" class="sc-preset-chip" data-preset="whoop1s">{ui.whoop1sPreset}</button>
|
|
35
|
+
<button type="button" class="sc-preset-chip" data-preset="longrange7">{ui.longrange7Preset}</button>
|
|
36
|
+
<button type="button" class="sc-preset-chip" data-preset="cinelifter8">{ui.cinelifter8Preset}</button>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div>
|
|
41
|
+
<div class="sc-cockpit-title">{ui.specsHeader}</div>
|
|
42
|
+
<div class="sc-field-group">
|
|
43
|
+
<div class="sc-field-header">
|
|
44
|
+
<label for="input-auw">{ui.auwGramsLabel}</label>
|
|
45
|
+
</div>
|
|
46
|
+
<input type="number" id="input-auw" class="sc-input" value="680" min="10" max="15000" step="5" />
|
|
47
|
+
<input type="range" id="range-auw" class="sc-slider" value="680" min="20" max="2500" step="5" aria-label={ui.auwGramsLabel} />
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div class="sc-field-group" style="margin-top: 0.8rem;">
|
|
51
|
+
<label>{ui.motorCountLabel}</label>
|
|
52
|
+
<div class="sc-custom-select" data-custom-select="motor-count">
|
|
53
|
+
<div class="sc-select-trigger">
|
|
54
|
+
<span>4 Motors (Quadcopter)</span>
|
|
55
|
+
<svg width="12" height="12" viewBox="0 0 24 24"><path d="M7 10l5 5 5-5z"/></svg>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="sc-select-dropdown">
|
|
58
|
+
<div class="sc-select-option active" data-value="4">4 Motors (Quadcopter)</div>
|
|
59
|
+
<div class="sc-select-option" data-value="6">6 Motors (Hexacopter)</div>
|
|
60
|
+
<div class="sc-select-option" data-value="8">8 Motors (Octocopter / X8)</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="sc-field-group" style="margin-top: 0.8rem;">
|
|
66
|
+
<div class="sc-field-header">
|
|
67
|
+
<label for="input-thrust-motor">{ui.thrustPerMotorLabel}</label>
|
|
68
|
+
</div>
|
|
69
|
+
<input type="number" id="input-thrust-motor" class="sc-input" value="1950" min="10" max="10000" step="10" />
|
|
70
|
+
<input type="range" id="range-thrust-motor" class="sc-slider" value="1950" min="50" max="4000" step="25" aria-label={ui.thrustPerMotorLabel} />
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div class="sc-field-group" style="margin-top: 0.8rem;">
|
|
74
|
+
<label>{ui.bladeCountLabel}</label>
|
|
75
|
+
<div class="sc-custom-select" data-custom-select="blade-count">
|
|
76
|
+
<div class="sc-select-trigger">
|
|
77
|
+
<span>{ui.blade3Option}</span>
|
|
78
|
+
<svg width="12" height="12" viewBox="0 0 24 24"><path d="M7 10l5 5 5-5z"/></svg>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="sc-select-dropdown">
|
|
81
|
+
<div class="sc-select-option" data-value="2">{ui.blade2Option}</div>
|
|
82
|
+
<div class="sc-select-option active" data-value="3">{ui.blade3Option}</div>
|
|
83
|
+
<div class="sc-select-option" data-value="4">{ui.blade4Option}</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div class="sc-field-group" style="margin-top: 0.8rem;">
|
|
89
|
+
<div class="sc-field-header">
|
|
90
|
+
<label for="input-prop-size">{ui.propellerSizeLabel}</label>
|
|
91
|
+
</div>
|
|
92
|
+
<input type="number" id="input-prop-size" class="sc-input" value="5.1" min="1" max="15" step="0.1" />
|
|
93
|
+
<input type="range" id="range-prop-size" class="sc-slider" value="5.1" min="1.5" max="10" step="0.1" aria-label={ui.propellerSizeLabel} />
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div class="sc-field-group" style="margin-top: 0.8rem;">
|
|
97
|
+
<div class="sc-field-header">
|
|
98
|
+
<label for="input-prop-pitch">{ui.propellerPitchLabel}</label>
|
|
99
|
+
</div>
|
|
100
|
+
<input type="number" id="input-prop-pitch" class="sc-input" value="4.3" min="1" max="10" step="0.1" />
|
|
101
|
+
<input type="range" id="range-prop-pitch" class="sc-slider" value="4.3" min="1.5" max="6.5" step="0.1" aria-label={ui.propellerPitchLabel} />
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div class="sc-throttle-box">
|
|
106
|
+
<div class="sc-field-header">
|
|
107
|
+
<label for="range-throttle-stick" class="sc-throttle-label">{ui.throttleStickLabel}</label>
|
|
108
|
+
<span class="sc-throttle-badge" data-metric="stickPercentBadge">70%</span>
|
|
109
|
+
</div>
|
|
110
|
+
<input type="range" id="range-throttle-stick" class="sc-slider sc-throttle-slider" value="70" min="0" max="100" step="1" aria-label={ui.throttleStickLabel} />
|
|
111
|
+
<div class="sc-snap-group">
|
|
112
|
+
<button type="button" class="sc-snap-btn" data-snap="idle">{ui.snapIdleLabel}</button>
|
|
113
|
+
<button type="button" class="sc-snap-btn" data-snap="hover">{ui.snapHoverLabel}</button>
|
|
114
|
+
<button type="button" class="sc-snap-btn" data-snap="cruise">{ui.snapCruiseLabel}</button>
|
|
115
|
+
<button type="button" class="sc-snap-btn" data-snap="punch">{ui.snapPunchLabel}</button>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div class="sc-cockpit-telemetry">
|
|
121
|
+
<div class="sc-telemetry-topbar">
|
|
122
|
+
<div class="sc-cockpit-title" style="margin-bottom: 0;">{ui.telemetryHeader}</div>
|
|
123
|
+
<div class="sc-safety-badge status-accent" data-metric="agilityBadge">{ui.tierAcroProTitle}</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div class="sc-hud-wrapper" data-element="svgContainer" set:html={initialSvg}></div>
|
|
127
|
+
|
|
128
|
+
<div class="sc-hud-metrics-grid">
|
|
129
|
+
<div class="sc-hud-card">
|
|
130
|
+
<div class="sc-hud-label">{ui.twrRatioLabel}</div>
|
|
131
|
+
<div class="sc-hud-val" data-metric="twrRatio">{initialResults.twrRatio.toFixed(2)} : 1</div>
|
|
132
|
+
</div>
|
|
133
|
+
<div class="sc-hud-card">
|
|
134
|
+
<div class="sc-hud-label">{ui.hoverThrottleLabel}</div>
|
|
135
|
+
<div class="sc-hud-val" data-metric="hoverThrottle">{initialResults.hoverThrottlePercent.toFixed(1)} %</div>
|
|
136
|
+
</div>
|
|
137
|
+
<div class="sc-hud-card highlight-cyan">
|
|
138
|
+
<div class="sc-hud-label">{ui.currentThrustLabel}</div>
|
|
139
|
+
<div class="sc-hud-val" data-metric="currentThrust">{initialResults.currentThrustGrams.toLocaleString()} g</div>
|
|
140
|
+
</div>
|
|
141
|
+
<div class="sc-hud-card highlight-purple">
|
|
142
|
+
<div class="sc-hud-label">{ui.instantGForceLabel}</div>
|
|
143
|
+
<div class="sc-hud-val" data-metric="instantGForce">{initialResults.instantGForce.toFixed(2)} G</div>
|
|
144
|
+
</div>
|
|
145
|
+
<div class="sc-hud-card">
|
|
146
|
+
<div class="sc-hud-label">{ui.zeroToHundredLabel}</div>
|
|
147
|
+
<div class="sc-hud-val" data-metric="zeroToHundred">{initialResults.zeroToHundredTimeSec.toFixed(2)} s</div>
|
|
148
|
+
</div>
|
|
149
|
+
<div class="sc-hud-card">
|
|
150
|
+
<div class="sc-hud-label">{ui.recommendedCamAngleLabel}</div>
|
|
151
|
+
<div class="sc-hud-val" data-metric="recommendedCamAngle">{initialResults.recommendedCamAngleDeg}°</div>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="sc-hud-card">
|
|
154
|
+
<div class="sc-hud-label">{ui.windResistanceLabel}</div>
|
|
155
|
+
<div class="sc-hud-val" data-metric="windResistance">{initialResults.windResistanceKmh} km/h</div>
|
|
156
|
+
</div>
|
|
157
|
+
<div class="sc-hud-card">
|
|
158
|
+
<div class="sc-hud-label">{ui.totalMaxThrustLabel}</div>
|
|
159
|
+
<div class="sc-hud-val" data-metric="totalMaxThrust">{initialResults.totalMaxThrustGrams.toLocaleString()} g</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div class="sc-tuning-grid">
|
|
164
|
+
<div class="sc-tuning-card">
|
|
165
|
+
<div class="sc-hud-label">{ui.tpaRecommendationLabel}</div>
|
|
166
|
+
<div class="sc-tuning-val" data-metric="tpaSetting">{initialResults.tuneAdvice.tpaSetting}</div>
|
|
167
|
+
</div>
|
|
168
|
+
<div class="sc-tuning-card">
|
|
169
|
+
<div class="sc-hud-label">{ui.dynamicIdleLabel}</div>
|
|
170
|
+
<div class="sc-tuning-val" data-metric="dynamicIdleSetting">{initialResults.tuneAdvice.dynamicIdleSetting}</div>
|
|
171
|
+
</div>
|
|
172
|
+
<div class="sc-tuning-card">
|
|
173
|
+
<div class="sc-hud-label">{ui.propwashRiskLabel}</div>
|
|
174
|
+
<div class="sc-tuning-val" data-metric="propwashRisk">{initialResults.tuneAdvice.propwashRisk}</div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div class="sc-flight-status-card">
|
|
179
|
+
<h4 data-metric="statusTitle">{ui.tierAcroProTitle}</h4>
|
|
180
|
+
<p data-metric="statusDesc">{ui.tierAcroProDesc}</p>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<script is:inline type="application/json" id="fpv-twr-ui-data" set:html={JSON.stringify(ui)}></script>
|
|
186
|
+
|
|
187
|
+
<script>
|
|
188
|
+
import { FpvDroneThrustToWeightRatioController } from './controller';
|
|
189
|
+
|
|
190
|
+
function initController() {
|
|
191
|
+
const root = document.getElementById('fpv-drone-thrust-to-weight-ratio-root');
|
|
192
|
+
if (!root) return;
|
|
193
|
+
const uiEl = document.getElementById('fpv-twr-ui-data');
|
|
194
|
+
const uiData = uiEl ? JSON.parse(uiEl.textContent || '{}') : {};
|
|
195
|
+
new FpvDroneThrustToWeightRatioController(root, uiData);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (document.readyState === 'loading') {
|
|
199
|
+
document.addEventListener('DOMContentLoaded', initController);
|
|
200
|
+
} else {
|
|
201
|
+
initController();
|
|
202
|
+
}
|
|
203
|
+
</script>
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import type { DroneTwrInputs } from './logic';
|
|
2
|
+
import { calculateTwrMetrics } from './logic';
|
|
3
|
+
import { evaluateAgilityPresentation } from './evaluator';
|
|
4
|
+
import { updateDOMMetrics, updateDOMSafety, renderTwrSVG, type DOMResultElements } from './dom-views';
|
|
5
|
+
import { loadSavedInputs, saveInputs } from './storage';
|
|
6
|
+
import type { FpvDroneThrustToWeightRatioUI } from './ui';
|
|
7
|
+
|
|
8
|
+
export interface PresetConfig {
|
|
9
|
+
auwGrams: number;
|
|
10
|
+
motorCount: number;
|
|
11
|
+
thrustPerMotorGrams: number;
|
|
12
|
+
propellerSizeInches: number;
|
|
13
|
+
propellerPitchInches: number;
|
|
14
|
+
bladeCount: number;
|
|
15
|
+
throttleStickPercent: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const PRESETS: Record<string, PresetConfig> = {
|
|
19
|
+
whoop1s: { auwGrams: 32, motorCount: 4, thrustPerMotorGrams: 38, propellerSizeInches: 1.6, propellerPitchInches: 1.5, bladeCount: 4, throttleStickPercent: 50 },
|
|
20
|
+
freestyle35: { auwGrams: 250, motorCount: 4, thrustPerMotorGrams: 750, propellerSizeInches: 3.5, propellerPitchInches: 2.8, bladeCount: 3, throttleStickPercent: 65 },
|
|
21
|
+
freestyle5: { auwGrams: 680, motorCount: 4, thrustPerMotorGrams: 1950, propellerSizeInches: 5.1, propellerPitchInches: 4.3, bladeCount: 3, throttleStickPercent: 70 },
|
|
22
|
+
longrange7: { auwGrams: 1150, motorCount: 4, thrustPerMotorGrams: 2400, propellerSizeInches: 7.0, propellerPitchInches: 4.0, bladeCount: 2, throttleStickPercent: 45 },
|
|
23
|
+
cinelifter8: { auwGrams: 4200, motorCount: 8, thrustPerMotorGrams: 3200, propellerSizeInches: 9.0, propellerPitchInches: 5.0, bladeCount: 3, throttleStickPercent: 55 },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export class FpvDroneThrustToWeightRatioController {
|
|
27
|
+
private container: HTMLElement;
|
|
28
|
+
private ui: FpvDroneThrustToWeightRatioUI;
|
|
29
|
+
private elements!: DOMResultElements;
|
|
30
|
+
private inputs!: DroneTwrInputs;
|
|
31
|
+
private activePreset: string | null = 'freestyle5';
|
|
32
|
+
|
|
33
|
+
constructor(container: HTMLElement, ui: FpvDroneThrustToWeightRatioUI) {
|
|
34
|
+
this.container = container;
|
|
35
|
+
this.ui = ui;
|
|
36
|
+
this.initElements();
|
|
37
|
+
this.initInputs();
|
|
38
|
+
this.bindEvents();
|
|
39
|
+
this.recalculate();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private initElements(): void {
|
|
43
|
+
this.elements = {
|
|
44
|
+
twrRatio: this.container.querySelector('[data-metric="twrRatio"]')!,
|
|
45
|
+
hoverThrottle: this.container.querySelector('[data-metric="hoverThrottle"]')!,
|
|
46
|
+
currentThrust: this.container.querySelector('[data-metric="currentThrust"]')!,
|
|
47
|
+
instantGForce: this.container.querySelector('[data-metric="instantGForce"]')!,
|
|
48
|
+
zeroToHundred: this.container.querySelector('[data-metric="zeroToHundred"]')!,
|
|
49
|
+
recommendedCamAngle: this.container.querySelector('[data-metric="recommendedCamAngle"]')!,
|
|
50
|
+
windResistance: this.container.querySelector('[data-metric="windResistance"]')!,
|
|
51
|
+
totalMaxThrust: this.container.querySelector('[data-metric="totalMaxThrust"]')!,
|
|
52
|
+
maxPitchAngle: this.container.querySelector('[data-metric="maxPitchAngle"]')!,
|
|
53
|
+
tpaSetting: this.container.querySelector('[data-metric="tpaSetting"]') || undefined,
|
|
54
|
+
dynamicIdleSetting: this.container.querySelector('[data-metric="dynamicIdleSetting"]') || undefined,
|
|
55
|
+
propwashRisk: this.container.querySelector('[data-metric="propwashRisk"]') || undefined,
|
|
56
|
+
agilityBadge: this.container.querySelector('[data-metric="agilityBadge"]')!,
|
|
57
|
+
statusTitle: this.container.querySelector('[data-metric="statusTitle"]')!,
|
|
58
|
+
statusDesc: this.container.querySelector('[data-metric="statusDesc"]')!,
|
|
59
|
+
svgContainer: this.container.querySelector('[data-element="svgContainer"]')!,
|
|
60
|
+
stickPercentBadge: this.container.querySelector('[data-metric="stickPercentBadge"]') || undefined,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private initInputs(): void {
|
|
65
|
+
const saved = loadSavedInputs();
|
|
66
|
+
this.inputs = Object.assign({}, PRESETS.freestyle5, saved);
|
|
67
|
+
this.syncInputElements();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private parseVal(selector: string): number {
|
|
71
|
+
const el = this.container.querySelector(selector) as HTMLInputElement;
|
|
72
|
+
if (!el || !el.value) return 0;
|
|
73
|
+
const cleanStr = el.value.replace(',', '.');
|
|
74
|
+
const val = parseFloat(cleanStr);
|
|
75
|
+
return Number.isFinite(val) ? val : 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private syncInputElements(): void {
|
|
79
|
+
this.setInputValue('#input-auw', String(this.inputs.auwGrams));
|
|
80
|
+
this.setInputValue('#range-auw', String(this.inputs.auwGrams));
|
|
81
|
+
this.setInputValue('#input-thrust-motor', String(this.inputs.thrustPerMotorGrams));
|
|
82
|
+
this.setInputValue('#range-thrust-motor', String(this.inputs.thrustPerMotorGrams));
|
|
83
|
+
this.setInputValue('#input-prop-size', String(this.inputs.propellerSizeInches));
|
|
84
|
+
this.setInputValue('#range-prop-size', String(this.inputs.propellerSizeInches));
|
|
85
|
+
this.setInputValue('#input-prop-pitch', String(this.inputs.propellerPitchInches));
|
|
86
|
+
this.setInputValue('#range-prop-pitch', String(this.inputs.propellerPitchInches));
|
|
87
|
+
this.setInputValue('#range-throttle-stick', String(this.inputs.throttleStickPercent));
|
|
88
|
+
|
|
89
|
+
this.updateCustomSelect('motor-count', String(this.inputs.motorCount));
|
|
90
|
+
this.updateCustomSelect('blade-count', String(this.inputs.bladeCount));
|
|
91
|
+
this.updatePresetChips();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private setInputValue(selector: string, val: string): void {
|
|
95
|
+
const el = this.container.querySelector(selector) as HTMLInputElement;
|
|
96
|
+
if (el) el.value = val;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private updatePresetChips(): void {
|
|
100
|
+
this.container.querySelectorAll('[data-preset]').forEach((btn) => {
|
|
101
|
+
const key = btn.getAttribute('data-preset');
|
|
102
|
+
btn.classList.toggle('active', key === this.activePreset);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private updateCustomSelect(name: string, value: string): void {
|
|
107
|
+
const wrapper = this.container.querySelector(`[data-custom-select="${name}"]`);
|
|
108
|
+
if (!wrapper) return;
|
|
109
|
+
const triggerText = wrapper.querySelector('.sc-select-trigger span');
|
|
110
|
+
const options = wrapper.querySelectorAll('.sc-select-option');
|
|
111
|
+
options.forEach((opt) => {
|
|
112
|
+
const isSelected = opt.getAttribute('data-value') === value;
|
|
113
|
+
opt.classList.toggle('active', isSelected);
|
|
114
|
+
if (isSelected && triggerText) triggerText.textContent = opt.textContent;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private bindEvents(): void {
|
|
119
|
+
this.bindNumberAndRangeInputs();
|
|
120
|
+
this.bindCustomSelects();
|
|
121
|
+
this.bindPresets();
|
|
122
|
+
this.bindThrottleStick();
|
|
123
|
+
this.bindQuickSnapButtons();
|
|
124
|
+
document.addEventListener('click', () => this.closeAllSelects());
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private bindNumberAndRangeInputs(): void {
|
|
128
|
+
this.bindRangeSync('#input-auw', '#range-auw', (v) => { this.inputs.auwGrams = v; });
|
|
129
|
+
this.bindRangeSync('#input-thrust-motor', '#range-thrust-motor', (v) => { this.inputs.thrustPerMotorGrams = v; });
|
|
130
|
+
this.bindRangeSync('#input-prop-size', '#range-prop-size', (v) => { this.inputs.propellerSizeInches = v; });
|
|
131
|
+
this.bindRangeSync('#input-prop-pitch', '#range-prop-pitch', (v) => { this.inputs.propellerPitchInches = v; });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private bindThrottleStick(): void {
|
|
135
|
+
const stick = this.container.querySelector('#range-throttle-stick') as HTMLInputElement;
|
|
136
|
+
stick?.addEventListener('input', () => {
|
|
137
|
+
this.inputs.throttleStickPercent = parseFloat(stick.value) || 0;
|
|
138
|
+
this.recalculate();
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private bindQuickSnapButtons(): void {
|
|
143
|
+
this.container.querySelectorAll('[data-snap]').forEach((btn) => {
|
|
144
|
+
btn.addEventListener('click', () => {
|
|
145
|
+
const snapType = btn.getAttribute('data-snap');
|
|
146
|
+
const results = calculateTwrMetrics(this.inputs);
|
|
147
|
+
if (snapType === 'idle') this.inputs.throttleStickPercent = 0;
|
|
148
|
+
if (snapType === 'hover') this.inputs.throttleStickPercent = results.hoverThrottlePercent;
|
|
149
|
+
if (snapType === 'cruise') this.inputs.throttleStickPercent = 50;
|
|
150
|
+
if (snapType === 'punch') this.inputs.throttleStickPercent = 100;
|
|
151
|
+
this.setInputValue('#range-throttle-stick', String(this.inputs.throttleStickPercent));
|
|
152
|
+
this.recalculate();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private bindCustomSelects(): void {
|
|
158
|
+
this.container.querySelectorAll('.sc-custom-select').forEach((wrapper) => {
|
|
159
|
+
const trigger = wrapper.querySelector('.sc-select-trigger');
|
|
160
|
+
trigger?.addEventListener('click', (e) => {
|
|
161
|
+
e.stopPropagation();
|
|
162
|
+
this.closeAllSelects(wrapper);
|
|
163
|
+
wrapper.classList.toggle('open');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
wrapper.querySelectorAll('.sc-select-option').forEach((opt) => {
|
|
167
|
+
opt.addEventListener('click', (e) => {
|
|
168
|
+
e.stopPropagation();
|
|
169
|
+
this.handleSelectOptionClick(wrapper, opt);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private handleSelectOptionClick(wrapper: Element, opt: Element): void {
|
|
176
|
+
const val = opt.getAttribute('data-value')!;
|
|
177
|
+
const name = wrapper.getAttribute('data-custom-select')!;
|
|
178
|
+
if (name === 'motor-count') this.inputs.motorCount = parseInt(val, 10);
|
|
179
|
+
if (name === 'blade-count') this.inputs.bladeCount = parseInt(val, 10);
|
|
180
|
+
this.updateCustomSelect(name, val);
|
|
181
|
+
wrapper.classList.remove('open');
|
|
182
|
+
this.onInputChanged();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private bindPresets(): void {
|
|
186
|
+
this.container.querySelectorAll('[data-preset]').forEach((btn) => {
|
|
187
|
+
btn.addEventListener('click', () => {
|
|
188
|
+
const key = btn.getAttribute('data-preset')!;
|
|
189
|
+
if (PRESETS[key]) {
|
|
190
|
+
this.activePreset = key;
|
|
191
|
+
this.inputs = { ...PRESETS[key] };
|
|
192
|
+
this.syncInputElements();
|
|
193
|
+
this.recalculate();
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private onInputChanged(): void {
|
|
200
|
+
this.activePreset = null;
|
|
201
|
+
this.updatePresetChips();
|
|
202
|
+
this.recalculate();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private bindRangeSync(textSel: string, rangeSel: string, setter: (val: number) => void): void {
|
|
206
|
+
const textEl = this.container.querySelector(textSel) as HTMLInputElement;
|
|
207
|
+
const rangeEl = this.container.querySelector(rangeSel) as HTMLInputElement;
|
|
208
|
+
|
|
209
|
+
textEl?.addEventListener('input', () => {
|
|
210
|
+
const val = this.parseVal(textSel);
|
|
211
|
+
if (rangeEl) rangeEl.value = String(val);
|
|
212
|
+
setter(val);
|
|
213
|
+
this.onInputChanged();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
rangeEl?.addEventListener('input', () => {
|
|
217
|
+
const val = parseFloat(rangeEl.value);
|
|
218
|
+
if (textEl) textEl.value = String(val);
|
|
219
|
+
setter(val);
|
|
220
|
+
this.onInputChanged();
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private closeAllSelects(except?: Element): void {
|
|
225
|
+
this.container.querySelectorAll('.sc-custom-select').forEach((w) => {
|
|
226
|
+
if (w !== except) w.classList.remove('open');
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private recalculate(): void {
|
|
231
|
+
const results = calculateTwrMetrics(this.inputs);
|
|
232
|
+
const agilityEval = evaluateAgilityPresentation(results.agilityTier, this.ui);
|
|
233
|
+
updateDOMMetrics(this.elements, results);
|
|
234
|
+
updateDOMSafety(this.elements, agilityEval);
|
|
235
|
+
if (this.elements.svgContainer) {
|
|
236
|
+
this.elements.svgContainer.innerHTML = renderTwrSVG(this.inputs, results, this.ui);
|
|
237
|
+
}
|
|
238
|
+
saveInputs(this.inputs);
|
|
239
|
+
}
|
|
240
|
+
}
|