@xivdyetools/core 1.3.7
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/LICENSE +37 -0
- package/README.md +400 -0
- package/dist/constants/index.d.ts +56 -0
- package/dist/constants/index.d.ts.map +1 -0
- package/dist/constants/index.js +103 -0
- package/dist/constants/index.js.map +1 -0
- package/dist/data/colors_xiv.json +3130 -0
- package/dist/data/locales/de.json +231 -0
- package/dist/data/locales/en.json +231 -0
- package/dist/data/locales/fr.json +231 -0
- package/dist/data/locales/ja.json +231 -0
- package/dist/data/locales/ko.json +233 -0
- package/dist/data/locales/zh.json +233 -0
- package/dist/data/presets.json +390 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/services/APIService.d.ts +246 -0
- package/dist/services/APIService.d.ts.map +1 -0
- package/dist/services/APIService.js +499 -0
- package/dist/services/APIService.js.map +1 -0
- package/dist/services/ColorService.d.ts +146 -0
- package/dist/services/ColorService.d.ts.map +1 -0
- package/dist/services/ColorService.js +209 -0
- package/dist/services/ColorService.js.map +1 -0
- package/dist/services/DyeService.d.ts +230 -0
- package/dist/services/DyeService.d.ts.map +1 -0
- package/dist/services/DyeService.js +326 -0
- package/dist/services/DyeService.js.map +1 -0
- package/dist/services/LocalizationService.d.ts +338 -0
- package/dist/services/LocalizationService.d.ts.map +1 -0
- package/dist/services/LocalizationService.js +449 -0
- package/dist/services/LocalizationService.js.map +1 -0
- package/dist/services/PaletteService.d.ts +137 -0
- package/dist/services/PaletteService.d.ts.map +1 -0
- package/dist/services/PaletteService.js +349 -0
- package/dist/services/PaletteService.js.map +1 -0
- package/dist/services/PresetService.d.ts +196 -0
- package/dist/services/PresetService.d.ts.map +1 -0
- package/dist/services/PresetService.js +261 -0
- package/dist/services/PresetService.js.map +1 -0
- package/dist/services/color/ColorAccessibility.d.ts +39 -0
- package/dist/services/color/ColorAccessibility.d.ts.map +1 -0
- package/dist/services/color/ColorAccessibility.js +71 -0
- package/dist/services/color/ColorAccessibility.js.map +1 -0
- package/dist/services/color/ColorConverter.d.ts +146 -0
- package/dist/services/color/ColorConverter.d.ts.map +1 -0
- package/dist/services/color/ColorConverter.js +393 -0
- package/dist/services/color/ColorConverter.js.map +1 -0
- package/dist/services/color/ColorManipulator.d.ts +36 -0
- package/dist/services/color/ColorManipulator.d.ts.map +1 -0
- package/dist/services/color/ColorManipulator.js +56 -0
- package/dist/services/color/ColorManipulator.js.map +1 -0
- package/dist/services/color/ColorblindnessSimulator.d.ts +35 -0
- package/dist/services/color/ColorblindnessSimulator.d.ts.map +1 -0
- package/dist/services/color/ColorblindnessSimulator.js +110 -0
- package/dist/services/color/ColorblindnessSimulator.js.map +1 -0
- package/dist/services/dye/DyeDatabase.d.ts +131 -0
- package/dist/services/dye/DyeDatabase.d.ts.map +1 -0
- package/dist/services/dye/DyeDatabase.js +367 -0
- package/dist/services/dye/DyeDatabase.js.map +1 -0
- package/dist/services/dye/DyeSearch.d.ts +55 -0
- package/dist/services/dye/DyeSearch.d.ts.map +1 -0
- package/dist/services/dye/DyeSearch.js +196 -0
- package/dist/services/dye/DyeSearch.js.map +1 -0
- package/dist/services/dye/HarmonyGenerator.d.ts +110 -0
- package/dist/services/dye/HarmonyGenerator.d.ts.map +1 -0
- package/dist/services/dye/HarmonyGenerator.js +221 -0
- package/dist/services/dye/HarmonyGenerator.js.map +1 -0
- package/dist/services/localization/LocaleLoader.d.ts +38 -0
- package/dist/services/localization/LocaleLoader.d.ts.map +1 -0
- package/dist/services/localization/LocaleLoader.js +83 -0
- package/dist/services/localization/LocaleLoader.js.map +1 -0
- package/dist/services/localization/LocaleRegistry.d.ts +73 -0
- package/dist/services/localization/LocaleRegistry.d.ts.map +1 -0
- package/dist/services/localization/LocaleRegistry.js +84 -0
- package/dist/services/localization/LocaleRegistry.js.map +1 -0
- package/dist/services/localization/TranslationProvider.d.ts +157 -0
- package/dist/services/localization/TranslationProvider.d.ts.map +1 -0
- package/dist/services/localization/TranslationProvider.js +289 -0
- package/dist/services/localization/TranslationProvider.js.map +1 -0
- package/dist/types/index.d.ts +409 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +87 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/logger.d.ts +84 -0
- package/dist/types/logger.d.ts.map +1 -0
- package/dist/types/logger.js +54 -0
- package/dist/types/logger.js.map +1 -0
- package/dist/utils/index.d.ts +441 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +577 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/kd-tree.d.ts +76 -0
- package/dist/utils/kd-tree.d.ts.map +1 -0
- package/dist/utils/kd-tree.js +195 -0
- package/dist/utils/kd-tree.js.map +1 -0
- package/dist/version.d.ts +11 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +11 -0
- package/dist/version.js.map +1 -0
- package/package.json +84 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dye Search
|
|
3
|
+
* Per R-4: Focused class for dye search and matching operations
|
|
4
|
+
* Handles finding dyes by name, category, color distance, etc.
|
|
5
|
+
*/
|
|
6
|
+
import { ColorConverter } from '../color/ColorConverter.js';
|
|
7
|
+
/**
|
|
8
|
+
* Dye search and matching utilities
|
|
9
|
+
* Per R-4: Single Responsibility - search operations only
|
|
10
|
+
*/
|
|
11
|
+
export class DyeSearch {
|
|
12
|
+
constructor(database) {
|
|
13
|
+
this.database = database;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Search dyes by name (case-insensitive, partial match)
|
|
17
|
+
*/
|
|
18
|
+
searchByName(query) {
|
|
19
|
+
this.database.ensureLoaded();
|
|
20
|
+
const lowerQuery = query.toLowerCase().trim();
|
|
21
|
+
if (lowerQuery.length === 0) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
const dyes = this.database.getDyesInternal();
|
|
25
|
+
return dyes.filter((dye) => dye.name.toLowerCase().includes(lowerQuery));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Search dyes by category
|
|
29
|
+
*/
|
|
30
|
+
searchByCategory(category) {
|
|
31
|
+
this.database.ensureLoaded();
|
|
32
|
+
const lowerCategory = category.toLowerCase();
|
|
33
|
+
const dyes = this.database.getDyesInternal();
|
|
34
|
+
return dyes.filter((dye) => dye.category.toLowerCase() === lowerCategory);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Filter dyes with optional exclusion list
|
|
38
|
+
*/
|
|
39
|
+
filterDyes(filter = {}) {
|
|
40
|
+
this.database.ensureLoaded();
|
|
41
|
+
let results = [...this.database.getDyesInternal()];
|
|
42
|
+
if (filter.category) {
|
|
43
|
+
results = results.filter((dye) => dye.category === filter.category);
|
|
44
|
+
}
|
|
45
|
+
if (filter.excludeIds && filter.excludeIds.length > 0) {
|
|
46
|
+
const excludeSet = new Set(filter.excludeIds);
|
|
47
|
+
results = results.filter((dye) => !excludeSet.has(dye.id));
|
|
48
|
+
}
|
|
49
|
+
if (filter.minPrice !== undefined) {
|
|
50
|
+
// Defensively handle undefined/null cost values
|
|
51
|
+
results = results.filter((dye) => (dye.cost ?? 0) >= filter.minPrice);
|
|
52
|
+
}
|
|
53
|
+
if (filter.maxPrice !== undefined) {
|
|
54
|
+
// Defensively handle undefined/null cost values
|
|
55
|
+
results = results.filter((dye) => (dye.cost ?? 0) <= filter.maxPrice);
|
|
56
|
+
}
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Find closest dye to a given hex color
|
|
61
|
+
* Per P-7: Uses k-d tree for O(log n) average case vs O(n) linear search
|
|
62
|
+
*/
|
|
63
|
+
findClosestDye(hex, excludeIds = []) {
|
|
64
|
+
this.database.ensureLoaded();
|
|
65
|
+
try {
|
|
66
|
+
const targetRgb = ColorConverter.hexToRgb(hex);
|
|
67
|
+
const targetPoint = {
|
|
68
|
+
x: targetRgb.r,
|
|
69
|
+
y: targetRgb.g,
|
|
70
|
+
z: targetRgb.b,
|
|
71
|
+
};
|
|
72
|
+
// Per P-7: Use k-d tree if available
|
|
73
|
+
const kdTree = this.database.getKdTree();
|
|
74
|
+
if (kdTree && !kdTree.isEmpty()) {
|
|
75
|
+
const excludeSet = new Set(excludeIds);
|
|
76
|
+
// CORE-BUG-005: Also exclude Facewear dyes in k-d tree path for consistency with fallback
|
|
77
|
+
const nearest = kdTree.nearestNeighbor(targetPoint, (data) => {
|
|
78
|
+
const dye = data;
|
|
79
|
+
return excludeSet.has(dye.id) || dye.category === 'Facewear';
|
|
80
|
+
});
|
|
81
|
+
if (nearest && nearest.data) {
|
|
82
|
+
return nearest.data;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Fallback to linear search (shouldn't happen if k-d tree is built)
|
|
86
|
+
let closest = null;
|
|
87
|
+
let minDistance = Infinity;
|
|
88
|
+
const excludeSet = new Set(excludeIds);
|
|
89
|
+
const dyes = this.database.getDyesInternal();
|
|
90
|
+
for (const dye of dyes) {
|
|
91
|
+
if (excludeSet.has(dye.id) || dye.category === 'Facewear') {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const distance = ColorConverter.getColorDistance(hex, dye.hex);
|
|
96
|
+
if (distance < minDistance) {
|
|
97
|
+
minDistance = distance;
|
|
98
|
+
closest = dye;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return closest;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Find dyes within a color distance threshold
|
|
113
|
+
* Per P-7: Uses k-d tree for efficient range queries
|
|
114
|
+
*/
|
|
115
|
+
findDyesWithinDistance(hex, maxDistance, limit) {
|
|
116
|
+
this.database.ensureLoaded();
|
|
117
|
+
try {
|
|
118
|
+
const targetRgb = ColorConverter.hexToRgb(hex);
|
|
119
|
+
const targetPoint = {
|
|
120
|
+
x: targetRgb.r,
|
|
121
|
+
y: targetRgb.g,
|
|
122
|
+
z: targetRgb.b,
|
|
123
|
+
};
|
|
124
|
+
// Per P-7: Use k-d tree if available
|
|
125
|
+
const kdTree = this.database.getKdTree();
|
|
126
|
+
if (kdTree && !kdTree.isEmpty()) {
|
|
127
|
+
const kdResults = kdTree.pointsWithinDistance(targetPoint, maxDistance);
|
|
128
|
+
// Convert to Dye array and apply limit
|
|
129
|
+
const dyes = kdResults.map((item) => item.point.data);
|
|
130
|
+
if (limit && limit > 0) {
|
|
131
|
+
return dyes.slice(0, limit);
|
|
132
|
+
}
|
|
133
|
+
return dyes;
|
|
134
|
+
}
|
|
135
|
+
// Fallback to linear search
|
|
136
|
+
const results = [];
|
|
137
|
+
const dyes = this.database.getDyesInternal();
|
|
138
|
+
for (const dye of dyes) {
|
|
139
|
+
try {
|
|
140
|
+
if (dye.category === 'Facewear') {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const distance = ColorConverter.getColorDistance(hex, dye.hex);
|
|
144
|
+
if (distance <= maxDistance) {
|
|
145
|
+
results.push({ dye, distance });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
results.sort((a, b) => a.distance - b.distance);
|
|
153
|
+
if (limit) {
|
|
154
|
+
results.splice(limit);
|
|
155
|
+
}
|
|
156
|
+
return results.map((item) => item.dye);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Get dyes sorted by brightness
|
|
164
|
+
*/
|
|
165
|
+
getDyesSortedByBrightness(ascending = true) {
|
|
166
|
+
this.database.ensureLoaded();
|
|
167
|
+
return [...this.database.getDyesInternal()].sort((a, b) => {
|
|
168
|
+
const brightnessA = a.hsv.v;
|
|
169
|
+
const brightnessB = b.hsv.v;
|
|
170
|
+
return ascending ? brightnessA - brightnessB : brightnessB - brightnessA;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Get dyes sorted by saturation
|
|
175
|
+
*/
|
|
176
|
+
getDyesSortedBySaturation(ascending = true) {
|
|
177
|
+
this.database.ensureLoaded();
|
|
178
|
+
return [...this.database.getDyesInternal()].sort((a, b) => {
|
|
179
|
+
const satA = a.hsv.s;
|
|
180
|
+
const satB = b.hsv.s;
|
|
181
|
+
return ascending ? satA - satB : satB - satA;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Get dyes sorted by hue
|
|
186
|
+
*/
|
|
187
|
+
getDyesSortedByHue(ascending = true) {
|
|
188
|
+
this.database.ensureLoaded();
|
|
189
|
+
return [...this.database.getDyesInternal()].sort((a, b) => {
|
|
190
|
+
const hueA = a.hsv.h;
|
|
191
|
+
const hueB = b.hsv.h;
|
|
192
|
+
return ascending ? hueA - hueB : hueB - hueA;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=DyeSearch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DyeSearch.js","sourceRoot":"","sources":["../../../src/services/dye/DyeSearch.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harmony Generator
|
|
3
|
+
* Per R-4: Focused class for color harmony generation
|
|
4
|
+
* Handles triadic, analogous, complementary, and other harmony schemes
|
|
5
|
+
*/
|
|
6
|
+
import type { Dye } from '../../types/index.js';
|
|
7
|
+
import type { DyeDatabase } from './DyeDatabase.js';
|
|
8
|
+
import type { DyeSearch } from './DyeSearch.js';
|
|
9
|
+
/**
|
|
10
|
+
* Color harmony generator
|
|
11
|
+
* Per R-4: Single Responsibility - harmony generation only
|
|
12
|
+
*/
|
|
13
|
+
export declare class HarmonyGenerator {
|
|
14
|
+
private database;
|
|
15
|
+
private search;
|
|
16
|
+
constructor(database: DyeDatabase, search: DyeSearch);
|
|
17
|
+
/**
|
|
18
|
+
* Find dyes that form a complementary color pair
|
|
19
|
+
* Excludes Facewear dyes (generic names like "Red", "Blue")
|
|
20
|
+
*/
|
|
21
|
+
findComplementaryPair(hex: string): Dye | null;
|
|
22
|
+
/**
|
|
23
|
+
* Find the closest dye that is not Facewear
|
|
24
|
+
* Iteratively searches until a non-Facewear dye is found
|
|
25
|
+
* CORE-BUG-003: Removed hard-coded limit of 10 iterations
|
|
26
|
+
*/
|
|
27
|
+
private findClosestNonFacewearDye;
|
|
28
|
+
/**
|
|
29
|
+
* Find analogous dyes (adjacent on color wheel)
|
|
30
|
+
* Returns dyes at ±angle degrees from the base color
|
|
31
|
+
*
|
|
32
|
+
* @remarks
|
|
33
|
+
* May return fewer dyes than expected if no suitable matches exist
|
|
34
|
+
* at the target hue positions or if all candidates are excluded.
|
|
35
|
+
*/
|
|
36
|
+
findAnalogousDyes(hex: string, angle?: number): Dye[];
|
|
37
|
+
/**
|
|
38
|
+
* Find triadic color scheme (colors 120° apart on color wheel)
|
|
39
|
+
*
|
|
40
|
+
* @remarks
|
|
41
|
+
* May return 0, 1, or 2 dyes depending on available matches.
|
|
42
|
+
* Use the length of the returned array to determine actual results.
|
|
43
|
+
*/
|
|
44
|
+
findTriadicDyes(hex: string): Dye[];
|
|
45
|
+
/**
|
|
46
|
+
* Find square color scheme (colors 90° apart on color wheel)
|
|
47
|
+
*
|
|
48
|
+
* @remarks
|
|
49
|
+
* May return fewer than 3 dyes if suitable matches are not found.
|
|
50
|
+
*/
|
|
51
|
+
findSquareDyes(hex: string): Dye[];
|
|
52
|
+
/**
|
|
53
|
+
* Find tetradic color scheme (two complementary pairs)
|
|
54
|
+
*
|
|
55
|
+
* @remarks
|
|
56
|
+
* May return fewer than 3 dyes if suitable matches are not found.
|
|
57
|
+
*/
|
|
58
|
+
findTetradicDyes(hex: string): Dye[];
|
|
59
|
+
/**
|
|
60
|
+
* Find monochromatic dyes (same hue, varying saturation/brightness)
|
|
61
|
+
* Excludes Facewear dyes (generic names like "Red", "Blue")
|
|
62
|
+
*/
|
|
63
|
+
findMonochromaticDyes(hex: string, limit?: number): Dye[];
|
|
64
|
+
/**
|
|
65
|
+
* Find compound harmony (analogous + complementary)
|
|
66
|
+
*
|
|
67
|
+
* @remarks
|
|
68
|
+
* May return fewer than 3 dyes if suitable matches are not found.
|
|
69
|
+
*/
|
|
70
|
+
findCompoundDyes(hex: string): Dye[];
|
|
71
|
+
/**
|
|
72
|
+
* Find split-complementary harmony (±30° from the complementary hue)
|
|
73
|
+
*
|
|
74
|
+
* @remarks
|
|
75
|
+
* May return 0, 1, or 2 dyes depending on available matches.
|
|
76
|
+
*/
|
|
77
|
+
findSplitComplementaryDyes(hex: string): Dye[];
|
|
78
|
+
/**
|
|
79
|
+
* Find shades (similar tones, ±15°)
|
|
80
|
+
*
|
|
81
|
+
* @remarks
|
|
82
|
+
* May return 0, 1, or 2 dyes depending on available matches.
|
|
83
|
+
*/
|
|
84
|
+
findShadesDyes(hex: string): Dye[];
|
|
85
|
+
/**
|
|
86
|
+
* Generic helper for hue-based harmonies
|
|
87
|
+
*
|
|
88
|
+
* @remarks
|
|
89
|
+
* This method may return fewer dyes than the number of offsets provided.
|
|
90
|
+
* This can happen when:
|
|
91
|
+
* - No dyes exist near the target hue position
|
|
92
|
+
* - All candidate dyes are in the 'Facewear' category (excluded)
|
|
93
|
+
* - The same dye would be selected multiple times (prevented by usedDyeIds)
|
|
94
|
+
*
|
|
95
|
+
* Consumers should check the returned array length rather than assuming
|
|
96
|
+
* a fixed number of results.
|
|
97
|
+
*
|
|
98
|
+
* @param hex - Base hex color
|
|
99
|
+
* @param offsets - Array of hue offsets in degrees
|
|
100
|
+
* @param options - Options including tolerance (default: 45°)
|
|
101
|
+
* @returns Array of matched dyes (may be shorter than offsets array)
|
|
102
|
+
*/
|
|
103
|
+
private findHarmonyDyesByOffsets;
|
|
104
|
+
/**
|
|
105
|
+
* Find closest dye by hue difference with graceful fallback
|
|
106
|
+
* Per P-2: Uses hue-indexed map for 70-90% speedup
|
|
107
|
+
*/
|
|
108
|
+
private findClosestDyeByHue;
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=HarmonyGenerator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HarmonyGenerator.d.ts","sourceRoot":"","sources":["../../../src/services/dye/HarmonyGenerator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAC;AAEhD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD;;;GAGG;AACH,qBAAa,gBAAgB;IAEzB,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,MAAM;gBADN,QAAQ,EAAE,WAAW,EACrB,MAAM,EAAE,SAAS;IAG3B;;;OAGG;IACH,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI;IAa9C;;;;OAIG;IACH,OAAO,CAAC,yBAAyB;IAoBjC;;;;;;;OAOG;IACH,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW,GAAG,GAAG,EAAE;IAKzD;;;;;;OAMG;IACH,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,EAAE;IAInC;;;;;OAKG;IACH,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,EAAE;IAIlC;;;;;OAKG;IACH,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,EAAE;IAKpC;;;OAGG;IACH,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,GAAE,MAAU,GAAG,GAAG,EAAE;IAkC5D;;;;;OAKG;IACH,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,EAAE;IAKpC;;;;;OAKG;IACH,0BAA0B,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,EAAE;IAI9C;;;;;OAKG;IACH,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,EAAE;IAOlC;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,wBAAwB;IA2BhC;;;OAGG;IACH,OAAO,CAAC,mBAAmB;CAsC5B"}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harmony Generator
|
|
3
|
+
* Per R-4: Focused class for color harmony generation
|
|
4
|
+
* Handles triadic, analogous, complementary, and other harmony schemes
|
|
5
|
+
*/
|
|
6
|
+
import { ColorManipulator } from '../color/ColorManipulator.js';
|
|
7
|
+
/**
|
|
8
|
+
* Color harmony generator
|
|
9
|
+
* Per R-4: Single Responsibility - harmony generation only
|
|
10
|
+
*/
|
|
11
|
+
export class HarmonyGenerator {
|
|
12
|
+
constructor(database, search) {
|
|
13
|
+
this.database = database;
|
|
14
|
+
this.search = search;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Find dyes that form a complementary color pair
|
|
18
|
+
* Excludes Facewear dyes (generic names like "Red", "Blue")
|
|
19
|
+
*/
|
|
20
|
+
findComplementaryPair(hex) {
|
|
21
|
+
this.database.ensureLoaded();
|
|
22
|
+
try {
|
|
23
|
+
const complementaryHex = ColorManipulator.invert(hex);
|
|
24
|
+
// Find closest dye, excluding Facewear
|
|
25
|
+
return this.findClosestNonFacewearDye(complementaryHex);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Invalid hex color
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Find the closest dye that is not Facewear
|
|
34
|
+
* Iteratively searches until a non-Facewear dye is found
|
|
35
|
+
* CORE-BUG-003: Removed hard-coded limit of 10 iterations
|
|
36
|
+
*/
|
|
37
|
+
findClosestNonFacewearDye(hex, excludeIds = []) {
|
|
38
|
+
const allExcluded = [...excludeIds];
|
|
39
|
+
// Get total dye count to avoid infinite loops while ensuring we check all dyes if needed
|
|
40
|
+
const totalDyes = this.database.getDyesInternal().length;
|
|
41
|
+
for (let i = 0; i < totalDyes; i++) {
|
|
42
|
+
const candidate = this.search.findClosestDye(hex, allExcluded);
|
|
43
|
+
if (!candidate)
|
|
44
|
+
return null;
|
|
45
|
+
if (candidate.category !== 'Facewear') {
|
|
46
|
+
return candidate;
|
|
47
|
+
}
|
|
48
|
+
// This candidate is Facewear, exclude it and try again
|
|
49
|
+
allExcluded.push(candidate.id);
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Find analogous dyes (adjacent on color wheel)
|
|
55
|
+
* Returns dyes at ±angle degrees from the base color
|
|
56
|
+
*
|
|
57
|
+
* @remarks
|
|
58
|
+
* May return fewer dyes than expected if no suitable matches exist
|
|
59
|
+
* at the target hue positions or if all candidates are excluded.
|
|
60
|
+
*/
|
|
61
|
+
findAnalogousDyes(hex, angle = 30) {
|
|
62
|
+
// Use harmony helper to find dyes at +angle and -angle positions
|
|
63
|
+
return this.findHarmonyDyesByOffsets(hex, [angle, -angle]);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Find triadic color scheme (colors 120° apart on color wheel)
|
|
67
|
+
*
|
|
68
|
+
* @remarks
|
|
69
|
+
* May return 0, 1, or 2 dyes depending on available matches.
|
|
70
|
+
* Use the length of the returned array to determine actual results.
|
|
71
|
+
*/
|
|
72
|
+
findTriadicDyes(hex) {
|
|
73
|
+
return this.findHarmonyDyesByOffsets(hex, [120, 240]);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Find square color scheme (colors 90° apart on color wheel)
|
|
77
|
+
*
|
|
78
|
+
* @remarks
|
|
79
|
+
* May return fewer than 3 dyes if suitable matches are not found.
|
|
80
|
+
*/
|
|
81
|
+
findSquareDyes(hex) {
|
|
82
|
+
return this.findHarmonyDyesByOffsets(hex, [90, 180, 270]);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Find tetradic color scheme (two complementary pairs)
|
|
86
|
+
*
|
|
87
|
+
* @remarks
|
|
88
|
+
* May return fewer than 3 dyes if suitable matches are not found.
|
|
89
|
+
*/
|
|
90
|
+
findTetradicDyes(hex) {
|
|
91
|
+
// Two adjacent hues + their complements (e.g., base+60 and base+240)
|
|
92
|
+
return this.findHarmonyDyesByOffsets(hex, [60, 180, 240]);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Find monochromatic dyes (same hue, varying saturation/brightness)
|
|
96
|
+
* Excludes Facewear dyes (generic names like "Red", "Blue")
|
|
97
|
+
*/
|
|
98
|
+
findMonochromaticDyes(hex, limit = 6) {
|
|
99
|
+
this.database.ensureLoaded();
|
|
100
|
+
const baseDye = this.findClosestNonFacewearDye(hex);
|
|
101
|
+
if (!baseDye)
|
|
102
|
+
return [];
|
|
103
|
+
const baseHue = baseDye.hsv.h;
|
|
104
|
+
const results = [];
|
|
105
|
+
const dyes = this.database.getDyesInternal();
|
|
106
|
+
// Find dyes with similar hue but different saturation/value
|
|
107
|
+
for (const dye of dyes) {
|
|
108
|
+
// Skip Facewear dyes (generic names)
|
|
109
|
+
if (dye.category === 'Facewear')
|
|
110
|
+
continue;
|
|
111
|
+
const hueDiff = Math.min(Math.abs(dye.hsv.h - baseHue), 360 - Math.abs(dye.hsv.h - baseHue));
|
|
112
|
+
// Hue must be very close (within ±15°)
|
|
113
|
+
if (hueDiff <= 15 && dye.id !== baseDye.id) {
|
|
114
|
+
// Calculate difference in saturation and value
|
|
115
|
+
const satDiff = Math.abs(dye.hsv.s - baseDye.hsv.s);
|
|
116
|
+
const valDiff = Math.abs(dye.hsv.v - baseDye.hsv.v);
|
|
117
|
+
const satValDiff = satDiff + valDiff;
|
|
118
|
+
results.push({ dye, satValDiff });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Sort by saturation/value difference (prefer more variety)
|
|
122
|
+
results.sort((a, b) => b.satValDiff - a.satValDiff);
|
|
123
|
+
return results.slice(0, limit).map((item) => item.dye);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Find compound harmony (analogous + complementary)
|
|
127
|
+
*
|
|
128
|
+
* @remarks
|
|
129
|
+
* May return fewer than 3 dyes if suitable matches are not found.
|
|
130
|
+
*/
|
|
131
|
+
findCompoundDyes(hex) {
|
|
132
|
+
// ±30° from base + complement
|
|
133
|
+
return this.findHarmonyDyesByOffsets(hex, [30, -30, 180], { tolerance: 35 });
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Find split-complementary harmony (±30° from the complementary hue)
|
|
137
|
+
*
|
|
138
|
+
* @remarks
|
|
139
|
+
* May return 0, 1, or 2 dyes depending on available matches.
|
|
140
|
+
*/
|
|
141
|
+
findSplitComplementaryDyes(hex) {
|
|
142
|
+
return this.findHarmonyDyesByOffsets(hex, [150, 210]);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Find shades (similar tones, ±15°)
|
|
146
|
+
*
|
|
147
|
+
* @remarks
|
|
148
|
+
* May return 0, 1, or 2 dyes depending on available matches.
|
|
149
|
+
*/
|
|
150
|
+
findShadesDyes(hex) {
|
|
151
|
+
this.database.ensureLoaded();
|
|
152
|
+
// Use tighter tolerance (5°) for shades to ensure results are close to target hue
|
|
153
|
+
return this.findHarmonyDyesByOffsets(hex, [15, -15], { tolerance: 5 });
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Generic helper for hue-based harmonies
|
|
157
|
+
*
|
|
158
|
+
* @remarks
|
|
159
|
+
* This method may return fewer dyes than the number of offsets provided.
|
|
160
|
+
* This can happen when:
|
|
161
|
+
* - No dyes exist near the target hue position
|
|
162
|
+
* - All candidate dyes are in the 'Facewear' category (excluded)
|
|
163
|
+
* - The same dye would be selected multiple times (prevented by usedDyeIds)
|
|
164
|
+
*
|
|
165
|
+
* Consumers should check the returned array length rather than assuming
|
|
166
|
+
* a fixed number of results.
|
|
167
|
+
*
|
|
168
|
+
* @param hex - Base hex color
|
|
169
|
+
* @param offsets - Array of hue offsets in degrees
|
|
170
|
+
* @param options - Options including tolerance (default: 45°)
|
|
171
|
+
* @returns Array of matched dyes (may be shorter than offsets array)
|
|
172
|
+
*/
|
|
173
|
+
findHarmonyDyesByOffsets(hex, offsets, options = {}) {
|
|
174
|
+
this.database.ensureLoaded();
|
|
175
|
+
const baseDye = this.search.findClosestDye(hex);
|
|
176
|
+
if (!baseDye)
|
|
177
|
+
return [];
|
|
178
|
+
const usedDyeIds = new Set([baseDye.id]);
|
|
179
|
+
const results = [];
|
|
180
|
+
const baseHue = baseDye.hsv.h;
|
|
181
|
+
const tolerance = options.tolerance ?? 45;
|
|
182
|
+
for (const offset of offsets) {
|
|
183
|
+
const targetHue = (baseHue + offset + 360) % 360;
|
|
184
|
+
const match = this.findClosestDyeByHue(targetHue, usedDyeIds, tolerance);
|
|
185
|
+
if (match) {
|
|
186
|
+
results.push(match);
|
|
187
|
+
usedDyeIds.add(match.id);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return results;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Find closest dye by hue difference with graceful fallback
|
|
194
|
+
* Per P-2: Uses hue-indexed map for 70-90% speedup
|
|
195
|
+
*/
|
|
196
|
+
findClosestDyeByHue(targetHue, usedIds, tolerance) {
|
|
197
|
+
let withinTolerance = null;
|
|
198
|
+
let bestOverall = null;
|
|
199
|
+
// Per P-2: Only search relevant hue buckets instead of all dyes
|
|
200
|
+
const bucketsToSearch = this.database.getHueBucketsToSearch(targetHue, tolerance);
|
|
201
|
+
for (const bucket of bucketsToSearch) {
|
|
202
|
+
const dyesInBucket = this.database.getDyesByHueBucket(bucket);
|
|
203
|
+
for (const dye of dyesInBucket) {
|
|
204
|
+
if (usedIds.has(dye.id) || dye.category === 'Facewear') {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const diff = Math.min(Math.abs(dye.hsv.h - targetHue), 360 - Math.abs(dye.hsv.h - targetHue));
|
|
208
|
+
if (!bestOverall || diff < bestOverall.diff) {
|
|
209
|
+
bestOverall = { dye, diff };
|
|
210
|
+
}
|
|
211
|
+
if (diff <= tolerance) {
|
|
212
|
+
if (!withinTolerance || diff < withinTolerance.diff) {
|
|
213
|
+
withinTolerance = { dye, diff };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return withinTolerance?.dye ?? bestOverall?.dye ?? null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=HarmonyGenerator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HarmonyGenerator.js","sourceRoot":"","sources":["../../../src/services/dye/HarmonyGenerator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocaleLoader - Loads locale JSON files
|
|
3
|
+
*
|
|
4
|
+
* Per R-4: Single Responsibility - locale file loading only
|
|
5
|
+
* Uses static imports for browser/bundler compatibility
|
|
6
|
+
*
|
|
7
|
+
* @module services/localization
|
|
8
|
+
*/
|
|
9
|
+
import type { LocaleCode, LocaleData } from '../../types/index.js';
|
|
10
|
+
/**
|
|
11
|
+
* Loads locale data from pre-bundled JSON files
|
|
12
|
+
*/
|
|
13
|
+
export declare class LocaleLoader {
|
|
14
|
+
/**
|
|
15
|
+
* Load locale data from pre-bundled JSON
|
|
16
|
+
*
|
|
17
|
+
* @param locale - Locale code to load
|
|
18
|
+
* @returns Locale data
|
|
19
|
+
* @throws {AppError} If locale file fails to load or is invalid
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* const loader = new LocaleLoader();
|
|
24
|
+
* const jaData = loader.loadLocale('ja');
|
|
25
|
+
* console.log(jaData.labels.dye); // "カララント:"
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
loadLocale(locale: LocaleCode): LocaleData;
|
|
29
|
+
/**
|
|
30
|
+
* Validate locale data structure
|
|
31
|
+
*
|
|
32
|
+
* @param data - Unknown data to validate
|
|
33
|
+
* @returns true if data matches LocaleData interface
|
|
34
|
+
* @private
|
|
35
|
+
*/
|
|
36
|
+
private isValidLocaleData;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=LocaleLoader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocaleLoader.d.ts","sourceRoot":"","sources":["../../../src/services/localization/LocaleLoader.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAqBnE;;GAEG;AACH,qBAAa,YAAY;IACvB;;;;;;;;;;;;;OAaG;IACH,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,UAAU;IAuB1C;;;;;;OAMG;IACH,OAAO,CAAC,iBAAiB;CAmB1B"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocaleLoader - Loads locale JSON files
|
|
3
|
+
*
|
|
4
|
+
* Per R-4: Single Responsibility - locale file loading only
|
|
5
|
+
* Uses static imports for browser/bundler compatibility
|
|
6
|
+
*
|
|
7
|
+
* @module services/localization
|
|
8
|
+
*/
|
|
9
|
+
import { AppError, ErrorCode } from '../../types/index.js';
|
|
10
|
+
// Static imports for all locale files (bundler-compatible)
|
|
11
|
+
import enLocale from '../../data/locales/en.json' with { type: 'json' };
|
|
12
|
+
import jaLocale from '../../data/locales/ja.json' with { type: 'json' };
|
|
13
|
+
import deLocale from '../../data/locales/de.json' with { type: 'json' };
|
|
14
|
+
import frLocale from '../../data/locales/fr.json' with { type: 'json' };
|
|
15
|
+
import koLocale from '../../data/locales/ko.json' with { type: 'json' };
|
|
16
|
+
import zhLocale from '../../data/locales/zh.json' with { type: 'json' };
|
|
17
|
+
// Map of locale codes to pre-loaded data
|
|
18
|
+
const localeMap = {
|
|
19
|
+
en: enLocale,
|
|
20
|
+
ja: jaLocale,
|
|
21
|
+
de: deLocale,
|
|
22
|
+
fr: frLocale,
|
|
23
|
+
ko: koLocale,
|
|
24
|
+
zh: zhLocale,
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Loads locale data from pre-bundled JSON files
|
|
28
|
+
*/
|
|
29
|
+
export class LocaleLoader {
|
|
30
|
+
/**
|
|
31
|
+
* Load locale data from pre-bundled JSON
|
|
32
|
+
*
|
|
33
|
+
* @param locale - Locale code to load
|
|
34
|
+
* @returns Locale data
|
|
35
|
+
* @throws {AppError} If locale file fails to load or is invalid
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* const loader = new LocaleLoader();
|
|
40
|
+
* const jaData = loader.loadLocale('ja');
|
|
41
|
+
* console.log(jaData.labels.dye); // "カララント:"
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
loadLocale(locale) {
|
|
45
|
+
try {
|
|
46
|
+
const data = localeMap[locale];
|
|
47
|
+
if (!data) {
|
|
48
|
+
throw new Error(`Locale "${locale}" not found in bundled locales`);
|
|
49
|
+
}
|
|
50
|
+
// Validate structure
|
|
51
|
+
if (!this.isValidLocaleData(data)) {
|
|
52
|
+
throw new Error(`Invalid locale data structure for ${locale}`);
|
|
53
|
+
}
|
|
54
|
+
return data;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
throw new AppError(ErrorCode.LOCALE_LOAD_FAILED, `Failed to load locale "${locale}": ${error instanceof Error ? error.message : 'Unknown error'}`, 'error');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Validate locale data structure
|
|
62
|
+
*
|
|
63
|
+
* @param data - Unknown data to validate
|
|
64
|
+
* @returns true if data matches LocaleData interface
|
|
65
|
+
* @private
|
|
66
|
+
*/
|
|
67
|
+
isValidLocaleData(data) {
|
|
68
|
+
if (!data || typeof data !== 'object') {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const localeData = data;
|
|
72
|
+
return (typeof localeData.locale === 'string' &&
|
|
73
|
+
typeof localeData.meta === 'object' &&
|
|
74
|
+
typeof localeData.labels === 'object' &&
|
|
75
|
+
typeof localeData.dyeNames === 'object' &&
|
|
76
|
+
typeof localeData.categories === 'object' &&
|
|
77
|
+
typeof localeData.acquisitions === 'object' &&
|
|
78
|
+
Array.isArray(localeData.metallicDyeIds) &&
|
|
79
|
+
typeof localeData.harmonyTypes === 'object' &&
|
|
80
|
+
typeof localeData.visionTypes === 'object');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=LocaleLoader.js.map
|