Doing more with IFC properties – sectioning floors

Let’s do something a little smarter with the IFC property data associated with the IFCSTOREY. Instead of using it to isolate a particular storey, we’re going to take a cross section through the model instead, which exposes the internal contents of the building at that level. The goal is to create and effect like this:

../../_images/ifc_floor_section.png

The effect is achieved by inserting a cut-plane at an appropriate height in the building. You’ll also notice that we’ve displayed a separate plan view. This is something we’ll talk about in a later section.

The key to getting the effect we want is to insert a cutplane near the top of the floor we want to view. If you’re not familiar with the concept of a HOOPS Visualize cutplane, please refer to the cutting sections article of the HOOPS Visualize Programming Guide.

Getting the storey elevation

The basis for positioning the cutplane is the ‘elevation’ property associated with an IFCSTOREY. We already know how to get the component which represents an IFCSTOREY. Let’s define a new utility function to return the elevation.

bool IFCUtils::FindIFCStoryElevation(HPS::Component component, float &value )
{
        HPS::Metadata md = component.GetMetadata( "IFCBUILDINGSTOREY/Elevation");

        if (md.Empty())
                return false;

        switch (md.Type())
        {
                case HPS::Type::DoubleMetadata:
                        value =(float) HPS::DoubleMetadata(md).GetValue();
                break;
                default:
                        return false;
        }

        return true;
}

The code is almost identical to the code we wrote earlier to get metadata. In this case, we want to use the numerical value that the “Elevation” property represents. From prior knowledge, we can assume it’s a numerical value and not a string, so we use an object of type HPS::DoubleMetadata to access its value.

One other thing to note is the compound label from the code snippet - “IFCBUILDINGSTOREY/Elevation”. HOOPS Exchange creates compound labels for IFC metadata containing the main type and sub property name. If you have used HOOPS Demo Viewer to view BIM data, you may have noticed that it displays these sub-properties slightly differently to this tutorial, but it accesses the properties in the same way. Other examples for compound labels:

  • IFCWINDOW/Name

  • IFCWINDOW/GlobalId

Saving the elevations by storey

Rather than ask for the elevation of a storey each time we want to access it, let’s just save it when we initially process the model on load. This means making changes to the function FindIFCStoreys.

void  IFCUtils::FindIFCStoreys(HPS::Component component, HPS::ComponentArray &ancestorcomponents, IFCSampleModel *pIFCModel )
{
	//
	// Get the Exchange Component Type
	// the IFCStory type will always be represented as Product Occurrence. If it not one of those or 
	// the root model file, then do not search any deeper
	//
	HPS::Component::ComponentType cType = component.GetComponentType();


	if (cType != HPS::Component::ComponentType::ExchangeProductOccurrence && cType != HPS::Component::ComponentType::ExchangeModelFile)
		return;

	ancestorcomponents.push_back(component);

	if (cType == HPS::Component::ComponentType::ExchangeProductOccurrence)
	{
		UTF8 strType;
		if (GetMetaDataType(component, strType))
		{

			if (strType == "IFCBUILDINGSTOREY")
			{
				//  Component Path needs to have components ordered leaf -> root
				//  For convenience we build it up the ancestor list vector root->leaf
				//  Create a reversed vector
				ComponentArray reverseComponents(ancestorcomponents);
				std::reverse(reverseComponents.begin(), reverseComponents.end());
				pIFCModel->_storeyPaths.push_back(ComponentPath(reverseComponents));
				ancestorcomponents.pop_back();

				float elevation;
				FindIFCStoryElevation(component, elevation);
				pIFCModel->_storeyElevations.push_back(elevation);

				return;
			}
		}
	}

	HPS::ComponentArray carray = component.GetSubcomponents();
	for (auto comp : carray)
	{
		FindIFCStoreys(comp, ancestorcomponents, pIFCModel);
	}

	ancestorcomponents.pop_back();
}

