Creating a Model Explosion Operation in HOOPS Visualize

A common operation that is useful in the CAD visualization workflow is to provide the ability to explode a model, enabling the user to see each individual part. There are many ways to architect a solution that supports this concept. One way is to instance the model parts so that you can manipulate them without changing anything to the underlying model. This allows you to swap out the exploded model and the original model quickly with the exploded instances simply being shown and hidden when desired.

To help provide a way to isolate this functionality, in collaboration with Chris Pillion, we have put together example explode code into an explosion manager (below) where you can simply pass the component root that you went to explode with a center of explosion. This is not production tested code, but something to provide guidance and/or an example on how an explode operator could work. We implemented an explode slider scale also that on update pushes a change to scale using “UpdateExplodeScale”, but that code is not fully shown in the example code below.

Reach out if you are interested in the full source associated with this explode code or similar functionality for viewing your CAD models.

How to call the explode manager (this example was used in the MfcSandbox sample code provided with the HOOPS Visualize installation package):

void CHPSView::OnUserCode3()
{
	GetCanvas().GetFrontView().GetAxisTriadControl().SetVisibility(true);

	// Can only run this code if a file is loaded and a CAD Model Exists.
	auto hpsDoc = GetCHPSDoc();
	if (hpsDoc != NULL) {
		auto cadModel = hpsDoc->GetCADModel();
		if (!cadModel.Empty()) {
			auto riComponents = cadModel.GetAllSubcomponents(HPS::Exchange::Component::ComponentType::ExchangeRepresentationItemMask);

			
			// Calculate Model Bounding and center of explode
			HPS::Point center_of_explode = HPS::Point(0, 0, 0);
			double explodeCenterSphereRadius = 1;
			HPS::SimpleCuboid bounding_cuboid;
			HPS::SimpleSphere bounding_sphere;
			HPS::KeyPathArray keyPathArray = HPS::Component::GetKeyPath(cadModel);
			HPS::KeyPath keyPath = keyPathArray.front();
			HPS::BoundingKit bounding_kit;
			if (keyPath.Size() != 0 && keyPath.ShowNetBounding(bounding_kit) && bounding_kit.ShowVolume(bounding_sphere, bounding_cuboid))
			{
				center_of_explode = bounding_sphere.center;
			}

			// Perform Explode
			m_explodeManager.SetModelSegmentKey(GetCHPSDoc()->GetCADModel().GetModel().GetSegmentKey());
			auto explodeSlider = GetCHPSDoc()->GetCHPSFrame()->GetExplodeSlider();
			auto scale = (float)explodeSlider->GetPos() / (explodeSlider->GetRangeMax() - explodeSlider->GetRangeMin());
			m_explodeManager.ExplodeChildren(riComponents, center_of_explode, scale);

			GetCanvas().Update();

		}
	}
}


void CHPSView::OnUserCode4()
{
	m_explodeManager.RemoveExplodedModel();
	GetCanvas().Update();
}

void CHPSView::UpdateExplodeScale() {
	auto ribbonSlider = GetCHPSDoc()->GetCHPSFrame()->GetExplodeSlider();
	auto scale = (float)ribbonSlider->GetPos() / (ribbonSlider->GetRangeMax() - ribbonSlider->GetRangeMin());

	auto hpsDoc = GetCHPSDoc();
	if (hpsDoc != NULL) {
		auto cadModel = hpsDoc->GetCADModel();
		if (!cadModel.Empty()) {
			auto riComponents = cadModel.GetAllSubcomponents(HPS::Exchange::Component::ComponentType::ExchangeRepresentationItemMask);

			m_explodeManager.UpdateScale(riComponents, scale);
			GetCanvas().Update();
		}
	}
}

ExplodeManager.cpp:

#include "stdafx.h"
#include "ExplodeManager.h"

//-------------------------------------------------------------------------------------------------------------------------
// 
// This is required to set the root key of the model so the explode operator can store the model while it is operating on an exploded version
// The original model is stored so we don't create any changes to the underlying model and can fully restore the model as needed.
// 
// This must be called before ExplodeChildren is call and should be updated if a new model is loaded.
// 
//-------------------------------------------------------------------------------------------------------------------------

void ExplodeManager::SetModelSegmentKey(HPS::SegmentKey modelSegmentKey) {
	m_modelSegmentKey = modelSegmentKey;
}

