@jjlmoya/utils-home 1.31.0 → 1.33.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 +4 -1
- 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 +120 -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/tools.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jjlmoya/utils-home",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.33.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"@jjlmoya/utils-shared": "1.2.0",
|
|
45
45
|
"astro": "^6.1.2",
|
|
46
46
|
"astro-icon": "^1.1.0",
|
|
47
|
+
"jspdf": "^4.2.1",
|
|
47
48
|
"qrcode": "^1.5.4"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
package/src/entries.ts
CHANGED
|
@@ -26,6 +26,8 @@ export { applianceCostCalculator } from './tool/applianceCostCalculator/entry';
|
|
|
26
26
|
export type { ApplianceCostCalculatorLocaleContent } from './tool/applianceCostCalculator/entry';
|
|
27
27
|
export { tileLayoutCalculator } from './tool/tileLayoutCalculator/entry';
|
|
28
28
|
export type { TileLayoutCalculatorLocaleContent } from './tool/tileLayoutCalculator/entry';
|
|
29
|
+
export { lightingCalculator } from './tool/lightingCalculator/entry';
|
|
30
|
+
export type { LightingCalculatorLocaleContent } from './tool/lightingCalculator/entry';
|
|
29
31
|
export { homeCategory } from './category';
|
|
30
32
|
import { dewPointCalculator } from './tool/dewPointCalculator/entry';
|
|
31
33
|
import { heatingComparator } from './tool/heatingComparator/entry';
|
|
@@ -41,4 +43,5 @@ import { vampireDrawSimulator } from './tool/vampireDrawSimulator/entry';
|
|
|
41
43
|
import { deskErgonomics } from './tool/deskErgonomics/entry';
|
|
42
44
|
import { applianceCostCalculator } from './tool/applianceCostCalculator/entry';
|
|
43
45
|
import { tileLayoutCalculator } from './tool/tileLayoutCalculator/entry';
|
|
44
|
-
|
|
46
|
+
import { lightingCalculator } from './tool/lightingCalculator/entry';
|
|
47
|
+
export const ALL_ENTRIES = [dewPointCalculator, heatingComparator, ledSavingCalculator, projectorCalculator, qrGenerator, solarCalculator, tariffComparator, wifiRangeSimulator, acTonnageCalculator, wallPaintingCalculator, vampireDrawSimulator, deskErgonomics, applianceCostCalculator, tileLayoutCalculator, lightingCalculator];
|
|
@@ -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 15 tools registered', () => {
|
|
21
|
+
expect(ALL_TOOLS.length).toBe(15);
|
|
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 15 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(15);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('homeCategory should be defined', () => {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { BibliographyEntry } from '../../types';
|
|
3
|
+
import { bibliography } from './bibliography';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
title?: string;
|
|
7
|
+
links?: BibliographyEntry[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { links = bibliography } = Astro.props;
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<ul class="lighting-bibliography">
|
|
14
|
+
{links.map((link) => (
|
|
15
|
+
<li>
|
|
16
|
+
<a href={link.url} target="_blank" rel="noopener noreferrer">
|
|
17
|
+
{link.name}
|
|
18
|
+
</a>
|
|
19
|
+
</li>
|
|
20
|
+
))}
|
|
21
|
+
</ul>
|
|
22
|
+
|
|
23
|
+
<style>
|
|
24
|
+
.lighting-bibliography {
|
|
25
|
+
list-style: none;
|
|
26
|
+
padding: 0;
|
|
27
|
+
margin: 0;
|
|
28
|
+
}
|
|
29
|
+
.lighting-bibliography li {
|
|
30
|
+
margin-bottom: 0.5rem;
|
|
31
|
+
}
|
|
32
|
+
.lighting-bibliography a {
|
|
33
|
+
color: var(--text-primary);
|
|
34
|
+
text-decoration: underline;
|
|
35
|
+
}
|
|
36
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const bibliography = [
|
|
2
|
+
{
|
|
3
|
+
name: 'IES Lighting Handbook 10th Edition',
|
|
4
|
+
url: 'https://www.ies.org/publications/ies-lighting-handbook/',
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
name: 'EN 12464-1:2021 Light and Lighting - Indoor Work Places',
|
|
8
|
+
url: 'https://standards.cencenelec.eu/',
|
|
9
|
+
},
|
|
10
|
+
];
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { LightingCalculatorUI } from './ui';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
ui?: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { ui = {} } = Astro.props;
|
|
9
|
+
const lUI = ui as LightingCalculatorUI;
|
|
10
|
+
|
|
11
|
+
const INIT_R_W = 4, INIT_R_L = 5, INIT_H = 2.7, INIT_W = 9, INIT_F = 6;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<div class="lc-wrap">
|
|
15
|
+
<div class="lc-card">
|
|
16
|
+
<div class="lc-head">
|
|
17
|
+
<span class="lc-title">{lUI.sectionTitle}</span>
|
|
18
|
+
<div class="lc-unit-toggle">
|
|
19
|
+
<button class="lc-unit-btn lc-unit-active" data-unit="metric">{lUI.btnMetric}</button>
|
|
20
|
+
<button class="lc-unit-btn" data-unit="imperial">{lUI.btnImperial}</button>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="lc-row">
|
|
24
|
+
<div class="lc-input-group">
|
|
25
|
+
<label class="lc-input-label" for="lc-w">{lUI.labelRoomWidth}</label>
|
|
26
|
+
<div class="lc-input-wrap">
|
|
27
|
+
<input id="lc-w" type="number" value={INIT_R_W} min="0.1" max="50" step="0.1" class="lc-input" />
|
|
28
|
+
<span class="lc-input-unit" id="lc-w-u">{lUI.unitMetricRoom}</span>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="lc-input-group">
|
|
32
|
+
<label class="lc-input-label" for="lc-l">{lUI.labelRoomLength}</label>
|
|
33
|
+
<div class="lc-input-wrap">
|
|
34
|
+
<input id="lc-l" type="number" value={INIT_R_L} min="0.1" max="50" step="0.1" class="lc-input" />
|
|
35
|
+
<span class="lc-input-unit" id="lc-l-u">{lUI.unitMetricRoom}</span>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="lc-input-group">
|
|
39
|
+
<label class="lc-input-label" for="lc-h">{lUI.labelHeight}</label>
|
|
40
|
+
<div class="lc-input-wrap">
|
|
41
|
+
<input id="lc-h" type="number" value={INIT_H} min="1.5" max="10" step="0.1" class="lc-input" />
|
|
42
|
+
<span class="lc-input-unit" id="lc-h-u">{lUI.unitHeight}</span>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="lc-row">
|
|
47
|
+
<div class="lc-chips">
|
|
48
|
+
<button class="lc-chip lc-chip-active" data-room="living">
|
|
49
|
+
<svg class="lc-chip-icon" viewBox="0 0 24 24"><path d="M7 6h10v3h-2V7H9v2H7V6zm-2 5h14v8h-2v-6H9v6H7v-8z"/></svg>
|
|
50
|
+
<span>{lUI.btnLiving}</span>
|
|
51
|
+
</button>
|
|
52
|
+
<button class="lc-chip" data-room="kitchen">
|
|
53
|
+
<svg class="lc-chip-icon" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
|
|
54
|
+
<span>{lUI.btnKitchen}</span>
|
|
55
|
+
</button>
|
|
56
|
+
<button class="lc-chip" data-room="bedroom">
|
|
57
|
+
<svg class="lc-chip-icon" viewBox="0 0 24 24"><path d="M7 14c1.66 0 3-1.34 3-3S8.66 8 7 8s-3 1.34-3 3 1.34 3 3 3zm12-3c0 1.66 1.34 3 3 3s3-1.34 3-3-1.34-3-3-3-3 1.34-3 3zm-3 4H7c-2.21 0-4 1.79-4 4v2h16v-2c0-2.21-1.79-4-4-4z"/></svg>
|
|
58
|
+
<span>{lUI.btnBedroom}</span>
|
|
59
|
+
</button>
|
|
60
|
+
<button class="lc-chip" data-room="bathroom">
|
|
61
|
+
<svg class="lc-chip-icon" viewBox="0 0 24 24"><path d="M7 7c0-2.21 1.79-4 4-4s4 1.79 4 4v3H7V7zm12 4H5v8c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2v-8z"/></svg>
|
|
62
|
+
<span>{lUI.btnBathroom}</span>
|
|
63
|
+
</button>
|
|
64
|
+
<button class="lc-chip" data-room="office">
|
|
65
|
+
<svg class="lc-chip-icon" viewBox="0 0 24 24"><path d="M20 18c1.1 0 1.99-.9 1.99-2L22 5c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v11c0 1.1.9 2 2 2H0c0 1.1.9 2 2 2h20c1.1 0 2-.9 2-2h-4zM4 5h16v11H4V5z"/></svg>
|
|
66
|
+
<span>{lUI.btnOffice}</span>
|
|
67
|
+
</button>
|
|
68
|
+
<button class="lc-chip" data-room="hallway">
|
|
69
|
+
<svg class="lc-chip-icon" viewBox="0 0 24 24"><path d="M19 19V5c0-1.1-.9-2-2-2H7c-1.1 0-2 .9-2 2v14H3v2h18v-2h-2zm-2 0H7V5h10v14z"/></svg>
|
|
70
|
+
<span>{lUI.btnHallway}</span>
|
|
71
|
+
</button>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="lc-row">
|
|
75
|
+
<div class="lc-input-group">
|
|
76
|
+
<label class="lc-input-label">{lUI.labelAmbient}</label>
|
|
77
|
+
<div class="lc-bulb-bar">
|
|
78
|
+
<button class="lc-ambient-btn" data-ambient="0.7">{lUI.btnAmbientCozy}</button>
|
|
79
|
+
<button class="lc-ambient-btn lc-ambient-active" data-ambient="1">{lUI.btnAmbientNormal}</button>
|
|
80
|
+
<button class="lc-ambient-btn" data-ambient="1.3">{lUI.btnAmbientBright}</button>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="lc-input-group lc-grow">
|
|
84
|
+
<label class="lc-input-label">{lUI.labelBulbType}</label>
|
|
85
|
+
<div class="lc-bulb-bar">
|
|
86
|
+
<button class="lc-bulb lc-bulb-active" data-bulb="led">
|
|
87
|
+
<span class="lc-bulb-dot" style="background:#22c55e;box-shadow:0 0 8px #22c55e"></span>
|
|
88
|
+
<span>{lUI.btnBulbLED}</span>
|
|
89
|
+
</button>
|
|
90
|
+
<button class="lc-bulb" data-bulb="cfl">
|
|
91
|
+
<span class="lc-bulb-dot" style="background:#f59e0b;box-shadow:0 0 8px #f59e0b"></span>
|
|
92
|
+
<span>{lUI.btnBulbCFL}</span>
|
|
93
|
+
</button>
|
|
94
|
+
<button class="lc-bulb" data-bulb="halogen">
|
|
95
|
+
<span class="lc-bulb-dot" style="background:#f97316;box-shadow:0 0 8px #f97316"></span>
|
|
96
|
+
<span>{lUI.btnBulbHalogen}</span>
|
|
97
|
+
</button>
|
|
98
|
+
<button class="lc-bulb" data-bulb="incandescent">
|
|
99
|
+
<span class="lc-bulb-dot" style="background:#ef4444;box-shadow:0 0 8px #ef4444"></span>
|
|
100
|
+
<span>{lUI.btnBulbIncandescent}</span>
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="lc-plan-wrap" id="lc-plan-wrap">
|
|
106
|
+
<svg class="lc-plan" viewBox="0 0 300 200" id="lc-plan"></svg>
|
|
107
|
+
</div>
|
|
108
|
+
<div class="lc-dial-row">
|
|
109
|
+
<div class="lc-dial-wrap">
|
|
110
|
+
<svg class="lc-dial" viewBox="0 0 120 70" id="lc-dial-lux"></svg>
|
|
111
|
+
<span class="lc-dial-label">{lUI.labelCurrentLux}</span>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
<div class="lc-status" id="lc-status">
|
|
115
|
+
<span class="lc-status-dot" id="lc-status-dot"></span>
|
|
116
|
+
<div class="lc-status-body">
|
|
117
|
+
<span class="lc-status-text" id="lc-status-text">{lUI.statusOptimal}</span>
|
|
118
|
+
<span class="lc-status-desc" id="lc-status-desc"></span>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="lc-victory" id="lc-victory">
|
|
122
|
+
<div class="lc-victory-title">{lUI.labelSummary}</div>
|
|
123
|
+
<div class="lc-victory-row">
|
|
124
|
+
<span class="lc-victory-label">{lUI.labelTotalLumens}</span>
|
|
125
|
+
<span class="lc-victory-value" id="lc-v-lumens">0</span>
|
|
126
|
+
</div>
|
|
127
|
+
<div class="lc-victory-row">
|
|
128
|
+
<span class="lc-victory-label">{lUI.labelSuggestedSetup}</span>
|
|
129
|
+
<span class="lc-victory-value" id="lc-v-setup">-</span>
|
|
130
|
+
</div>
|
|
131
|
+
<button class="lc-export-btn" id="lc-export">{lUI.btnExport}</button>
|
|
132
|
+
</div>
|
|
133
|
+
<div class="lc-manual-toggle" id="lc-manual-toggle">
|
|
134
|
+
<svg viewBox="0 0 24 24"><path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/></svg>
|
|
135
|
+
{lUI.labelManualAdjust}
|
|
136
|
+
</div>
|
|
137
|
+
<div class="lc-manual" id="lc-manual">
|
|
138
|
+
<div class="lc-row">
|
|
139
|
+
<div class="lc-input-group">
|
|
140
|
+
<label class="lc-input-label" for="lc-bw">{lUI.labelBulbWatt}</label>
|
|
141
|
+
<div class="lc-input-wrap">
|
|
142
|
+
<input id="lc-bw" type="number" value={INIT_W} min="1" max="200" step="1" class="lc-input" />
|
|
143
|
+
<span class="lc-input-unit">{lUI.unitWatt}</span>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="lc-input-group">
|
|
147
|
+
<label class="lc-input-label" for="lc-f">{lUI.labelFixtures}</label>
|
|
148
|
+
<div class="lc-input-wrap">
|
|
149
|
+
<input id="lc-f" type="number" value={INIT_F} min="1" max="50" step="1" class="lc-input" />
|
|
150
|
+
<span class="lc-input-unit">{lUI.unitBulbs}</span>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<script>
|
|
159
|
+
import { loadState, saveState, restoreInputs, runCalc, updateUnitLabels, convertValue } from './state';
|
|
160
|
+
import { drawPlan, drawDial, generatePDF } from './draw';
|
|
161
|
+
import type { State } from './state';
|
|
162
|
+
import type { LightingResult } from './logic';
|
|
163
|
+
|
|
164
|
+
const st: State = loadState();
|
|
165
|
+
|
|
166
|
+
function el(id: string) { return document.getElementById(id); }
|
|
167
|
+
function setTxt(id: string, v: string) { const e = el(id); if (e) e.textContent = v; }
|
|
168
|
+
function getNum(id: string): number { const e = el(id) as HTMLInputElement | null; return e ? parseFloat(e.value) || 0 : 0; }
|
|
169
|
+
function setVal(id: string, v: string) { const e = el(id) as HTMLInputElement | null; if (e) e.value = v; }
|
|
170
|
+
|
|
171
|
+
function updateStatus(r: LightingResult) {
|
|
172
|
+
const statusEl = el('lc-status');
|
|
173
|
+
const stText = el('lc-status-text');
|
|
174
|
+
const stDesc = el('lc-status-desc');
|
|
175
|
+
const planWrap = el('lc-plan-wrap');
|
|
176
|
+
if (statusEl) statusEl.className = 'lc-status ' + r.status;
|
|
177
|
+
if (planWrap) planWrap.className = 'lc-plan-wrap ' + r.status;
|
|
178
|
+
if (stText) {
|
|
179
|
+
if (r.status === 'optimal') stText.textContent = 'Perfect';
|
|
180
|
+
else if (r.status === 'insufficient') stText.textContent = 'Too Dim';
|
|
181
|
+
else stText.textContent = 'Too Bright';
|
|
182
|
+
}
|
|
183
|
+
if (stDesc) stDesc.textContent = r.sensoryContext;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function updateVictory(r: LightingResult) {
|
|
187
|
+
setTxt('lc-v-lumens', String(r.requiredLumens));
|
|
188
|
+
setTxt('lc-v-setup', r.suggestedProducts);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function render() {
|
|
192
|
+
const r = runCalc(st, getNum);
|
|
193
|
+
const f = st.unitSys === 'metric' ? 1 : 0.3048;
|
|
194
|
+
const w = getNum('lc-w') * f;
|
|
195
|
+
const l = getNum('lc-l') * f;
|
|
196
|
+
const plan = el('lc-plan') as SVGSVGElement | null;
|
|
197
|
+
if (plan) drawPlan(plan, { widthM: w, lengthM: l, optimalBulbs: r.optimalBulbs, luxRatio: r.luxRatio, ambiance: st.luxMultiplier, status: r.status });
|
|
198
|
+
const maxDial = Math.max(r.targetLux, r.currentLux) * 1.2;
|
|
199
|
+
const dialLux = el('lc-dial-lux') as SVGSVGElement | null;
|
|
200
|
+
if (dialLux) drawDial(dialLux, { value: r.currentLux, target: r.targetLux, max: maxDial, label: 'LUX' });
|
|
201
|
+
updateStatus(r);
|
|
202
|
+
updateVictory(r);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function persist() { saveState(st, getNum); }
|
|
206
|
+
|
|
207
|
+
function applyUnit(s: 'metric' | 'imperial') {
|
|
208
|
+
st.unitSys = s;
|
|
209
|
+
persist();
|
|
210
|
+
document.querySelectorAll('.lc-unit-btn').forEach((b) => b.classList.toggle('lc-unit-active', (b as HTMLElement).dataset.unit === s));
|
|
211
|
+
const toMetric = s === 'metric';
|
|
212
|
+
convertValue('lc-w', getNum, setVal, toMetric);
|
|
213
|
+
convertValue('lc-l', getNum, setVal, toMetric);
|
|
214
|
+
convertValue('lc-h', getNum, setVal, toMetric);
|
|
215
|
+
updateUnitLabels(st.unitSys, setTxt);
|
|
216
|
+
render();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function setRoom(type: string) {
|
|
220
|
+
st.roomType = type;
|
|
221
|
+
persist();
|
|
222
|
+
document.querySelectorAll('.lc-chip').forEach((b) => b.classList.toggle('lc-chip-active', (b as HTMLElement).dataset.room === type));
|
|
223
|
+
render();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function setBulb(type: string) {
|
|
227
|
+
st.bulbType = type;
|
|
228
|
+
persist();
|
|
229
|
+
document.querySelectorAll('.lc-bulb').forEach((b) => b.classList.toggle('lc-bulb-active', (b as HTMLElement).dataset.bulb === type));
|
|
230
|
+
render();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function setAmbient(m: string) {
|
|
234
|
+
st.luxMultiplier = parseFloat(m);
|
|
235
|
+
persist();
|
|
236
|
+
document.querySelectorAll('.lc-ambient-btn').forEach((b) => b.classList.toggle('lc-ambient-active', (b as HTMLElement).dataset.ambient === m));
|
|
237
|
+
render();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function init() {
|
|
241
|
+
restoreInputs(getNum, setVal);
|
|
242
|
+
updateUnitLabels(st.unitSys, setTxt);
|
|
243
|
+
document.querySelectorAll('.lc-unit-btn').forEach((b) => b.classList.toggle('lc-unit-active', (b as HTMLElement).dataset.unit === st.unitSys));
|
|
244
|
+
document.querySelectorAll('.lc-chip').forEach((b) => b.classList.toggle('lc-chip-active', (b as HTMLElement).dataset.room === st.roomType));
|
|
245
|
+
document.querySelectorAll('.lc-bulb').forEach((b) => b.classList.toggle('lc-bulb-active', (b as HTMLElement).dataset.bulb === st.bulbType));
|
|
246
|
+
document.querySelectorAll('.lc-ambient-btn').forEach((b) => b.classList.toggle('lc-ambient-active', (b as HTMLElement).dataset.ambient === String(st.luxMultiplier)));
|
|
247
|
+
const fInput = el('lc-f') as HTMLInputElement | null;
|
|
248
|
+
if (fInput && !fInput.value) {
|
|
249
|
+
const calc = runCalc(st, getNum);
|
|
250
|
+
setVal('lc-f', String(calc.optimalBulbs));
|
|
251
|
+
}
|
|
252
|
+
document.querySelectorAll('.lc-unit-btn').forEach((btn) => {
|
|
253
|
+
btn.addEventListener('click', () => {
|
|
254
|
+
const u = (btn as HTMLElement).dataset.unit as 'metric' | 'imperial';
|
|
255
|
+
if (u) applyUnit(u);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
document.querySelectorAll('.lc-chip').forEach((btn) => {
|
|
259
|
+
btn.addEventListener('click', () => {
|
|
260
|
+
const t = (btn as HTMLElement).dataset.room || 'living';
|
|
261
|
+
setRoom(t);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
document.querySelectorAll('.lc-bulb').forEach((btn) => {
|
|
265
|
+
btn.addEventListener('click', () => {
|
|
266
|
+
const t = (btn as HTMLElement).dataset.bulb || 'led';
|
|
267
|
+
setBulb(t);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
document.querySelectorAll('.lc-ambient-btn').forEach((btn) => {
|
|
271
|
+
btn.addEventListener('click', () => {
|
|
272
|
+
const m = (btn as HTMLElement).dataset.ambient || '1';
|
|
273
|
+
setAmbient(m);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
['lc-w', 'lc-l', 'lc-h', 'lc-bw', 'lc-f'].forEach((id) => {
|
|
277
|
+
const e = el(id);
|
|
278
|
+
if (e) e.addEventListener('input', () => { persist(); render(); });
|
|
279
|
+
});
|
|
280
|
+
const manualToggle = el('lc-manual-toggle');
|
|
281
|
+
const manual = el('lc-manual');
|
|
282
|
+
if (manualToggle && manual) {
|
|
283
|
+
manualToggle.addEventListener('click', () => {
|
|
284
|
+
manual.classList.toggle('lc-manual-open');
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
const exportBtn = el('lc-export');
|
|
288
|
+
if (exportBtn) {
|
|
289
|
+
exportBtn.addEventListener('click', () => {
|
|
290
|
+
const r = runCalc(st, getNum);
|
|
291
|
+
const unitLabel = st.unitSys === 'metric' ? 'm' : 'ft';
|
|
292
|
+
generatePDF({
|
|
293
|
+
roomType: st.roomType,
|
|
294
|
+
width: getNum('lc-w'),
|
|
295
|
+
length: getNum('lc-l'),
|
|
296
|
+
height: getNum('lc-h'),
|
|
297
|
+
r,
|
|
298
|
+
unitSys: st.unitSys,
|
|
299
|
+
unitLabel,
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
render();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
document.addEventListener('astro:page-load', init);
|
|
307
|
+
init();
|
|
308
|
+
</script>
|
|
@@ -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
|
+
}
|