@jjlmoya/utils-motor 1.2.0 → 1.3.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 +3 -1
- package/src/entries.ts +7 -1
- package/src/index.ts +10 -0
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/seo_translation_completeness.test.ts +69 -0
- package/src/tests/tool_validation.test.ts +1 -1
- package/src/tool/carMotorcycleGearRatioCalculator/bibliography.astro +6 -0
- package/src/tool/carMotorcycleGearRatioCalculator/bibliography.ts +12 -0
- package/src/tool/carMotorcycleGearRatioCalculator/car-motorcycle-gear-ratio-rpm-speed-calculator.css +668 -0
- package/src/tool/carMotorcycleGearRatioCalculator/component.astro +137 -0
- package/src/tool/carMotorcycleGearRatioCalculator/controller.ts +209 -0
- package/src/tool/carMotorcycleGearRatioCalculator/dom-views.ts +121 -0
- package/src/tool/carMotorcycleGearRatioCalculator/entry.ts +30 -0
- package/src/tool/carMotorcycleGearRatioCalculator/evaluator.ts +28 -0
- package/src/tool/carMotorcycleGearRatioCalculator/i18n/de.ts +162 -0
- package/src/tool/carMotorcycleGearRatioCalculator/i18n/en.ts +162 -0
- package/src/tool/carMotorcycleGearRatioCalculator/i18n/es.ts +162 -0
- package/src/tool/carMotorcycleGearRatioCalculator/i18n/fr.ts +162 -0
- package/src/tool/carMotorcycleGearRatioCalculator/i18n/id.ts +162 -0
- package/src/tool/carMotorcycleGearRatioCalculator/i18n/it.ts +162 -0
- package/src/tool/carMotorcycleGearRatioCalculator/i18n/ja.ts +162 -0
- package/src/tool/carMotorcycleGearRatioCalculator/i18n/ko.ts +162 -0
- package/src/tool/carMotorcycleGearRatioCalculator/i18n/nl.ts +162 -0
- package/src/tool/carMotorcycleGearRatioCalculator/i18n/pl.ts +162 -0
- package/src/tool/carMotorcycleGearRatioCalculator/i18n/pt.ts +162 -0
- package/src/tool/carMotorcycleGearRatioCalculator/i18n/ru.ts +162 -0
- package/src/tool/carMotorcycleGearRatioCalculator/i18n/sv.ts +162 -0
- package/src/tool/carMotorcycleGearRatioCalculator/i18n/tr.ts +162 -0
- package/src/tool/carMotorcycleGearRatioCalculator/i18n/zh.ts +162 -0
- package/src/tool/carMotorcycleGearRatioCalculator/index.ts +10 -0
- package/src/tool/carMotorcycleGearRatioCalculator/logic.test.ts +51 -0
- package/src/tool/carMotorcycleGearRatioCalculator/logic.ts +91 -0
- package/src/tool/carMotorcycleGearRatioCalculator/seo.astro +15 -0
- package/src/tool/carMotorcycleGearRatioCalculator/storage.ts +46 -0
- package/src/tool/carMotorcycleGearRatioCalculator/ui.ts +50 -0
- package/src/tool/tirePressureConverter/bibliography.astro +6 -0
- package/src/tool/tirePressureConverter/bibliography.ts +12 -0
- package/src/tool/tirePressureConverter/component.astro +82 -0
- package/src/tool/tirePressureConverter/controller.ts +120 -0
- package/src/tool/tirePressureConverter/dom-views.ts +31 -0
- package/src/tool/tirePressureConverter/entry.ts +30 -0
- package/src/tool/tirePressureConverter/evaluator.ts +8 -0
- package/src/tool/tirePressureConverter/i18n/de.ts +96 -0
- package/src/tool/tirePressureConverter/i18n/en.ts +96 -0
- package/src/tool/tirePressureConverter/i18n/es.ts +96 -0
- package/src/tool/tirePressureConverter/i18n/fr.ts +96 -0
- package/src/tool/tirePressureConverter/i18n/id.ts +95 -0
- package/src/tool/tirePressureConverter/i18n/it.ts +95 -0
- package/src/tool/tirePressureConverter/i18n/ja.ts +95 -0
- package/src/tool/tirePressureConverter/i18n/ko.ts +95 -0
- package/src/tool/tirePressureConverter/i18n/nl.ts +95 -0
- package/src/tool/tirePressureConverter/i18n/pl.ts +95 -0
- package/src/tool/tirePressureConverter/i18n/pt.ts +95 -0
- package/src/tool/tirePressureConverter/i18n/ru.ts +95 -0
- package/src/tool/tirePressureConverter/i18n/sv.ts +95 -0
- package/src/tool/tirePressureConverter/i18n/tr.ts +95 -0
- package/src/tool/tirePressureConverter/i18n/zh.ts +95 -0
- package/src/tool/tirePressureConverter/index.ts +10 -0
- package/src/tool/tirePressureConverter/logic.test.ts +37 -0
- package/src/tool/tirePressureConverter/logic.ts +47 -0
- package/src/tool/tirePressureConverter/seo.astro +15 -0
- package/src/tool/tirePressureConverter/storage.ts +27 -0
- package/src/tool/tirePressureConverter/tire-pressure-converter-psi-bar-kpa-kgf-cm2.css +293 -0
- package/src/tool/tirePressureConverter/ui.ts +20 -0
- package/src/tools.ts +3 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { CarMotorcycleGearRatioCalculatorUI } from './ui';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
ui: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const ui = Astro.props.ui as CarMotorcycleGearRatioCalculatorUI;
|
|
9
|
+
const initialValues = {
|
|
10
|
+
vehicle: 'car',
|
|
11
|
+
engineRpm: 3000,
|
|
12
|
+
redlineRpm: 7000,
|
|
13
|
+
primaryRatio: 1,
|
|
14
|
+
finalDriveRatio: 4,
|
|
15
|
+
wheelDiameter: 600,
|
|
16
|
+
targetSpeed: 90,
|
|
17
|
+
gearRatios: [3.2, 2.1, 1.4, 1, 0.8, 0.65],
|
|
18
|
+
};
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<div class="gear-tool" data-gear-tool>
|
|
22
|
+
<script is:inline type="application/json" data-gear-ui set:html={JSON.stringify(ui)}></script>
|
|
23
|
+
<script is:inline type="application/json" data-gear-initial set:html={JSON.stringify(initialValues)}></script>
|
|
24
|
+
|
|
25
|
+
<div class="gear-tool-intro">
|
|
26
|
+
<span class="tool-kicker">{ui.transmissionSceneLabel}</span>
|
|
27
|
+
<p>{ui.editInputsHint}</p>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div class="gear-layout">
|
|
31
|
+
<section class="gear-controls" aria-label={ui.gearStackLabel}>
|
|
32
|
+
<div class="control-block control-block-profile">
|
|
33
|
+
<div class="control-heading">
|
|
34
|
+
<span>{ui.vehicleProfileLabel}</span>
|
|
35
|
+
<span class="control-note" data-active-profile>{ui.carLabel}</span>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="profile-chips" role="group" aria-label={ui.vehicleProfileLabel}>
|
|
38
|
+
<button type="button" class="profile-chip is-active" data-profile="car">
|
|
39
|
+
<span class="profile-chip-mark">C</span>{ui.carPresetLabel}
|
|
40
|
+
</button>
|
|
41
|
+
<button type="button" class="profile-chip" data-profile="motorcycle">
|
|
42
|
+
<span class="profile-chip-mark">M</span>{ui.motorcyclePresetLabel}
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="control-block control-block-units">
|
|
48
|
+
<div class="control-heading">
|
|
49
|
+
<span>{ui.unitsLabel}</span>
|
|
50
|
+
<span class="unit-switch" role="group" aria-label={ui.unitsLabel}>
|
|
51
|
+
<button type="button" class="unit-button is-active" data-unit="metric">{ui.metricLabel}</button>
|
|
52
|
+
<button type="button" class="unit-button" data-unit="imperial">{ui.imperialLabel}</button>
|
|
53
|
+
</span>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="control-grid">
|
|
58
|
+
<label class="input-field">
|
|
59
|
+
<span>{ui.engineRpmLabel}</span>
|
|
60
|
+
<span class="input-with-unit"><input type="number" min="500" max="16000" step="100" value={initialValues.engineRpm} data-field="engineRpm" /><b>RPM</b></span>
|
|
61
|
+
</label>
|
|
62
|
+
<label class="input-field">
|
|
63
|
+
<span>{ui.redlineLabel}</span>
|
|
64
|
+
<span class="input-with-unit"><input type="number" min="1500" max="20000" step="100" value={initialValues.redlineRpm} data-field="redlineRpm" /><b>RPM</b></span>
|
|
65
|
+
</label>
|
|
66
|
+
<label class="input-field">
|
|
67
|
+
<span>{ui.primaryRatioLabel}</span>
|
|
68
|
+
<span class="input-with-unit"><input type="number" min="0.1" max="10" step="0.01" value={initialValues.primaryRatio} data-field="primaryRatio" /><b>{ui.ratioUnit}</b></span>
|
|
69
|
+
</label>
|
|
70
|
+
<label class="input-field">
|
|
71
|
+
<span>{ui.finalDriveLabel}</span>
|
|
72
|
+
<span class="input-with-unit"><input type="number" min="0.1" max="10" step="0.01" value={initialValues.finalDriveRatio} data-field="finalDriveRatio" /><b>{ui.ratioUnit}</b></span>
|
|
73
|
+
</label>
|
|
74
|
+
<label class="input-field">
|
|
75
|
+
<span>{ui.wheelDiameterLabel}</span>
|
|
76
|
+
<span class="input-with-unit"><input type="number" min="200" max="1200" step="1" value={initialValues.wheelDiameter} data-field="wheelDiameter" /><b data-wheel-unit>{ui.wheelUnitMetric}</b></span>
|
|
77
|
+
</label>
|
|
78
|
+
<label class="input-field">
|
|
79
|
+
<span>{ui.targetSpeedLabel}</span>
|
|
80
|
+
<span class="input-with-unit"><input type="number" min="1" max="400" step="1" value={initialValues.targetSpeed} data-field="targetSpeed" /><b data-speed-unit>{ui.speedUnitMetric}</b></span>
|
|
81
|
+
</label>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div class="control-block gear-stack-block">
|
|
85
|
+
<div class="control-heading">
|
|
86
|
+
<span>{ui.gearStackLabel}</span>
|
|
87
|
+
<span class="control-note">{ui.gearRatioHint}</span>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="gear-input-grid">
|
|
90
|
+
{initialValues.gearRatios.map((ratio, index) => (
|
|
91
|
+
<label class="gear-input">
|
|
92
|
+
<span>{ui.gearLabel} {index + 1}</span>
|
|
93
|
+
<input type="number" min="0.1" max="8" step="0.01" value={ratio} data-gear={index} aria-label={`${ui.gearLabel} ${index + 1}`} />
|
|
94
|
+
</label>
|
|
95
|
+
))}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<button type="button" class="reset-button" data-reset>{ui.resetLabel}</button>
|
|
100
|
+
</section>
|
|
101
|
+
|
|
102
|
+
<section class="gear-visual" aria-label={ui.transmissionSceneLabel}>
|
|
103
|
+
<div class="transmission-scene">
|
|
104
|
+
<svg class="scene-svg" viewBox="0 0 760 420" role="img" aria-label={ui.transmissionSceneLabel}>
|
|
105
|
+
<path class="scene-road" d="M20 345H740" />
|
|
106
|
+
<path class="scene-shaft" data-shaft-speed d="M74 190H590" />
|
|
107
|
+
<circle class="scene-engine" cx="78" cy="190" r="52" />
|
|
108
|
+
<circle class="scene-engine-core" cx="78" cy="190" r="18" />
|
|
109
|
+
<circle class="scene-gear scene-gear-large" data-gear-rotation="primary" cx="244" cy="190" r="84" />
|
|
110
|
+
<circle class="scene-gear scene-gear-mid" data-gear-rotation="selected" cx="366" cy="190" r="61" />
|
|
111
|
+
<circle class="scene-gear scene-gear-small" data-gear-rotation="final" cx="468" cy="190" r="39" />
|
|
112
|
+
<path class="scene-final-drive" d="M509 190L585 190" />
|
|
113
|
+
<circle class="scene-wheel" data-wheel-rotation cx="654" cy="190" r="104" />
|
|
114
|
+
<circle class="scene-wheel-inner" cx="654" cy="190" r="70" />
|
|
115
|
+
<path class="scene-spoke" d="M654 120V260M584 190H724M605 141L703 239M703 141L605 239" />
|
|
116
|
+
<text x="52" y="300">{ui.engineLabel}</text>
|
|
117
|
+
<text x="300" y="300">{ui.gearboxLabel}</text>
|
|
118
|
+
<text x="608" y="300">{ui.wheelLabel}</text>
|
|
119
|
+
</svg>
|
|
120
|
+
<div class="scene-readout" data-scene-readout></div>
|
|
121
|
+
<div class="motion-hint">
|
|
122
|
+
<strong>{ui.motionHintLabel}</strong>
|
|
123
|
+
<span>{ui.motionHintText}</span>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</section>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<section class="gear-results" data-results aria-label={ui.transmissionSceneLabel}></section>
|
|
130
|
+
<p class="educational-notice">{ui.educationalNotice}</p>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<script>
|
|
134
|
+
import { initializeGearRatioCalculator } from './controller';
|
|
135
|
+
|
|
136
|
+
initializeGearRatioCalculator();
|
|
137
|
+
</script>
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { evaluateTransmission } from './evaluator';
|
|
2
|
+
import { renderResults } from './dom-views';
|
|
3
|
+
import {
|
|
4
|
+
calculateTransmission,
|
|
5
|
+
convertSpeed,
|
|
6
|
+
convertWheelDiameter,
|
|
7
|
+
type TransmissionInputs,
|
|
8
|
+
type UnitSystem,
|
|
9
|
+
} from './logic';
|
|
10
|
+
import { loadSettings, saveSettings, type StoredSettings } from './storage';
|
|
11
|
+
import type { CarMotorcycleGearRatioCalculatorUI } from './ui';
|
|
12
|
+
|
|
13
|
+
type VehicleProfile = 'car' | 'motorcycle';
|
|
14
|
+
|
|
15
|
+
interface UnitFieldOptions {
|
|
16
|
+
root: HTMLElement;
|
|
17
|
+
field: string;
|
|
18
|
+
value: number | undefined;
|
|
19
|
+
fallback: number;
|
|
20
|
+
unitSystem: UnitSystem;
|
|
21
|
+
formatter: (value: number, unitSystem: UnitSystem) => number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface Preset {
|
|
25
|
+
vehicle: VehicleProfile;
|
|
26
|
+
engineRpm: number;
|
|
27
|
+
redlineRpm: number;
|
|
28
|
+
primaryRatio: number;
|
|
29
|
+
finalDriveRatio: number;
|
|
30
|
+
wheelDiameter: number;
|
|
31
|
+
targetSpeed: number;
|
|
32
|
+
gearRatios: number[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const presets: Record<VehicleProfile, Preset> = {
|
|
36
|
+
car: {
|
|
37
|
+
vehicle: 'car', engineRpm: 3000, redlineRpm: 7000, primaryRatio: 1,
|
|
38
|
+
finalDriveRatio: 4, wheelDiameter: 600, targetSpeed: 90, gearRatios: [3.2, 2.1, 1.4, 1, 0.8, 0.65],
|
|
39
|
+
},
|
|
40
|
+
motorcycle: {
|
|
41
|
+
vehicle: 'motorcycle', engineRpm: 5000, redlineRpm: 11000, primaryRatio: 1.9,
|
|
42
|
+
finalDriveRatio: 2.8, wheelDiameter: 660, targetSpeed: 90, gearRatios: [2.7, 1.9, 1.45, 1.15, 0.95, 0.8],
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function initializeGearRatioCalculator(): void {
|
|
47
|
+
const root = document.querySelector<HTMLElement>('[data-gear-tool]');
|
|
48
|
+
if (!root) return;
|
|
49
|
+
const ui = readUi(root);
|
|
50
|
+
const loaded = loadSettings();
|
|
51
|
+
const stored = loaded.vehicleProfile === undefined ? {} : loaded;
|
|
52
|
+
const unitSystem = stored.unitSystem ?? 'metric';
|
|
53
|
+
const vehicle: VehicleProfile = stored.vehicleProfile ?? 'car';
|
|
54
|
+
applySettings(root, stored, unitSystem);
|
|
55
|
+
setActiveProfile(root, ui, vehicle);
|
|
56
|
+
bindEvents(root, ui);
|
|
57
|
+
refresh(root, ui, unitSystem);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readUi(root: HTMLElement): CarMotorcycleGearRatioCalculatorUI {
|
|
61
|
+
const node = root.querySelector('[data-gear-ui]');
|
|
62
|
+
return JSON.parse(node?.textContent ?? '{}') as CarMotorcycleGearRatioCalculatorUI;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function applySettings(root: HTMLElement, stored: Partial<StoredSettings>, unitSystem: UnitSystem): void {
|
|
66
|
+
const defaults = presets.car;
|
|
67
|
+
setStoredField(root, 'engineRpm', stored.engineRpm, defaults.engineRpm);
|
|
68
|
+
setStoredField(root, 'redlineRpm', stored.redlineRpm, defaults.redlineRpm);
|
|
69
|
+
setStoredField(root, 'primaryRatio', stored.primaryRatio, defaults.primaryRatio);
|
|
70
|
+
setStoredField(root, 'finalDriveRatio', stored.finalDriveRatio, defaults.finalDriveRatio);
|
|
71
|
+
setUnitField({ root, field: 'wheelDiameter', value: stored.wheelDiameter, fallback: defaults.wheelDiameter, unitSystem, formatter: displayedWheelValue });
|
|
72
|
+
setUnitField({ root, field: 'targetSpeed', value: stored.targetSpeed, fallback: defaults.targetSpeed, unitSystem, formatter: displayedSpeedValue });
|
|
73
|
+
const gearRatios = stored.gearRatios ?? presets.car.gearRatios;
|
|
74
|
+
gearRatios.forEach((ratio, index) => setGear(root, index, ratio));
|
|
75
|
+
updateUnitLabels(root, unitSystem);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function bindEvents(
|
|
79
|
+
root: HTMLElement,
|
|
80
|
+
ui: CarMotorcycleGearRatioCalculatorUI,
|
|
81
|
+
): void {
|
|
82
|
+
root.addEventListener('input', () => refresh(root, ui, currentUnit(root)));
|
|
83
|
+
root.addEventListener('click', (event) => {
|
|
84
|
+
const target = event.target as HTMLElement;
|
|
85
|
+
const profile = target.closest<HTMLElement>('[data-profile]')?.dataset.profile as VehicleProfile | undefined;
|
|
86
|
+
const unit = target.closest<HTMLElement>('[data-unit]')?.dataset.unit as UnitSystem | undefined;
|
|
87
|
+
if (profile) selectProfile(root, ui, profile);
|
|
88
|
+
if (unit) selectUnit(root, ui, unit);
|
|
89
|
+
if (target.closest('[data-reset]')) resetTool(root, ui);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function selectProfile(root: HTMLElement, ui: CarMotorcycleGearRatioCalculatorUI, profile: VehicleProfile): void {
|
|
94
|
+
const preset = presets[profile];
|
|
95
|
+
const unitSystem = currentUnit(root);
|
|
96
|
+
setField(root, 'engineRpm', preset.engineRpm);
|
|
97
|
+
setField(root, 'redlineRpm', preset.redlineRpm);
|
|
98
|
+
setField(root, 'primaryRatio', preset.primaryRatio);
|
|
99
|
+
setField(root, 'finalDriveRatio', preset.finalDriveRatio);
|
|
100
|
+
setField(root, 'wheelDiameter', displayedWheelValue(preset.wheelDiameter, unitSystem));
|
|
101
|
+
setField(root, 'targetSpeed', displayedSpeedValue(preset.targetSpeed, unitSystem));
|
|
102
|
+
preset.gearRatios.forEach((ratio, index) => setGear(root, index, ratio));
|
|
103
|
+
root.querySelectorAll('[data-profile]').forEach((button) => button.classList.toggle('is-active', button.getAttribute('data-profile') === profile));
|
|
104
|
+
setActiveProfile(root, ui, profile);
|
|
105
|
+
refresh(root, ui, currentUnit(root));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function selectUnit(root: HTMLElement, ui: CarMotorcycleGearRatioCalculatorUI, next: UnitSystem): void {
|
|
109
|
+
const current = currentUnit(root);
|
|
110
|
+
if (current !== next) {
|
|
111
|
+
const wheel = readNumber(root, 'wheelDiameter');
|
|
112
|
+
const speed = readNumber(root, 'targetSpeed');
|
|
113
|
+
setField(root, 'wheelDiameter', roundValue(convertWheelDiameter(wheel, current, next), next === 'metric' ? 0 : 1));
|
|
114
|
+
setField(root, 'targetSpeed', roundValue(convertSpeed(speed, current, next), next === 'metric' ? 0 : 1));
|
|
115
|
+
}
|
|
116
|
+
updateUnitLabels(root, next);
|
|
117
|
+
refresh(root, ui, next);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function resetTool(root: HTMLElement, ui: CarMotorcycleGearRatioCalculatorUI): void {
|
|
121
|
+
updateUnitLabels(root, 'metric');
|
|
122
|
+
selectProfile(root, ui, 'car');
|
|
123
|
+
refresh(root, ui, 'metric');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function refresh(root: HTMLElement, ui: CarMotorcycleGearRatioCalculatorUI, unitSystem: UnitSystem): void {
|
|
127
|
+
const inputs = readInputs(root, unitSystem);
|
|
128
|
+
const result = calculateTransmission(inputs);
|
|
129
|
+
const evaluation = evaluateTransmission(inputs, result);
|
|
130
|
+
renderResults(root, { ui, inputs, result, evaluation });
|
|
131
|
+
saveSettings({ ...inputs, vehicleProfile: currentProfile(root) } as StoredSettings);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function readInputs(root: HTMLElement, unitSystem: UnitSystem): TransmissionInputs {
|
|
135
|
+
const gearRatios = Array.from(root.querySelectorAll<HTMLInputElement>('[data-gear]')).map((input) => Number(input.value));
|
|
136
|
+
return {
|
|
137
|
+
engineRpm: readNumber(root, 'engineRpm'),
|
|
138
|
+
redlineRpm: readNumber(root, 'redlineRpm'),
|
|
139
|
+
primaryRatio: readNumber(root, 'primaryRatio'),
|
|
140
|
+
finalDriveRatio: readNumber(root, 'finalDriveRatio'),
|
|
141
|
+
wheelDiameter: readNumber(root, 'wheelDiameter'),
|
|
142
|
+
targetSpeed: readNumber(root, 'targetSpeed'),
|
|
143
|
+
unitSystem,
|
|
144
|
+
gearRatios,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function updateUnitLabels(root: HTMLElement, unitSystem: UnitSystem): void {
|
|
149
|
+
root.querySelectorAll('[data-unit]').forEach((button) => button.classList.toggle('is-active', button.getAttribute('data-unit') === unitSystem));
|
|
150
|
+
const wheel = root.querySelector<HTMLElement>('[data-wheel-unit]');
|
|
151
|
+
const speed = root.querySelector<HTMLElement>('[data-speed-unit]');
|
|
152
|
+
if (wheel) wheel.textContent = unitSystem === 'metric' ? 'mm' : 'in';
|
|
153
|
+
if (speed) speed.textContent = unitSystem === 'metric' ? 'km/h' : 'mph';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function currentUnit(root: HTMLElement): UnitSystem {
|
|
157
|
+
return root.querySelector<HTMLElement>('[data-unit].is-active')?.dataset.unit === 'imperial' ? 'imperial' : 'metric';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function currentProfile(root: HTMLElement): VehicleProfile {
|
|
161
|
+
return root.querySelector<HTMLElement>('[data-profile].is-active')?.dataset.profile === 'motorcycle' ? 'motorcycle' : 'car';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function setActiveProfile(root: HTMLElement, ui: CarMotorcycleGearRatioCalculatorUI, profile: VehicleProfile): void {
|
|
165
|
+
root.querySelectorAll('[data-profile]').forEach((button) => button.classList.toggle('is-active', button.getAttribute('data-profile') === profile));
|
|
166
|
+
const profileLabel = root.querySelector<HTMLElement>('[data-active-profile]');
|
|
167
|
+
if (profileLabel) profileLabel.textContent = profile === 'car' ? ui.carLabel : ui.motorcycleLabel;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function readNumber(root: HTMLElement, field: string): number {
|
|
171
|
+
const input = root.querySelector<HTMLInputElement>(`[data-field="${field}"]`);
|
|
172
|
+
const value = Number(input?.value);
|
|
173
|
+
return Number.isFinite(value) && value > 0 ? value : 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function setField(root: HTMLElement, field: string, value: number): void {
|
|
177
|
+
const input = root.querySelector<HTMLInputElement>(`[data-field="${field}"]`);
|
|
178
|
+
if (input) input.value = String(value);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function setStoredField(root: HTMLElement, field: string, value: number | undefined, fallback: number): void {
|
|
182
|
+
setField(root, field, value === undefined ? fallback : value);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function setUnitField(options: UnitFieldOptions): void {
|
|
186
|
+
const { root, field, value, fallback, unitSystem, formatter } = options;
|
|
187
|
+
const rawValue = value === undefined ? formatter(fallback, unitSystem) : roundValue(value, unitSystem === 'metric' ? 0 : 1);
|
|
188
|
+
setField(root, field, rawValue);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function setGear(root: HTMLElement, index: number, value: number): void {
|
|
192
|
+
const input = root.querySelector<HTMLInputElement>(`[data-gear="${index}"]`);
|
|
193
|
+
if (input) input.value = String(value);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function displayedWheelValue(value: number, unitSystem: UnitSystem): number {
|
|
197
|
+
const converted = convertWheelDiameter(value, 'metric', unitSystem);
|
|
198
|
+
return roundValue(converted, unitSystem === 'metric' ? 0 : 1);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function displayedSpeedValue(value: number, unitSystem: UnitSystem): number {
|
|
202
|
+
const converted = convertSpeed(value, 'metric', unitSystem);
|
|
203
|
+
return roundValue(converted, unitSystem === 'metric' ? 0 : 1);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function roundValue(value: number, decimals: number): number {
|
|
207
|
+
const factor = 10 ** decimals;
|
|
208
|
+
return Math.round(value * factor) / factor;
|
|
209
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { CarMotorcycleGearRatioCalculatorUI } from './ui';
|
|
2
|
+
import type { CalculationEvaluation } from './evaluator';
|
|
3
|
+
import type { TransmissionInputs, TransmissionResult } from './logic';
|
|
4
|
+
|
|
5
|
+
interface ViewContext {
|
|
6
|
+
ui: CarMotorcycleGearRatioCalculatorUI;
|
|
7
|
+
inputs: TransmissionInputs;
|
|
8
|
+
result: TransmissionResult;
|
|
9
|
+
evaluation: CalculationEvaluation;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function renderResults(root: HTMLElement, context: ViewContext): void {
|
|
13
|
+
const result = root.querySelector<HTMLElement>('[data-results]');
|
|
14
|
+
if (!result) return;
|
|
15
|
+
result.innerHTML = buildResultsMarkup(context);
|
|
16
|
+
renderScene(root, context);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function buildResultsMarkup(context: ViewContext): string {
|
|
20
|
+
const { ui, inputs, result, evaluation } = context;
|
|
21
|
+
const selected = result.gears.find((gear) => gear.gear === evaluation.selectedGear);
|
|
22
|
+
return `
|
|
23
|
+
<div class="results-head">
|
|
24
|
+
<div>
|
|
25
|
+
<span class="eyebrow">${ui.speedAtRpmLabel}</span>
|
|
26
|
+
<strong>${formatValue(selected?.speed ?? 0)} ${speedUnit(inputs)}</strong>
|
|
27
|
+
<span class="results-context">${selected ? `${ui.gearLabel} ${selected.gear} · ${formatRatio(selected.gearRatio)}:1` : '-'}</span>
|
|
28
|
+
</div>
|
|
29
|
+
<span class="status-badge status-${evaluation.tone}">${ui[evaluation.messageKey]}</span>
|
|
30
|
+
</div>
|
|
31
|
+
${selected ? buildAnswerMarkup(selected, inputs, ui) : ''}
|
|
32
|
+
<div class="ladder-heading">
|
|
33
|
+
<span>${ui.gearStackLabel}</span>
|
|
34
|
+
<small>${ui.chooseGearHint}</small>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="gear-result-list" role="list" aria-label="${ui.gearStackLabel}">
|
|
37
|
+
${result.gears.map((gear) => buildGearRow(gear, inputs, ui, gear.gear === evaluation.selectedGear)).join('')}
|
|
38
|
+
</div>
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildAnswerMarkup(
|
|
43
|
+
selected: TransmissionResult['gears'][number],
|
|
44
|
+
inputs: TransmissionInputs,
|
|
45
|
+
ui: CarMotorcycleGearRatioCalculatorUI,
|
|
46
|
+
): string {
|
|
47
|
+
return `
|
|
48
|
+
<div class="answer-card">
|
|
49
|
+
<span class="eyebrow">${ui.decisionLabel}</span>
|
|
50
|
+
<div class="answer-grid">
|
|
51
|
+
<div class="answer-primary">
|
|
52
|
+
<span>${ui.decisionCruiseLabel}</span>
|
|
53
|
+
<strong>${formatValue(selected.rpmAtTarget)} <small>RPM</small></strong>
|
|
54
|
+
<b>${formatValue(inputs.targetSpeed)} ${speedUnit(inputs)} ${ui.targetSpeedMarkerLabel}</b>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="answer-secondary">
|
|
57
|
+
<span>${ui.overallRatioLabel}</span>
|
|
58
|
+
<strong>${formatRatio(selected.overallRatio)}<small>:1</small></strong>
|
|
59
|
+
<b>${formatValue(selected.wheelRpm)} ${ui.wheelRpmLabel}</b>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
<p>${ui.decisionHelper}</p>
|
|
63
|
+
</div>
|
|
64
|
+
`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function buildGearRow(
|
|
68
|
+
gear: TransmissionResult['gears'][number],
|
|
69
|
+
inputs: TransmissionInputs,
|
|
70
|
+
ui: CarMotorcycleGearRatioCalculatorUI,
|
|
71
|
+
active: boolean,
|
|
72
|
+
): string {
|
|
73
|
+
const activeClass = active ? ' gear-row-active' : '';
|
|
74
|
+
return `
|
|
75
|
+
<div class="gear-row${activeClass}" role="listitem">
|
|
76
|
+
<span class="gear-number">${ui.gearLabel} ${gear.gear}</span>
|
|
77
|
+
<span class="gear-ratio">${formatRatio(gear.gearRatio)}:1</span>
|
|
78
|
+
<span class="gear-speed"><b>${formatValue(gear.speed)}</b> ${speedUnit(inputs)}</span>
|
|
79
|
+
<span class="gear-rpm"><b>${formatValue(gear.rpmAtTarget)}</b> RPM</span>
|
|
80
|
+
</div>
|
|
81
|
+
`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function renderScene(root: HTMLElement, context: ViewContext): void {
|
|
85
|
+
const scene = root.querySelector<HTMLElement>('[data-scene-readout]');
|
|
86
|
+
if (!scene) return;
|
|
87
|
+
const selected = context.result.gears.find((gear) => gear.gear === context.evaluation.selectedGear);
|
|
88
|
+
scene.innerHTML = `
|
|
89
|
+
<span class="scene-label">${context.ui.transmissionSceneLabel}</span>
|
|
90
|
+
<div class="scene-readout-row">
|
|
91
|
+
<span>${context.ui.engineLabel}<b>${formatValue(context.inputs.engineRpm)} RPM</b></span>
|
|
92
|
+
<span>${context.ui.gearboxLabel}<b>${selected ? `${context.ui.gearLabel} ${selected.gear} · ${formatRatio(selected.gearRatio)}:1` : '-'}</b></span>
|
|
93
|
+
<span>${context.ui.wheelLabel}<b>${formatValue(selected?.speed ?? 0)} ${speedUnit(context.inputs)}</b></span>
|
|
94
|
+
</div>
|
|
95
|
+
`;
|
|
96
|
+
updateSceneGeometry(root, context);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function updateSceneGeometry(root: HTMLElement, context: ViewContext): void {
|
|
100
|
+
const selected = context.result.gears.find((gear) => gear.gear === context.evaluation.selectedGear);
|
|
101
|
+
const wheel = root.querySelector<HTMLElement>('[data-wheel-rotation]');
|
|
102
|
+
const shaft = root.querySelector<HTMLElement>('[data-shaft-speed]');
|
|
103
|
+
const engineFactor = Math.max(context.inputs.engineRpm, 500) / 6000;
|
|
104
|
+
const wheelFactor = Math.max(selected?.speed ?? 0, 5) / 120;
|
|
105
|
+
root.style.setProperty('--engine-duration', `${Math.max(0.6, 2.8 / engineFactor)}s`);
|
|
106
|
+
root.style.setProperty('--wheel-duration', `${Math.max(0.7, 3.4 / wheelFactor)}s`);
|
|
107
|
+
if (wheel) wheel.style.setProperty('--wheel-speed', `${Math.min(selected?.speed ?? 0, 240) / 240}`);
|
|
108
|
+
if (shaft) shaft.style.setProperty('--shaft-speed', `${Math.min(context.inputs.engineRpm, 12000) / 12000}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function speedUnit(inputs: TransmissionInputs): string {
|
|
112
|
+
return inputs.unitSystem === 'metric' ? 'km/h' : 'mph';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function formatValue(value: number): string {
|
|
116
|
+
return new Intl.NumberFormat('en', { maximumFractionDigits: 1 }).format(value);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function formatRatio(value: number): string {
|
|
120
|
+
return new Intl.NumberFormat('en', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value);
|
|
121
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { MotorToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
import type { CarMotorcycleGearRatioCalculatorUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export type { CarMotorcycleGearRatioCalculatorUI };
|
|
5
|
+
export type CarMotorcycleGearRatioCalculatorLocaleContent = ToolLocaleContent<CarMotorcycleGearRatioCalculatorUI>;
|
|
6
|
+
|
|
7
|
+
export const carMotorcycleGearRatioCalculator: MotorToolEntry<CarMotorcycleGearRatioCalculatorUI> = {
|
|
8
|
+
id: 'car-motorcycle-gear-ratio-rpm-speed-calculator',
|
|
9
|
+
icons: {
|
|
10
|
+
bg: 'mdi:car-cog',
|
|
11
|
+
fg: 'mdi:transmission-tower',
|
|
12
|
+
},
|
|
13
|
+
i18n: {
|
|
14
|
+
de: () => import('./i18n/de').then((module) => module.content),
|
|
15
|
+
en: () => import('./i18n/en').then((module) => module.content),
|
|
16
|
+
es: () => import('./i18n/es').then((module) => module.content),
|
|
17
|
+
fr: () => import('./i18n/fr').then((module) => module.content),
|
|
18
|
+
'id': () => import('./i18n/id').then((module) => module.content),
|
|
19
|
+
it: () => import('./i18n/it').then((module) => module.content),
|
|
20
|
+
ja: () => import('./i18n/ja').then((module) => module.content),
|
|
21
|
+
ko: () => import('./i18n/ko').then((module) => module.content),
|
|
22
|
+
nl: () => import('./i18n/nl').then((module) => module.content),
|
|
23
|
+
pl: () => import('./i18n/pl').then((module) => module.content),
|
|
24
|
+
pt: () => import('./i18n/pt').then((module) => module.content),
|
|
25
|
+
ru: () => import('./i18n/ru').then((module) => module.content),
|
|
26
|
+
sv: () => import('./i18n/sv').then((module) => module.content),
|
|
27
|
+
tr: () => import('./i18n/tr').then((module) => module.content),
|
|
28
|
+
zh: () => import('./i18n/zh').then((module) => module.content),
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TransmissionInputs } from './logic';
|
|
2
|
+
import type { TransmissionResult } from './logic';
|
|
3
|
+
|
|
4
|
+
export type CalculationTone = 'normal' | 'warning' | 'critical';
|
|
5
|
+
|
|
6
|
+
export interface CalculationEvaluation {
|
|
7
|
+
tone: CalculationTone;
|
|
8
|
+
messageKey: 'readyMessage' | 'highRpmMessage' | 'lowRpmMessage' | 'invalidMessage';
|
|
9
|
+
selectedGear: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function evaluateTransmission(
|
|
13
|
+
inputs: TransmissionInputs,
|
|
14
|
+
result: TransmissionResult,
|
|
15
|
+
): CalculationEvaluation {
|
|
16
|
+
if (result.gears.length === 0 || result.gears.some((gear) => !Number.isFinite(gear.speed))) {
|
|
17
|
+
return { tone: 'critical', messageKey: 'invalidMessage', selectedGear: 0 };
|
|
18
|
+
}
|
|
19
|
+
const targetGear = result.gears.find((gear) => gear.gear === result.selectedGear);
|
|
20
|
+
if (!targetGear) return { tone: 'critical', messageKey: 'invalidMessage', selectedGear: 0 };
|
|
21
|
+
if (targetGear.rpmAtTarget > inputs.redlineRpm) {
|
|
22
|
+
return { tone: 'critical', messageKey: 'highRpmMessage', selectedGear: targetGear.gear };
|
|
23
|
+
}
|
|
24
|
+
if (targetGear.rpmAtTarget < 1200) {
|
|
25
|
+
return { tone: 'warning', messageKey: 'lowRpmMessage', selectedGear: targetGear.gear };
|
|
26
|
+
}
|
|
27
|
+
return { tone: 'normal', messageKey: 'readyMessage', selectedGear: targetGear.gear };
|
|
28
|
+
}
|