@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,48 @@
|
|
|
1
|
+
export interface LightingCalculatorUI extends Record<string, string> {
|
|
2
|
+
sectionTitle: string;
|
|
3
|
+
labelRoomType: string;
|
|
4
|
+
labelRoomWidth: string;
|
|
5
|
+
labelRoomLength: string;
|
|
6
|
+
labelHeight: string;
|
|
7
|
+
labelBulbType: string;
|
|
8
|
+
labelBulbWatt: string;
|
|
9
|
+
labelFixtures: string;
|
|
10
|
+
labelAmbient: string;
|
|
11
|
+
btnAmbientCozy: string;
|
|
12
|
+
btnAmbientNormal: string;
|
|
13
|
+
btnAmbientBright: string;
|
|
14
|
+
unitMetricRoom: string;
|
|
15
|
+
unitImperialRoom: string;
|
|
16
|
+
unitHeight: string;
|
|
17
|
+
unitBulbs: string;
|
|
18
|
+
unitWatt: string;
|
|
19
|
+
unitLux: string;
|
|
20
|
+
labelTargetLux: string;
|
|
21
|
+
labelCurrentLux: string;
|
|
22
|
+
labelBulbsNeeded: string;
|
|
23
|
+
labelRoomArea: string;
|
|
24
|
+
statusOptimal: string;
|
|
25
|
+
statusInsufficient: string;
|
|
26
|
+
statusExcess: string;
|
|
27
|
+
btnLiving: string;
|
|
28
|
+
btnKitchen: string;
|
|
29
|
+
btnBedroom: string;
|
|
30
|
+
btnBathroom: string;
|
|
31
|
+
btnOffice: string;
|
|
32
|
+
btnHallway: string;
|
|
33
|
+
btnBulbLED: string;
|
|
34
|
+
btnBulbCFL: string;
|
|
35
|
+
btnBulbHalogen: string;
|
|
36
|
+
btnBulbIncandescent: string;
|
|
37
|
+
btnMetric: string;
|
|
38
|
+
btnImperial: string;
|
|
39
|
+
tipOptimal: string;
|
|
40
|
+
tipInsufficient: string;
|
|
41
|
+
tipExcess: string;
|
|
42
|
+
labelManualAdjust: string;
|
|
43
|
+
labelSummary: string;
|
|
44
|
+
labelTotalLumens: string;
|
|
45
|
+
labelSuggestedSetup: string;
|
|
46
|
+
btnExport: string;
|
|
47
|
+
|
|
48
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { tileLayoutCalculator } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'es' } = Astro.props;
|
|
11
|
+
const content = await tileLayoutCalculator.i18n[locale]?.();
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{content && <SharedBibliography links={content.bibliography} />}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const bibliography = [
|
|
2
|
+
{
|
|
3
|
+
name: 'The Tile Council of North America Installation Guidelines',
|
|
4
|
+
url: 'https://tcnatile.com/products/publications/2026-tcna-handbook-for-ceramic-glass-and-stone-tile-installation/',
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
name: 'Ceramic Tile Distributors Association Best Practices',
|
|
8
|
+
url: 'https://www.ctdahome.org/page/resources',
|
|
9
|
+
},
|
|
10
|
+
];
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { TileLayoutCalculatorUI } from './ui';
|
|
3
|
+
import { fmt1 } from './logic';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
ui?: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { ui = {} } = Astro.props;
|
|
10
|
+
const tUI = ui as TileLayoutCalculatorUI;
|
|
11
|
+
|
|
12
|
+
const INIT_R_W = 4, INIT_R_L = 5, INIT_T_W = 30, INIT_T_L = 30, INIT_G = 3, INIT_W = 10, INIT_TPB = 8, INIT_P = 25;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<div class="tile-wrapper">
|
|
16
|
+
<div
|
|
17
|
+
class="tile-card"
|
|
18
|
+
data-u-mr={tUI.unitMetricRoom}
|
|
19
|
+
data-u-ir={tUI.unitImperialRoom}
|
|
20
|
+
data-u-mt={tUI.unitMetricTile}
|
|
21
|
+
data-u-it={tUI.unitImperialTile}
|
|
22
|
+
data-u-mg={tUI.unitGroutMetric}
|
|
23
|
+
data-u-ig={tUI.unitGroutImperial}
|
|
24
|
+
data-u-pct={tUI.unitPercent}
|
|
25
|
+
data-u-box={tUI.unitBoxes}
|
|
26
|
+
data-currency={tUI.currency}
|
|
27
|
+
>
|
|
28
|
+
<div class="tile-left">
|
|
29
|
+
<p class="tile-section-title">{tUI.sectionTitle}</p>
|
|
30
|
+
<div class="tile-unit-toggle">
|
|
31
|
+
<button class="tile-unit-btn tile-unit-active" data-unit="metric">{tUI.btnMetric}</button>
|
|
32
|
+
<button class="tile-unit-btn" data-unit="imperial">{tUI.btnImperial}</button>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="tile-field">
|
|
35
|
+
<label class="tile-label">{tUI.labelPattern}</label>
|
|
36
|
+
<div class="tile-pattern-toggle">
|
|
37
|
+
<button class="tile-pattern-btn tile-pattern-active" data-pattern="straight">{tUI.btnPatternStraight}</button>
|
|
38
|
+
<button class="tile-pattern-btn" data-pattern="brick">{tUI.btnPatternBrick}</button>
|
|
39
|
+
<button class="tile-pattern-btn" data-pattern="diagonal">{tUI.btnPatternDiagonal}</button>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="tile-field">
|
|
43
|
+
<label class="tile-label" for="tl-rw">{tUI.labelRoomWidth}</label>
|
|
44
|
+
<div class="tile-number-row">
|
|
45
|
+
<input type="number" id="tl-rw" value={INIT_R_W} min="0.1" max="50" step="0.1" class="tile-number-input" />
|
|
46
|
+
<span class="tile-number-unit" id="tl-rw-u">{tUI.unitMetricRoom}</span>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="tile-field">
|
|
50
|
+
<label class="tile-label" for="tl-rl">{tUI.labelRoomLength}</label>
|
|
51
|
+
<div class="tile-number-row">
|
|
52
|
+
<input type="number" id="tl-rl" value={INIT_R_L} min="0.1" max="50" step="0.1" class="tile-number-input" />
|
|
53
|
+
<span class="tile-number-unit" id="tl-rl-u">{tUI.unitMetricRoom}</span>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="tile-field">
|
|
57
|
+
<label class="tile-label" for="tl-tw">{tUI.labelTileWidth}</label>
|
|
58
|
+
<div class="tile-number-row">
|
|
59
|
+
<input type="number" id="tl-tw" value={INIT_T_W} min="5" max="200" step="1" class="tile-number-input" />
|
|
60
|
+
<span class="tile-number-unit" id="tl-tw-u">{tUI.unitMetricTile}</span>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="tile-field">
|
|
64
|
+
<label class="tile-label" for="tl-tl">{tUI.labelTileLength}</label>
|
|
65
|
+
<div class="tile-number-row">
|
|
66
|
+
<input type="number" id="tl-tl" value={INIT_T_L} min="5" max="200" step="1" class="tile-number-input" />
|
|
67
|
+
<span class="tile-number-unit" id="tl-tl-u">{tUI.unitMetricTile}</span>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="tile-field">
|
|
71
|
+
<label class="tile-label" for="tl-g">{tUI.labelGrout}</label>
|
|
72
|
+
<div class="tile-number-row">
|
|
73
|
+
<input type="number" id="tl-g" value={INIT_G} min="0" step="0.5" class="tile-number-input" />
|
|
74
|
+
<span class="tile-number-unit" id="tl-g-u">{tUI.unitGroutMetric}</span>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="tile-field">
|
|
78
|
+
<label class="tile-label" for="tl-w">{tUI.labelWaste}</label>
|
|
79
|
+
<div class="tile-number-row">
|
|
80
|
+
<input type="number" id="tl-w" value={INIT_W} min="0" max="50" step="1" class="tile-number-input" />
|
|
81
|
+
<span class="tile-number-unit">{tUI.unitPercent}</span>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="tile-field">
|
|
85
|
+
<label class="tile-label" for="tl-tpb">{tUI.labelTilesPerBox}</label>
|
|
86
|
+
<div class="tile-number-row">
|
|
87
|
+
<input type="number" id="tl-tpb" value={INIT_TPB} min="1" step="1" class="tile-number-input" />
|
|
88
|
+
<span class="tile-number-unit">{tUI.unitBoxes}</span>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="tile-field">
|
|
92
|
+
<label class="tile-label" for="tl-p">{tUI.labelPrice}</label>
|
|
93
|
+
<div class="tile-number-row">
|
|
94
|
+
<input type="number" id="tl-p" value={INIT_P} min="0" step="0.01" class="tile-price-input" />
|
|
95
|
+
<span class="tile-number-unit" id="tl-p-u">{tUI.unitPrice}</span>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
<div class="tile-right">
|
|
100
|
+
<div class="tile-visual-title">{tUI.visualTitle}</div>
|
|
101
|
+
<div class="tile-svg-wrap" id="tl-sw"><div class="tile-dim-label" id="tl-dl"></div></div>
|
|
102
|
+
<div class="tile-result-badge" id="tl-rb">{tUI.resultBadge}</div>
|
|
103
|
+
<div class="tile-stats">
|
|
104
|
+
<div class="tile-stat">
|
|
105
|
+
<p class="tile-stat-label">{tUI.labelArea}</p>
|
|
106
|
+
<p id="tl-a" class="tile-stat-value">{fmt1(INIT_R_W * INIT_R_L)} m²</p>
|
|
107
|
+
</div>
|
|
108
|
+
<div class="tile-stat-divider"></div>
|
|
109
|
+
<div class="tile-stat">
|
|
110
|
+
<p class="tile-stat-label">{tUI.labelTiles}</p>
|
|
111
|
+
<p id="tl-t" class="tile-stat-value">0</p>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="tile-stat-divider"></div>
|
|
114
|
+
<div class="tile-stat">
|
|
115
|
+
<p class="tile-stat-label">{tUI.labelBoxes}</p>
|
|
116
|
+
<p id="tl-b" class="tile-stat-value">0</p>
|
|
117
|
+
</div>
|
|
118
|
+
<div class="tile-stat-divider"></div>
|
|
119
|
+
<div class="tile-stat">
|
|
120
|
+
<p class="tile-stat-label">{tUI.labelCost}</p>
|
|
121
|
+
<p id="tl-c" class="tile-stat-value">0</p>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
<div class="tile-waste-row">
|
|
125
|
+
<p class="tile-waste-label">{tUI.labelWasteCount}</p>
|
|
126
|
+
<p id="tl-wc" class="tile-waste-value">0</p>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<script>
|
|
133
|
+
import { calculateTileLayout, fmt0, fmt2 } from './logic';
|
|
134
|
+
import type { TileLayoutResult } from './logic';
|
|
135
|
+
|
|
136
|
+
const LS_U = 'tile-layout-unit';
|
|
137
|
+
const LS_P = 'tile-layout-pattern';
|
|
138
|
+
const LS_V = 'tile-layout-values';
|
|
139
|
+
let unitSys: 'metric' | 'imperial' = 'metric';
|
|
140
|
+
let pattern: 'straight' | 'brick' | 'diagonal' = 'straight';
|
|
141
|
+
|
|
142
|
+
function el(id: string) { return document.getElementById(id); }
|
|
143
|
+
function setTxt(id: string, v: string) { const e = el(id); if (e) e.textContent = v; }
|
|
144
|
+
function getNum(id: string): number { const e = el(id) as HTMLInputElement | null; return e ? parseFloat(e.value) || 0 : 0; }
|
|
145
|
+
function ds(c: HTMLElement, k: string): string { return c.dataset[k] ?? ''; }
|
|
146
|
+
|
|
147
|
+
function factors() {
|
|
148
|
+
return unitSys === 'metric'
|
|
149
|
+
? { rm: 1, tm: 0.01, gm: 0.001 }
|
|
150
|
+
: { rm: 0.3048, tm: 0.0254, gm: 0.0254 };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function makeSVG(ns: string, r: TileLayoutResult): SVGElement {
|
|
154
|
+
const svg = document.createElementNS(ns, 'svg');
|
|
155
|
+
svg.setAttribute('viewBox', `0 0 ${r.roomWidthM} ${r.roomLengthM}`); svg.setAttribute('class', 'tile-svg');
|
|
156
|
+
svg.style.width = '100%'; svg.style.height = '100%';
|
|
157
|
+
return svg;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function drawSimple(ns: string, svg: SVGElement, r: TileLayoutResult) {
|
|
161
|
+
const bg = document.createElementNS(ns, 'rect');
|
|
162
|
+
bg.setAttribute('x', '0'); bg.setAttribute('y', '0');
|
|
163
|
+
bg.setAttribute('width', String(r.roomWidthM)); bg.setAttribute('height', String(r.roomLengthM));
|
|
164
|
+
bg.setAttribute('class', 'tile-rect-primary'); svg.appendChild(bg);
|
|
165
|
+
const step = Math.max(r.tileWidthM, r.tileLengthM);
|
|
166
|
+
for (let x = 0; x < r.roomWidthM; x += step) {
|
|
167
|
+
const l = document.createElementNS(ns, 'line');
|
|
168
|
+
l.setAttribute('x1', String(x)); l.setAttribute('y1', '0');
|
|
169
|
+
l.setAttribute('x2', String(x)); l.setAttribute('y2', String(r.roomLengthM));
|
|
170
|
+
l.setAttribute('class', 'tile-grid-bg'); svg.appendChild(l);
|
|
171
|
+
}
|
|
172
|
+
for (let y = 0; y < r.roomLengthM; y += step) {
|
|
173
|
+
const l = document.createElementNS(ns, 'line');
|
|
174
|
+
l.setAttribute('x1', '0'); l.setAttribute('y1', String(y));
|
|
175
|
+
l.setAttribute('x2', String(r.roomWidthM)); l.setAttribute('y2', String(y));
|
|
176
|
+
l.setAttribute('class', 'tile-grid-bg'); svg.appendChild(l);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function drawTile(ns: string, r: TileLayoutResult, parent: SVGElement, cell: { c: number; ro: number; x: number; y: number }) {
|
|
181
|
+
const w = Math.max(0, Math.min(r.tileWidthM, r.roomWidthM - cell.x));
|
|
182
|
+
const h = Math.max(0, Math.min(r.tileLengthM, r.roomLengthM - cell.y));
|
|
183
|
+
if (w <= 0 || h <= 0) return;
|
|
184
|
+
const rect = document.createElementNS(ns, 'rect');
|
|
185
|
+
rect.setAttribute('x', String(cell.x)); rect.setAttribute('y', String(cell.y));
|
|
186
|
+
rect.setAttribute('width', String(w)); rect.setAttribute('height', String(h));
|
|
187
|
+
rect.setAttribute('rx', String(Math.max(r.groutM * 0.3, 0.002)));
|
|
188
|
+
rect.setAttribute('class', (cell.c + cell.ro) % 2 === 0 ? 'tile-rect-primary' : 'tile-rect-secondary');
|
|
189
|
+
parent.appendChild(rect);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function drawStraight(ns: string, svg: SVGElement, r: TileLayoutResult) {
|
|
193
|
+
for (let c = 0; c < r.cols; c++) {
|
|
194
|
+
for (let ro = 0; ro < r.rows; ro++) {
|
|
195
|
+
const x = c * (r.tileWidthM + r.groutM);
|
|
196
|
+
const y = ro * (r.tileLengthM + r.groutM);
|
|
197
|
+
drawTile(ns, r, svg, { c, ro, x, y });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function drawBrick(ns: string, svg: SVGElement, r: TileLayoutResult) {
|
|
203
|
+
const offset = (r.tileWidthM + r.groutM) / 2;
|
|
204
|
+
for (let ro = 0; ro < r.rows; ro++) {
|
|
205
|
+
const rowOffset = ro % 2 === 1 ? offset : 0;
|
|
206
|
+
if (rowOffset > 0) {
|
|
207
|
+
const halfW = r.tileWidthM - rowOffset;
|
|
208
|
+
const y = ro * (r.tileLengthM + r.groutM);
|
|
209
|
+
const h = Math.max(0, Math.min(r.tileLengthM, r.roomLengthM - y));
|
|
210
|
+
if (halfW > 0 && h > 0) {
|
|
211
|
+
const rect = document.createElementNS(ns, 'rect');
|
|
212
|
+
rect.setAttribute('x', '0');
|
|
213
|
+
rect.setAttribute('y', String(y));
|
|
214
|
+
rect.setAttribute('width', String(Math.min(halfW, r.roomWidthM)));
|
|
215
|
+
rect.setAttribute('height', String(h));
|
|
216
|
+
rect.setAttribute('rx', String(Math.max(r.groutM * 0.3, 0.002)));
|
|
217
|
+
rect.setAttribute('class', (-1 + ro) % 2 === 0 ? 'tile-rect-primary' : 'tile-rect-secondary');
|
|
218
|
+
svg.appendChild(rect);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (let c = 0; c < r.cols; c++) {
|
|
222
|
+
const x = c * (r.tileWidthM + r.groutM) + rowOffset;
|
|
223
|
+
const y = ro * (r.tileLengthM + r.groutM);
|
|
224
|
+
drawTile(ns, r, svg, { c, ro, x, y });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function drawDiagonal(ns: string, svg: SVGElement, r: TileLayoutResult) {
|
|
230
|
+
const g = document.createElementNS(ns, 'g');
|
|
231
|
+
g.setAttribute('transform', `rotate(45 ${r.roomWidthM / 2} ${r.roomLengthM / 2})`);
|
|
232
|
+
for (let c = 0; c < r.cols; c++) {
|
|
233
|
+
for (let ro = 0; ro < r.rows; ro++) {
|
|
234
|
+
const x = c * (r.tileWidthM + r.groutM);
|
|
235
|
+
const y = ro * (r.tileLengthM + r.groutM);
|
|
236
|
+
drawTile(ns, r, g, { c, ro, x, y });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
svg.appendChild(g);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function buildSVG(r: TileLayoutResult) {
|
|
243
|
+
const wrap = el('tl-sw');
|
|
244
|
+
if (!wrap) return;
|
|
245
|
+
const ns = 'http://www.w3.org/2000/svg';
|
|
246
|
+
const svg = makeSVG(ns, r);
|
|
247
|
+
const totalTiles = r.cols * r.rows;
|
|
248
|
+
if (totalTiles > 2000) {
|
|
249
|
+
drawSimple(ns, svg, r);
|
|
250
|
+
} else if (pattern === 'brick') {
|
|
251
|
+
drawBrick(ns, svg, r);
|
|
252
|
+
} else if (pattern === 'diagonal') {
|
|
253
|
+
drawDiagonal(ns, svg, r);
|
|
254
|
+
} else {
|
|
255
|
+
drawStraight(ns, svg, r);
|
|
256
|
+
}
|
|
257
|
+
const oldSvg = wrap.querySelector('svg');
|
|
258
|
+
if (oldSvg) oldSvg.remove();
|
|
259
|
+
wrap.appendChild(svg);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function setState(r: TileLayoutResult) {
|
|
263
|
+
const wrap = el('tl-sw');
|
|
264
|
+
if (!wrap) return;
|
|
265
|
+
wrap.classList.remove('tile-state-optimal', 'tile-state-warning');
|
|
266
|
+
const cutW = r.cols * r.tileWidthM + (r.cols - 1) * r.groutM;
|
|
267
|
+
const cutL = r.rows * r.tileLengthM + (r.rows - 1) * r.groutM;
|
|
268
|
+
const hasCut = cutW > r.roomWidthM + 0.01 || cutL > r.roomLengthM + 0.01;
|
|
269
|
+
if (r.wastePercent <= 5 && !hasCut) {
|
|
270
|
+
wrap.classList.add('tile-state-optimal');
|
|
271
|
+
} else if (r.wastePercent >= 20 || hasCut) {
|
|
272
|
+
wrap.classList.add('tile-state-warning');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function calculate(card: HTMLElement) {
|
|
277
|
+
const f = factors();
|
|
278
|
+
const r = calculateTileLayout({
|
|
279
|
+
roomWidthM: getNum('tl-rw') * f.rm,
|
|
280
|
+
roomLengthM: getNum('tl-rl') * f.rm,
|
|
281
|
+
tileWidthM: getNum('tl-tw') * f.tm,
|
|
282
|
+
tileLengthM: getNum('tl-tl') * f.tm,
|
|
283
|
+
groutM: getNum('tl-g') * f.gm,
|
|
284
|
+
wastePercent: getNum('tl-w'),
|
|
285
|
+
tilesPerBox: getNum('tl-tpb'),
|
|
286
|
+
pricePerBox: getNum('tl-p'),
|
|
287
|
+
});
|
|
288
|
+
const areaUnit = unitSys === 'metric' ? 'm²' : 'ft²';
|
|
289
|
+
const currencyCode = ds(card, 'currency') || 'USD';
|
|
290
|
+
const costFormatted = new Intl.NumberFormat(navigator.language, { style: 'currency', currency: currencyCode }).format(r.totalCost);
|
|
291
|
+
setTxt('tl-a', `${fmt2(r.roomArea)} ${areaUnit}`);
|
|
292
|
+
setTxt('tl-t', fmt0(r.totalTiles));
|
|
293
|
+
setTxt('tl-b', fmt0(r.boxes));
|
|
294
|
+
setTxt('tl-c', costFormatted);
|
|
295
|
+
setTxt('tl-wc', fmt0(r.wasteTiles));
|
|
296
|
+
buildSVG(r);
|
|
297
|
+
setTxt('tl-dl', `${r.cols} x ${r.rows}`);
|
|
298
|
+
setState(r);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function saveValues() {
|
|
302
|
+
const v = {
|
|
303
|
+
rw: getNum('tl-rw'),
|
|
304
|
+
rl: getNum('tl-rl'),
|
|
305
|
+
tw: getNum('tl-tw'),
|
|
306
|
+
tl: getNum('tl-tl'),
|
|
307
|
+
g: getNum('tl-g'),
|
|
308
|
+
w: getNum('tl-w'),
|
|
309
|
+
tpb: getNum('tl-tpb'),
|
|
310
|
+
p: getNum('tl-p'),
|
|
311
|
+
};
|
|
312
|
+
localStorage.setItem(LS_V, JSON.stringify(v));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function restoreValues() {
|
|
316
|
+
const s = localStorage.getItem(LS_V);
|
|
317
|
+
if (!s) return;
|
|
318
|
+
try {
|
|
319
|
+
const v = JSON.parse(s) as Record<string, number>;
|
|
320
|
+
const ids = ['tl-rw', 'tl-rl', 'tl-tw', 'tl-tl', 'tl-g', 'tl-w', 'tl-tpb', 'tl-p'];
|
|
321
|
+
ids.forEach((id) => {
|
|
322
|
+
const e = el(id) as HTMLInputElement | null;
|
|
323
|
+
if (e && v[id.replace('tl-', '')] !== undefined) e.value = String(v[id.replace('tl-', '')]);
|
|
324
|
+
});
|
|
325
|
+
} catch {}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function updateLabels(card: HTMLElement) {
|
|
329
|
+
const i = unitSys === 'imperial';
|
|
330
|
+
setTxt('tl-rw-u', i ? ds(card, 'uIr') : ds(card, 'uMr'));
|
|
331
|
+
setTxt('tl-rl-u', i ? ds(card, 'uIr') : ds(card, 'uMr'));
|
|
332
|
+
setTxt('tl-tw-u', i ? ds(card, 'uIt') : ds(card, 'uMt'));
|
|
333
|
+
setTxt('tl-tl-u', i ? ds(card, 'uIt') : ds(card, 'uMt'));
|
|
334
|
+
setTxt('tl-g-u', i ? ds(card, 'uIg') : ds(card, 'uMg'));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function applyUnit(s: 'metric' | 'imperial', card: HTMLElement) {
|
|
338
|
+
unitSys = s;
|
|
339
|
+
localStorage.setItem(LS_U, s);
|
|
340
|
+
document.querySelectorAll('.tile-unit-btn').forEach((b) => b.classList.toggle('tile-unit-active', (b as HTMLElement).dataset.unit === s));
|
|
341
|
+
const isM = s === 'metric';
|
|
342
|
+
function upd(id: string, factor: number) {
|
|
343
|
+
const e = el(id) as HTMLInputElement | null;
|
|
344
|
+
if (e) e.value = String(Math.round((getNum(id) * factor) * 10) / 10);
|
|
345
|
+
}
|
|
346
|
+
upd('tl-rw', isM ? 0.3048 : 1 / 0.3048);
|
|
347
|
+
upd('tl-rl', isM ? 0.3048 : 1 / 0.3048);
|
|
348
|
+
upd('tl-tw', isM ? 2.54 : 1 / 2.54);
|
|
349
|
+
upd('tl-tl', isM ? 2.54 : 1 / 2.54);
|
|
350
|
+
upd('tl-g', isM ? 25.4 : 1 / 25.4);
|
|
351
|
+
updateLabels(card);
|
|
352
|
+
calculate(card);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function applyPattern(p: 'straight' | 'brick' | 'diagonal', card: HTMLElement) {
|
|
356
|
+
pattern = p;
|
|
357
|
+
localStorage.setItem(LS_P, p);
|
|
358
|
+
document.querySelectorAll('.tile-pattern-btn').forEach((b) => b.classList.toggle('tile-pattern-active', (b as HTMLElement).dataset.pattern === p));
|
|
359
|
+
calculate(card);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function onClickParticle(ev: MouseEvent) {
|
|
363
|
+
for (let i = 0; i < 5; i++) {
|
|
364
|
+
const p = document.createElement('span');
|
|
365
|
+
p.className = 'tile-particle';
|
|
366
|
+
p.textContent = ['+', '~', '*'][Math.floor(Math.random() * 3)];
|
|
367
|
+
const a = Math.random() * 6.28;
|
|
368
|
+
const d = 20 + Math.random() * 30;
|
|
369
|
+
p.style.left = `${ev.clientX}px`;
|
|
370
|
+
p.style.top = `${ev.clientY}px`;
|
|
371
|
+
p.style.setProperty('--tx', `${Math.cos(a) * d}px`);
|
|
372
|
+
p.style.setProperty('--ty', `${Math.sin(a) * d}px`);
|
|
373
|
+
document.body.appendChild(p);
|
|
374
|
+
setTimeout(() => p.remove(), 800);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function bindInputs(card: HTMLElement) {
|
|
379
|
+
['tl-rw', 'tl-rl', 'tl-tw', 'tl-tl', 'tl-g', 'tl-w', 'tl-tpb', 'tl-p'].forEach((id) => {
|
|
380
|
+
const e = el(id);
|
|
381
|
+
if (e) e.addEventListener('input', () => { saveValues(); calculate(card); });
|
|
382
|
+
});
|
|
383
|
+
document.querySelectorAll('.tile-unit-btn').forEach((btn) => {
|
|
384
|
+
btn.addEventListener('click', () => {
|
|
385
|
+
const s = (btn as HTMLElement).dataset.unit as 'metric' | 'imperial';
|
|
386
|
+
if (s) applyUnit(s, card);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
document.querySelectorAll('.tile-pattern-btn').forEach((btn) => {
|
|
390
|
+
btn.addEventListener('click', () => {
|
|
391
|
+
const p = (btn as HTMLElement).dataset.pattern as 'straight' | 'brick' | 'diagonal';
|
|
392
|
+
if (p) applyPattern(p, card);
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
document.querySelectorAll('input, button').forEach((b) => b.addEventListener('click', onClickParticle as EventListener));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function init() {
|
|
399
|
+
const card = document.querySelector('.tile-card') as HTMLElement | null;
|
|
400
|
+
if (!card) return;
|
|
401
|
+
const saved = localStorage.getItem(LS_U) as 'metric' | 'imperial' | null;
|
|
402
|
+
if (saved) unitSys = saved;
|
|
403
|
+
const savedP = localStorage.getItem(LS_P) as 'straight' | 'brick' | 'diagonal' | null;
|
|
404
|
+
if (savedP) pattern = savedP;
|
|
405
|
+
document.querySelectorAll('.tile-unit-btn').forEach((b) => b.classList.toggle('tile-unit-active', (b as HTMLElement).dataset.unit === unitSys));
|
|
406
|
+
document.querySelectorAll('.tile-pattern-btn').forEach((b) => b.classList.toggle('tile-pattern-active', (b as HTMLElement).dataset.pattern === pattern));
|
|
407
|
+
restoreValues();
|
|
408
|
+
updateLabels(card);
|
|
409
|
+
bindInputs(card);
|
|
410
|
+
calculate(card);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
document.addEventListener('astro:page-load', init);
|
|
414
|
+
init();
|
|
415
|
+
</script>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { HomeToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
import type { TileLayoutCalculatorUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export type TileLayoutCalculatorLocaleContent = ToolLocaleContent<TileLayoutCalculatorUI>;
|
|
5
|
+
|
|
6
|
+
export const tileLayoutCalculator: HomeToolEntry<TileLayoutCalculatorUI> = {
|
|
7
|
+
id: 'tile-layout-calculator',
|
|
8
|
+
icons: {
|
|
9
|
+
bg: 'mdi:floor-plan',
|
|
10
|
+
fg: 'mdi:checkerboard',
|
|
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
|
+
};
|