There are a couple of modifications to this function to note. First, having found a component which represents a storey, we call now call FindIFCStoryElevation. Second, we’ve changed the function parameters to pass a pointer to a new object type IFCSampleModel.

We could have just added another parameter to the function, an array of elevations corresponding to each storey, but what if we want to gather additional information in future?

Let’s add a class to wrap around this additional model information we are gathering and use this as a parameter instead. It’s also an appropriate time to think about application data model associativity.

The sample we’re building is just a tutorial and we don’t necessarily need a full application data model to back up the application. However, in a real-world application, the application is going to have its own data model representing concepts which are important to the application’s functionality.

So IFCSampleModel is really a placeholder for an application object which corresponds to a Model object in HOOPS Visualize, a ModelFile in HOOPS Exchange and an IFCProject. For a real-world application, you would need to add code to maintain synchronization of each model if the model was changed.

Updating the ComboBox

With the additional information we just gathered about the floor elevations, we can create a drop-down box. Previously, the storeys were just listed in the order we came across them, but with the elevations, we can list them in order using a new utility function:

void IFCUtils::SortIFCStoreysByElevation(IFCSampleModel * pIFCModel)
{
	if (NULL == pIFCModel)
		return;

	std::vector<int>  indices(pIFCModel->_storeyElevations.size());

	int index = 0;
	for (auto &a : indices)
		a = index++;

	// sort the indices based on the height;

	std::sort(std::begin(indices), std::end(indices),
		[pIFCModel](int a, int b) { return pIFCModel->_storeyElevations[a] > pIFCModel->_storeyElevations[b]; });

     std::vector<float> tempElevations(  pIFCModel->_storeyElevations);
	 HPS::ComponentPathArray tempPaths(  pIFCModel->_storeyPaths );

	 index = 0;
	 for (auto a : indices)
	 {
		 pIFCModel->_storeyElevations[index] = tempElevations[a];
		 pIFCModel->_storeyPaths[index] = tempPaths[a];
		 index++;
	 }
}

Now, we will call this new function when we initialize the ComboBox in CHPSView::OnLoadedIFCFile:

HPS::Component::ComponentType cType = cadModel.GetComponentType();
ComponentArray ancestorcomponents;

// only update the floors for the first model loaded
IFCSampleModel * pIFCModel = pDoc->GetIFCSampleModel();
if (mergedFile)
{
    pIFCModel->IncrMergeMount();
    if (pIFCModel->GetMergeCount() > 1)
        return;
}
else
    pIFCModel->ResetMergeCount();

pIFCModel->Reset();

IFCUtils::FindIFCStoreys(cadModel, ancestorcomponents, pIFCModel );

IFCUtils::SortIFCStoreysByElevation(pIFCModel);

//
// now populate the dropdown
//
CHPSFrame * pFrame = (CHPSFrame*)AfxGetMainWnd();
CMFCRibbonComboBox *pCombo = DYNAMIC_DOWNCAST(CMFCRibbonComboBox, pFrame->m_wndRibbonBar.FindByID(ID_COMBO_FLOOR));

pCombo->RemoveAllItems();

ifcPopulateFloorsDropDown(pCombo, pIFCModel);

Next, as we got the height of each storey when we reordered the list, let’s put the elevation in the label for each floor. ifcPopulateFloorsDropDown is used to get the elevations:

void ifcPopulateFloorsDropDown(CMFCRibbonComboBox *pCombo, IFCSampleModel *pIFCModel)
{
        // The first entry will be to show the entire model
        pCombo->AddItem(CString("All"),NULL);
        pCombo->SelectItem(0);

        // now add an entry for each
        int index = 0;
        for (auto path : pIFCModel->_storeyPaths)
        {
                //lets get the name of the component
                float elevation = pIFCModel->_storeyElevations[index];

                Component front = path.Front();

                HPS::UTF8 strFrontName = front.GetName();
                CString csName(strFrontName.GetBytes());

                CString csValue;
                csValue.Format(_T("- %f"),  elevation);

                pCombo->AddItem(csName + csValue, index++);
        }
}