//-------------------------------------------------------------------------------------------------------------------------
// This code simply removes the old exploded model and replaces it with one that is scaled at the new scale.
// This code was not designed for efficient updating of scale, but to provide a concept if you want to provide multiple explode levels.
//-------------------------------------------------------------------------------------------------------------------------

void ExplodeManager::UpdateScale(HPS::ComponentArray& riComponents, float scale) {
	if (!m_explodedModelSegmentKey.Empty()) {
		RemoveExplodedModel();
		ExplodeChildren(riComponents, m_last_explode_center, scale);
	}
}

//-------------------------------------------------------------------------------------------------------------------------
// 
// The explode children function creates instances of all the items with-in the model that are exploded to the scale as desired from the center of the explode_center
// The original model is stored in full capacity in the storageSegment so that it can be recalled without any manipulation.
// 
//-------------------------------------------------------------------------------------------------------------------------

void ExplodeManager::ExplodeChildren(HPS::ComponentArray& riComponents, HPS::Point explode_center, float scale) {
	m_last_explode_center = explode_center; // Store last explode center so we can update the scale with the last known explode center.

	if (!m_modelSegmentKey.Empty() && m_explodedModelSegmentKey.Empty()) {
		if (m_modelStorageSegment.Empty()) {
			m_modelStorageSegment = HPS::Database::CreateRootSegment();
			m_modelStorageSegment.SetName(m_storageSegmentName.c_str());
		}

		if (!m_explodedModelSegmentKey.Empty()) m_explodedModelSegmentKey.Delete();

		m_explodedModelSegmentKey = HPS::Database::CreateRootSegment();
		m_explodedModelSegmentKey.SetName(m_explodeSegmentName.c_str());

		// TODO: Add code to check if this already segment and all its children exists - if so, just update the component matrices
		

		scale = scale * 10;

		for (auto riComponent : riComponents) {
			HPS::KeyPathArray keyPathArray = HPS::Component::GetKeyPath(riComponent);

			auto nthInstance = 0;
			for (auto keyPath : keyPathArray) {

				if (keyPath.Size() <= 0) {
					continue;
				}

				// Need to append additional info to avoid returning the existing segment (if instanced) - Using a simple keypath counter.
				auto riComponentName = riComponent.GetName();
				auto childExplodeSegment = m_explodedModelSegmentKey.Subsegment(riComponentName + "_Instance_" + nthInstance);

				HPS::MatrixKit componentDefaultMatrix;
				HPS::MaterialMappingKit componentDefaultMaterial;
				keyPath.ShowNetModellingMatrix(componentDefaultMatrix);
				keyPath.ShowNetMaterialMapping(componentDefaultMaterial);

				HPS::SimpleCuboid bounding_cuboid;
				HPS::SimpleSphere bounding_sphere;
				HPS::BoundingKit bounding_kit;

				if (keyPath.ShowNetBounding(false, bounding_kit) && bounding_kit.ShowVolume(bounding_sphere, bounding_cuboid))
				{
					// Scale the explosion to the distance of desire.
					auto scaled_explode_world_vector = (bounding_sphere.center - explode_center).Scale(HPS::Vector(scale, scale, scale));

					// Create a translation matrix kit from the explode vector.
					HPS::MatrixKit updateMatrix = componentDefaultMatrix.Translate(scaled_explode_world_vector);

					auto refKey = m_explodedModelSegmentKey.ReferenceGeometry(keyPath.Front());
					refKey.SetMaterialMapping(componentDefaultMaterial);
					refKey.SetModellingMatrix(updateMatrix);
				}
				nthInstance++;
			}

		}

		auto modelIncludes = GatherIncludes(m_modelSegmentKey);

		for (auto includeKey : modelIncludes) {
			auto segmentName = includeKey.GetTarget().Name();
			if (segmentName == m_explodeSegmentName.c_str()) {
				continue;
			}
			else {
				includeKey.MoveTo(m_modelStorageSegment);
			}
		}
		auto explodeModelIncludeKey = m_modelSegmentKey.IncludeSegment(m_explodedModelSegmentKey);
	}
}

