@jjlmoya/utils-home 1.17.0 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/category/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 +7 -1
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/no_en_dash.test.ts +70 -0
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/acTonnageCalculator/ac-tonnage-calculator.css +467 -0
- package/src/tool/acTonnageCalculator/bibliography.astro +14 -0
- package/src/tool/acTonnageCalculator/bibliography.ts +5 -0
- package/src/tool/acTonnageCalculator/client-animations.ts +80 -0
- package/src/tool/acTonnageCalculator/client.ts +171 -0
- package/src/tool/acTonnageCalculator/component.astro +186 -0
- package/src/tool/acTonnageCalculator/entry.ts +29 -0
- package/src/tool/acTonnageCalculator/i18n/de.ts +63 -0
- package/src/tool/acTonnageCalculator/i18n/en.ts +136 -0
- package/src/tool/acTonnageCalculator/i18n/es.ts +136 -0
- package/src/tool/acTonnageCalculator/i18n/fr.ts +61 -0
- package/src/tool/acTonnageCalculator/i18n/id.ts +61 -0
- package/src/tool/acTonnageCalculator/i18n/it.ts +61 -0
- package/src/tool/acTonnageCalculator/i18n/ja.ts +61 -0
- package/src/tool/acTonnageCalculator/i18n/ko.ts +61 -0
- package/src/tool/acTonnageCalculator/i18n/nl.ts +61 -0
- package/src/tool/acTonnageCalculator/i18n/pl.ts +61 -0
- package/src/tool/acTonnageCalculator/i18n/pt.ts +61 -0
- package/src/tool/acTonnageCalculator/i18n/ru.ts +61 -0
- package/src/tool/acTonnageCalculator/i18n/sv.ts +61 -0
- package/src/tool/acTonnageCalculator/i18n/tr.ts +61 -0
- package/src/tool/acTonnageCalculator/i18n/zh.ts +61 -0
- package/src/tool/acTonnageCalculator/index.ts +8 -0
- package/src/tool/acTonnageCalculator/logic.ts +56 -0
- package/src/tool/acTonnageCalculator/seo.astro +15 -0
- package/src/tool/acTonnageCalculator/ui.ts +39 -0
- package/src/tool/dewPointCalculator/bibliography.ts +2 -2
- package/src/tool/dewPointCalculator/i18n/de.ts +5 -5
- package/src/tool/dewPointCalculator/i18n/en.ts +6 -6
- package/src/tool/dewPointCalculator/i18n/es.ts +5 -5
- package/src/tool/dewPointCalculator/i18n/fr.ts +6 -6
- package/src/tool/dewPointCalculator/i18n/id.ts +5 -5
- package/src/tool/dewPointCalculator/i18n/it.ts +5 -5
- package/src/tool/dewPointCalculator/i18n/ja.ts +4 -4
- package/src/tool/dewPointCalculator/i18n/ko.ts +4 -4
- package/src/tool/dewPointCalculator/i18n/nl.ts +5 -5
- package/src/tool/dewPointCalculator/i18n/pl.ts +5 -5
- package/src/tool/dewPointCalculator/i18n/pt.ts +5 -5
- package/src/tool/dewPointCalculator/i18n/ru.ts +11 -11
- package/src/tool/dewPointCalculator/i18n/sv.ts +5 -5
- package/src/tool/dewPointCalculator/i18n/tr.ts +4 -4
- package/src/tool/dewPointCalculator/i18n/zh.ts +5 -5
- package/src/tool/heatingComparator/i18n/de.ts +8 -8
- package/src/tool/heatingComparator/i18n/en.ts +1 -1
- package/src/tool/heatingComparator/i18n/es.ts +1 -1
- package/src/tool/heatingComparator/i18n/fr.ts +7 -7
- package/src/tool/heatingComparator/i18n/id.ts +1 -1
- package/src/tool/heatingComparator/i18n/it.ts +1 -1
- package/src/tool/heatingComparator/i18n/nl.ts +1 -1
- package/src/tool/heatingComparator/i18n/pl.ts +1 -1
- package/src/tool/heatingComparator/i18n/pt.ts +1 -1
- package/src/tool/heatingComparator/i18n/ru.ts +12 -12
- package/src/tool/heatingComparator/i18n/sv.ts +4 -4
- package/src/tool/heatingComparator/i18n/zh.ts +6 -6
- package/src/tool/ledSavingCalculator/bibliography.ts +3 -3
- package/src/tool/ledSavingCalculator/i18n/de.ts +4 -4
- package/src/tool/ledSavingCalculator/i18n/en.ts +4 -4
- package/src/tool/ledSavingCalculator/i18n/es.ts +4 -4
- package/src/tool/ledSavingCalculator/i18n/fr.ts +8 -8
- package/src/tool/ledSavingCalculator/i18n/id.ts +3 -3
- package/src/tool/ledSavingCalculator/i18n/it.ts +4 -4
- package/src/tool/ledSavingCalculator/i18n/ja.ts +3 -3
- package/src/tool/ledSavingCalculator/i18n/ko.ts +2 -2
- package/src/tool/ledSavingCalculator/i18n/nl.ts +3 -3
- package/src/tool/ledSavingCalculator/i18n/pl.ts +3 -3
- package/src/tool/ledSavingCalculator/i18n/pt.ts +3 -3
- package/src/tool/ledSavingCalculator/i18n/ru.ts +6 -6
- package/src/tool/ledSavingCalculator/i18n/sv.ts +3 -3
- package/src/tool/ledSavingCalculator/i18n/tr.ts +3 -3
- package/src/tool/ledSavingCalculator/i18n/zh.ts +4 -4
- package/src/tool/projectorCalculator/bibliography.ts +3 -3
- package/src/tool/projectorCalculator/i18n/de.ts +2 -2
- package/src/tool/projectorCalculator/i18n/en.ts +1 -1
- package/src/tool/projectorCalculator/i18n/es.ts +2 -2
- package/src/tool/projectorCalculator/i18n/fr.ts +4 -4
- package/src/tool/projectorCalculator/i18n/id.ts +2 -2
- package/src/tool/projectorCalculator/i18n/it.ts +2 -2
- package/src/tool/projectorCalculator/i18n/ja.ts +2 -2
- package/src/tool/projectorCalculator/i18n/ko.ts +2 -2
- package/src/tool/projectorCalculator/i18n/nl.ts +2 -2
- package/src/tool/projectorCalculator/i18n/pl.ts +3 -3
- package/src/tool/projectorCalculator/i18n/pt.ts +2 -2
- package/src/tool/projectorCalculator/i18n/ru.ts +5 -5
- package/src/tool/projectorCalculator/i18n/sv.ts +2 -2
- package/src/tool/projectorCalculator/i18n/tr.ts +2 -2
- package/src/tool/projectorCalculator/i18n/zh.ts +4 -4
- package/src/tool/qrGenerator/bibliography.ts +1 -1
- package/src/tool/qrGenerator/i18n/en.ts +1 -1
- package/src/tool/qrGenerator/i18n/fr.ts +1 -1
- package/src/tool/solarCalculator/bibliography.ts +2 -2
- package/src/tool/solarCalculator/i18n/de.ts +2 -2
- package/src/tool/solarCalculator/i18n/en.ts +5 -5
- package/src/tool/solarCalculator/i18n/es.ts +3 -3
- package/src/tool/solarCalculator/i18n/fr.ts +6 -6
- package/src/tool/solarCalculator/i18n/id.ts +2 -2
- package/src/tool/solarCalculator/i18n/it.ts +2 -2
- package/src/tool/solarCalculator/i18n/ja.ts +2 -2
- package/src/tool/solarCalculator/i18n/ko.ts +2 -2
- package/src/tool/solarCalculator/i18n/nl.ts +2 -2
- package/src/tool/solarCalculator/i18n/pl.ts +3 -3
- package/src/tool/solarCalculator/i18n/pt.ts +2 -2
- package/src/tool/solarCalculator/i18n/ru.ts +5 -5
- package/src/tool/solarCalculator/i18n/sv.ts +2 -2
- package/src/tool/solarCalculator/i18n/tr.ts +2 -2
- package/src/tool/solarCalculator/i18n/zh.ts +3 -3
- package/src/tool/tariffComparator/bibliography.ts +1 -1
- package/src/tool/tariffComparator/i18n/en.ts +3 -3
- package/src/tool/tariffComparator/i18n/es.ts +3 -3
- package/src/tool/tariffComparator/i18n/fr.ts +6 -6
- package/src/tool/tariffComparator/i18n/pl.ts +1 -1
- package/src/tool/tariffComparator/i18n/zh.ts +1 -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 +4 -0
|
@@ -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 };
|