The drop-down box will now look like this, with floors listed in order of elevation:

../../_images/combobox.png

Inserting the cutting plane

Finally, we get to the code which inserts the cutting plane. This code replaces the previous code which simply isolated the floor.

We use the index of the drop-down box item to select the appropriate floor of the building. The first position is reserved for showing the whole building. We do this by removing the cutplane. Removing the cutplane is simply a matter of calling HPS::SegmentKey::Flush on that segment to remove the cutplane from the database.

With regard to inserting the cutplane, we create a child segment off the main model where we store our cutplane. The actual cutting plane is defined by using a HPS::CuttingSectionKit and calling the function HPS::SegmentKey::InsertCuttingSection.

HPS::CuttingSectionKit cutKit;
HPS::Plane cutPlane;

cutPlane.a = 0;
cutPlane.b = 0;
cutPlane.c = 1;
cutPlane.d = -(elevation + height *.75);
cutKit.SetPlanes(cutPlane);
cutKit.SetVisualization(HPS::CuttingSection::Mode::None, RGBAColor(0.7f, 0.7f, 0.7f, 0.15f), 1);

mainCutplaneKey.InsertCuttingSection(cutKit);

The location of the cutting plane is defined using the parameters for the geometric plane equation. For this tutorial, all our cutting planes are going to be parallel to the Z axis, so the equation is simple - the direction along the Z axis is a combination of the elevation of the floor and the height of the floor.

Finally, we set how the cutplane should be shown. In this case, we do not wish to see the plane itself (just the cut geometry), so we set the visualization mode to None.

The last thing to discuss is how to get the floor height. In this case, there was no explicit property to read containing a height. We might have created a function to estimate the height by looking the geometry representation. For this tutorial, we just calculate a floor height by looking at the elevation of the storey above. For the top floor, the roof, a default value is used.

The full code can be seen here:

void CHPSView::OnComboFloor()
{
        CHPSFrame * pFrame = (CHPSFrame*)AfxGetMainWnd();
        CMFCRibbonComboBox *pCombo = DYNAMIC_DOWNCAST(CMFCRibbonComboBox, pFrame->m_wndRibbonBar.FindByID(ID_COMBO_FLOOR));

        int selectionIndex = pCombo->GetCurSel();

        IFCSampleModel *pIFCModel = GetCHPSDoc()->GetIFCSampleModel();
        int storeyIndex = selectionIndex - 1;
        pIFCModel->SetCurrentStorey(storeyIndex);


        if (storeyIndex < 0 )
        {
                HPS::CADModel cadModel = GetCHPSDoc()->GetCADModel();
                cadModel.ResetVisibility(_canvas);

                HPS::SegmentKey mainCutplaneKey = cadModel.GetModel().GetSegmentKey().Subsegment("Cutplane");
                mainCutplaneKey.Flush();
        }
        else
        {
                float elevation, height;
                pIFCModel->GetElevationAndHeight(storeyIndex, elevation,height);

                if (height < 0.01)
                        height = 0.5;

                HPS::CADModel cadModel = GetCHPSDoc()->GetCADModel();
                cadModel.ResetVisibility(_canvas);

                HPS::SegmentKey mainCutplaneKey = cadModel.GetModel().GetSegmentKey().Subsegment("Cutplane");
                mainCutplaneKey.Flush();

                HPS::CuttingSectionKit cutKit;
                HPS::Plane cutPlane;

                cutPlane.a = 0;
                cutPlane.b = 0;
                cutPlane.c = 1;
                cutPlane.d = -(elevation + height *.75);
                cutKit.SetPlanes(cutPlane);
                cutKit.SetVisualization(HPS::CuttingSection::Mode::None, RGBAColor(0.7f, 0.7f, 0.7f, 0.15f), 1);

                mainCutplaneKey.InsertCuttingSection(cutKit);
        }
        _canvas.Update();
}