//-------------------------------------------------------------------------------------------------------------------------
// This is a helper function to gather the children includes of the model to save for recall
//-------------------------------------------------------------------------------------------------------------------------
HPS::IncludeKeyArray ExplodeManager::GatherIncludes(HPS::SegmentKey searchSegmentRoot) {
	HPS::SearchResults segmentSearchResults;
	size_t numResults = searchSegmentRoot.Find(HPS::Search::Type::Include, HPS::Search::Space::SegmentOnly, segmentSearchResults);

	HPS::IncludeKeyArray segmentIncludes;

	HPS::SearchResultsIterator it = segmentSearchResults.GetIterator();
	while (it.IsValid())
	{
		HPS::Key key = it.GetItem();

		if (key.Type() == HPS::Type::IncludeKey)
		{
			segmentIncludes.push_back((HPS::IncludeKey)key);
		}
		it.Next();
	}
	return segmentIncludes;
}

//-------------------------------------------------------------------------------------------------------------------------
//
// This simply deletes the exploded model by segment key and restores the stored model back into the base model segment
//
//-------------------------------------------------------------------------------------------------------------------------

void ExplodeManager::RemoveExplodedModel() {
	if (!m_modelSegmentKey.Empty() && !m_modelStorageSegment.Empty()) {
		// Reset exploded model to original model from the stored includes.
		auto storageIncludes = GatherIncludes(m_modelStorageSegment);

		for (auto includeKey : storageIncludes) {
			auto segmentName = includeKey.GetTarget().Name();
			if (segmentName != m_explodeSegmentName.c_str()) {
				includeKey.MoveTo(m_modelSegmentKey);
				//includeKey.Delete();
			}
		}
		m_modelStorageSegment.Delete();
		m_explodedModelSegmentKey.Delete();
	}
}

Explode Manager.h

#pragma once

#include "hps.h"
#include "sprk.h"

//-------------------------------------------------------------------------------------------------------------------------
//
// A manage to handle the necessary compoents of an exploded model for viewing and restoring.
// 
//-------------------------------------------------------------------------------------------------------------------------
class ExplodeManager
{
private:
	HPS::SegmentKey		m_modelSegmentKey;
	HPS::SegmentKey		m_modelStorageSegment;
	HPS::SegmentKey		m_explodedModelSegmentKey;
	const std::string	m_explodeSegmentName = "ExplodeModel";
	const std::string	m_storageSegmentName = "StorageSegment";
	HPS::Point			m_last_explode_center;

public:
	void				 SetModelSegmentKey(HPS::SegmentKey modelSegmentKey);
	void				 UpdateScale(HPS::ComponentArray& riComponents, float scale);
	void				 ExplodeChildren(HPS::ComponentArray& subComponents, HPS::Point explode_center, float scale /* as a decimal percentage between 0 and 1*/);
	HPS::IncludeKeyArray GatherIncludes(HPS::SegmentKey searchSegmentRoot);
	void				 RemoveExplodedModel();
};
4 Likes

Hi Jeff,

I was wondering if it is still possible to share the full source associated with the explode mode, or alternatively if you can guide me to any other latest reference for this functionality. I am trying to explode the assembly into its parts like we usually do in SolidWorks to interact with individual bodies.

Thank you,
Kazi

Hi @koa228 ,

I just set up Jeff’s code and it seems pretty complete except for the details around the UI setup (setting up the slider).

I was able to make a slight modification to the code so that the explosion updates in real time by adding an event handler to the slider:
hps_explode

If you are referring to the ability to move individual components, I recommend taking a look at the handles operator. The source code for this operator is included in the Visualize package in <HOOPS_Visualize>\samples\operators
handles_operator

I hope this helps, and if you have any additional questions, let me know!

1 Like

Hi @beau.trifiro,

Thank you very much for sharing both the slider and operator-based approaches—they have been extremely helpful!

However, I have a few concerns:

  1. I am using the code as provided, but since I wasn’t able to properly set up the slider in the GUI, I attempted to hardcode the scale value and pass it to ExplodeChildren and UpdateScale. The code runs, detects the number of parts “riComponents” in the assembly, and scales the vectors “scaled_explode_world_vector”. However, the model disappears afterward. I tried the zoom-fit but nothing happens. I suspect this might be due to an issue with the scale value, even though I’ve tried several different values. On the other hand, the “Remove Explode” functionality is working as expected.


