@ifc-lite/viewer 1.25.2 → 1.27.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/.turbo/turbo-build.log +40 -30
- package/CHANGELOG.md +110 -0
- package/dist/assets/{basketViewActivator-CTgyKI3U.js → basketViewActivator-B3CdrLsb.js} +7 -7
- package/dist/assets/{bcf-7jQby1qi.js → bcf-QeHK_Aud.js} +5 -5
- package/dist/assets/{browser-DXS29_v9.js → browser-BIoDDfBW.js} +1 -1
- package/dist/assets/{cesium-BoVuJvTC.js → cesium-CzZn5yVA.js} +319 -319
- package/dist/assets/{deflate-Cfp9t1Df.js → deflate-B-d0SYQM.js} +1 -1
- package/dist/assets/exceljs.min-DsuzKYnj.js +29 -0
- package/dist/assets/{exporters-DfSvJPi4.js → exporters-B4LbZFeT.js} +1434 -1179
- package/dist/assets/geometry.worker-BdH-E6NB.js +1 -0
- package/dist/assets/{geotiff-xZoE8BkO.js → geotiff-CrVtDRFq.js} +10 -10
- package/dist/assets/html2canvas.esm-Ge7aVWlp.js +5 -0
- package/dist/assets/{ids-Cu73hD0Y.js → ids-DjsGFN10.js} +21 -21
- package/dist/assets/ifc-lite_bg-DsYUIHm3.wasm +0 -0
- package/dist/assets/{index-WSbA5iy6.js → index-COYokSKc.js} +44122 -38782
- package/dist/assets/index-ajK6D32J.css +1 -0
- package/dist/assets/index.es-CY202jA3.js +6866 -0
- package/dist/assets/{jpeg-DhwFEbqb.js → jpeg-D4wOkf5h.js} +1 -1
- package/dist/assets/jspdf.es.min-DIGb9BHN.js +19571 -0
- package/dist/assets/jspdf.plugin.autotable-BBLUVd7n.js +2 -0
- package/dist/assets/{lerc-Dz6BXOVb.js → lerc-DmW0_tgf.js} +1 -1
- package/dist/assets/{lzw-C9z0fG2o.js → lzw-oWetY-d6.js} +1 -1
- package/dist/assets/{maplibre-gl-Do6O5tDc.js → maplibre-gl-BF3Z0idw.js} +1 -1
- package/dist/assets/{native-bridge-RvDmzO-2.js → native-bridge-BX8_tHXE.js} +1 -1
- package/dist/assets/{packbits-jfwifz7C.js → packbits-F8Nkp4NY.js} +1 -1
- package/dist/assets/{pako.esm-Cram60i4.js → pako.esm-n3Pgozwg.js} +1 -1
- package/dist/assets/{parser.worker-C594dWxH.js → parser.worker-D591Zu_-.js} +3 -3
- package/dist/assets/pdf-Dsh3HPZB.js +135 -0
- package/dist/assets/raw-D9iw0tmc.js +1 -0
- package/dist/assets/{sandbox-DDSZ7rek.js → sandbox-BAC3a-eN.js} +4235 -2716
- package/dist/assets/server-client-Cjwnm7il.js +706 -0
- package/dist/assets/{webimage-XFHVyVtC.js → webimage-BLV1dgmd.js} +1 -1
- package/dist/assets/xlsx-Bc2HTrjC.js +142 -0
- package/dist/assets/{zip-BJqVbRkU.js → zip-DFgP-l20.js} +1 -1
- package/dist/assets/{zstd-3q5qcl5V.js → zstd-C_1HxVrA.js} +1 -1
- package/dist/index.html +8 -8
- package/package.json +13 -9
- package/src/components/extensions/FlavorDialog.tsx +18 -2
- package/src/components/extensions/FlavorListView.tsx +12 -3
- package/src/components/mcp/PlaygroundChat.tsx +1 -0
- package/src/components/mcp/data.ts +6 -0
- package/src/components/mcp/playground-dispatcher.ts +277 -0
- package/src/components/mcp/types.ts +2 -1
- package/src/components/ui/combo-input.tsx +163 -0
- package/src/components/ui/tabs.tsx +1 -1
- package/src/components/viewer/ClashBcfExportDialog.tsx +271 -0
- package/src/components/viewer/ClashPanel.tsx +370 -0
- package/src/components/viewer/ClashSettingsDialog.tsx +407 -0
- package/src/components/viewer/CommandPalette.tsx +14 -15
- package/src/components/viewer/MainToolbar.tsx +155 -175
- package/src/components/viewer/PropertiesPanel.tsx +13 -6
- package/src/components/viewer/SearchInline.tsx +62 -2
- package/src/components/viewer/SearchModal.filter.builder.tsx +24 -393
- package/src/components/viewer/SearchModal.filter.editors.tsx +503 -0
- package/src/components/viewer/SearchModal.filter.tsx +64 -1
- package/src/components/viewer/SearchModal.tsx +19 -6
- package/src/components/viewer/ViewerLayout.tsx +5 -0
- package/src/components/viewer/Viewport.tsx +64 -9
- package/src/components/viewer/ViewportContainer.tsx +45 -3
- package/src/components/viewer/bcf/BCFOverlay.tsx +5 -4
- package/src/components/viewer/lists/ColumnHeaderMenu.tsx +84 -0
- package/src/components/viewer/lists/ListBuilder.tsx +789 -280
- package/src/components/viewer/lists/ListGroupingBar.tsx +72 -0
- package/src/components/viewer/lists/ListPanel.tsx +49 -5
- package/src/components/viewer/lists/ListResultsTable.tsx +270 -176
- package/src/components/viewer/lists/list-table-utils.ts +123 -0
- package/src/components/viewer/useGeometryStreaming.ts +21 -1
- package/src/generated/mcp-catalog.json +4 -0
- package/src/hooks/ingest/streamCleanup.test.ts +41 -0
- package/src/hooks/ingest/streamCleanup.ts +45 -0
- package/src/hooks/ingest/viewerModelIngest.ts +64 -42
- package/src/hooks/ingest/watchedGeometryStream.test.ts +78 -0
- package/src/hooks/ingest/watchedGeometryStream.ts +76 -0
- package/src/hooks/source-key.ts +35 -0
- package/src/hooks/useAlignmentLines3D.ts +139 -0
- package/src/hooks/useClash.ts +420 -0
- package/src/hooks/useGridLines3D.ts +140 -0
- package/src/hooks/useIfcFederation.ts +16 -2
- package/src/hooks/useIfcLoader.ts +5 -7
- package/src/lib/clash/persistence.ts +308 -0
- package/src/lib/geo/effective-georef.test.ts +66 -0
- package/src/lib/length-unit-scale.ts +41 -0
- package/src/lib/lists/adapter.ts +136 -11
- package/src/lib/lists/export/csv.ts +47 -0
- package/src/lib/lists/export/index.ts +49 -0
- package/src/lib/lists/export/model.ts +111 -0
- package/src/lib/lists/export/pdf.ts +67 -0
- package/src/lib/lists/export/xlsx.ts +83 -0
- package/src/lib/lists/index.ts +2 -0
- package/src/lib/search/filter-evaluate.test.ts +81 -0
- package/src/lib/search/filter-evaluate.ts +59 -87
- package/src/lib/search/filter-match.ts +167 -0
- package/src/lib/search/filter-rules.test.ts +25 -0
- package/src/lib/search/filter-rules.ts +75 -2
- package/src/lib/search/filter-schema.ts +0 -0
- package/src/lib/slab-edit.test.ts +72 -0
- package/src/lib/slab-edit.ts +159 -19
- package/src/sdk/adapters/export-adapter.ts +3 -3
- package/src/sdk/adapters/query-adapter.ts +3 -3
- package/src/services/extensions/host.ts +13 -0
- package/src/store/constants.ts +33 -25
- package/src/store/index.ts +29 -8
- package/src/store/slices/clashSlice.ts +251 -0
- package/src/store/slices/listSlice.ts +6 -0
- package/src/store/slices/mutationSlice.ts +14 -6
- package/src/store/slices/searchSlice.ts +29 -3
- package/src/store/slices/visibilitySlice.test.ts +23 -5
- package/src/store/slices/visibilitySlice.ts +18 -8
- package/src/utils/nativeSpatialDataStore.ts +6 -0
- package/src/utils/serverDataModel.test.ts +6 -0
- package/src/utils/serverDataModel.ts +7 -0
- package/dist/assets/geometry.worker-Cyn5BybV.js +0 -1
- package/dist/assets/ifc-lite_bg-ksLBP5cA.wasm +0 -0
- package/dist/assets/index-Bws3UAkj.css +0 -1
- package/dist/assets/raw-R2QfzPAR.js +0 -1
- package/dist/assets/server-client-Ctk8_Bof.js +0 -626
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
import type { CellValue } from '@ifc-lite/lists';
|
|
6
|
+
import { displayCell, type ExportModel } from './model';
|
|
7
|
+
|
|
8
|
+
function esc(s: string, delim: string): string {
|
|
9
|
+
return /["\r\n]/.test(s) || s.includes(delim) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* CSV faithful to the configured columns. When grouped, a leading "Group"
|
|
14
|
+
* column preserves the grouping as data (so it stays re-importable), rows are
|
|
15
|
+
* ordered by group, and a TOTAL row carries the grand count + sums.
|
|
16
|
+
*/
|
|
17
|
+
export function toCsv(model: ExportModel, delimiter = ','): string {
|
|
18
|
+
const grouped = model.groups !== null;
|
|
19
|
+
const header = [...(grouped ? ['Group'] : []), ...model.columns.map((c) => c.label)];
|
|
20
|
+
const lines = [header.map((h) => esc(h, delimiter)).join(delimiter)];
|
|
21
|
+
|
|
22
|
+
const line = (groupLabel: string | null, values: CellValue[]) => {
|
|
23
|
+
const cells = grouped ? [esc(groupLabel ?? '', delimiter)] : [];
|
|
24
|
+
for (let i = 0; i < model.columns.length; i++) cells.push(esc(displayCell(values[i]), delimiter));
|
|
25
|
+
return cells.join(delimiter);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
if (grouped && model.groups) {
|
|
29
|
+
for (const g of model.groups) for (const r of g.rows) lines.push(line(g.label, r));
|
|
30
|
+
} else {
|
|
31
|
+
for (const r of model.rows) lines.push(line(null, r));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (model.sumColumnIds.length > 0) {
|
|
35
|
+
const totalLabel = `TOTAL (${model.totals.count})`;
|
|
36
|
+
const cells = grouped ? [esc(totalLabel, delimiter)] : [];
|
|
37
|
+
for (let i = 0; i < model.columns.length; i++) {
|
|
38
|
+
const c = model.columns[i];
|
|
39
|
+
if (c.summed) cells.push(esc(displayCell(model.totals.sums[c.id]), delimiter));
|
|
40
|
+
else if (!grouped && i === 0) cells.push(esc(totalLabel, delimiter));
|
|
41
|
+
else cells.push('');
|
|
42
|
+
}
|
|
43
|
+
lines.push(cells.join(delimiter));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return lines.join('\r\n');
|
|
47
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* List results export — CSV / Excel / PDF, all driven by one normalised model
|
|
7
|
+
* (columns, grouping, sums, totals). Excel and PDF writers (and their heavy
|
|
8
|
+
* libs) are lazy-loaded so they never touch the initial bundle.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { toCsv } from './csv';
|
|
12
|
+
import type { ExportModel } from './model';
|
|
13
|
+
|
|
14
|
+
export type ExportFormat = 'csv' | 'xlsx' | 'pdf';
|
|
15
|
+
|
|
16
|
+
export const EXPORT_LABELS: Record<ExportFormat, string> = {
|
|
17
|
+
csv: 'CSV (.csv)',
|
|
18
|
+
xlsx: 'Excel (.xlsx)',
|
|
19
|
+
pdf: 'PDF (.pdf)',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function slug(s: string): string {
|
|
23
|
+
return (s || 'list').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60) || 'list';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function download(blob: Blob, filename: string): void {
|
|
27
|
+
const url = URL.createObjectURL(blob);
|
|
28
|
+
const a = document.createElement('a');
|
|
29
|
+
a.href = url;
|
|
30
|
+
a.download = filename;
|
|
31
|
+
a.click();
|
|
32
|
+
setTimeout(() => URL.revokeObjectURL(url), 1500);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function exportList(format: ExportFormat, model: ExportModel): Promise<void> {
|
|
36
|
+
const name = slug(model.title);
|
|
37
|
+
if (format === 'csv') {
|
|
38
|
+
download(new Blob([toCsv(model)], { type: 'text/csv;charset=utf-8;' }), `${name}.csv`);
|
|
39
|
+
} else if (format === 'xlsx') {
|
|
40
|
+
const { toXlsx } = await import('./xlsx');
|
|
41
|
+
download(await toXlsx(model), `${name}.xlsx`);
|
|
42
|
+
} else {
|
|
43
|
+
const { toPdf } = await import('./pdf');
|
|
44
|
+
download(await toPdf(model), `${name}.pdf`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { buildExportModel } from './model';
|
|
49
|
+
export type { ExportModel } from './model';
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Normalised export model shared by the CSV / Excel / PDF writers. Built from
|
|
7
|
+
* the on-screen list view so every export honours the configured columns
|
|
8
|
+
* (order, labels, widths), the active grouping, and the summed columns —
|
|
9
|
+
* grouped sections with per-group count + subtotals, plus grand totals.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { CellValue, ColumnDefinition, ListRow, ListGrouping } from '@ifc-lite/lists';
|
|
13
|
+
|
|
14
|
+
export interface ExportColumn {
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
numeric: boolean;
|
|
18
|
+
summed: boolean;
|
|
19
|
+
/** Pixel width from the table (for proportional column sizing in exports). */
|
|
20
|
+
width: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ExportGroup {
|
|
24
|
+
label: string;
|
|
25
|
+
count: number;
|
|
26
|
+
sums: Record<string, number>;
|
|
27
|
+
rows: CellValue[][];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ExportModel {
|
|
31
|
+
title: string;
|
|
32
|
+
generatedAt: string;
|
|
33
|
+
columns: ExportColumn[];
|
|
34
|
+
/** Grouped sections (with member rows), or null when the list isn't grouped. */
|
|
35
|
+
groups: ExportGroup[] | null;
|
|
36
|
+
/** All rows in display order (flat) — used by writers that don't section. */
|
|
37
|
+
rows: CellValue[][];
|
|
38
|
+
groupColumnId: string | null;
|
|
39
|
+
sumColumnIds: string[];
|
|
40
|
+
totals: { count: number; sums: Record<string, number> };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface BuildModelInput {
|
|
44
|
+
title: string;
|
|
45
|
+
columns: ColumnDefinition[];
|
|
46
|
+
/** Rows already filtered + sorted exactly as shown on screen. */
|
|
47
|
+
rows: ListRow[];
|
|
48
|
+
grouping?: ListGrouping;
|
|
49
|
+
numericCols: boolean[];
|
|
50
|
+
columnWidths: number[];
|
|
51
|
+
generatedAt: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Format a cell for text-based exports (CSV/PDF). Excel keeps raw numbers. */
|
|
55
|
+
export function displayCell(value: CellValue): string {
|
|
56
|
+
if (value === null || value === undefined) return '';
|
|
57
|
+
if (typeof value === 'boolean') return value ? 'Yes' : 'No';
|
|
58
|
+
if (typeof value === 'number') {
|
|
59
|
+
if (Number.isInteger(value)) return value.toLocaleString();
|
|
60
|
+
return value.toFixed(4).replace(/\.?0+$/, '');
|
|
61
|
+
}
|
|
62
|
+
return String(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function buildExportModel(input: BuildModelInput): ExportModel {
|
|
66
|
+
const { columns, rows, grouping, numericCols, columnWidths, title, generatedAt } = input;
|
|
67
|
+
const sumColumnIds = grouping?.sumColumnIds ?? [];
|
|
68
|
+
const exportCols: ExportColumn[] = columns.map((c, i) => ({
|
|
69
|
+
id: c.id,
|
|
70
|
+
label: c.label ?? c.propertyName,
|
|
71
|
+
numeric: !!numericCols[i],
|
|
72
|
+
summed: sumColumnIds.includes(c.id),
|
|
73
|
+
width: columnWidths[i] ?? 120,
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
const sumIdx = sumColumnIds
|
|
77
|
+
.map((id) => ({ id, idx: columns.findIndex((c) => c.id === id) }))
|
|
78
|
+
.filter((s) => s.idx >= 0);
|
|
79
|
+
const zeroSums = (): Record<string, number> => Object.fromEntries(sumIdx.map((s) => [s.id, 0]));
|
|
80
|
+
const addSums = (acc: Record<string, number>, values: CellValue[]) => {
|
|
81
|
+
for (const s of sumIdx) {
|
|
82
|
+
const v = values[s.idx];
|
|
83
|
+
if (typeof v === 'number' && Number.isFinite(v)) acc[s.id] += v;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const totals = { count: rows.length, sums: zeroSums() };
|
|
88
|
+
const flatRows: CellValue[][] = [];
|
|
89
|
+
for (const r of rows) { flatRows.push(r.values); addSums(totals.sums, r.values); }
|
|
90
|
+
|
|
91
|
+
const groupColumnId = grouping?.columnId && columns.some((c) => c.id === grouping.columnId)
|
|
92
|
+
? grouping.columnId : null;
|
|
93
|
+
|
|
94
|
+
let groups: ExportGroup[] | null = null;
|
|
95
|
+
if (groupColumnId) {
|
|
96
|
+
const groupIdx = columns.findIndex((c) => c.id === groupColumnId);
|
|
97
|
+
const byKey = new Map<string, ExportGroup>();
|
|
98
|
+
for (const r of rows) {
|
|
99
|
+
const raw = r.values[groupIdx];
|
|
100
|
+
const label = raw === null || raw === undefined || raw === '' ? '(none)' : displayCell(raw);
|
|
101
|
+
let g = byKey.get(label);
|
|
102
|
+
if (!g) { g = { label, count: 0, sums: zeroSums(), rows: [] }; byKey.set(label, g); }
|
|
103
|
+
g.count++;
|
|
104
|
+
g.rows.push(r.values);
|
|
105
|
+
addSums(g.sums, r.values);
|
|
106
|
+
}
|
|
107
|
+
groups = Array.from(byKey.values()).sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { title, generatedAt, columns: exportCols, groups, rows: flatRows, groupColumnId, sumColumnIds, totals };
|
|
111
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
import type { CellValue } from '@ifc-lite/lists';
|
|
6
|
+
import { displayCell, type ExportModel } from './model';
|
|
7
|
+
|
|
8
|
+
/** Quality tabular PDF report: title + meta, dark header, grouped sections
|
|
9
|
+
* (bold group rows carrying per-group count + subtotals), right-aligned
|
|
10
|
+
* numerics, a grand-total foot, and page numbers. */
|
|
11
|
+
export async function toPdf(model: ExportModel): Promise<Blob> {
|
|
12
|
+
const { jsPDF } = await import('jspdf');
|
|
13
|
+
const autoTable = (await import('jspdf-autotable')).default;
|
|
14
|
+
|
|
15
|
+
const landscape = model.columns.length > 5;
|
|
16
|
+
const doc = new jsPDF({ orientation: landscape ? 'landscape' : 'portrait', unit: 'pt', format: 'a4' });
|
|
17
|
+
|
|
18
|
+
doc.setFont('helvetica', 'bold'); doc.setFontSize(14);
|
|
19
|
+
doc.text(model.title || 'List', 40, 42);
|
|
20
|
+
doc.setFont('helvetica', 'normal'); doc.setFontSize(9); doc.setTextColor(130);
|
|
21
|
+
doc.text(`${model.totals.count.toLocaleString()} elements · ${model.generatedAt}`, 40, 58);
|
|
22
|
+
doc.setTextColor(0);
|
|
23
|
+
|
|
24
|
+
const head = [model.columns.map((c) => c.label)];
|
|
25
|
+
const cell = (vals: CellValue[], i: number) => displayCell(vals[i]);
|
|
26
|
+
const body: Array<Array<string | { content: string; styles: Record<string, unknown> }>> = [];
|
|
27
|
+
|
|
28
|
+
if (model.groups) {
|
|
29
|
+
for (const g of model.groups) {
|
|
30
|
+
body.push(model.columns.map((c, i) => ({
|
|
31
|
+
content: i === 0 ? `${g.label} (${g.count})` : (c.summed ? displayCell(g.sums[c.id]) : ''),
|
|
32
|
+
styles: { fontStyle: 'bold', fillColor: [226, 232, 240] as unknown as number[] },
|
|
33
|
+
})));
|
|
34
|
+
for (const r of g.rows) body.push(model.columns.map((_, i) => cell(r, i)));
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
for (const r of model.rows) body.push(model.columns.map((_, i) => cell(r, i)));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const foot = model.sumColumnIds.length > 0
|
|
41
|
+
? [model.columns.map((c, i) => (c.summed ? displayCell(model.totals.sums[c.id]) : (i === 0 ? `Total (${model.totals.count})` : '')))]
|
|
42
|
+
: undefined;
|
|
43
|
+
|
|
44
|
+
const columnStyles: Record<number, { halign: 'right' }> = {};
|
|
45
|
+
model.columns.forEach((c, i) => { if (c.numeric) columnStyles[i] = { halign: 'right' }; });
|
|
46
|
+
|
|
47
|
+
autoTable(doc, {
|
|
48
|
+
head,
|
|
49
|
+
body,
|
|
50
|
+
foot,
|
|
51
|
+
startY: 72,
|
|
52
|
+
margin: { left: 40, right: 40, top: 70, bottom: 40 },
|
|
53
|
+
styles: { fontSize: 8, cellPadding: 3, overflow: 'ellipsize', lineColor: [226, 232, 240], lineWidth: 0.5 },
|
|
54
|
+
headStyles: { fillColor: [51, 65, 85], textColor: 255, fontStyle: 'bold' },
|
|
55
|
+
footStyles: { fillColor: [241, 245, 249], textColor: 20, fontStyle: 'bold' },
|
|
56
|
+
columnStyles,
|
|
57
|
+
didDrawPage: () => {
|
|
58
|
+
doc.setFontSize(8); doc.setTextColor(150);
|
|
59
|
+
const w = doc.internal.pageSize.getWidth();
|
|
60
|
+
const h = doc.internal.pageSize.getHeight();
|
|
61
|
+
doc.text(`Page ${doc.getNumberOfPages()}`, w - 60, h - 20);
|
|
62
|
+
doc.setTextColor(0);
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return doc.output('blob');
|
|
67
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
import type { CellValue } from '@ifc-lite/lists';
|
|
6
|
+
import { displayCell, type ExportModel, type ExportColumn } from './model';
|
|
7
|
+
|
|
8
|
+
const XLSX_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
|
9
|
+
const NUM_FMT = '#,##0.####';
|
|
10
|
+
|
|
11
|
+
/** px → Excel column-width units (≈ 7px per char). */
|
|
12
|
+
const excelWidth = (px: number): number => Math.max(8, Math.min(80, Math.round(px / 7)));
|
|
13
|
+
|
|
14
|
+
/** Excel keeps real numbers (so the recipient can re-aggregate); other types
|
|
15
|
+
* fall back to the same display string the table shows. */
|
|
16
|
+
function cellValue(v: CellValue, c: ExportColumn): string | number | null {
|
|
17
|
+
if (v === null || v === undefined) return null;
|
|
18
|
+
if (c.numeric && typeof v === 'number' && Number.isFinite(v)) return v;
|
|
19
|
+
return displayCell(v);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function toXlsx(model: ExportModel): Promise<Blob> {
|
|
23
|
+
const ExcelJS = (await import('exceljs')).default;
|
|
24
|
+
const wb = new ExcelJS.Workbook();
|
|
25
|
+
wb.creator = 'IFC-Lite';
|
|
26
|
+
const ws = wb.addWorksheet((model.title || 'List').slice(0, 31), {
|
|
27
|
+
views: [{ state: 'frozen', ySplit: 4 }],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const cols = model.columns;
|
|
31
|
+
const ncol = cols.length;
|
|
32
|
+
|
|
33
|
+
// Title + meta.
|
|
34
|
+
ws.addRow([model.title || 'List']);
|
|
35
|
+
ws.mergeCells(1, 1, 1, ncol);
|
|
36
|
+
ws.getCell(1, 1).font = { bold: true, size: 14 };
|
|
37
|
+
ws.addRow([`${model.totals.count.toLocaleString()} elements · ${model.generatedAt}`]);
|
|
38
|
+
ws.mergeCells(2, 1, 2, ncol);
|
|
39
|
+
ws.getCell(2, 1).font = { italic: true, size: 9, color: { argb: 'FF94A3B8' } };
|
|
40
|
+
ws.addRow([]);
|
|
41
|
+
|
|
42
|
+
// Header.
|
|
43
|
+
const header = ws.addRow(cols.map((c) => c.label));
|
|
44
|
+
header.font = { bold: true, color: { argb: 'FFFFFFFF' } };
|
|
45
|
+
header.eachCell((cell) => {
|
|
46
|
+
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF334155' } };
|
|
47
|
+
cell.alignment = { vertical: 'middle' };
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Column widths + numeric formatting/alignment.
|
|
51
|
+
cols.forEach((c, i) => {
|
|
52
|
+
const col = ws.getColumn(i + 1);
|
|
53
|
+
col.width = excelWidth(c.width);
|
|
54
|
+
if (c.numeric) { col.numFmt = NUM_FMT; col.alignment = { horizontal: 'right' }; }
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const addDataRow = (values: CellValue[], outline?: number) => {
|
|
58
|
+
const r = ws.addRow(cols.map((c, i) => cellValue(values[i], c)));
|
|
59
|
+
if (outline) r.outlineLevel = outline;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (model.groups) {
|
|
63
|
+
for (const g of model.groups) {
|
|
64
|
+
const gr = ws.addRow(cols.map((c, i) => (i === 0 ? `${g.label} (${g.count})` : (c.summed ? g.sums[c.id] : null))));
|
|
65
|
+
gr.font = { bold: true };
|
|
66
|
+
gr.eachCell((cell) => { cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE2E8F0' } }; });
|
|
67
|
+
for (const row of g.rows) addDataRow(row, 1);
|
|
68
|
+
}
|
|
69
|
+
ws.properties.outlineLevelRow = 1;
|
|
70
|
+
} else {
|
|
71
|
+
for (const row of model.rows) addDataRow(row);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Grand total.
|
|
75
|
+
if (model.sumColumnIds.length > 0) {
|
|
76
|
+
const tr = ws.addRow(cols.map((c, i) => (c.summed ? model.totals.sums[c.id] : (i === 0 ? `Total (${model.totals.count})` : null))));
|
|
77
|
+
tr.font = { bold: true };
|
|
78
|
+
tr.eachCell((cell) => { cell.border = { top: { style: 'double', color: { argb: 'FF334155' } } }; });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const buf = await wb.xlsx.writeBuffer();
|
|
82
|
+
return new Blob([buf], { type: XLSX_MIME });
|
|
83
|
+
}
|
package/src/lib/lists/index.ts
CHANGED
|
@@ -14,10 +14,12 @@ export type {
|
|
|
14
14
|
ConditionOperator,
|
|
15
15
|
DiscoveredColumns,
|
|
16
16
|
EntityAttribute,
|
|
17
|
+
ListGrouping,
|
|
17
18
|
} from '@ifc-lite/lists';
|
|
18
19
|
export {
|
|
19
20
|
ENTITY_ATTRIBUTES,
|
|
20
21
|
executeList,
|
|
22
|
+
summariseListRows,
|
|
21
23
|
listResultToCSV,
|
|
22
24
|
discoverColumns,
|
|
23
25
|
LIST_PRESETS,
|
|
@@ -256,6 +256,87 @@ describe('matchQuantityRule', () => {
|
|
|
256
256
|
});
|
|
257
257
|
});
|
|
258
258
|
|
|
259
|
+
describe('materialNamesOf', () => {
|
|
260
|
+
it('collects top-level, layer, constituent, profile, and list names', () => {
|
|
261
|
+
const names = __internal.materialNamesOf({
|
|
262
|
+
type: 'MaterialLayerSet',
|
|
263
|
+
name: 'Wall Buildup',
|
|
264
|
+
layers: [
|
|
265
|
+
{ materialName: 'Concrete C30/37' },
|
|
266
|
+
{ materialName: 'Rigid Insulation', name: 'Insulation Layer' },
|
|
267
|
+
],
|
|
268
|
+
materials: [{ name: 'Steel S355' }],
|
|
269
|
+
});
|
|
270
|
+
assert.deepStrictEqual(names, [
|
|
271
|
+
'Wall Buildup',
|
|
272
|
+
'Concrete C30/37',
|
|
273
|
+
'Rigid Insulation',
|
|
274
|
+
'Insulation Layer',
|
|
275
|
+
'Steel S355',
|
|
276
|
+
]);
|
|
277
|
+
});
|
|
278
|
+
it('returns [] for a null MaterialInfo (no association)', () => {
|
|
279
|
+
assert.deepStrictEqual(__internal.materialNamesOf(null), []);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe('matchClassificationRule', () => {
|
|
284
|
+
const refs = [
|
|
285
|
+
{ system: 'Uniclass 2015', identification: 'Pr_60_10_32', name: 'External wall' },
|
|
286
|
+
{ system: 'OmniClass', identification: '23-13 11 11', name: 'Walls' },
|
|
287
|
+
];
|
|
288
|
+
it('isSet / isNotSet check presence, optionally scoped by system', () => {
|
|
289
|
+
assert.strictEqual(__internal.matchClassificationRule(Rule.classification('', 'isSet', ''), refs), true);
|
|
290
|
+
assert.strictEqual(__internal.matchClassificationRule(Rule.classification('OmniClass', 'isSet', ''), refs), true);
|
|
291
|
+
assert.strictEqual(__internal.matchClassificationRule(Rule.classification('SfB', 'isSet', ''), refs), false);
|
|
292
|
+
assert.strictEqual(__internal.matchClassificationRule(Rule.classification('SfB', 'isNotSet', ''), refs), true);
|
|
293
|
+
assert.strictEqual(__internal.matchClassificationRule(Rule.classification('', 'isNotSet', ''), []), true);
|
|
294
|
+
});
|
|
295
|
+
it('value ops match code (identification) OR name', () => {
|
|
296
|
+
assert.strictEqual(__internal.matchClassificationRule(Rule.classification('', 'contains', 'Pr_60'), refs), true);
|
|
297
|
+
assert.strictEqual(__internal.matchClassificationRule(Rule.classification('', 'contains', 'external'), refs), true);
|
|
298
|
+
assert.strictEqual(__internal.matchClassificationRule(Rule.classification('', 'eq', 'Pr_60_10_32'), refs), true);
|
|
299
|
+
assert.strictEqual(__internal.matchClassificationRule(Rule.classification('', 'contains', 'Ss_'), refs), false);
|
|
300
|
+
});
|
|
301
|
+
it('system scope excludes refs from other systems', () => {
|
|
302
|
+
// Pr_60 only exists in the Uniclass ref — scoping to OmniClass misses it.
|
|
303
|
+
assert.strictEqual(__internal.matchClassificationRule(Rule.classification('OmniClass', 'contains', 'Pr_60'), refs), false);
|
|
304
|
+
assert.strictEqual(__internal.matchClassificationRule(Rule.classification('OmniClass', 'contains', '23-13'), refs), true);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe('elevationOf + elevation rule', () => {
|
|
309
|
+
function withHierarchy(store: IfcDataStore): IfcDataStore {
|
|
310
|
+
// elementToStorey: 10,20 → storey 100 (z=0); 30 → storey 200 (z=3.5).
|
|
311
|
+
// 40 is unplaced (no storey) → elevation null → never matches.
|
|
312
|
+
(store as unknown as { spatialHierarchy: unknown }).spatialHierarchy = {
|
|
313
|
+
elementToStorey: new Map<number, number>([[10, 100], [20, 100], [30, 200]]),
|
|
314
|
+
storeyElevations: new Map<number, number>([[100, 0], [200, 3.5]]),
|
|
315
|
+
};
|
|
316
|
+
return store;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
it('resolves elevation from the element’s storey, null when unplaced', () => {
|
|
320
|
+
const store = withHierarchy(buildStore(rows));
|
|
321
|
+
assert.strictEqual(__internal.elevationOf(store, 10), 0);
|
|
322
|
+
assert.strictEqual(__internal.elevationOf(store, 30), 3.5);
|
|
323
|
+
assert.strictEqual(__internal.elevationOf(store, 40), null);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('elevation > 3 matches only elements on the high storey', () => {
|
|
327
|
+
const store = withHierarchy(buildStore(rows));
|
|
328
|
+
const out = evaluateFilterRules('m1', store, [Rule.elevation('gt', 3)], 'AND');
|
|
329
|
+
assert.deepStrictEqual(out.map((r) => r.expressId), [30]);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('elevation rule excludes unplaced elements even with lte', () => {
|
|
333
|
+
const store = withHierarchy(buildStore(rows));
|
|
334
|
+
const out = evaluateFilterRules('m1', store, [Rule.elevation('lte', 100)], 'AND');
|
|
335
|
+
// 10, 20 (z=0) and 30 (z=3.5) qualify; 40 (unplaced) is excluded.
|
|
336
|
+
assert.deepStrictEqual(out.map((r) => r.expressId).sort(), [10, 20, 30]);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
259
340
|
describe('evaluateFilterRulesFederated — per-model candidate narrowing', () => {
|
|
260
341
|
it('candidateExpressIdsByModel narrows each model independently', async () => {
|
|
261
342
|
const a = buildStore(rows);
|