@jjlmoya/utils-drones 1.30.0 → 1.32.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 (67) hide show
  1. package/package.json +1 -1
  2. package/src/category/i18n/en.ts +1 -1
  3. package/src/category/i18n/fr.ts +1 -1
  4. package/src/category/index.ts +2 -0
  5. package/src/components/PreviewNavSidebar.astro +116 -116
  6. package/src/components/PreviewToolbar.astro +143 -143
  7. package/src/data.ts +1 -1
  8. package/src/entries.ts +4 -0
  9. package/src/env.d.ts +1 -1
  10. package/src/index.ts +1 -1
  11. package/src/layouts/PreviewLayout.astro +118 -118
  12. package/src/pages/[locale].astro +251 -251
  13. package/src/pages/index.astro +4 -4
  14. package/src/tests/faq_count.test.ts +1 -1
  15. package/src/tests/locale_completeness.test.ts +1 -2
  16. package/src/tests/mocks/astro_mock.js +1 -1
  17. package/src/tests/no_h1_in_components.test.ts +1 -1
  18. package/src/tests/tool_validation.test.ts +17 -17
  19. package/src/tool/antenna-length-calculator/bibliography.astro +6 -6
  20. package/src/tool/antenna-length-calculator/component.astro +329 -329
  21. package/src/tool/antenna-length-calculator/seo.astro +15 -15
  22. package/src/tool/drone-battery-c-rating-calculator/i18n/de.ts +192 -192
  23. package/src/tool/drone-battery-c-rating-calculator/i18n/en.ts +192 -192
  24. package/src/tool/drone-battery-c-rating-calculator/i18n/es.ts +192 -192
  25. package/src/tool/drone-battery-c-rating-calculator/i18n/id.ts +192 -192
  26. package/src/tool/drone-battery-c-rating-calculator/i18n/ja.ts +192 -192
  27. package/src/tool/drone-battery-c-rating-calculator/i18n/ko.ts +192 -192
  28. package/src/tool/drone-battery-c-rating-calculator/i18n/nl.ts +192 -192
  29. package/src/tool/drone-battery-c-rating-calculator/i18n/pl.ts +192 -192
  30. package/src/tool/drone-battery-c-rating-calculator/i18n/ru.ts +192 -192
  31. package/src/tool/drone-battery-c-rating-calculator/i18n/sv.ts +192 -192
  32. package/src/tool/drone-battery-c-rating-calculator/i18n/tr.ts +192 -192
  33. package/src/tool/drone-battery-c-rating-calculator/i18n/zh.ts +192 -192
  34. package/src/tool/drone-flight-time/component.astro +187 -187
  35. package/src/tool/drone-flight-time/seo.astro +15 -15
  36. package/src/tool/drone-motor-propeller-calculator/bibliography.astro +6 -0
  37. package/src/tool/drone-motor-propeller-calculator/bibliography.ts +12 -0
  38. package/src/tool/drone-motor-propeller-calculator/component.astro +109 -0
  39. package/src/tool/drone-motor-propeller-calculator/controller.ts +174 -0
  40. package/src/tool/drone-motor-propeller-calculator/dom-views.ts +78 -0
  41. package/src/tool/drone-motor-propeller-calculator/drone-motor-propeller-calculator.css +562 -0
  42. package/src/tool/drone-motor-propeller-calculator/entry.ts +27 -0
  43. package/src/tool/drone-motor-propeller-calculator/evaluator.ts +14 -0
  44. package/src/tool/drone-motor-propeller-calculator/i18n/de.ts +202 -0
  45. package/src/tool/drone-motor-propeller-calculator/i18n/en.ts +202 -0
  46. package/src/tool/drone-motor-propeller-calculator/i18n/es.ts +202 -0
  47. package/src/tool/drone-motor-propeller-calculator/i18n/fr.ts +202 -0
  48. package/src/tool/drone-motor-propeller-calculator/i18n/id.ts +192 -0
  49. package/src/tool/drone-motor-propeller-calculator/i18n/it.ts +192 -0
  50. package/src/tool/drone-motor-propeller-calculator/i18n/ja.ts +192 -0
  51. package/src/tool/drone-motor-propeller-calculator/i18n/ko.ts +192 -0
  52. package/src/tool/drone-motor-propeller-calculator/i18n/nl.ts +192 -0
  53. package/src/tool/drone-motor-propeller-calculator/i18n/pl.ts +192 -0
  54. package/src/tool/drone-motor-propeller-calculator/i18n/pt.ts +192 -0
  55. package/src/tool/drone-motor-propeller-calculator/i18n/ru.ts +192 -0
  56. package/src/tool/drone-motor-propeller-calculator/i18n/sv.ts +192 -0
  57. package/src/tool/drone-motor-propeller-calculator/i18n/tr.ts +192 -0
  58. package/src/tool/drone-motor-propeller-calculator/i18n/zh.ts +192 -0
  59. package/src/tool/drone-motor-propeller-calculator/index.ts +11 -0
  60. package/src/tool/drone-motor-propeller-calculator/logic.test.ts +56 -0
  61. package/src/tool/drone-motor-propeller-calculator/logic.ts +108 -0
  62. package/src/tool/drone-motor-propeller-calculator/seo.astro +15 -0
  63. package/src/tool/drone-motor-propeller-calculator/storage.ts +26 -0
  64. package/src/tool/drone-motor-propeller-calculator/ui.ts +55 -0
  65. package/src/tool/gps-coordinates-converter/component.astro +275 -275
  66. package/src/tool/gps-coordinates-converter/seo.astro +15 -15
  67. package/src/tools.ts +3 -0
