Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions components/model-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ interface PropertyRowProps {
icon?: React.ReactNode;
copyValue?: string;
t?: (key: string, options?: any) => string;
selectPath?: string[];
}

const PropertyRow: React.FC<PropertyRowProps> = ({
Expand All @@ -211,11 +212,17 @@ const PropertyRow: React.FC<PropertyRowProps> = ({
icon,
copyValue,
t,
selectPath,
}) => {
const { ifcApi } = useIFCContext();
const { ifcApi, selectElementsByProperty } = useIFCContext();
const handleCopy = () => {
if (copyValue !== undefined) navigator.clipboard.writeText(copyValue);
};
const handleSelect = () => {
if (selectPath) {
selectElementsByProperty(selectPath, propValue);
}
};
Comment on lines +221 to +225
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Normalize selection payload before calling selectElementsByProperty.

Pass the underlying value(s) for unit-wrapped and {value,type} shapes; skip null/empty.

-  const handleSelect = () => {
-    if (selectPath) {
-      selectElementsByProperty(selectPath, propValue);
-    }
-  };
+  const handleSelect = () => {
+    if (!selectPath) return;
+    const v = propValue as any;
+    const normalized =
+      v && typeof v === "object"
+        ? Array.isArray(v.values)
+          ? v.values
+          : (v.value ?? v)
+        : v;
+    if (normalized !== undefined && normalized !== null && normalized !== "") {
+      selectElementsByProperty(selectPath, normalized);
+    }
+  };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleSelect = () => {
if (selectPath) {
selectElementsByProperty(selectPath, propValue);
}
};
const handleSelect = () => {
if (!selectPath) return;
const v = propValue as any;
const normalized =
v && typeof v === "object"
? Array.isArray(v.values)
? v.values
: (v.value ?? v)
: v;
if (normalized !== undefined && normalized !== null && normalized !== "") {
selectElementsByProperty(selectPath, normalized);
}
};
🤖 Prompt for AI Agents
In components/model-info.tsx around lines 221 to 225, the handleSelect callback
passes propValue directly to selectElementsByProperty; normalize the payload
first by unwrapping unit-wrapped values and {value,type} objects to their
underlying primitive(s), supporting both single values and arrays, and filter
out null/undefined/empty-string entries so you never call
selectElementsByProperty with empty or wrapper objects; finally call
selectElementsByProperty(selectPath, normalizedValue) only when normalizedValue
has at least one valid entry.

return (
<div className="grid grid-cols-[auto_1fr] gap-x-3 items-start py-1.5 border-b border-border/50 last:border-b-0">
<div className="flex items-center text-muted-foreground text-xs font-medium">
Expand All @@ -233,6 +240,20 @@ const PropertyRow: React.FC<PropertyRowProps> = ({
}
>
{renderPropertyValue(propValue, propKey, ifcApi, t)}
{selectPath && (
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<button onClick={handleSelect} className="opacity-60 hover:opacity-100">
<MousePointer2 className="w-3 h-3" />
</button>
</TooltipTrigger>
<TooltipContent>
{t?.('selectAllMatching') || 'Select all'}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{copyValue !== undefined && (
<button onClick={handleCopy} className="opacity-60 hover:opacity-100">
<Copy className="w-3 h-3" />
Expand Down Expand Up @@ -361,6 +382,7 @@ export function ModelInfo() {
getNaturalIfcClassName,
getClassificationsForElement,
getElementPropertiesCached,
selectElementsByProperty,
} = useIFCContext();
const { t, i18n } = useTranslation();

Expand Down Expand Up @@ -594,8 +616,26 @@ export function ModelInfo() {
<Box className="w-3.5 h-3.5 mr-1.5 opacity-80" />
<span>{t('IFC Class')}:</span>
</div>
<div className="text-xs truncate text-right font-medium">
<div className="text-xs truncate text-right font-medium flex items-center justify-end gap-1">
{ifcType || 'Unknown'}
{ifcType && (
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={(e) => {
e.stopPropagation();
selectElementsByProperty(['ifcType'], ifcType);
}}
className="opacity-60 hover:opacity-100"
>
<MousePointer2 className="w-3 h-3" />
</button>
</TooltipTrigger>
<TooltipContent>
{t('selectAllMatching')}
</TooltipContent>
</Tooltip>
)}
</div>
</div>
</TooltipTrigger>
Expand Down Expand Up @@ -704,6 +744,7 @@ export function ModelInfo() {
propValue={value}
icon={getPropertyIcon(key)}
t={t}
selectPath={['attributes', key]}
/>
))}
</CollapsibleSection>
Expand Down Expand Up @@ -786,6 +827,7 @@ export function ModelInfo() {
propValue={propValue}
icon={getPropertyIcon(propName)}
t={t}
selectPath={['propertySets', psetName, propName]}
/>
),
)
Expand Down Expand Up @@ -821,6 +863,7 @@ export function ModelInfo() {
propValue={propValue}
icon={getPropertyIcon(propName)}
t={t}
selectPath={['typeSets', psetName, propName]}
/>
))}
</CollapsibleSection>
Expand Down
40 changes: 40 additions & 0 deletions context/ifc-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import { exportClassificationsToExcel } from "@/services/classification-export-s
import { parseClassificationsFromExcel } from "@/services/classification-import-service";
import { getBufferedConsole, type ConsoleUpdate } from "@/services/buffered-console-service";

function deepEqual(a: any, b: any) {
return JSON.stringify(a) === JSON.stringify(b);
}
Comment on lines +25 to +27
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid JSON.stringify-based deep equality (NaN/-0/cycles pitfalls).

JSON.stringify conflates NaN and null, doesn’t handle -0, and can throw on cycles. Also many IFC values are wrapped as { value, unit }.

Apply this diff to make equality safer and tolerant of common IFC wrappers:

-function deepEqual(a: any, b: any) {
-  return JSON.stringify(a) === JSON.stringify(b);
-}
+function deepEqual(a: any, b: any) {
+  // Unwrap simple IFC value wrappers
+  const unwrap = (v: any) =>
+    v && typeof v === "object" && "value" in v ? (v as any).value : v;
+  a = unwrap(a);
+  b = unwrap(b);
+  // Handle NaN and -0 correctly
+  if (typeof a === "number" || typeof b === "number") {
+    if (Number.isNaN(a) && Number.isNaN(b)) return true;
+    return Object.is(a, b);
+  }
+  // Fast path for primitives and reference equality
+  if (a === b) return true;
+  if (
+    a === null ||
+    b === null ||
+    typeof a !== "object" ||
+    typeof b !== "object"
+  ) {
+    return false;
+  }
+  // Best-effort structural compare without bringing a new dep
+  try {
+    return JSON.stringify(a) === JSON.stringify(b);
+  } catch {
+    return false;
+  }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function deepEqual(a: any, b: any) {
return JSON.stringify(a) === JSON.stringify(b);
}
function deepEqual(a: any, b: any) {
// Unwrap simple IFC value wrappers
const unwrap = (v: any) =>
v && typeof v === "object" && "value" in v ? (v as any).value : v;
a = unwrap(a);
b = unwrap(b);
// Handle NaN and -0 correctly
if (typeof a === "number" || typeof b === "number") {
if (Number.isNaN(a) && Number.isNaN(b)) return true;
return Object.is(a, b);
}
// Fast path for primitives and reference equality
if (a === b) return true;
if (
a === null ||
b === null ||
typeof a !== "object" ||
typeof b !== "object"
) {
return false;
}
// Best-effort structural compare without bringing a new dep
try {
return JSON.stringify(a) === JSON.stringify(b);
} catch {
return false;
}
}


// Define interfaces for progress tracking
export interface RuleProgress {
active: boolean;
Expand Down Expand Up @@ -132,6 +136,7 @@ interface IFCContextType {
selectElements: (selection: SelectedElementInfo[]) => void;
toggleElementSelection: (element: SelectedElementInfo, additive: boolean) => void;
clearSelection: () => void;
selectElementsByProperty: (path: string[], value: any) => Promise<void>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify callsites pass canonical paths/values.

Ensure all calls pass paths rooted at the ParsedElementProperties shape and raw values (not formatted strings with units).

Run:


🏁 Script executed:

#!/bin/bash
# Inspect callsites and passed shapes
rg -n --type=ts --type=tsx -C2 'selectElementsByProperty\\('
rg -n --type=ts --type=tsx -C3 'selectPath\\s*:' | sed -n '1,200p'

Length of output: 132


🏁 Script executed:

#!/bin/bash
# Search for callsites of selectElementsByProperty in all .ts and .tsx files
rg -n --glob '*.ts' --glob '*.tsx' -C2 'selectElementsByProperty\(' .

# Search for usages of selectPath property in all .ts and .tsx files
rg -n --glob '*.ts' --glob '*.tsx' -C3 'selectPath\s*:' .

Length of output: 706


🏁 Script executed:

#!/bin/bash
# Extract component logic around first callsite
sed -n '180,260p' components/model-info.tsx

# Extract component logic around second callsite
sed -n '600,680p' components/model-info.tsx

# Show context hook destructuring
rg -n "useIfcContext" -C5 components/model-info.tsx

# Show how propValue is defined or updated
rg -n "propValue" -C10 components/model-info.tsx

# Confirm selectPath origin
rg -n "selectPath" -C5 components/model-info.tsx

Length of output: 15600


🏁 Script executed:

#!/bin/bash
# Print first 200 lines of context/ifc-context.tsx
sed -n '1,200p' context/ifc-context.tsx

# Find ParsedElementProperties definition
rg -n "ParsedElementProperties" -C5 context/ifc-context.tsx

# Find selectElementsByProperty signature
rg -n "selectElementsByProperty" -C5 context/ifc-context.tsx

Length of output: 10945


🏁 Script executed:

#!/bin/bash
# Locate and display the ParsedElementProperties interface definition
rg -n "interface ParsedElementProperties" -C5 .

Length of output: 662


Correct selectPath for typeSets usage

  • In components/model-info.tsx, update
    selectPath={['typeSets', psetName, propName]}
    to
    selectPath={['propertySets', psetName, propName]}
    so it aligns with ParsedElementProperties.propertySets.
🤖 Prompt for AI Agents
In context/ifc-context.tsx around line 139, the review notes that
components/model-info.tsx is passing selectPath=['typeSets', psetName, propName]
but ParsedElementProperties uses propertySets; update usages to
selectPath=['propertySets', psetName, propName] so the selector targets the
correct field. Find all occurrences where 'typeSets' is used for property
selection in that component (or related components) and replace them with
'propertySets', ensuring psetName and propName remain the same and
tests/components referencing the old path are updated accordingly.

toggleClassificationHighlight: (classificationCode: string) => void;
setElementProperties: (properties: any | null) => void;
setAvailableCategoriesForModel: (
Expand Down Expand Up @@ -1577,6 +1582,40 @@ export function IFCContextProvider({ children }: { children: ReactNode }) {
setElementPropertiesInternal,
]);

const selectElementsByProperty = useCallback(
async (path: string[], value: any) => {
if (!ifcApiInternal) return;
const matches: SelectedElementInfo[] = [];
for (const model of loadedModels) {
if (model.modelID == null) continue;
const elements = IFCElementExtractor.getAllElements(ifcApiInternal, model.modelID);
for (const el of elements) {
try {
const props = await PropertyCache.getProperties(
ifcApiInternal,
model.modelID,
el.expressID,
);
let current: any = props;
for (const part of path) {
if (current == null) break;
current = current[part as keyof typeof current];
}
if (current !== undefined && deepEqual(current, value)) {
matches.push({ modelID: model.modelID, expressID: el.expressID });
}
} catch {
// Ignore property fetch errors
}
}
}
if (matches.length > 0) {
selectElements(matches);
}
},
[ifcApiInternal, loadedModels, selectElements],
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make selection more robust and responsive (init properties, normalize values, yield during long scans).

Avoid UI stalls with large models and ensure property helper is initialized; compare normalized values (unwrap {value}).

Apply this diff:

 const selectElementsByProperty = useCallback(
   async (path: string[], value: any) => {
-      if (!ifcApiInternal) return;
+      if (!ifcApiInternal) return;
+      // Ensure properties helper is available
+      if (!ifcApiInternal.properties) {
+        try {
+          ifcApiInternal.properties = new Properties(ifcApiInternal);
+        } catch (e) {
+          console.error("IFCContext: Failed to init properties in selectElementsByProperty", e);
+          return;
+        }
+      }
       const matches: SelectedElementInfo[] = [];
+      let processed = 0;
+      // Normalize once for reliable comparisons
+      const unwrap = (v: any) =>
+        v && typeof v === "object" && "value" in v ? (v as any).value : v;
+      const target = unwrap(value);
       for (const model of loadedModels) {
         if (model.modelID == null) continue;
         const elements = IFCElementExtractor.getAllElements(ifcApiInternal, model.modelID);
         for (const el of elements) {
           try {
             const props = await PropertyCache.getProperties(
               ifcApiInternal,
               model.modelID,
               el.expressID,
             );
             let current: any = props;
             for (const part of path) {
               if (current == null) break;
               current = current[part as keyof typeof current];
             }
-            if (current !== undefined && deepEqual(current, value)) {
+            if (current !== undefined && deepEqual(unwrap(current), target)) {
               matches.push({ modelID: model.modelID, expressID: el.expressID });
             }
           } catch {
             // Ignore property fetch errors
           }
+          // Yield periodically to keep UI responsive on large models
+          processed++;
+          if (processed % 250 === 0) {
+            await (async () => new Promise((r) => setTimeout(r, 0)))();
+          }
         }
       }
       if (matches.length > 0) {
         selectElements(matches);
       }
     },
     [ifcApiInternal, loadedModels, selectElements],
   );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const selectElementsByProperty = useCallback(
async (path: string[], value: any) => {
if (!ifcApiInternal) return;
const matches: SelectedElementInfo[] = [];
for (const model of loadedModels) {
if (model.modelID == null) continue;
const elements = IFCElementExtractor.getAllElements(ifcApiInternal, model.modelID);
for (const el of elements) {
try {
const props = await PropertyCache.getProperties(
ifcApiInternal,
model.modelID,
el.expressID,
);
let current: any = props;
for (const part of path) {
if (current == null) break;
current = current[part as keyof typeof current];
}
if (current !== undefined && deepEqual(current, value)) {
matches.push({ modelID: model.modelID, expressID: el.expressID });
}
} catch {
// Ignore property fetch errors
}
}
}
if (matches.length > 0) {
selectElements(matches);
}
},
[ifcApiInternal, loadedModels, selectElements],
);
const selectElementsByProperty = useCallback(
async (path: string[], value: any) => {
if (!ifcApiInternal) return;
// Ensure properties helper is available
if (!ifcApiInternal.properties) {
try {
ifcApiInternal.properties = new Properties(ifcApiInternal);
} catch (e) {
console.error(
"IFCContext: Failed to init properties in selectElementsByProperty",
e,
);
return;
}
}
const matches: SelectedElementInfo[] = [];
let processed = 0;
// Normalize once for reliable comparisons
const unwrap = (v: any) =>
v && typeof v === "object" && "value" in v ? (v as any).value : v;
const target = unwrap(value);
for (const model of loadedModels) {
if (model.modelID == null) continue;
const elements = IFCElementExtractor.getAllElements(
ifcApiInternal,
model.modelID,
);
for (const el of elements) {
try {
const props = await PropertyCache.getProperties(
ifcApiInternal,
model.modelID,
el.expressID,
);
let current: any = props;
for (const part of path) {
if (current == null) break;
current = current[part as keyof typeof current];
}
if (
current !== undefined &&
deepEqual(unwrap(current), target)
) {
matches.push({
modelID: model.modelID,
expressID: el.expressID,
});
}
} catch {
// Ignore property fetch errors
}
// Yield periodically to keep UI responsive on large models
processed++;
if (processed % 250 === 0) {
await (async () => new Promise((r) => setTimeout(r, 0)))();
}
}
}
if (matches.length > 0) {
selectElements(matches);
}
},
[ifcApiInternal, loadedModels, selectElements],
);


const toggleElementSelection = useCallback(
(element: SelectedElementInfo, additive: boolean) => {
setSelectedElements((prev) => {
Expand Down Expand Up @@ -2298,6 +2337,7 @@ export function IFCContextProvider({ children }: { children: ReactNode }) {
selectElements,
toggleElementSelection,
clearSelection,
selectElementsByProperty,
toggleClassificationHighlight,
setElementProperties,
setAvailableCategoriesForModel,
Expand Down
1 change: 1 addition & 0 deletions public/locales/de/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"no": "Nein",
"notSet": "Nicht gesetzt",
"emptyList": "Leere Liste",
"selectAllMatching": "Alle Elemente mit gleichem Wert auswählen",
"listCount": "Liste ({{count}} Elemente)",
"complexData": "Komplexe Daten",
"loadingSchemaPreview": "Schema-Vorschau wird geladen...",
Expand Down
1 change: 1 addition & 0 deletions public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"no": "No",
"notSet": "Not set",
"emptyList": "Empty list",
"selectAllMatching": "Select all elements with same value",
"listCount": "List ({{count}} items)",
"complexData": "Complex data",
"loadingSchemaPreview": "Loading schema preview...",
Expand Down
1 change: 1 addition & 0 deletions public/locales/fr/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"no": "Non",
"notSet": "Non défini",
"emptyList": "Liste vide",
"selectAllMatching": "Sélectionner tous les éléments avec la même valeur",
"listCount": "Liste ({{count}} éléments)",
"complexData": "Données complexes",
"loadingSchemaPreview": "Chargement de l'aperçu du schéma...",
Expand Down
1 change: 1 addition & 0 deletions public/locales/it/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"no": "No",
"notSet": "Non impostato",
"emptyList": "Lista vuota",
"selectAllMatching": "Seleziona tutti gli elementi con lo stesso valore",
"listCount": "Lista ({{count}} elementi)",
"complexData": "Dati complessi",
"loadingSchemaPreview": "Caricamento anteprima schema...",
Expand Down