...
 
Commits (2)
  • David Thompson's avatar
    Improvements to the bounding box widget. · 4220f4d6
    David Thompson authored
    + Accept bounding boxes defined via either a single
      DoubleItem or a Group containing DoubleItems in a
      variety of configurations (including some with and
      some without Tait-Bryan angles, which are similar
      to Euler angles).
    + Properly restore bounding box state from saved items.
    4220f4d6
  • David Thompson's avatar
    Add a release note. · 00d79b0f
    David Thompson authored
    00d79b0f
# ParaView Extensions
## Widgets
The box widget now accepts a single DoubleItem (with 6 entries)
specifying an axis-aligned bounding box or a GroupItem
containing multiple DoubleItems that configure a bounding box
in different ways depending on how they are used.
See the pqSMTKBoxItemWidget header for details.
......@@ -1816,6 +1816,42 @@
</PropertyGroup>
</Proxy>
<!-- Axis-aligned box widget -->
<Proxy
name="SMTKAxisAlignedBoxWidget"
class="vtkPVBox"
>
<DoubleVectorProperty command="SetBounds"
default_values="0 1 0 1 0 1"
name="Bounds"
number_of_elements="6"></DoubleVectorProperty>
<DoubleVectorProperty animateable="1"
command="SetPosition"
default_values="0.0 0.0 0.0"
name="Position"
number_of_elements="3">
<DoubleRangeDomain name="range" />
<Documentation>
Set the position of the box.
</Documentation>
</DoubleVectorProperty>
<DoubleVectorProperty animateable="1"
command="SetScale"
default_values="1.0 1.0 1.0"
name="Scale"
number_of_elements="3">
<DoubleRangeDomain name="range" />
<Documentation>
Set the size of the box via a scale factor.
</Documentation>
</DoubleVectorProperty>
<PropertyGroup panel_widget="InteractiveBox" label="Box Parameters">
<Property function="Position" name="Position" />
<Property function="Scale" name="Scale" />
<Property function="PlaceWidget" name="Bounds" />
</PropertyGroup>
</Proxy>
</ProxyGroup>
<ProxyGroup name="settings">
......
......@@ -11,6 +11,7 @@
#include "smtk/extension/paraview/widgets/pqSMTKAttributeItemWidgetP.h"
#include "smtk/attribute/DoubleItem.h"
#include "smtk/attribute/GroupItem.h"
#include "smtk/io/Logger.h"
......@@ -23,12 +24,15 @@
#include "pqPipelineSource.h"
#include "pqServer.h"
#include "pqSpherePropertyWidget.h"
#include "vtkMath.h"
#include "vtkPVXMLElement.h"
#include "vtkSMNewWidgetRepresentationProxy.h"
#include "vtkSMProperty.h"
#include "vtkSMPropertyGroup.h"
#include "vtkSMPropertyHelper.h"
#include "vtkSMProxy.h"
#include "vtkVector.h"
#include "vtkVectorOperators.h"
using qtItem = smtk::extension::qtItem;
using AttributeItemInfo = smtk::extension::AttributeItemInfo;
......@@ -52,12 +56,34 @@ qtItem* pqSMTKBoxItemWidget::createBoxItemWidget(const AttributeItemInfo& info)
bool pqSMTKBoxItemWidget::createProxyAndWidget(
vtkSMProxy*& proxy, pqInteractivePropertyWidget*& widget)
{
ItemBindings binding;
std::vector<smtk::attribute::DoubleItemPtr> items;
bool haveItems = this->fetchBoxItems(binding, items);
if (!haveItems || binding == ItemBindings::Invalid)
{
smtkErrorMacro(smtk::io::Logger::instance(), "Could not find items for widget.");
return false;
}
// I. Create the ParaView widget and a proxy for its representation.
pqApplicationCore* paraViewApp = pqApplicationCore::instance();
pqServer* server = paraViewApp->getActiveServer();
pqObjectBuilder* builder = paraViewApp->getObjectBuilder();
proxy = builder->createProxy("implicit_functions", "Box", server, "");
// The paraview widget uses the list of available properties in the
// proxy's property group to decide which UI elements should be
// exposed. Specifically, rotation is disabled if there is no
// property for the Euler angles. So... choose which object the proxy
// should point to based on whether the SMTK items model Euler angles
// or not:
if (binding <= ItemBindings::AxisAlignedCenterDeltas)
{
proxy = builder->createProxy("smtk_widgets", "SMTKAxisAlignedBoxWidget", server, "");
}
else
{
proxy = builder->createProxy("implicit_functions", "Box", server, "");
}
if (!proxy)
{
return false;
......@@ -65,62 +91,396 @@ bool pqSMTKBoxItemWidget::createProxyAndWidget(
widget = new pqBoxPropertyWidget(proxy, proxy->GetPropertyGroup(0));
// II. Initialize the properties.
// For now, since we want to map this to a vector of 6 doubles,
// we do not allow rotation:
m_p->m_pvwidget = widget;
this->updateWidgetFromItem();
auto widgetProxy = widget->widgetProxy();
auto valueItem = this->itemAs<smtk::attribute::DoubleItem>();
// FIXME! Determine bounds properly from scene if requested by m_itemInfo.
// For now, just initialize the box using the item's values if they are
// non-default (or the item has no default).
if (valueItem && valueItem->numberOfValues() == 6)
{
if (!valueItem->hasDefault() || !valueItem->isUsingDefault())
{
double lo[3];
double hi[3];
auto valIt = valueItem->begin();
for (int ii = 0; ii < 3; ++ii)
{
lo[ii] = *valIt;
++valIt;
hi[ii] = *valIt;
++valIt;
}
vtkSMPropertyHelper(widgetProxy, "Position").Set(lo, 3);
vtkSMPropertyHelper(widgetProxy, "Scale").Set(hi, 3);
}
}
vtkSMPropertyHelper(widgetProxy, "RotationEnabled").Set(false);
widgetProxy->UpdateVTKObjects();
// vtkSMPropertyHelper(widgetProxy, "RotationEnabled").Set(false);
return widget != nullptr;
}
/// Retrieve property values from ParaView proxy and store them in the attribute's Item.
void pqSMTKBoxItemWidget::updateItemFromWidget()
{
vtkSMNewWidgetRepresentationProxy* widget = m_p->m_pvwidget->widgetProxy();
auto valueItem = this->itemAs<smtk::attribute::DoubleItem>();
if (!valueItem || valueItem->numberOfValues() != 6)
std::vector<smtk::attribute::DoubleItemPtr> items;
ItemBindings binding;
if (!this->fetchBoxItems(binding, items))
{
smtkErrorMacro(smtk::io::Logger::instance(),
"Item widget has an update but the item does not exist or is not sized properly.");
"Item widget has an update but the item(s) do not exist or are not sized properly.");
return;
}
// Values held by widget
vtkVector3d origin;
vtkVector3d length;
vtkVector3d angles;
vtkSMPropertyHelper originHelper(widget, "Position");
vtkSMPropertyHelper lengthHelper(widget, "Scale");
vtkSMPropertyHelper anglesHelper(widget, "Rotation", /* quiet */ true);
originHelper.Get(origin.GetData(), 3);
lengthHelper.Get(length.GetData(), 3);
vtkVector3d center;
vtkVector3d deltas;
vtkVector3d point2;
bool didChange = false;
for (int i = 0; i < 3; ++i)
// Current values held in items:
vtkVector3d curAngles;
vtkVector3d curCenter;
vtkVector3d curPoint1;
vtkVector3d curPoint2;
vtkVector3d curDeltas;
// Translate widget values to item values and fetch current item values:
if (binding >= ItemBindings::EulerAngleBounds)
{
double lo = originHelper.GetAsDouble(i);
double hi = lengthHelper.GetAsDouble(i);
didChange |= (valueItem->value(2 * i) != lo) || (valueItem->value(2 * i + 1) != hi);
valueItem->setValue(2 * i, lo);
valueItem->setValue(2 * i + 1, hi);
vtkVector3d e1, e2, e3;
anglesHelper.Get(angles.GetData(), 3);
const double cth = cos(vtkMath::RadiansFromDegrees(angles[0]));
const double cph = cos(vtkMath::RadiansFromDegrees(angles[1]));
const double cps = cos(vtkMath::RadiansFromDegrees(angles[2]));
const double sth = sin(vtkMath::RadiansFromDegrees(angles[0]));
const double sph = sin(vtkMath::RadiansFromDegrees(angles[1]));
const double sps = sin(vtkMath::RadiansFromDegrees(angles[2]));
// From https://en.wikipedia.org/wiki/Euler_angles#Intrinsic_rotations :
// VTK uses Tait-Bryan Y_1 X_2 Z_3 angles to store orientation;
// This is the corresponding direction cosine matrix (DCM) for
// theta = X, phi = Y, psi = Z:
e1 = { cth * cps + sth * sph * sps, cps * sth * sph - cth * sps, cph * sth };
e2 = { cph * sps, cph * cps, -sph };
e3 = { cth * sph * sps - cps * sth, cth * cps * sph + sth * sps, cph * cth };
if (binding == ItemBindings::EulerAngleCenterDeltas)
{
deltas = 0.5 * length;
center = origin + deltas[0] * e1 + deltas[1] * e2 + deltas[2] * e3;
curCenter = vtkVector3d(&(*items[0]->begin()));
curDeltas = vtkVector3d(&(*items[1]->begin()));
curAngles = vtkVector3d(&(*items[2]->begin()));
}
else
{
point2 = origin + length[0] * e1 + length[1] * e2 + length[2] * e3;
if (binding == ItemBindings::EulerAngleMinMax)
{
curPoint1 = vtkVector3d(&(*items[0]->begin()));
curPoint2 = vtkVector3d(&(*items[1]->begin()));
curAngles = vtkVector3d(&(*items[2]->begin()));
}
else // binding == ItemBindings::EulerAngleBounds
{
curPoint1 = vtkVector3d(items[0]->value(0), items[0]->value(2), items[0]->value(4));
curPoint2 = vtkVector3d(items[0]->value(1), items[0]->value(3), items[0]->value(5));
curAngles = vtkVector3d(&(*items[1]->begin()));
}
}
}
else
{
if (binding == ItemBindings::AxisAlignedCenterDeltas)
{
deltas = 0.5 * length;
center = origin + deltas;
curCenter = vtkVector3d(&(*items[0]->begin()));
curDeltas = vtkVector3d(&(*items[1]->begin()));
}
else
{
point2 = origin + length;
if (binding == ItemBindings::AxisAlignedMinMax)
{
curPoint1 = vtkVector3d(&(*items[0]->begin()));
curPoint2 = vtkVector3d(&(*items[1]->begin()));
}
else
{
curPoint1 = vtkVector3d(items[0]->value(0), items[0]->value(2), items[0]->value(4));
curPoint2 = vtkVector3d(items[0]->value(1), items[0]->value(3), items[0]->value(5));
}
}
}
switch (binding)
{
case ItemBindings::AxisAlignedBounds:
case ItemBindings::EulerAngleBounds:
if (curPoint1 != origin || curPoint2 != point2)
{
didChange = true;
for (int ii = 0; ii < 3; ++ii)
{
items[0]->setValue(2 * ii, origin[ii]);
items[0]->setValue(2 * ii + 1, point2[ii]);
}
}
if (binding == ItemBindings::EulerAngleBounds && curAngles != angles)
{
didChange = true;
items[1]->setValues(angles.GetData(), angles.GetData() + 3);
}
break;
case ItemBindings::AxisAlignedMinMax:
case ItemBindings::EulerAngleMinMax:
if (curPoint1 != origin || curPoint2 != point2)
{
didChange = true;
items[0]->setValues(origin.GetData(), origin.GetData() + 3);
items[1]->setValues(point2.GetData(), point2.GetData() + 3);
}
if (binding == ItemBindings::EulerAngleMinMax && curAngles != angles)
{
didChange = true;
items[2]->setValues(angles.GetData(), angles.GetData() + 3);
}
break;
case ItemBindings::AxisAlignedCenterDeltas:
case ItemBindings::EulerAngleCenterDeltas:
if (curCenter != center || curDeltas != deltas)
{
didChange = true;
items[0]->setValues(center.GetData(), center.GetData() + 3);
items[1]->setValues(deltas.GetData(), deltas.GetData() + 3);
}
if (binding == ItemBindings::EulerAngleCenterDeltas && curAngles != angles)
{
didChange = true;
items[2]->setValues(angles.GetData(), angles.GetData() + 3);
}
break;
case ItemBindings::Invalid:
default:
smtkErrorMacro(smtk::io::Logger::instance(), "Grrk");
break;
}
if (didChange)
{
emit modified();
}
}
void pqSMTKBoxItemWidget::updateWidgetFromItem()
{
vtkSMNewWidgetRepresentationProxy* widget = m_p->m_pvwidget->widgetProxy();
std::vector<smtk::attribute::DoubleItemPtr> items;
ItemBindings binding;
if (!this->fetchBoxItems(binding, items))
{
smtkErrorMacro(smtk::io::Logger::instance(),
"Item signaled an update but the item(s) do not exist or are not sized properly.");
return;
}
// Unlike updateItemFromWidget, we don't care if we cause ParaView an unnecessary update;
// we might cause an extra render but we won't accidentally mark a resource as modified.
// Since there's no need to compare new values to old, this is simpler than updateItemFromWidget:
switch (binding)
{
case ItemBindings::AxisAlignedBounds:
case ItemBindings::EulerAngleBounds:
{
vtkVector3d point1(items[0]->value(0), items[0]->value(2), items[0]->value(4));
vtkVector3d point2(items[0]->value(1), items[0]->value(3), items[0]->value(5));
vtkVector3d length = point2 - point1;
vtkSMPropertyHelper(widget, "Position").Set(point1.GetData(), 3);
vtkSMPropertyHelper(widget, "Scale").Set(length.GetData(), 3);
if (binding == ItemBindings::EulerAngleBounds)
{
vtkVector3d angles(&(*items[1]->begin()));
vtkSMPropertyHelper(widget, "Rotation").Set(angles.GetData(), 3);
}
}
break;
case ItemBindings::AxisAlignedMinMax:
case ItemBindings::EulerAngleMinMax:
{
vtkVector3d point1(&(*items[0]->begin()));
vtkVector3d point2(&(*items[1]->begin()));
vtkVector3d length = point2 - point1;
vtkSMPropertyHelper(widget, "Position").Set(point1.GetData(), 3);
vtkSMPropertyHelper(widget, "Scale").Set(length.GetData(), 3);
if (binding == ItemBindings::EulerAngleMinMax)
{
vtkVector3d angles(&(*items[2]->begin()));
vtkSMPropertyHelper(widget, "Rotation").Set(angles.GetData(), 3);
}
}
break;
case ItemBindings::AxisAlignedCenterDeltas:
case ItemBindings::EulerAngleCenterDeltas:
{
vtkVector3d center(&(*items[0]->begin()));
vtkVector3d deltas(&(*items[1]->begin()));
vtkVector3d origin;
vtkVector3d length = 2 * deltas;
if (binding == ItemBindings::AxisAlignedCenterDeltas)
{
origin = center - deltas;
}
else
{
vtkVector3d angles(&(*items[2]->begin()));
vtkSMPropertyHelper(widget, "Rotation").Set(angles.GetData(), 3);
const double cth = cos(vtkMath::RadiansFromDegrees(angles[0]));
const double cph = cos(vtkMath::RadiansFromDegrees(angles[1]));
const double cps = cos(vtkMath::RadiansFromDegrees(angles[2]));
const double sth = sin(vtkMath::RadiansFromDegrees(angles[0]));
const double sph = sin(vtkMath::RadiansFromDegrees(angles[1]));
const double sps = sin(vtkMath::RadiansFromDegrees(angles[2]));
// From https://en.wikipedia.org/wiki/Euler_angles#Intrinsic_rotations :
// VTK uses Tait-Bryan Y_1 X_2 Z_3 angles to store orientation;
// This is the corresponding direction cosine matrix (DCM) for
// theta = X, phi = Y, psi = Z:
vtkVector3d e1 = { cth * cps + sth * sph * sps, cps * sth * sph - cth * sps, cph * sth };
vtkVector3d e2 = { cph * sps, cph * cps, -sph };
vtkVector3d e3 = { cth * sph * sps - cps * sth, cth * cps * sph + sth * sps, cph * cth };
origin = center - deltas[0] * e1 - deltas[1] * e2 - deltas[2] * e3;
}
vtkSMPropertyHelper(widget, "Position").Set(origin.GetData(), 3);
vtkSMPropertyHelper(widget, "Scale").Set(length.GetData(), 3);
}
break;
case ItemBindings::Invalid:
default:
{
smtkErrorMacro(smtk::io::Logger::instance(), "Grrk");
}
break;
}
}
bool pqSMTKBoxItemWidget::fetchBoxItems(
ItemBindings& binding, std::vector<smtk::attribute::DoubleItemPtr>& items)
{
items.clear();
// Check to see if item is a vector of doubles:
auto doubleItem = m_itemInfo.itemAs<smtk::attribute::DoubleItem>();
if (doubleItem)
{
if (doubleItem->numberOfValues() == 6)
{
binding = ItemBindings::AxisAlignedBounds;
items.push_back(doubleItem);
return true;
}
binding = ItemBindings::Invalid;
return false;
}
// Check to see if item is a group containing items of double-vector items.
auto groupItem = m_itemInfo.itemAs<smtk::attribute::GroupItem>();
if (!groupItem || groupItem->numberOfGroups() < 1 || groupItem->numberOfItemsPerGroup() < 2)
{
smtkErrorMacro(
smtk::io::Logger::instance(), "Expected a group item with 1 group of 2 or more items.");
return false;
}
// Find items in the group based on names in the configuration info:
// bds, min, max, center, delta, angle
std::string bdsItemName;
std::string minItemName;
std::string maxItemName;
std::string ctrItemName;
std::string dltItemName;
std::string angItemName;
if (!m_itemInfo.component().attribute("Bounds", bdsItemName))
{
bdsItemName = "Bounds";
}
if (!m_itemInfo.component().attribute("Min", minItemName))
{
minItemName = "Min";
}
if (!m_itemInfo.component().attribute("Max", maxItemName))
{
maxItemName = "Max";
}
if (!m_itemInfo.component().attribute("Center", ctrItemName))
{
ctrItemName = "Center";
}
if (!m_itemInfo.component().attribute("Deltas", dltItemName))
{
dltItemName = "Deltas";
}
if (!m_itemInfo.component().attribute("Angles", angItemName))
{
angItemName = "Angles";
}
auto bdsItem = groupItem->findAs<smtk::attribute::DoubleItem>(bdsItemName);
auto minItem = groupItem->findAs<smtk::attribute::DoubleItem>(minItemName);
auto maxItem = groupItem->findAs<smtk::attribute::DoubleItem>(maxItemName);
auto ctrItem = groupItem->findAs<smtk::attribute::DoubleItem>(ctrItemName);
auto dltItem = groupItem->findAs<smtk::attribute::DoubleItem>(dltItemName);
auto angItem = groupItem->findAs<smtk::attribute::DoubleItem>(angItemName);
bool angleItemInUse =
angItem && (!angItem->isOptional() || angItem->isEnabled()) && angItem->numberOfValues() == 3;
if (bdsItem && bdsItem->numberOfValues() == 6)
{
items.push_back(bdsItem);
if (angleItemInUse)
{
items.push_back(angItem);
binding = ItemBindings::EulerAngleBounds;
return true;
}
else
{
binding = ItemBindings::AxisAlignedBounds;
return true;
}
}
if (minItem && maxItem && minItem->numberOfValues() == 3 && maxItem->numberOfValues() == 3)
{
items.push_back(minItem);
items.push_back(maxItem);
if (angleItemInUse)
{
items.push_back(angItem);
binding = ItemBindings::EulerAngleMinMax;
return true;
}
else
{
binding = ItemBindings::AxisAlignedMinMax;
return true;
}
}
if (ctrItem && dltItem && ctrItem->numberOfValues() == 3 && dltItem->numberOfValues() == 3)
{
items.push_back(ctrItem);
items.push_back(dltItem);
if (angleItemInUse)
{
items.push_back(angItem);
binding = ItemBindings::EulerAngleCenterDeltas;
return true;
}
else
{
binding = ItemBindings::AxisAlignedCenterDeltas;
return true;
}
}
binding = ItemBindings::Invalid;
return false;
}
......@@ -39,16 +39,29 @@ public:
static qtItem* createBoxItemWidget(const AttributeItemInfo& info);
bool createProxyAndWidget(vtkSMProxy*& proxy, pqInteractivePropertyWidget*& widget) override;
/// Retrieve property values from ParaView proxy and store them in the attribute's Item.
void updateItemFromWidget() override;
/// Retrieve property values from the attribute's Item and update the ParaView proxy.
void updateWidgetFromItem() override;
protected:
/// Describe how an attribute's items specify a bounding box.
enum class ItemBindings
{
AxisAlignedBounds, //!< 1 item with 6 values (xmin, xmax, ymin, ymax, zmin, zmax)
AxisAlignedMinMax, //!< 2 items with 3 values each (xlo, ylo, zlo), (xhi, yhi, zhi)
AxisAlignedCenterDeltas, //!< 2 items with 3 values each (xc, yc, zc), (dx, dy, dz)
EulerAngleMinMax, //!< 3 items with 3 values each (xlo, ylo, zlo), (xhi, yhi, zhi), (roll, pitch, yaw)
EulerAngleCenterDeltas //!< 3 items with 3 values each (xc, yc, zc), (dx, dy, dz), (roll, pitch, yaw)
/// 1 item with 6 values (xmin, xmax, ymin, ymax, zmin, zmax)
AxisAlignedBounds,
/// 2 items with 3 values each (xlo, ylo, zlo), (xhi, yhi, zhi)
AxisAlignedMinMax,
/// 2 items with 3 values each (xc, yc, zc), (dx, dy, dz)
AxisAlignedCenterDeltas,
/// 1 item with 6 values (min/max as above), 1 item with (roll, pitch, yaw)
EulerAngleBounds,
/// 3 items with 3 values each (xlo, ylo, zlo), (xhi, yhi, zhi), (roll, pitch, yaw)
EulerAngleMinMax,
/// 3 items with 3 values each (xc, yc, zc), (dx, dy, dz), (roll, pitch, yaw)
EulerAngleCenterDeltas,
/// No consistent set of items detected.
Invalid
};
/**\brief Starting with the widget's assigned item (which may
* be a GroupItem or a DoubleItem), determine and return bound items.
......@@ -56,15 +69,14 @@ protected:
* If errors are encountered, this method returns false.
* If the name of a DoubleItem is provided, then the AxisAlignedBounds binding
* is assumed and that item is returned as the sole entry of \items.
* Otherwise, the named item must be a Group holding items called out as one
* of the following:
* + AxisAlignedMinMax: "Min", "Max" with numberOfValues == 3
* + AxisAlignedCenterDeltas: "Center", "Deltas", with numberOfValues == 3
* + EulerAngleMinMax: "Angles", "Min", "Max" with numberOfValues == 3
* + EulerAngleCenterDeltas: "Angles, "Center", "Deltas" with numberOfValues == 3
* Otherwise, the named item must be a Group holding items called out as via
* one of the remaining valid ItemBindings enumerants.
*
* Euler angles must be provided in degrees and are roll, pitch, and yaw
* (i.e., rotation about the x, y, and z axes, respectively).
* Angles must be provided in degrees and are Tait-Bryan angles
* as used by VTK (i.e., rotations about y then x then z axes).
* These are similar to Euler angles and sometimes called as such.
* See https://en.wikipedia.org/wiki/Euler_angles#Intrinsic_rotations
* for more information. VTK uses the Y_1 X_2 Z_3 ordering.
*/
bool fetchBoxItems(ItemBindings& binding, std::vector<smtk::attribute::DoubleItemPtr>& items);
};
......