@@ -0,0 +1,174 @@
1
+ import { calculateDroneMotorMetrics, type BladeCount, type DroneMotorInputs, type MotorCount } from './logic';
2
+ import { evaluateSetup } from './evaluator';
3
+ import { formatInputValue, parseInputValue, renderResults, updateInputUnitLabels } from './dom-views';
4
+ import { readStoredState, writeStoredState, type DisplayUnit } from './storage';
5
+ import type { DroneMotorPropellerUI } from './ui';
6
+
7
+ interface Preset {
8
+ id: string;
9
+ inputs: DroneMotorInputs;
10
+ }
11
+
12
+ const DEFAULT_INPUTS: DroneMotorInputs = {
13
+ motorKv: 1950,
14
+ batteryVoltage: 22.2,
15
+ propDiameterInches: 5.1,
16
+ propPitchInches: 4.3,
17
+ bladeCount: 3,
18
+ motorCount: 4,
19
+ droneWeightGrams: 700,
20
+ benchThrustGrams: 1900,
21
+ benchVoltage: 22.2,
22
+ };
23
+
24
+ const PRESETS: Preset[] = [
25
+ { id: 'tiny-cruiser', inputs: { motorKv: 3500, batteryVoltage: 11.1, propDiameterInches: 3.5, propPitchInches: 3, bladeCount: 3, motorCount: 4, droneWeightGrams: 280, benchThrustGrams: 550, benchVoltage: 11.1 } },
26
+ { id: 'freestyle', inputs: DEFAULT_INPUTS },
27
+ { id: 'long-range', inputs: { motorKv: 1500, batteryVoltage: 22.2, propDiameterInches: 7, propPitchInches: 3.5, bladeCount: 2, motorCount: 4, droneWeightGrams: 1200, benchThrustGrams: 1800, benchVoltage: 22.2 } },
28
+ { id: 'cinelifter', inputs: { motorKv: 700, batteryVoltage: 22.2, propDiameterInches: 8, propPitchInches: 4, bladeCount: 3, motorCount: 8, droneWeightGrams: 4200, benchThrustGrams: 1600, benchVoltage: 22.2 } },
29
+ ];
30
+
31
+ function getInput(root: HTMLElement, field: string): HTMLInputElement | null {
32
+ return root.querySelector<HTMLInputElement>(`[data-field="${field}"]`);
33
+ }
34
+
35
+ function getNumber(root: HTMLElement, field: string, kind: 'mass' | 'length' | 'plain', unit: DisplayUnit): number {
36
+ const element = getInput(root, field);
37
+ if (!element || element.value.trim() === '') return 0;
38
+ return kind === 'plain' ? Number(element.value) : parseInputValue(element.value, kind, unit);
39
+ }
40
+
41
+ function getChoice(root: HTMLElement, field: string, fallback: number): number {
42
+ const selected = root.querySelector<HTMLElement>(`[data-choice="${field}"].is-selected`);
43
+ return Number(selected?.dataset.value ?? fallback);
44
+ }
45
+
46
+ function readInputs(root: HTMLElement, unit: DisplayUnit): DroneMotorInputs {
47
+ return {
48
+ motorKv: getNumber(root, 'motorKv', 'plain', unit),
49
+ batteryVoltage: getNumber(root, 'batteryVoltage', 'plain', unit),
50
+ propDiameterInches: getNumber(root, 'propDiameterInches', 'length', unit),
51
+ propPitchInches: getNumber(root, 'propPitchInches', 'length', unit),
52
+ bladeCount: getChoice(root, 'bladeCount', 3) as BladeCount,
53
+ motorCount: getChoice(root, 'motorCount', 4) as MotorCount,
54
+ droneWeightGrams: getNumber(root, 'droneWeightGrams', 'mass', unit),
55
+ benchThrustGrams: getOptionalNumber(root, 'benchThrustGrams', 'mass', unit),
56
+ benchVoltage: getOptionalNumber(root, 'benchVoltage', 'plain', unit),
57
+ };
58
+ }
59
+
60
+ function getOptionalNumber(root: HTMLElement, field: string, kind: 'mass' | 'plain', unit: DisplayUnit): number | undefined {
61
+ const value = getNumber(root, field, kind, unit);
62
+ return value > 0 ? value : undefined;
63
+ }
64
+
65
+ function setInput(root: HTMLElement, field: string, value: string): void {
66
+ const element = getInput(root, field);
67
+ if (element) element.value = value;
68
+ }
69
+
70
+ function setPhysicalInputs(root: HTMLElement, inputs: DroneMotorInputs, unit: DisplayUnit): void {
71
+ setInput(root, 'motorKv', String(inputs.motorKv));
72
+ setInput(root, 'batteryVoltage', String(inputs.batteryVoltage));
73
+ setInput(root, 'propDiameterInches', formatInputValue(inputs.propDiameterInches, 'length', unit));
74
+ setInput(root, 'propPitchInches', formatInputValue(inputs.propPitchInches, 'length', unit));
75
+ setInput(root, 'droneWeightGrams', formatInputValue(inputs.droneWeightGrams, 'mass', unit));
76
+ setInput(root, 'benchThrustGrams', inputs.benchThrustGrams ? formatInputValue(inputs.benchThrustGrams, 'mass', unit) : '');
77
+ setInput(root, 'benchVoltage', inputs.benchVoltage ? String(inputs.benchVoltage) : '');
78
+ selectChoice(root, 'bladeCount', inputs.bladeCount);
79
+ selectChoice(root, 'motorCount', inputs.motorCount);
80
+ }
81
+
82
+ function selectChoice(root: HTMLElement, field: string, value: number): void {
83
+ root.querySelectorAll<HTMLElement>(`[data-choice="${field}"]`).forEach((item) => {
84
+ item.classList.toggle('is-selected', Number(item.dataset.value) === value);
85
+ item.setAttribute('aria-pressed', String(Number(item.dataset.value) === value));
86
+ });
87
+ }
88
+
89
+ function selectPreset(root: HTMLElement, id: string): void {
90
+ root.querySelectorAll<HTMLElement>('[data-preset]').forEach((item) => {
91
+ item.classList.toggle('is-selected', item.dataset.preset === id);
92
+ });
93
+ }
94
+
95
+ function clearBenchData(root: HTMLElement, ui: DroneMotorPropellerUI): void {
96
+ setInput(root, 'benchThrustGrams', '');
97
+ setInput(root, 'benchVoltage', '');
98
+ selectPreset(root, '');
99
+ recalculate(root, ui, root.dataset.unit === 'imperial' ? 'imperial' : 'metric');
100
+ }
101
+
102
+ function updateUnitButtons(root: HTMLElement, unit: DisplayUnit): void {
103
+ root.querySelectorAll<HTMLElement>('[data-unit]').forEach((item) => {
104
+ item.classList.toggle('is-selected', item.dataset.unit === unit);
105
+ item.setAttribute('aria-pressed', String(item.dataset.unit === unit));
106
+ });
107
+ }
108
+
109
+ function recalculate(root: HTMLElement, ui: DroneMotorPropellerUI, unit: DisplayUnit): void {
110
+ const inputs = readInputs(root, unit);
111
+ if (inputs.motorKv <= 0 || inputs.batteryVoltage <= 0 || inputs.propDiameterInches <= 0 || inputs.propPitchInches <= 0 || inputs.droneWeightGrams <= 0) return;
112
+ const results = calculateDroneMotorMetrics(inputs);
113
+ const evaluation = evaluateSetup(results);
114
+ renderResults({ root, results, labels: ui, unit }, evaluation.status);
115
+ writeStoredState({ inputs, unit });
116
+ }
117
+
118
+ function changeUnit(root: HTMLElement, ui: DroneMotorPropellerUI, from: DisplayUnit, to: DisplayUnit): void {
119
+ if (from === to) return;
120
+ const inputs = readInputs(root, from);
121
+ setPhysicalInputs(root, inputs, to);
122
+ updateUnitButtons(root, to);
123
+ updateInputUnitLabels(root, to);
124
+ root.dataset.unit = to;
125
+ recalculate(root, ui, to);
126
+ }
127
+
128
+ function restoreState(root: HTMLElement): DisplayUnit {
129
+ const stored = readStoredState();
130
+ const unit = stored.unit === 'imperial' ? 'imperial' : 'metric';
131
+ setPhysicalInputs(root, stored.inputs ?? DEFAULT_INPUTS, unit);
132
+ return unit;
133
+ }
134
+
135
+ function readUi(root: HTMLElement): DroneMotorPropellerUI {
136
+ const element = root.querySelector<HTMLScriptElement>('[data-drone-motor-ui]');
137
+ return JSON.parse(element?.textContent ?? '{}') as DroneMotorPropellerUI;
138
+ }
139
+
140
+ function bindEvents(root: HTMLElement, ui: DroneMotorPropellerUI): void {
141
+ root.querySelectorAll<HTMLElement>('[data-preset]').forEach((item) => item.addEventListener('click', () => {
142
+ const preset = PRESETS.find((entry) => entry.id === item.dataset.preset);
143
+ if (!preset) return;
144
+ const unit = root.dataset.unit === 'imperial' ? 'imperial' : 'metric';
145
+ setPhysicalInputs(root, preset.inputs, unit);
146
+ selectPreset(root, preset.id);
147
+ recalculate(root, ui, unit);
148
+ }));
149
+ root.querySelectorAll<HTMLElement>('[data-choice]').forEach((item) => item.addEventListener('click', () => {
150
+ selectChoice(root, item.dataset.choice ?? '', Number(item.dataset.value));
151
+ selectPreset(root, '');
152
+ recalculate(root, ui, root.dataset.unit === 'imperial' ? 'imperial' : 'metric');
153
+ }));
154
+ root.querySelectorAll<HTMLElement>('[data-unit]').forEach((item) => item.addEventListener('click', () => {
155
+ const from = root.dataset.unit === 'imperial' ? 'imperial' : 'metric';
156
+ const to = item.dataset.unit === 'imperial' ? 'imperial' : 'metric';
157
+ changeUnit(root, ui, from, to);
158
+ }));
159
+ root.querySelectorAll<HTMLInputElement>('input[data-field]').forEach((item) => item.addEventListener('input', () => {
160
+ selectPreset(root, '');
161
+ recalculate(root, ui, root.dataset.unit === 'imperial' ? 'imperial' : 'metric');
162
+ }));
163
+ root.querySelector<HTMLElement>('[data-action="clear-bench"]')?.addEventListener('click', () => clearBenchData(root, ui));
164
+ }
165
+
166
+ export function initDroneMotorPropellerCalculator(root: HTMLElement): void {
167
+ const ui = readUi(root);
168
+ const unit = restoreState(root);
169
+ root.dataset.unit = unit;
170
+ updateUnitButtons(root, unit);
171
+ updateInputUnitLabels(root, unit);
172
+ bindEvents(root, ui);
173
+ recalculate(root, ui, unit);
174
+ }
@@ -0,0 +1,78 @@
1
+ import type { DroneMotorResults } from './logic';
2
+ import type { DisplayUnit } from './storage';
3
+ import type { DroneMotorPropellerUI } from './ui';
4
+
5
+ interface RenderContext {
6
+ root: HTMLElement;
7
+ results: DroneMotorResults;
8
+ labels: DroneMotorPropellerUI;
9
+ unit: DisplayUnit;
10
+ }
11
+
12
+ function formatNumber(value: number, digits = 0): string {
13
+ return new Intl.NumberFormat('en-US', { maximumFractionDigits: digits }).format(value);
14
+ }
15
+
16
+ function formatMass(grams: number, unit: DisplayUnit): string {
17
+ return unit === 'metric' ? `${formatNumber(grams)} g` : `${formatNumber(grams / 28.3495, 1)} oz`;
18
+ }
19
+
20
+ function formatSpeed(kmh: number, unit: DisplayUnit): string {
21
+ return unit === 'metric' ? `${formatNumber(kmh)} km/h` : `${formatNumber(kmh / 1.60934)} mph`;
22
+ }
23
+
24
+ function setText(root: HTMLElement, key: string, value: string): void {
25
+ const element = root.querySelector<HTMLElement>(`[data-output="${key}"]`);
26
+ if (element) element.textContent = value;
27
+ }
28
+
29
+ function getStatusText(status: string, labels: DroneMotorPropellerUI): string {
30
+ if (status === 'underpowered') return labels.underpoweredStatus;
31
+ if (status === 'workable') return labels.workableStatus;
32
+ return labels.headroomStatus;
33
+ }
34
+
35
+ function getStatusAdvice(status: string, labels: DroneMotorPropellerUI): string {
36
+ if (status === 'underpowered') return labels.underpoweredAdvice;
37
+ if (status === 'workable') return labels.workableAdvice;
38
+ return labels.headroomAdvice;
39
+ }
40
+
41
+ export function renderResults(context: RenderContext, status: string): void {
42
+ const { root, results, labels, unit } = context;
43
+ setText(root, 'loaded-rpm', `${formatNumber(results.loadedRpm)} rpm`);
44
+ setText(root, 'pitch-speed', formatSpeed(results.pitchSpeedKmh, unit));
45
+ setText(root, 'thrust-per-motor', formatMass(results.thrustPerMotorGrams, unit));
46
+ setText(root, 'total-thrust', formatMass(results.totalThrustGrams, unit));
47
+ setText(root, 'total-power', `${formatNumber(results.totalPowerWatts)} W`);
48
+ setText(root, 'total-current', `${formatNumber(results.totalCurrentAmps, 1)} A`);
49
+ setText(root, 'thrust-margin', `${formatNumber(results.thrustMarginPercent)}%`);
50
+ setText(root, 'hover-throttle', `${formatNumber(results.hoverThrottlePercent)}%`);
51
+ setText(root, 'status', getStatusText(status, labels));
52
+ setText(root, 'status-advice', getStatusAdvice(status, labels));
53
+ const sourceLabel = results.thrustSource === 'bench'
54
+ ? `${labels.benchBasedLabel} ${labels.sourceNote}`
55
+ : `${labels.estimatedLabel} ${labels.modelSourceNote}`;
56
+ setText(root, 'source-note', sourceLabel);
57
+ root.dataset.status = status;
58
+ root.style.setProperty('--lift-ratio', String(Math.min(results.totalThrustGrams / (results.totalThrustGrams + 1), 1)));
59
+ root.style.setProperty('--rotor-speed', `${Math.max(80, 90000 / Math.max(results.loadedRpm, 1))}ms`);
60
+ }
61
+
62
+ export function updateInputUnitLabels(root: HTMLElement, unit: DisplayUnit): void {
63
+ const massUnit = unit === 'metric' ? 'g' : 'oz';
64
+ const lengthUnit = unit === 'metric' ? 'cm' : 'in';
65
+ root.querySelectorAll<HTMLElement>('[data-unit-label="mass"]').forEach((item) => { item.textContent = massUnit; });
66
+ root.querySelectorAll<HTMLElement>('[data-unit-label="length"]').forEach((item) => { item.textContent = lengthUnit; });
67
+ }
68
+
69
+ export function formatInputValue(value: number, kind: 'mass' | 'length', unit: DisplayUnit): string {
70
+ if (kind === 'mass') return unit === 'metric' ? String(Math.round(value)) : String(Math.round(value / 28.3495 * 10) / 10);
71
+ return unit === 'metric' ? String(Math.round(value * 2.54 * 10) / 10) : String(value);
72
+ }
73
+
74
+ export function parseInputValue(value: string, kind: 'mass' | 'length', unit: DisplayUnit): number {
75
+ const parsed = Number(value);
76
+ if (kind === 'mass') return unit === 'metric' ? parsed : parsed * 28.3495;
77
+ return unit === 'metric' ? parsed / 2.54 : parsed;
78
+ }