@twinfinity/printing 6.0.1-ci.28782-beta → 6.0.2-beta
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/README.md +3 -3
- package/dist/BuildInfo.js +3 -3
- package/dist/BuildInfo.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/layout/analyzeTemplate.js.map +1 -1
- package/dist/layout/clipUtils.js.map +1 -1
- package/dist/layout/colorConstants.js.map +1 -1
- package/dist/layout/elementKinds.js.map +1 -1
- package/dist/layout/exportDrawing.js.map +1 -1
- package/dist/layout/filters.js.map +1 -1
- package/dist/layout/geometryConstants.js.map +1 -1
- package/dist/layout/index.js.map +1 -1
- package/dist/layout/itemBuilders.js.map +1 -1
- package/dist/layout/labelsRender.js.map +1 -1
- package/dist/layout/legendConstants.js.map +1 -1
- package/dist/layout/legendRender.js.map +1 -1
- package/dist/layout/legendRows.js.map +1 -1
- package/dist/layout/legendUtils.js.map +1 -1
- package/dist/layout/metadata.js.map +1 -1
- package/dist/layout/model.js.map +1 -1
- package/dist/layout/options.js.map +1 -1
- package/dist/layout/presets.js.map +1 -1
- package/dist/layout/qrRender.js.map +1 -1
- package/dist/layout/renderDrawing.js.map +1 -1
- package/dist/layout/renderUtils.js.map +1 -1
- package/dist/layout/sectionValidation.js.map +1 -1
- package/dist/layout/templateVariables.js.map +1 -1
- package/dist/layout/typeGuards.js.map +1 -1
- package/dist/layout/types.js.map +1 -1
- package/dist/layout/utils.js.map +1 -1
- package/dist/layout/validation.js.map +1 -1
- package/dist/layout/validationHelpers.js.map +1 -1
- package/dist/layout/validationUtils.js.map +1 -1
- package/dist/layout/viewportPrep.js.map +1 -1
- package/dist/layout/viewportRender.js.map +1 -1
- package/dist/layout/viewportUtils.js.map +1 -1
- package/dist/layout/warnings.js.map +1 -1
- package/dist/printToPdf.js.map +1 -1
- package/dist/printToSvg.js.map +1 -1
- package/dist/sections.js.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/src/BuildInfo.ts +41 -41
- package/src/index.ts +10 -10
- package/src/layout/analyzeTemplate.ts +218 -218
- package/src/layout/clipUtils.ts +193 -193
- package/src/layout/colorConstants.ts +16 -16
- package/src/layout/elementKinds.ts +35 -35
- package/src/layout/exportDrawing.ts +206 -206
- package/src/layout/filters.ts +80 -80
- package/src/layout/geometryConstants.ts +11 -11
- package/src/layout/index.ts +671 -671
- package/src/layout/itemBuilders.ts +159 -159
- package/src/layout/labelsRender.ts +170 -170
- package/src/layout/legendConstants.ts +11 -11
- package/src/layout/legendRender.ts +543 -543
- package/src/layout/legendRows.ts +62 -62
- package/src/layout/legendUtils.ts +144 -144
- package/src/layout/metadata.ts +210 -210
- package/src/layout/model.ts +372 -372
- package/src/layout/options.ts +17 -17
- package/src/layout/presets.ts +51 -51
- package/src/layout/qrRender.ts +126 -126
- package/src/layout/qrcode.d.ts +2 -2
- package/src/layout/renderDrawing.ts +710 -710
- package/src/layout/renderUtils.ts +138 -138
- package/src/layout/sectionValidation.ts +2 -2
- package/src/layout/templateVariables.ts +317 -317
- package/src/layout/typeGuards.ts +9 -9
- package/src/layout/types.ts +835 -835
- package/src/layout/utils.ts +85 -85
- package/src/layout/validation.ts +392 -392
- package/src/layout/validationHelpers.ts +59 -59
- package/src/layout/validationUtils.ts +6 -6
- package/src/layout/viewportPrep.ts +351 -351
- package/src/layout/viewportRender.ts +442 -442
- package/src/layout/viewportUtils.ts +50 -50
- package/src/layout/warnings.ts +3 -3
- package/src/printToPdf.ts +108 -108
- package/src/printToSvg.ts +188 -188
- package/src/sections.ts +1019 -1019
- package/src/types.ts +49 -49
package/src/layout/legendRows.ts
CHANGED
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
import { getPathValue } from './renderUtils';
|
|
2
|
-
import { parseLegendColor } from './legendUtils';
|
|
3
|
-
|
|
4
|
-
import { isRecord, getProp } from './typeGuards';
|
|
5
|
-
|
|
6
|
-
export interface LegendRowOptions {
|
|
7
|
-
colorKeyPath?: string;
|
|
8
|
-
classPath?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface LegendRow {
|
|
12
|
-
productId?: string;
|
|
13
|
-
productName?: string;
|
|
14
|
-
area?: number | string | null;
|
|
15
|
-
className?: string | null;
|
|
16
|
-
x?: number;
|
|
17
|
-
z?: number;
|
|
18
|
-
properties?: unknown;
|
|
19
|
-
propertySets?: unknown;
|
|
20
|
-
color?: [number, number, number, number];
|
|
21
|
-
[key: string]: unknown;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Build standard legend rows from product items.
|
|
26
|
-
* Expected fields on input item:
|
|
27
|
-
* - productId / productName / area
|
|
28
|
-
* - x / z (model coords)
|
|
29
|
-
* - properties / propertySets
|
|
30
|
-
* - gid (fallback id)
|
|
31
|
-
*/
|
|
32
|
-
export const buildLegendRows = (items: unknown[], options?: LegendRowOptions): LegendRow[] => {
|
|
33
|
-
const colorKeyPath = options?.colorKeyPath ?? 'productId';
|
|
34
|
-
return (Array.isArray(items) ? items : []).map((item): LegendRow => {
|
|
35
|
-
const colorKey = getPathValue(item, colorKeyPath) ?? getProp(item, 'gid') ?? JSON.stringify(item);
|
|
36
|
-
const itemColor = getProp(item, 'color');
|
|
37
|
-
const color = parseLegendColor(itemColor, String(colorKey));
|
|
38
|
-
const classPath = options?.classPath ?? 'ifcClass';
|
|
39
|
-
const className = (() => {
|
|
40
|
-
const explicit = getPathValue(item, classPath);
|
|
41
|
-
if (explicit !== undefined && explicit !== null) return String(explicit);
|
|
42
|
-
const propSets = getProp(item, 'propertySets');
|
|
43
|
-
if (isRecord(propSets)) {
|
|
44
|
-
for (const set of Object.values(propSets)) {
|
|
45
|
-
const ifcClassProp = isRecord(set) ? getProp(set, 'ifcClass') : null;
|
|
46
|
-
const candidate = isRecord(ifcClassProp) ? getProp(ifcClassProp, 'value') : null;
|
|
47
|
-
if (candidate !== undefined && candidate !== null) return String(candidate);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return null;
|
|
51
|
-
})();
|
|
52
|
-
const areaVal = getProp(item, 'area');
|
|
53
|
-
const area = typeof areaVal === 'number' || typeof areaVal === 'string' ? areaVal : null;
|
|
54
|
-
const baseObj = isRecord(item) ? (item as Record<string, unknown>) : {};
|
|
55
|
-
return {
|
|
56
|
-
...baseObj,
|
|
57
|
-
className,
|
|
58
|
-
area,
|
|
59
|
-
color
|
|
60
|
-
};
|
|
61
|
-
});
|
|
62
|
-
};
|
|
1
|
+
import { getPathValue } from './renderUtils';
|
|
2
|
+
import { parseLegendColor } from './legendUtils';
|
|
3
|
+
|
|
4
|
+
import { isRecord, getProp } from './typeGuards';
|
|
5
|
+
|
|
6
|
+
export interface LegendRowOptions {
|
|
7
|
+
colorKeyPath?: string;
|
|
8
|
+
classPath?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface LegendRow {
|
|
12
|
+
productId?: string;
|
|
13
|
+
productName?: string;
|
|
14
|
+
area?: number | string | null;
|
|
15
|
+
className?: string | null;
|
|
16
|
+
x?: number;
|
|
17
|
+
z?: number;
|
|
18
|
+
properties?: unknown;
|
|
19
|
+
propertySets?: unknown;
|
|
20
|
+
color?: [number, number, number, number];
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build standard legend rows from product items.
|
|
26
|
+
* Expected fields on input item:
|
|
27
|
+
* - productId / productName / area
|
|
28
|
+
* - x / z (model coords)
|
|
29
|
+
* - properties / propertySets
|
|
30
|
+
* - gid (fallback id)
|
|
31
|
+
*/
|
|
32
|
+
export const buildLegendRows = (items: unknown[], options?: LegendRowOptions): LegendRow[] => {
|
|
33
|
+
const colorKeyPath = options?.colorKeyPath ?? 'productId';
|
|
34
|
+
return (Array.isArray(items) ? items : []).map((item): LegendRow => {
|
|
35
|
+
const colorKey = getPathValue(item, colorKeyPath) ?? getProp(item, 'gid') ?? JSON.stringify(item);
|
|
36
|
+
const itemColor = getProp(item, 'color');
|
|
37
|
+
const color = parseLegendColor(itemColor, String(colorKey));
|
|
38
|
+
const classPath = options?.classPath ?? 'ifcClass';
|
|
39
|
+
const className = (() => {
|
|
40
|
+
const explicit = getPathValue(item, classPath);
|
|
41
|
+
if (explicit !== undefined && explicit !== null) return String(explicit);
|
|
42
|
+
const propSets = getProp(item, 'propertySets');
|
|
43
|
+
if (isRecord(propSets)) {
|
|
44
|
+
for (const set of Object.values(propSets)) {
|
|
45
|
+
const ifcClassProp = isRecord(set) ? getProp(set, 'ifcClass') : null;
|
|
46
|
+
const candidate = isRecord(ifcClassProp) ? getProp(ifcClassProp, 'value') : null;
|
|
47
|
+
if (candidate !== undefined && candidate !== null) return String(candidate);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
})();
|
|
52
|
+
const areaVal = getProp(item, 'area');
|
|
53
|
+
const area = typeof areaVal === 'number' || typeof areaVal === 'string' ? areaVal : null;
|
|
54
|
+
const baseObj = isRecord(item) ? (item as Record<string, unknown>) : {};
|
|
55
|
+
return {
|
|
56
|
+
...baseObj,
|
|
57
|
+
className,
|
|
58
|
+
area,
|
|
59
|
+
color
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
};
|
|
@@ -1,144 +1,144 @@
|
|
|
1
|
-
import {
|
|
2
|
-
LEGEND_TEXT_WIDTH_FACTOR,
|
|
3
|
-
LEGEND_MIN_CONTENT_WIDTH_EM,
|
|
4
|
-
LEGEND_TEXT_Y_OFFSET_FACTOR,
|
|
5
|
-
PX_PER_MM
|
|
6
|
-
} from './legendConstants';
|
|
7
|
-
// v2 format: { digits?: number; prefix?: string; suffix?: string } or undefined
|
|
8
|
-
type LegendFormat = { digits?: number; prefix?: string; suffix?: string } | string | undefined;
|
|
9
|
-
import {
|
|
10
|
-
RGB_MAX,
|
|
11
|
-
RED_SHIFT,
|
|
12
|
-
GREEN_SHIFT,
|
|
13
|
-
HASH_COLOR_MODULO,
|
|
14
|
-
HASH_GREEN_SHIFT,
|
|
15
|
-
HASH_BLUE_SHIFT,
|
|
16
|
-
RGB_MIN
|
|
17
|
-
} from './colorConstants';
|
|
18
|
-
|
|
19
|
-
import { isRecord, getProp } from './typeGuards';
|
|
20
|
-
|
|
21
|
-
export function parseLegendColor(value: unknown, fallbackKey: string): [number, number, number, number] {
|
|
22
|
-
if (Array.isArray(value) && (value.length === 3 || value.length === 4)) {
|
|
23
|
-
const [r, g, b, a = RGB_MAX] = value.map((v) => Number(v) || 0);
|
|
24
|
-
return [clampColor(r), clampColor(g), clampColor(b), clampColor(a)];
|
|
25
|
-
}
|
|
26
|
-
if (typeof value === 'string') {
|
|
27
|
-
const hex = value.trim();
|
|
28
|
-
const match = /^#?([a-fA-F0-9]{6})([a-fA-F0-9]{2})?$/.exec(hex);
|
|
29
|
-
if (match) {
|
|
30
|
-
const intVal = parseInt(match[1], 16);
|
|
31
|
-
const aHex = match[2];
|
|
32
|
-
return [
|
|
33
|
-
(intVal >> RED_SHIFT) & 0xff,
|
|
34
|
-
(intVal >> GREEN_SHIFT) & 0xff,
|
|
35
|
-
intVal & 0xff,
|
|
36
|
-
aHex ? parseInt(aHex, 16) : RGB_MAX
|
|
37
|
-
];
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
const hash = Math.abs(hashCode(fallbackKey || ''));
|
|
41
|
-
return [
|
|
42
|
-
hash % HASH_COLOR_MODULO,
|
|
43
|
-
(hash >> HASH_GREEN_SHIFT) % HASH_COLOR_MODULO,
|
|
44
|
-
(hash >> HASH_BLUE_SHIFT) % HASH_COLOR_MODULO,
|
|
45
|
-
RGB_MAX
|
|
46
|
-
];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function formatLegendValue(value: unknown, fmt?: LegendFormat): string {
|
|
50
|
-
if (fmt === undefined || fmt === null || fmt === 'string') {
|
|
51
|
-
return value === undefined || value === null ? '' : String(value);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (typeof fmt === 'string') {
|
|
55
|
-
const fixedMatch = /^fixed\((\d+)(?:,\s*([^)]*))?\)$/i.exec(fmt);
|
|
56
|
-
if (fixedMatch) {
|
|
57
|
-
const digits = parseInt(fixedMatch[1], 10);
|
|
58
|
-
const suffix = fixedMatch[2] || '';
|
|
59
|
-
const num = Number(value);
|
|
60
|
-
if (Number.isFinite(num)) {
|
|
61
|
-
return `${num.toFixed(digits)}${suffix ? ` ${suffix.trim()}` : ''}`.trim();
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return String(value ?? '');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (isRecord(fmt)) {
|
|
68
|
-
const digits = getProp(fmt, 'digits');
|
|
69
|
-
const prefix = getProp(fmt, 'prefix');
|
|
70
|
-
const suffix = getProp(fmt, 'suffix');
|
|
71
|
-
const num = Number(value);
|
|
72
|
-
if (Number.isFinite(num) && typeof digits === 'number') {
|
|
73
|
-
const prefixStr = typeof prefix === 'string' ? prefix : '';
|
|
74
|
-
const suffixStr = typeof suffix === 'string' ? suffix : '';
|
|
75
|
-
return `${prefixStr}${num.toFixed(digits)}${suffixStr}`;
|
|
76
|
-
}
|
|
77
|
-
return String(value ?? '');
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return String(value ?? '');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function computeLegendColumnWidths(
|
|
84
|
-
columnLabels: { label: string; minWidthMm?: number; maxWidthMm?: number }[],
|
|
85
|
-
rows: { values: string[] }[],
|
|
86
|
-
fontSizeMm: number,
|
|
87
|
-
cellPaddingMm: number,
|
|
88
|
-
swatchWidthMm: number,
|
|
89
|
-
fontFamily: string
|
|
90
|
-
): { colWidthsMm: number[]; columnCount: number } {
|
|
91
|
-
const columnCount = Math.min(6, Math.max(2, columnLabels.length + 1));
|
|
92
|
-
const colWidthsMm: number[] = new Array(columnCount).fill(0);
|
|
93
|
-
colWidthsMm[0] = swatchWidthMm;
|
|
94
|
-
const measureTextWidthMm = createLegendTextMeasurer(fontFamily, fontSizeMm);
|
|
95
|
-
|
|
96
|
-
for (let c = 1; c < columnCount; c++) {
|
|
97
|
-
const colIdx = c - 1;
|
|
98
|
-
const col = columnLabels[colIdx];
|
|
99
|
-
const minWidth = col.minWidthMm ?? fontSizeMm * LEGEND_MIN_CONTENT_WIDTH_EM;
|
|
100
|
-
const maxWidth = col.maxWidthMm ?? Number.POSITIVE_INFINITY;
|
|
101
|
-
let maxWidthMeasured = measureTextWidthMm(col.label ?? '');
|
|
102
|
-
rows.forEach((row) => {
|
|
103
|
-
const text = row.values[colIdx] ?? '';
|
|
104
|
-
maxWidthMeasured = Math.max(maxWidthMeasured, measureTextWidthMm(text));
|
|
105
|
-
});
|
|
106
|
-
const contentWidth = Math.max(minWidth, Math.min(maxWidth, maxWidthMeasured + cellPaddingMm * 2));
|
|
107
|
-
colWidthsMm[c] = contentWidth;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return { colWidthsMm, columnCount };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function computeLegendTextYOffset(fontSizeMm: number): number {
|
|
114
|
-
return fontSizeMm * LEGEND_TEXT_Y_OFFSET_FACTOR;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export const createLegendTextMeasurer = (fontFamily: string, fontSizeMm: number): ((text: string) => number) => {
|
|
118
|
-
if (typeof document !== 'undefined') {
|
|
119
|
-
const canvas = document.createElement('canvas');
|
|
120
|
-
canvas.width = 1024;
|
|
121
|
-
canvas.height = 64;
|
|
122
|
-
const canvasCtx = canvas.getContext('2d');
|
|
123
|
-
if (canvasCtx) {
|
|
124
|
-
canvasCtx.font = `${fontSizeMm * PX_PER_MM}px ${fontFamily}`;
|
|
125
|
-
const measureTextWidthMm = (text: string): number => {
|
|
126
|
-
const metrics = canvasCtx.measureText(text);
|
|
127
|
-
return metrics.width / PX_PER_MM;
|
|
128
|
-
};
|
|
129
|
-
return measureTextWidthMm;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return (text: string): number => text.length * fontSizeMm * LEGEND_TEXT_WIDTH_FACTOR;
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const clampColor = (value: number): number => Math.max(RGB_MIN, Math.min(RGB_MAX, Math.round(value)));
|
|
136
|
-
|
|
137
|
-
const hashCode = (str: string): number => {
|
|
138
|
-
let hash = 0;
|
|
139
|
-
for (let i = 0; i < str.length; i++) {
|
|
140
|
-
hash = (hash << 5) - hash + str.charCodeAt(i);
|
|
141
|
-
hash |= 0;
|
|
142
|
-
}
|
|
143
|
-
return hash;
|
|
144
|
-
};
|
|
1
|
+
import {
|
|
2
|
+
LEGEND_TEXT_WIDTH_FACTOR,
|
|
3
|
+
LEGEND_MIN_CONTENT_WIDTH_EM,
|
|
4
|
+
LEGEND_TEXT_Y_OFFSET_FACTOR,
|
|
5
|
+
PX_PER_MM
|
|
6
|
+
} from './legendConstants';
|
|
7
|
+
// v2 format: { digits?: number; prefix?: string; suffix?: string } or undefined
|
|
8
|
+
type LegendFormat = { digits?: number; prefix?: string; suffix?: string } | string | undefined;
|
|
9
|
+
import {
|
|
10
|
+
RGB_MAX,
|
|
11
|
+
RED_SHIFT,
|
|
12
|
+
GREEN_SHIFT,
|
|
13
|
+
HASH_COLOR_MODULO,
|
|
14
|
+
HASH_GREEN_SHIFT,
|
|
15
|
+
HASH_BLUE_SHIFT,
|
|
16
|
+
RGB_MIN
|
|
17
|
+
} from './colorConstants';
|
|
18
|
+
|
|
19
|
+
import { isRecord, getProp } from './typeGuards';
|
|
20
|
+
|
|
21
|
+
export function parseLegendColor(value: unknown, fallbackKey: string): [number, number, number, number] {
|
|
22
|
+
if (Array.isArray(value) && (value.length === 3 || value.length === 4)) {
|
|
23
|
+
const [r, g, b, a = RGB_MAX] = value.map((v) => Number(v) || 0);
|
|
24
|
+
return [clampColor(r), clampColor(g), clampColor(b), clampColor(a)];
|
|
25
|
+
}
|
|
26
|
+
if (typeof value === 'string') {
|
|
27
|
+
const hex = value.trim();
|
|
28
|
+
const match = /^#?([a-fA-F0-9]{6})([a-fA-F0-9]{2})?$/.exec(hex);
|
|
29
|
+
if (match) {
|
|
30
|
+
const intVal = parseInt(match[1], 16);
|
|
31
|
+
const aHex = match[2];
|
|
32
|
+
return [
|
|
33
|
+
(intVal >> RED_SHIFT) & 0xff,
|
|
34
|
+
(intVal >> GREEN_SHIFT) & 0xff,
|
|
35
|
+
intVal & 0xff,
|
|
36
|
+
aHex ? parseInt(aHex, 16) : RGB_MAX
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const hash = Math.abs(hashCode(fallbackKey || ''));
|
|
41
|
+
return [
|
|
42
|
+
hash % HASH_COLOR_MODULO,
|
|
43
|
+
(hash >> HASH_GREEN_SHIFT) % HASH_COLOR_MODULO,
|
|
44
|
+
(hash >> HASH_BLUE_SHIFT) % HASH_COLOR_MODULO,
|
|
45
|
+
RGB_MAX
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function formatLegendValue(value: unknown, fmt?: LegendFormat): string {
|
|
50
|
+
if (fmt === undefined || fmt === null || fmt === 'string') {
|
|
51
|
+
return value === undefined || value === null ? '' : String(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (typeof fmt === 'string') {
|
|
55
|
+
const fixedMatch = /^fixed\((\d+)(?:,\s*([^)]*))?\)$/i.exec(fmt);
|
|
56
|
+
if (fixedMatch) {
|
|
57
|
+
const digits = parseInt(fixedMatch[1], 10);
|
|
58
|
+
const suffix = fixedMatch[2] || '';
|
|
59
|
+
const num = Number(value);
|
|
60
|
+
if (Number.isFinite(num)) {
|
|
61
|
+
return `${num.toFixed(digits)}${suffix ? ` ${suffix.trim()}` : ''}`.trim();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return String(value ?? '');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (isRecord(fmt)) {
|
|
68
|
+
const digits = getProp(fmt, 'digits');
|
|
69
|
+
const prefix = getProp(fmt, 'prefix');
|
|
70
|
+
const suffix = getProp(fmt, 'suffix');
|
|
71
|
+
const num = Number(value);
|
|
72
|
+
if (Number.isFinite(num) && typeof digits === 'number') {
|
|
73
|
+
const prefixStr = typeof prefix === 'string' ? prefix : '';
|
|
74
|
+
const suffixStr = typeof suffix === 'string' ? suffix : '';
|
|
75
|
+
return `${prefixStr}${num.toFixed(digits)}${suffixStr}`;
|
|
76
|
+
}
|
|
77
|
+
return String(value ?? '');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return String(value ?? '');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function computeLegendColumnWidths(
|
|
84
|
+
columnLabels: { label: string; minWidthMm?: number; maxWidthMm?: number }[],
|
|
85
|
+
rows: { values: string[] }[],
|
|
86
|
+
fontSizeMm: number,
|
|
87
|
+
cellPaddingMm: number,
|
|
88
|
+
swatchWidthMm: number,
|
|
89
|
+
fontFamily: string
|
|
90
|
+
): { colWidthsMm: number[]; columnCount: number } {
|
|
91
|
+
const columnCount = Math.min(6, Math.max(2, columnLabels.length + 1));
|
|
92
|
+
const colWidthsMm: number[] = new Array(columnCount).fill(0);
|
|
93
|
+
colWidthsMm[0] = swatchWidthMm;
|
|
94
|
+
const measureTextWidthMm = createLegendTextMeasurer(fontFamily, fontSizeMm);
|
|
95
|
+
|
|
96
|
+
for (let c = 1; c < columnCount; c++) {
|
|
97
|
+
const colIdx = c - 1;
|
|
98
|
+
const col = columnLabels[colIdx];
|
|
99
|
+
const minWidth = col.minWidthMm ?? fontSizeMm * LEGEND_MIN_CONTENT_WIDTH_EM;
|
|
100
|
+
const maxWidth = col.maxWidthMm ?? Number.POSITIVE_INFINITY;
|
|
101
|
+
let maxWidthMeasured = measureTextWidthMm(col.label ?? '');
|
|
102
|
+
rows.forEach((row) => {
|
|
103
|
+
const text = row.values[colIdx] ?? '';
|
|
104
|
+
maxWidthMeasured = Math.max(maxWidthMeasured, measureTextWidthMm(text));
|
|
105
|
+
});
|
|
106
|
+
const contentWidth = Math.max(minWidth, Math.min(maxWidth, maxWidthMeasured + cellPaddingMm * 2));
|
|
107
|
+
colWidthsMm[c] = contentWidth;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { colWidthsMm, columnCount };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function computeLegendTextYOffset(fontSizeMm: number): number {
|
|
114
|
+
return fontSizeMm * LEGEND_TEXT_Y_OFFSET_FACTOR;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const createLegendTextMeasurer = (fontFamily: string, fontSizeMm: number): ((text: string) => number) => {
|
|
118
|
+
if (typeof document !== 'undefined') {
|
|
119
|
+
const canvas = document.createElement('canvas');
|
|
120
|
+
canvas.width = 1024;
|
|
121
|
+
canvas.height = 64;
|
|
122
|
+
const canvasCtx = canvas.getContext('2d');
|
|
123
|
+
if (canvasCtx) {
|
|
124
|
+
canvasCtx.font = `${fontSizeMm * PX_PER_MM}px ${fontFamily}`;
|
|
125
|
+
const measureTextWidthMm = (text: string): number => {
|
|
126
|
+
const metrics = canvasCtx.measureText(text);
|
|
127
|
+
return metrics.width / PX_PER_MM;
|
|
128
|
+
};
|
|
129
|
+
return measureTextWidthMm;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return (text: string): number => text.length * fontSizeMm * LEGEND_TEXT_WIDTH_FACTOR;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const clampColor = (value: number): number => Math.max(RGB_MIN, Math.min(RGB_MAX, Math.round(value)));
|
|
136
|
+
|
|
137
|
+
const hashCode = (str: string): number => {
|
|
138
|
+
let hash = 0;
|
|
139
|
+
for (let i = 0; i < str.length; i++) {
|
|
140
|
+
hash = (hash << 5) - hash + str.charCodeAt(i);
|
|
141
|
+
hash |= 0;
|
|
142
|
+
}
|
|
143
|
+
return hash;
|
|
144
|
+
};
|