//Explode
void CHPSView::OnUserCode3()
{
	GetCanvas().GetFrontView().GetAxisTriadControl().SetVisibility(true);

	// Can only run this code if a file is loaded and a CAD Model Exists.
	auto hpsDoc = GetCHPSDoc();
	if (hpsDoc != NULL) {
		auto cadModel = hpsDoc->GetCADModel();
		if (!cadModel.Empty()) {
			auto riComponents = cadModel.GetAllSubcomponents(HPS::Exchange::Component::ComponentType::ExchangeRepresentationItemMask);


			// Calculate Model Bounding and center of explode
			HPS::Point center_of_explode = HPS::Point(0, 0, 0);
			double explodeCenterSphereRadius = 1;
			HPS::SimpleCuboid bounding_cuboid;
			HPS::SimpleSphere bounding_sphere;
			HPS::KeyPathArray keyPathArray = HPS::Component::GetKeyPath(cadModel);
			HPS::KeyPath keyPath = keyPathArray.front();
			HPS::BoundingKit bounding_kit;
			if (keyPath.Size() != 0 && keyPath.ShowNetBounding(bounding_kit) && bounding_kit.ShowVolume(bounding_sphere, bounding_cuboid))
			{
				center_of_explode = bounding_sphere.center;
			}

			// Perform Explode
			m_explodeManager.SetModelSegmentKey(GetCHPSDoc()->GetCADModel().GetModel().GetSegmentKey());
			//auto explodeSlider = GetCHPSDoc()->GetCHPSFrame()->GetExplodeSlider();
			//auto scale = (float)explodeSlider->GetPos() / (explodeSlider->GetRangeMax() - explodeSlider->GetRangeMin());
			float scale = 0.5; //(GetPos() / (ribbonSlider->GetRangeMax() - ribbonSlider->GetRangeMin() = 50/(100 - 0 ) = 0.5)
			m_explodeManager.ExplodeChildren(riComponents, center_of_explode, scale);

			GetCanvas().Update();

		}
	}
}

// Remove Explode
void CHPSView::OnUserCode1()
{
	m_explodeManager.RemoveExplodedModel();
	GetCanvas().Update();
}

void CHPSView::UpdateExplodeScale() {
	//auto ribbonSlider = GetCHPSDoc()->GetCHPSFrame()->GetExplodeSlider();
	//auto scale = (float)ribbonSlider->GetPos() / (ribbonSlider->GetRangeMax() - ribbonSlider->GetRangeMin());
	float scale = 0.5; //(GetPos() / (ribbonSlider->GetRangeMax() - ribbonSlider->GetRangeMin() = 50/(100 - 0 ) = 0.5)

	auto hpsDoc = GetCHPSDoc();
	if (hpsDoc != NULL) {
		auto cadModel = hpsDoc->GetCADModel();
		if (!cadModel.Empty()) {
			auto riComponents = cadModel.GetAllSubcomponents(HPS::Exchange::Component::ComponentType::ExchangeRepresentationItemMask);

			m_explodeManager.UpdateScale(riComponents, scale);
			GetCanvas().Update();
		}
	}
}
  1. Regarding the slider setup, while I can create an event handler in CHPSView, I believe part of the setup might also involve CHPSFrame as used here “auto ribbonSlider = GetCHPSDoc()->GetCHPSFrame()->GetExplodeSlider();” . I would greatly appreciate it if you could provide some guidance on how you created the slider in the video you shared.

Thank you again for your time and support—it is truly appreciated!

Hi @kahmed ,

I’m not sure why the hardcoded value isn’t working for you - I tried and was not able to replicate the result. I would suggest creating a ticket and including the CAD model and your model import code for our support team to troubleshoot.

Here are the steps I took to add an explode slider using Visual Studio 2019 (please note I put this together relatively quickly, so there may be better ways of going about this):

  1. Add the CHPSView::OnUserCode4 sample code provided in the first post of this topic, only.

  2. In the Solution Explorer:
    a) Right click Header Files then Add Exisiting Item and select ExplodeManager.h.
    b) Right click Source Files then Add Existing Item and select ExplodeManager.cpp.

  3. In CHPSView.cpp, add:

#include "ExplodeManager.h"
ExplodeManager m_explodeManager = ExplodeManager();
  1. In the Resource View of the project, double click IDR_RIBBON, drag a Panel next to the User Code panel and drag a Slider into that panel. (I set the panel caption to Explode.)

  1. Click the slider and set the ID to something like ID_SLIDER_EXPLODE.

  2. Right click the slider and click Add Event Handler.

  3. Under Class list, I selected CHPSView and named the function OnSliderExplode.

  4. CHPSView::OnSliderExplode will be defined as:

void CHPSView::OnSliderExplode()
{
	UpdateExplodeScale();
}
  1. Now, define UpdateExplodeScale (a slightly modified version of the code sample Jeff provided for OnUserCode3):
void CHPSView::UpdateExplodeScale() {

	// Can only run this code if a file is loaded and a CAD Model Exists.
	auto hpsDoc = GetCHPSDoc();
	if (hpsDoc != NULL) {
		auto cadModel = hpsDoc->GetCADModel();
		if (!cadModel.Empty()) {
		
			m_explodeManager.RemoveExplodedModel();

			auto riComponents = cadModel.GetAllSubcomponents(HPS::Exchange::Component::ComponentType::ExchangeRepresentationItemMask);


			// Calculate Model Bounding and center of explode
			HPS::Point center_of_explode = HPS::Point(0, 0, 0);
			double explodeCenterSphereRadius = 1;
			HPS::SimpleCuboid bounding_cuboid;
			HPS::SimpleSphere bounding_sphere;
			HPS::KeyPathArray keyPathArray = HPS::Component::GetKeyPath(cadModel);
			HPS::KeyPath keyPath = keyPathArray.front();
			HPS::BoundingKit bounding_kit;
			if (keyPath.Size() != 0 && keyPath.ShowNetBounding(bounding_kit) && bounding_kit.ShowVolume(bounding_sphere, bounding_cuboid))
			{
				center_of_explode = bounding_sphere.center;
			}

			// Perform Explode
			m_explodeManager.SetModelSegmentKey(GetCHPSDoc()->GetCADModel().GetModel().GetSegmentKey());
			auto explodeSlider = GetCHPSDoc()->GetCHPSFrame()->GetExplodeSlider();
			auto scale = (float)explodeSlider->GetPos() / (explodeSlider->GetRangeMax() - explodeSlider->GetRangeMin());
			m_explodeManager.ExplodeChildren(riComponents, center_of_explode, scale);

			GetCanvas().Update();

		}
	}
}
  1. Update CHPSView.h to include void UpdateExplodeScale(); (I added it as a private method).

  2. Update CHPSFrame.cpp:

a) Right before CHPSFrame::OnCreate, add CMFCRibbonSlider* m_pExplodeSliderCtrl;

b) In CHPSFrame::OnCreate, add the following:

...
m_wndRibbonBar.LoadFromResource(IDR_RIBBON); // add code below

CObject* ribbonSlider = ((&m_wndRibbonBar)->FindByID(ID_SLIDER_EXPLODE));
m_pExplodeSliderCtrl = DYNAMIC_DOWNCAST(CMFCRibbonSlider, ribbonSlider);
...

c) At the bottom of the file, add:

CMFCRibbonSlider* CHPSFrame::GetExplodeSlider() {
	return m_pExplodeSliderCtrl;
}
  1. In CHPSFrame.h add public method:
    CMFCRibbonSlider* GetExplodeSlider();

  2. In CHPSView.cpp, add #include "CHPSFrame.h"

  3. You can also add UpdateExplodeScale(); to CHPSView::OnUserCode3() if you’d like the ability to toggle the exploded view without changing it via the slider.

  4. Import a model and move the slider.

2 Likes

Hi @beau.trifiro,

Thank you very much for the detailed explanation on setting up the explode slider. I was able to implement it, and the slider is functioning as expected, just like in the video you shared.

However, I noticed that this version of the code works only with the model import process that uses the Exchange sprocket and Parasolid connector of Exchange. Unfortunately, the code doesn’t seem to work when using the Exchange-Parasolid sprocket - the model disappears as soon as I move the slider. I noticed the distinction between these model import methods is discussed in the Tech Soft 3D forum post: HOOPS Visualize - Exchange & Parasolid integration samples.

Would you have any suggestions on how to make this compatible with the Exchange-Parasolid sprocket?

Thanks again for your help!

Hi @kahmed,

In order to get this example to work with the Exchange-Parasolid sprocket, in the UpdateExplodeScale function, redefine riComponents as:

auto riComponents = cadModel.GetAllSubcomponents(HPS::Exchange::Component::ComponentType::ParasolidTopoBody)

More info about the component structure of Exchange-Parasolid files can be found here.

1 Like

Hi @beau.trifiro,

It worked!

Thank you very much for all your time and guidance; I greatly appreciate it! Thank you.

1 Like