@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.
Files changed (174) hide show
  1. package/package.json +1 -1
  2. package/src/category/i18n/de.ts +10 -10
  3. package/src/category/i18n/en.ts +8 -8
  4. package/src/category/i18n/es.ts +2 -2
  5. package/src/category/i18n/fr.ts +15 -15
  6. package/src/category/i18n/id.ts +8 -8
  7. package/src/category/i18n/it.ts +7 -7
  8. package/src/category/i18n/nl.ts +8 -8
  9. package/src/category/i18n/pl.ts +10 -10
  10. package/src/category/i18n/pt.ts +8 -8
  11. package/src/category/i18n/ru.ts +10 -10
  12. package/src/category/i18n/sv.ts +8 -8
  13. package/src/category/i18n/tr.ts +4 -4
  14. package/src/category/i18n/zh.ts +8 -8
  15. package/src/entries.ts +4 -1
  16. package/src/pages/[locale]/[slug].astro +28 -12
  17. package/src/tests/locale_completeness.test.ts +4 -22
  18. package/src/tests/no_en_dash.test.ts +70 -0
  19. package/src/tests/shared-test-helpers.ts +56 -0
  20. package/src/tests/tool_exports.test.ts +34 -0
  21. package/src/tests/tool_validation.test.ts +2 -2
  22. package/src/tool/dewPointCalculator/bibliography.ts +10 -0
  23. package/src/tool/dewPointCalculator/i18n/de.ts +7 -17
  24. package/src/tool/dewPointCalculator/i18n/en.ts +8 -18
  25. package/src/tool/dewPointCalculator/i18n/es.ts +7 -17
  26. package/src/tool/dewPointCalculator/i18n/fr.ts +8 -18
  27. package/src/tool/dewPointCalculator/i18n/id.ts +7 -17
  28. package/src/tool/dewPointCalculator/i18n/it.ts +7 -17
  29. package/src/tool/dewPointCalculator/i18n/ja.ts +6 -16
  30. package/src/tool/dewPointCalculator/i18n/ko.ts +6 -16
  31. package/src/tool/dewPointCalculator/i18n/nl.ts +7 -17
  32. package/src/tool/dewPointCalculator/i18n/pl.ts +7 -17
  33. package/src/tool/dewPointCalculator/i18n/pt.ts +7 -17
  34. package/src/tool/dewPointCalculator/i18n/ru.ts +13 -23
  35. package/src/tool/dewPointCalculator/i18n/sv.ts +7 -17
  36. package/src/tool/dewPointCalculator/i18n/tr.ts +6 -16
  37. package/src/tool/dewPointCalculator/i18n/zh.ts +7 -17
  38. package/src/tool/dewPointCalculator/seo.astro +2 -1
  39. package/src/tool/heatingComparator/bibliography.ts +14 -0
  40. package/src/tool/heatingComparator/i18n/de.ts +10 -24
  41. package/src/tool/heatingComparator/i18n/en.ts +3 -13
  42. package/src/tool/heatingComparator/i18n/es.ts +3 -17
  43. package/src/tool/heatingComparator/i18n/fr.ts +9 -19
  44. package/src/tool/heatingComparator/i18n/id.ts +3 -17
  45. package/src/tool/heatingComparator/i18n/it.ts +3 -17
  46. package/src/tool/heatingComparator/i18n/ja.ts +296 -310
  47. package/src/tool/heatingComparator/i18n/ko.ts +296 -306
  48. package/src/tool/heatingComparator/i18n/nl.ts +3 -17
  49. package/src/tool/heatingComparator/i18n/pl.ts +3 -17
  50. package/src/tool/heatingComparator/i18n/pt.ts +3 -17
  51. package/src/tool/heatingComparator/i18n/ru.ts +14 -24
  52. package/src/tool/heatingComparator/i18n/sv.ts +6 -20
  53. package/src/tool/heatingComparator/i18n/tr.ts +2 -16
  54. package/src/tool/heatingComparator/i18n/zh.ts +296 -306
  55. package/src/tool/heatingComparator/seo.astro +3 -3
  56. package/src/tool/ledSavingCalculator/bibliography.ts +14 -0
  57. package/src/tool/ledSavingCalculator/i18n/de.ts +6 -16
  58. package/src/tool/ledSavingCalculator/i18n/en.ts +6 -20
  59. package/src/tool/ledSavingCalculator/i18n/es.ts +6 -20
  60. package/src/tool/ledSavingCalculator/i18n/fr.ts +10 -24
  61. package/src/tool/ledSavingCalculator/i18n/id.ts +5 -15
  62. package/src/tool/ledSavingCalculator/i18n/it.ts +6 -16
  63. package/src/tool/ledSavingCalculator/i18n/ja.ts +5 -15
  64. package/src/tool/ledSavingCalculator/i18n/ko.ts +4 -14
  65. package/src/tool/ledSavingCalculator/i18n/nl.ts +5 -15
  66. package/src/tool/ledSavingCalculator/i18n/pl.ts +5 -15
  67. package/src/tool/ledSavingCalculator/i18n/pt.ts +5 -15
  68. package/src/tool/ledSavingCalculator/i18n/ru.ts +8 -18
  69. package/src/tool/ledSavingCalculator/i18n/sv.ts +5 -15
  70. package/src/tool/ledSavingCalculator/i18n/tr.ts +5 -15
  71. package/src/tool/ledSavingCalculator/i18n/zh.ts +6 -16
  72. package/src/tool/ledSavingCalculator/seo.astro +2 -1
  73. package/src/tool/projectorCalculator/bibliography.ts +5 -0
  74. package/src/tool/projectorCalculator/i18n/de.ts +4 -8
  75. package/src/tool/projectorCalculator/i18n/en.ts +3 -8
  76. package/src/tool/projectorCalculator/i18n/es.ts +4 -9
  77. package/src/tool/projectorCalculator/i18n/fr.ts +6 -11
  78. package/src/tool/projectorCalculator/i18n/id.ts +4 -9
  79. package/src/tool/projectorCalculator/i18n/it.ts +4 -8
  80. package/src/tool/projectorCalculator/i18n/ja.ts +175 -179
  81. package/src/tool/projectorCalculator/i18n/ko.ts +175 -179
  82. package/src/tool/projectorCalculator/i18n/nl.ts +4 -8
  83. package/src/tool/projectorCalculator/i18n/pl.ts +5 -9
  84. package/src/tool/projectorCalculator/i18n/pt.ts +4 -8
  85. package/src/tool/projectorCalculator/i18n/ru.ts +7 -11
  86. package/src/tool/projectorCalculator/i18n/sv.ts +4 -8
  87. package/src/tool/projectorCalculator/i18n/tr.ts +4 -8
  88. package/src/tool/projectorCalculator/i18n/zh.ts +175 -179
  89. package/src/tool/projectorCalculator/seo.astro +2 -1
  90. package/src/tool/qrGenerator/bibliography.ts +14 -0
  91. package/src/tool/qrGenerator/i18n/de.ts +192 -202
  92. package/src/tool/qrGenerator/i18n/en.ts +3 -17
  93. package/src/tool/qrGenerator/i18n/es.ts +2 -16
  94. package/src/tool/qrGenerator/i18n/fr.ts +3 -17
  95. package/src/tool/qrGenerator/i18n/id.ts +146 -150
  96. package/src/tool/qrGenerator/i18n/it.ts +169 -173
  97. package/src/tool/qrGenerator/i18n/ja.ts +146 -150
  98. package/src/tool/qrGenerator/i18n/ko.ts +146 -150
  99. package/src/tool/qrGenerator/i18n/nl.ts +146 -150
  100. package/src/tool/qrGenerator/i18n/pl.ts +146 -150
  101. package/src/tool/qrGenerator/i18n/pt.ts +146 -150
  102. package/src/tool/qrGenerator/i18n/ru.ts +146 -150
  103. package/src/tool/qrGenerator/i18n/sv.ts +146 -150
  104. package/src/tool/qrGenerator/i18n/tr.ts +146 -150
  105. package/src/tool/qrGenerator/i18n/zh.ts +146 -150
  106. package/src/tool/qrGenerator/seo.astro +2 -1
  107. package/src/tool/solarCalculator/bibliography.ts +5 -0
  108. package/src/tool/solarCalculator/i18n/de.ts +141 -145
  109. package/src/tool/solarCalculator/i18n/en.ts +7 -12
  110. package/src/tool/solarCalculator/i18n/es.ts +5 -10
  111. package/src/tool/solarCalculator/i18n/fr.ts +8 -13
  112. package/src/tool/solarCalculator/i18n/id.ts +4 -8
  113. package/src/tool/solarCalculator/i18n/it.ts +4 -8
  114. package/src/tool/solarCalculator/i18n/ja.ts +121 -125
  115. package/src/tool/solarCalculator/i18n/ko.ts +116 -120
  116. package/src/tool/solarCalculator/i18n/nl.ts +4 -7
  117. package/src/tool/solarCalculator/i18n/pl.ts +5 -9
  118. package/src/tool/solarCalculator/i18n/pt.ts +4 -8
  119. package/src/tool/solarCalculator/i18n/ru.ts +7 -10
  120. package/src/tool/solarCalculator/i18n/sv.ts +4 -7
  121. package/src/tool/solarCalculator/i18n/tr.ts +4 -7
  122. package/src/tool/solarCalculator/i18n/zh.ts +116 -120
  123. package/src/tool/solarCalculator/seo.astro +2 -1
  124. package/src/tool/tariffComparator/bibliography.ts +7 -0
  125. package/src/tool/tariffComparator/i18n/de.ts +129 -132
  126. package/src/tool/tariffComparator/i18n/en.ts +5 -12
  127. package/src/tool/tariffComparator/i18n/es.ts +5 -12
  128. package/src/tool/tariffComparator/i18n/fr.ts +8 -15
  129. package/src/tool/tariffComparator/i18n/id.ts +2 -5
  130. package/src/tool/tariffComparator/i18n/it.ts +2 -5
  131. package/src/tool/tariffComparator/i18n/ja.ts +129 -132
  132. package/src/tool/tariffComparator/i18n/ko.ts +129 -132
  133. package/src/tool/tariffComparator/i18n/nl.ts +2 -5
  134. package/src/tool/tariffComparator/i18n/pl.ts +3 -6
  135. package/src/tool/tariffComparator/i18n/pt.ts +2 -5
  136. package/src/tool/tariffComparator/i18n/ru.ts +2 -5
  137. package/src/tool/tariffComparator/i18n/sv.ts +2 -5
  138. package/src/tool/tariffComparator/i18n/tr.ts +2 -5
  139. package/src/tool/tariffComparator/i18n/zh.ts +129 -132
  140. package/src/tool/tariffComparator/seo.astro +2 -1
  141. package/src/tool/wifiRangeSimulator/bibliography.astro +14 -0
  142. package/src/tool/wifiRangeSimulator/bibliography.ts +14 -0
  143. package/src/tool/wifiRangeSimulator/component.astro +170 -0
  144. package/src/tool/wifiRangeSimulator/entry.ts +29 -0
  145. package/src/tool/wifiRangeSimulator/i18n/de.ts +477 -0
  146. package/src/tool/wifiRangeSimulator/i18n/en.ts +477 -0
  147. package/src/tool/wifiRangeSimulator/i18n/es.ts +477 -0
  148. package/src/tool/wifiRangeSimulator/i18n/fr.ts +477 -0
  149. package/src/tool/wifiRangeSimulator/i18n/id.ts +477 -0
  150. package/src/tool/wifiRangeSimulator/i18n/it.ts +477 -0
  151. package/src/tool/wifiRangeSimulator/i18n/ja.ts +477 -0
  152. package/src/tool/wifiRangeSimulator/i18n/ko.ts +477 -0
  153. package/src/tool/wifiRangeSimulator/i18n/nl.ts +477 -0
  154. package/src/tool/wifiRangeSimulator/i18n/pl.ts +477 -0
  155. package/src/tool/wifiRangeSimulator/i18n/pt.ts +477 -0
  156. package/src/tool/wifiRangeSimulator/i18n/ru.ts +477 -0
  157. package/src/tool/wifiRangeSimulator/i18n/sv.ts +477 -0
  158. package/src/tool/wifiRangeSimulator/i18n/tr.ts +477 -0
  159. package/src/tool/wifiRangeSimulator/i18n/zh.ts +477 -0
  160. package/src/tool/wifiRangeSimulator/i18n-utils.ts +14 -0
  161. package/src/tool/wifiRangeSimulator/index.ts +8 -0
  162. package/src/tool/wifiRangeSimulator/logic.ts +220 -0
  163. package/src/tool/wifiRangeSimulator/seo.astro +15 -0
  164. package/src/tool/wifiRangeSimulator/sketch-actions.ts +168 -0
  165. package/src/tool/wifiRangeSimulator/sketch-events.ts +138 -0
  166. package/src/tool/wifiRangeSimulator/sketch-render-dash.ts +170 -0
  167. package/src/tool/wifiRangeSimulator/sketch-render-device.ts +42 -0
  168. package/src/tool/wifiRangeSimulator/sketch-render.ts +155 -0
  169. package/src/tool/wifiRangeSimulator/sketch-state.ts +186 -0
  170. package/src/tool/wifiRangeSimulator/sketch.ts +100 -0
  171. package/src/tool/wifiRangeSimulator/ui.ts +69 -0
  172. package/src/tool/wifiRangeSimulator/wifi-range-simulator.css +583 -0
  173. package/src/tools.ts +2 -0
  174. package/src/types.ts +0 -2
