@jjlmoya/utils-home 1.30.0 → 1.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/entries.ts +7 -1
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/lightingCalculator/bibliography.astro +36 -0
- package/src/tool/lightingCalculator/bibliography.ts +10 -0
- package/src/tool/lightingCalculator/component.astro +308 -0
- package/src/tool/lightingCalculator/draw.ts +247 -0
- package/src/tool/lightingCalculator/entry.ts +29 -0
- package/src/tool/lightingCalculator/how-many-lights-per-room.css +493 -0
- package/src/tool/lightingCalculator/i18n/de.ts +213 -0
- package/src/tool/lightingCalculator/i18n/en.ts +213 -0
- package/src/tool/lightingCalculator/i18n/es.ts +213 -0
- package/src/tool/lightingCalculator/i18n/fr.ts +213 -0
- package/src/tool/lightingCalculator/i18n/id.ts +213 -0
- package/src/tool/lightingCalculator/i18n/it.ts +213 -0
- package/src/tool/lightingCalculator/i18n/ja.ts +213 -0
- package/src/tool/lightingCalculator/i18n/ko.ts +213 -0
- package/src/tool/lightingCalculator/i18n/nl.ts +213 -0
- package/src/tool/lightingCalculator/i18n/pl.ts +213 -0
- package/src/tool/lightingCalculator/i18n/pt.ts +213 -0
- package/src/tool/lightingCalculator/i18n/ru.ts +213 -0
- package/src/tool/lightingCalculator/i18n/sv.ts +213 -0
- package/src/tool/lightingCalculator/i18n/tr.ts +213 -0
- package/src/tool/lightingCalculator/i18n/zh.ts +213 -0
- package/src/tool/lightingCalculator/index.ts +9 -0
- package/src/tool/lightingCalculator/logic.ts +119 -0
- package/src/tool/lightingCalculator/seo.astro +15 -0
- package/src/tool/lightingCalculator/state.ts +113 -0
- package/src/tool/lightingCalculator/ui.ts +48 -0
- package/src/tool/tileLayoutCalculator/bibliography.astro +14 -0
- package/src/tool/tileLayoutCalculator/bibliography.ts +10 -0
- package/src/tool/tileLayoutCalculator/component.astro +415 -0
- package/src/tool/tileLayoutCalculator/entry.ts +29 -0
- package/src/tool/tileLayoutCalculator/i18n/de.ts +208 -0
- package/src/tool/tileLayoutCalculator/i18n/en.ts +208 -0
- package/src/tool/tileLayoutCalculator/i18n/es.ts +208 -0
- package/src/tool/tileLayoutCalculator/i18n/fr.ts +208 -0
- package/src/tool/tileLayoutCalculator/i18n/id.ts +208 -0
- package/src/tool/tileLayoutCalculator/i18n/it.ts +208 -0
- package/src/tool/tileLayoutCalculator/i18n/ja.ts +208 -0
- package/src/tool/tileLayoutCalculator/i18n/ko.ts +208 -0
- package/src/tool/tileLayoutCalculator/i18n/nl.ts +208 -0
- package/src/tool/tileLayoutCalculator/i18n/pl.ts +208 -0
- package/src/tool/tileLayoutCalculator/i18n/pt.ts +208 -0
- package/src/tool/tileLayoutCalculator/i18n/ru.ts +208 -0
- package/src/tool/tileLayoutCalculator/i18n/sv.ts +208 -0
- package/src/tool/tileLayoutCalculator/i18n/tr.ts +208 -0
- package/src/tool/tileLayoutCalculator/i18n/zh.ts +208 -0
- package/src/tool/tileLayoutCalculator/index.ts +9 -0
- package/src/tool/tileLayoutCalculator/logic.ts +55 -0
- package/src/tool/tileLayoutCalculator/seo.astro +15 -0
- package/src/tool/tileLayoutCalculator/tile-layout-calculator.css +404 -0
- package/src/tool/tileLayoutCalculator/ui.ts +37 -0
- package/src/tools.ts +4 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import type { LightingResult } from './logic';
|
|
2
|
+
|
|
3
|
+
interface PlanConfig {
|
|
4
|
+
widthM: number;
|
|
5
|
+
lengthM: number;
|
|
6
|
+
optimalBulbs: number;
|
|
7
|
+
luxRatio: number;
|
|
8
|
+
ambiance: number;
|
|
9
|
+
status: 'optimal' | 'insufficient' | 'excess';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface DialConfig {
|
|
13
|
+
value: number;
|
|
14
|
+
target: number;
|
|
15
|
+
max: number;
|
|
16
|
+
label: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function addRect(svg: SVGSVGElement, cfg: { x: number; y: number; w: number; h: number; fill: string; stroke: string; strokeWidth: number }) {
|
|
20
|
+
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
21
|
+
rect.setAttribute('x', String(cfg.x));
|
|
22
|
+
rect.setAttribute('y', String(cfg.y));
|
|
23
|
+
rect.setAttribute('width', String(cfg.w));
|
|
24
|
+
rect.setAttribute('height', String(cfg.h));
|
|
25
|
+
rect.setAttribute('rx', '16');
|
|
26
|
+
rect.setAttribute('fill', cfg.fill);
|
|
27
|
+
rect.setAttribute('stroke', cfg.stroke);
|
|
28
|
+
rect.setAttribute('stroke-width', String(cfg.strokeWidth));
|
|
29
|
+
svg.appendChild(rect);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function addGlow(svg: SVGSVGElement, cfg: { cx: number; cy: number; color: string; opacity: number; luxRatio: number; ambiance: number }) {
|
|
33
|
+
const radius = (6 + cfg.luxRatio * 18) * cfg.ambiance;
|
|
34
|
+
const gradientId = `glow-${cfg.cx}-${cfg.cy}`.replace(/\./g, '_');
|
|
35
|
+
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
|
|
36
|
+
const radial = document.createElementNS('http://www.w3.org/2000/svg', 'radialGradient');
|
|
37
|
+
radial.setAttribute('id', gradientId);
|
|
38
|
+
radial.setAttribute('cx', '50%');
|
|
39
|
+
radial.setAttribute('cy', '50%');
|
|
40
|
+
radial.setAttribute('r', '50%');
|
|
41
|
+
const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
|
|
42
|
+
stop1.setAttribute('offset', '0%');
|
|
43
|
+
stop1.setAttribute('stop-color', cfg.color);
|
|
44
|
+
stop1.setAttribute('stop-opacity', String(cfg.opacity * 0.6));
|
|
45
|
+
const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
|
|
46
|
+
stop2.setAttribute('offset', '100%');
|
|
47
|
+
stop2.setAttribute('stop-color', cfg.color);
|
|
48
|
+
stop2.setAttribute('stop-opacity', '0');
|
|
49
|
+
radial.appendChild(stop1);
|
|
50
|
+
radial.appendChild(stop2);
|
|
51
|
+
defs.appendChild(radial);
|
|
52
|
+
svg.appendChild(defs);
|
|
53
|
+
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
54
|
+
circle.setAttribute('cx', String(cfg.cx));
|
|
55
|
+
circle.setAttribute('cy', String(cfg.cy));
|
|
56
|
+
circle.setAttribute('r', String(radius));
|
|
57
|
+
circle.setAttribute('fill', `url(#${gradientId})`);
|
|
58
|
+
circle.setAttribute('pointer-events', 'none');
|
|
59
|
+
svg.appendChild(circle);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function addFixture(svg: SVGSVGElement, cfg: { cx: number; cy: number; color: string; opacity: number }) {
|
|
63
|
+
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
64
|
+
circle.setAttribute('cx', String(cfg.cx));
|
|
65
|
+
circle.setAttribute('cy', String(cfg.cy));
|
|
66
|
+
circle.setAttribute('r', '3');
|
|
67
|
+
circle.setAttribute('fill', cfg.color);
|
|
68
|
+
circle.setAttribute('opacity', String(cfg.opacity));
|
|
69
|
+
svg.appendChild(circle);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function addOverlay(svg: SVGSVGElement, cfg: { x: number; y: number; w: number; h: number; opacity: number }) {
|
|
73
|
+
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
74
|
+
rect.setAttribute('x', String(cfg.x));
|
|
75
|
+
rect.setAttribute('y', String(cfg.y));
|
|
76
|
+
rect.setAttribute('width', String(cfg.w));
|
|
77
|
+
rect.setAttribute('height', String(cfg.h));
|
|
78
|
+
rect.setAttribute('rx', '8');
|
|
79
|
+
rect.setAttribute('fill', `rgba(0,0,0,${cfg.opacity})`);
|
|
80
|
+
rect.setAttribute('pointer-events', 'none');
|
|
81
|
+
svg.appendChild(rect);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getAmbianceColor(ambiance: number): string {
|
|
85
|
+
if (ambiance < 0.9) return '#f59e0b';
|
|
86
|
+
if (ambiance > 1.1) return '#fff';
|
|
87
|
+
return '#facc15';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getPositions(count: number, w: number, h: number): Array<{ x: number; y: number }> {
|
|
91
|
+
const positions: Array<{ x: number; y: number }> = [];
|
|
92
|
+
const pad = 20;
|
|
93
|
+
const effW = w - pad * 2;
|
|
94
|
+
const effH = h - pad * 2;
|
|
95
|
+
if (count <= 1) {
|
|
96
|
+
positions.push({ x: w / 2, y: h / 2 });
|
|
97
|
+
return positions;
|
|
98
|
+
}
|
|
99
|
+
const cols = Math.ceil(Math.sqrt(count * (effW / effH)));
|
|
100
|
+
let rows = Math.ceil(count / cols);
|
|
101
|
+
if (rows * cols < count) rows += 1;
|
|
102
|
+
const cellW = effW / cols;
|
|
103
|
+
const cellH = effH / rows;
|
|
104
|
+
for (let i = 0; i < count; i++) {
|
|
105
|
+
const c = i % cols;
|
|
106
|
+
const r = Math.floor(i / cols);
|
|
107
|
+
const x = pad + cellW * c + cellW / 2;
|
|
108
|
+
const y = pad + cellH * r + cellH / 2;
|
|
109
|
+
positions.push({ x, y });
|
|
110
|
+
}
|
|
111
|
+
return positions;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function drawFixtures(svg: SVGSVGElement, cfg: PlanConfig, origin: { cx: number; cy: number; rx: number; ry: number }) {
|
|
115
|
+
const w = origin.rx * 2;
|
|
116
|
+
const h = origin.ry * 2;
|
|
117
|
+
const opacity = 0.6 + cfg.luxRatio * 0.4;
|
|
118
|
+
const haloColor = getAmbianceColor(cfg.ambiance);
|
|
119
|
+
const positions = getPositions(cfg.optimalBulbs, w, h);
|
|
120
|
+
positions.forEach((p) => {
|
|
121
|
+
const bx = origin.cx - origin.rx + p.x;
|
|
122
|
+
const by = origin.cy - origin.ry + p.y;
|
|
123
|
+
addGlow(svg, { cx: bx, cy: by, color: haloColor, opacity, luxRatio: cfg.luxRatio, ambiance: cfg.ambiance });
|
|
124
|
+
addFixture(svg, { cx: bx, cy: by, color: '#fff', opacity });
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function drawPlan(svg: SVGSVGElement, cfg: PlanConfig) {
|
|
129
|
+
svg.innerHTML = '';
|
|
130
|
+
const maxDim = Math.max(cfg.widthM, cfg.lengthM);
|
|
131
|
+
const scale = 280 / maxDim;
|
|
132
|
+
const w = cfg.widthM * scale;
|
|
133
|
+
const h = cfg.lengthM * scale;
|
|
134
|
+
const cx = 150;
|
|
135
|
+
const cy = 100;
|
|
136
|
+
const rx = w / 2;
|
|
137
|
+
const ry = h / 2;
|
|
138
|
+
addRect(svg, { x: 0, y: 0, w: 300, h: 200, fill: '#ffffff', stroke: '#ffffff', strokeWidth: 0 });
|
|
139
|
+
const opacity = Math.min(0.85, Math.max(0, 1 - cfg.luxRatio));
|
|
140
|
+
addOverlay(svg, { x: cx - rx, y: cy - ry, w, h, opacity });
|
|
141
|
+
drawFixtures(svg, cfg, { cx, cy, rx, ry });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function addDialArc(svg: SVGSVGElement, cfg: { radius: number; color: string; dash: string; offset: number }) {
|
|
145
|
+
const cx = 60;
|
|
146
|
+
const cy = 55;
|
|
147
|
+
const arc = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
148
|
+
arc.setAttribute('d', `M ${cx - cfg.radius} ${cy} A ${cfg.radius} ${cfg.radius} 0 0 1 ${cx + cfg.radius} ${cy}`);
|
|
149
|
+
arc.setAttribute('fill', 'none');
|
|
150
|
+
arc.setAttribute('stroke', cfg.color);
|
|
151
|
+
arc.setAttribute('stroke-width', '8');
|
|
152
|
+
arc.setAttribute('stroke-linecap', 'round');
|
|
153
|
+
arc.setAttribute('stroke-dasharray', cfg.dash);
|
|
154
|
+
if (cfg.offset !== 0) arc.setAttribute('stroke-dashoffset', String(cfg.offset));
|
|
155
|
+
svg.appendChild(arc);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function addDialText(svg: SVGSVGElement, cfg: { x: number; y: number; text: string; size: string; color: string; weight: string }) {
|
|
159
|
+
const t = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
160
|
+
t.setAttribute('x', String(cfg.x));
|
|
161
|
+
t.setAttribute('y', String(cfg.y));
|
|
162
|
+
t.setAttribute('text-anchor', 'middle');
|
|
163
|
+
t.setAttribute('fill', cfg.color);
|
|
164
|
+
t.setAttribute('font-size', cfg.size);
|
|
165
|
+
t.setAttribute('font-weight', cfg.weight);
|
|
166
|
+
t.textContent = cfg.text;
|
|
167
|
+
svg.appendChild(t);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getDialColor(value: number, target: number): string {
|
|
171
|
+
const minOk = target * 0.9;
|
|
172
|
+
const maxOk = target * 1.3;
|
|
173
|
+
if (value >= minOk && value <= maxOk) return '#22c55e';
|
|
174
|
+
if (value < minOk) return '#f59e0b';
|
|
175
|
+
return '#ef4444';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function drawDial(svg: SVGSVGElement, cfg: DialConfig) {
|
|
179
|
+
svg.innerHTML = '';
|
|
180
|
+
const radius = 45;
|
|
181
|
+
const arcLen = Math.PI * radius;
|
|
182
|
+
const minOk = cfg.target * 0.9;
|
|
183
|
+
const maxOk = cfg.target * 1.3;
|
|
184
|
+
const minOkLen = (minOk / cfg.max) * arcLen;
|
|
185
|
+
const maxOkLen = (maxOk / cfg.max) * arcLen;
|
|
186
|
+
const valueLen = (cfg.value / cfg.max) * arcLen;
|
|
187
|
+
addDialArc(svg, { radius, color: 'var(--border-color)', dash: `${minOkLen} ${arcLen - minOkLen}`, offset: 0 });
|
|
188
|
+
addDialArc(svg, { radius, color: 'rgba(34, 197, 94, 0.2)', dash: `${maxOkLen - minOkLen} ${arcLen - (maxOkLen - minOkLen)}`, offset: -minOkLen });
|
|
189
|
+
addDialArc(svg, { radius, color: 'var(--border-color)', dash: `${arcLen - maxOkLen} ${maxOkLen}`, offset: -maxOkLen });
|
|
190
|
+
addDialArc(svg, { radius, color: getDialColor(cfg.value, cfg.target), dash: `${valueLen} ${arcLen - valueLen}`, offset: 0 });
|
|
191
|
+
addDialText(svg, { x: 60, y: 53, text: String(Math.round(cfg.value)), size: '12', color: 'var(--text-main)', weight: '700' });
|
|
192
|
+
addDialText(svg, { x: 60, y: 65, text: cfg.label, size: '7', color: 'var(--text-muted)', weight: '500' });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function drawBulbs(container: HTMLElement, optimal: number) {
|
|
196
|
+
container.innerHTML = '';
|
|
197
|
+
const maxVisible = Math.min(optimal, 20);
|
|
198
|
+
const activeColor = '#facc15';
|
|
199
|
+
for (let i = 0; i < maxVisible; i++) {
|
|
200
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
201
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
202
|
+
svg.setAttribute('width', '20');
|
|
203
|
+
svg.setAttribute('height', '20');
|
|
204
|
+
svg.style.filter = 'drop-shadow(0 0 3px rgba(250, 204, 21, 0.5))';
|
|
205
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
206
|
+
path.setAttribute('d', 'M12 2C8.13 2 5 5.13 5 9c0 2.38 1.19 4.47 3 5.74V17c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2.26c1.81-1.27 3-3.36 3-5.74 0-3.87-3.13-7-7-7z');
|
|
207
|
+
path.setAttribute('fill', activeColor);
|
|
208
|
+
path.setAttribute('stroke', activeColor);
|
|
209
|
+
path.setAttribute('stroke-width', '1');
|
|
210
|
+
svg.appendChild(path);
|
|
211
|
+
container.appendChild(svg);
|
|
212
|
+
}
|
|
213
|
+
if (optimal > maxVisible) {
|
|
214
|
+
const span = document.createElement('span');
|
|
215
|
+
span.style.cssText = 'font-size:0.75rem;color:var(--text-muted);align-self:center;';
|
|
216
|
+
span.textContent = `+${optimal - maxVisible}`;
|
|
217
|
+
container.appendChild(span);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
interface PDFData {
|
|
222
|
+
roomType: string;
|
|
223
|
+
width: number;
|
|
224
|
+
length: number;
|
|
225
|
+
height: number;
|
|
226
|
+
r: LightingResult;
|
|
227
|
+
unitSys: string;
|
|
228
|
+
unitLabel: string;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function generatePDF(data: PDFData) {
|
|
232
|
+
import('jspdf').then(({ jsPDF }) => {
|
|
233
|
+
const doc = new jsPDF();
|
|
234
|
+
doc.setFontSize(18);
|
|
235
|
+
doc.text('Lighting Plan', 14, 20);
|
|
236
|
+
doc.setFontSize(12);
|
|
237
|
+
doc.text(`Room: ${data.roomType}`, 14, 35);
|
|
238
|
+
doc.text(`Dimensions: ${data.width}${data.unitLabel} × ${data.length}${data.unitLabel} × ${data.height}${data.unitLabel}`, 14, 45);
|
|
239
|
+
doc.text(`Area: ${Math.round(data.r.roomArea)} m²`, 14, 55);
|
|
240
|
+
doc.text(`Target lux: ${data.r.targetLux}`, 14, 65);
|
|
241
|
+
doc.text(`Current lux: ${data.r.currentLux}`, 14, 75);
|
|
242
|
+
doc.text(`Required lumens: ${data.r.requiredLumens}`, 14, 85);
|
|
243
|
+
doc.text(`Recommended: ${data.r.suggestedProducts}`, 14, 95);
|
|
244
|
+
doc.text(`Status: ${data.r.status}`, 14, 105);
|
|
245
|
+
doc.save('lighting-plan.pdf');
|
|
246
|
+
});
|
|
247
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { HomeToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
import type { LightingCalculatorUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export type LightingCalculatorLocaleContent = ToolLocaleContent<LightingCalculatorUI>;
|
|
5
|
+
|
|
6
|
+
export const lightingCalculator: HomeToolEntry<LightingCalculatorUI> = {
|
|
7
|
+
id: 'how-many-lights-per-room',
|
|
8
|
+
icons: {
|
|
9
|
+
bg: 'mdi:lightbulb',
|
|
10
|
+
fg: 'mdi:flash',
|
|
11
|
+
},
|
|
12
|
+
i18n: {
|
|
13
|
+
de: async () => (await import('./i18n/de')).content,
|
|
14
|
+
en: async () => (await import('./i18n/en')).content,
|
|
15
|
+
es: async () => (await import('./i18n/es')).content,
|
|
16
|
+
fr: async () => (await import('./i18n/fr')).content,
|
|
17
|
+
id: async () => (await import('./i18n/id')).content,
|
|
18
|
+
it: async () => (await import('./i18n/it')).content,
|
|
19
|
+
ja: async () => (await import('./i18n/ja')).content,
|
|
20
|
+
ko: async () => (await import('./i18n/ko')).content,
|
|
21
|
+
nl: async () => (await import('./i18n/nl')).content,
|
|
22
|
+
pl: async () => (await import('./i18n/pl')).content,
|
|
23
|
+
pt: async () => (await import('./i18n/pt')).content,
|
|
24
|
+
ru: async () => (await import('./i18n/ru')).content,
|
|
25
|
+
sv: async () => (await import('./i18n/sv')).content,
|
|
26
|
+
tr: async () => (await import('./i18n/tr')).content,
|
|
27
|
+
zh: async () => (await import('./i18n/zh')).content,
|
|
28
|
+
},
|
|
29
|
+
};
|