Commit 45c19684 authored by David Thompson's avatar David Thompson
Browse files

Add geometry style (parametric/discrete) to models.

This commit adds a new enum named `ModelGeometryStyle` used to
indicate whether a model is discrete or parametric (or some other
type in the future — maybe point clouds, implicit models like RBFs
or other non-solid-models). The various sessions mark their models
with the appropriate type. It is stored as an integer property.

This also adds some new image tests to the various sessions.

Finally, it adds SetEntityProperty to the simple Python API
and uses that in the image tests to set colors on groups.
parent 98cf3247
......@@ -21,6 +21,25 @@ When a model operation is performed,
— depending on how much information the modeling kernel provides about affected model entities —
entities in SMTK’s storage are partially or totally marked as dirty and retranscribed on demand.
Transcription involves mapping :smtk:`unique identifiers <UUID>` to
:smtk:`Entity` records, to :smtk:`Tessellation` records, to :smtk:`Arrangement`
records, and to property dictionaries.
A property dictionary maps a name to a vector of string, floating-point, and/or integer values
of arbitrary length per model entity.
While property names are arbitrary, there are some special property names used by the
modeling system:
* :smtk:`SMTK_GEOM_STYLE_PROP` marks models with a :smtk:`ModelGeometryStyle` enum
indicating whether the model is discrete or parametric.
* :smtk:`SMTK_TESS_GEN_PROP` marks cells that have tessellations with an integer
"generation" number indicating the age of the tessellation.
* :smtk:`SMTK_MESH_GEN_PROP` marks cells that have an analysis mesh with an integer
"generation" number indicating the age of the mesh.
When a property value can be reliably determined by a session's modeling kernel
(independent of the model manager), the session should add that property name to the list
reported to the model manager for erasure when a model entity is being deleted.
(Other user-assigned properties are not deleted by default when an entity is erased.)
Registration and initialization of Sessions and Operators
---------------------------------------------------------
......
......@@ -36,6 +36,7 @@
#ifdef CGM_HAVE_VERSION_H
# include "cgm_version.h"
#endif
#include "GeometryQueryEngine.hpp"
#include "RefEntity.hpp"
#include "DagType.hpp"
#include "Body.hpp"
......@@ -54,6 +55,8 @@
#include "GMem.hpp"
#include <string.h> // for strcmp
typedef DLIList<RefEntity*> DLIRefList;
using smtk::model::EntityRef;
......@@ -381,6 +384,10 @@ smtk::model::SessionInfoBits Session::addBodyToManager(
if (requestedInfo & smtk::model::SESSION_PROPERTIES)
{
// Set properties.
smtk::model::ModelGeometryStyle gstyle =
!strcmp("facet", body->get_geometry_query_engine()->modeler_type()) ?
smtk::model::DISCRETE : smtk::model::PARAMETRIC;
mutableEntityRef.setIntegerProperty(SMTK_GEOM_STYLE_PROP, static_cast<int>(gstyle));
this->addNamesIfAny(mutableEntityRef, body);
// If the color is not the default color, add it as a property.
this->colorPropFromIndex(mutableEntityRef, body->color());
......
......@@ -74,6 +74,10 @@ class TestCGMBooleans(smtk.testing.TestCase):
self.assertImageMatchIfFileExists(['baselines', 'cgm', 'booleans.png'])
self.interact()
mod = smtk.model.Model(bsuni)
self.assertEqual(
mod.geometryStyle(), smtk.model.PARAMETRIC,
'Expected a parametric solid model, got {gs}'.format(gs=mod.geometryStyle()))
if self.writeJSON:
json = smtk.io.ExportJSON.fromModelManager(self.mgr)
sphFile = open('boolean.json', 'w')
......
......@@ -1523,6 +1523,10 @@ bool Session::addProperties(
cellOut.setName(uname);
}
}
if (cellOut.isModel())
{
cellOut.setIntegerProperty(SMTK_GEOM_STYLE_PROP, smtk::model::DISCRETE);
}
return hasProps;
}
......
......@@ -16,30 +16,94 @@ import smtk
import smtk.testing
from smtk.simple import *
try:
import unittest2 as unittest
except ImportError:
import unittest
def hex2rgb(hexstr):
hh = hexstr[1:] if hexstr[0] == '#' else hexstr
rr = int(hh[0:2],16) / 255.
gg = int(hh[2:4],16) / 255.
bb = int(hh[4:6],16) / 255.
return (rr, gg, bb)
class TestDiscreteSession(unittest.TestCase):
class TestDiscreteSession(smtk.testing.TestCase):
def resetTestFiles(self):
self.filesToTest = []
def addExternalFile(self, pathStr, numCells, numGroups):
self.filesToTest += [{'filename':pathStr, 'numCells':numCells, 'numGroups':numGroups}]
def addExternalFile(self, pathStr, numCells, numGroups, validator = None):
self.filesToTest += [{'filename':pathStr, 'numCells':numCells, 'numGroups':numGroups, 'validator':validator}]
def addTestFile(self, pathList, numCells, numGroups):
def addTestFile(self, pathList, numCells, numGroups, validator = None):
self.addExternalFile(
os.path.join(*([smtk.testing.DATA_DIR,] + pathList)),
numCells, numGroups)
numCells, numGroups, validator)
def validateTest2D(self, model):
"Verify that the test2D model is imported correctly."
faces = [smtk.model.Face(x) for x in model.cells()]
f4l = [f for f in faces if f.name() == 'Face4']
self.assertEqual(len(f4l), 1, 'Could not find test2D "Face4"')
face4 = f4l[0]
outer = face4.positiveUse().loops()
self.assertEqual(len(outer), 1, 'Face4 should have 1 outer loop')
inner = outer[0].containedLoops()
self.assertEqual(len(inner), 1, 'Face4\'s outer loop should have 1 inner loop')
self.assertEqual(len(inner[0].edgeUses()), 1, 'Face4\'s inner loop should have 1 edge use')
innerEdge = inner[0].edgeUses()[0].edge()
self.assertEqual(innerEdge.name(), 'Edge10', 'Face4\'s inner loop should one edge named "Edge10"')
def validatePMDC(self, model):
"Verify that the PMDC model is imported correctly."
# Assign a color to each face in the groups.
groupColors = {
'Coil1': '#875d4f',
'Coil2': '#896f59',
'Coil3': '#a99b86',
'Core': '#5c3935',
'Magnet1Caps': '#093020',
'Magnet1RotorFace': '#0e433b',
'Magnet2Caps': '#104c57',
'Magnet2RotorFace': '#0e5168',
'MagnetHousingFace': '#125b6e',
'OuterSurf': '#90ab77',
'OuterUpperLower': '#a7b894',
'Rotor': '#c4d6b3',
'RotorMagnetFace': '#5a8559',
}
for grp in model.groups():
if grp.name() in groupColors:
color = groupColors[grp.name()]
SetEntityProperty(grp.members(), 'color', as_float=hex2rgb(color))
# TODO: Should run grow operator on some of the faces here.
# Especially if we test group membership afterwards.
if self.haveVTK() and self.haveVTKExtension():
self.startRenderTest()
mbs = self.addModelToScene(model)
self.renderer.SetBackground(1,1,1)
cam = self.renderer.GetActiveCamera()
cam.SetFocalPoint(0., 0., 0.)
cam.SetPosition(10,15,20)
cam.SetViewUp(0,1,0)
self.renderer.ResetCamera()
self.renderWindow.Render()
self.assertImageMatch(['baselines', 'discrete', 'pmdc.png'])
self.interact()
def setUp(self):
import os, sys
self.resetTestFiles()
self.addTestFile(['cmb', 'test2D.cmb'], 4, 0)
self.addTestFile(['cmb', 'test2D.cmb'], 4, 0, self.validateTest2D)
self.addTestFile(['cmb', 'SimpleBox.cmb'], 1, 2)
self.addTestFile(['cmb', 'smooth_surface.cmb'], 6, 0)
self.addTestFile(['cmb', 'pmdc.cmb'], 7, 13, self.validatePMDC)
self.mgr = smtk.model.Manager.create()
sess = self.mgr.createSession('discrete')
......@@ -59,24 +123,37 @@ class TestDiscreteSession(unittest.TestCase):
print '\n '.join(sref.operatorNames())
print '\n'
def verifyRead(self, filename, numCells, numGroups):
def verifyRead(self, filename, numCells, numGroups, validator):
"""Read a single file and validate that the reader worked.
This is done by checking the number of free cells and groups
reported by the output model as well as running an optional
function on the model to do further model-specific testing."""
print '\n\nFile: {fname}'.format(fname=filename)
mod = smtk.model.Model(Read(filename)[0])
print ' {mt} model'.format(mt=smtk.model.ModelGeometryStyleName(mod.geometryStyle()))
print '\nFree cells:\n %s' % '\n '.join([x.name() for x in mod.cells()])
print '\nGroups:\n %s\n' % '\n '.join([x.name() for x in mod.groups()])
if (numCells >= 0 and len(mod.cells()) != numCells) or (numGroups >= 0 and len(mod.groups()) != numGroups):
print smtk.io.ExportJSON.fromModelManager(self.mgr)
self.assertEqual(
mod.geometryStyle(), smtk.model.DISCRETE,
'Expected a discrete model, got a {mt} model'.format(
mt=smtk.model.ModelGeometryStyleName(mod.geometryStyle())))
if numCells >= 0:
self.assertEqual(len(mod.cells()), numCells, 'Expected {nc} free cells'.format(nc=numCells))
if numGroups >= 0:
self.assertEqual(len(mod.groups()), numGroups, 'Expected {ng} groups'.format(ng=numGroups))
if validator:
validator(mod)
def testRead(self):
"Read each file named in setUp and validate the reader worked."
for test in self.filesToTest:
self.verifyRead(test['filename'], test['numCells'], test['numGroups'])
self.verifyRead(test['filename'], test['numCells'], test['numGroups'], test['validator'])
if self.shouldSave:
out = file('test.json', 'w')
......@@ -85,4 +162,4 @@ class TestDiscreteSession(unittest.TestCase):
if __name__ == '__main__':
smtk.testing.process_arguments()
unittest.main()
smtk.testing.main()
......@@ -189,6 +189,8 @@ SessionInfoBits Session::transcribeInternal(
case EXO_MODEL:
mutableEntityRef.manager()->insertModel(
mutableEntityRef.entity(), dim, dim);
mutableEntityRef.setIntegerProperty(
SMTK_GEOM_STYLE_PROP, smtk::model::DISCRETE);
break;
case EXO_BLOCK:
entityDimBits = Entity::dimensionToDimensionBits(dim);
......
......@@ -13,12 +13,14 @@ import smtk
import smtk.testing
from smtk.simple import *
try:
import unittest2 as unittest
except ImportError:
import unittest
def hex2rgb(hexstr):
hh = hexstr[1:] if hexstr[0] == '#' else hexstr
rr = int(hh[0:2],16) / 255.
gg = int(hh[2:4],16) / 255.
bb = int(hh[4:6],16) / 255.
return (rr, gg, bb)
class TestExodusSession(unittest.TestCase):
class TestExodusSession(smtk.testing.TestCase):
def setUp(self):
import os, sys
......@@ -32,20 +34,26 @@ class TestExodusSession(unittest.TestCase):
ents = Read(self.filename)
self.model = smtk.model.Model(ents[0])
#Verify that the model is marked as discrete
self.assertEqual(
self.model.geometryStyle(), smtk.model.DISCRETE,
'Expected discrete model, got {gs}'.format(gs=self.model.geometryStyle()))
#Verify that the file contains the proper number of groups.
numGroups = len(self.model.groups())
self.assertEqual(numGroups, 11, 'Expected 11 groups, found %d' % numGroups)
#Verify that the group names match those from the Exodus file.
nameset = set([
'Unnamed block ID: 1 Type: HEX8',
'Unnamed set ID: 1',
'Unnamed set ID: 2',
'Unnamed set ID: 3',
'Unnamed set ID: 4',
'Unnamed set ID: 5',
'Unnamed set ID: 6',
'Unnamed set ID: 7'])
nameset = {
'Unnamed block ID: 1 Type: HEX8': '#5a5255',
'Unnamed set ID: 1': '#ae5a41',
'Unnamed set ID: 2': '#559e83',
'Unnamed set ID: 3': '#c3cb71',
'Unnamed set ID: 4': '#1b85b8',
'Unnamed set ID: 5': '#cb2c31',
'Unnamed set ID: 6': '#8b1ec4',
'Unnamed set ID: 7': '#ff6700'
}
self.assertTrue(all([x.name() in nameset for x in self.model.groups()]),
'Not all group names recognized.')
......@@ -67,6 +75,26 @@ class TestExodusSession(unittest.TestCase):
self.assertEqual(gtc, expectedgrouptypecounts,
'At least one group was of the wrong type.')
if self.haveVTK() and self.haveVTKExtension():
# Render groups with colors:
for grp in self.model.groups():
color = hex2rgb(nameset[grp.name()])
SetEntityProperty(grp, 'color', as_float=color)
self.startRenderTest()
mbs = self.addModelToScene(self.model)
self.renderer.SetBackground(1,1,1)
cam = self.renderer.GetActiveCamera()
cam.SetFocalPoint(0., 0., 0.)
cam.SetPosition(19,17,-43)
cam.SetViewUp(-0.891963, -0.122107, -0.435306)
self.renderer.ResetCamera()
self.renderWindow.Render()
self.assertImageMatch(['baselines', 'exodus', 'disk_out_ref.png'])
self.interact()
if __name__ == '__main__':
smtk.testing.process_arguments()
unittest.main()
smtk.testing.main()
......@@ -13,6 +13,8 @@
#include "smtk/CoreExports.h" // for SMTKCORE_EXPORT macro
#include "smtk/SystemConfig.h"
#include <string>
namespace smtk {
namespace model {
......@@ -183,6 +185,65 @@ inline bool isInstance(BitFlags entityFlags) { return (entityFlags & ENTITY_MASK
inline bool isSessionRef(BitFlags entityFlags) { return (entityFlags & ENTITY_MASK) == SESSION; }
/**\brief Enumerate how model domains are represented.
*
* Currently a model may be listed as PARAMETRIC or DISCRETE.
*
* PARAMETRIC models have cells that are assumed to have a
* parameter-space domain mapped to world coordinates by a
* smooth curve or surface, usually described with a single
* piecewise-continuous spline per entity.
* Their display tessellations are not required to be
* conformal (water-tight) and generally aren't.
*
* DISCRETE models are piecewise linear cell complexes
* with no explicit parameter-space (although these are
* easy to generate) whose geometry is exactly represented
* by the display tessellation, which is required to be
* conformal. Each entity has 1 or more geometric primitives
* that, taken together, specify the point locus of the
* entity is world coordinates.
*
* Other (non-solid) model types such as point clouds may
* be supported eventually; do not treat this enumeration
* as a boolean value.
*/
enum ModelGeometryStyle
{
DISCRETE, /**< Cells are discretized as a polygonal/polyhedral piecewise
linear complex (PLC) with a supporting tessellation that
are usually composed of multiple primitive geometric
shapes like triangles or tetrahedra. */
PARAMETRIC /**< Cells are defined by a map from parametric coordinates to
world coordinates, usually as analytic splines. */
};
/**\brief Given a ModelGeometryStyle, return a short string description.
*
*/
inline std::string ModelGeometryStyleName(ModelGeometryStyle s)
{
switch (s)
{
case DISCRETE: return "discrete";
case PARAMETRIC: return "parametric";
default:
// fall through
break;
}
return "undefined";
}
/**\brief Given a string name, return the matching ModelGeometryStyle.
*
*/
inline ModelGeometryStyle NamedModelGeometryStyle(const std::string& s)
{
if (s == "discrete")
return DISCRETE;
return PARAMETRIC;
}
} // namespace model
} // namespace smtk
......
......@@ -59,12 +59,19 @@
* Short (8 bytes or less) means single word comparison suffices on many platforms => fast.
*/
#define SMTK_TESS_GEN_PROP "_tessgen"
/**\brief The name of an integer property used to store mesh Tessellation generation numbers.
*
* \sa SMTK_TESS_GEN_PROP
*/
#define SMTK_MESH_GEN_PROP "_meshgen"
/**\brief The name of an integer property used to store the geometric representation style of a model.
*
* \sa ModelGeometryStyle
*/
#define SMTK_GEOM_STYLE_PROP "_geomstyle"
namespace smtk {
namespace model {
......
......@@ -18,6 +18,21 @@
namespace smtk {
namespace model {
/**\brief Return information about how the modeling kernel represents this model.
*
* \sa ModelGeometryStyle
*/
ModelGeometryStyle Model::geometryStyle() const
{
if (this->hasIntegerProperty(SMTK_GEOM_STYLE_PROP))
{
const smtk::model::IntegerList& plist(this->integerProperty(SMTK_GEOM_STYLE_PROP));
if (!plist.empty())
return static_cast<ModelGeometryStyle>(plist[0]);
}
return PARAMETRIC;
}
/**\brief Set the number coordinate values used to specify each point in the locus of this model.
*
* This should meet or exceed the maxParametricDimension() of every entity in the model.
......
......@@ -30,6 +30,8 @@ class SMTKCORE_EXPORT Model : public EntityRef
public:
SMTK_ENTITYREF_CLASS(Model,EntityRef,isModel);
ModelGeometryStyle geometryStyle() const;
void setEmbeddingDimension(int dim);
EntityRef parent() const;
......
......@@ -163,36 +163,45 @@ DescriptivePhrases SimpleModelSubphrases::subphrases(
bool SimpleModelSubphrases::shouldOmitProperty(
DescriptivePhrase::Ptr parent, PropertyType ptype, const std::string& pname) const
{
if (ptype == STRING_PROPERTY && pname == "name")
return true;
if (ptype == FLOAT_PROPERTY && pname == "color")
return true;
if (ptype == INTEGER_PROPERTY && pname == "visible")
return true;
if (ptype == INTEGER_PROPERTY && pname == "block_index")
return true;
if (ptype == INTEGER_PROPERTY && pname == "visibility")
return true;
if (ptype == INTEGER_PROPERTY && pname == "cmb id")
return true;
if (ptype == INTEGER_PROPERTY && pname == "membership mask")
return true;
if (ptype == STRING_PROPERTY)
{
if (pname == "name")
return true;
}
if (
ptype == INTEGER_PROPERTY &&
parent && parent->relatedEntity().isModel())
if (ptype == FLOAT_PROPERTY)
{
if (pname.find("_counters") != std::string::npos)
if (pname == "color")
return true;
}
if (
ptype == INTEGER_PROPERTY &&
parent && parent->relatedEntity().isCellEntity())
if (ptype == INTEGER_PROPERTY)
{
if (pname.find(SMTK_TESS_GEN_PROP) != std::string::npos)
if (pname == "visible")
return true;
else if (pname == "block_index")
return true;
else if (pname == "visibility")
return true;
else if (pname == "cmb id")
return true;
else if (pname == "membership mask")
return true;
else if (parent)
{
if (parent->relatedEntity().isModel())
{
if (pname.find("_counters") != std::string::npos)
return true;
else if (pname.find(SMTK_GEOM_STYLE_PROP) != std::string::npos)
return true;
}
else if (parent->relatedEntity().isCellEntity())
{
if (pname.find(SMTK_TESS_GEN_PROP) != std::string::npos)
return true;
}
}
}
return false;
}
......
......@@ -419,6 +419,44 @@ def Sweep(stuffToSweep, method = SweepType.EXTRUDE, **kwargs):
PrintResultLog(res)
return res.findModelEntity('created').value(0)
def SetEntityProperty(ents, propName, **kwargs):
"""Set a property value (or vector of values) on an entity (or vector of entities).
You may pass any combination of "as_int", "as_float", or "as_string" as named
arguments specifying the property values. The values of these named arguments may
be a single value or a list of values. Values will be coerced to the named type.
Example:
SetEntityProperty(face, 'color', as_float=(1., 0., 0.))
SetEntityProperty(edge, 'name', as_string='edge 20')
SetEntityProperty(body, 'visited', as_int='edge 20')
"""
sref = GetActiveSession()
spr = sref.op('set property')
if hasattr(ents, '__iter__'):
[spr.associateEntity(ent) for ent in ents]
else:
spr.associateEntity(ents)
spr.findAsString('name').setValue(propName)
if 'as_int' in kwargs:
vlist = kwargs['as_int']
if not hasattr(vlist, '__iter__'):
vlist = [vlist,]
SetVectorValue(spr.findAsInt('integer value'), vlist)
if 'as_float' in kwargs:
vlist = kwargs['as_float']
if not hasattr(vlist, '__iter__'):
vlist = [vlist,]
SetVectorValue(spr.findAsDouble('float value'), vlist)
if 'as_string' in kwargs:
vlist = kwargs['as_string']
if not hasattr(vlist, '__iter__'):
vlist = [vlist,]
SetVectorValue(spr.findAsString('string value'), vlist)
res = spr.operate()
return res.findInt('outcome').value(0)
def Read(filename, **kwargs):
"""Read entities from an file (in the native modeling kernel format).
"""
......@@ -431,6 +469,21 @@ def Read(filename, **kwargs):
PrintResultLog(res)
return GetVectorValue(res.findModelEntity('created'))
def Import(filename, **kwargs):
"""Import entities from an file (in a non-native modeling kernel format).
"""
sref = GetActiveSession()
rdr = sref.op('import')
# Not all modeling kernels have an import operator:
if rdr == None:
return []
rdr.findAsFile('filename').setValue(0, filename)
if 'filetype' in kwargs:
rdr.findAsString('filetype').setValue(0, kwargs['filetype'])
res = rdr.operate()
PrintResultLog(res)
return GetVectorValue(res.findModelEntity('created'))
def Write(filename, entities = [], **kwargs):
"""Write a set of entities to an file (in the native modeling kernel format).
"""
......
......@@ -22,6 +22,7 @@
<suppress-warning text="skipping field 'RefItem::m_values' with unmatched type 'std::vector&lt;attribute::WeakAttributePtr&gt;'"/>
<suppress-warning text="skipping field 'RefItemDefinition::m_definition' with unmatched type 'smtk::weak_ptr&lt;smtk::attribute::Definition&gt;'"/>
<suppress-warning text="skipping field 'EntityRef::m_manager' with unmatched type 'smtk::weak_ptr&lt;smtk::model::Manager&gt;'"/>
<suppress-warning text="skipping field 'Manager::m_analysisMesh' with unmatched type 'smtk::shared_ptr&lt;UUIDsToTessellations&gt;'"/>
<!-- Suppressed because they are templated methods -->
<suppress-warning text="skipping function 'smtk::view::Group::addSubView', unmatched return type 'smtk::internal::shared_ptr_type&lt;T&gt;::SharedPointerType'"/>
......@@ -80,6 +81,7 @@
<!-- Use cJSON objects. For now we will ignore these-->
<suppress-warning text="skipping function 'smtk::io::ImportJSON::ofDanglingEntities', unmatched parameter type 'cJSON*'"/>
</