@@ -0,0 +1,170 @@
1
+ import type { Point, Segment, PlacedObject, PlacedDevice, SignalResult } from './logic';
2
+ import { calculateSignalFromSketch, getVerdictColor, computeStreamingVerdict } from './logic';
3
+ import { renderRouter, renderRays } from './sketch-render';
4
+ import { getUI } from './i18n-utils';
5
+
6
+ const CIRCUMFERENCE = 2 * Math.PI * 70;
7
+
8
+ function setText(id: string, val: string) {
9
+ const el = document.getElementById(id);
10
+ if (el) el.textContent = val;
11
+ }
12
+
13
+ function setAttr(id: string, attr: string, val: string) {
14
+ const el = document.getElementById(id);
15
+ if (el) el.setAttribute(attr, val);
16
+ }
17
+
18
+ function setStyle(id: string, prop: string, val: string) {
19
+ const el = document.getElementById(id);
20
+ if (el) (el as HTMLElement).style.setProperty(prop, val);
21
+ }
22
+
23
+ function updateRing(percent: number, color: string) {
24
+ const offset = CIRCUMFERENCE - (CIRCUMFERENCE * percent) / 100;
25
+ setAttr('sketch-ring-fill', 'stroke-dashoffset', String(offset));
26
+ setAttr('sketch-ring-fill', 'stroke', color);
27
+ setStyle('sketch-ring-pct', 'color', color);
28
+ setText('sketch-ring-pct', `${percent}%`);
29
+ }
30
+
31
+ function getStatusCol(status: string, pct: number): string {
32
+ if (pct >= 60) return '#22c55e';
33
+ if (pct >= 30) return '#f59e0b';
34
+ return '#ef4444';
35
+ }
36
+
37
+ function updateStreaming(result: ReturnType<typeof calculateSignalFromSketch>) {
38
+ const keys = ['4kStreaming', 'onlineGaming', 'videoCalls', 'basicBrowsing'];
39
+ const thresholds: Record<string, number> = { '4kStreaming': result.strengthPercent, onlineGaming: result.strengthPercent, videoCalls: result.strengthPercent, basicBrowsing: result.strengthPercent };
40
+ keys.forEach((k) => {
41
+ const el = document.getElementById(`sketch-badge-${k}`);
42
+ if (!el) return;
43
+ el.textContent = result.streamingVerdict[k];
44
+ const col = getStatusCol(result.streamingVerdict[k], thresholds[k]);
45
+ (el as HTMLElement).style.color = col;
46
+ (el as HTMLElement).style.background = col + '14';
47
+ });
48
+ }
49
+
50
+ function buildMiniBar(percent: number, color: string): string {
51
+ const w = Math.max(4, Math.round((percent / 100) * 60));
52
+ return `<svg width="64" height="6" style="flex-shrink:0"><rect width="64" height="6" rx="3" fill="var(--bg-muted)"/><rect width="${w}" height="6" rx="3" fill="${color}"/></svg>`;
53
+ }
54
+
55
+ function buildDeviceCard(dev: PlacedDevice, result: ReturnType<typeof calculateSignalFromSketch>): string {
56
+ const color = getVerdictColor(result.verdict);
57
+ const ui = getUI();
58
+ return `
59
+ <div class="sketch-device-card">
60
+ <div class="sketch-device-header">
61
+ <span class="sketch-device-dot" style="background:${color}"></span>
62
+ <span class="sketch-device-name">${dev.name}</span>
63
+ <span class="sketch-device-pct" style="color:${color}">${result.strengthPercent}%</span>
64
+ </div>
65
+ <div class="sketch-device-bar-row">
66
+ ${buildMiniBar(result.strengthPercent, color)}
67
+ <span class="sketch-device-range">${result.effectiveRange}${ui.labelMeters}</span>
68
+ </div>
69
+ </div>
70
+ `;
71
+ }
72
+
73
+ function renderDevicePanel(devices: PlacedDevice[], router: Point, walls: Segment[], objects: PlacedObject[]) {
74
+ const panel = document.getElementById('sketch-device-panel');
75
+ if (!panel) return;
76
+ const ui = getUI();
77
+ panel.innerHTML = `<div class="sketch-device-section-title">${ui.labelPerDevice}</div>`;
78
+ devices.forEach((dev) => {
79
+ const result = calculateSignalFromSketch(router, dev, walls, objects);
80
+ const div = document.createElement('div');
81
+ div.innerHTML = buildDeviceCard(dev, result);
82
+ panel.appendChild(div.firstElementChild as HTMLElement);
83
+ });
84
+ }
85
+
86
+ function buildTips(walls: Segment[], objects: PlacedObject[], result: ReturnType<typeof calculateSignalFromSketch>): string[] {
87
+ const ui = getUI();
88
+ const tips: string[] = [];
89
+ const concreteCount = walls.filter((w) => w.material === 'concrete').length;
90
+ const metalCount = walls.filter((w) => w.material === 'metalDoor').length;
91
+ const total = walls.length + objects.length;
92
+
93
+ if (concreteCount > 0 || metalCount > 0) {
94
+ tips.push(ui.tipMoveRouter);
95
+ }
96
+ if (total >= 3) {
97
+ tips.push(ui.tipReduceObstacles);
98
+ }
99
+ if (result.verdict === 'dead' || result.verdict === 'poor') {
100
+ tips.push(ui.tipElevateRouter);
101
+ }
102
+ if (objects.some((o) => o.material === 'aquarium')) {
103
+ tips.push(ui.tipFishTank);
104
+ }
105
+ if (objects.some((o) => o.material === 'microwave')) {
106
+ tips.push(ui.tipMicrowave);
107
+ }
108
+ return tips;
109
+ }
110
+
111
+ function renderTips(tips: string[]) {
112
+ const area = document.getElementById('sketch-tip-area');
113
+ if (!area) return;
114
+ area.innerHTML = '';
115
+ tips.forEach((text) => {
116
+ const card = document.createElement('div');
117
+ card.className = 'sketch-tip-card';
118
+ card.innerHTML = `<div class="sketch-tip-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></div><div class="sketch-tip-text">${text}</div>`;
119
+ area.appendChild(card);
120
+ });
121
+ }
122
+
123
+ function averageResults(results: ReturnType<typeof calculateSignalFromSketch>[]): SignalResult {
124
+ const ui = getUI();
125
+ const avgPct = Math.round(results.reduce((s, r) => s + r.strengthPercent, 0) / results.length);
126
+ const avgRange = Math.round(results.reduce((s, r) => s + r.effectiveRange, 0) / results.length);
127
+ let verdict: SignalResult['verdict'];
128
+ if (avgPct >= 80) verdict = 'perfect';
129
+ else if (avgPct >= 60) verdict = 'good';
130
+ else if (avgPct >= 40) verdict = 'fair';
131
+ else if (avgPct >= 20) verdict = 'poor';
132
+ else verdict = 'dead';
133
+ return {
134
+ strengthPercent: avgPct,
135
+ effectiveRange: avgRange,
136
+ verdict,
137
+ streamingVerdict: computeStreamingVerdict(avgPct, ui),
138
+ rayCount: 1,
139
+ };
140
+ }
141
+
142
+ export function updateDashboard(
143
+ router: Point,
144
+ devices: PlacedDevice[],
145
+ walls: Segment[],
146
+ objects: PlacedObject[]
147
+ ) {
148
+ if (devices.length === 0) return;
149
+
150
+ const ui = getUI();
151
+ const allResults = devices.map((dev) => calculateSignalFromSketch(router, dev, walls, objects));
152
+ const avgResult = averageResults(allResults);
153
+ const color = getVerdictColor(avgResult.verdict);
154
+
155
+ updateRing(avgResult.strengthPercent, color);
156
+ setText('sketch-range-val', `${avgResult.effectiveRange} ${ui.labelMeters}`);
157
+ updateStreaming(avgResult);
158
+ renderTips(buildTips(walls, objects, avgResult));
159
+ renderDevicePanel(devices, router, walls, objects);
160
+ renderRays(router, devices, walls, objects);
161
+ renderRouter(router, color);
162
+
163
+ const ring = document.getElementById('sketch-ring');
164
+ if (ring) {
165
+ ring.classList.remove('sketch-peak-anim', 'sketch-danger-anim');
166
+ void ring.offsetWidth;
167
+ if (avgResult.verdict === 'perfect') ring.classList.add('sketch-peak-anim');
168
+ if (avgResult.verdict === 'dead') ring.classList.add('sketch-danger-anim');
169
+ }
170
+ }
@@ -0,0 +1,42 @@
1
+ import type { PlacedDevice } from './logic';
2
+
3
+ function setAttrs(el: Element, attrs: Record<string, string>) {
4
+ Object.entries(attrs).forEach(([k, v]) => el.setAttribute(k, v));
5
+ }
6
+
7
+ function makeGlow(color: string) {
8
+ const el = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
9
+ setAttrs(el, { cx: '0', cy: '0', r: '24', fill: 'none', stroke: color, 'stroke-width': '2', opacity: '0.3' });
10
+ return el;
11
+ }
12
+
13
+ function makeRect() {
14
+ const el = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
15
+ setAttrs(el, { x: '-14', y: '-14', width: '28', height: '28', rx: '6', fill: 'var(--bg-surface)', stroke: 'var(--border-color)', 'stroke-width': '1' });
16
+ return el;
17
+ }
18
+
19
+ function makeIcon() {
20
+ const el = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
21
+ setAttrs(el, { x: '-8', y: '-8', width: '16', height: '16', viewBox: '0 0 24 24', fill: 'none', stroke: 'var(--text-main)', 'stroke-width': '2', 'stroke-linecap': 'round', 'stroke-linejoin': 'round' });
22
+ el.innerHTML = '<rect x="2" y="4" width="20" height="14" rx="2"/><line x1="8" y1="22" x2="16" y2="22"/><line x1="12" y1="18" x2="12" y2="22"/>';
23
+ return el;
24
+ }
25
+
26
+ function makeLabel(name: string) {
27
+ const el = document.createElementNS('http://www.w3.org/2000/svg', 'text');
28
+ setAttrs(el, { x: '0', y: '26', 'text-anchor': 'middle', fill: 'var(--text-muted)', 'font-size': '8', 'font-weight': '700' });
29
+ el.textContent = name;
30
+ return el;
31
+ }
32
+
33
+ export function buildDeviceSVG(dev: PlacedDevice, color: string) {
34
+ const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
35
+ setAttrs(g, { id: `sketch-device-${dev.id}`, transform: `translate(${dev.x}, ${dev.y})`, style: 'cursor:grab' });
36
+ g.dataset.deviceId = dev.id;
37
+ g.appendChild(makeGlow(color));
38
+ g.appendChild(makeRect());
39
+ g.appendChild(makeIcon());
40
+ g.appendChild(makeLabel(dev.name));
41
+ return g;
42
+ }
@@ -0,0 +1,155 @@
1
+ import type { Point, Segment, PlacedObject, PlacedDevice } from './logic';
2
+ import { calculateSignalFromSketch, getVerdictColor, MATERIALS } from './logic';
3
+ import { buildDeviceSVG } from './sketch-render-device';
4
+ import { getUI } from './i18n-utils';
5
+
6
+ export function renderWalls(walls: Segment[]) {
7
+ const layer = document.getElementById('sketch-walls-layer');
8
+ if (!layer) return;
9
+ layer.innerHTML = '';
10
+ walls.forEach((wall) => {
11
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
12
+ line.setAttribute('x1', String(wall.a.x));
13
+ line.setAttribute('y1', String(wall.a.y));
14
+ line.setAttribute('x2', String(wall.b.x));
15
+ line.setAttribute('y2', String(wall.b.y));
16
+ line.setAttribute('stroke', MATERIALS[wall.material].color);
17
+ line.setAttribute('stroke-width', '6');
18
+ line.setAttribute('stroke-linecap', 'round');
19
+ line.setAttribute('class', 'sketch-wall-line');
20
+ layer.appendChild(line);
21
+ });
22
+ }
23
+
24
+ function buildObjRect(g: SVGGElement, obj: PlacedObject) {
25
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
26
+ rect.setAttribute('x', String(-obj.width / 2));
27
+ rect.setAttribute('y', String(-obj.height / 2));
28
+ rect.setAttribute('width', String(obj.width));
29
+ rect.setAttribute('height', String(obj.height));
30
+ rect.setAttribute('rx', '4');
31
+ rect.setAttribute('fill', MATERIALS[obj.material].color);
32
+ rect.setAttribute('stroke', 'rgba(0,0,0,0.15)');
33
+ rect.setAttribute('stroke-width', '1');
34
+ g.appendChild(rect);
35
+ }
36
+
37
+ function buildObjLabel(g: SVGGElement, obj: PlacedObject) {
38
+ const ui = getUI();
39
+ const txt = document.createElementNS('http://www.w3.org/2000/svg', 'text');
40
+ txt.setAttribute('y', '4');
41
+ txt.setAttribute('text-anchor', 'middle');
42
+ txt.setAttribute('fill', '#fff');
43
+ txt.setAttribute('font-size', '7');
44
+ txt.setAttribute('font-weight', '700');
45
+ txt.setAttribute('style', 'text-shadow:0 1px 2px rgba(0,0,0,0.4);pointer-events:none');
46
+ txt.textContent = `${obj.attenuation}${ui.labelDb}`;
47
+ g.appendChild(txt);
48
+ }
49
+
50
+ export function renderObjects(objects: PlacedObject[]) {
51
+ const layer = document.getElementById('sketch-objs-layer');
52
+ if (!layer) return;
53
+ const existing = layer.querySelectorAll('[data-obj-id]');
54
+ existing.forEach((el) => el.remove());
55
+ objects.forEach((obj) => {
56
+ const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
57
+ g.setAttribute('transform', `translate(${obj.x}, ${obj.y})`);
58
+ g.setAttribute('class', 'sketch-obj-group');
59
+ g.dataset.objId = obj.id;
60
+ buildObjRect(g, obj);
61
+ buildObjLabel(g, obj);
62
+ layer.appendChild(g);
63
+ });
64
+ }
65
+
66
+ export function renderRouter(router: Point, color?: string) {
67
+ const g = document.getElementById('sketch-router-group');
68
+ if (g) g.setAttribute('transform', `translate(${router.x}, ${router.y})`);
69
+ if (color) {
70
+ const glow = document.getElementById('sketch-router-glow');
71
+ if (glow) glow.setAttribute('stroke', color);
72
+ }
73
+ }
74
+
75
+ export function renderDevices(devices: PlacedDevice[], router: Point, walls: Segment[], objects: PlacedObject[]) {
76
+ const layer = document.getElementById('sketch-objs-layer');
77
+ if (!layer) return;
78
+ const existing = layer.querySelectorAll('[data-device-id]');
79
+ existing.forEach((el) => el.remove());
80
+
81
+ devices.forEach((dev) => {
82
+ const result = calculateSignalFromSketch(router, dev, walls, objects);
83
+ const color = getVerdictColor(result.verdict);
84
+ const svgEl = buildDeviceSVG(dev, color);
85
+ layer.appendChild(svgEl);
86
+ });
87
+ }
88
+
89
+ export function renderRays(router: Point, devices: PlacedDevice[], walls: Segment[], objects: PlacedObject[]) {
90
+ const layer = document.getElementById('sketch-rays-layer');
91
+ if (!layer) return;
92
+ layer.innerHTML = '';
93
+
94
+ devices.forEach((dev) => {
95
+ const result = calculateSignalFromSketch(router, dev, walls, objects);
96
+ const color = getVerdictColor(result.verdict);
97
+ const dx = dev.x - router.x;
98
+ const dy = dev.y - router.y;
99
+ const len = Math.sqrt(dx * dx + dy * dy);
100
+ const angle = Math.atan2(dy, dx);
101
+ const spread = 0.06;
102
+
103
+ [ -1, 0, 1 ].forEach((offset) => {
104
+ const a = angle + offset * spread;
105
+ const ex = router.x + Math.cos(a) * len;
106
+ const ey = router.y + Math.sin(a) * len;
107
+ const d = `M ${router.x} ${router.y} L ${ex} ${ey}`;
108
+
109
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
110
+ path.setAttribute('d', d);
111
+ path.setAttribute('fill', 'none');
112
+ path.setAttribute('stroke', color);
113
+ path.setAttribute('stroke-width', '1.5');
114
+ path.setAttribute('stroke-linecap', 'round');
115
+ path.setAttribute('opacity', String(0.25 + Math.abs(offset) * 0.15));
116
+ layer.appendChild(path);
117
+ });
118
+ });
119
+ }
120
+
121
+ export function spawnParticle(x: number, y: number, text: string) {
122
+ const wrap = document.getElementById('sketch-canvas-wrap');
123
+ if (!wrap) return;
124
+ const p = document.createElement('span');
125
+ p.className = 'sketch-particle';
126
+ p.textContent = text;
127
+ const rect = wrap.getBoundingClientRect();
128
+ p.style.left = `${x - rect.left}px`;
129
+ p.style.top = `${y - rect.top}px`;
130
+ wrap.appendChild(p);
131
+ setTimeout(() => p.remove(), 900);
132
+ }
133
+
134
+ export function hitObject(obj: PlacedObject, p: Point): boolean {
135
+ const hw = obj.width / 2;
136
+ const hh = obj.height / 2;
137
+ return p.x >= obj.x - hw && p.x <= obj.x + hw && p.y >= obj.y - hh && p.y <= obj.y + hh;
138
+ }
139
+
140
+ export function hitWall(wall: Segment, p: Point): boolean {
141
+ const d = dist(wall.a, wall.b);
142
+ const d1 = dist(p, wall.a);
143
+ const d2 = dist(p, wall.b);
144
+ return Math.abs(d - (d1 + d2)) < 8;
145
+ }
146
+
147
+ export function hitDevice(dev: PlacedDevice, p: Point): boolean {
148
+ return dist(p, dev) < 35;
149
+ }
150
+
151
+ function dist(a: Point, b: Point): number {
152
+ const dx = b.x - a.x;
153
+ const dy = b.y - a.y;
154
+ return Math.sqrt(dx * dx + dy * dy);
155
+ }
@@ -0,0 +1,186 @@
1
+ import type { Point, Segment, PlacedDevice, PlacedObject } from './logic';
2
+ import { getUI } from './i18n-utils';
3
+
4
+ export interface State {
5
+ tool: string;
6
+ drawing: boolean;
7
+ drawStart: Point;
8
+ router: Point;
9
+ devices: PlacedDevice[];
10
+ selectedDeviceId: string | null;
11
+ walls: Segment[];
12
+ objects: PlacedObject[];
13
+ dragging: string | null;
14
+ dragOffset: Point;
15
+ nextId: number;
16
+ zoom: number;
17
+ panX: number;
18
+ panY: number;
19
+ }
20
+
21
+ export interface Snapshot {
22
+ router: Point;
23
+ devices: PlacedDevice[];
24
+ walls: Segment[];
25
+ objects: PlacedObject[];
26
+ nextId: number;
27
+ zoom: number;
28
+ panX: number;
29
+ panY: number;
30
+ }
31
+
32
+ export interface History {
33
+ stack: Snapshot[];
34
+ pos: number;
35
+ }
36
+
37
+ export function defaultState(): State {
38
+ const ui = getUI();
39
+ return {
40
+ tool: 'select',
41
+ drawing: false,
42
+ drawStart: { x: 0, y: 0 },
43
+ router: { x: 150, y: 250 },
44
+ devices: [{ id: 'dev-1', x: 400, y: 250, name: `${ui.labelDevicePrefix} 1` }],
45
+ selectedDeviceId: null,
46
+ walls: [],
47
+ objects: [],
48
+ dragging: null,
49
+ dragOffset: { x: 0, y: 0 },
50
+ nextId: 2,
51
+ zoom: 1.5,
52
+ panX: 0,
53
+ panY: 0,
54
+ };
55
+ }
56
+
57
+ export function saveState(s: State): void {
58
+ const payload: Snapshot = {
59
+ router: { ...s.router },
60
+ devices: s.devices.map((d) => ({ ...d })),
61
+ walls: s.walls.map((w) => ({ ...w, a: { ...w.a }, b: { ...w.b } })),
62
+ objects: s.objects.map((o) => ({ ...o })),
63
+ nextId: s.nextId,
64
+ zoom: s.zoom,
65
+ panX: s.panX,
66
+ panY: s.panY,
67
+ };
68
+ localStorage.setItem('wifi-sketch-v1', JSON.stringify(payload));
69
+ }
70
+
71
+ function dedupeDevices(devices: PlacedDevice[]): PlacedDevice[] {
72
+ const seen = new Set<string>();
73
+ let next = 1;
74
+ return devices.map((d) => {
75
+ if (!seen.has(d.id)) {
76
+ seen.add(d.id);
77
+ return d;
78
+ }
79
+ while (seen.has(`dev-${next}`)) next++;
80
+ const newId = `dev-${next}`;
81
+ seen.add(newId);
82
+ return { ...d, id: newId };
83
+ });
84
+ }
85
+
86
+ export function loadState(): State | null {
87
+ const raw = localStorage.getItem('wifi-sketch-v1');
88
+ if (!raw) return null;
89
+ try {
90
+ const snap = JSON.parse(raw) as Snapshot;
91
+ const devices = dedupeDevices(snap.devices);
92
+ return {
93
+ tool: 'select',
94
+ drawing: false,
95
+ drawStart: { x: 0, y: 0 },
96
+ router: snap.router,
97
+ devices,
98
+ selectedDeviceId: null,
99
+ walls: snap.walls,
100
+ objects: snap.objects,
101
+ dragging: null,
102
+ dragOffset: { x: 0, y: 0 },
103
+ nextId: snap.nextId,
104
+ zoom: typeof snap.zoom === 'number' ? snap.zoom : 1,
105
+ panX: typeof snap.panX === 'number' ? snap.panX : 0,
106
+ panY: typeof snap.panY === 'number' ? snap.panY : 0,
107
+ };
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+
113
+ export function ptToSvg(svg: SVGSVGElement, clientX: number, clientY: number, zoom: number): Point {
114
+ const rect = svg.getBoundingClientRect();
115
+ return {
116
+ x: (clientX - rect.left) / zoom,
117
+ y: (clientY - rect.top) / zoom,
118
+ };
119
+ }
120
+
121
+ export function isWallTool(tool: string): boolean {
122
+ return ['drywall', 'brick', 'concrete', 'stoneWall'].includes(tool);
123
+ }
124
+
125
+ export function dist(a: Point, b: Point): number {
126
+ const dx = b.x - a.x;
127
+ const dy = b.y - a.y;
128
+ return Math.sqrt(dx * dx + dy * dy);
129
+ }
130
+
131
+ export function hitRouter(s: State, p: Point): boolean {
132
+ return dist(p, s.router) < 22;
133
+ }
134
+
135
+ export function nearestDevice(devices: PlacedDevice[], p: Point): PlacedDevice | null {
136
+ let nearest: PlacedDevice | null = null;
137
+ let nearestDist = Infinity;
138
+ for (const dev of devices) {
139
+ const d = dist(p, dev);
140
+ if (d < nearestDist) {
141
+ nearestDist = d;
142
+ nearest = dev;
143
+ }
144
+ }
145
+ if (nearestDist > 50) return null;
146
+ return nearest;
147
+ }
148
+
149
+ export function renderDeviceSelection(s: State) {
150
+ document.querySelectorAll('.sketch-device-selected').forEach((el) => {
151
+ el.remove();
152
+ });
153
+ if (!s.selectedDeviceId) return;
154
+ const dev = s.devices.find((d) => d.id === s.selectedDeviceId);
155
+ if (!dev) return;
156
+ const layer = document.getElementById('sketch-objs-layer');
157
+ if (!layer) return;
158
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
159
+ circle.setAttribute('cx', String(dev.x));
160
+ circle.setAttribute('cy', String(dev.y));
161
+ circle.setAttribute('r', '30');
162
+ circle.setAttribute('fill', 'none');
163
+ circle.setAttribute('stroke', '#0ea5e9');
164
+ circle.setAttribute('stroke-width', '3');
165
+ circle.setAttribute('stroke-dasharray', '4 2');
166
+ circle.setAttribute('opacity', '0.8');
167
+ circle.setAttribute('class', 'sketch-device-selected');
168
+ circle.setAttribute('pointer-events', 'none');
169
+ layer.appendChild(circle);
170
+ }
171
+
172
+ export function removePreviewWall() {
173
+ const preview = document.getElementById('sketch-draw-line');
174
+ if (preview) preview.setAttribute('opacity', '0');
175
+ }
176
+
177
+ export function updatePreviewWall(start: Point, end: Point, material: string) {
178
+ const preview = document.getElementById('sketch-draw-line');
179
+ if (!preview) return;
180
+ preview.setAttribute('x1', String(start.x));
181
+ preview.setAttribute('y1', String(start.y));
182
+ preview.setAttribute('x2', String(end.x));
183
+ preview.setAttribute('y2', String(end.y));
184
+ preview.setAttribute('stroke', material);
185
+ preview.setAttribute('opacity', '0.6');
186
+ }
@@ -0,0 +1,100 @@
1
+ import type { State, Snapshot } from './sketch-state';
2
+ import {
3
+ defaultState,
4
+ loadState,
5
+ ptToSvg,
6
+ renderDeviceSelection,
7
+ removePreviewWall,
8
+ } from './sketch-state';
9
+ import { renderWalls, renderObjects, renderRouter, renderDevices, hitObject, hitWall } from './sketch-render';
10
+ import { updateDashboard } from './sketch-render-dash';
11
+ import { initToolButtons, addToHistory, applySnapshot, renderZoom, saveState } from './sketch-actions';
12
+ import { handleMouseDown, handleMouseMove, handleMouseUp } from './sketch-events';
13
+
14
+ function initRender(s: State) {
15
+ renderWalls(s.walls);
16
+ renderObjects(s.objects);
17
+ renderRouter(s.router);
18
+ renderDevices(s.devices, s.router, s.walls, s.objects);
19
+ updateDashboard(s.router, s.devices, s.walls, s.objects);
20
+ renderDeviceSelection(s);
21
+ renderZoom(s);
22
+ }
23
+
24
+ function removeObj(s: State, h: { stack: Snapshot[]; pos: number }, p: Point) {
25
+ const objIdx = s.objects.findIndex((o) => hitObject(o, p));
26
+ if (objIdx < 0) return false;
27
+ s.objects.splice(objIdx, 1);
28
+ saveState(s);
29
+ addToHistory(h, s);
30
+ renderObjects(s.objects);
31
+ updateDashboard(s.router, s.devices, s.walls, s.objects);
32
+ return true;
33
+ }
34
+
35
+ function removeWall(s: State, h: { stack: Snapshot[]; pos: number }, p: Point) {
36
+ const wallIdx = s.walls.findIndex((w) => hitWall(w, p));
37
+ if (wallIdx < 0) return false;
38
+ s.walls.splice(wallIdx, 1);
39
+ saveState(s);
40
+ addToHistory(h, s);
41
+ renderWalls(s.walls);
42
+ updateDashboard(s.router, s.devices, s.walls, s.objects);
43
+ return true;
44
+ }
45
+
46
+ function bindCanvas(s: State, svg: SVGSVGElement, h: { stack: Snapshot[]; pos: number }, wrap: HTMLElement) {
47
+ wrap.addEventListener('mousedown', (e) => {
48
+ handleMouseDown(s, svg, e);
49
+ });
50
+ wrap.addEventListener('mousemove', (e) => {
51
+ handleMouseMove(s, svg, e);
52
+ });
53
+ wrap.addEventListener('mouseup', (e) => {
54
+ handleMouseUp(s, svg, h, e);
55
+ });
56
+ wrap.addEventListener('mouseleave', () => {
57
+ if (s.dragging || s.drawing) {
58
+ removePreviewWall();
59
+ s.dragging = null;
60
+ s.drawing = false;
61
+ saveState(s);
62
+ }
63
+ });
64
+ wrap.addEventListener('dblclick', (e) => {
65
+ const p = ptToSvg(svg, e.clientX, e.clientY, s.zoom);
66
+ if (removeObj(s, h, p)) return;
67
+ removeWall(s, h, p);
68
+ });
69
+ }
70
+
71
+ function bindKeyboard(s: State, h: { stack: Snapshot[]; pos: number }) {
72
+ document.addEventListener('keydown', (e) => {
73
+ if (e.key === 'z' && (e.ctrlKey || e.metaKey)) {
74
+ e.preventDefault();
75
+ if (h.pos <= 0) return;
76
+ h.pos--;
77
+ applySnapshot(s, h.stack[h.pos]);
78
+ initRender(s);
79
+ saveState(s);
80
+ }
81
+ });
82
+ }
83
+
84
+ export function initSketch() {
85
+ const wrap = document.getElementById('sketch-canvas-wrap');
86
+ if (!wrap) return;
87
+ const svg = wrap.querySelector('svg') as SVGSVGElement;
88
+ if (!svg) return;
89
+
90
+ const state: State = loadState() || defaultState();
91
+ const history = { stack: [] as Snapshot[], pos: -1 };
92
+ addToHistory(history, state);
93
+
94
+ initToolButtons(state, history);
95
+ initRender(state);
96
+ bindCanvas(state, svg, history, wrap);
97
+ bindKeyboard(state, history);
98
+ }
99
+
100
+ export type { State };