@jjlmoya/utils-home 1.31.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 +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 +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/tools.ts +2 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
export interface LightingInput {
|
|
2
|
+
roomWidthM: number;
|
|
3
|
+
roomLengthM: number;
|
|
4
|
+
roomHeightM: number;
|
|
5
|
+
roomType: string;
|
|
6
|
+
bulbType: string;
|
|
7
|
+
bulbWatt: number;
|
|
8
|
+
fixtures: number;
|
|
9
|
+
luxMultiplier: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface LightingResult {
|
|
13
|
+
roomArea: number;
|
|
14
|
+
targetLux: number;
|
|
15
|
+
requiredLumens: number;
|
|
16
|
+
bulbLumens: number;
|
|
17
|
+
currentLux: number;
|
|
18
|
+
bulbsNeeded: number;
|
|
19
|
+
optimalBulbs: number;
|
|
20
|
+
optimalWatt: number;
|
|
21
|
+
utilizationFactor: number;
|
|
22
|
+
status: 'optimal' | 'insufficient' | 'excess';
|
|
23
|
+
luxRatio: number;
|
|
24
|
+
sensoryContext: string;
|
|
25
|
+
suggestedProducts: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const LUX_TARGETS: Record<string, number> = {
|
|
29
|
+
living: 150,
|
|
30
|
+
kitchen: 300,
|
|
31
|
+
bedroom: 100,
|
|
32
|
+
bathroom: 200,
|
|
33
|
+
office: 500,
|
|
34
|
+
hallway: 100,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const LUMENS_PER_WATT: Record<string, number> = {
|
|
38
|
+
led: 100,
|
|
39
|
+
cfl: 60,
|
|
40
|
+
halogen: 20,
|
|
41
|
+
incandescent: 15,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const UF_MAP: Record<string, number> = {
|
|
45
|
+
low: 0.55,
|
|
46
|
+
medium: 0.50,
|
|
47
|
+
high: 0.45,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function getUF(roomHeightM: number): number {
|
|
51
|
+
if (roomHeightM <= 2.5) return UF_MAP.low;
|
|
52
|
+
if (roomHeightM <= 3.5) return UF_MAP.medium;
|
|
53
|
+
return UF_MAP.high;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getSensoryContext(currentLux: number, targetLux: number): string {
|
|
57
|
+
const ratio = targetLux > 0 ? currentLux / targetLux : 0;
|
|
58
|
+
if (ratio < 0.3) return 'Like a dark corridor. You need significantly more light.';
|
|
59
|
+
if (ratio < 0.6) return 'Like a dim restaurant. Good for atmosphere, but not for daily tasks.';
|
|
60
|
+
if (ratio < 0.9) return 'Getting there. Like a cozy evening, but slightly too dark for comfort.';
|
|
61
|
+
if (ratio <= 1.3) return 'Perfectly balanced. Like a comfortable living room during the day.';
|
|
62
|
+
if (ratio <= 1.8) return 'Bright and energetic. Like a well-lit office or kitchen.';
|
|
63
|
+
return 'Very intense. Like a hospital or studio. Consider dimming for comfort.';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getSuggestedProducts(
|
|
67
|
+
bulbsNeeded: number,
|
|
68
|
+
bulbType: string,
|
|
69
|
+
optimalWatt: number,
|
|
70
|
+
): string {
|
|
71
|
+
const typeName = bulbType.toUpperCase();
|
|
72
|
+
const watt = Math.round(optimalWatt);
|
|
73
|
+
return `${bulbsNeeded} x ${typeName} ${watt}W bulbs ≈ ${bulbsNeeded * watt * (LUMENS_PER_WATT[bulbType] ?? 100)} lumens total`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getStatus(currentLux: number, targetLux: number): 'optimal' | 'insufficient' | 'excess' {
|
|
77
|
+
if (currentLux < targetLux * 0.9) return 'insufficient';
|
|
78
|
+
if (currentLux > targetLux * 1.3) return 'excess';
|
|
79
|
+
return 'optimal';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getLuxRatio(currentLux: number, targetLux: number): number {
|
|
83
|
+
return targetLux > 0 ? currentLux / targetLux : 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function computeLighting(i: LightingInput) {
|
|
87
|
+
const roomArea = i.roomWidthM * i.roomLengthM;
|
|
88
|
+
const baseLux = LUX_TARGETS[i.roomType] ?? 150;
|
|
89
|
+
const targetLux = Math.round(baseLux * i.luxMultiplier);
|
|
90
|
+
const utilizationFactor = getUF(i.roomHeightM);
|
|
91
|
+
const maintenanceFactor = 0.8;
|
|
92
|
+
const requiredLumens = Math.ceil((targetLux * roomArea) / (utilizationFactor * maintenanceFactor));
|
|
93
|
+
const bulbLumens = i.bulbWatt * (LUMENS_PER_WATT[i.bulbType] ?? 100);
|
|
94
|
+
const currentLumens = bulbLumens * i.fixtures;
|
|
95
|
+
const currentLux = Math.round(currentLumens * utilizationFactor * maintenanceFactor / roomArea);
|
|
96
|
+
const bulbsNeeded = Math.ceil(requiredLumens / (bulbLumens || 1));
|
|
97
|
+
const optimalBulbs = bulbsNeeded;
|
|
98
|
+
const optimalWatt = Math.ceil(requiredLumens / (optimalBulbs * (LUMENS_PER_WATT[i.bulbType] ?? 100)));
|
|
99
|
+
return {
|
|
100
|
+
roomArea, targetLux, requiredLumens, bulbLumens, currentLux,
|
|
101
|
+
bulbsNeeded, optimalBulbs, optimalWatt, utilizationFactor,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function calculateLighting(i: LightingInput): LightingResult {
|
|
106
|
+
const c = computeLighting(i);
|
|
107
|
+
const luxRatio = getLuxRatio(c.currentLux, c.targetLux);
|
|
108
|
+
const status = getStatus(c.currentLux, c.targetLux);
|
|
109
|
+
return {
|
|
110
|
+
...c,
|
|
111
|
+
status,
|
|
112
|
+
luxRatio,
|
|
113
|
+
sensoryContext: getSensoryContext(c.currentLux, c.targetLux),
|
|
114
|
+
suggestedProducts: getSuggestedProducts(c.optimalBulbs, i.bulbType, c.optimalWatt),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function fmt0(n: number): string { return String(Math.round(n)); }
|
|
119
|
+
export function fmt1(n: number): string { return n.toFixed(1); }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { lightingCalculator } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'en' } = Astro.props;
|
|
11
|
+
const content = await lightingCalculator.i18n[locale]?.();
|
|
12
|
+
if (!content) return null;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
{content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { calculateLighting } from './logic';
|
|
2
|
+
import type { LightingInput, LightingResult } from './logic';
|
|
3
|
+
|
|
4
|
+
const LS_KEY = 'lighting-calc';
|
|
5
|
+
|
|
6
|
+
export interface State {
|
|
7
|
+
unitSys: 'metric' | 'imperial';
|
|
8
|
+
roomType: string;
|
|
9
|
+
bulbType: string;
|
|
10
|
+
luxMultiplier: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const defaultState: State = {
|
|
14
|
+
unitSys: 'metric',
|
|
15
|
+
roomType: 'living',
|
|
16
|
+
bulbType: 'led',
|
|
17
|
+
luxMultiplier: 1,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function loadState(): State {
|
|
21
|
+
const s = localStorage.getItem(LS_KEY);
|
|
22
|
+
if (!s) return { ...defaultState };
|
|
23
|
+
try {
|
|
24
|
+
const v = JSON.parse(s);
|
|
25
|
+
return {
|
|
26
|
+
unitSys: v.unit || defaultState.unitSys,
|
|
27
|
+
roomType: v.room || defaultState.roomType,
|
|
28
|
+
bulbType: v.bulb || defaultState.bulbType,
|
|
29
|
+
luxMultiplier: typeof v.ambient === 'number' ? v.ambient : defaultState.luxMultiplier,
|
|
30
|
+
};
|
|
31
|
+
} catch {
|
|
32
|
+
return { ...defaultState };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function saveState(
|
|
37
|
+
st: State,
|
|
38
|
+
getNum: (id: string) => number,
|
|
39
|
+
) {
|
|
40
|
+
const v = {
|
|
41
|
+
unit: st.unitSys,
|
|
42
|
+
room: st.roomType,
|
|
43
|
+
bulb: st.bulbType,
|
|
44
|
+
ambient: st.luxMultiplier,
|
|
45
|
+
w: getNum('lc-w'),
|
|
46
|
+
l: getNum('lc-l'),
|
|
47
|
+
h: getNum('lc-h'),
|
|
48
|
+
bw: getNum('lc-bw'),
|
|
49
|
+
f: getNum('lc-f'),
|
|
50
|
+
};
|
|
51
|
+
localStorage.setItem(LS_KEY, JSON.stringify(v));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function restoreInputs(
|
|
55
|
+
getNum: (id: string) => number,
|
|
56
|
+
setVal: (id: string, v: string) => void,
|
|
57
|
+
) {
|
|
58
|
+
const s = localStorage.getItem(LS_KEY);
|
|
59
|
+
if (!s) return;
|
|
60
|
+
try {
|
|
61
|
+
const v = JSON.parse(s);
|
|
62
|
+
const ids = ['lc-w', 'lc-l', 'lc-h', 'lc-bw', 'lc-f'];
|
|
63
|
+
const keys = ['w', 'l', 'h', 'bw', 'f'];
|
|
64
|
+
ids.forEach((id, i) => {
|
|
65
|
+
if (v[keys[i]] !== undefined) setVal(id, String(v[keys[i]]));
|
|
66
|
+
});
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function factors(unitSys: 'metric' | 'imperial') {
|
|
71
|
+
return unitSys === 'metric'
|
|
72
|
+
? { rm: 1, hm: 1 }
|
|
73
|
+
: { rm: 0.3048, hm: 0.3048 };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function runCalc(
|
|
77
|
+
st: State,
|
|
78
|
+
getNum: (id: string) => number,
|
|
79
|
+
): LightingResult {
|
|
80
|
+
const f = factors(st.unitSys);
|
|
81
|
+
const input: LightingInput = {
|
|
82
|
+
roomWidthM: getNum('lc-w') * f.rm,
|
|
83
|
+
roomLengthM: getNum('lc-l') * f.rm,
|
|
84
|
+
roomHeightM: getNum('lc-h') * f.hm,
|
|
85
|
+
roomType: st.roomType,
|
|
86
|
+
bulbType: st.bulbType,
|
|
87
|
+
bulbWatt: getNum('lc-bw'),
|
|
88
|
+
fixtures: getNum('lc-f'),
|
|
89
|
+
luxMultiplier: st.luxMultiplier,
|
|
90
|
+
};
|
|
91
|
+
return calculateLighting(input);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function updateUnitLabels(
|
|
95
|
+
unitSys: 'metric' | 'imperial',
|
|
96
|
+
setTxt: (id: string, v: string) => void,
|
|
97
|
+
) {
|
|
98
|
+
const isM = unitSys === 'metric';
|
|
99
|
+
setTxt('lc-w-u', isM ? 'm' : 'ft');
|
|
100
|
+
setTxt('lc-l-u', isM ? 'm' : 'ft');
|
|
101
|
+
setTxt('lc-h-u', isM ? 'm' : 'ft');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function convertValue(
|
|
105
|
+
id: string,
|
|
106
|
+
getNum: (id: string) => number,
|
|
107
|
+
setVal: (id: string, v: string) => void,
|
|
108
|
+
toMetric: boolean,
|
|
109
|
+
) {
|
|
110
|
+
const factor = toMetric ? 0.3048 : 1 / 0.3048;
|
|
111
|
+
const v = getNum(id);
|
|
112
|
+
setVal(id, String(Math.round(v * factor * 10) / 10));
|
|
113
|
+
}
|
|
@@ -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
|
+
}
|
package/src/tools.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { VAMPIRE_DRAW_SIMULATOR_TOOL } from './tool/vampireDrawSimulator/index';
|
|
|
14
14
|
import { DESK_ERGONOMICS_TOOL } from './tool/deskErgonomics/index';
|
|
15
15
|
import { APPLIANCE_COST_CALCULATOR_TOOL } from './tool/applianceCostCalculator/index';
|
|
16
16
|
import { TILE_LAYOUT_CALCULATOR_TOOL } from './tool/tileLayoutCalculator/index';
|
|
17
|
+
import { LIGHTING_CALCULATOR_TOOL } from './tool/lightingCalculator/index';
|
|
17
18
|
|
|
18
19
|
export const ALL_TOOLS: ToolDefinition[] = [
|
|
19
20
|
QR_GENERATOR_TOOL,
|
|
@@ -30,5 +31,6 @@ export const ALL_TOOLS: ToolDefinition[] = [
|
|
|
30
31
|
DESK_ERGONOMICS_TOOL,
|
|
31
32
|
APPLIANCE_COST_CALCULATOR_TOOL,
|
|
32
33
|
TILE_LAYOUT_CALCULATOR_TOOL,
|
|
34
|
+
LIGHTING_CALCULATOR_TOOL,
|
|
33
35
|
];
|
|
34
36
|
|