@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
|
@@ -32,15 +32,12 @@ import { COMMON_IFC_TYPES } from '@/lib/search/common-ifc-types';
|
|
|
32
32
|
import {
|
|
33
33
|
Rule,
|
|
34
34
|
type FilterRule,
|
|
35
|
-
type SetOp,
|
|
36
|
-
type StringOp,
|
|
37
|
-
type ValueOp,
|
|
38
|
-
type NumericOp,
|
|
39
35
|
type Combinator,
|
|
40
36
|
} from '@/lib/search/filter-rules';
|
|
41
37
|
import {
|
|
42
38
|
discoverFilterSchema,
|
|
43
39
|
discoverPropertyAndQuantitySchema,
|
|
40
|
+
discoverFilterValues,
|
|
44
41
|
} from '@/lib/search/filter-schema';
|
|
45
42
|
import {
|
|
46
43
|
loadSavedFilters,
|
|
@@ -48,33 +45,7 @@ import {
|
|
|
48
45
|
deleteSavedFilter,
|
|
49
46
|
type SavedFilterPreset,
|
|
50
47
|
} from '@/lib/search/saved-filters';
|
|
51
|
-
|
|
52
|
-
// ── Op constants ──────────────────────────────────────────────────────
|
|
53
|
-
|
|
54
|
-
const SET_OPS: SetOp[] = ['in', 'notIn'];
|
|
55
|
-
const STRING_OPS: StringOp[] = ['eq', 'ne', 'contains', 'notContains', 'startsWith'];
|
|
56
|
-
const VALUE_OPS: ValueOp[] = [
|
|
57
|
-
'eq', 'ne', 'contains', 'notContains', 'gt', 'gte', 'lt', 'lte', 'isSet', 'isNotSet',
|
|
58
|
-
];
|
|
59
|
-
const NUMERIC_OPS: NumericOp[] = ['eq', 'ne', 'gt', 'gte', 'lt', 'lte'];
|
|
60
|
-
|
|
61
|
-
const OP_LABEL: Record<string, string> = {
|
|
62
|
-
in: 'is one of', notIn: 'is not one of',
|
|
63
|
-
eq: '=', ne: '≠',
|
|
64
|
-
contains: 'contains', notContains: 'does not contain',
|
|
65
|
-
startsWith: 'starts with',
|
|
66
|
-
gt: '>', gte: '≥', lt: '<', lte: '≤',
|
|
67
|
-
isSet: 'is set', isNotSet: 'is not set',
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const RULE_KIND_LABEL: Record<FilterRule['kind'], string> = {
|
|
71
|
-
storey: 'Storey',
|
|
72
|
-
ifcType: 'IFC Type',
|
|
73
|
-
predefinedType: 'Predefined Type',
|
|
74
|
-
name: 'Name',
|
|
75
|
-
property: 'Property',
|
|
76
|
-
quantity: 'Quantity',
|
|
77
|
-
};
|
|
48
|
+
import { RuleRow, RULE_KIND_LABEL } from './SearchModal.filter.editors';
|
|
78
49
|
|
|
79
50
|
export function SearchModalFilterBuilder() {
|
|
80
51
|
const {
|
|
@@ -91,6 +62,7 @@ export function SearchModalFilterBuilder() {
|
|
|
91
62
|
clearFilterRules,
|
|
92
63
|
setFilterSchema,
|
|
93
64
|
setFilterPsetQtoSchema,
|
|
65
|
+
setFilterValueSchema,
|
|
94
66
|
setSearchFilter,
|
|
95
67
|
} = useViewerStore(
|
|
96
68
|
useShallow((s) => ({
|
|
@@ -107,6 +79,7 @@ export function SearchModalFilterBuilder() {
|
|
|
107
79
|
clearFilterRules: s.clearFilterRules,
|
|
108
80
|
setFilterSchema: s.setFilterSchema,
|
|
109
81
|
setFilterPsetQtoSchema: s.setFilterPsetQtoSchema,
|
|
82
|
+
setFilterValueSchema: s.setFilterValueSchema,
|
|
110
83
|
setSearchFilter: s.setSearchFilter,
|
|
111
84
|
})),
|
|
112
85
|
);
|
|
@@ -134,6 +107,20 @@ export function SearchModalFilterBuilder() {
|
|
|
134
107
|
setFilterPsetQtoSchema(activeModelId, discoverPropertyAndQuantitySchema(activeStore));
|
|
135
108
|
}, [activeModelId, activeStore, filter.rules, schemaMap, setFilterPsetQtoSchema]);
|
|
136
109
|
|
|
110
|
+
// Lazy value discovery — distinct material / classification / property
|
|
111
|
+
// values for the chip value suggestions. Fired the first time a rule that
|
|
112
|
+
// benefits from them (property, material, classification) appears.
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (!activeModelId || !activeStore) return;
|
|
115
|
+
const entry = schemaMap.get(activeModelId);
|
|
116
|
+
if (entry?.values) return;
|
|
117
|
+
const needs = filter.rules.some(
|
|
118
|
+
(r) => r.kind === 'property' || r.kind === 'material' || r.kind === 'classification',
|
|
119
|
+
);
|
|
120
|
+
if (!needs) return;
|
|
121
|
+
setFilterValueSchema(activeModelId, discoverFilterValues(activeStore));
|
|
122
|
+
}, [activeModelId, activeStore, filter.rules, schemaMap, setFilterValueSchema]);
|
|
123
|
+
|
|
137
124
|
const ifcTypeOptions = useMemo<string[]>(() => {
|
|
138
125
|
if (schemaEntry?.basic.ifcTypes && schemaEntry.basic.ifcTypes.length > 0) {
|
|
139
126
|
return schemaEntry.basic.ifcTypes;
|
|
@@ -153,6 +140,9 @@ export function SearchModalFilterBuilder() {
|
|
|
153
140
|
case 'name': rule = Rule.name('contains', ''); break;
|
|
154
141
|
case 'property': rule = Rule.property('', '', 'eq', ''); break;
|
|
155
142
|
case 'quantity': rule = Rule.quantity('', '', 'gt', 0); break;
|
|
143
|
+
case 'material': rule = Rule.material('contains', ''); break;
|
|
144
|
+
case 'classification': rule = Rule.classification('', 'contains', ''); break;
|
|
145
|
+
case 'elevation': rule = Rule.elevation('gt', 0); break;
|
|
156
146
|
}
|
|
157
147
|
addFilterRule(rule);
|
|
158
148
|
}, [addFilterRule]);
|
|
@@ -254,7 +244,8 @@ export function SearchModalFilterBuilder() {
|
|
|
254
244
|
<div className="flex flex-col gap-2">
|
|
255
245
|
{filter.rules.length === 0 && (
|
|
256
246
|
<p className="rounded border border-dashed border-zinc-300 bg-zinc-50 px-3 py-3 text-center text-xs italic text-muted-foreground dark:border-zinc-800 dark:bg-zinc-900/30">
|
|
257
|
-
Add a rule to start filtering — pick by storey, IFC type, name,
|
|
247
|
+
Add a rule to start filtering — pick by storey, IFC type, name,
|
|
248
|
+
property, quantity, material, classification, or elevation.
|
|
258
249
|
</p>
|
|
259
250
|
)}
|
|
260
251
|
{filter.rules.map((rule, i) => (
|
|
@@ -264,6 +255,7 @@ export function SearchModalFilterBuilder() {
|
|
|
264
255
|
ifcTypeOptions={ifcTypeOptions}
|
|
265
256
|
storeyOptions={storeyOptions}
|
|
266
257
|
psetQto={schemaEntry?.psetQto ?? null}
|
|
258
|
+
valueSchema={schemaEntry?.values ?? null}
|
|
267
259
|
onChange={(next) => updateFilterRule(i, next)}
|
|
268
260
|
onRemove={() => removeFilterRule(i)}
|
|
269
261
|
/>
|
|
@@ -400,367 +392,6 @@ function AddRuleMenu({
|
|
|
400
392
|
);
|
|
401
393
|
}
|
|
402
394
|
|
|
403
|
-
interface RuleRowProps {
|
|
404
|
-
rule: FilterRule;
|
|
405
|
-
ifcTypeOptions: string[];
|
|
406
|
-
storeyOptions: ReadonlyArray<readonly [string, number | null]>;
|
|
407
|
-
psetQto: { psets: ReadonlyArray<readonly [string, ReadonlyArray<string>]>; qtos: ReadonlyArray<readonly [string, ReadonlyArray<readonly [string, string]>]> } | null;
|
|
408
|
-
onChange: (next: FilterRule) => void;
|
|
409
|
-
onRemove: () => void;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function RuleRow({ rule, ifcTypeOptions, storeyOptions, psetQto, onChange, onRemove }: RuleRowProps) {
|
|
413
|
-
return (
|
|
414
|
-
<div className="flex flex-wrap items-center gap-1.5 rounded border border-zinc-200 bg-white px-2 py-1.5 dark:border-zinc-800 dark:bg-zinc-950">
|
|
415
|
-
<span className="rounded bg-zinc-100 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">
|
|
416
|
-
{RULE_KIND_LABEL[rule.kind]}
|
|
417
|
-
</span>
|
|
418
|
-
|
|
419
|
-
{rule.kind === 'storey' && (
|
|
420
|
-
<SetRuleEditor
|
|
421
|
-
values={rule.values}
|
|
422
|
-
op={rule.op}
|
|
423
|
-
options={storeyOptions.map(([name, elev]) => ({
|
|
424
|
-
label: elev != null ? `${name} (${elev.toFixed(2)} m)` : name,
|
|
425
|
-
value: name,
|
|
426
|
-
}))}
|
|
427
|
-
onChange={(values, op) => onChange(Rule.storey(values, op))}
|
|
428
|
-
/>
|
|
429
|
-
)}
|
|
430
|
-
|
|
431
|
-
{rule.kind === 'ifcType' && (
|
|
432
|
-
<SetRuleEditor
|
|
433
|
-
values={rule.values}
|
|
434
|
-
op={rule.op}
|
|
435
|
-
options={ifcTypeOptions.map((t) => ({ label: t, value: t }))}
|
|
436
|
-
onChange={(values, op) => onChange(Rule.ifcType(values, op))}
|
|
437
|
-
/>
|
|
438
|
-
)}
|
|
439
|
-
|
|
440
|
-
{rule.kind === 'predefinedType' && (
|
|
441
|
-
<PredefinedTypeEditor
|
|
442
|
-
values={rule.values}
|
|
443
|
-
op={rule.op}
|
|
444
|
-
onChange={(values, op) => onChange(Rule.predefinedType(values, op))}
|
|
445
|
-
/>
|
|
446
|
-
)}
|
|
447
|
-
|
|
448
|
-
{rule.kind === 'name' && (
|
|
449
|
-
<NameEditor
|
|
450
|
-
op={rule.op}
|
|
451
|
-
value={rule.value}
|
|
452
|
-
onChange={(op, value) => onChange(Rule.name(op, value))}
|
|
453
|
-
/>
|
|
454
|
-
)}
|
|
455
|
-
|
|
456
|
-
{rule.kind === 'property' && (
|
|
457
|
-
<PropertyEditor rule={rule} psetQto={psetQto} onChange={onChange} />
|
|
458
|
-
)}
|
|
459
|
-
|
|
460
|
-
{rule.kind === 'quantity' && (
|
|
461
|
-
<QuantityEditor rule={rule} psetQto={psetQto} onChange={onChange} />
|
|
462
|
-
)}
|
|
463
|
-
|
|
464
|
-
<button
|
|
465
|
-
type="button"
|
|
466
|
-
onClick={onRemove}
|
|
467
|
-
aria-label="Remove rule"
|
|
468
|
-
className="ml-auto rounded p-1 text-muted-foreground hover:bg-zinc-100 hover:text-foreground dark:hover:bg-zinc-800"
|
|
469
|
-
>
|
|
470
|
-
<Trash2 className="h-3 w-3" />
|
|
471
|
-
</button>
|
|
472
|
-
</div>
|
|
473
|
-
);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// ── Per-kind editors ──────────────────────────────────────────────────
|
|
477
|
-
|
|
478
|
-
interface SetRuleEditorProps {
|
|
479
|
-
values: string[];
|
|
480
|
-
op: SetOp;
|
|
481
|
-
options: Array<{ label: string; value: string }>;
|
|
482
|
-
onChange: (values: string[], op: SetOp) => void;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function SetRuleEditor({ values, op, options, onChange }: SetRuleEditorProps) {
|
|
486
|
-
const toggle = (v: string) => {
|
|
487
|
-
const next = values.includes(v) ? values.filter((x) => x !== v) : [...values, v];
|
|
488
|
-
onChange(next, op);
|
|
489
|
-
};
|
|
490
|
-
return (
|
|
491
|
-
<>
|
|
492
|
-
<OpDropdown ops={SET_OPS} value={op} onChange={(next) => onChange(values, next)} />
|
|
493
|
-
<DropdownMenu>
|
|
494
|
-
<DropdownMenuTrigger asChild>
|
|
495
|
-
<Button variant="outline" size="sm" className="h-7 gap-1 text-xs font-mono">
|
|
496
|
-
{values.length === 0 ? 'Pick values…' : `${values.length} selected`}
|
|
497
|
-
</Button>
|
|
498
|
-
</DropdownMenuTrigger>
|
|
499
|
-
<DropdownMenuContent align="start" className="max-h-72 overflow-y-auto">
|
|
500
|
-
{options.length === 0 && (
|
|
501
|
-
<DropdownMenuItem disabled className="text-muted-foreground italic">
|
|
502
|
-
No options available — load a model first.
|
|
503
|
-
</DropdownMenuItem>
|
|
504
|
-
)}
|
|
505
|
-
{options.map((o) => (
|
|
506
|
-
<DropdownMenuItem
|
|
507
|
-
key={o.value}
|
|
508
|
-
onSelect={(e) => {
|
|
509
|
-
// Keep the menu open for multi-select.
|
|
510
|
-
e.preventDefault();
|
|
511
|
-
toggle(o.value);
|
|
512
|
-
}}
|
|
513
|
-
className="font-mono"
|
|
514
|
-
>
|
|
515
|
-
<span className="mr-2 inline-block w-3 text-center">
|
|
516
|
-
{values.includes(o.value) ? '✓' : ''}
|
|
517
|
-
</span>
|
|
518
|
-
{o.label}
|
|
519
|
-
</DropdownMenuItem>
|
|
520
|
-
))}
|
|
521
|
-
</DropdownMenuContent>
|
|
522
|
-
</DropdownMenu>
|
|
523
|
-
{values.length > 0 && (
|
|
524
|
-
<div className="flex flex-wrap items-center gap-1">
|
|
525
|
-
{values.map((v) => (
|
|
526
|
-
<span
|
|
527
|
-
key={v}
|
|
528
|
-
className="inline-flex items-center gap-1 rounded bg-zinc-100 px-1.5 py-0.5 text-[10px] font-mono dark:bg-zinc-800"
|
|
529
|
-
>
|
|
530
|
-
{v}
|
|
531
|
-
<button
|
|
532
|
-
type="button"
|
|
533
|
-
aria-label={`Remove ${v}`}
|
|
534
|
-
onClick={() => toggle(v)}
|
|
535
|
-
className="text-muted-foreground hover:text-foreground"
|
|
536
|
-
>
|
|
537
|
-
×
|
|
538
|
-
</button>
|
|
539
|
-
</span>
|
|
540
|
-
))}
|
|
541
|
-
</div>
|
|
542
|
-
)}
|
|
543
|
-
</>
|
|
544
|
-
);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
function PredefinedTypeEditor({
|
|
548
|
-
values,
|
|
549
|
-
op,
|
|
550
|
-
onChange,
|
|
551
|
-
}: {
|
|
552
|
-
values: string[];
|
|
553
|
-
op: SetOp;
|
|
554
|
-
onChange: (values: string[], op: SetOp) => void;
|
|
555
|
-
}) {
|
|
556
|
-
// Predefined types aren't materialised in the parser today — pick
|
|
557
|
-
// them via free-text. The user enters comma-separated values.
|
|
558
|
-
const text = values.join(', ');
|
|
559
|
-
return (
|
|
560
|
-
<>
|
|
561
|
-
<OpDropdown ops={SET_OPS} value={op} onChange={(next) => onChange(values, next)} />
|
|
562
|
-
<Input
|
|
563
|
-
placeholder="e.g. SOLIDWALL, PARTITIONING"
|
|
564
|
-
value={text}
|
|
565
|
-
onChange={(e) =>
|
|
566
|
-
onChange(
|
|
567
|
-
e.target.value.split(',').map((s) => s.trim()).filter((s) => s.length > 0),
|
|
568
|
-
op,
|
|
569
|
-
)
|
|
570
|
-
}
|
|
571
|
-
className="h-7 w-72 text-xs font-mono"
|
|
572
|
-
/>
|
|
573
|
-
</>
|
|
574
|
-
);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
function NameEditor({
|
|
578
|
-
op,
|
|
579
|
-
value,
|
|
580
|
-
onChange,
|
|
581
|
-
}: {
|
|
582
|
-
op: StringOp;
|
|
583
|
-
value: string;
|
|
584
|
-
onChange: (op: StringOp, value: string) => void;
|
|
585
|
-
}) {
|
|
586
|
-
return (
|
|
587
|
-
<>
|
|
588
|
-
<OpDropdown ops={STRING_OPS} value={op} onChange={(next) => onChange(next, value)} />
|
|
589
|
-
<Input
|
|
590
|
-
placeholder="text"
|
|
591
|
-
value={value}
|
|
592
|
-
onChange={(e) => onChange(op, e.target.value)}
|
|
593
|
-
className="h-7 w-56 text-xs font-mono"
|
|
594
|
-
/>
|
|
595
|
-
</>
|
|
596
|
-
);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
interface PropertyEditorProps {
|
|
600
|
-
rule: Extract<FilterRule, { kind: 'property' }>;
|
|
601
|
-
psetQto: RuleRowProps['psetQto'];
|
|
602
|
-
onChange: (next: FilterRule) => void;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
function PropertyEditor({ rule, psetQto, onChange }: PropertyEditorProps) {
|
|
606
|
-
const psetNames = useMemo(() => (psetQto ? psetQto.psets.map(([n]) => n) : []), [psetQto]);
|
|
607
|
-
const propNames = useMemo(() => {
|
|
608
|
-
if (!psetQto) return [];
|
|
609
|
-
const entry = psetQto.psets.find(([n]) => n === rule.setName);
|
|
610
|
-
return entry ? Array.from(entry[1]) : [];
|
|
611
|
-
}, [psetQto, rule.setName]);
|
|
612
|
-
|
|
613
|
-
const valueless = rule.op === 'isSet' || rule.op === 'isNotSet';
|
|
614
|
-
|
|
615
|
-
return (
|
|
616
|
-
<>
|
|
617
|
-
<FreeOrPickInput
|
|
618
|
-
placeholder="Pset_… (e.g. Pset_WallCommon)"
|
|
619
|
-
value={rule.setName}
|
|
620
|
-
options={psetNames}
|
|
621
|
-
widthClass="w-52"
|
|
622
|
-
onChange={(next) => onChange({ ...rule, setName: next, propertyName: '' })}
|
|
623
|
-
/>
|
|
624
|
-
<span className="text-muted-foreground">.</span>
|
|
625
|
-
<FreeOrPickInput
|
|
626
|
-
placeholder="prop name"
|
|
627
|
-
value={rule.propertyName}
|
|
628
|
-
options={propNames}
|
|
629
|
-
widthClass="w-44"
|
|
630
|
-
onChange={(next) => onChange({ ...rule, propertyName: next })}
|
|
631
|
-
/>
|
|
632
|
-
<OpDropdown ops={VALUE_OPS} value={rule.op} onChange={(next) => onChange({ ...rule, op: next })} />
|
|
633
|
-
{!valueless && (
|
|
634
|
-
<Input
|
|
635
|
-
placeholder="value"
|
|
636
|
-
value={rule.value}
|
|
637
|
-
onChange={(e) => onChange({ ...rule, value: e.target.value })}
|
|
638
|
-
className="h-7 w-40 text-xs font-mono"
|
|
639
|
-
/>
|
|
640
|
-
)}
|
|
641
|
-
</>
|
|
642
|
-
);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
interface QuantityEditorProps {
|
|
646
|
-
rule: Extract<FilterRule, { kind: 'quantity' }>;
|
|
647
|
-
psetQto: RuleRowProps['psetQto'];
|
|
648
|
-
onChange: (next: FilterRule) => void;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
function QuantityEditor({ rule, psetQto, onChange }: QuantityEditorProps) {
|
|
652
|
-
const qsetNames = useMemo(() => (psetQto ? psetQto.qtos.map(([n]) => n) : []), [psetQto]);
|
|
653
|
-
const qtyNames = useMemo(() => {
|
|
654
|
-
if (!psetQto) return [];
|
|
655
|
-
const entry = psetQto.qtos.find(([n]) => n === rule.setName);
|
|
656
|
-
return entry ? entry[1].map(([n]) => n) : [];
|
|
657
|
-
}, [psetQto, rule.setName]);
|
|
658
|
-
|
|
659
|
-
return (
|
|
660
|
-
<>
|
|
661
|
-
<FreeOrPickInput
|
|
662
|
-
placeholder="Qto_… (e.g. Qto_WallBaseQuantities)"
|
|
663
|
-
value={rule.setName}
|
|
664
|
-
options={qsetNames}
|
|
665
|
-
widthClass="w-56"
|
|
666
|
-
onChange={(next) => onChange({ ...rule, setName: next, quantityName: '' })}
|
|
667
|
-
/>
|
|
668
|
-
<span className="text-muted-foreground">.</span>
|
|
669
|
-
<FreeOrPickInput
|
|
670
|
-
placeholder="quantity name"
|
|
671
|
-
value={rule.quantityName}
|
|
672
|
-
options={qtyNames}
|
|
673
|
-
widthClass="w-44"
|
|
674
|
-
onChange={(next) => onChange({ ...rule, quantityName: next })}
|
|
675
|
-
/>
|
|
676
|
-
<OpDropdown ops={NUMERIC_OPS} value={rule.op} onChange={(next) => onChange({ ...rule, op: next })} />
|
|
677
|
-
<Input
|
|
678
|
-
type="number"
|
|
679
|
-
placeholder="value"
|
|
680
|
-
value={rule.value}
|
|
681
|
-
onChange={(e) => onChange({ ...rule, value: Number.parseFloat(e.target.value) || 0 })}
|
|
682
|
-
className="h-7 w-32 text-xs font-mono"
|
|
683
|
-
/>
|
|
684
|
-
</>
|
|
685
|
-
);
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
// ── Building-block widgets ───────────────────────────────────────────
|
|
689
|
-
|
|
690
|
-
function OpDropdown<T extends string>({
|
|
691
|
-
ops,
|
|
692
|
-
value,
|
|
693
|
-
onChange,
|
|
694
|
-
}: {
|
|
695
|
-
ops: ReadonlyArray<T>;
|
|
696
|
-
value: T;
|
|
697
|
-
onChange: (next: T) => void;
|
|
698
|
-
}) {
|
|
699
|
-
return (
|
|
700
|
-
<DropdownMenu>
|
|
701
|
-
<DropdownMenuTrigger asChild>
|
|
702
|
-
<Button variant="outline" size="sm" className="h-7 min-w-[3.5rem] gap-1 text-xs font-mono">
|
|
703
|
-
{OP_LABEL[value] ?? value}
|
|
704
|
-
</Button>
|
|
705
|
-
</DropdownMenuTrigger>
|
|
706
|
-
<DropdownMenuContent>
|
|
707
|
-
{ops.map((op) => (
|
|
708
|
-
<DropdownMenuItem key={op} onSelect={() => onChange(op)} className="font-mono">
|
|
709
|
-
{OP_LABEL[op] ?? op}
|
|
710
|
-
<span className="ml-2 text-[10px] text-muted-foreground">{op}</span>
|
|
711
|
-
</DropdownMenuItem>
|
|
712
|
-
))}
|
|
713
|
-
</DropdownMenuContent>
|
|
714
|
-
</DropdownMenu>
|
|
715
|
-
);
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
/**
|
|
719
|
-
* Free-text input that exposes a small dropdown of known options when
|
|
720
|
-
* the schema knows them. Users can either pick from the menu or type a
|
|
721
|
-
* value not present in the schema (useful for typos / custom psets).
|
|
722
|
-
*/
|
|
723
|
-
function FreeOrPickInput({
|
|
724
|
-
placeholder,
|
|
725
|
-
value,
|
|
726
|
-
options,
|
|
727
|
-
widthClass,
|
|
728
|
-
onChange,
|
|
729
|
-
}: {
|
|
730
|
-
placeholder: string;
|
|
731
|
-
value: string;
|
|
732
|
-
options: ReadonlyArray<string>;
|
|
733
|
-
widthClass: string;
|
|
734
|
-
onChange: (next: string) => void;
|
|
735
|
-
}) {
|
|
736
|
-
return (
|
|
737
|
-
<div className="relative inline-flex items-center gap-1">
|
|
738
|
-
<Input
|
|
739
|
-
placeholder={placeholder}
|
|
740
|
-
value={value}
|
|
741
|
-
onChange={(e) => onChange(e.target.value)}
|
|
742
|
-
className={`h-7 ${widthClass} text-xs font-mono`}
|
|
743
|
-
/>
|
|
744
|
-
{options.length > 0 && (
|
|
745
|
-
<DropdownMenu>
|
|
746
|
-
<DropdownMenuTrigger asChild>
|
|
747
|
-
<Button variant="ghost" size="sm" className="h-7 px-1 text-[10px] text-muted-foreground" title="Pick from schema">
|
|
748
|
-
▾
|
|
749
|
-
</Button>
|
|
750
|
-
</DropdownMenuTrigger>
|
|
751
|
-
<DropdownMenuContent align="start" className="max-h-72 overflow-y-auto">
|
|
752
|
-
{options.map((o) => (
|
|
753
|
-
<DropdownMenuItem key={o} onSelect={() => onChange(o)} className="font-mono">
|
|
754
|
-
{o}
|
|
755
|
-
</DropdownMenuItem>
|
|
756
|
-
))}
|
|
757
|
-
</DropdownMenuContent>
|
|
758
|
-
</DropdownMenu>
|
|
759
|
-
)}
|
|
760
|
-
</div>
|
|
761
|
-
);
|
|
762
|
-
}
|
|
763
|
-
|
|
764
395
|
function truncate(s: string, max: number): string {
|
|
765
396
|
return s.length <= max ? s : s.slice(0, max - 1) + '…';
|
|
766
397
|
}
|