@jjlmoya/utils-home 1.16.0 → 1.23.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/i18n/de.ts +10 -10
- package/src/category/i18n/en.ts +8 -8
- package/src/category/i18n/es.ts +2 -2
- package/src/category/i18n/fr.ts +15 -15
- package/src/category/i18n/id.ts +8 -8
- package/src/category/i18n/it.ts +7 -7
- package/src/category/i18n/nl.ts +8 -8
- package/src/category/i18n/pl.ts +10 -10
- package/src/category/i18n/pt.ts +8 -8
- package/src/category/i18n/ru.ts +10 -10
- package/src/category/i18n/sv.ts +8 -8
- package/src/category/i18n/tr.ts +4 -4
- package/src/category/i18n/zh.ts +8 -8
- package/src/entries.ts +4 -1
- package/src/pages/[locale]/[slug].astro +28 -12
- package/src/tests/locale_completeness.test.ts +4 -22
- package/src/tests/no_en_dash.test.ts +70 -0
- package/src/tests/shared-test-helpers.ts +56 -0
- package/src/tests/tool_exports.test.ts +34 -0
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/dewPointCalculator/bibliography.ts +10 -0
- package/src/tool/dewPointCalculator/i18n/de.ts +7 -17
- package/src/tool/dewPointCalculator/i18n/en.ts +8 -18
- package/src/tool/dewPointCalculator/i18n/es.ts +7 -17
- package/src/tool/dewPointCalculator/i18n/fr.ts +8 -18
- package/src/tool/dewPointCalculator/i18n/id.ts +7 -17
- package/src/tool/dewPointCalculator/i18n/it.ts +7 -17
- package/src/tool/dewPointCalculator/i18n/ja.ts +6 -16
- package/src/tool/dewPointCalculator/i18n/ko.ts +6 -16
- package/src/tool/dewPointCalculator/i18n/nl.ts +7 -17
- package/src/tool/dewPointCalculator/i18n/pl.ts +7 -17
- package/src/tool/dewPointCalculator/i18n/pt.ts +7 -17
- package/src/tool/dewPointCalculator/i18n/ru.ts +13 -23
- package/src/tool/dewPointCalculator/i18n/sv.ts +7 -17
- package/src/tool/dewPointCalculator/i18n/tr.ts +6 -16
- package/src/tool/dewPointCalculator/i18n/zh.ts +7 -17
- package/src/tool/dewPointCalculator/seo.astro +2 -1
- package/src/tool/heatingComparator/bibliography.ts +14 -0
- package/src/tool/heatingComparator/i18n/de.ts +10 -24
- package/src/tool/heatingComparator/i18n/en.ts +3 -13
- package/src/tool/heatingComparator/i18n/es.ts +3 -17
- package/src/tool/heatingComparator/i18n/fr.ts +9 -19
- package/src/tool/heatingComparator/i18n/id.ts +3 -17
- package/src/tool/heatingComparator/i18n/it.ts +3 -17
- package/src/tool/heatingComparator/i18n/ja.ts +296 -310
- package/src/tool/heatingComparator/i18n/ko.ts +296 -306
- package/src/tool/heatingComparator/i18n/nl.ts +3 -17
- package/src/tool/heatingComparator/i18n/pl.ts +3 -17
- package/src/tool/heatingComparator/i18n/pt.ts +3 -17
- package/src/tool/heatingComparator/i18n/ru.ts +14 -24
- package/src/tool/heatingComparator/i18n/sv.ts +6 -20
- package/src/tool/heatingComparator/i18n/tr.ts +2 -16
- package/src/tool/heatingComparator/i18n/zh.ts +296 -306
- package/src/tool/heatingComparator/seo.astro +3 -3
- package/src/tool/ledSavingCalculator/bibliography.ts +14 -0
- package/src/tool/ledSavingCalculator/i18n/de.ts +6 -16
- package/src/tool/ledSavingCalculator/i18n/en.ts +6 -20
- package/src/tool/ledSavingCalculator/i18n/es.ts +6 -20
- package/src/tool/ledSavingCalculator/i18n/fr.ts +10 -24
- package/src/tool/ledSavingCalculator/i18n/id.ts +5 -15
- package/src/tool/ledSavingCalculator/i18n/it.ts +6 -16
- package/src/tool/ledSavingCalculator/i18n/ja.ts +5 -15
- package/src/tool/ledSavingCalculator/i18n/ko.ts +4 -14
- package/src/tool/ledSavingCalculator/i18n/nl.ts +5 -15
- package/src/tool/ledSavingCalculator/i18n/pl.ts +5 -15
- package/src/tool/ledSavingCalculator/i18n/pt.ts +5 -15
- package/src/tool/ledSavingCalculator/i18n/ru.ts +8 -18
- package/src/tool/ledSavingCalculator/i18n/sv.ts +5 -15
- package/src/tool/ledSavingCalculator/i18n/tr.ts +5 -15
- package/src/tool/ledSavingCalculator/i18n/zh.ts +6 -16
- package/src/tool/ledSavingCalculator/seo.astro +2 -1
- package/src/tool/projectorCalculator/bibliography.ts +5 -0
- package/src/tool/projectorCalculator/i18n/de.ts +4 -8
- package/src/tool/projectorCalculator/i18n/en.ts +3 -8
- package/src/tool/projectorCalculator/i18n/es.ts +4 -9
- package/src/tool/projectorCalculator/i18n/fr.ts +6 -11
- package/src/tool/projectorCalculator/i18n/id.ts +4 -9
- package/src/tool/projectorCalculator/i18n/it.ts +4 -8
- package/src/tool/projectorCalculator/i18n/ja.ts +175 -179
- package/src/tool/projectorCalculator/i18n/ko.ts +175 -179
- package/src/tool/projectorCalculator/i18n/nl.ts +4 -8
- package/src/tool/projectorCalculator/i18n/pl.ts +5 -9
- package/src/tool/projectorCalculator/i18n/pt.ts +4 -8
- package/src/tool/projectorCalculator/i18n/ru.ts +7 -11
- package/src/tool/projectorCalculator/i18n/sv.ts +4 -8
- package/src/tool/projectorCalculator/i18n/tr.ts +4 -8
- package/src/tool/projectorCalculator/i18n/zh.ts +175 -179
- package/src/tool/projectorCalculator/seo.astro +2 -1
- package/src/tool/qrGenerator/bibliography.ts +14 -0
- package/src/tool/qrGenerator/i18n/de.ts +192 -202
- package/src/tool/qrGenerator/i18n/en.ts +3 -17
- package/src/tool/qrGenerator/i18n/es.ts +2 -16
- package/src/tool/qrGenerator/i18n/fr.ts +3 -17
- package/src/tool/qrGenerator/i18n/id.ts +146 -150
- package/src/tool/qrGenerator/i18n/it.ts +169 -173
- package/src/tool/qrGenerator/i18n/ja.ts +146 -150
- package/src/tool/qrGenerator/i18n/ko.ts +146 -150
- package/src/tool/qrGenerator/i18n/nl.ts +146 -150
- package/src/tool/qrGenerator/i18n/pl.ts +146 -150
- package/src/tool/qrGenerator/i18n/pt.ts +146 -150
- package/src/tool/qrGenerator/i18n/ru.ts +146 -150
- package/src/tool/qrGenerator/i18n/sv.ts +146 -150
- package/src/tool/qrGenerator/i18n/tr.ts +146 -150
- package/src/tool/qrGenerator/i18n/zh.ts +146 -150
- package/src/tool/qrGenerator/seo.astro +2 -1
- package/src/tool/solarCalculator/bibliography.ts +5 -0
- package/src/tool/solarCalculator/i18n/de.ts +141 -145
- package/src/tool/solarCalculator/i18n/en.ts +7 -12
- package/src/tool/solarCalculator/i18n/es.ts +5 -10
- package/src/tool/solarCalculator/i18n/fr.ts +8 -13
- package/src/tool/solarCalculator/i18n/id.ts +4 -8
- package/src/tool/solarCalculator/i18n/it.ts +4 -8
- package/src/tool/solarCalculator/i18n/ja.ts +121 -125
- package/src/tool/solarCalculator/i18n/ko.ts +116 -120
- package/src/tool/solarCalculator/i18n/nl.ts +4 -7
- package/src/tool/solarCalculator/i18n/pl.ts +5 -9
- package/src/tool/solarCalculator/i18n/pt.ts +4 -8
- package/src/tool/solarCalculator/i18n/ru.ts +7 -10
- package/src/tool/solarCalculator/i18n/sv.ts +4 -7
- package/src/tool/solarCalculator/i18n/tr.ts +4 -7
- package/src/tool/solarCalculator/i18n/zh.ts +116 -120
- package/src/tool/solarCalculator/seo.astro +2 -1
- package/src/tool/tariffComparator/bibliography.ts +7 -0
- package/src/tool/tariffComparator/i18n/de.ts +129 -132
- package/src/tool/tariffComparator/i18n/en.ts +5 -12
- package/src/tool/tariffComparator/i18n/es.ts +5 -12
- package/src/tool/tariffComparator/i18n/fr.ts +8 -15
- package/src/tool/tariffComparator/i18n/id.ts +2 -5
- package/src/tool/tariffComparator/i18n/it.ts +2 -5
- package/src/tool/tariffComparator/i18n/ja.ts +129 -132
- package/src/tool/tariffComparator/i18n/ko.ts +129 -132
- package/src/tool/tariffComparator/i18n/nl.ts +2 -5
- package/src/tool/tariffComparator/i18n/pl.ts +3 -6
- package/src/tool/tariffComparator/i18n/pt.ts +2 -5
- package/src/tool/tariffComparator/i18n/ru.ts +2 -5
- package/src/tool/tariffComparator/i18n/sv.ts +2 -5
- package/src/tool/tariffComparator/i18n/tr.ts +2 -5
- package/src/tool/tariffComparator/i18n/zh.ts +129 -132
- package/src/tool/tariffComparator/seo.astro +2 -1
- package/src/tool/wifiRangeSimulator/bibliography.astro +14 -0
- package/src/tool/wifiRangeSimulator/bibliography.ts +14 -0
- package/src/tool/wifiRangeSimulator/component.astro +170 -0
- package/src/tool/wifiRangeSimulator/entry.ts +29 -0
- package/src/tool/wifiRangeSimulator/i18n/de.ts +477 -0
- package/src/tool/wifiRangeSimulator/i18n/en.ts +477 -0
- package/src/tool/wifiRangeSimulator/i18n/es.ts +477 -0
- package/src/tool/wifiRangeSimulator/i18n/fr.ts +477 -0
- package/src/tool/wifiRangeSimulator/i18n/id.ts +477 -0
- package/src/tool/wifiRangeSimulator/i18n/it.ts +477 -0
- package/src/tool/wifiRangeSimulator/i18n/ja.ts +477 -0
- package/src/tool/wifiRangeSimulator/i18n/ko.ts +477 -0
- package/src/tool/wifiRangeSimulator/i18n/nl.ts +477 -0
- package/src/tool/wifiRangeSimulator/i18n/pl.ts +477 -0
- package/src/tool/wifiRangeSimulator/i18n/pt.ts +477 -0
- package/src/tool/wifiRangeSimulator/i18n/ru.ts +477 -0
- package/src/tool/wifiRangeSimulator/i18n/sv.ts +477 -0
- package/src/tool/wifiRangeSimulator/i18n/tr.ts +477 -0
- package/src/tool/wifiRangeSimulator/i18n/zh.ts +477 -0
- package/src/tool/wifiRangeSimulator/i18n-utils.ts +14 -0
- package/src/tool/wifiRangeSimulator/index.ts +8 -0
- package/src/tool/wifiRangeSimulator/logic.ts +220 -0
- package/src/tool/wifiRangeSimulator/seo.astro +15 -0
- package/src/tool/wifiRangeSimulator/sketch-actions.ts +168 -0
- package/src/tool/wifiRangeSimulator/sketch-events.ts +138 -0
- package/src/tool/wifiRangeSimulator/sketch-render-dash.ts +170 -0
- package/src/tool/wifiRangeSimulator/sketch-render-device.ts +42 -0
- package/src/tool/wifiRangeSimulator/sketch-render.ts +155 -0
- package/src/tool/wifiRangeSimulator/sketch-state.ts +186 -0
- package/src/tool/wifiRangeSimulator/sketch.ts +100 -0
- package/src/tool/wifiRangeSimulator/ui.ts +69 -0
- package/src/tool/wifiRangeSimulator/wifi-range-simulator.css +583 -0
- package/src/tools.ts +2 -0
- package/src/types.ts +0 -2
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
export interface Point {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface Segment {
|
|
7
|
+
a: Point;
|
|
8
|
+
b: Point;
|
|
9
|
+
material: string;
|
|
10
|
+
attenuation: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PlacedObject {
|
|
14
|
+
id: string;
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
material: string;
|
|
18
|
+
attenuation: number;
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PlacedDevice {
|
|
24
|
+
id: string;
|
|
25
|
+
x: number;
|
|
26
|
+
y: number;
|
|
27
|
+
name: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SignalResult {
|
|
31
|
+
strengthPercent: number;
|
|
32
|
+
effectiveRange: number;
|
|
33
|
+
verdict: 'perfect' | 'good' | 'fair' | 'poor' | 'dead';
|
|
34
|
+
streamingVerdict: Record<string, string>;
|
|
35
|
+
rayCount: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const MATERIALS: Record<string, { attenuation: number; color: string }> = {
|
|
39
|
+
drywall: { attenuation: 3, color: '#e7e5e4' },
|
|
40
|
+
brick: { attenuation: 8, color: '#a16207' },
|
|
41
|
+
concrete: { attenuation: 15, color: '#57534e' },
|
|
42
|
+
stoneWall: { attenuation: 22, color: '#44403c' },
|
|
43
|
+
woodDoor: { attenuation: 4, color: '#92400e' },
|
|
44
|
+
metalDoor: { attenuation: 18, color: '#334155' },
|
|
45
|
+
window: { attenuation: 2, color: '#bfdbfe' },
|
|
46
|
+
fridge: { attenuation: 10, color: '#94a3b8' },
|
|
47
|
+
aquarium: { attenuation: 12, color: '#0ea5e9' },
|
|
48
|
+
microwave: { attenuation: 5, color: '#475569' },
|
|
49
|
+
mirror: { attenuation: 6, color: '#c0c0c0' },
|
|
50
|
+
furniture: { attenuation: 5, color: '#78350f' },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function dist(a: Point, b: Point): number {
|
|
54
|
+
const dx = b.x - a.x;
|
|
55
|
+
const dy = b.y - a.y;
|
|
56
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function sub(a: Point, b: Point): Point {
|
|
60
|
+
return { x: a.x - b.x, y: a.y - b.y };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalize(v: Point): Point {
|
|
64
|
+
const len = Math.sqrt(v.x * v.x + v.y * v.y);
|
|
65
|
+
if (len < 1e-9) return { x: 0, y: 0 };
|
|
66
|
+
return { x: v.x / len, y: v.y / len };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function intersectRayWithSegment(origin: Point, dir: Point, seg: Segment): { point: Point; d: number } | null {
|
|
70
|
+
const segDir = sub(seg.b, seg.a);
|
|
71
|
+
const rxs = dir.x * segDir.y - dir.y * segDir.x;
|
|
72
|
+
if (Math.abs(rxs) < 1e-9) return null;
|
|
73
|
+
const qp = sub(seg.a, origin);
|
|
74
|
+
const t = (qp.x * segDir.y - qp.y * segDir.x) / rxs;
|
|
75
|
+
const u = (qp.x * dir.y - qp.y * dir.x) / rxs;
|
|
76
|
+
if (t > 0 && u >= 0 && u <= 1) {
|
|
77
|
+
return { point: { x: origin.x + t * dir.x, y: origin.y + t * dir.y }, d: t };
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getObjectSegments(obj: PlacedObject): Segment[] {
|
|
83
|
+
const hw = obj.width / 2;
|
|
84
|
+
const hh = obj.height / 2;
|
|
85
|
+
const tl = { x: obj.x - hw, y: obj.y - hh };
|
|
86
|
+
const tr = { x: obj.x + hw, y: obj.y - hh };
|
|
87
|
+
const br = { x: obj.x + hw, y: obj.y + hh };
|
|
88
|
+
const bl = { x: obj.x - hw, y: obj.y + hh };
|
|
89
|
+
return [
|
|
90
|
+
{ a: tl, b: tr, material: obj.material, attenuation: obj.attenuation },
|
|
91
|
+
{ a: tr, b: br, material: obj.material, attenuation: obj.attenuation },
|
|
92
|
+
{ a: br, b: bl, material: obj.material, attenuation: obj.attenuation },
|
|
93
|
+
{ a: bl, b: tl, material: obj.material, attenuation: obj.attenuation },
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function findAllHits(origin: Point, dir: Point, segments: Segment[], maxDist: number) {
|
|
98
|
+
const hits: { point: Point; seg: Segment; d: number }[] = [];
|
|
99
|
+
for (const seg of segments) {
|
|
100
|
+
const hit = intersectRayWithSegment(origin, dir, seg);
|
|
101
|
+
if (hit && hit.d > 1 && hit.d < maxDist) {
|
|
102
|
+
hits.push({ point: hit.point, seg, d: hit.d });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
hits.sort((a, b) => a.d - b.d);
|
|
106
|
+
return hits;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function castDirectRay(router: Point, device: Point, segments: Segment[]): number {
|
|
110
|
+
const directDist = dist(router, device);
|
|
111
|
+
const dir = normalize(sub(device, router));
|
|
112
|
+
const hits = findAllHits(router, dir, segments, directDist);
|
|
113
|
+
return hits.reduce((sum, h) => sum + h.seg.attenuation, 0);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function buildSegments(walls: Segment[], objects: PlacedObject[]): Segment[] {
|
|
117
|
+
const segs: Segment[] = [...walls];
|
|
118
|
+
for (const obj of objects) {
|
|
119
|
+
segs.push(...getObjectSegments(obj));
|
|
120
|
+
}
|
|
121
|
+
return segs;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function computeVerdict(percent: number): SignalResult['verdict'] {
|
|
125
|
+
if (percent >= 80) return 'perfect';
|
|
126
|
+
if (percent >= 60) return 'good';
|
|
127
|
+
if (percent >= 40) return 'fair';
|
|
128
|
+
if (percent >= 20) return 'poor';
|
|
129
|
+
return 'dead';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function streamStatus(pct: number, thresholds: number[], labels: string[]): string {
|
|
133
|
+
for (let i = 0; i < thresholds.length; i++) {
|
|
134
|
+
if (pct >= thresholds[i]) return labels[i];
|
|
135
|
+
}
|
|
136
|
+
return labels[labels.length - 1];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function streaming4K(pct: number, labels: string[]): string {
|
|
140
|
+
return streamStatus(pct, [70, 40], labels);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function streamingGaming(pct: number, labels: string[]): string {
|
|
144
|
+
return streamStatus(pct, [60, 30], labels);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function streamingCalls(pct: number, labels: string[]): string {
|
|
148
|
+
return streamStatus(pct, [50, 25], labels);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function streamingBrowsing(pct: number, labels: string[]): string {
|
|
152
|
+
return streamStatus(pct, [20], labels);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const DEFAULT_STATUS_LABELS = {
|
|
156
|
+
statusPerfect: 'Perfect',
|
|
157
|
+
statusGood: 'Good',
|
|
158
|
+
statusFair: 'Fair',
|
|
159
|
+
statusPoor: 'Poor',
|
|
160
|
+
statusImpossible: 'Impossible',
|
|
161
|
+
statusLowLatency: 'Low Latency',
|
|
162
|
+
statusLagWarning: 'Lag Warning',
|
|
163
|
+
statusDisconnect: 'Disconnect',
|
|
164
|
+
statusStable: 'Stable',
|
|
165
|
+
statusPixelated: 'Pixelated',
|
|
166
|
+
statusDropped: 'Dropped',
|
|
167
|
+
statusPass: 'Pass',
|
|
168
|
+
statusUnusable: 'Unusable',
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export function computeStreamingVerdict(pct: number, labels?: typeof DEFAULT_STATUS_LABELS): Record<string, string> {
|
|
172
|
+
const l = labels || DEFAULT_STATUS_LABELS;
|
|
173
|
+
return {
|
|
174
|
+
'4kStreaming': streaming4K(pct, [l.statusPerfect, l.statusGood, l.statusImpossible]),
|
|
175
|
+
onlineGaming: streamingGaming(pct, [l.statusLowLatency, l.statusLagWarning, l.statusDisconnect]),
|
|
176
|
+
videoCalls: streamingCalls(pct, [l.statusStable, l.statusPixelated, l.statusDropped]),
|
|
177
|
+
basicBrowsing: streamingBrowsing(pct, [l.statusPass, l.statusUnusable]),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function calculateSignalFromSketch(
|
|
182
|
+
router: Point,
|
|
183
|
+
device: Point,
|
|
184
|
+
walls: Segment[],
|
|
185
|
+
objects: PlacedObject[]
|
|
186
|
+
): SignalResult {
|
|
187
|
+
const segments = buildSegments(walls, objects);
|
|
188
|
+
const directDist = dist(router, device);
|
|
189
|
+
|
|
190
|
+
const distanceLoss = Math.min(70, directDist / 8);
|
|
191
|
+
const obstacleLoss = castDirectRay(router, device, segments);
|
|
192
|
+
const totalLoss = Math.min(100, distanceLoss + obstacleLoss);
|
|
193
|
+
const strengthPercent = Math.max(0, Math.round(100 - totalLoss));
|
|
194
|
+
|
|
195
|
+
const effectiveRange = Math.max(0, Math.round(directDist / 10));
|
|
196
|
+
const verdict = computeVerdict(strengthPercent);
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
strengthPercent,
|
|
200
|
+
effectiveRange,
|
|
201
|
+
verdict,
|
|
202
|
+
streamingVerdict: computeStreamingVerdict(strengthPercent),
|
|
203
|
+
rayCount: 1,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function getVerdictColor(verdict: SignalResult['verdict']): string {
|
|
208
|
+
switch (verdict) {
|
|
209
|
+
case 'perfect':
|
|
210
|
+
return '#22c55e';
|
|
211
|
+
case 'good':
|
|
212
|
+
return '#84cc16';
|
|
213
|
+
case 'fair':
|
|
214
|
+
return '#f59e0b';
|
|
215
|
+
case 'poor':
|
|
216
|
+
return '#f97316';
|
|
217
|
+
case 'dead':
|
|
218
|
+
return '#ef4444';
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { wifiRangeSimulator } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'en' } = Astro.props;
|
|
11
|
+
const content = await wifiRangeSimulator.i18n[locale]?.();
|
|
12
|
+
if (!content) return null;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
{content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { PlacedDevice } from './logic';
|
|
2
|
+
import type { State, Snapshot, History } from './sketch-state';
|
|
3
|
+
import { saveState, renderDeviceSelection } from './sketch-state';
|
|
4
|
+
import { renderObjects, renderWalls, renderRouter, renderDevices, spawnParticle } from './sketch-render';
|
|
5
|
+
import { updateDashboard } from './sketch-render-dash';
|
|
6
|
+
import { getUI } from './i18n-utils';
|
|
7
|
+
export { saveState };
|
|
8
|
+
|
|
9
|
+
export function addToHistory(h: History, s: State) {
|
|
10
|
+
if (h.pos < h.stack.length - 1) {
|
|
11
|
+
h.stack = h.stack.slice(0, h.pos + 1);
|
|
12
|
+
}
|
|
13
|
+
const snap: Snapshot = {
|
|
14
|
+
router: { ...s.router },
|
|
15
|
+
devices: s.devices.map((d) => ({ ...d })),
|
|
16
|
+
walls: s.walls.map((w) => ({ ...w, a: { ...w.a }, b: { ...w.b } })),
|
|
17
|
+
objects: s.objects.map((o) => ({ ...o })),
|
|
18
|
+
nextId: s.nextId,
|
|
19
|
+
zoom: s.zoom,
|
|
20
|
+
panX: s.panX,
|
|
21
|
+
panY: s.panY,
|
|
22
|
+
};
|
|
23
|
+
h.stack.push(snap);
|
|
24
|
+
if (h.stack.length > 50) h.stack.shift();
|
|
25
|
+
h.pos = h.stack.length - 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function applySnapshot(s: State, snap: Snapshot) {
|
|
29
|
+
s.router = { ...snap.router };
|
|
30
|
+
s.devices = snap.devices.map((d) => ({ ...d }));
|
|
31
|
+
s.walls = snap.walls.map((w) => ({ ...w, a: { ...w.a }, b: { ...w.b } }));
|
|
32
|
+
s.objects = snap.objects.map((o) => ({ ...o }));
|
|
33
|
+
s.nextId = snap.nextId;
|
|
34
|
+
s.zoom = typeof snap.zoom === 'number' ? snap.zoom : 1;
|
|
35
|
+
s.panX = typeof snap.panX === 'number' ? snap.panX : 0;
|
|
36
|
+
s.panY = typeof snap.panY === 'number' ? snap.panY : 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function renderZoom(s: State) {
|
|
40
|
+
const world = document.getElementById('sketch-world');
|
|
41
|
+
if (world) {
|
|
42
|
+
world.style.transform = `translate(${s.panX}px, ${s.panY}px) scale(${s.zoom})`;
|
|
43
|
+
}
|
|
44
|
+
const track = document.getElementById('sketch-scale-track');
|
|
45
|
+
if (track) {
|
|
46
|
+
const segPx = Math.max(6, 10 * s.zoom);
|
|
47
|
+
track.style.width = (segPx * 5) + 'px';
|
|
48
|
+
track.querySelectorAll('.sketch-scale-segment').forEach((el) => {
|
|
49
|
+
(el as HTMLElement).style.width = segPx + 'px';
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getNextDeviceId(s: State): number {
|
|
55
|
+
const used = new Set(s.devices.map((d) => d.id));
|
|
56
|
+
let n = 1;
|
|
57
|
+
while (used.has(`dev-${n}`)) n++;
|
|
58
|
+
return n;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function addDevice(s: State, h: History) {
|
|
62
|
+
const ui = getUI();
|
|
63
|
+
if (s.devices.length >= 4) {
|
|
64
|
+
spawnParticle(400, 250, ui.labelMaxDevices);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const id = getNextDeviceId(s);
|
|
68
|
+
s.nextId = id + 1;
|
|
69
|
+
const dev: PlacedDevice = {
|
|
70
|
+
id: `dev-${id}`,
|
|
71
|
+
x: 680,
|
|
72
|
+
y: 160 + (s.devices.length * 80),
|
|
73
|
+
name: `${ui.labelDevicePrefix} ${s.devices.length + 1}`,
|
|
74
|
+
};
|
|
75
|
+
s.devices.push(dev);
|
|
76
|
+
saveState(s);
|
|
77
|
+
addToHistory(h, s);
|
|
78
|
+
renderDevices(s.devices, s.router, s.walls, s.objects);
|
|
79
|
+
updateDashboard(s.router, s.devices, s.walls, s.objects);
|
|
80
|
+
renderDeviceSelection(s);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function doUndo(s: State, h: History) {
|
|
84
|
+
if (h.pos <= 0) return;
|
|
85
|
+
h.pos--;
|
|
86
|
+
applySnapshot(s, h.stack[h.pos]);
|
|
87
|
+
renderWalls(s.walls);
|
|
88
|
+
renderObjects(s.objects);
|
|
89
|
+
renderRouter(s.router);
|
|
90
|
+
renderDevices(s.devices, s.router, s.walls, s.objects);
|
|
91
|
+
updateDashboard(s.router, s.devices, s.walls, s.objects);
|
|
92
|
+
renderDeviceSelection(s);
|
|
93
|
+
renderZoom(s);
|
|
94
|
+
saveState(s);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function doClear(s: State, h: History) {
|
|
98
|
+
const ui = getUI();
|
|
99
|
+
s.walls = [];
|
|
100
|
+
s.objects = [];
|
|
101
|
+
s.devices = [{ id: 'dev-1', x: 680, y: 250, name: `${ui.labelDevicePrefix} 1` }];
|
|
102
|
+
s.nextId = 2;
|
|
103
|
+
s.selectedDeviceId = null;
|
|
104
|
+
saveState(s);
|
|
105
|
+
addToHistory(h, s);
|
|
106
|
+
renderWalls(s.walls);
|
|
107
|
+
renderObjects(s.objects);
|
|
108
|
+
renderDevices(s.devices, s.router, s.walls, s.objects);
|
|
109
|
+
updateDashboard(s.router, s.devices, s.walls, s.objects);
|
|
110
|
+
renderDeviceSelection(s);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function doClearWalls(s: State, h: History) {
|
|
114
|
+
s.walls = [];
|
|
115
|
+
saveState(s);
|
|
116
|
+
addToHistory(h, s);
|
|
117
|
+
renderWalls(s.walls);
|
|
118
|
+
updateDashboard(s.router, s.devices, s.walls, s.objects);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function doClearObjects(s: State, h: History) {
|
|
122
|
+
s.objects = [];
|
|
123
|
+
saveState(s);
|
|
124
|
+
addToHistory(h, s);
|
|
125
|
+
renderObjects(s.objects);
|
|
126
|
+
updateDashboard(s.router, s.devices, s.walls, s.objects);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function setTool(s: State, tool: string) {
|
|
130
|
+
s.tool = tool;
|
|
131
|
+
document.querySelectorAll('[data-tool]').forEach((b) => b.classList.remove('active'));
|
|
132
|
+
const active = document.querySelector(`[data-tool="${tool}"]`);
|
|
133
|
+
if (active) active.classList.add('active');
|
|
134
|
+
const wrap = document.getElementById('sketch-canvas-wrap');
|
|
135
|
+
if (wrap) wrap.style.cursor = tool === 'select' ? 'default' : 'crosshair';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const toolMap: Record<string, (s: State, h: History) => void> = {
|
|
139
|
+
zoomIn: (s) => {
|
|
140
|
+
s.zoom = Math.min(3, Math.round((s.zoom + 0.25) * 100) / 100);
|
|
141
|
+
renderZoom(s);
|
|
142
|
+
saveState(s);
|
|
143
|
+
},
|
|
144
|
+
zoomOut: (s) => {
|
|
145
|
+
s.zoom = Math.max(0.5, Math.round((s.zoom - 0.25) * 100) / 100);
|
|
146
|
+
renderZoom(s);
|
|
147
|
+
saveState(s);
|
|
148
|
+
},
|
|
149
|
+
undo: doUndo,
|
|
150
|
+
addDevice,
|
|
151
|
+
clearWalls: doClearWalls,
|
|
152
|
+
clearObjects: doClearObjects,
|
|
153
|
+
clear: doClear,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export function initToolButtons(s: State, h: History) {
|
|
157
|
+
document.querySelectorAll('[data-tool]').forEach((btn) => {
|
|
158
|
+
btn.addEventListener('click', () => {
|
|
159
|
+
const tool = (btn as HTMLElement).dataset.tool;
|
|
160
|
+
if (!tool) return;
|
|
161
|
+
if (toolMap[tool]) {
|
|
162
|
+
toolMap[tool](s, h);
|
|
163
|
+
} else {
|
|
164
|
+
setTool(s, tool);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { Point } from './logic';
|
|
2
|
+
import { MATERIALS } from './logic';
|
|
3
|
+
import type { State, History } from './sketch-state';
|
|
4
|
+
import {
|
|
5
|
+
ptToSvg,
|
|
6
|
+
isWallTool,
|
|
7
|
+
hitRouter,
|
|
8
|
+
nearestDevice,
|
|
9
|
+
renderDeviceSelection,
|
|
10
|
+
removePreviewWall,
|
|
11
|
+
updatePreviewWall,
|
|
12
|
+
} from './sketch-state';
|
|
13
|
+
import { renderObjects, renderWalls, renderRouter, renderDevices } from './sketch-render';
|
|
14
|
+
import { updateDashboard } from './sketch-render-dash';
|
|
15
|
+
import { addToHistory, saveState, renderZoom } from './sketch-actions';
|
|
16
|
+
import { spawnParticle } from './sketch-render';
|
|
17
|
+
|
|
18
|
+
function startSelect(s: State, p: Point, e: MouseEvent) {
|
|
19
|
+
if (hitRouter(s, p)) {
|
|
20
|
+
s.dragging = 'router';
|
|
21
|
+
s.selectedDeviceId = null;
|
|
22
|
+
renderDeviceSelection(s);
|
|
23
|
+
s.dragOffset = { x: p.x - s.router.x, y: p.y - s.router.y };
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const dev = nearestDevice(s.devices, p);
|
|
27
|
+
if (dev) {
|
|
28
|
+
s.selectedDeviceId = dev.id;
|
|
29
|
+
s.dragging = `device:${dev.id}`;
|
|
30
|
+
s.dragOffset = { x: p.x - dev.x, y: p.y - dev.y };
|
|
31
|
+
renderDeviceSelection(s);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
s.selectedDeviceId = null;
|
|
35
|
+
renderDeviceSelection(s);
|
|
36
|
+
s.dragging = 'pan';
|
|
37
|
+
s.dragOffset = { x: e.clientX, y: e.clientY };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function placeObject(s: State, p: Point) {
|
|
41
|
+
const m = MATERIALS[s.tool];
|
|
42
|
+
if (!m) return;
|
|
43
|
+
const id = `obj-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
44
|
+
s.objects.push({
|
|
45
|
+
id,
|
|
46
|
+
x: p.x,
|
|
47
|
+
y: p.y,
|
|
48
|
+
material: s.tool,
|
|
49
|
+
attenuation: m.attenuation,
|
|
50
|
+
width: 32,
|
|
51
|
+
height: 32,
|
|
52
|
+
});
|
|
53
|
+
saveState(s);
|
|
54
|
+
renderObjects(s.objects);
|
|
55
|
+
updateDashboard(s.router, s.devices, s.walls, s.objects);
|
|
56
|
+
spawnParticle(p.x, p.y, `+${s.tool}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function handleMouseDown(s: State, svg: SVGSVGElement, e: MouseEvent) {
|
|
60
|
+
const p = ptToSvg(svg, e.clientX, e.clientY, s.zoom);
|
|
61
|
+
if (s.tool === 'select') {
|
|
62
|
+
startSelect(s, p, e);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (isWallTool(s.tool)) {
|
|
66
|
+
s.drawing = true;
|
|
67
|
+
s.drawStart = p;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
placeObject(s, p);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function handleMouseMove(s: State, svg: SVGSVGElement, e: MouseEvent) {
|
|
74
|
+
if (!s.dragging && !s.drawing) return;
|
|
75
|
+
if (s.dragging === 'pan') {
|
|
76
|
+
const dx = e.clientX - s.dragOffset.x;
|
|
77
|
+
const dy = e.clientY - s.dragOffset.y;
|
|
78
|
+
s.panX += dx;
|
|
79
|
+
s.panY += dy;
|
|
80
|
+
s.dragOffset = { x: e.clientX, y: e.clientY };
|
|
81
|
+
renderZoom(s);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const p = ptToSvg(svg, e.clientX, e.clientY, s.zoom);
|
|
85
|
+
updateDrag(s, p);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function handleMouseUp(s: State, svg: SVGSVGElement, h: History, e: MouseEvent) {
|
|
89
|
+
if (s.drawing && isWallTool(s.tool)) {
|
|
90
|
+
const p = ptToSvg(svg, e.clientX, e.clientY, s.zoom);
|
|
91
|
+
const dx = p.x - s.drawStart.x;
|
|
92
|
+
const dy = p.y - s.drawStart.y;
|
|
93
|
+
if (Math.sqrt(dx * dx + dy * dy) > 5) {
|
|
94
|
+
const m = MATERIALS[s.tool];
|
|
95
|
+
s.walls.push({
|
|
96
|
+
a: { ...s.drawStart },
|
|
97
|
+
b: { ...p },
|
|
98
|
+
material: s.tool,
|
|
99
|
+
attenuation: m.attenuation,
|
|
100
|
+
});
|
|
101
|
+
saveState(s);
|
|
102
|
+
addToHistory(h, s);
|
|
103
|
+
renderWalls(s.walls);
|
|
104
|
+
updateDashboard(s.router, s.devices, s.walls, s.objects);
|
|
105
|
+
}
|
|
106
|
+
removePreviewWall();
|
|
107
|
+
}
|
|
108
|
+
if (s.dragging) {
|
|
109
|
+
s.dragging = null;
|
|
110
|
+
saveState(s);
|
|
111
|
+
addToHistory(h, s);
|
|
112
|
+
}
|
|
113
|
+
s.drawing = false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function updateDrag(s: State, p: Point) {
|
|
117
|
+
if (s.dragging === 'router') {
|
|
118
|
+
s.router.x = p.x - s.dragOffset.x;
|
|
119
|
+
s.router.y = p.y - s.dragOffset.y;
|
|
120
|
+
renderRouter(s.router);
|
|
121
|
+
updateDashboard(s.router, s.devices, s.walls, s.objects);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (s.dragging?.startsWith('device:')) {
|
|
125
|
+
const id = s.dragging.slice(7);
|
|
126
|
+
const dev = s.devices.find((d) => d.id === id);
|
|
127
|
+
if (!dev) return;
|
|
128
|
+
dev.x = p.x - s.dragOffset.x;
|
|
129
|
+
dev.y = p.y - s.dragOffset.y;
|
|
130
|
+
renderDevices(s.devices, s.router, s.walls, s.objects);
|
|
131
|
+
updateDashboard(s.router, s.devices, s.walls, s.objects);
|
|
132
|
+
renderDeviceSelection(s);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (s.drawing && isWallTool(s.tool)) {
|
|
136
|
+
updatePreviewWall(s.drawStart, p, MATERIALS[s.tool].color);
|
|
137
|
+
}
|
|
138
|
+
}
|