Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
87 changes: 31 additions & 56 deletions components/classification-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export function ClassificationPanel() {
const { t } = useTranslation();
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
const [isBsddDialogOpen, setIsBsddDialogOpen] = useState(false);
const [bsddFeatureSeen, setBsddFeatureSeen] = useState(false);
const [bsddFeatureSeen, setBsddFeatureSeen] = useState(true); // Default to true to prevent initial layout shift
const [newClassification, setNewClassification] = useState({
code: "",
name: "",
Expand Down Expand Up @@ -417,10 +417,13 @@ export function ClassificationPanel() {
}, [exportableModels, selectedModelIdForExport]);

useEffect(() => {
// Feature highlight: show pulse on 3-dot menu until user opens it once
// Feature highlight: show subtle indicator until user opens it once
try {
const seen = localStorage.getItem("bsddFeatureSeen");
setBsddFeatureSeen(seen === "true");
// Only update state if it's actually false, to prevent unnecessary re-renders
if (seen !== "true") {
setBsddFeatureSeen(false);
}
} catch (e) {
// ignore
}
Expand Down Expand Up @@ -1153,13 +1156,15 @@ export function ClassificationPanel() {
{" "}
{/* Subtle look */}
<MoreHorizontal className="w-5 h-5 text-muted-foreground" />
{/* BSDD feature indicator with nice animations */}
{!bsddFeatureSeen && (
<>
{/* Glow ping dot */}
{/* Glow ping dot with smooth animation */}
<span className="pointer-events-none absolute -top-1.5 -right-1.5 w-3.5 h-3.5 rounded-full bg-primary/70 animate-ping" />
<span className="pointer-events-none absolute -top-1.5 -right-1.5 w-3.5 h-3.5 rounded-full bg-primary shadow-sm" />
{/* Floating label */}
<span className="pointer-events-none select-none absolute right-6 top-full mt-1 bg-primary text-primary-foreground text-[10px] leading-none px-2 py-1 rounded-full shadow-md animate-bounce">

{/* Floating label with bounce animation - positioned to not affect layout */}
<span className="pointer-events-none select-none absolute right-6 top-full mt-1 bg-primary text-primary-foreground text-[10px] leading-none px-2 py-1 rounded-full shadow-md animate-bounce whitespace-nowrap z-10">
bSDD search
</span>
</>
Expand Down Expand Up @@ -1725,58 +1730,28 @@ export function ClassificationPanel() {
</div>
)}
{sortedClassificationEntries.length === 0 ? (
<div className="text-center py-8 flex-grow flex flex-col items-center justify-center">
{searchQuery ? (
<p className="text-base font-medium text-foreground/80">
<div className="text-center py-8 flex-grow flex flex-col items-center justify-center min-h-[200px] relative">
{/* Always render both states with consistent structure to prevent layout shifts */}
<div className={`flex flex-col items-center justify-center transition-opacity duration-200 ${searchQuery ? 'opacity-100' : 'opacity-0'} absolute inset-0`}>
<p className="text-base font-medium text-foreground/80 min-h-[1.5rem] flex items-center">
{t("classifications.noSearchResults")}
</p>
) : (
<>
<div className="flex justify-center mb-4">
<Cuboid className="h-12 w-12 text-foreground/30" />
</div>
<p className="text-base font-medium text-foreground/80 mb-2">
{t("noClassificationsAdded")}
</p>
<p className="text-sm text-foreground/60 mb-4">
{t("addClassification")}
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-2">
{isLoadingUniclass ? (
<Button disabled>{t("buttons.loadingUniclass")}</Button>
) : errorLoadingUniclass ? (
<Button variant="destructive" disabled>
{t("buttons.uniclassError", { message: errorLoadingUniclass })}
</Button>
) : defaultUniclassPr.length > 0 ? (
<Button
onClick={handleAddAllUniclassPr}
disabled={areAllUniclassAdded()}
>
{t("buttons.loadUniclass", { count: defaultUniclassPr.length })}
</Button>
) : (
<Button disabled>{t("buttons.noUniclassFound")}</Button>
)}
{isLoadingEBKPH ? (
<Button disabled>{t("buttons.loadingEbkph")}</Button>
) : errorLoadingEBKPH ? (
<Button variant="destructive" disabled>
{t("buttons.ebkphError", { message: errorLoadingEBKPH })}
</Button>
) : defaultEBKPH.length > 0 ? (
<Button
onClick={handleAddAlleBKPH}
disabled={areAlleBKPHAdded()}
>
{t("buttons.loadEbkph", { count: defaultEBKPH.length })}
</Button>
) : (
<Button disabled>{t("buttons.noEbkphFound")}</Button>
)}
</div>
</>
)}
</div>

<div className={`flex flex-col items-center justify-center transition-opacity duration-200 ${searchQuery ? 'opacity-0' : 'opacity-100'} absolute inset-0`}>
<div className="flex justify-center mb-4">
<Cuboid className="h-12 w-12 text-foreground/30" />
</div>
<p className="text-base font-medium text-foreground/80 mb-2 min-h-[1.5rem] flex items-center">
{t("noClassificationsAdded")}
</p>
<p className="text-sm text-muted-foreground mb-4">
{t("addClassification")}
</p>
<p className="text-xs text-muted-foreground">
Use the + button above to create your first classification
</p>
</div>
</div>
) : (
<div className="flex-grow overflow-hidden bg-card shadow-sm rounded-lg flex flex-col min-h-0 border border-border">
Expand Down
15 changes: 15 additions & 0 deletions components/ifc-model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
LoadedModelData,
SelectedElementInfo,
} from "@/context/ifc-context";
import { IFCElementExtractor } from "@/services/ifc-element-extractor";
import { PropertyIndex } from "@/services/property-index";
import * as THREE from "three";
import {
IfcAPI,
Expand Down Expand Up @@ -914,6 +916,19 @@ export function IFCModel({ modelData, outlineLayer }: IFCModelProps) {
`IFCModel (${modelData.id}): Spatial structure extraction failed or empty.`
);

// Build property index for fast property-based selections
try {
console.log(`IFCModel (${modelData.id}): Building property index for fast selections...`);
const allElements = IFCElementExtractor.getAllElements(ifcApi, newIfcModelID);
await PropertyIndex.buildIndex(ifcApi, newIfcModelID, allElements, (progress) => {
console.log(`IFCModel (${modelData.id}): Property index building progress: ${progress}%`);
});
console.log(`IFCModel (${modelData.id}): Property index built successfully`);
} catch (indexError) {
console.warn(`IFCModel (${modelData.id}): Failed to build property index:`, indexError);
// Continue without index - selections will use fallback method
}

const allTypesResult = ifcApi.GetIfcEntityList(newIfcModelID);
const allTypesArray: number[] = Array.isArray(allTypesResult)
? allTypesResult
Expand Down
27 changes: 25 additions & 2 deletions components/ifc-viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -938,6 +938,7 @@ function ViewerContent() {
addIFCModel,
clearSelection,
ruleProgress,
selectionProgress,
showAllClassificationColors,
toggleShowAllClassificationColors,
isolateUnclassified,
Expand Down Expand Up @@ -2182,6 +2183,28 @@ function ViewerContent() {
</div>
</div>
)}

{/* Selection Processing Progress */}
{selectionProgress?.active && (
<div
className="pointer-events-none absolute z-50 w-[min(400px,45vw)]"
style={{
bottom: `${Math.max(10, consolePosition.y - dragOffset.y + (ruleProgress?.active ? 120 : 0))}px`,
right: `${Math.max(10, consolePosition.x - dragOffset.x)}px`,
transform: isDragging ? 'scale(1.02)' : 'scale(1)',
transition: isDragging ? 'none' : 'transform 0.2s ease-out'
}}
>
<div className="pointer-events-auto rounded-xl border border-border bg-background/95 backdrop-blur-md shadow-xl overflow-hidden animate-in slide-in-from-bottom-2 duration-300">
<SimpleProgress
percent={selectionProgress.percent}
status={selectionProgress.status}
matchCount={selectionProgress.matchCount}
active={selectionProgress.active}
/>
</div>
</div>
)}
</div>
</Panel>

Expand Down Expand Up @@ -2440,7 +2463,7 @@ function FileUpload({ isAdding = false }: FileUploadProps) {
<UploadCloud className="h-12 w-12 text-foreground/30" />
</div>
<p className="text-base font-medium text-foreground/80 mb-2">{t('ifcModelViewer')}</p>
<p className="text-sm text-foreground/60 mb-6">
<p className="text-sm text-muted-foreground mb-6">
{t('uploadIFCFile')}
</p>
<input
Expand Down Expand Up @@ -2470,7 +2493,7 @@ function FileUpload({ isAdding = false }: FileUploadProps) {
<UploadCloud className="h-12 w-12 text-foreground/30" />
</div>
<p className="text-base font-medium text-foreground/80 mb-2">{t('ifcModelViewer')}</p>
<p className="text-sm text-foreground/60 mb-6">
<p className="text-sm text-muted-foreground mb-6">
{t('uploadIFCFile')}
</p>
<input
Expand Down
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
Loading