@jjlmoya/utils-home 1.30.0 → 1.31.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/entries.ts +4 -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/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 +2 -0
package/package.json
CHANGED
package/src/entries.ts
CHANGED
|
@@ -24,6 +24,8 @@ export { deskErgonomics } from './tool/deskErgonomics/entry';
|
|
|
24
24
|
export type { DeskErgonomicsLocaleContent } from './tool/deskErgonomics/entry';
|
|
25
25
|
export { applianceCostCalculator } from './tool/applianceCostCalculator/entry';
|
|
26
26
|
export type { ApplianceCostCalculatorLocaleContent } from './tool/applianceCostCalculator/entry';
|
|
27
|
+
export { tileLayoutCalculator } from './tool/tileLayoutCalculator/entry';
|
|
28
|
+
export type { TileLayoutCalculatorLocaleContent } from './tool/tileLayoutCalculator/entry';
|
|
27
29
|
export { homeCategory } from './category';
|
|
28
30
|
import { dewPointCalculator } from './tool/dewPointCalculator/entry';
|
|
29
31
|
import { heatingComparator } from './tool/heatingComparator/entry';
|
|
@@ -38,4 +40,5 @@ import { wallPaintingCalculator } from './tool/wallPaintingCalculator/entry';
|
|
|
38
40
|
import { vampireDrawSimulator } from './tool/vampireDrawSimulator/entry';
|
|
39
41
|
import { deskErgonomics } from './tool/deskErgonomics/entry';
|
|
40
42
|
import { applianceCostCalculator } from './tool/applianceCostCalculator/entry';
|
|
41
|
-
|
|
43
|
+
import { tileLayoutCalculator } from './tool/tileLayoutCalculator/entry';
|
|
44
|
+
export const ALL_ENTRIES = [dewPointCalculator, heatingComparator, ledSavingCalculator, projectorCalculator, qrGenerator, solarCalculator, tariffComparator, wifiRangeSimulator, acTonnageCalculator, wallPaintingCalculator, vampireDrawSimulator, deskErgonomics, applianceCostCalculator, tileLayoutCalculator];
|
package/src/index.ts
CHANGED
|
@@ -28,4 +28,5 @@ export { WALL_PAINTING_CALCULATOR_TOOL } from './tool/wallPaintingCalculator';
|
|
|
28
28
|
export { VAMPIRE_DRAW_SIMULATOR_TOOL } from './tool/vampireDrawSimulator';
|
|
29
29
|
export { DESK_ERGONOMICS_TOOL } from './tool/deskErgonomics';
|
|
30
30
|
export { APPLIANCE_COST_CALCULATOR_TOOL } from './tool/applianceCostCalculator';
|
|
31
|
+
export { TILE_LAYOUT_CALCULATOR_TOOL } from './tool/tileLayoutCalculator';
|
|
31
32
|
|
|
@@ -17,8 +17,8 @@ describe('Locale Completeness Validation', () => {
|
|
|
17
17
|
});
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
it('should have
|
|
21
|
-
expect(ALL_TOOLS.length).toBe(
|
|
20
|
+
it('should have 14 tools registered', () => {
|
|
21
|
+
expect(ALL_TOOLS.length).toBe(14);
|
|
22
22
|
});
|
|
23
23
|
});
|
|
24
24
|
|
|
@@ -4,8 +4,8 @@ import { homeCategory } from '../data';
|
|
|
4
4
|
|
|
5
5
|
describe('Tool Validation Suite', () => {
|
|
6
6
|
describe('Library Registration', () => {
|
|
7
|
-
it('should have
|
|
8
|
-
expect(ALL_TOOLS.length).toBe(
|
|
7
|
+
it('should have 14 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(14);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('homeCategory should be defined', () => {
|
|
@@ -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
|
+
};
|