diff --git a/data/attribute/widgets/gallery-disk.sbt b/data/attribute/widgets/gallery-disk.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..8853ddbf0592a36def885d8e16378019608404d5
--- /dev/null
+++ b/data/attribute/widgets/gallery-disk.sbt
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<SMTK_AttributeResource Version="3">
+  <Definitions>
+    <AttDef Type="Example">
+      <ItemDefinitions>
+        <Group Name="disk" NumberOfRequiredGroups="1">
+          <ItemDefinitions>
+            <Double Name="center" NumberOfRequiredValues="3"/>
+            <Double Name="normal" NumberOfRequiredValues="3"/>
+            <Double Name="radius"/>
+          </ItemDefinitions>
+        </Group>
+      </ItemDefinitions>
+    </AttDef>
+  </Definitions>
+  <Attributes>
+    <Att Name="Disk Widget Example" Type="Example">
+      <Items>
+        <Group Name="disk" NumberOfGroups="1">
+          <GroupClusters>
+            <Cluster Ith="0">
+              <Double Name="center">
+                <Values>
+                  <Val Ith="0">0.0</Val>
+                  <Val Ith="1">0.0</Val>
+                  <Val Ith="2">0.0</Val>
+                </Values>
+              </Double>
+              <Double Name="normal">
+                <Values>
+                  <Val Ith="0">0.0</Val>
+                  <Val Ith="1">0.0</Val>
+                  <Val Ith="2">1.0</Val>
+                </Values>
+              </Double>
+              <Double Name="radius">
+                <Values>
+                  <Val Ith="0">0.5</Val>
+                </Values>
+              </Double>
+            </Cluster>
+          </GroupClusters>
+        </Group>
+      </Items>
+    </Att>
+  </Attributes>
+  <Views>
+    <View
+      Type="Instanced"
+      Title="Example"
+      TopLevel="true"
+      FilterByAdvanceLevel="false"
+      FilterByCategoryMode="false"
+    >
+      <InstancedAttributes>
+        <Att Type="Example" Name="Disk Widget Example"
+        >
+          <ItemViews>
+            <View Item="disk" Type="Disk" Center="center" Normal="normal" Radius="radius" ShowControls="true"/>
+          </ItemViews>
+        </Att>
+      </InstancedAttributes>
+    </View>
+  </Views>
+</SMTK_AttributeResource>
diff --git a/doc/release/notes/geometry-attribute.rst b/doc/release/notes/geometry-attribute.rst
new file mode 100644
index 0000000000000000000000000000000000000000..ff7f1c06bb33e08666e8f4b1839ff902073e2e14
--- /dev/null
+++ b/doc/release/notes/geometry-attribute.rst
@@ -0,0 +1,12 @@
+Geometry Subsystem
+==================
+
+Attribute Resources with Renderable Geometry
+--------------------------------------------
+
+The ``smtk::extension::vtk::source::SourceFromAttribute`` class has been removed.
+This class provided no renderable geometry for attribute resources
+and interfered with other plugins that do provide renderable geometry.
+
+Previously, it served to force creation of a ParaView pipeline object
+for attribute resources, but the need for this no longer exists.
diff --git a/doc/release/notes/operation-handlers.rst b/doc/release/notes/operation-handlers.rst
new file mode 100644
index 0000000000000000000000000000000000000000..758d14a9a6090a381a90b5033a6036fe1306ec27
--- /dev/null
+++ b/doc/release/notes/operation-handlers.rst
@@ -0,0 +1,32 @@
+Operation System
+================
+
+Handlers for specific operation instances
+-----------------------------------------
+
+You may now add and remove handlers to an instance of an operation.
+A handler is a function to be invoked upon the next completion of
+one instance of an operation object (regardless of whether the result
+indicates success or failure).
+
+Handlers are function objects with a signature similar to observers
+(see :ref:`operation-observers`)
+except that they are only called upon completion of an operation:
+handlers may not cancel the operation and are not passed an ``EventType``
+(only the operation and its result).
+Handlers, like observers, are invoked at a time when all the resource
+locks required for the operation are held.
+
+Handlers are invoked on the thread in which the operation is run (unlike
+observers, which are invoked on the main/GUI thread in Qt applications).
+
+Handlers are invoked only for the instance of the operation they are
+added to; if you create multiple operations of the same type and add
+a handler to one, only that instance will have the handler called.
+
+Handlers are invoked zero or one times at most.
+Operation handlers are removed each time the operation is invoked;
+you are responsible for adding handlers for each invocation.
+It is acceptable for a handler to add itself to the operation which
+invoked it (as the container of handlers is copied and cleared before
+any handlers are invoked).
diff --git a/doc/release/notes/resource-unit-system-typo.rst b/doc/release/notes/resource-unit-system-typo.rst
new file mode 100644
index 0000000000000000000000000000000000000000..815a23e621908bafe6f67373d023803756f292bd
--- /dev/null
+++ b/doc/release/notes/resource-unit-system-typo.rst
@@ -0,0 +1,21 @@
+Resource system
+===============
+
+Unit system API name change
+---------------------------
+
+The methods to set or get the system of units for a resource have
+been renamed to match the terminology most engineers use: "unit"
+is singular since there is one system holding all of the units.
+
++ :smtk:`unitsSystem()<smtk::resource::Resource::unitsSystem()` becomes
+  :smtk:`unitSystem()<smtk::resource::Resource::unitSystem()>`
++ :smtk:`setUnitsSystem()<smtk::resource::Resource::setUnitsSystem()` becomes
+  :smtk:`setUnitSystem()<smtk::resource::Resource::setUnitSystem()`
+
+The attribute resource has additional methods that have changed:
+
++ :smtk:`Definition::setItemDefinitionUnitsSystem()<smtk::attribute::Definition::setItemDefinitionUnitsSystem()` becomes
+  :smtk:`Definition::setItemDefinitionUnitSystem()<smtk::attribute::Definition::setItemDefinitionUnitSystem()`
++ :smtk:`ItemDefinition::setUnitsSystem()<smtk::attribute::Definition::setUnitsSystem()` becomes
+  :smtk:`ItemDefinition::setUnitSystem()<smtk::attribute::Definition::setUnitSystem()`
diff --git a/doc/userguide/attribute/concepts.rst b/doc/userguide/attribute/concepts.rst
index c96b65d1d3fd1222c8d4a0f64c6758ad6099b1db..ef2c82c638394e00760c0b381e09c94ecbcc070c 100644
--- a/doc/userguide/attribute/concepts.rst
+++ b/doc/userguide/attribute/concepts.rst
@@ -379,7 +379,7 @@ Related API
 * :smtk:`Definition::units` - method to return the units associated with the definition (either locally set or inherited from its derived definition)
 * :smtk:`Definition::localUnits` - method to return the local units explicitly associated with the definition
 * :smtk:`Definition::setLocalUnits` - method to set the units explicitly associated with the definition
-* :smtk:`Definition::unitsSystem` - method to return the units system associated with the definition
+* :smtk:`Definition::unitSystem` - method to return the units system associated with the definition
 
 Please see `unitAttributeUnits <https://gitlab.kitware.com/cmb/smtk/-/blob/master/smtk/attribute/testing/cxx/unitAttributeUnits.cxx>`_ for a simple example of using units with Attributes and Definitions.
 
diff --git a/doc/userguide/operation/operators.rst b/doc/userguide/operation/operators.rst
index 98a63f0ddd9ac08d69c6eff994716795e6604c06..3d6beb6259036c460d17d897c440a20c4668c935 100644
--- a/doc/userguide/operation/operators.rst
+++ b/doc/userguide/operation/operators.rst
@@ -60,3 +60,34 @@ means that one need not construct an instance of the operator's C++ class in ord
 to obtain information about it;
 instead, simply call :smtk:`operatorSystem() <smtk::model::Session::operatorSystem>`
 on the session and ask for all the definitions which inherit "operator".
+
+Handlers
+--------
+
+You may add and remove handlers to an instance of an operation.
+A handler is a function to be invoked upon the next completion of
+one instance of an operation object (regardless of whether the result
+indicates success or failure).
+
+Handlers are function objects with a signature similar to observers
+except that they are only called upon completion of an operation:
+handlers may not cancel the operation and are not passed an ``EventType``
+(only the operation and its result).
+Handlers, like observers, are invoked at a time when all the resource
+locks required for the operation are held.
+
+Handlers are invoked on the thread in which the operation is run (unlike
+observers, which are invoked on the main/GUI thread in Qt applications).
+
+Handlers are invoked only for the instance of the operation they are
+added to; if you create multiple operations of the same type and add
+a handler to one, only that instance will have the handler called.
+
+Handlers are invoked zero or one times at most.
+Operation handlers are removed each time the operation is invoked;
+you are responsible for adding handlers for each invocation.
+If an operation is canceled by an observer, the handler is removed
+from the operation and not invoked.
+It is acceptable for a handler to add itself to the operation which
+invoked it (as the container of handlers is copied and cleared before
+any handlers are invoked).
diff --git a/doc/userguide/operation/support.rst b/doc/userguide/operation/support.rst
index 0cef96f93231c2e3c5fd186ce5cbe2ccfe6525e6..668bd75218315b6101e9346f94fe9ea598d06638 100644
--- a/doc/userguide/operation/support.rst
+++ b/doc/userguide/operation/support.rst
@@ -188,6 +188,8 @@ as may operations that only require read access to the same resource.
 However, operations that require write access to the same resource will
 be run sequentially.
 
+.. _operation-observers:
+
 Observing operations
 --------------------
 
diff --git a/doc/userguide/task/agents.rst b/doc/userguide/task/agents.rst
index 0a97ef82677a895b8a4566d1c02ba1201a6fb660..7b5b7cb958f7eb1ed95250766feb252fbfa704fe 100644
--- a/doc/userguide/task/agents.rst
+++ b/doc/userguide/task/agents.rst
@@ -488,6 +488,10 @@ this agent:
   If an object is a resource, each specifier is a tuple holding a UUID and ``null``.
   If an object is a component, each specified is a tuple holding the UUID of the
   component's parent resource and the component's UUID.
+* ``required-counts``: is a map from a role name to an array of 2 integers specifying
+  the minimum and maximum number of objects permitted in the given role. A ``-1`` for
+  the second array value indicates there is no maximum. If both numbers are ``-1``,
+  then no objects are allowed in the given role.
 
 Example
 """""""
diff --git a/smtk/attribute/Attribute.cxx b/smtk/attribute/Attribute.cxx
index 0d9e37b01cfc4837864fc1801e0c21fcb8ba70f6..244c780cd84232ec18c66c83231feb28596730aa 100644
--- a/smtk/attribute/Attribute.cxx
+++ b/smtk/attribute/Attribute.cxx
@@ -1208,7 +1208,7 @@ bool Attribute::setLocalUnits(const std::string& newUnits)
     return false;
   }
 
-  const auto& unitSys = m_definition->unitsSystem();
+  const auto& unitSys = m_definition->unitSystem();
   // Can't determine if the units are compatible w/o units system
   if (!unitSys)
   {
diff --git a/smtk/attribute/Definition.cxx b/smtk/attribute/Definition.cxx
index 5a233cfd24285627e09c289d47443457c252486b..d533adef5be1a8e32d3d071b60e942a7caba4ac7 100644
--- a/smtk/attribute/Definition.cxx
+++ b/smtk/attribute/Definition.cxx
@@ -690,19 +690,19 @@ bool Definition::addItemDefinition(smtk::attribute::ItemDefinitionPtr cdef)
   std::size_t n = m_itemDefs.size();
   m_itemDefs.push_back(cdef);
   m_itemDefPositions[cdef->name()] = static_cast<int>(n);
-  this->setItemDefinitionUnitsSystem(cdef);
+  this->setItemDefinitionUnitSystem(cdef);
   this->updateDerivedDefinitions();
   return true;
 }
 
-void Definition::setItemDefinitionUnitsSystem(
+void Definition::setItemDefinitionUnitSystem(
   const smtk::attribute::ItemDefinitionPtr& itemDef) const
 {
-  const auto& defUnitsSystem = this->unitsSystem();
+  const auto& defUnitSystem = this->unitSystem();
   auto attRes = this->attributeResource();
-  if (defUnitsSystem)
+  if (defUnitSystem)
   {
-    itemDef->setUnitsSystem(defUnitsSystem);
+    itemDef->setUnitSystem(defUnitSystem);
   }
 }
 
@@ -914,15 +914,15 @@ void Definition::applyAdvanceLevels(
   }
 }
 
-const std::shared_ptr<units::System>& Definition::unitsSystem() const
+const std::shared_ptr<units::System>& Definition::unitSystem() const
 {
-  static std::shared_ptr<units::System> nullUnitsSystem;
+  static std::shared_ptr<units::System> nullUnitSystem;
   auto attRes = this->attributeResource();
   if (attRes)
   {
-    return attRes->unitsSystem();
+    return attRes->unitSystem();
   }
-  return nullUnitsSystem;
+  return nullUnitSystem;
 }
 
 bool Definition::setLocalUnits(const std::string& newUnits, bool force)
@@ -933,7 +933,7 @@ bool Definition::setLocalUnits(const std::string& newUnits, bool force)
     m_localUnits = newUnits;
     return true;
   }
-  const auto& unitSys = this->unitsSystem();
+  const auto& unitSys = this->unitSystem();
   if (!unitSys)
   {
     return false; // There is no unit system
diff --git a/smtk/attribute/Definition.h b/smtk/attribute/Definition.h
index 5881045f0dac3b3d64438e89ce0af560d34b10dc..e39efaaacb8104f81c7431dfc785b2dabb98a919 100644
--- a/smtk/attribute/Definition.h
+++ b/smtk/attribute/Definition.h
@@ -21,6 +21,7 @@
 #include "smtk/attribute/Tag.h"
 
 #include "smtk/common/Categories.h"
+#include "smtk/common/Deprecation.h"
 
 #include "smtk/model/EntityRef.h"      //for EntityRef version of canBeAssociated
 #include "smtk/model/EntityTypeBits.h" // for BitFlags type
@@ -359,7 +360,7 @@ public:
     {
       std::size_t n = m_itemDefs.size();
       item = SharedTypes::RawPointerType::New(name);
-      this->setItemDefinitionUnitsSystem(item);
+      this->setItemDefinitionUnitSystem(item);
       m_itemDefs.push_back(item);
       m_itemDefPositions[name] = static_cast<int>(n);
       this->updateDerivedDefinitions();
@@ -481,7 +482,10 @@ public:
   bool setLocalUnits(const std::string& newUnits, bool force = false);
 
   /// \brief Gets the system of units used by this definition.
-  const std::shared_ptr<units::System>& unitsSystem() const;
+  const std::shared_ptr<units::System>& unitSystem() const;
+  SMTK_DEPRECATED_IN_NEXT("Use unitSystem() instead.")
+  const std::shared_ptr<units::System>& unitsSystem() const
+  { return this->unitSystem(); }
 
 protected:
   friend class smtk::attribute::Resource;
@@ -510,7 +514,10 @@ protected:
     const unsigned int& readLevelFromParent,
     const unsigned int& writeLevelFromParent);
 
-  void setItemDefinitionUnitsSystem(const smtk::attribute::ItemDefinitionPtr& itemDef) const;
+  void setItemDefinitionUnitSystem(const smtk::attribute::ItemDefinitionPtr& itemDef) const;
+  SMTK_DEPRECATED_IN_NEXT("Use setItemDefinitionUnitSystem() instead.")
+  void setItemDefinitionUnitsSystem(const smtk::attribute::ItemDefinitionPtr& itemDef) const
+  { this->setItemDefinitionUnitSystem(itemDef); }
 
   smtk::attribute::WeakResourcePtr m_resource;
   smtk::common::UUID m_id;
diff --git a/smtk/attribute/DoubleItem.cxx b/smtk/attribute/DoubleItem.cxx
index 5e36badb0f9d7275232d7aca4cf22c3c58bdaf72..170dfc78ccfc9ac1380202c2d6aa2a201bb51db0 100644
--- a/smtk/attribute/DoubleItem.cxx
+++ b/smtk/attribute/DoubleItem.cxx
@@ -207,26 +207,26 @@ bool DoubleItem::setValue(std::size_t element, const double& val, const std::str
   const std::string& myUnitStr = this->supportedUnits();
   if (myUnitStr != valUnitStr)
   {
-    auto unitsSystem = this->definition()->unitsSystem();
+    auto unitSystem = this->definition()->unitSystem();
     // Is there a units system specified?
-    if (!unitsSystem)
+    if (!unitSystem)
     {
       return false; // we can not convert units
     }
     bool status;
-    auto myUnits = unitsSystem->unit(myUnitStr, &status);
+    auto myUnits = unitSystem->unit(myUnitStr, &status);
     if (!status)
     {
       return false; // Could not find the base's units
     }
-    auto valUnits = unitsSystem->unit(valUnitStr, &status);
+    auto valUnits = unitSystem->unit(valUnitStr, &status);
     if (!status)
     {
       return false; // Could not find vals' units
     }
 
     units::Measurement m(val, valUnits);
-    auto newM = unitsSystem->convert(m, myUnits, &status);
+    auto newM = unitSystem->convert(m, myUnits, &status);
     if (!status)
     {
       return false; // could not convert
@@ -268,7 +268,7 @@ bool DoubleItem::setValueFromString(std::size_t element, const std::string& val)
     return false; // badly formatted string
   }
 
-  auto unitsSystem = this->definition()->unitsSystem();
+  auto unitSystem = this->definition()->unitSystem();
   const std::string& myUnitStr = this->supportedUnits();
 
   units::Unit myUnit;
@@ -276,11 +276,11 @@ bool DoubleItem::setValueFromString(std::size_t element, const std::string& val)
   // item has known units. Note that we don't need to do conversion
   // if the value does not have units specified
   bool convert = false;
-  if (unitsSystem && (!(myUnitStr.empty() || valUnitsStr.empty())))
+  if (unitSystem && (!(myUnitStr.empty() || valUnitsStr.empty())))
   {
     // If we have a units System, let's see if the base units
     // are valid?
-    myUnit = unitsSystem->unit(myUnitStr, &convert);
+    myUnit = unitSystem->unit(myUnitStr, &convert);
   }
 
   double convertedVal;
@@ -304,7 +304,7 @@ bool DoubleItem::setValueFromString(std::size_t element, const std::string& val)
     // We can convert units
     bool status;
 
-    auto valMeasure = unitsSystem->measurement(val, &status);
+    auto valMeasure = unitSystem->measurement(val, &status);
     if (!status)
     {
       // Could not parse the value
@@ -312,7 +312,7 @@ bool DoubleItem::setValueFromString(std::size_t element, const std::string& val)
     }
     if (!valMeasure.m_units.dimensionless())
     {
-      auto convertedMeasure = unitsSystem->convert(valMeasure, myUnit, &status);
+      auto convertedMeasure = unitSystem->convert(valMeasure, myUnit, &status);
       if (!status)
       {
         return false;
@@ -519,9 +519,9 @@ double DoubleItem::value(std::size_t element, smtk::io::Logger& log) const
       return eval; // no conversion needed
     }
     // We need to convert to the units of the item
-    auto unitsSystem = this->definition()->unitsSystem();
+    auto unitSystem = this->definition()->unitSystem();
     // Is there a units system specified?
-    if (!unitsSystem)
+    if (!unitSystem)
     {
       smtkErrorMacro(
         log,
@@ -531,7 +531,7 @@ double DoubleItem::value(std::size_t element, smtk::io::Logger& log) const
       return 0.0;
     }
     bool status;
-    auto myUnits = unitsSystem->unit(this->units(), &status);
+    auto myUnits = unitSystem->unit(this->units(), &status);
     if (!status)
     {
       smtkErrorMacro(
@@ -541,7 +541,7 @@ double DoubleItem::value(std::size_t element, smtk::io::Logger& log) const
                   << " are not supported for conversion.");
       return 0.0;
     }
-    auto exUnits = unitsSystem->unit(exStr, &status);
+    auto exUnits = unitSystem->unit(exStr, &status);
     if (!status)
     {
       smtkErrorMacro(
@@ -553,7 +553,7 @@ double DoubleItem::value(std::size_t element, smtk::io::Logger& log) const
     }
 
     units::Measurement m(eval, exUnits);
-    auto newM = unitsSystem->convert(m, myUnits, &status);
+    auto newM = unitSystem->convert(m, myUnits, &status);
     if (!status)
     {
       smtkErrorMacro(
diff --git a/smtk/attribute/DoubleItemDefinition.cxx b/smtk/attribute/DoubleItemDefinition.cxx
index 6dbb2bacc6ffb0a083036b4eaf082cf2c77558c1..9964adc256140aa34887449ae71d0b4bfe2e9f48 100644
--- a/smtk/attribute/DoubleItemDefinition.cxx
+++ b/smtk/attribute/DoubleItemDefinition.cxx
@@ -124,21 +124,21 @@ bool DoubleItemDefinition::setDefaultValue(
       return false;
     }
     // do we have a unit system?
-    if (m_unitsSystem)
+    if (m_unitSystem)
     {
       // Are the units compatible?
       bool status;
-      auto defUnit = m_unitsSystem->unit(m_units, &status);
+      auto defUnit = m_unitSystem->unit(m_units, &status);
       if (!status)
       {
         return false; // Could not find the definition's units
       }
-      auto valUnit = m_unitsSystem->unit(units, &status);
+      auto valUnit = m_unitSystem->unit(units, &status);
       if (!status)
       {
         return false; // Could not find the default vals' units
       }
-      auto converter = m_unitsSystem->convert(valUnit, defUnit);
+      auto converter = m_unitSystem->convert(valUnit, defUnit);
       if (!converter)
       {
         return false; // Could not find a conversion between the units
@@ -257,11 +257,11 @@ bool DoubleItemDefinition::reevaluateDefaults()
   std::string units;
   units::Unit defUnit;
   bool convert = false;
-  if (m_unitsSystem)
+  if (m_unitSystem)
   {
     // If we have an units System, lets see if the definition's units
     // are valid?
-    defUnit = m_unitsSystem->unit(m_units, &convert);
+    defUnit = m_unitSystem->unit(m_units, &convert);
   }
 
   // Can we not do conversion?
@@ -302,7 +302,7 @@ bool DoubleItemDefinition::reevaluateDefaults()
     double convertedVal;
     for (std::size_t i = 0; i < m_defaultValuesAsStrings.size(); i++)
     {
-      auto valMeasure = m_unitsSystem->measurement(m_defaultValuesAsStrings[i], &status);
+      auto valMeasure = m_unitSystem->measurement(m_defaultValuesAsStrings[i], &status);
       if (!status)
       {
         // Could not parse the value
@@ -310,7 +310,7 @@ bool DoubleItemDefinition::reevaluateDefaults()
       }
       if (!valMeasure.m_units.dimensionless())
       {
-        auto convertedMeasure = m_unitsSystem->convert(valMeasure, defUnit, &status);
+        auto convertedMeasure = m_unitSystem->convert(valMeasure, defUnit, &status);
         if (!status)
         {
           return false;
diff --git a/smtk/attribute/GroupItemDefinition.cxx b/smtk/attribute/GroupItemDefinition.cxx
index 61e05b590b081319a4a374761b3e03822520e933..eda96072f60da84765df3937dfca81345199a5eb 100644
--- a/smtk/attribute/GroupItemDefinition.cxx
+++ b/smtk/attribute/GroupItemDefinition.cxx
@@ -57,7 +57,7 @@ bool GroupItemDefinition::addItemDefinition(smtk::attribute::ItemDefinitionPtr c
   {
     cdef->setIsOptional(true);
   }
-  cdef->setUnitsSystem(m_unitsSystem);
+  cdef->setUnitSystem(m_unitSystem);
   return true;
 }
 
@@ -238,12 +238,12 @@ bool GroupItemDefinition::removeItemDefinition(ItemDefinitionPtr itemDef)
   return true;
 }
 
-void GroupItemDefinition::setUnitsSystem(const shared_ptr<units::System>& unitsSystem)
+void GroupItemDefinition::setUnitSystem(const shared_ptr<units::System>& unitSystem)
 {
-  m_unitsSystem = unitsSystem;
+  m_unitSystem = unitSystem;
 
   for (const auto& item : m_itemDefs)
   {
-    item->setUnitsSystem(m_unitsSystem);
+    item->setUnitSystem(m_unitSystem);
   }
 }
diff --git a/smtk/attribute/GroupItemDefinition.h b/smtk/attribute/GroupItemDefinition.h
index 47ca4c1f392016a2011567be73c0343e860bee80..53a0fee95d189795ecc7544bb285d3a875bb3e60 100644
--- a/smtk/attribute/GroupItemDefinition.h
+++ b/smtk/attribute/GroupItemDefinition.h
@@ -65,8 +65,8 @@ public:
       {
         item->setIsOptional(true);
       }
-      // We need to get a pointer to the base Item class to set the unitsSystem
-      static_cast<ItemDefinition*>(item.get())->setUnitsSystem(m_unitsSystem);
+      // We need to get a pointer to the base Item class to set the unitSystem
+      static_cast<ItemDefinition*>(item.get())->setUnitSystem(m_unitSystem);
       m_itemDefs.push_back(item);
       m_itemDefPositions[inName] = static_cast<int>(n);
     }
@@ -159,7 +159,10 @@ protected:
   void applyAdvanceLevels(
     const unsigned int& readLevelFromParent,
     const unsigned int& writeLevelFromParent) override;
-  void setUnitsSystem(const shared_ptr<units::System>& unitsSystem) override;
+  void setUnitSystem(const shared_ptr<units::System>& unitSystem) override;
+  SMTK_DEPRECATED_IN_NEXT("Use setUnitSystem() instead.")
+  void setUnitsSystem(const shared_ptr<units::System>& unitsSystem) override
+  { this->setUnitSystem(unitsSystem); }
   std::vector<smtk::attribute::ItemDefinitionPtr> m_itemDefs;
   std::map<std::string, int> m_itemDefPositions;
   std::vector<std::string> m_labels;
diff --git a/smtk/attribute/ItemDefinition.cxx b/smtk/attribute/ItemDefinition.cxx
index 5923c35eeb641010bdfe8ca53c540c86c67feba5..f7874a611ca61966a1048170772c9855de979d73 100644
--- a/smtk/attribute/ItemDefinition.cxx
+++ b/smtk/attribute/ItemDefinition.cxx
@@ -157,7 +157,7 @@ bool ItemDefinition::removeTag(const std::string& name)
   return false;
 }
 
-void ItemDefinition::setUnitsSystem(const shared_ptr<units::System>& unitsSystem)
+void ItemDefinition::setUnitSystem(const shared_ptr<units::System>& unitSystem)
 {
-  m_unitsSystem = unitsSystem;
+  m_unitSystem = unitSystem;
 }
diff --git a/smtk/attribute/ItemDefinition.h b/smtk/attribute/ItemDefinition.h
index 1ac7f9eb00525a2858d219eb86fea4aab8ae3dbe..9b2a45e31ef99c4766055f6f12a6b0c14eacf468 100644
--- a/smtk/attribute/ItemDefinition.h
+++ b/smtk/attribute/ItemDefinition.h
@@ -23,6 +23,7 @@
 #include "smtk/attribute/Item.h" // For Item Types.
 #include "smtk/attribute/Tag.h"
 #include "smtk/common/Categories.h"
+#include "smtk/common/Deprecation.h"
 
 #include <queue>
 #include <set>
@@ -167,8 +168,10 @@ public:
   bool removeTag(const std::string& name);
   ///@}
 
-  ///\brief Return the unitsSystem of the Definition
-  const shared_ptr<units::System>& unitsSystem() const { return m_unitsSystem; }
+  ///\brief Return the unitSystem of the Definition
+  const shared_ptr<units::System>& unitSystem() const { return m_unitSystem; }
+  SMTK_DEPRECATED_IN_NEXT("Use unitSystem() instead.")
+  const shared_ptr<units::System>& unitsSystem() const { return m_unitSystem; }
 
   virtual smtk::attribute::ItemPtr buildItem(Attribute* owningAttribute, int itemPosition)
     const = 0;
@@ -189,10 +192,13 @@ protected:
     const unsigned int& readLevelFromParent,
     const unsigned int& writeLevelFromParent);
 
-  ///\brief Set the unitsSystem of the Definition
+  ///\brief Set the unitSystem of the Definition
   ///
   /// Note that this should be done before units are specified in the Definition
-  virtual void setUnitsSystem(const shared_ptr<units::System>& unitsSystem);
+  virtual void setUnitSystem(const shared_ptr<units::System>& unitSystem);
+  SMTK_DEPRECATED_IN_NEXT("Use setUnitSystem() instead.")
+  virtual void setUnitsSystem(const shared_ptr<units::System>& unitsSystem)
+  { this->setUnitSystem(unitsSystem); }
 
   int m_version;
   bool m_isOptional;
@@ -207,7 +213,7 @@ protected:
   unsigned int m_advanceLevel[2];
   attribute::Tags m_tags;
   smtk::common::Categories::CombinationMode m_combinationMode;
-  std::shared_ptr<units::System> m_unitsSystem;
+  std::shared_ptr<units::System> m_unitSystem;
 
 private:
   // constant value that should never be changed
diff --git a/smtk/attribute/ReferenceItemDefinition.cxx b/smtk/attribute/ReferenceItemDefinition.cxx
index 75005792726beacf6f6fd53d384f0b1bb52dd47e..7abd002d1b604bd1992ff61791d7f02bb1a04cea 100644
--- a/smtk/attribute/ReferenceItemDefinition.cxx
+++ b/smtk/attribute/ReferenceItemDefinition.cxx
@@ -611,13 +611,13 @@ std::size_t ReferenceItemDefinition::testConditionals(PersistentObjectPtr& objec
   return s_invalidIndex;
 }
 
-void ReferenceItemDefinition::setUnitsSystem(const shared_ptr<units::System>& unitsSystem)
+void ReferenceItemDefinition::setUnitSystem(const shared_ptr<units::System>& unitSystem)
 {
-  m_unitsSystem = unitsSystem;
+  m_unitSystem = unitSystem;
 
   for (const auto& item : m_itemDefs)
   {
-    item.second->setUnitsSystem(m_unitsSystem);
+    item.second->setUnitSystem(m_unitSystem);
   }
 }
 } // namespace attribute
diff --git a/smtk/attribute/ReferenceItemDefinition.h b/smtk/attribute/ReferenceItemDefinition.h
index 1acd7c629650b9e2c785e800bc2651b939af3445..b792cb2a6e493f8ce2505c5118c0c32480de28cd 100644
--- a/smtk/attribute/ReferenceItemDefinition.h
+++ b/smtk/attribute/ReferenceItemDefinition.h
@@ -265,7 +265,10 @@ protected:
     const smtk::common::Categories::Stack& inheritedFromParent,
     smtk::common::Categories& inheritedToParent) override;
 
-  void setUnitsSystem(const shared_ptr<units::System>& unitsSystem) override;
+  void setUnitSystem(const shared_ptr<units::System>& unitSystem) override;
+  SMTK_DEPRECATED_IN_NEXT("Use setUnitSystem() instead.")
+  void setUnitsSystem(const shared_ptr<units::System>& unitsSystem) override
+  { this->setUnitSystem(unitsSystem); }
 
   /// Debug method that returns a description concerning the validity of the component
   std::string componentValidityCheck(const smtk::resource::Component* comp) const;
diff --git a/smtk/attribute/Resource.cxx b/smtk/attribute/Resource.cxx
index afab3d5e37ecb0525265c5fe400b621a8f1deb75..850b2d108c63dc41a46c710084dfcdd35aec5009 100644
--- a/smtk/attribute/Resource.cxx
+++ b/smtk/attribute/Resource.cxx
@@ -54,7 +54,7 @@ Resource::Resource(const smtk::common::UUID& myID, smtk::resource::ManagerPtr ma
   : smtk::resource::DerivedFrom<Resource, smtk::geometry::Resource>(myID, manager)
 {
   queries().registerQueries<QueryList>();
-  m_unitsSystem = units::System::
+  m_unitSystem = units::System::
     createWithDefaults(); // Create a unit system with some prefixes, dimensions, and units.
 
   // Do not display resource views in the attribute panel automatically.
@@ -66,7 +66,7 @@ Resource::Resource(smtk::resource::ManagerPtr manager)
   : smtk::resource::DerivedFrom<Resource, smtk::geometry::Resource>(manager)
 {
   queries().registerQueries<QueryList>();
-  m_unitsSystem = units::System::
+  m_unitSystem = units::System::
     createWithDefaults(); // Create a unit system with some prefixes, dimensions, and units.
 
   // Do not display resource views in the attribute panel automatically.
@@ -84,20 +84,20 @@ Resource::~Resource()
   }
 }
 
-bool Resource::setUnitsSystem(const shared_ptr<units::System>& unitsSystem)
+bool Resource::setUnitSystem(const shared_ptr<units::System>& unitSystem)
 {
-  if (m_unitsSystem == unitsSystem)
+  if (m_unitSystem == unitSystem)
   {
     return true;
   }
   // Since changing units systems can invalidate Item Definitions and Items
   // we only can change systems when there are no definitions or if there was not
   // currently an units system specified.
-  if (m_unitsSystem && !m_definitions.empty())
+  if (m_unitSystem && !m_definitions.empty())
   {
     return false;
   }
-  m_unitsSystem = unitsSystem;
+  m_unitSystem = unitSystem;
   return true;
 }
 
diff --git a/smtk/attribute/Resource.h b/smtk/attribute/Resource.h
index cae76c0d69d325c155099bec06c3307547731f0b..7d2f20d745c3c525f7cd8ce1761725e060291080 100644
--- a/smtk/attribute/Resource.h
+++ b/smtk/attribute/Resource.h
@@ -106,7 +106,10 @@ public:
 
   ~Resource() override;
 
-  bool setUnitsSystem(const shared_ptr<units::System>& unitsSystem) override;
+  bool setUnitSystem(const shared_ptr<units::System>& unitSystem) override;
+  SMTK_DEPRECATED_IN_NEXT("Use setUnitSystem instead.")
+  bool setUnitsSystem(const shared_ptr<units::System>& unitsSystem) override
+  { return this->setUnitSystem(unitsSystem); }
 
   smtk::attribute::DefinitionPtr createDefinition(
     const std::string& typeName,
@@ -443,7 +446,7 @@ public:
   ///
   /// If \a options has copyTemplateData() set to true, then this resource's
   /// Definition instances will be copied to the output resources.
-  /// In addition, unitsSystem and Analysis information is copied.
+  /// In addition, unitSystem and Analysis information is copied.
   std::shared_ptr<smtk::resource::Resource> clone(
     smtk::resource::CopyOptions& options) const override;
 
diff --git a/smtk/attribute/ValueItem.cxx b/smtk/attribute/ValueItem.cxx
index cfcb55df33d29ef94a9ad675fbed2a14170dd05c..d30160239afbf3480862b966a944eb6d9bf039ab 100644
--- a/smtk/attribute/ValueItem.cxx
+++ b/smtk/attribute/ValueItem.cxx
@@ -340,7 +340,7 @@ bool ValueItem::isAcceptable(const smtk::attribute::AttributePtr& exp) const
     return false;
   }
 
-  const auto& unitSys = def->unitsSystem();
+  const auto& unitSys = def->unitSystem();
   // Can't determine if the units are compatible w/o units system
   if (!unitSys)
   {
@@ -976,11 +976,11 @@ std::string ValueItem::supportedUnits() const
 {
   bool status = false;
   auto munits = this->units();
-  auto unitsSystem = m_definition->unitsSystem();
+  auto unitSystem = m_definition->unitSystem();
 
-  if (!(munits.empty() || (unitsSystem == nullptr)))
+  if (!(munits.empty() || (unitSystem == nullptr)))
   {
-    auto myUnit = unitsSystem->unit(munits, &status);
+    auto myUnit = unitSystem->unit(munits, &status);
   }
   return (status ? munits : std::string());
 }
diff --git a/smtk/attribute/ValueItemDefinition.cxx b/smtk/attribute/ValueItemDefinition.cxx
index 5eacf6f6d97fa47feeaae6b60baf80d0964a4166..8d9772eb9e9fb208fd6830d1c8f1f8c12844d98d 100644
--- a/smtk/attribute/ValueItemDefinition.cxx
+++ b/smtk/attribute/ValueItemDefinition.cxx
@@ -521,7 +521,7 @@ bool ValueItemDefinition::addItemDefinition(smtk::attribute::ItemDefinitionPtr c
     return false;
   }
   m_itemDefs[cdef->name()] = cdef;
-  cdef->setUnitsSystem(m_unitsSystem);
+  cdef->setUnitSystem(m_unitSystem);
   return true;
 }
 
@@ -575,13 +575,13 @@ std::vector<std::string> ValueItemDefinition::relevantEnums(
   return result;
 }
 
-void ValueItemDefinition::setUnitsSystem(const shared_ptr<units::System>& unitsSystem)
+void ValueItemDefinition::setUnitSystem(const shared_ptr<units::System>& unitSystem)
 {
-  m_unitsSystem = unitsSystem;
+  m_unitSystem = unitSystem;
 
   for (const auto& item : m_itemDefs)
   {
-    item.second->setUnitsSystem(m_unitsSystem);
+    item.second->setUnitSystem(m_unitSystem);
   }
 }
 
@@ -605,10 +605,10 @@ bool ValueItemDefinition::isDiscreteIndexValid(int index) const
 
 bool ValueItemDefinition::hasSupportedUnits() const
 {
-  if (!(m_units.empty() || (m_unitsSystem == nullptr)))
+  if (!(m_units.empty() || (m_unitSystem == nullptr)))
   {
     bool status;
-    auto defUnit = m_unitsSystem->unit(m_units, &status);
+    auto defUnit = m_unitSystem->unit(m_units, &status);
     return status;
   }
   return false;
@@ -617,9 +617,9 @@ bool ValueItemDefinition::hasSupportedUnits() const
 std::string ValueItemDefinition::supportedUnits() const
 {
   bool status = false;
-  if (!(m_units.empty() || (m_unitsSystem == nullptr)))
+  if (!(m_units.empty() || (m_unitSystem == nullptr)))
   {
-    auto defUnit = m_unitsSystem->unit(m_units, &status);
+    auto defUnit = m_unitSystem->unit(m_units, &status);
   }
   return (status ? m_units : std::string());
 }
diff --git a/smtk/attribute/ValueItemDefinition.h b/smtk/attribute/ValueItemDefinition.h
index 07223e3b2d8352dce32266fb2e364f8d360a29dd..b858bd774f04567da98cfb0037b34a904a1c3a78 100644
--- a/smtk/attribute/ValueItemDefinition.h
+++ b/smtk/attribute/ValueItemDefinition.h
@@ -248,8 +248,8 @@ public:
     }
     item = SharedTypes::RawPointerType::New(idName);
     m_itemDefs[item->name()] = item;
-    // We need to get a pointer to the base Item class to set the unitsSystem
-    static_cast<ItemDefinition*>(item.get())->setUnitsSystem(m_unitsSystem);
+    // We need to get a pointer to the base Item class to set the unitSystem
+    static_cast<ItemDefinition*>(item.get())->setUnitSystem(m_unitSystem);
     return item;
   }
 
@@ -290,7 +290,10 @@ protected:
     const unsigned int& readLevelFromParent,
     const unsigned int& writeLevelFromParent) override;
 
-  void setUnitsSystem(const shared_ptr<units::System>& unitsSystem) override;
+  void setUnitSystem(const shared_ptr<units::System>& unitSystem) override;
+  SMTK_DEPRECATED_IN_NEXT("Use setUnitSystem() instead.")
+  void setUnitsSystem(const shared_ptr<units::System>& unitsSystem) override
+  { this->setUnitSystem(unitsSystem); }
 
   virtual void updateDiscreteValue() = 0;
   bool m_hasDefault;
diff --git a/smtk/attribute/operators/Write.cxx b/smtk/attribute/operators/Write.cxx
index 1e554a7960fae0e981ab79584634275cc5a179e2..56b560df06dc8c1056f91ead25f768b53b242bde 100644
--- a/smtk/attribute/operators/Write.cxx
+++ b/smtk/attribute/operators/Write.cxx
@@ -52,7 +52,10 @@ Write::Result Write::operateInternal()
   }
 
   {
-    std::ofstream file(resource->location());
+    // Ensure parent directory exists. It may not if we are part of a project
+    // that has organized us into an unrealized subdirectory.
+    auto location = resource->absoluteLocation(/*create directories*/true);
+    std::ofstream file(location);
     if (!file.good())
     {
       smtkErrorMacro(log(), "Unable to open \"" << resource->location() << "\" for writing.");
@@ -82,6 +85,17 @@ void Write::markModifiedResources(Write::Result& /*unused*/)
   }
 }
 
+void Write::generateSummary(Result& res)
+{
+  if (smtk::operation::outcome(res) != Outcome::SUCCEEDED)
+  {
+    this->Superclass::generateSummary(res);
+  }
+  auto resourceItem = this->parameters()->associations();
+  auto resource = std::dynamic_pointer_cast<smtk::resource::Resource>(resourceItem->value());
+  smtkInfoMacro(this->log(), "Wrote \"" << resource->location() << "\".");
+}
+
 bool write(
   const smtk::resource::ResourcePtr& resource,
   const std::shared_ptr<smtk::common::Managers>& managers)
diff --git a/smtk/attribute/operators/Write.h b/smtk/attribute/operators/Write.h
index 30ccd4b85077477f2a45e4e5c7a6b80dd0b66cfa..1925dee44c56c6ae6958e11983f6589978259503 100644
--- a/smtk/attribute/operators/Write.h
+++ b/smtk/attribute/operators/Write.h
@@ -31,6 +31,7 @@ protected:
   Result operateInternal() override;
   const char* xmlDescription() const override;
   void markModifiedResources(Result&) override;
+  void generateSummary(Result&) override;
 };
 
 SMTKCORE_EXPORT bool write(
diff --git a/smtk/attribute/pybind11/PybindAssociate.h b/smtk/attribute/pybind11/PybindAssociate.h
index d87c54fc7fa0aecd387e5029a3effd7c6d30e2bf..5ce9cfa50cb5199bc36c15f710b44da428c9d9f6 100644
--- a/smtk/attribute/pybind11/PybindAssociate.h
+++ b/smtk/attribute/pybind11/PybindAssociate.h
@@ -24,8 +24,6 @@ inline PySharedPtrClass< smtk::attribute::Associate, smtk::operation::XMLOperati
   PySharedPtrClass< smtk::attribute::Associate, smtk::operation::XMLOperation > instance(m, "Associate");
   instance
     .def(py::init<>())
-    .def(py::init<::smtk::attribute::Associate const &>())
-    .def("deepcopy", (smtk::attribute::Associate & (smtk::attribute::Associate::*)(::smtk::attribute::Associate const &)) &smtk::attribute::Associate::operator=)
     .def_static("create", (std::shared_ptr<smtk::attribute::Associate> (*)()) &smtk::attribute::Associate::create)
     .def_static("create", (std::shared_ptr<smtk::attribute::Associate> (*)(::std::shared_ptr<smtk::attribute::Associate> &)) &smtk::attribute::Associate::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<const smtk::attribute::Associate> (smtk::attribute::Associate::*)() const) &smtk::attribute::Associate::shared_from_this)
diff --git a/smtk/attribute/pybind11/PybindDissociate.h b/smtk/attribute/pybind11/PybindDissociate.h
index a9d928807c96091bb6eb18d93a8d78caf50e4fd8..d587cc83ea71ab9a41211e8e0ec2cbc7d48d3b17 100644
--- a/smtk/attribute/pybind11/PybindDissociate.h
+++ b/smtk/attribute/pybind11/PybindDissociate.h
@@ -24,8 +24,6 @@ inline PySharedPtrClass< smtk::attribute::Dissociate, smtk::operation::XMLOperat
   PySharedPtrClass< smtk::attribute::Dissociate, smtk::operation::XMLOperation > instance(m, "Dissociate");
   instance
     .def(py::init<>())
-    .def(py::init<::smtk::attribute::Dissociate const &>())
-    .def("deepcopy", (smtk::attribute::Dissociate & (smtk::attribute::Dissociate::*)(::smtk::attribute::Dissociate const &)) &smtk::attribute::Dissociate::operator=)
     .def_static("create", (std::shared_ptr<smtk::attribute::Dissociate> (*)()) &smtk::attribute::Dissociate::create)
     .def_static("create", (std::shared_ptr<smtk::attribute::Dissociate> (*)(::std::shared_ptr<smtk::attribute::Dissociate> &)) &smtk::attribute::Dissociate::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<const smtk::attribute::Dissociate> (smtk::attribute::Dissociate::*)() const) &smtk::attribute::Dissociate::shared_from_this)
diff --git a/smtk/attribute/pybind11/PybindExport.h b/smtk/attribute/pybind11/PybindExport.h
index a20bb348f9c9f434aef9320abd5a5e3651123286..49431b4de754232737f4929aa66549747677668d 100644
--- a/smtk/attribute/pybind11/PybindExport.h
+++ b/smtk/attribute/pybind11/PybindExport.h
@@ -24,8 +24,6 @@ inline PySharedPtrClass< smtk::attribute::Export, smtk::operation::XMLOperation
   PySharedPtrClass< smtk::attribute::Export, smtk::operation::XMLOperation > instance(m, "Export");
   instance
     .def(py::init<>())
-    .def(py::init<::smtk::attribute::Export const &>())
-    .def("deepcopy", (smtk::attribute::Export & (smtk::attribute::Export::*)(::smtk::attribute::Export const &)) &smtk::attribute::Export::operator=)
     .def_static("create", (std::shared_ptr<smtk::attribute::Export> (*)()) &smtk::attribute::Export::create)
     .def_static("create", (std::shared_ptr<smtk::attribute::Export> (*)(::std::shared_ptr<smtk::attribute::Export> &)) &smtk::attribute::Export::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<const smtk::attribute::Export> (smtk::attribute::Export::*)() const) &smtk::attribute::Export::shared_from_this)
diff --git a/smtk/attribute/pybind11/PybindImport.h b/smtk/attribute/pybind11/PybindImport.h
index 2c6d16ad18d71b202e08791226ea80fa1e96d82c..9b4d38eb7fb48eb66d67cc4bc3e228959586568e 100644
--- a/smtk/attribute/pybind11/PybindImport.h
+++ b/smtk/attribute/pybind11/PybindImport.h
@@ -24,8 +24,6 @@ inline PySharedPtrClass< smtk::attribute::Import, smtk::operation::XMLOperation
   PySharedPtrClass< smtk::attribute::Import, smtk::operation::XMLOperation > instance(m, "Import");
   instance
     .def(py::init<>())
-    .def(py::init<::smtk::attribute::Import const &>())
-    .def("deepcopy", (smtk::attribute::Import & (smtk::attribute::Import::*)(::smtk::attribute::Import const &)) &smtk::attribute::Import::operator=)
     .def_static("create", (std::shared_ptr<smtk::attribute::Import> (*)()) &smtk::attribute::Import::create)
     .def_static("create", (std::shared_ptr<smtk::attribute::Import> (*)(::std::shared_ptr<smtk::attribute::Import> &)) &smtk::attribute::Import::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<const smtk::attribute::Import> (smtk::attribute::Import::*)() const) &smtk::attribute::Import::shared_from_this)
diff --git a/smtk/attribute/pybind11/PybindRead.h b/smtk/attribute/pybind11/PybindRead.h
index e72a26a73a777edc0bc6eb6e43ad328e8149d39e..45c1b3ba30f4b16c5dba1bbb1d8033b1e9784558 100644
--- a/smtk/attribute/pybind11/PybindRead.h
+++ b/smtk/attribute/pybind11/PybindRead.h
@@ -25,8 +25,6 @@ inline PySharedPtrClass< smtk::attribute::Read, smtk::operation::XMLOperation >
   PySharedPtrClass< smtk::attribute::Read, smtk::operation::XMLOperation > instance(m, "Read");
   instance
     .def(py::init<>())
-    .def(py::init<::smtk::attribute::Read const &>())
-    .def("deepcopy", (smtk::attribute::Read & (smtk::attribute::Read::*)(::smtk::attribute::Read const &)) &smtk::attribute::Read::operator=)
     .def_static("create", (std::shared_ptr<smtk::attribute::Read> (*)()) &smtk::attribute::Read::create)
     .def_static("create", (std::shared_ptr<smtk::attribute::Read> (*)(::std::shared_ptr<smtk::attribute::Read> &)) &smtk::attribute::Read::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<const smtk::attribute::Read> (smtk::attribute::Read::*)() const) &smtk::attribute::Read::shared_from_this)
diff --git a/smtk/attribute/pybind11/PybindReferenceItem.h b/smtk/attribute/pybind11/PybindReferenceItem.h
index 9d04c5fba079ab97d134772b869ce6d47c2610e6..ae4adfc792a5cf28cec1c32bc5ceb51fb1b169e8 100644
--- a/smtk/attribute/pybind11/PybindReferenceItem.h
+++ b/smtk/attribute/pybind11/PybindReferenceItem.h
@@ -57,6 +57,21 @@ inline PySharedPtrClass< smtk::attribute::ReferenceItem, smtk::attribute::Item >
     .def("unset", &smtk::attribute::ReferenceItem::unset, py::arg("i") = 0)
     .def("valueAsString", (std::string (smtk::attribute::ReferenceItem::*)() const) &smtk::attribute::ReferenceItem::valueAsString)
     .def("valueAsString", (std::string (smtk::attribute::ReferenceItem::*)(::size_t) const) &smtk::attribute::ReferenceItem::valueAsString, py::arg("i"))
+    .def("setValues", [&](smtk::attribute::ReferenceItem* self, const std::vector<smtk::resource::PersistentObject::Ptr>& values)
+      {
+        return self->setValues(values.begin(), values.end());
+      }, py::arg("values"))
+    .def("values", [&](smtk::attribute::ReferenceItem* self) -> std::vector<smtk::resource::PersistentObject::Ptr>
+      {
+        std::vector<smtk::resource::PersistentObject::Ptr> values;
+        std::size_t nn = self->numberOfValues();
+        values.reserve(nn);
+        for (std::size_t ii = 0; ii < nn; ++ii)
+        {
+          values.push_back(self->isSet(ii) ? self->value(ii) : smtk::resource::PersistentObject::Ptr());
+        }
+        return values;
+      })
     ;
   return instance;
 }
diff --git a/smtk/attribute/pybind11/PybindWrite.h b/smtk/attribute/pybind11/PybindWrite.h
index b7a3b083475770cb482cf28116142b9c0edc4282..22cd6bd27aaaeb96545fdb7d87654b2d24bd887f 100644
--- a/smtk/attribute/pybind11/PybindWrite.h
+++ b/smtk/attribute/pybind11/PybindWrite.h
@@ -24,8 +24,6 @@ inline PySharedPtrClass< smtk::attribute::Write, smtk::operation::XMLOperation >
   PySharedPtrClass< smtk::attribute::Write, smtk::operation::XMLOperation > instance(m, "Write");
   instance
     .def(py::init<>())
-    .def(py::init<::smtk::attribute::Write const &>())
-    .def("deepcopy", (smtk::attribute::Write & (smtk::attribute::Write::*)(::smtk::attribute::Write const &)) &smtk::attribute::Write::operator=)
     .def_static("create", (std::shared_ptr<smtk::attribute::Write> (*)()) &smtk::attribute::Write::create)
     .def_static("create", (std::shared_ptr<smtk::attribute::Write> (*)(::std::shared_ptr<smtk::attribute::Write> &)) &smtk::attribute::Write::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<const smtk::attribute::Write> (smtk::attribute::Write::*)() const) &smtk::attribute::Write::shared_from_this)
diff --git a/smtk/common/CMakeLists.txt b/smtk/common/CMakeLists.txt
index 2c5489637c96efcf2e43762ee3f8ac2727255aaf..132b5029b941f38a0fbd3a62fa288f8ffc004d70 100644
--- a/smtk/common/CMakeLists.txt
+++ b/smtk/common/CMakeLists.txt
@@ -17,6 +17,7 @@ set(commonSrcs
   TimeZone.cxx
   timezonespec.cxx
   TypeContainer.cxx
+  URL.cxx
   UUID.cxx
   UUIDGenerator.cxx
   VersionNumber.cxx
@@ -63,6 +64,7 @@ set(commonHeaders
   TypeName.h
   TypeContainer.h
   TypeTraits.h
+  URL.h
   UUID.h
   UUIDGenerator.h
   VersionNumber.h
diff --git a/smtk/common/Paths.cxx b/smtk/common/Paths.cxx
index f3eeae0c17cd93a0da531ebd6debb186a01f6e7d..a33386c789d2d4610e8fe2c5c2e74d67428aa1ff 100644
--- a/smtk/common/Paths.cxx
+++ b/smtk/common/Paths.cxx
@@ -265,6 +265,11 @@ std::string Paths::uniquePath()
   return boost::filesystem::unique_path().string();
 }
 
+std::string Paths::uniquePath(const std::string& modelPath)
+{
+  return boost::filesystem::unique_path(modelPath).string();
+}
+
 /**\brief Return the best guess at the directory containing the current process's executable.
   */
 std::string Paths::executableDirectory()
diff --git a/smtk/common/Paths.h b/smtk/common/Paths.h
index 2b6a77d688a3aeead20ecba08553b276e4f787d2..7a4c21f156ff8b5bb80ce88dabfbd4b96de956b7 100644
--- a/smtk/common/Paths.h
+++ b/smtk/common/Paths.h
@@ -64,6 +64,8 @@ public:
   static std::string replaceFilename(const std::string& path, const std::string& newFilename);
   static std::string tempDirectory();
   static std::string uniquePath();
+  /// The \a modelPath must have percent (%) signs that will be replaced with random characters.
+  static std::string uniquePath(const std::string& modelPath);
 
   std::string executableDirectory();
   std::string toplevelDirectory();
diff --git a/smtk/common/URL.cxx b/smtk/common/URL.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..5841685132d2f35e9fb1b7471fa315a147d4c839
--- /dev/null
+++ b/smtk/common/URL.cxx
@@ -0,0 +1,172 @@
+//=========================================================================
+//  Copyright (c) Kitware, Inc.
+//  All rights reserved.
+//  See LICENSE.txt for details.
+//
+//  This software is distributed WITHOUT ANY WARRANTY; without even
+//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+//  PURPOSE.  See the above copyright notice for more information.
+//=========================================================================
+#include "smtk/common/URL.h"
+
+#include "tao/pegtl.hpp"
+#include "tao/pegtl/contrib/uri.hpp"
+
+namespace pegtl = tao::pegtl;
+
+namespace smtk
+{
+namespace common
+{
+namespace uri
+{
+
+template< bool (smtk::common::URL::*Field)(smtk::string::Token) >
+struct bind
+{
+  template< typename Input >
+  static void apply( const Input& in, smtk::common::URL& url )
+  {
+    (url.*Field)(smtk::string::Token(in.string()));
+  }
+};
+
+// clang-format off
+template< typename Rule > struct action {};
+
+template<> struct action< pegtl::uri::scheme > : bind< &URL::setScheme > {};
+template<> struct action< pegtl::uri::authority > : bind< &URL::setAuthority > {};
+// template<> struct action< pegtl::uri::host > : bind< &URL::setHost > {};
+// template<> struct action< pegtl::uri::port > : bind< &URL::setPort > {};
+template<> struct action< pegtl::uri::path_noscheme > : bind< &URL::setPath > {};
+template<> struct action< pegtl::uri::path_rootless > : bind< &URL::setPath > {};
+template<> struct action< pegtl::uri::path_absolute > : bind< &URL::setPath > {};
+template<> struct action< pegtl::uri::path_abempty > : bind< &URL::setPath > {};
+template<> struct action< pegtl::uri::query > : bind< &URL::setQuery > {};
+template<> struct action< pegtl::uri::fragment > : bind< &URL::setFragment > {};
+// clang-format on
+
+} // anonymous namespace
+
+URL::URL(const std::string& txt)
+{
+  using grammar = pegtl::must< pegtl::uri::URI >;
+  pegtl::memory_input<> input( txt, "smtk::common::URL" );
+  pegtl::parse< grammar, uri::action >( input, *this );
+}
+
+URL::URL(Token scheme, Token authority, Token path, Token query, Token fragment)
+  : m_scheme(scheme)
+  , m_authority(authority)
+  , m_path(path)
+  , m_query(query)
+  , m_fragment(fragment)
+{
+}
+
+bool URL::valid() const
+{
+  // The URL "/" is not valid. If you want the root of the local filesystem, use "file:///".
+  return m_path.valid() || m_scheme.valid();
+}
+
+bool URL::setScheme(Token scheme)
+{
+  if (scheme == m_scheme) { return false; }
+  m_scheme = scheme;
+  return true;
+}
+
+bool URL::setAuthority(Token authority)
+{
+  if (authority == m_authority) { return false; }
+  m_authority = authority;
+  return true;
+}
+
+bool URL::setPath(Token path)
+{
+  if (path == m_path) { return false; }
+  m_path = path;
+  return true;
+}
+
+bool URL::setQuery(Token query)
+{
+  if (query == m_query) { return false; }
+  m_query = query;
+  return true;
+}
+
+bool URL::setFragment(Token fragment)
+{
+  if (fragment == m_fragment) { return false; }
+  m_fragment = fragment;
+  return true;
+}
+
+bool URL::operator!=(URL const& other) const
+{
+  return m_scheme != other.m_scheme ||
+    m_authority != other.m_authority ||
+    m_path != other.m_path ||
+    m_query != other.m_query ||
+    m_fragment != other.m_fragment;
+}
+
+bool URL::operator==(URL const& other) const
+{
+  return m_scheme == other.m_scheme &&
+    m_authority == other.m_authority &&
+    m_path == other.m_path &&
+    m_query == other.m_query &&
+    m_fragment == other.m_fragment;
+}
+
+bool URL::operator<(URL const& other) const
+{
+  return
+    m_scheme < other.m_scheme || (
+      m_scheme == other.m_scheme && (
+        m_authority < other.m_authority || (
+          m_authority == other.m_authority && (
+            m_path < other.m_path || (
+              m_path == other.m_path && (
+                m_query < other.m_query || (
+                  m_query == other.m_query &&
+                    m_fragment < other.m_fragment
+                )
+              )
+            )
+          )
+        )
+      )
+    );
+}
+
+URL::operator bool() const
+{
+  return this->valid();
+}
+
+URL::operator std::string() const
+{
+  std::string result;
+  if (m_scheme.valid()) { result += m_scheme.data() + ":"; }
+  if (m_authority.valid()) { result += "//" + m_authority.data(); }
+  // result += "/";
+  if (m_path.valid()) { result += m_path.data(); }
+  if (m_query.valid()) { result += "?" + m_query.data(); }
+  if (m_fragment.valid()) { result += "#" + m_fragment.data(); }
+  return result;
+}
+
+std::ostream& operator<<(std::ostream& stream, const URL& url)
+{
+  std::string data = (std::string)url;
+  stream << data;
+  return stream;
+}
+
+} // namespace common
+} // namespace smtk
diff --git a/smtk/common/URL.h b/smtk/common/URL.h
new file mode 100644
index 0000000000000000000000000000000000000000..5151eaf2cc9ddafbce0fbebacffbc638d14eddf0
--- /dev/null
+++ b/smtk/common/URL.h
@@ -0,0 +1,74 @@
+//=========================================================================
+//  Copyright (c) Kitware, Inc.
+//  All rights reserved.
+//  See LICENSE.txt for details.
+//
+//  This software is distributed WITHOUT ANY WARRANTY; without even
+//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+//  PURPOSE.  See the above copyright notice for more information.
+//=========================================================================
+#ifndef smtk_common_URL_h
+#define smtk_common_URL_h
+
+#include "smtk/string/Token.h"
+
+namespace smtk
+{
+namespace common
+{
+
+/** An RFC3986-compliant Uniform Resource Locator (URL).
+  *
+  * This class holds URL data as a set of string tokens.
+  * If given a single string, that string is parsed and
+  * decomposed into tokens for querying data.
+  */
+class SMTKCORE_EXPORT URL
+{
+public:
+  using Token = smtk::string::Token;
+
+  URL() = default;
+  URL(const URL& other) = default;
+  URL(const std::string& txt);
+  URL(Token scheme, Token authority, Token path, Token query = Token(), Token fragment = Token());
+
+  bool valid() const;
+  bool operator!=(URL const& other) const;
+  bool operator==(URL const& other) const;
+  bool operator<(URL const& other) const;
+
+  URL& operator=(URL const& other) = default;
+
+  operator bool() const;
+  explicit operator std::string() const;
+
+  Token scheme() const { return m_scheme; }
+  bool setScheme(Token scheme);
+
+  Token authority() const { return m_authority; }
+  bool setAuthority(Token authority);
+
+  Token path() const { return m_path; }
+  bool setPath(Token path);
+
+  Token query() const { return m_query; }
+  bool setQuery(Token query);
+
+  Token fragment() const { return m_fragment; }
+  bool setFragment(Token fragment);
+
+protected:
+  Token m_scheme;
+  Token m_authority;
+  Token m_path;
+  Token m_query;
+  Token m_fragment;
+};
+
+SMTKCORE_EXPORT std::ostream& operator<<(std::ostream& stream, const URL& url);
+
+} // namespace common
+} // namespace smtk
+
+#endif // smtk_common_URL_h
diff --git a/smtk/common/pybind11/PybindCommon.cxx b/smtk/common/pybind11/PybindCommon.cxx
index a26b1e806da68d7fff48612dfdd5d6a9cfbbaca7..8f8ffb9150814d96243d98ef495b0dd1b28a90d7 100644
--- a/smtk/common/pybind11/PybindCommon.cxx
+++ b/smtk/common/pybind11/PybindCommon.cxx
@@ -42,6 +42,7 @@ using PySharedPtrClass = py::class_<T, std::shared_ptr<T>, Args...>;
 #include "PybindRangeDetector.h"
 #include "PybindStringUtil.h"
 #include "PybindTimeZone.h"
+#include "PybindURL.h"
 #include "PybindUUID.h"
 #include "PybindUUIDGenerator.h"
 #include "PybindUnionFind.h"
@@ -73,6 +74,7 @@ PYBIND11_MODULE(_smtkPybindCommon, common)
 #endif
   py::class_< smtk::common::StringUtil > smtk_common_StringUtil = pybind11_init_smtk_common_StringUtil(common);
   py::class_< smtk::common::TimeZone > smtk_common_TimeZone = pybind11_init_smtk_common_TimeZone(common);
+  py::class_< smtk::common::URL > smtk_common_URL = pybind11_init_smtk_common_URL(common);
   py::class_< smtk::common::UUID > smtk_common_UUID = pybind11_init_smtk_common_UUID(common);
   py::class_< smtk::common::UUIDGenerator > smtk_common_UUIDGenerator = pybind11_init_smtk_common_UUIDGenerator(common);
   py::class_< smtk::common::Version > smtk_common_Version = pybind11_init_smtk_common_Version(common);
diff --git a/smtk/common/pybind11/PybindURL.h b/smtk/common/pybind11/PybindURL.h
new file mode 100644
index 0000000000000000000000000000000000000000..48e93c88ed24fe337fa8f350cab8e69454686cac
--- /dev/null
+++ b/smtk/common/pybind11/PybindURL.h
@@ -0,0 +1,58 @@
+//=========================================================================
+//  Copyright (c) Kitware, Inc.
+//  All rights reserved.
+//  See LICENSE.txt for details.
+//
+//  This software is distributed WITHOUT ANY WARRANTY; without even
+//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+//  PURPOSE.  See the above copyright notice for more information.
+//=========================================================================
+
+#ifndef pybind_smtk_common_URL_h
+#define pybind_smtk_common_URL_h
+
+#include <pybind11/pybind11.h>
+
+#include "smtk/common/URL.h"
+
+namespace py = pybind11;
+
+inline py::class_< smtk::common::URL > pybind11_init_smtk_common_URL(py::module &m)
+{
+  py::class_< smtk::common::URL > instance(m, "URL");
+  instance
+    .def(py::init<>())
+    .def(py::init<::smtk::common::URL const &>())
+    .def(py::init<::std::string const &>())
+    .def("__eq__", [](smtk::common::URL* url, smtk::common::URL* other)
+      { return *url == *other; }, py::arg("other"))
+    .def("__lt__", [](smtk::common::URL* url, smtk::common::URL* other)
+      { return *url < *other; }, py::arg("other"))
+    .def("__hash__", [](smtk::common::URL* url) { return smtk::string::Token((std::string)*url).id(); })
+    .def("__str__", [](smtk::common::URL* url) { return (std::string)*url; })
+    .def("__repr__", [](smtk::common::URL* url) { return "URL('" + (std::string)*url + "')"; })
+    .def("deepcopy",
+      (smtk::common::URL & (smtk::common::URL::*)(::smtk::common::URL const &))
+      &smtk::common::URL::operator=)
+    .def("valid", &smtk::common::URL::valid)
+    .def("to_string", [](const smtk::common::URL& url)
+      {
+        auto result = (std::string)url;
+        return result;
+      }
+    )
+    .def("scheme", (smtk::string::Token (smtk::common::URL::*)() const)&smtk::common::URL::scheme)
+    .def("authority", (smtk::string::Token (smtk::common::URL::*)() const)&smtk::common::URL::authority)
+    .def("path", (smtk::string::Token (smtk::common::URL::*)() const)&smtk::common::URL::path)
+    .def("query", (smtk::string::Token (smtk::common::URL::*)() const)&smtk::common::URL::query)
+    .def("fragment", (smtk::string::Token (smtk::common::URL::*)() const)&smtk::common::URL::fragment)
+    .def("setScheme", [](smtk::common::URL* url, const std::string& txt) { return url->setScheme(txt); }, py::arg("scheme"))
+    .def("setAuthority", [](smtk::common::URL* url, const std::string& txt) { return url->setAuthority(txt); }, py::arg("authority"))
+    .def("setPath", [](smtk::common::URL* url, const std::string& txt) { return url->setPath(txt); }, py::arg("path"))
+    .def("setQuery", [](smtk::common::URL* url, const std::string& txt) { return url->setQuery(txt); }, py::arg("query"))
+    .def("setFragment", [](smtk::common::URL* url, const std::string& txt) { return url->setFragment(txt); }, py::arg("fragment"))
+    ;
+  return instance;
+}
+
+#endif
diff --git a/smtk/common/pybind11/PybindUUID.h b/smtk/common/pybind11/PybindUUID.h
index a3c0c968b71f2d7696e5aaf4ff01fc5483ec67e3..36bff5a284248746d40104070d74b4cc15cfb8ff 100644
--- a/smtk/common/pybind11/PybindUUID.h
+++ b/smtk/common/pybind11/PybindUUID.h
@@ -31,6 +31,8 @@ inline py::class_< smtk::common::UUID > pybind11_init_smtk_common_UUID(py::modul
     .def("__ne__", (bool (smtk::common::UUID::*)(::smtk::common::UUID const &) const) &smtk::common::UUID::operator!=)
     .def("__eq__", (bool (smtk::common::UUID::*)(::smtk::common::UUID const &) const) &smtk::common::UUID::operator==)
     .def("__lt__", (bool (smtk::common::UUID::*)(::smtk::common::UUID const &) const) &smtk::common::UUID::operator<)
+    .def("__hash__", [](const smtk::common::UUID& uid) { return uid.hash(); })
+    .def("__repr__", [](const smtk::common::UUID& uid) { return "UUID('" + uid.toString() + "')"; })
     .def("deepcopy", (smtk::common::UUID & (smtk::common::UUID::*)(::smtk::common::UUID const &)) &smtk::common::UUID::operator=)
     .def_static("random", &smtk::common::UUID::random)
     .def_static("null", &smtk::common::UUID::null)
diff --git a/smtk/common/testing/python/CMakeLists.txt b/smtk/common/testing/python/CMakeLists.txt
index 65a8ab094dccb0b4c2caf2c0ce7c85f2febfd10a..8b08400cd0de7f19001e2d10f933389590c17398 100644
--- a/smtk/common/testing/python/CMakeLists.txt
+++ b/smtk/common/testing/python/CMakeLists.txt
@@ -1,6 +1,7 @@
 set(smtkCommonPythonTests
   uuidGenerator
   datetimezonepairtest
+  testURL
 )
 
 if (ParaView_VERSION VERSION_GREATER_EQUAL "5.10.0")
diff --git a/smtk/common/testing/python/testURL.py b/smtk/common/testing/python/testURL.py
new file mode 100644
index 0000000000000000000000000000000000000000..fef0e47f20adb76e409bc680e1f54225a7e2709f
--- /dev/null
+++ b/smtk/common/testing/python/testURL.py
@@ -0,0 +1,55 @@
+# =============================================================================
+#
+#  Copyright (c) Kitware, Inc.
+#  All rights reserved.
+#  See LICENSE.txt for details.
+#
+#  This software is distributed WITHOUT ANY WARRANTY; without even
+#  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+#  PURPOSE.  See the above copyright notice for more information.
+#
+# =============================================================================
+import os
+import sys
+import unittest
+import smtk
+import smtk.string
+import smtk.common
+from smtk import common
+import smtk.testing
+import uuid
+
+
+class TestURL(unittest.TestCase):
+
+    def test(self):
+        blank = smtk.common.URL()
+        self.assertFalse(blank.valid(), 'Default-constructed URL should be invalid.')
+        urls = ('https://kitware.com/foo/bar', \
+                'file:///foo/bar', \
+                'file://baz/foo/bar', \
+                'http://xyzzy@baz/foo/bar?bleb',
+                'http://xyzzy@baz/foo/bar?bleb#flim')
+        allParsed = []
+        for txt in urls:
+            parsed = smtk.common.URL(txt)
+            print('"', parsed.scheme().data(), \
+                  '", "', parsed.authority().data(), \
+                  '", "', parsed.path().data(), \
+                  '", "', parsed.query().data(), \
+                  '", "', parsed.fragment().data(), \
+                  '"')
+            print(parsed.to_string(), txt == parsed.to_string())
+            self.assertEqual(parsed.to_string(), txt, 'Round-tripped URL was not identical.')
+            allParsed.append(parsed)
+        self.assertEqual(len(allParsed), len(urls), 'Expected to parse every URL.')
+        self.assertEqual(allParsed, [smtk.common.URL(x) for x in urls], \
+            'Same test in should produce identical URLs out.')
+        s1 = set(allParsed)
+        s2 = set([smtk.common.URL(x) for x in urls])
+        print(s1)
+        self.assertEqual(s1, s2, 'Hashing should produce identical sets of URLs.')
+
+if __name__ == '__main__':
+    smtk.testing.process_arguments()
+    unittest.main()
diff --git a/smtk/extension/paraview/appcomponents/pqSMTKAttributePanel.cxx b/smtk/extension/paraview/appcomponents/pqSMTKAttributePanel.cxx
index ad3460849f7a76cbe892d03948e18ec7160be234..3385fb853e4b7642e36b1a110904721fecdce7c0 100644
--- a/smtk/extension/paraview/appcomponents/pqSMTKAttributePanel.cxx
+++ b/smtk/extension/paraview/appcomponents/pqSMTKAttributePanel.cxx
@@ -503,7 +503,8 @@ bool pqSMTKAttributePanel::displayResourceInternal(
   {
     // Only use the top level view if the resource is not
     // managed by a project
-    if (rsrc && (rsrc->parentResource() == nullptr))
+    auto* project = dynamic_cast<smtk::project::Project*>(rsrc->parentResource());
+    if (rsrc && (!project || (project && project->taskManager().taskInstances().topLevelTasks().empty())))
     {
       theView = rsrc->findTopLevelView();
     }
diff --git a/smtk/extension/paraview/appcomponents/pqSMTKDiagramPanel.cxx b/smtk/extension/paraview/appcomponents/pqSMTKDiagramPanel.cxx
index 12d042d3ae263ad2aba1ca0fee02de2cc6e79d94..857f359ed1720ed831abad8eb73632fb84a9f4e3 100644
--- a/smtk/extension/paraview/appcomponents/pqSMTKDiagramPanel.cxx
+++ b/smtk/extension/paraview/appcomponents/pqSMTKDiagramPanel.cxx
@@ -211,3 +211,21 @@ bool pqSMTKDiagramPanel::configure(const nlohmann::json& data)
   }
   return false;
 }
+
+void pqSMTKDiagramPanel::focusPanel()
+{
+  // If we are owned by a dock widget, ensure the dock is shown.
+  if (auto* dock = qobject_cast<QDockWidget*>(this->parent()))
+  {
+    auto* action = dock->toggleViewAction();
+    if (!action->isChecked())
+    {
+      action->trigger();
+    }
+  }
+  // Raise our parent widget.
+  if (auto* parent = qobject_cast<QWidget*>(this->parent()))
+  {
+    parent->raise();
+  }
+}
diff --git a/smtk/extension/paraview/appcomponents/pqSMTKDiagramPanel.h b/smtk/extension/paraview/appcomponents/pqSMTKDiagramPanel.h
index 3f3f9115cdc6d46965e9bb6135f70cb06a9e6157..6c3e0759a622c0a4fa2db7e2679205d0737cf31f 100644
--- a/smtk/extension/paraview/appcomponents/pqSMTKDiagramPanel.h
+++ b/smtk/extension/paraview/appcomponents/pqSMTKDiagramPanel.h
@@ -63,6 +63,10 @@ public:
 Q_SIGNALS:
   void titleChanged(QString title);
 
+public Q_SLOTS:
+  /// Bring this panel into focus (showing and raising it as required).
+  virtual void focusPanel();
+
 protected Q_SLOTS:
   virtual void resourceManagerAdded(pqSMTKWrapper* mgr, pqServer* server);
   virtual void resourceManagerRemoved(pqSMTKWrapper* mgr, pqServer* server);
diff --git a/smtk/extension/paraview/project/CMakeLists.txt b/smtk/extension/paraview/project/CMakeLists.txt
index 3da2593762f0dfe017173987181da5be103921dc..bf00a7d8d58233f071715574fff38f2a656e751d 100644
--- a/smtk/extension/paraview/project/CMakeLists.txt
+++ b/smtk/extension/paraview/project/CMakeLists.txt
@@ -4,7 +4,9 @@ set(classes
   pqSMTKProjectMenu
   pqSMTKProjectPanel
   pqSMTKTaskResourceVisibility
+  pqTaskControlView
   Registrar
+  Utility
 )
 
 smtk_encode_file("${CMAKE_CURRENT_SOURCE_DIR}/ProjectPanelConfiguration.json"
diff --git a/smtk/extension/paraview/project/Registrar.cxx b/smtk/extension/paraview/project/Registrar.cxx
index f8e72dfca414f5f1fc5030d1f4e9f3953e900605..edb97a507f0de10279dc2b5e5f7a82dbac75ada2 100644
--- a/smtk/extension/paraview/project/Registrar.cxx
+++ b/smtk/extension/paraview/project/Registrar.cxx
@@ -12,6 +12,7 @@
 #include "smtk/extension/paraview/project/Registrar.h"
 
 #include "smtk/extension/paraview/project/pqSMTKProjectBrowser.h"
+#include "smtk/extension/paraview/project/pqTaskControlView.h"
 
 namespace smtk
 {
@@ -21,6 +22,12 @@ namespace paraview
 {
 namespace project
 {
+
+using ViewWidgetList = std::tuple<
+  pqSMTKProjectBrowser,
+  pqTaskControlView
+>;
+
 void Registrar::registerTo(const smtk::project::Manager::Ptr& projectManager)
 {
   projectManager->registerProject("basic");
@@ -34,13 +41,14 @@ void Registrar::unregisterFrom(const smtk::project::Manager::Ptr& projectManager
 void Registrar::registerTo(const smtk::view::Manager::Ptr& viewManager)
 {
   (void)viewManager;
-  viewManager->viewWidgetFactory().registerType<pqSMTKProjectBrowser>();
+  viewManager->viewWidgetFactory().registerTypes<ViewWidgetList>();
+  viewManager->viewWidgetFactory().addAlias<pqTaskControlView>("TaskControl");
 }
 
 void Registrar::unregisterFrom(const smtk::view::Manager::Ptr& viewManager)
 {
   (void)viewManager;
-  viewManager->viewWidgetFactory().unregisterType<pqSMTKProjectBrowser>();
+  viewManager->viewWidgetFactory().unregisterTypes<ViewWidgetList>();
 }
 } // namespace project
 } // namespace paraview
diff --git a/smtk/extension/paraview/project/Utility.cxx b/smtk/extension/paraview/project/Utility.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..a06c2fb12103592dc64940a0a09653d1d7f3115b
--- /dev/null
+++ b/smtk/extension/paraview/project/Utility.cxx
@@ -0,0 +1,66 @@
+//=========================================================================
+//  Copyright (c) Kitware, Inc.
+//  All rights reserved.
+//  See LICENSE.txt for details.
+//
+//  This software is distributed WITHOUT ANY WARRANTY; without even
+//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+//  PURPOSE.  See the above copyright notice for more information.
+//=========================================================================
+#include "smtk/extension/paraview/project/Utility.h"
+
+#include "smtk/extension/paraview/appcomponents/pqSMTKBehavior.h"
+#include "smtk/extension/paraview/appcomponents/pqSMTKResource.h"
+
+#include "smtk/task/Manager.h"
+
+#include "pqDataRepresentation.h"
+
+namespace smtk
+{
+namespace paraview
+{
+
+RepresentationObjectMap representationsOfObjects(
+  const smtk::task::Manager::ResourceObjectMap& resourcesToObjects)
+{
+  // Filter representations by the \a spec.
+  RepresentationObjectMap representations;
+
+  auto* behavior = pqSMTKBehavior::instance();
+  for (const auto& entry : resourcesToObjects)
+  {
+    smtk::resource::Resource* resource = entry.first;
+    if (!resource) { continue; }
+
+    QPointer<pqSMTKResource> pvrsrc = behavior->getPVResource(resource->shared_from_this());
+    if (!pvrsrc)
+    {
+      continue;
+    }
+
+    for (const auto& view : pvrsrc->getViews())
+    {
+      for (const auto& representation : pvrsrc->getRepresentations(view))
+      {
+        for (const auto& object : entry.second)
+        {
+          representations[representation].insert(object);
+        }
+      }
+    }
+  }
+
+  return representations;
+}
+
+RepresentationObjectMap relevantRepresentations(
+  const nlohmann::json& spec, smtk::task::Manager* taskMgr, smtk::task::Task* task)
+{
+  auto objectMap = taskMgr->workflowObjects(spec, task);
+  auto representations = representationsOfObjects(objectMap);
+  return representations;
+}
+
+} // namespace paraview
+} // namespace smtk
diff --git a/smtk/extension/paraview/project/Utility.h b/smtk/extension/paraview/project/Utility.h
new file mode 100644
index 0000000000000000000000000000000000000000..7e87a6e740104746e5c14bb63dab2aae26925bdf
--- /dev/null
+++ b/smtk/extension/paraview/project/Utility.h
@@ -0,0 +1,52 @@
+//=========================================================================
+//  Copyright (c) Kitware, Inc.
+//  All rights reserved.
+//  See LICENSE.txt for details.
+//
+//  This software is distributed WITHOUT ANY WARRANTY; without even
+//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+//  PURPOSE.  See the above copyright notice for more information.
+//=========================================================================
+#ifndef smtk_extension_paraview_project_Utility_h
+#define smtk_extension_paraview_project_Utility_h
+
+#include "smtk/extension/paraview/project/smtkPQProjectExtModule.h"
+
+#include "smtk/project/Observer.h"
+#include "smtk/task/Active.h"
+#include "smtk/task/Manager.h"
+#include "smtk/task/Task.h"
+
+#include "smtk/PublicPointerDefs.h"
+
+class pqDataRepresentation;
+
+namespace smtk
+{
+namespace paraview
+{
+
+using RepresentationObjectMap =
+  std::map<pqDataRepresentation*, std::unordered_set<smtk::resource::PersistentObject*>>;
+
+/// Given a map from resources to persistent objects, return a map from
+/// representations tied to the resource keys to persisten object values.
+///
+/// This is intended for use by user interface code that needs to adjust the visibility of
+/// components and/or resources.
+RepresentationObjectMap representationsOfObjects(
+  const smtk::task::Manager::ResourceObjectMap& resourcesToObjects);
+
+/// Return a list of representations that are relevant to \a spec and \a task.
+///
+/// These representations will correspond to a resources that match
+/// a directive above because (a) they are part of the project owning the
+/// task manager or (b) they are on input ports of the task referenced by
+/// the \a spec.
+RepresentationObjectMap relevantRepresentations(
+  const nlohmann::json& spec, smtk::task::Manager* taskMgr, smtk::task::Task* task);
+
+} // namespace paraview
+} // namespace smtk
+
+#endif // smtk_extension_paraview_project_Utility_h
diff --git a/smtk/extension/paraview/project/pqSMTKTaskResourceVisibility.cxx b/smtk/extension/paraview/project/pqSMTKTaskResourceVisibility.cxx
index 303981911b2fe129e06174fb5291feb762d46e2e..e21b3850ee409ee6feb346bbbf43eb86a511ca29 100644
--- a/smtk/extension/paraview/project/pqSMTKTaskResourceVisibility.cxx
+++ b/smtk/extension/paraview/project/pqSMTKTaskResourceVisibility.cxx
@@ -36,93 +36,16 @@
 
 #include <QTimer>
 
+// Change "#undef" to "#define" to print messages as this behavior
+// responds to changes in tasks with 3-d view style.
+#undef SMTK_DBG_3D_STYLE
+
 using namespace smtk::string::literals;
 
 namespace
 {
-pqSMTKTaskResourceVisibility* gInstance = nullptr;
-
-bool resourceMatch(
-  const std::string& filter,
-  smtk::resource::PersistentObject* object,
-  smtk::resource::Resource*& rsrc)
-{
-  if (auto* resource = dynamic_cast<smtk::resource::Resource*>(object))
-  {
-    rsrc = resource;
-    if (filter == "*" || rsrc->matchesType(filter))
-    {
-      return true;
-    }
-  }
-  else if (auto* comp = dynamic_cast<smtk::resource::Component*>(object))
-  {
-    if ((rsrc = comp->parentResource()))
-    {
-      if (filter == "*" || rsrc->matchesType(filter))
-      {
-        return true;
-      }
-    }
-  }
-  return false;
-}
 
-bool componentMatch(
-  const std::string& filter,
-  smtk::resource::PersistentObject* object,
-  smtk::resource::Resource* rsrc)
-{
-  // An empty filter string indicates only resources are allowed.
-  if (filter.empty())
-  {
-    return (object == rsrc);
-  }
-  // "*" allows any component:
-  else if (filter == "*")
-  {
-    // Assume that if object is not a pointer to the parent resource,
-    // it must be a component:
-    return (object != rsrc);
-  }
-  // We have a non-trivial filter string; we must have a component.
-  if (auto* comp = dynamic_cast<smtk::resource::Component*>(object))
-  {
-    auto query = rsrc->queryOperation(filter);
-    return query ? query(*comp) : false;
-  }
-  return false;
-}
-
-bool filterObject(const nlohmann::json::array_t& filter, smtk::resource::PersistentObject* object)
-{
-  for (const auto& filterTuple : filter)
-  {
-    try
-    {
-      if (filterTuple.is_array() && filterTuple.size() == 2)
-      {
-        smtk::resource::Resource* rsrc{ nullptr };
-        if (resourceMatch(filterTuple[0].get<std::string>(), object, rsrc))
-        {
-          auto compSpec = filterTuple[1].is_null() ? "" : filterTuple[1].get<std::string>();
-          if (componentMatch(compSpec, object, rsrc))
-          {
-            return true;
-          }
-        }
-      }
-    }
-    catch (nlohmann::json::exception& e)
-    {
-      smtkErrorMacro(
-        smtk::io::Logger::instance(),
-        "Cannot process filter \"" << filterTuple.dump() << "\";" << e.what());
-      continue;
-    }
-  }
-  return false;
-}
+pqSMTKTaskResourceVisibility* gInstance = nullptr;
 
 } // anonymous namespace
 
@@ -299,7 +222,6 @@ void pqSMTKTaskResourceVisibility::handleTaskEvent(
   {
     m_currentTask->observers().erase(m_currentTaskObserver);
     this->processTaskEvent(m_currentTask, "deactivated"_token);
-    std::cout << "Process \"deactivated\" event for \"" << m_currentTask->name() << "\".\n";
     // self->displayResource(nullptr);
   }
   m_currentTask = nextTask;
@@ -316,7 +238,7 @@ void pqSMTKTaskResourceVisibility::handleTaskEvent(
     auto* pqCore = pqApplicationCore::instance();
     if (pqCore)
     {
-      if (auto* panel = dynamic_cast<pqSMTKDiagramPanel*>(pqCore->manager("smtk task diagram")))
+      if (auto* panel = dynamic_cast<pqSMTKDiagramPanel*>(pqCore->manager("smtk task panel")))
       {
         for (const auto& generatorEntry : panel->diagram()->generators())
         {
@@ -331,8 +253,10 @@ void pqSMTKTaskResourceVisibility::handleTaskEvent(
       }
     }
   }
+#ifdef SMTK_DBG_3D_STYLE
   std::cout << "Process \"activated\" event for \""
             << (m_currentTask ? m_currentTask->name() : "(default)") << "\".\n";
+#endif
   this->processTaskEvent(m_currentTask, "activated"_token);
 }
 
@@ -396,7 +320,7 @@ void pqSMTKTaskResourceVisibility::applyColorBy(
         << spec.dump(2) << "\n");
     return;
   }
-  auto representations = this->relevantRepresentations(spec);
+  auto representations = smtk::paraview::relevantRepresentations(spec, m_currentTaskManager, m_currentTask);
   auto mode(spec.at("mode").get<smtk::string::Token>());
   bool needsRender = false;
   for (const auto& entry : representations)
@@ -461,7 +385,7 @@ void pqSMTKTaskResourceVisibility::applyShowObjects(
           << spec.dump(2) << "\n");
       return;
     }
-    auto representations = this->relevantRepresentations(spec);
+    auto representations = smtk::paraview::relevantRepresentations(spec, m_currentTaskManager, m_currentTask);
     for (const auto& entry : representations)
     {
       auto* representation = entry.first;
@@ -479,171 +403,3 @@ void pqSMTKTaskResourceVisibility::applyShowObjects(
     pqActiveObjects::instance().activeView()->render();
   }
 }
-
-std::map<pqRepresentation*, std::unordered_set<smtk::resource::PersistentObject*>>
-pqSMTKTaskResourceVisibility::relevantRepresentations(const nlohmann::json& spec)
-{
-  // Filter representations by the \a spec.
-  std::map<pqRepresentation*, std::unordered_set<smtk::resource::PersistentObject*>>
-    representations;
-
-  nlohmann::json source;
-  nlohmann::json filter;
-  if (spec.contains("source"))
-  {
-    source = spec.at("source");
-    if (!source.is_object() || !source.contains("type"))
-    {
-      smtkErrorMacro(
-        smtk::io::Logger::instance(),
-        "Spec source must be a dictionary with 'type' key, "
-        "got \""
-          << source.dump() << "\"");
-      return representations;
-    }
-  }
-  else
-  {
-    source = { { "type", "project resources" } };
-  }
-
-  if (spec.contains("filter"))
-  {
-    filter = spec.at("filter");
-    if (!filter.is_array())
-    {
-      smtkErrorMacro(
-        smtk::io::Logger::instance(),
-        "Spec filter must be an array, got \"" << filter.dump() << "\".");
-      return representations;
-    }
-  }
-  else
-  {
-    // Accept any resource or component:
-    filter = nlohmann::json::array_t({ { "*", "*" }, { "*", nullptr } });
-  }
-
-  auto* behavior = pqSMTKBehavior::instance();
-  auto sourceType = source["type"].get<smtk::string::Token>();
-  std::unordered_set<smtk::resource::PersistentObject*> objects;
-  switch (sourceType.id())
-  {
-    default:
-      smtkErrorMacro(
-        smtk::io::Logger::instance(), "Unknown source type \"" << sourceType.data() << "\".");
-      // fall through
-    case "project resources"_hash:
-    {
-      if (!m_currentTaskManager)
-      {
-        return representations;
-      }
-
-      auto* project = dynamic_cast<smtk::project::Project*>(m_currentTaskManager->resource());
-      if (!project)
-      {
-        return representations;
-      }
-
-      for (const auto& resource : project->resources())
-      {
-        if (filterObject(filter, resource.get()))
-        {
-          objects.insert(resource.get());
-        }
-      }
-    }
-    break;
-    case "active task port"_hash:
-    {
-      if (!m_currentTask || !source.contains("port"))
-      {
-        smtkErrorMacro(smtk::io::Logger::instance(), "No active task or no \"port\" specified.");
-        return representations;
-      }
-
-      // We are asked to find objects on a port of the (now) active task.
-      // Determine whether we are filtering on role or not.
-      smtk::string::Token sourceRole;
-      if (source.contains("role"))
-      {
-        sourceRole = source.at("role").get<smtk::string::Token>();
-      }
-      // Find the correct port:
-      const auto& taskPortMap = m_currentTask->ports();
-      auto it = taskPortMap.find(source["port"]);
-      if (it == taskPortMap.end())
-      {
-        return representations;
-      }
-      // Fetch data from the port. We only understand ObjectsInRoles at this point:
-      auto portData = it->second->parent()->portData(it->second);
-      if (portData)
-      {
-        if (auto objectsInRoles = std::dynamic_pointer_cast<smtk::task::ObjectsInRoles>(portData))
-        {
-          for (const auto& entry : objectsInRoles->data())
-          {
-            if (sourceRole.valid() && entry.first != sourceRole)
-            { // Skip objects in unrequested roles.
-              continue;
-            }
-            // Filter objects as requested.
-            for (const auto& object : entry.second)
-            {
-              if (filterObject(filter, object))
-              {
-                objects.insert(object);
-              }
-            }
-          }
-        }
-        else
-        {
-          smtkErrorMacro(
-            smtk::io::Logger::instance(),
-            "Unhandled port data type \"" << portData->typeName() << "\".");
-        }
-      }
-    }
-    break;
-  }
-
-  for (const auto& object : objects)
-  {
-    smtk::resource::Resource* resource{ nullptr };
-    QPointer<pqSMTKResource> pvrsrc;
-    if ((resource = dynamic_cast<smtk::resource::Resource*>(object)))
-    {
-      pvrsrc = behavior->getPVResource(resource->shared_from_this());
-    }
-    else if (auto* comp = dynamic_cast<smtk::resource::Component*>(object))
-    {
-      resource = comp->resource().get();
-      pvrsrc = behavior->getPVResource(comp->resource());
-    }
-    if (!pvrsrc || !this->isResourceRelevant(resource->shared_from_this(), filter))
-    {
-      continue;
-    }
-
-    for (const auto& view : pvrsrc->getViews())
-    {
-      for (const auto& representation : pvrsrc->getRepresentations(view))
-      {
-        representations[representation].insert(object);
-      }
-    }
-  }
-
-  return representations;
-}
-
-bool pqSMTKTaskResourceVisibility::isResourceRelevant(
-  const std::shared_ptr<smtk::resource::Resource>& resource,
-  const nlohmann::json& filter)
-{
-  smtk::resource::Resource* rsrc = resource.get();
-  return filterObject(filter, rsrc);
-}
diff --git a/smtk/extension/paraview/project/pqSMTKTaskResourceVisibility.h b/smtk/extension/paraview/project/pqSMTKTaskResourceVisibility.h
index 11ebb9456b055ee6a96ca81fba7a7726c0b1a803..cae7e9a81f5954f486e42f6baf212580f957bcc0 100644
--- a/smtk/extension/paraview/project/pqSMTKTaskResourceVisibility.h
+++ b/smtk/extension/paraview/project/pqSMTKTaskResourceVisibility.h
@@ -10,7 +10,7 @@
 #ifndef smtk_extension_paraview_appcomponents_pqSMTKTaskResourceVisibility_h
 #define smtk_extension_paraview_appcomponents_pqSMTKTaskResourceVisibility_h
 
-#include "smtk/extension/paraview/project/smtkPQProjectExtModule.h"
+#include "smtk/extension/paraview/project/Utility.h"
 
 #include "smtk/project/Observer.h"
 #include "smtk/task/Active.h"
@@ -55,10 +55,10 @@ class pqServer;
   * + "port" (with the name of a port on the active task) if the type is "active task port".
   * + "roles" (with the name of a role for data on the port) if the type is "active task port".
   *
-  * The "filter" must be an dictionary holding:
+  * The "filter" must be an array holding:
   *
-  * + "resources", an optional array of accepted resource type-names or "*". If not present, "*" is assumed.
-  * + "components", an optional array of accepted component-filters or "*". If not present, only
+  * + dictionaries with a "resource" and "component" key or
+  * + arrays holding two strings (a resource and component filter, in that order).
   *
   * When a task is deactivated and no new task is activated at the same time,
   * (1) if the task-path is empty (i.e., the top-level tasks are showing), then the default
@@ -102,7 +102,8 @@ class pqServer;
   * + hide markup resources present on the "input" port of the active task (without toggling
   *   per-component visibility) when a task with the "example" style is deactivated; and
   * + show both resources and components (toggling as needed) on the active task's "output" port
-  *   when any task with the "example" style is deactivated.
+  *   when any task with the "example" style is deactivated. (This way, as long as the task is
+  *   active, its input port data is visible; when deactivated, its output port data is visible.)
   */
 class SMTKPQPROJECTEXT_EXPORT pqSMTKTaskResourceVisibility : public QObject
 {
@@ -136,20 +137,6 @@ protected: // NOLINT(readability-redundant-access-specifiers)
     smtk::task::Task* task,
     smtk::string::Token event);
 
-  /// Return a list of representations that are relevant to \a spec.
-  ///
-  /// These representations will correspond to a resources that match
-  /// a directive above because (a) they are part of the project owning the
-  /// task manager or (b) they are on input ports of the task referenced by
-  /// the \a spec.
-  std::map<pqRepresentation*, std::unordered_set<smtk::resource::PersistentObject*>>
-  relevantRepresentations(const nlohmann::json& spec);
-
-  /// Is the given resource relevant to the \a spec?
-  bool isResourceRelevant(
-    const std::shared_ptr<smtk::resource::Resource>& resource,
-    const nlohmann::json& filter);
-
   std::map<smtk::project::ManagerPtr, smtk::project::Observers::Key> m_projectManagerObservers;
   smtk::task::Task* m_currentTask{ nullptr };
   smtk::task::Manager* m_currentTaskManager{ nullptr };
diff --git a/smtk/extension/paraview/project/pqTaskControlView.cxx b/smtk/extension/paraview/project/pqTaskControlView.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..3a61e41468a0a7bf5a9d028ddbad14d698616104
--- /dev/null
+++ b/smtk/extension/paraview/project/pqTaskControlView.cxx
@@ -0,0 +1,690 @@
+//=========================================================================
+//  Copyright (c) Kitware, Inc.
+//  All rights reserved.
+//  See LICENSE.txt for details.
+//
+//  This software is distributed WITHOUT ANY WARRANTY; without even
+//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+//  PURPOSE.  See the above copyright notice for more information.
+//=========================================================================
+
+#include "smtk/extension/paraview/project/pqTaskControlView.h"
+
+#include "smtk/extension/paraview/appcomponents/pqSMTKDiagramPanel.h"
+#include "smtk/extension/paraview/appcomponents/pqSMTKResourceRepresentation.h"
+#include "smtk/extension/paraview/appcomponents/pqSMTKResource.h"
+#include "smtk/extension/paraview/project/Utility.h"
+
+#include "smtk/extension/qt/qtUIManager.h"
+
+#include "smtk/view/Configuration.h"
+#include "smtk/view/Manager.h"
+
+#include "smtk/operation/Manager.h"
+#include "smtk/project/Manager.h"
+#include "smtk/task/Manager.h"
+#include "smtk/task/Active.h"
+
+#include "smtk/common/Managers.h"
+
+#include "smtk/io/Logger.h"
+
+#include "pqApplicationCore.h"
+#include "pqSMAdaptor.h"
+#include "pqView.h"
+
+#include "vtkSMProperty.h"
+#include "vtkSMPropertyHelper.h"
+#include "vtkSMProxy.h"
+
+#include <QFile>
+#include <QSlider>
+#include <QFont>
+#include <QFormLayout>
+#include <QFrame>
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QScrollArea>
+#include <QSize>
+#include <QTabWidget>
+#include <QPushButton>
+#include <QVBoxLayout>
+#include <QTimer>
+#include <QVariant>
+
+using namespace smtk::paraview;
+using namespace smtk::string::literals;
+
+namespace smtk
+{
+namespace extension
+{
+namespace // anonymous
+{
+
+bool representationObjectMapContainsResource(const RepresentationObjectMap::value_type& entry)
+{
+  auto pvDRep = dynamic_cast<pqDataRepresentation*>(entry.first);
+  auto pvRsrc = pvDRep ? dynamic_cast<pqSMTKResource*>(pvDRep->getInput()) : nullptr;
+  if (!pvRsrc) { return false; }
+  auto it = entry.second.find(pvRsrc->getResource().get());
+  return (it != entry.second.end() || entry.second.find(nullptr) != entry.second.end());
+}
+
+} // anonymous namespace
+
+/// Private storage for a task-control views.
+class pqTaskControlView::Internal
+{
+public:
+  Internal(pqTaskControlView* self)
+    : m_view(self)
+  {}
+
+  /// Add a widget for a \a childSpec.
+  ///
+  /// If \a layout is non-null and appropriate for the \a childSpec,
+  /// the current \a layout and \a widget are used. Otherwise, a new
+  /// layout is created.
+  bool addChildItem(
+    const smtk::view::Configuration::Component& childSpec,
+    QLayout*& layout,
+    QWidget*& parent,
+    int& childNum,
+    smtk::task::Task* task);
+
+  /// Called when the active task changes.
+  void activeTaskChange(smtk::task::Task* prev, smtk::task::Task* next)
+  {
+    (void)prev;
+    (void)next;
+    m_view->updateWithActiveTask(next);
+  }
+
+  void addTaskDescription(
+    QLayout* layout, smtk::task::Task* task, const smtk::view::Configuration::Component& spec)
+  {
+    (void)spec; // Currently no options to look up.
+    auto* description = new QLabel();
+    smtk::task::Task::InformationOptions opt;
+    description->setObjectName("taskDescription");
+    description->setText(QString::fromStdString(task ? task->information(opt) : ""));
+    description->setWordWrap(true); // Allow line breaks so panel is not forced to be crazy wide.
+    layout->addWidget(description);
+    ++this->numChildren;
+  }
+
+  void addTaskStatus(
+    QFormLayout* formLayout, smtk::task::Task* task, const smtk::view::Configuration::Component& spec)
+  {
+    auto* label = new QLabel();
+    label->setObjectName("taskName");
+    auto txt = QString::fromStdString(task ? task->name() : "");
+    if (spec.attributeAsBool("ReturnToDiagram"))
+    {
+      txt += QString::fromStdString("<a href=\"#returnToDiagram\">⤴</a>");
+    }
+    label->setText(txt);
+    label->setOpenExternalLinks(false);
+    QObject::connect(label, &QLabel::linkActivated, m_view, &pqTaskControlView::returnToDiagram);
+    auto* ctrl = new QPushButton();
+    ctrl->setObjectName("taskCompleter");
+    ctrl->setText(
+      task ? (task->isCompleted() ? "Completed" : "Complete") : "Complete");
+    ctrl->setCheckable(true);
+    ctrl->setChecked(task ? task->isCompleted() : false);
+    ctrl->setEnabled(task ? task->state() >= smtk::task::State::Completable : false);
+    QObject::connect(ctrl, &QAbstractButton::toggled, m_view, &pqTaskControlView::updateTaskCompletion);
+    if (task)
+    {
+      m_taskStateObserver = task->observers().insert(
+        [ctrl, this](smtk::task::Task& t, smtk::task::State prev, smtk::task::State next)
+        {
+          (void)prev;
+          ctrl->setChecked(t.isCompleted());
+          ctrl->setEnabled(next >= smtk::task::State::Completable);
+        }
+      );
+    }
+    formLayout->addRow(label, ctrl);
+    ++this->numChildren;
+  }
+
+  void addRepresentationControl(
+    QFormLayout* formLayout,
+    smtk::task::Task* task,
+    const smtk::view::Configuration::Component& spec)
+  {
+    (void)task;
+    auto* field = new QHBoxLayout;
+    std::string name;
+    if (!spec.attribute("Name", name))
+    {
+      smtkWarningMacro(smtk::io::Logger::instance(), "RepresentationControl without name.");
+      name = "Unknown";
+    }
+    std::string label;
+    if (!spec.attribute("Label", label))
+    {
+      smtkWarningMacro(smtk::io::Logger::instance(), "RepresentationControl without label.");
+      label = name;
+    }
+    for (const auto& controlSpec : spec.children())
+    {
+      if (controlSpec.name() == "Control")
+      {
+        if (controlSpec.attributeAsString("Type") == "Visibility")
+        {
+          auto* ctrl = new QPushButton;
+          ctrl->setObjectName("visibility");
+          ctrl->setText("Visibility");
+          ctrl->setCheckable(true);
+          ctrl->setToolTip("Toggle visibility");
+          bool initiallyVisible{ true };
+          controlSpec.attributeAsBool("InitiallyVisible", initiallyVisible);
+          ctrl->setChecked(initiallyVisible);
+          m_representationSpecs[name] = spec;
+          QObject::connect(
+            ctrl, &QPushButton::toggled, [this, name](bool toggled) { m_view->toggleVisibility(name, toggled); });
+          field->addWidget(ctrl);
+          // Now force state to match button upon view construction.
+          m_view->toggleVisibility(name, initiallyVisible);
+        }
+        else if (controlSpec.attributeAsString("Type") == "Opacity")
+        {
+          auto* ctrl = new QSlider;
+          ctrl->setObjectName("opacity");
+          ctrl->setMinimum(0);
+          ctrl->setMaximum(255);
+          ctrl->setPageStep(16);
+          ctrl->setOrientation(Qt::Horizontal);
+          int initialValue{ 255 };
+          controlSpec.attributeAsInt("InitialValue", initialValue);
+          ctrl->setValue(initialValue);
+          ctrl->setToolTip("Adjust opacity");
+          m_representationSpecs[name] = spec;
+          QObject::connect(
+            ctrl, &QSlider::valueChanged, [this, name](int value) { m_view->updateOpacity(name, value / 255.0); });
+          field->addWidget(ctrl);
+          // Now force state to match slider upon view construction.
+          m_view->updateOpacity(name, initialValue / 255.);
+        }
+      }
+    }
+    formLayout->addRow(QString::fromStdString(label), field);
+    ++this->numChildren;
+  }
+
+  void addOperationControl(
+    QLayout* layout,
+    smtk::task::Task* task,
+    const smtk::view::Configuration::Component& spec)
+  {
+    (void)layout;
+    (void)task;
+    (void)spec;
+    std::string opType;
+    if (!spec.attribute("Type", opType) || opType.empty())
+    {
+      smtkErrorMacro(smtk::io::Logger::instance(),
+        "Operation type must be specified.");
+      return;
+    }
+    std::string opLabel;
+    spec.attribute("Label", opLabel);
+    if (opLabel.empty())
+    {
+      opLabel = "Run " + opType;
+    }
+    auto taskMgr = task->manager();
+    auto mgrs = taskMgr->managers();
+    auto opMgr = mgrs ? mgrs->get<smtk::operation::Manager::Ptr>() : nullptr;
+    auto op = opMgr ? opMgr->create(opType) : nullptr;
+    if (!op)
+    {
+      smtkErrorMacro(smtk::io::Logger::instance(),
+        "Could not create \"" << opType << "\" from mgr " << opMgr << ".");
+      return;
+    }
+    auto opButton = new QPushButton;
+    opButton->setText(QString::fromStdString(opLabel));
+    QObject::connect(opButton, &QAbstractButton::clicked, [task, opMgr, op, spec](bool clicked)
+      {
+        (void)clicked;
+        for (const auto& paramSpec : spec.children())
+        {
+          if (paramSpec.name() == "Association")
+          {
+            if (!op->parameters()->associations())
+            {
+              smtkErrorMacro(smtk::io::Logger::instance(),
+                "No associations can be specified for operation.");
+              continue;
+            }
+            op->parameters()->associations()->reset();
+            auto data = task->manager()->workflowObjects(paramSpec, task);
+            for (const auto& entry : data)
+            {
+              for (const auto& obj : entry.second)
+              {
+                if (obj)
+                {
+                  op->parameters()->associations()->appendValue(obj->shared_from_this());
+                }
+                else
+                {
+                  op->parameters()->associations()->appendValue(entry.first->shared_from_this());
+                }
+              }
+            }
+          }
+          else if (paramSpec.name() == "Reference")
+          {
+            std::string refPath;
+            smtk::attribute::ReferenceItem::Ptr refItem;
+            if (!paramSpec.attribute("Path", refPath) || !(refItem = op->parameters()->itemAtPathAs<smtk::attribute::ReferenceItem>(refPath)))
+            {
+              smtkErrorMacro(smtk::io::Logger::instance(),
+                "No reference item at path '" << refPath << "'.");
+              continue;
+            }
+            refItem->reset();
+            auto data = task->manager()->workflowObjects(paramSpec, task);
+            for (const auto& entry : data)
+            {
+              for (const auto& obj : entry.second)
+              {
+                if (obj)
+                {
+                  refItem->appendValue(obj->shared_from_this());
+                }
+                else
+                {
+                  refItem->appendValue(entry.first->shared_from_this());
+                }
+              }
+            }
+          }
+          else
+          {
+            smtkErrorMacro(smtk::io::Logger::instance(),
+              "Unhandled element '" << paramSpec.name() << "'.");
+          }
+        }
+        opMgr->launchers()(op);
+      });
+
+    if (auto* formLayout = dynamic_cast<QFormLayout*>(layout))
+    {
+      formLayout->addRow(QString::fromStdString(opLabel), opButton);
+    }
+    else
+    {
+      layout->addWidget(opButton);
+    }
+    ++this->numChildren;
+  }
+
+  void addFormGroup(
+    QFormLayout* formLayout,
+    smtk::task::Task* task,
+    const smtk::view::Configuration::Component& spec,
+    QWidget*& parent,
+    int& childNum)
+  {
+    // Create an internal HBoxLayout and add children (presumed
+    // to be non-form controls) to that. Then add the HBox plus
+    // the form-title to \a formLayout.
+    QLayout* hbox = new QHBoxLayout;
+    for (const auto& formChild : spec.children())
+    {
+      this->addChildItem(formChild, hbox, parent, childNum, task);
+    }
+    std::string formLabel;
+    if (!spec.attribute("Title", formLabel))
+    {
+      formLabel = " ";
+    }
+    formLayout->addRow(QString::fromStdString(formLabel), hbox);
+  }
+
+  pqTaskControlView* m_view{ nullptr };
+  smtk::task::Manager* m_currentTaskManager{ nullptr };
+  smtk::task::Task* m_currentTask{ nullptr };
+  smtk::project::Observers::Key m_projectObserverKey;
+  smtk::task::Active::Observers::Key m_activeTaskObserverKey;
+  std::unordered_map<smtk::string::Token, smtk::view::Configuration::Component> m_representationSpecs;
+  int numChildren{ 0 };
+  smtk::task::Task::Observers::Key m_taskStateObserver;
+};
+
+bool pqTaskControlView::Internal::addChildItem(
+  const smtk::view::Configuration::Component& childSpec,
+  QLayout*& layout,
+  QWidget*& parent,
+  int& childNum,
+  smtk::task::Task* task)
+{
+  bool needLayout = !layout;
+  smtk::string::Token childType = childSpec.name();
+  static std::unordered_set<smtk::string::Token> formItems{
+    "ActiveTaskStatus"_token, "RepresentationControl"_token, "FormGroup"_token
+  };
+
+  bool isFormItem = (formItems.find(childType) != formItems.end());
+  auto* formLayout = dynamic_cast<QFormLayout*>(layout);
+  needLayout |= (isFormItem && !formLayout) || (!isFormItem && formLayout);
+  if (needLayout)
+  {
+    parent = new QWidget();
+    std::ostringstream pname;
+    pname << "TaskControlWidget" << childNum++;
+    parent->setObjectName(pname.str().c_str());
+    if (isFormItem)
+    {
+      formLayout = new QFormLayout(parent);
+      layout = formLayout;
+    }
+    else
+    {
+      layout = new QVBoxLayout(parent);
+    }
+    m_view->widget()->layout()->addWidget(parent);
+  }
+  switch (childType.id())
+  {
+  case "ActiveTaskDescription"_hash:
+    this->addTaskDescription(layout, task, childSpec);
+    break;
+  case "ActiveTaskStatus"_hash:
+    this->addTaskStatus(formLayout, task, childSpec);
+    break;
+  case "RepresentationControl"_hash:
+    this->addRepresentationControl(formLayout, task, childSpec);
+    break;
+  case "OperationControl"_hash:
+    this->addOperationControl(layout, task, childSpec);
+    break;
+  case "FormGroup"_hash:
+    this->addFormGroup(formLayout, task, childSpec, parent, childNum);
+    break;
+  default:
+    return false;
+    break;
+  }
+  return true;
+}
+
+qtBaseView* pqTaskControlView::createViewWidget(const smtk::view::Information& info)
+{
+  if (!qtBaseView::validateInformation(info))
+  {
+    return nullptr; // \a info is not suitable for this View
+  }
+  auto* view = new pqTaskControlView(info);
+  view->buildUI();
+  return view;
+}
+
+pqTaskControlView::pqTaskControlView(const smtk::view::Information& info)
+  : qtBaseView(info)
+{
+  m_p = new Internal(this);
+  // auto viewConfig = this->configuration();
+  // if (viewConfig)
+  // {
+  // }
+}
+
+pqTaskControlView::~pqTaskControlView()
+{
+  delete m_p;
+}
+
+bool pqTaskControlView::isEmpty() const
+{
+  return m_p->numChildren == 0;
+}
+
+bool pqTaskControlView::isValid() const
+{
+  // This view is always valid for now.
+  return true;
+}
+
+void pqTaskControlView::updateUI()
+{
+  auto projMgr = this->uiManager()->managers().get<smtk::project::Manager::Ptr>();
+  auto project = projMgr ? *projMgr->projects().begin() : nullptr;
+  auto* taskMgr = project ? &project->taskManager() : nullptr;
+  auto* activeTask = taskMgr ? taskMgr->active().task() : nullptr;
+  this->updateWithActiveTask(activeTask);
+}
+
+void pqTaskControlView::showAdvanceLevelOverlay(bool show)
+{
+  this->qtBaseView::showAdvanceLevelOverlay(show);
+}
+
+void pqTaskControlView::onShowCategory()
+{
+  this->Superclass::onShowCategory();
+}
+
+void pqTaskControlView::returnToDiagram()
+{
+  auto* pqCore = pqApplicationCore::instance();
+  if (!pqCore)
+  {
+    smtkErrorMacro(smtk::io::Logger::instance(),
+      "Cannot return to diagram panel as there is no application core.");
+    return;
+  }
+  auto* panel = dynamic_cast<pqSMTKDiagramPanel*>(pqCore->manager("smtk task panel"));
+  if (!panel)
+  {
+    smtkErrorMacro(smtk::io::Logger::instance(),
+      "Cannot return to diagram panel as there is no diagram panel.");
+    return;
+  }
+  panel->focusPanel();
+}
+
+void pqTaskControlView::updateTaskCompletion(bool completed)
+{
+  auto projMgr = this->uiManager()->managers().get<smtk::project::Manager::Ptr>();
+  auto project = projMgr ? *projMgr->projects().begin() : nullptr;
+  auto* taskMgr = project ? &project->taskManager() : nullptr;
+  if (taskMgr)
+  {
+    auto* task = taskMgr->active().task();
+    if (task && task->state() >= smtk::task::State::Completable)
+    {
+      QTimer::singleShot(0, [this, task, completed]()
+        {
+          if (task->markCompleted(completed) && completed)
+          {
+            this->returnToDiagram();
+          }
+        }
+      );
+    }
+  }
+}
+
+void pqTaskControlView::toggleVisibility(const std::string& name, bool show)
+{
+  if (m_p->m_currentTaskManager)
+  {
+    auto objMap = m_p->m_currentTaskManager->workflowObjects(m_p->m_representationSpecs[name], m_p->m_currentTask);
+    auto repMap = smtk::paraview::representationsOfObjects(objMap);
+    for (const auto& entry : repMap)
+    {
+      bool needRender = false;
+      // Toggle the whole representation's visibility if either
+      // (1) entry.second contains a null pointer (indicating the resource itself
+      //     is supposed to have its visibility changed) or
+      // (2) \a show is set to true (indicating that some components that were
+      //     hidden should now be shown – which cannot happen without the
+      //     resource-representation visibility being set to true).
+      if (show || representationObjectMapContainsResource(entry))
+      {
+        if (entry.first->isVisible() != show)
+        {
+          entry.first->setVisible(show);
+          needRender = true;
+        }
+      }
+      auto pvDRep = dynamic_cast<pqSMTKResourceRepresentation*>(entry.first);
+      if (!pvDRep)
+      {
+        smtkErrorMacro(smtk::io::Logger::instance(),
+          "Could not downcast representation " << entry.first << " to an SMTK resource representation.");
+        continue;
+      }
+      // Now, tell the representation to toggle individual component visibilities.
+      for (const auto& obj : entry.second)
+      {
+        if (!obj) { continue; }
+        if (auto comp = dynamic_cast<smtk::resource::Component*>(obj)->shared_from_this())
+        {
+          needRender |= pvDRep->setVisibility(comp, show);
+        }
+      }
+      if (needRender)
+      {
+        entry.first->getView()->render();
+      }
+    }
+  }
+}
+
+void pqTaskControlView::updateOpacity(const std::string& name, double opacity)
+{
+  if (m_p->m_currentTaskManager)
+  {
+    auto objMap = m_p->m_currentTaskManager->workflowObjects(m_p->m_representationSpecs[name], m_p->m_currentTask);
+    auto repMap = smtk::paraview::representationsOfObjects(objMap);
+    for (const auto& entry : repMap)
+    {
+      bool needRender = false;
+      // Toggle the whole representation's visibility if either
+      // (1) entry.second contains a null pointer (indicating the resource itself
+      //     is supposed to have its visibility changed) or
+      // (2) \a show is set to true (indicating that some components that were
+      //     hidden should now be shown – which cannot happen without the
+      //     resource-representation visibility being set to true).
+      if (representationObjectMapContainsResource(entry))
+      {
+        pqSMAdaptor::setElementProperty(entry.first->getProxy()->GetProperty("Opacity"), opacity);
+        entry.first->getProxy()->UpdateVTKObjects();
+        needRender = true;
+      }
+      // TODO: Handle per-block opacity settings for components.
+      //       Iterate over entry.second, find component inside the
+      //       pqSMTKRepresentation and set per-block opacity.
+      if (needRender)
+      {
+        entry.first->getView()->render();
+      }
+    }
+  }
+}
+
+void pqTaskControlView::buildUI()
+{
+  this->createWidget();
+
+  QVBoxLayout* parentlayout = static_cast<QVBoxLayout*>(this->parentWidget()->layout());
+  if (!parentlayout)
+  {
+    smtkErrorMacro(smtk::io::Logger::instance(),
+      "No parent layout for task control view.");
+    return;
+  }
+
+  if (!this->isTopLevel())
+  {
+    parentlayout->setAlignment(Qt::AlignTop);
+    parentlayout->addWidget(this->Widget);
+    return;
+  }
+  // XXX TODO.
+}
+
+void pqTaskControlView::createWidget()
+{
+  smtk::view::ConfigurationPtr view = this->configuration();
+  if (!view)
+  {
+    return;
+  }
+  // If we have a pre-existing widget, destroy it as we are about to recreate it.
+  QVBoxLayout* parentlayout = static_cast<QVBoxLayout*>(this->parentWidget()->layout());
+  if (this->Widget)
+  {
+    if (parentlayout)
+    {
+      parentlayout->removeWidget(this->Widget);
+    }
+    delete this->Widget;
+  }
+
+  this->Widget = new QFrame(this->parentWidget());
+  this->Widget->setObjectName(view->name().c_str());
+
+  // Create the layout for the frame
+  QVBoxLayout* layout = new QVBoxLayout(this->Widget);
+  layout->setMargin(0);
+  this->Widget->setLayout(layout);
+
+  // Start observing changes in the active task:
+  auto projMgr = this->uiManager()->managers().get<smtk::project::Manager::Ptr>();
+  auto project = projMgr ? *projMgr->projects().begin() : nullptr;
+  auto* taskMgr = project ? &project->taskManager() : nullptr;
+  if (taskMgr)
+  {
+    m_p->m_activeTaskObserverKey = taskMgr->active().observers().insert(
+      [&](smtk::task::Task* prev, smtk::task::Task* next) { m_p->activeTaskChange(prev, next); },
+      0, false, "TaskControlView");
+  }
+  else
+  {
+    m_p->m_activeTaskObserverKey.release();
+  }
+
+  this->updateUI();
+}
+
+void pqTaskControlView::updateWithActiveTask(smtk::task::Task* task)
+{
+  m_p->m_currentTaskManager = task ? task->manager() : nullptr;
+  m_p->m_currentTask = task;
+
+  smtk::view::ConfigurationPtr view = this->configuration();
+  if (!view)
+  {
+    return;
+  }
+  // Remove all previous children (if any).
+  if (this->widget())
+  {
+    for (auto* child : this->widget()->findChildren<QWidget*>(QString(), Qt::FindDirectChildrenOnly))
+    {
+      delete child;
+    }
+  }
+
+  // Now process all of this view's entries
+  QLayout* activeLayout = nullptr;
+  QWidget* activeWidget = nullptr;
+  int childNum = 0;
+  for (const auto& child : view->details().children())
+  {
+    m_p->addChildItem(child, activeLayout, activeWidget, childNum, task);
+  }
+}
+
+
+} // namespace extension
+} // namespace smtk
diff --git a/smtk/extension/paraview/project/pqTaskControlView.h b/smtk/extension/paraview/project/pqTaskControlView.h
new file mode 100644
index 0000000000000000000000000000000000000000..64463f7e1d5a852bffc6e85123998610d64fe19b
--- /dev/null
+++ b/smtk/extension/paraview/project/pqTaskControlView.h
@@ -0,0 +1,71 @@
+//=========================================================================
+//  Copyright (c) Kitware, Inc.
+//  All rights reserved.
+//  See LICENSE.txt for details.
+//
+//  This software is distributed WITHOUT ANY WARRANTY; without even
+//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+//  PURPOSE.  See the above copyright notice for more information.
+//=========================================================================
+#ifndef smtk_extension_pqTaskControlView_h
+#define smtk_extension_pqTaskControlView_h
+
+#include "smtk/extension/paraview/project/smtkPQProjectExtModule.h" // For export.
+#include "smtk/extension/qt/qtBaseView.h"
+#include "smtk/view/Configuration.h" // For API.
+
+namespace smtk
+{
+namespace extension
+{
+
+/**\brief A view for task-level controls.
+  *
+  * Views of this type may contain any number of widgets for manipulating
+  * the currently-active task's completion status, port data, etc.
+  * The view may also be configured to show diagnostic/summary information
+  * about the active task.
+  */
+class SMTKPQPROJECTEXT_EXPORT pqTaskControlView : public qtBaseView
+{
+  Q_OBJECT
+
+public:
+  smtkTypenameMacro(smtk::extension::pqTaskControlView);
+  smtkSuperclassMacro(smtk::extension::qtBaseView);
+
+  static qtBaseView* createViewWidget(const smtk::view::Information& info);
+  pqTaskControlView(const smtk::view::Information& info);
+  ~pqTaskControlView() override;
+
+  /// Returns true if the view does not contain any information to display.
+  bool isEmpty() const override;
+
+  /// Returns false when users must use the view to adjust the workflow.
+  ///
+  /// Currently this always returns true, even when the active task is incomplete.
+  bool isValid() const override;
+
+public Q_SLOTS:
+  void updateUI() override;
+  void showAdvanceLevelOverlay(bool show) override;
+  void onShowCategory() override;
+  void returnToDiagram();
+  void updateTaskCompletion(bool completed);
+  void toggleVisibility(const std::string& name, bool show);
+  void updateOpacity(const std::string& name, double opacity);
+
+protected:
+  void buildUI() override;
+  void createWidget() override;
+  void updateWithActiveTask(smtk::task::Task* task);
+
+private:
+  class Internal;
+  Internal* m_p;
+};
+
+} // namespace extension
+} // namespace smtk
+
+#endif
diff --git a/smtk/extension/paraview/server/smconfig.xml b/smtk/extension/paraview/server/smconfig.xml
index 7a15c29fec533065c4db844701a4ac3bf0c01137..597a266e5a2169ba8f5f5ba44a76c61b3bbe461f 100644
--- a/smtk/extension/paraview/server/smconfig.xml
+++ b/smtk/extension/paraview/server/smconfig.xml
@@ -1171,6 +1171,63 @@
         <BooleanDomain name="bool"/>
       </IntVectorProperty>
     </Proxy>
+
+    <Proxy
+      class="vtkDiskRepresentation"
+      name="DiskRepresentation">
+      <IntVectorProperty
+        animateable="1"
+        command="SetVisibility"
+        default_values="0"
+        name="Visibility"
+        number_of_elements="1">
+        <BooleanDomain name="bool" />
+      </IntVectorProperty>
+      <DoubleVectorProperty
+        argument_is_array="1"
+        command="SetCenterPoint"
+        default_values="0 0 0"
+        information_property="CenterPointInfo"
+        name="CenterPoint"
+        number_of_elements="3">
+        <DoubleRangeDomain name="range" />
+      </DoubleVectorProperty>
+      <DoubleVectorProperty
+        command="GetCenterPoint"
+        information_only="1"
+        name="CenterPointInfo">
+        <SimpleDoubleInformationHelper />
+      </DoubleVectorProperty>
+      <DoubleVectorProperty
+        argument_is_array="1"
+        command="SetNormal"
+        default_values="0 0 0"
+        information_property="NormalInfo"
+        name="Normal"
+        number_of_elements="3">
+        <DoubleRangeDomain name="range" />
+      </DoubleVectorProperty>
+      <DoubleVectorProperty
+        command="GetNormalVector"
+        information_only="1"
+        name="NormalInfo">
+        <SimpleDoubleInformationHelper />
+      </DoubleVectorProperty>
+      <DoubleVectorProperty
+        command="SetRadius"
+        default_values="1"
+        information_property="RadiusInfo"
+        name="Radius"
+        number_of_elements="1">
+        <DoubleRangeDomain name="range" />
+      </DoubleVectorProperty>
+      <DoubleVectorProperty
+        command="GetRadius"
+        information_only="1"
+        name="RadiusInfo">
+        <SimpleDoubleInformationHelper />
+      </DoubleVectorProperty>
+    </Proxy>
   </ProxyGroup>
 
 <!-- ====================================================================== -->
@@ -1183,6 +1240,13 @@
       name="ConeWidget">
     </Proxy>
 
+    <Proxy
+      base_proxygroup="3d_widgets"
+      base_proxyname="WidgetBase"
+      class="vtkDiskWidget"
+      name="DiskWidget">
+    </Proxy>
+
   </ProxyGroup>
 
 <!-- ====================================================================== -->
@@ -1228,6 +1292,38 @@
       </PropertyGroup>
     </Proxy>
 
+    <Proxy
+      name="ImplicitDisk"
+      class="vtkImplicitDisk"
+      >
+      <DoubleVectorProperty
+        name="CenterPoint"
+        command="SetCenterPoint"
+        default_values="0 0 0"
+        number_of_elements="3">
+        <DoubleRangeDomain name="range"/>
+      </DoubleVectorProperty>
+      <DoubleVectorProperty
+        name="Radius"
+        command="SetRadius"
+        default_values="0.5"
+        number_of_elements="1">
+        <DoubleRangeDomain name="range" min="0.0"/>
+      </DoubleVectorProperty>
+      <DoubleVectorProperty
+        name="Normal"
+        command="SetNormal"
+        default_values="0 0 1"
+        number_of_elements="3">
+        <DoubleRangeDomain name="range"/>
+      </DoubleVectorProperty>
+      <PropertyGroup label="Disk Parameters" panel_widget="InteractiveDisk">
+        <Property function="CenterPoint" name="CenterPoint" />
+        <Property function="Radius" name="Radius" />
+        <Property function="Normal" name="Normal" />
+      </PropertyGroup>
+    </Proxy>
+
   </ProxyGroup>
 
 <!-- ====================================================================== -->
@@ -1975,6 +2071,41 @@
       </SubProxy>
     </NewWidgetRepresentationProxy>
 
+    <!-- Disk widget -->
+    <NewWidgetRepresentationProxy
+      class="vtk3DWidgetRepresentation"
+      name="DiskWidgetRepresentation">
+      <IntVectorProperty
+        command="SetEnabled"
+        default_values="0"
+        name="Enabled"
+        number_of_elements="1">
+        <BooleanDomain name="bool" />
+        <Documentation>Enable/Disable widget interaction.</Documentation>
+      </IntVectorProperty>
+      <SubProxy>
+        <Proxy
+          name="Prop"
+          proxygroup="3d_widget_representations"
+          proxyname="DiskRepresentation"></Proxy>
+        <ExposedProperties>
+          <Property name="Visibility" />
+          <Property name="CenterPointInfo" />
+          <Property name="CenterPoint" />
+          <Property name="NormalInfo" />
+          <Property name="Normal" />
+          <Property name="RadiusInfo" />
+          <Property name="Radius" />
+        </ExposedProperties>
+      </SubProxy>
+      <SubProxy>
+        <Proxy
+          name="Widget"
+          proxygroup="3d_widgets"
+          proxyname="DiskWidget"></Proxy>
+      </SubProxy>
+    </NewWidgetRepresentationProxy>
+
   </ProxyGroup>
 
   <!-- Expose user preferences -->
diff --git a/smtk/extension/paraview/widgets/CMakeLists.txt b/smtk/extension/paraview/widgets/CMakeLists.txt
index d1bfb909d651f1de2726da1a0de808f636e35275..24a19f19c232caa165f5261a9d67f4bdbc81cefa 100644
--- a/smtk/extension/paraview/widgets/CMakeLists.txt
+++ b/smtk/extension/paraview/widgets/CMakeLists.txt
@@ -8,11 +8,13 @@ set(headers
 set(classes
   Registrar
   pqConePropertyWidget
+  pqDiskPropertyWidget
   pqPointPropertyWidget
   pqSMTKAttributeItemWidget
   pqSMTKBoxItemWidget
   pqSMTKTransformWidget
   pqSMTKConeItemWidget
+  pqSMTKDiskItemWidget
   pqSMTKInfiniteCylinderItemWidget
   pqSMTKLineItemWidget
   pqSMTKPointItemWidget
@@ -23,6 +25,7 @@ set(classes
 
 set(ui_files
   resources/pqConePropertyWidget.ui
+  resources/pqDiskPropertyWidget.ui
   resources/pqPointPropertyWidget.ui
 )
 
diff --git a/smtk/extension/paraview/widgets/plugin/pqSMTKWidgetsAutoStart.cxx b/smtk/extension/paraview/widgets/plugin/pqSMTKWidgetsAutoStart.cxx
index 07621f35703be7325d9b5fa35ef8f8c7c51bf722..60f2158b9a00db31a60951b8e12aef137540e92f 100644
--- a/smtk/extension/paraview/widgets/plugin/pqSMTKWidgetsAutoStart.cxx
+++ b/smtk/extension/paraview/widgets/plugin/pqSMTKWidgetsAutoStart.cxx
@@ -13,6 +13,7 @@
 
 #include "smtk/extension/paraview/widgets/pqSMTKBoxItemWidget.h"
 #include "smtk/extension/paraview/widgets/pqSMTKConeItemWidget.h"
+#include "smtk/extension/paraview/widgets/pqSMTKDiskItemWidget.h"
 #include "smtk/extension/paraview/widgets/pqSMTKCoordinateFrameItemWidget.h"
 #include "smtk/extension/paraview/widgets/pqSMTKInfiniteCylinderItemWidget.h"
 #include "smtk/extension/paraview/widgets/pqSMTKLineItemWidget.h"
@@ -46,24 +47,21 @@ void pqSMTKWidgetsAutoStart::startup()
   */
 
   // Register qtItem widget subclasses implemented using ParaView 3-D widgets:
+  // clang-format off
   qtSMTKUtilities::registerItemConstructor("Box", pqSMTKBoxItemWidget::createBoxItemWidget);
   qtSMTKUtilities::registerItemConstructor("Cone", pqSMTKConeItemWidget::createConeItemWidget);
-  qtSMTKUtilities::registerItemConstructor(
-    "Cylinder", pqSMTKConeItemWidget::createCylinderItemWidget);
-  qtSMTKUtilities::registerItemConstructor(
-    "CoordinateFrame", pqSMTKCoordinateFrameItemWidget::createCoordinateFrameItemWidget);
-  qtSMTKUtilities::registerItemConstructor(
-    "InfiniteCylinder", pqSMTKInfiniteCylinderItemWidget::createCylinderItemWidget);
+  qtSMTKUtilities::registerItemConstructor("Cylinder", pqSMTKConeItemWidget::createCylinderItemWidget);
+  qtSMTKUtilities::registerItemConstructor("CoordinateFrame", pqSMTKCoordinateFrameItemWidget::createCoordinateFrameItemWidget);
+  qtSMTKUtilities::registerItemConstructor("Disk", pqSMTKDiskItemWidget::createDiskItemWidget);
+  qtSMTKUtilities::registerItemConstructor("InfiniteCylinder", pqSMTKInfiniteCylinderItemWidget::createCylinderItemWidget);
   qtSMTKUtilities::registerItemConstructor("Line", pqSMTKLineItemWidget::createLineItemWidget);
   qtSMTKUtilities::registerItemConstructor("Plane", pqSMTKPlaneItemWidget::createPlaneItemWidget);
   qtSMTKUtilities::registerItemConstructor("Point", pqSMTKPointItemWidget::createPointItemWidget);
   qtSMTKUtilities::registerItemConstructor("Slice", pqSMTKSliceItemWidget::createSliceItemWidget);
-  qtSMTKUtilities::registerItemConstructor(
-    "Sphere", pqSMTKSphereItemWidget::createSphereItemWidget);
-  qtSMTKUtilities::registerItemConstructor(
-    "Spline", pqSMTKSplineItemWidget::createSplineItemWidget);
-  qtSMTKUtilities::registerItemConstructor(
-    "Transform", pqSMTKTransformWidget::createTransformWidget);
+  qtSMTKUtilities::registerItemConstructor("Sphere", pqSMTKSphereItemWidget::createSphereItemWidget);
+  qtSMTKUtilities::registerItemConstructor("Spline", pqSMTKSplineItemWidget::createSplineItemWidget);
+  qtSMTKUtilities::registerItemConstructor("Transform", pqSMTKTransformWidget::createTransformWidget);
+  // clang-format on
 }
 
 void pqSMTKWidgetsAutoStart::shutdown()
diff --git a/smtk/extension/paraview/widgets/pqDiskPropertyWidget.cxx b/smtk/extension/paraview/widgets/pqDiskPropertyWidget.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..0b7e9b0e7925f9c9e86cda50f8f6e7843f59b199
--- /dev/null
+++ b/smtk/extension/paraview/widgets/pqDiskPropertyWidget.cxx
@@ -0,0 +1,134 @@
+//=========================================================================
+//  Copyright (c) Kitware, Inc.
+//  All rights reserved.
+//  See LICENSE.txt for details.
+//
+//  This software is distributed WITHOUT ANY WARRANTY; without even
+//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+//  PURPOSE.  See the above copyright notice for more information.
+//=========================================================================
+#include "smtk/extension/paraview/widgets/pqDiskPropertyWidget.h"
+#include "smtk/extension/paraview/widgets/pqPointPickingVisibilityHelper.h"
+#include "smtk/extension/paraview/widgets/ui_pqDiskPropertyWidget.h"
+
+#include "pqCoreUtilities.h"
+#include "pqPointPickingHelper.h"
+#include "pqView.h"
+
+#include "vtkSMNewWidgetRepresentationProxy.h"
+#include "vtkSMProperty.h"
+#include "vtkSMPropertyGroup.h"
+#include "vtkSMPropertyHelper.h"
+
+#include "vtkCommand.h"
+#include "vtkMath.h"
+#include "vtkVector.h"
+#include "vtkVectorOperators.h"
+
+#include <QCheckBox>
+
+class pqDiskPropertyWidget::Internals
+{
+public:
+  Internals() = default;
+
+  Ui::DiskPropertyWidget Ui;
+};
+
+pqDiskPropertyWidget::pqDiskPropertyWidget(
+  vtkSMProxy* smproxy,
+  vtkSMPropertyGroup* smgroup,
+  QWidget* parentObj)
+  : Superclass("representations", "DiskWidgetRepresentation", smproxy, smgroup, parentObj)
+  , m_p(new pqDiskPropertyWidget::Internals())
+{
+  Ui::DiskPropertyWidget& ui = m_p->Ui;
+  ui.setupUi(this);
+
+  // link show3DWidget checkbox
+  QObject::connect(ui.show3DWidget, &QCheckBox::toggled, this, &pqDiskPropertyWidget::setWidgetVisible);
+  QObject::connect(this, &pqDiskPropertyWidget::widgetVisibilityToggled, ui.show3DWidget, &QCheckBox::setChecked);
+  this->setWidgetVisible(ui.show3DWidget->isChecked());
+
+#ifdef Q_OS_MAC
+  ui.pickLabel->setText(ui.pickLabel->text().replace("Ctrl", "Cmd"));
+#endif
+
+  if (vtkSMProperty* ctr = smgroup->GetProperty("CenterPoint"))
+  {
+    ui.labelCenter->setText(tr(ctr->GetXMLLabel()));
+    this->addPropertyLink(ui.centerX, "text2", SIGNAL(textChangedAndEditingFinished()), ctr, 0);
+    this->addPropertyLink(ui.centerY, "text2", SIGNAL(textChangedAndEditingFinished()), ctr, 1);
+    this->addPropertyLink(ui.centerZ, "text2", SIGNAL(textChangedAndEditingFinished()), ctr, 2);
+    ui.labelCenter->setText(ctr->GetXMLLabel());
+  }
+  else
+  {
+    qCritical("Missing required property for function 'CenterPoint'.");
+  }
+
+  if (vtkSMProperty* nrm = smgroup->GetProperty("Normal"))
+  {
+    ui.labelNormal->setText(tr(nrm->GetXMLLabel()));
+    this->addPropertyLink(ui.normalX, "text2", SIGNAL(textChangedAndEditingFinished()), nrm, 0);
+    this->addPropertyLink(ui.normalY, "text2", SIGNAL(textChangedAndEditingFinished()), nrm, 1);
+    this->addPropertyLink(ui.normalZ, "text2", SIGNAL(textChangedAndEditingFinished()), nrm, 2);
+    ui.labelNormal->setText(nrm->GetXMLLabel());
+  }
+  else
+  {
+    qCritical("Missing required property for function 'Normal'.");
+  }
+
+  if (vtkSMProperty* rad = smgroup->GetProperty("Radius"))
+  {
+    ui.labelRadius->setText(tr(rad->GetXMLLabel()));
+    this->addPropertyLink(ui.radius, "text2", SIGNAL(textChangedAndEditingFinished()), rad, 0);
+  }
+  else
+  {
+    qCritical("Missing required property for function 'Radius'.");
+  }
+
+  pqPointPickingHelper* pickHelper = new pqPointPickingHelper(QKeySequence(tr("P")), false, this);
+  QObject::connect(this, &pqDiskPropertyWidget::viewChanged, pickHelper, &pqPointPickingHelper::setView);
+  QObject::connect(pickHelper, &pqPointPickingHelper::pick, this, &pqDiskPropertyWidget::pick);
+  pqPointPickingVisibilityHelper<pqPointPickingHelper>{ *this, *pickHelper };
+
+  pqPointPickingHelper* pickHelper2 =
+    new pqPointPickingHelper(QKeySequence(tr("Ctrl+P")), true, this);
+  QObject::connect(this, &pqDiskPropertyWidget::viewChanged, pickHelper2, &pqPointPickingHelper::setView);
+  QObject::connect(pickHelper2, &pqPointPickingHelper::pick, this, &pqDiskPropertyWidget::pick);
+  pqPointPickingVisibilityHelper<pqPointPickingHelper>{ *this, *pickHelper2 };
+
+  pqCoreUtilities::connect(
+    this->widgetProxy(), vtkCommand::PropertyModifiedEvent, this, SLOT(updateInformationLabels()));
+  this->updateInformationLabels();
+}
+
+pqDiskPropertyWidget::~pqDiskPropertyWidget() = default;
+
+void pqDiskPropertyWidget::pick(double wx, double wy, double wz)
+{
+  double position[3] = { wx, wy, wz };
+  vtkSMNewWidgetRepresentationProxy* wdgProxy = this->widgetProxy();
+  vtkSMPropertyHelper(wdgProxy, "CenterPoint").Set(position, 3);
+  wdgProxy->UpdateVTKObjects();
+  Q_EMIT this->changeAvailable();
+  this->render();
+}
+
+void pqDiskPropertyWidget::updateInformationLabels()
+{
+  // Ui::DiskPropertyWidget& ui = m_p->Ui;
+
+  vtkVector3d ctr, nrm;
+  vtkSMProxy* wproxy = this->widgetProxy();
+  vtkSMPropertyHelper(wproxy, "CenterPoint").Get(ctr.GetData(), 3);
+  vtkSMPropertyHelper(wproxy, "Normal").Get(nrm.GetData(), 3);
+}
+
+void pqDiskPropertyWidget::placeWidget()
+{
+  // Nothing to do?
+}
diff --git a/smtk/extension/paraview/widgets/pqDiskPropertyWidget.h b/smtk/extension/paraview/widgets/pqDiskPropertyWidget.h
new file mode 100644
index 0000000000000000000000000000000000000000..1b0c3b0ec3fb06380b0d1f92bf46017dbf8a3f92
--- /dev/null
+++ b/smtk/extension/paraview/widgets/pqDiskPropertyWidget.h
@@ -0,0 +1,40 @@
+//=========================================================================
+//  Copyright (c) Kitware, Inc.
+//  All rights reserved.
+//  See LICENSE.txt for details.
+//
+//  This software is distributed WITHOUT ANY WARRANTY; without even
+//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+//  PURPOSE.  See the above copyright notice for more information.
+//=========================================================================
+#ifndef smtk_extension_paraview_widgets_pqDiskPropertyWidget_h
+#define smtk_extension_paraview_widgets_pqDiskPropertyWidget_h
+
+#include "smtk/extension/paraview/widgets/pqSMTKInteractivePropertyWidget.h"
+#include "smtk/extension/paraview/widgets/smtkPQWidgetsExtModule.h"
+
+class SMTKPQWIDGETSEXT_EXPORT pqDiskPropertyWidget : public pqSMTKInteractivePropertyWidget
+{
+  Q_OBJECT
+  using Superclass = pqSMTKInteractivePropertyWidget;
+
+public:
+  pqDiskPropertyWidget(vtkSMProxy* proxy, vtkSMPropertyGroup* smgroup, QWidget* parent = nullptr);
+  ~pqDiskPropertyWidget() override;
+
+public Q_SLOTS:
+  void pick(double, double, double);
+
+protected Q_SLOTS:
+  void updateInformationLabels();
+  void placeWidget() override;
+
+protected:
+  class Internals;
+  Internals* m_p;
+
+private:
+  Q_DISABLE_COPY(pqDiskPropertyWidget);
+};
+
+#endif // smtk_extension_paraview_widgets_pqDiskPropertyWidget_h
diff --git a/smtk/extension/paraview/widgets/pqSMTKDiskItemWidget.cxx b/smtk/extension/paraview/widgets/pqSMTKDiskItemWidget.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..f004ab44124c1c7424a664e99f848068f5c5e195
--- /dev/null
+++ b/smtk/extension/paraview/widgets/pqSMTKDiskItemWidget.cxx
@@ -0,0 +1,230 @@
+//=========================================================================
+//  Copyright (c) Kitware, Inc.
+//  All rights reserved.
+//  See LICENSE.txt for details.
+//
+//  This software is distributed WITHOUT ANY WARRANTY; without even
+//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+//  PURPOSE.  See the above copyright notice for more information.
+//=========================================================================
+#include "smtk/extension/paraview/widgets/pqSMTKDiskItemWidget.h"
+#include "smtk/extension/paraview/widgets/pqDiskPropertyWidget.h"
+#include "smtk/extension/paraview/widgets/pqSMTKAttributeItemWidgetP.h"
+
+#include "smtk/attribute/DoubleItem.h"
+#include "smtk/attribute/GroupItem.h"
+
+#include "smtk/io/Logger.h"
+
+#include "pqActiveObjects.h"
+#include "pqApplicationCore.h"
+#include "pqDataRepresentation.h"
+#include "pqImplicitPlanePropertyWidget.h"
+#include "pqObjectBuilder.h"
+#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 qtAttributeItemInfo = smtk::extension::qtAttributeItemInfo;
+
+pqSMTKDiskItemWidget::pqSMTKDiskItemWidget(
+  const smtk::extension::qtAttributeItemInfo& info,
+  Qt::Orientation orient)
+  : pqSMTKAttributeItemWidget(info, orient)
+{
+  this->createWidget();
+}
+
+pqSMTKDiskItemWidget::~pqSMTKDiskItemWidget() = default;
+
+qtItem* pqSMTKDiskItemWidget::createDiskItemWidget(const qtAttributeItemInfo& info)
+{
+  return new pqSMTKDiskItemWidget(info);
+}
+
+bool pqSMTKDiskItemWidget::createProxyAndWidget(
+  vtkSMProxy*& proxy,
+  pqInteractivePropertyWidget*& widget)
+{
+  ItemBindings binding;
+  std::vector<smtk::attribute::DoubleItemPtr> items;
+  bool haveItems = this->fetchDiskItems(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", "ImplicitDisk", server, "");
+  if (!proxy)
+  {
+    return false;
+  }
+  auto* diskWidget = new pqDiskPropertyWidget(proxy, proxy->GetPropertyGroup(0));
+  widget = diskWidget;
+
+  // II. Initialize the properties.
+  m_p->m_pvwidget = widget;
+  this->updateWidgetFromItem();
+  auto* widgetProxy = widget->widgetProxy();
+  widgetProxy->UpdateVTKObjects();
+  // vtkSMPropertyHelper(widgetProxy, "RotationEnabled").Set(false);
+
+  return widget != nullptr;
+}
+
+bool pqSMTKDiskItemWidget::updateItemFromWidgetInternal()
+{
+  vtkSMNewWidgetRepresentationProxy* widget = m_p->m_pvwidget->widgetProxy();
+  std::vector<smtk::attribute::DoubleItemPtr> items;
+  ItemBindings binding;
+  if (!this->fetchDiskItems(binding, items))
+  {
+    smtkErrorMacro(
+      smtk::io::Logger::instance(),
+      "Item widget has an update but the item(s) do not exist or are not sized properly.");
+    return false;
+  }
+
+  // Values held by widget
+  vtkVector3d ctr;
+  vtkVector3d nrm;
+  double rad;
+  vtkSMPropertyHelper ctrHelper(widget, "CenterPoint");
+  vtkSMPropertyHelper nrmHelper(widget, "Normal");
+  vtkSMPropertyHelper radHelper(widget, "Radius");
+  ctrHelper.Get(ctr.GetData(), 3);
+  nrmHelper.Get(nrm.GetData(), 3);
+  radHelper.Get(&rad, 1);
+  bool didChange = false;
+
+  // Current values held in items:
+  vtkVector3d curPt0;
+  vtkVector3d curPt1;
+  double curRad;
+
+  // Translate widget values to item values and fetch current item values:
+  curPt0 = vtkVector3d(&(*items[0]->begin()));
+  curPt1 = vtkVector3d(&(*items[1]->begin()));
+  curRad = *items[2]->begin();
+  switch (binding)
+  {
+    case ItemBindings::DiskPointsRadii:
+      if (curPt0 != ctr || curPt1 != nrm || curRad != rad)
+      {
+        didChange = true;
+        items[0]->setValues(ctr.GetData(), ctr.GetData() + 3);
+        items[1]->setValues(nrm.GetData(), nrm.GetData() + 3);
+        items[2]->setValue(rad);
+      }
+      break;
+    case ItemBindings::Invalid:
+    default:
+      smtkErrorMacro(smtk::io::Logger::instance(), "Unable to determine item binding.");
+      break;
+  }
+
+  return didChange;
+}
+
+bool pqSMTKDiskItemWidget::updateWidgetFromItemInternal()
+{
+  vtkSMNewWidgetRepresentationProxy* widget = m_p->m_pvwidget->widgetProxy();
+  std::vector<smtk::attribute::DoubleItemPtr> items;
+  ItemBindings binding;
+  if (!this->fetchDiskItems(binding, items))
+  {
+    smtkErrorMacro(
+      smtk::io::Logger::instance(),
+      "Item signaled an update but the item(s) do not exist or are not sized properly.");
+    return false;
+  }
+
+  // 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:
+  vtkVector3d ctr(&(*items[0]->begin()));
+  vtkVector3d nrm(&(*items[1]->begin()));
+  double radius = items[2]->value(0);
+  vtkSMPropertyHelper(widget, "CenterPoint").Set(ctr.GetData(), 3);
+  vtkSMPropertyHelper(widget, "Normal").Set(nrm.GetData(), 3);
+  vtkSMPropertyHelper(widget, "Radius").Set(&radius, 1);
+  switch (binding)
+  {
+    case ItemBindings::DiskPointsRadii:
+      break;
+    case ItemBindings::Invalid:
+    default:
+    {
+      smtkErrorMacro(smtk::io::Logger::instance(), "Unhandled item binding.");
+    }
+    break;
+  }
+  return true; // TODO: determine whether values were changed to avoid unnecessary renders.
+}
+
+bool pqSMTKDiskItemWidget::fetchDiskItems(
+  ItemBindings& binding,
+  std::vector<smtk::attribute::DoubleItemPtr>& items)
+{
+  items.clear();
+
+  // 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() < 3)
+  {
+    smtkErrorMacro(
+      smtk::io::Logger::instance(), "Expected a group item with 1 group of 3 or more items.");
+    return false;
+  }
+
+  // Find items in the group based on names in the configuration info:
+  // ctr, nrm, rad
+  std::string ctrItemName;
+  std::string nrmItemName;
+  std::string radItemName;
+  if (!m_itemInfo.component().attribute("Center", ctrItemName))
+  {
+    ctrItemName = "Center";
+  }
+  if (!m_itemInfo.component().attribute("Normal", nrmItemName))
+  {
+    nrmItemName = "Normal";
+  }
+  if (!m_itemInfo.component().attribute("Radius", radItemName))
+  {
+    radItemName = "Radius";
+  }
+  auto ctrItem = groupItem->findAs<smtk::attribute::DoubleItem>(ctrItemName);
+  auto nrmItem = groupItem->findAs<smtk::attribute::DoubleItem>(nrmItemName);
+  auto radItem = groupItem->findAs<smtk::attribute::DoubleItem>(radItemName);
+
+  if (
+    ctrItem && ctrItem->numberOfValues() == 3 && nrmItem && nrmItem->numberOfValues() == 3 &&
+    radItem && radItem->numberOfValues() == 1)
+  {
+    items.push_back(ctrItem);
+    items.push_back(nrmItem);
+    items.push_back(radItem);
+    binding = ItemBindings::DiskPointsRadii;
+    return true;
+  }
+
+  binding = ItemBindings::Invalid;
+  return false;
+}
diff --git a/smtk/extension/paraview/widgets/pqSMTKDiskItemWidget.h b/smtk/extension/paraview/widgets/pqSMTKDiskItemWidget.h
new file mode 100644
index 0000000000000000000000000000000000000000..e78312bafc145a7968ebcd631d14dc662f306199
--- /dev/null
+++ b/smtk/extension/paraview/widgets/pqSMTKDiskItemWidget.h
@@ -0,0 +1,74 @@
+//=========================================================================
+//  Copyright (c) Kitware, Inc.
+//  All rights reserved.
+//  See LICENSE.txt for details.
+//
+//  This software is distributed WITHOUT ANY WARRANTY; without even
+//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+//  PURPOSE.  See the above copyright notice for more information.
+//=========================================================================
+#ifndef smtk_extension_paraview_widgets_pqSMTKDiskItemWidget_h
+#define smtk_extension_paraview_widgets_pqSMTKDiskItemWidget_h
+
+#include "smtk/extension/paraview/widgets/pqSMTKAttributeItemWidget.h"
+
+/**\brief Display an interactive disk widget.
+  *
+  * The widget currently accepts a center point, normal, and radius
+  * that defines 2-d disk embedded in 3-d.
+  *
+  * In the future, other item types (such as a GroupItem holding
+  * children specifying 3 points on the circumference of a disk;
+  * or a center point and point on the circumferece; or a pair of
+  * diametrically opposed points and a normal) may be supported.
+  *
+  * Currently, there is no support to initialize the placement to
+  * a given set of bounds; if you use this widget as part of the
+  * user interface to an operation, implement a configure() method
+  * on the operation to contextually place the widget based on
+  * associations.
+  */
+class SMTKPQWIDGETSEXT_EXPORT pqSMTKDiskItemWidget : public pqSMTKAttributeItemWidget
+{
+  Q_OBJECT
+public:
+  pqSMTKDiskItemWidget(
+    const smtk::extension::qtAttributeItemInfo& info,
+    Qt::Orientation orient = Qt::Horizontal);
+  ~pqSMTKDiskItemWidget() override;
+
+  /// Create an instance of the widget that allows users to define a disk.
+  static qtItem* createDiskItemWidget(const qtAttributeItemInfo& info);
+
+  bool createProxyAndWidget(vtkSMProxy*& proxy, pqInteractivePropertyWidget*& widget) override;
+
+protected Q_SLOTS:
+  /// Retrieve property values from ParaView proxy and store them in the attribute's Item.
+  bool updateItemFromWidgetInternal() override;
+  /// Retrieve property values from the attribute's Item and update the ParaView proxy.
+  bool updateWidgetFromItemInternal() override;
+
+protected:
+  /// Describe how an attribute's items specify a disk or cylinder.
+  enum class ItemBindings
+  {
+    /// 2 items with 3 values, 1 items with 1 value (cx, cy, cz, nx, ny, nz, rr).
+    DiskPointsRadii,
+    /// No consistent set of items detected.
+    Invalid
+  };
+  /**\brief Starting with the widget's assigned item (which must currently
+    *       be a GroupItem), determine and return bound items.
+    *
+    * The named item must be a Group holding items as called out by
+    * one of the valid ItemBindings enumerants.
+    * The items inside the Group must currently be Double items.
+    *
+    * If errors (such as a lack of matching item names or an
+    * unexpected number of values per item) are encountered,
+    * this method returns false.
+    */
+  bool fetchDiskItems(ItemBindings& binding, std::vector<smtk::attribute::DoubleItemPtr>& items);
+};
+
+#endif // smtk_extension_paraview_widgets_pqSMTKDiskItemWidget_h
diff --git a/smtk/extension/paraview/widgets/resources/pqDiskPropertyWidget.ui b/smtk/extension/paraview/widgets/resources/pqDiskPropertyWidget.ui
new file mode 100644
index 0000000000000000000000000000000000000000..3aeb2a730bb8977a6dd48c1479204a7ba56f335a
--- /dev/null
+++ b/smtk/extension/paraview/widgets/resources/pqDiskPropertyWidget.ui
@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>DiskPropertyWidget</class>
+ <widget class="QWidget" name="DiskPropertyWidget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>341</width>
+    <height>155</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Form</string>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <property name="spacing">
+    <number>2</number>
+   </property>
+   <item row="5" column="1">
+    <widget class="pqDoubleLineEdit" name="normalX"/>
+   </item>
+   <item row="5" column="0">
+    <widget class="QLabel" name="labelNormal">
+     <property name="text">
+      <string>Normal</string>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item row="7" column="0" colspan="4">
+    <widget class="QLabel" name="pickLabel">
+     <property name="font">
+      <font>
+       <weight>75</weight>
+       <bold>true</bold>
+      </font>
+     </property>
+     <property name="text">
+      <string>Note: Use 'P' to move the center to the pointer or 'Ctrl+P' to snap to the closest mesh point. </string>
+     </property>
+     <property name="alignment">
+      <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="0" colspan="3">
+    <widget class="QCheckBox" name="show3DWidget">
+     <property name="text">
+      <string>Show disk</string>
+     </property>
+     <property name="checked">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item row="6" column="0">
+    <widget class="QLabel" name="labelRadius">
+     <property name="text">
+      <string>Radius</string>
+     </property>
+    </widget>
+   </item>
+   <item row="4" column="3">
+    <widget class="pqDoubleLineEdit" name="centerZ"/>
+   </item>
+   <item row="5" column="3">
+    <widget class="pqDoubleLineEdit" name="normalZ"/>
+   </item>
+   <item row="4" column="1">
+    <widget class="pqDoubleLineEdit" name="centerX"/>
+   </item>
+   <item row="4" column="0">
+    <widget class="QLabel" name="labelCenter">
+     <property name="text">
+      <string>Center</string>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item row="6" column="1">
+    <widget class="pqDoubleLineEdit" name="radius"/>
+   </item>
+   <item row="5" column="2">
+    <widget class="pqDoubleLineEdit" name="normalY"/>
+   </item>
+   <item row="4" column="2">
+    <widget class="pqDoubleLineEdit" name="centerY"/>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>pqDoubleLineEdit</class>
+   <extends>QLineEdit</extends>
+   <header>pqDoubleLineEdit.h</header>
+  </customwidget>
+ </customwidgets>
+ <tabstops>
+  <tabstop>show3DWidget</tabstop>
+  <tabstop>centerX</tabstop>
+  <tabstop>centerY</tabstop>
+  <tabstop>centerZ</tabstop>
+  <tabstop>normalX</tabstop>
+  <tabstop>normalY</tabstop>
+  <tabstop>normalZ</tabstop>
+  <tabstop>radius</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/smtk/extension/qt/diagram/qtBaseTaskNode.cxx b/smtk/extension/qt/diagram/qtBaseTaskNode.cxx
index 22a79b75101862395175d3b4e4de19aa2390723e..7fdbb840f88b8d9e5b6bb01c928b111049ce112b 100644
--- a/smtk/extension/qt/diagram/qtBaseTaskNode.cxx
+++ b/smtk/extension/qt/diagram/qtBaseTaskNode.cxx
@@ -91,7 +91,7 @@ void qtBaseTaskNode::updateTaskState(smtk::task::State prev, smtk::task::State n
   (void)active;
   // Update the tool tip with diagnostic information
   smtk::task::Task::InformationOptions opt;
-  opt.m_includeTitle = false;
+  opt.m_includeTitle = true;
   this->setToolTip(QString::fromStdString(m_task->information(opt)));
 }
 
@@ -101,7 +101,7 @@ void qtBaseTaskNode::dataUpdated()
 
   // Update the tool tip with diagnostic information
   smtk::task::Task::InformationOptions opt;
-  opt.m_includeTitle = false;
+  opt.m_includeTitle = true;
   this->setToolTip(QString::fromStdString(m_task->information(opt)));
 }
 
diff --git a/smtk/extension/qt/qtAttribute.cxx b/smtk/extension/qt/qtAttribute.cxx
index 17fc3c95f5cf58c454a083666ffcaa62f80310a5..d5ec6b31cb715f6e50f791db3e89726267b7458e 100644
--- a/smtk/extension/qt/qtAttribute.cxx
+++ b/smtk/extension/qt/qtAttribute.cxx
@@ -286,7 +286,7 @@ void qtAttribute::createBasicLayout(bool includeAssociations)
       QString baseUnits = (att->definition()->units() == "*") ? att->units().c_str()
                                                               : att->definition()->units().c_str();
       auto* unitsWidget =
-        new qtUnitsLineEdit(baseUnits, att->definition()->unitsSystem(), uiManager, unitFrame);
+        new qtUnitsLineEdit(baseUnits, att->definition()->unitSystem(), uiManager, unitFrame);
       std::string unitsWidgetName = att->name() + "UnitsLineWidget";
       unitsWidget->setObjectName(unitsWidgetName.c_str());
       unitsLayout->addWidget(unitsWidget);
diff --git a/smtk/extension/qt/qtDoubleUnitsLineEdit.cxx b/smtk/extension/qt/qtDoubleUnitsLineEdit.cxx
index 346e28ed1c69e134a76a899dd896a1b337819e2a..e2180bf1b2e0c4b0acde749005ee1c58d2590fdc 100644
--- a/smtk/extension/qt/qtDoubleUnitsLineEdit.cxx
+++ b/smtk/extension/qt/qtDoubleUnitsLineEdit.cxx
@@ -167,7 +167,7 @@ qtDoubleUnitsLineEdit* qtDoubleUnitsLineEdit::checkAndCreate(
   }
 
   // Get units system
-  auto unitSystem = dItem->definition()->unitsSystem();
+  auto unitSystem = dItem->definition()->unitSystem();
   if (unitSystem == nullptr)
   {
     return nullptr;
@@ -302,7 +302,7 @@ void qtDoubleUnitsLineEdit::onTextEdited()
 
   auto dItem = m_inputsItem->itemAs<DoubleItem>();
   auto dUnits = dItem->units();
-  auto unitSystem = dItem->definition()->unitsSystem();
+  auto unitSystem = dItem->definition()->unitSystem();
 
   // Parsing the Item's unit string
   bool parsedOK = false;
diff --git a/smtk/extension/qt/qtInputsItem.cxx b/smtk/extension/qt/qtInputsItem.cxx
index 7b70552d52cbbe44aa1b81fa0755ac1ad74210ae..8589a48f5e32a4043a86187ddf9520ea579a3e0a 100644
--- a/smtk/extension/qt/qtInputsItem.cxx
+++ b/smtk/extension/qt/qtInputsItem.cxx
@@ -744,9 +744,9 @@ void qtInputsItem::showExpressionResultWidgets(
   auto itemUnits = item->units();
   if (!itemUnits.empty())
   {
-    auto unitsSystem = item->definition()->unitsSystem();
+    auto unitSystem = item->definition()->unitSystem();
     bool parsed = false;
-    unitsSystem->unit(itemUnits, &parsed);
+    unitSystem->unit(itemUnits, &parsed);
     if (parsed)
     {
       displayText = QString("%1 %2").arg(text).arg(itemUnits.c_str());
@@ -981,11 +981,11 @@ QFrame* qtInputsItem::createLabelFrame(
     if (addUnitsLabel && !vitemDef->isDiscrete())
     {
       // Check if units are "valid"
-      const auto& unitsSystem = vitemDef->unitsSystem();
-      if (unitsSystem)
+      const auto& unitSystem = vitemDef->unitSystem();
+      if (unitSystem)
       {
         bool unitsParsed = false;
-        units::Unit defUnit = unitsSystem->unit(valUnits, &unitsParsed);
+        units::Unit defUnit = unitSystem->unit(valUnits, &unitsParsed);
         addUnitsLabel = addUnitsLabel && (!unitsParsed);
       }
     }
diff --git a/smtk/extension/qt/qtWorkletModel.cxx b/smtk/extension/qt/qtWorkletModel.cxx
index e89a43bf7d514cbf7635e2df39c1d559ea50ab0d..245ee18081c78e4593e9ed2def51aa9cd594dd6a 100644
--- a/smtk/extension/qt/qtWorkletModel.cxx
+++ b/smtk/extension/qt/qtWorkletModel.cxx
@@ -283,12 +283,18 @@ void qtWorkletModel::workletUpdate(
   // components.  So use a set to assemble all of the uniquely created worklets and then
   // insert them all at the same time.
   std::set<smtk::task::Worklet::Ptr> workletsToBeInserted;
+  std::set<smtk::task::Worklet::Ptr> allWorklets(m_worklets.begin(), m_worklets.end());
   smtk::resource::Component::Visitor visitor =
-    [this, workletTypeName, &workletsToBeInserted](const smtk::resource::Component::Ptr& comp) {
+    [this, workletTypeName, &allWorklets, &workletsToBeInserted](
+      const smtk::resource::Component::Ptr& comp) {
       if (comp->matchesType(workletTypeName))
       {
         auto worklet = std::dynamic_pointer_cast<smtk::task::Worklet>(comp);
-        m_worklets.push_back(worklet);
+        if (allWorklets.find(worklet) == allWorklets.end())
+        {
+          m_worklets.push_back(worklet);
+          allWorklets.insert(worklet);
+        }
         // Lets see if the worklet should be presented?
         if (m_parentTask)
         {
@@ -324,7 +330,11 @@ void qtWorkletModel::workletUpdate(
   {
     if (auto worklet = std::dynamic_pointer_cast<smtk::task::Worklet>(comp))
     {
-      m_worklets.push_back(worklet);
+      if (allWorklets.find(worklet) == allWorklets.end())
+      {
+        m_worklets.push_back(worklet);
+        allWorklets.insert(worklet);
+      }
       // Lets see if the worklet should be presented?
       if (m_parentTask)
       {
diff --git a/smtk/extension/vtk/markup/Geometry.cxx b/smtk/extension/vtk/markup/Geometry.cxx
index fb0700fe050b4f4e1b6e33f629a48a1f32f39562..8239b19679f2e0c2ba62b32011c8202a128d2cfa 100644
--- a/smtk/extension/vtk/markup/Geometry.cxx
+++ b/smtk/extension/vtk/markup/Geometry.cxx
@@ -61,6 +61,16 @@ void Geometry::queryGeometry(const smtk::resource::PersistentObject::Ptr& obj, C
 
   entry.m_geometry = nullptr;
 
+  // Check component-wide blanking
+  if (auto* spatial = dynamic_cast<smtk::markup::SpatialData*>(component.get()))
+  {
+    if (spatial->isBlanked())
+    {
+      entry.m_generation = Invalid;
+      return;
+    }
+  }
+
   if (auto* image = dynamic_cast<smtk::markup::ImageData*>(component.get()))
   {
     auto shape = image->shapeData();
diff --git a/smtk/extension/vtk/source/CMakeLists.txt b/smtk/extension/vtk/source/CMakeLists.txt
index 1f7d2a9221a401198d2f7d5bc762ffbe5b79d09a..040407f99ffc23339241f18d38db2353e69bca81 100644
--- a/smtk/extension/vtk/source/CMakeLists.txt
+++ b/smtk/extension/vtk/source/CMakeLists.txt
@@ -7,7 +7,9 @@ set(classes
   vtkAttributeMultiBlockSource
   vtkCmbLayeredConeSource
   vtkConeFrustum
+  vtkDisk
   vtkImplicitConeFrustum
+  vtkImplicitDisk
   vtkModelMultiBlockSource
   vtkModelView
   vtkResourceMultiBlockSource)
diff --git a/smtk/extension/vtk/source/vtkDisk.cxx b/smtk/extension/vtk/source/vtkDisk.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..c8663d85b9f314e1494d468456e09109ff077507
--- /dev/null
+++ b/smtk/extension/vtk/source/vtkDisk.cxx
@@ -0,0 +1,185 @@
+//=========================================================================
+//  Copyright (c) Kitware, Inc.
+//  All rights reserved.
+//  See LICENSE.txt for details.
+//
+//  This software is distributed WITHOUT ANY WARRANTY; without even
+//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+//  PURPOSE.  See the above copyright notice for more information.
+//=========================================================================
+#include "smtk/extension/vtk/source/vtkDisk.h"
+
+#include "vtkCellArray.h"
+#include "vtkDoubleArray.h"
+#include "vtkFloatArray.h"
+#include "vtkIdTypeArray.h"
+#include "vtkInformation.h"
+#include "vtkInformationVector.h"
+#include "vtkMath.h"
+#include "vtkNew.h"
+#include "vtkObjectFactory.h"
+#include "vtkPointData.h"
+#include "vtkPolyData.h"
+#include "vtkStreamingDemandDrivenPipeline.h"
+#include "vtkTransform.h"
+#include "vtkVector.h"
+#include "vtkVectorOperators.h"
+
+#include <cmath>
+
+vtkStandardNewMacro(vtkDisk);
+
+vtkDisk::vtkDisk(int res)
+  : Resolution(res <= 3 ? 3 : res)
+{
+  this->SetNumberOfInputPorts(0);
+  this->SetNumberOfOutputPorts(NumberOfOutputs);
+}
+
+vtkDisk::~vtkDisk() = default;
+
+void vtkDisk::PrintSelf(ostream& os, vtkIndent indent)
+{
+  this->Superclass::PrintSelf(os, indent);
+
+  os << indent << "CenterPoint: (" << this->CenterPoint[0] << ", " << this->CenterPoint[1] << ", "
+     << this->CenterPoint[2] << ")\n";
+  os << indent << "Radius: " << this->Radius << "\n";
+  os << indent << "Normal: (" << this->Normal[0] << ", " << this->Normal[1] << ", "
+     << this->Normal[2] << ")\n";
+  os << indent << "Resolution: " << this->Resolution << "\n";
+  os << indent << "Output Points Precision: " << this->OutputPointsPrecision << "\n";
+}
+
+int vtkDisk::RequestData(
+  vtkInformation* /*request*/,
+  vtkInformationVector** /*inputVector*/,
+  vtkInformationVector* outputVector)
+{
+  // vtkInformation* outInfo = outputVector->GetInformationObject(0);
+  // clang-format off
+  vtkPolyData* diskFace = vtkPolyData::GetData(outputVector, static_cast<int>(OutputPorts::DiskFace));
+  vtkPolyData* normEdge = vtkPolyData::GetData(outputVector, static_cast<int>(OutputPorts::DiskNormal));
+  vtkPolyData* diskEdge = vtkPolyData::GetData(outputVector, static_cast<int>(OutputPorts::DiskEdge));
+  vtkPolyData* ctrVert = vtkPolyData::GetData(outputVector, static_cast<int>(OutputPorts::CenterVertex));
+  if (!diskFace || !normEdge || !diskEdge || !ctrVert)
+  {
+    vtkErrorMacro("No output provided.");
+    return 0;
+  }
+  diskFace->Initialize();
+  normEdge->Initialize();
+  diskEdge->Initialize();
+  ctrVert->Initialize();
+
+  vtkVector3d p0(this->CenterPoint);
+  vtkVector3d nn(this->Normal);
+
+  vtkDebugMacro("DiskSource Executing");
+
+  // Get axes xx and yy in the plane normal to the disk axis
+  vtkVector3d axis = nn.Normalized();
+  vtkVector3d p1 = p0 + axis * this->Radius;
+  vtkVector3d px(1., 0., 0.);
+  vtkVector3d py(0., 1., 0.);
+  vtkVector3d yy = axis.Cross(px);
+  if (yy.Norm() < 1e-10)
+  {
+    yy = axis.Cross(py);
+  }
+  yy.Normalize();
+  vtkVector3d xx = yy.Cross(axis).Normalized();
+
+  // I. Compute the point coordinates.
+  vtkNew<vtkPoints> facePts;
+  facePts->SetDataType(
+    this->OutputPointsPrecision == vtkAlgorithm::DOUBLE_PRECISION ? VTK_DOUBLE : VTK_FLOAT);
+  facePts->SetNumberOfPoints(1 + this->Resolution);
+  facePts->SetPoint(this->Resolution, p0.GetData());
+
+  vtkNew<vtkPoints> normPts;
+  normPts->SetDataType(
+    this->OutputPointsPrecision == vtkAlgorithm::DOUBLE_PRECISION ? VTK_DOUBLE : VTK_FLOAT);
+  normPts->Allocate(2);
+  normPts->InsertNextPoint(p0.GetData());
+  normPts->InsertNextPoint(p1.GetData());
+
+  vtkNew<vtkPoints> edgePts;
+  edgePts->SetDataType(
+    this->OutputPointsPrecision == vtkAlgorithm::DOUBLE_PRECISION ? VTK_DOUBLE : VTK_FLOAT);
+  edgePts->Allocate(this->Resolution);
+
+  vtkNew<vtkPoints> centerPoint;
+  centerPoint->SetDataType(
+    this->OutputPointsPrecision == vtkAlgorithm::DOUBLE_PRECISION ? VTK_DOUBLE : VTK_FLOAT);
+  centerPoint->Allocate(1);
+  centerPoint->InsertNextPoint(p0.GetData());
+
+  vtkNew<vtkDoubleArray> faceNrm; // point normals
+  faceNrm->SetNumberOfComponents(3);
+  faceNrm->SetName("normals");
+  faceNrm->SetNumberOfTuples(facePts->GetNumberOfPoints());
+  faceNrm->FillComponent(0, nn[0]);
+  faceNrm->FillComponent(1, nn[1]);
+  faceNrm->FillComponent(2, nn[2]);
+
+  double angle = 2.0 * vtkMath::Pi() / this->Resolution;
+
+  // Disk face and edge points
+  for (int ii = 0; ii < this->Resolution; ++ii)
+  {
+    double theta = angle * ii;
+    vtkVector3d pt = p0 + this->Radius * (xx * cos(theta) + yy * sin(theta));
+    facePts->SetPoint(ii, pt.GetData());
+    edgePts->InsertNextPoint(pt.GetData());
+  }
+
+  // II. Prepare face connectivity
+  vtkIdType numConnEdge = 2 + this->Resolution;
+  vtkIdType numConnFace = (3 + 1) * this->Resolution;
+
+  vtkNew<vtkCellArray> facePolys;
+  vtkNew<vtkCellArray> edgeLines;
+  vtkNew<vtkCellArray> normLines;
+  vtkNew<vtkCellArray> centerVertex;
+  vtkNew<vtkIdTypeArray> faceConn;
+  vtkNew<vtkIdTypeArray> edgeConn;
+  vtkNew<vtkIdTypeArray> normConn;
+  faceConn->Allocate(numConnFace);
+  edgeConn->Allocate(numConnEdge);
+  normConn->SetNumberOfTuples(3);
+  normLines->Allocate(3);
+  vtkIdType zero = 0;
+  centerVertex->InsertNextCell(1, &zero);
+
+  // Disk face connectivity (triangles).
+  // The last point is the center of the disk.
+  edgeConn->InsertNextValue(this->Resolution);
+  for (int ii = 0; ii < this->Resolution; ++ii)
+  {
+    edgeConn->InsertNextValue(ii);
+    faceConn->InsertNextValue(3);
+    faceConn->InsertNextValue(this->Resolution);
+    faceConn->InsertNextValue(ii);
+    faceConn->InsertNextValue((ii + 1) % this->Resolution);
+  }
+  normConn->SetValue(0, 2);
+  normConn->SetValue(1, 0);
+  normConn->SetValue(2, 1);
+
+  facePolys->SetCells(this->Resolution, faceConn);
+  edgeLines->SetCells(1, edgeConn);
+  normLines->SetCells(1, normConn);
+
+  diskFace->SetPoints(facePts);
+  diskFace->SetPolys(facePolys);
+  diskFace->GetPointData()->SetNormals(faceNrm);
+  normEdge->SetPoints(normPts);
+  normEdge->SetLines(normLines);
+  diskEdge->SetPoints(edgePts);
+  diskEdge->SetLines(edgeLines);
+  ctrVert->SetPoints(centerPoint);
+  ctrVert->SetVerts(centerVertex);
+
+  return 1;
+}
diff --git a/smtk/extension/vtk/source/vtkDisk.h b/smtk/extension/vtk/source/vtkDisk.h
new file mode 100644
index 0000000000000000000000000000000000000000..38b1859c94cbc4fe299f5708c9abd538da570455
--- /dev/null
+++ b/smtk/extension/vtk/source/vtkDisk.h
@@ -0,0 +1,121 @@
+//=========================================================================
+//  Copyright (c) Kitware, Inc.
+//  All rights reserved.
+//  See LICENSE.txt for details.
+//
+//  This software is distributed WITHOUT ANY WARRANTY; without even
+//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+//  PURPOSE.  See the above copyright notice for more information.
+//=========================================================================
+#ifndef vtkDisk_h
+#define vtkDisk_h
+
+#include "smtk/extension/vtk/source/vtkImplicitDisk.h" // For ivar
+#include "vtkPolyDataAlgorithm.h"
+
+#include "vtkCell.h" // Needed for VTK_CELL_SIZE
+
+/**
+ * @class   vtkDisk
+ * @brief   Generate a polygonal approximation to a disk
+ *
+ * vtkDisk creates a disk with a given center, normal, and radius.
+ * By default, point 1 lies at the origin with radius 0.5
+ * and the normal is (0,0,1).
+ * The resolution specifies the number of points around the circle;
+ * it must be at least 3 and defaults to 32.
+ *
+ * Note that unlike many other source filters, this one is *not*
+ * intended for use as an input to a glyph filter or glyph mapper.
+ * Instead, its purpose is to provide a high-quality visual representation
+ * of a planar disk.
+ * That means it uses more points and cells than is strictly necessary
+ * but is able to provide a better visual quality as a result.
+ */
+class VTKSMTKSOURCEEXT_EXPORT vtkDisk : public vtkPolyDataAlgorithm
+{
+public:
+  vtkTypeMacro(vtkDisk, vtkPolyDataAlgorithm);
+  void PrintSelf(ostream& os, vtkIndent indent) override;
+
+  vtkDisk(const vtkDisk&) = delete;
+  vtkDisk& operator=(const vtkDisk&) = delete;
+
+  /// An enum indexing data present at each output.
+  enum OutputPorts
+  {
+    DiskFace = 0, //!< Triangulation of the disk face.
+    DiskNormal,   //!< Line from the center point along the normal vector (length of disk radius).
+    DiskEdge,     //!< Polyline of the disk boundary.
+    CenterVertex, //!< A single vertex at the center of the bottom face.
+    NumberOfOutputs
+  };
+
+  /**
+   * Construct with default parameters.
+   */
+  static vtkDisk* New();
+
+  //@{
+  /**
+   * Set/get the radius at the bottom of the cone.
+   *
+   * It must be non-negative.
+   */
+  vtkSetClampMacro(Radius, double, 0.0, VTK_DOUBLE_MAX);
+  vtkGetMacro(Radius, double);
+  //@}
+
+  //@{
+  /**
+   * Set/get the bottom point of the cone.
+   * The default is 0,0,0.
+   */
+  vtkSetVector3Macro(CenterPoint, double);
+  vtkGetVectorMacro(CenterPoint, double, 3);
+  //@}
+
+  //@{
+  /**
+   * Set/get the normal to the disk's plane.
+   * The default is 0,0,1.
+   */
+  vtkSetVector3Macro(Normal, double);
+  vtkGetVectorMacro(Normal, double, 3);
+  //@}
+
+  //@{
+  /**
+   * Set/get the number of facets used to represent the conical side-face.
+   *
+   * This defaults to 32 and has a minimum of 3.
+   */
+  vtkSetClampMacro(Resolution, int, 3, VTK_CELL_SIZE);
+  vtkGetMacro(Resolution, int);
+  //@}
+
+  //@{
+  /**
+   * Set/get the desired precision for the output points.
+   * vtkAlgorithm::SINGLE_PRECISION - Output single-precision floating point.
+   * vtkAlgorithm::DOUBLE_PRECISION - Output double-precision floating point.
+   */
+  vtkSetMacro(OutputPointsPrecision, int);
+  vtkGetMacro(OutputPointsPrecision, int);
+  //@}
+
+protected:
+  vtkDisk(int res = 32);
+  ~vtkDisk() override;
+
+  // int RequestInformation(vtkInformation* , vtkInformationVector** , vtkInformationVector* ) override;
+  int RequestData(vtkInformation*, vtkInformationVector**, vtkInformationVector*) override;
+
+  double CenterPoint[3]{ 0, 0, 0 };
+  double Radius{ 0.5 };
+  double Normal[3]{ 0, 0, 1 };
+  int Resolution;
+  int OutputPointsPrecision{ SINGLE_PRECISION };
+};
+
+#endif
diff --git a/smtk/extension/vtk/source/vtkImplicitDisk.cxx b/smtk/extension/vtk/source/vtkImplicitDisk.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..225d5b09715786609f8510bcd243b505ed2435d3
--- /dev/null
+++ b/smtk/extension/vtk/source/vtkImplicitDisk.cxx
@@ -0,0 +1,112 @@
+//=========================================================================
+//  Copyright (c) Kitware, Inc.
+//  All rights reserved.
+//  See LICENSE.txt for details.
+//
+//  This software is distributed WITHOUT ANY WARRANTY; without even
+//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+//  PURPOSE.  See the above copyright notice for more information.
+//=========================================================================
+#include "smtk/extension/vtk/source/vtkImplicitDisk.h"
+
+#include "vtkCone.h"
+#include "vtkMath.h"
+#include "vtkObjectFactory.h"
+#include "vtkPlane.h"
+#include "vtkTransform.h"
+#include "vtkVectorOperators.h"
+
+#include <cmath>
+
+vtkStandardNewMacro(vtkImplicitDisk);
+
+vtkImplicitDisk::vtkImplicitDisk()
+  : CenterPoint{ 0, 0, 0 }
+  , Normal{ 0, 0, 1 }
+{
+}
+
+vtkImplicitDisk::~vtkImplicitDisk() = default;
+
+void vtkImplicitDisk::PrintSelf(ostream& os, vtkIndent indent)
+{
+  this->Superclass::PrintSelf(os, indent);
+
+  os << indent << "CenterPoint: (" << this->CenterPoint << ")\n";
+  os << indent << "Radius: " << this->Radius << "\n";
+  os << indent << "Normal: (" << this->Normal << ")\n";
+}
+
+double vtkImplicitDisk::EvaluateFunction(double xx[3])
+{
+  vtkVector3d pp(xx);
+  vtkVector3d delta = this->CenterPoint - pp;
+  double halfPlane = delta.Dot(this->Normal);
+  delta = delta - halfPlane * this->Normal;
+  double outOfDisk = delta.Norm() - this->Radius;
+  double outOfPlane = halfPlane < 0. ? -halfPlane : halfPlane;
+  // outOfPlane is >= 0.
+  // outOfDisk is < 0. if inside, > 0 if outside.
+  // The signed distance is a combination of these two distances, with its
+  // sign modulated by the sign of outOfDisk.
+  double signedDist = std::copysign(std::sqrt(outOfDisk * outOfDisk + outOfPlane * outOfPlane), outOfDisk);
+  return signedDist;
+}
+
+void vtkImplicitDisk::EvaluateGradient(double xx[3], double gg[3])
+{
+  vtkVector3d pp(xx);
+  vtkVector3d delta = this->CenterPoint - pp;
+  double halfPlane = delta.Dot(this->Normal);
+  delta = delta - halfPlane * this->Normal;
+  double outOfDisk = delta.Norm() - this->Radius;
+  // outOfPlane is (0,0,0) on the plane with normal this->Normal passing
+  // through this->CenterPoint and unit length pointing away from this
+  // plane everywhere else in space.
+  vtkVector3d outOfPlane;
+  if (halfPlane > 0.)
+  {
+    outOfPlane = -this->Normal * halfPlane;
+  }
+  else
+  {
+    outOfPlane = this->Normal * halfPlane;
+  }
+  // inPlane = (0,0,0) at CenterPoint, unit length pointing away from disk center elswhere.
+  vtkVector3d inPlane = delta.Normalized() * (outOfDisk < 0 ? 0. : -outOfDisk);
+  vtkVector3d gradient = inPlane + outOfPlane;
+  gg[0] = gradient[0];
+  gg[1] = gradient[1];
+  gg[2] = gradient[2];
+}
+
+bool vtkImplicitDisk::SetRadius(double radius)
+{
+  if (radius <= 0.0)
+  {
+    return false;
+  }
+  this->Radius = radius;
+  return true;
+}
+
+bool vtkImplicitDisk::SetCenterPoint(const vtkVector3d& pt)
+{
+  if (pt == this->CenterPoint)
+  {
+    return false;
+  }
+  this->CenterPoint = pt;
+  return true;
+}
+
+bool vtkImplicitDisk::SetNormal(const vtkVector3d& normal)
+{
+  auto nn = normal.Normalized();
+  if (nn == this->Normal)
+  {
+    return false;
+  }
+  this->Normal = nn;
+  return true;
+}
diff --git a/smtk/extension/vtk/source/vtkImplicitDisk.h b/smtk/extension/vtk/source/vtkImplicitDisk.h
new file mode 100644
index 0000000000000000000000000000000000000000..eafccf9b8b2be606de5bf3d12fe2af1b9fcb319e
--- /dev/null
+++ b/smtk/extension/vtk/source/vtkImplicitDisk.h
@@ -0,0 +1,121 @@
+//=========================================================================
+//  Copyright (c) Kitware, Inc.
+//  All rights reserved.
+//  See LICENSE.txt for details.
+//
+//  This software is distributed WITHOUT ANY WARRANTY; without even
+//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+//  PURPOSE.  See the above copyright notice for more information.
+//=========================================================================
+#ifndef vtkImplicitDisk_h
+#define vtkImplicitDisk_h
+
+#include "smtk/extension/vtk/source/vtkSMTKSourceExtModule.h" // For export macro
+#include "vtkImplicitFunction.h"
+#include "vtkNew.h"
+#include "vtkVector.h"
+
+class vtkCone;
+class vtkPlane;
+class vtkTransform;
+
+/**
+ * @class vtkImplicitDisk
+ * @brief Generate an implicit function whose 0-isocontour
+ *        is a planar disk.
+ *
+ * vtkImplicitDisk creates a signed distance function for a
+ * disk whose center lies at a given point with the specified
+ * normal vector and radius.
+ *
+ * By default, point 1 (the center) lies at the origin with radius 0.5
+ * and with a z-normal (0, 0, 1).
+ */
+class VTKSMTKSOURCEEXT_EXPORT vtkImplicitDisk : public vtkImplicitFunction
+{
+public:
+  vtkTypeMacro(vtkImplicitDisk, vtkImplicitFunction);
+  void PrintSelf(ostream& os, vtkIndent indent) override;
+
+  vtkImplicitDisk(const vtkImplicitDisk&) = delete;
+  vtkImplicitDisk& operator=(const vtkImplicitDisk&) = delete;
+
+  ///@{
+  /**
+   * Evaluate our implicit function.
+   */
+  using vtkImplicitFunction::EvaluateFunction;
+  double EvaluateFunction(double xx[3]) override;
+  ///@}
+
+  /**
+    * Evaluate gradient of boolean combination.
+    */
+  void EvaluateGradient(double xx[3], double gg[3]) override;
+
+  /**
+   * Construct with default parameters.
+   */
+  static vtkImplicitDisk* New();
+
+  //@{
+  /**
+   * Set/get the radius at the bottom of the cone.
+   *
+   * It must be non-negative.
+   */
+  virtual bool SetRadius(double radius);
+  vtkGetMacro(Radius, double);
+  //@}
+
+  //@{
+  /**
+   * Set/get the bottom point of the cone.
+   * The default is 0,0,0.
+   */
+  virtual bool SetCenterPoint(const vtkVector3d& pt);
+  virtual bool SetCenterPoint(double x, double y, double z)
+  {
+    return this->SetCenterPoint(vtkVector3d(x, y, z));
+  }
+  vtkGetMacro(CenterPoint, vtkVector3d);
+  //@}
+
+  //@{
+  /**
+   * Set/get the top point of the cone.
+   * The default is 0,0,0.
+   */
+  virtual bool SetNormal(const vtkVector3d& pt);
+  virtual bool SetNormal(double x, double y, double z)
+  {
+    return this->SetNormal(vtkVector3d(x, y, z));
+  }
+  virtual bool SetNormal(double* xyz) VTK_SIZEHINT(3)
+  {
+    return this->SetNormal(vtkVector3d(xyz[0], xyz[1], xyz[2]));
+  }
+  vtkGetMacro(Normal, vtkVector3d);
+  //@}
+
+protected:
+  vtkImplicitDisk();
+  ~vtkImplicitDisk() override;
+
+  /// Update the internal implicits and mark this object as modified.
+  ///
+  /// This is invoked by SetCenterPoint, SetRadius, SetNormal.
+  void UpdateImplicit();
+
+  // vtkNew<vtkCone> InfiniteCone;
+  // vtkNew<vtkTransform> ConeTransform;
+
+  vtkVector3d CenterPoint;
+  double Radius{ 0.5 };
+  // vtkNew<vtkPlane> BottomPlane;
+
+  vtkVector3d Normal;
+  // vtkNew<vtkPlane> TopPlane;
+};
+
+#endif
diff --git a/smtk/extension/vtk/widgets/CMakeLists.txt b/smtk/extension/vtk/widgets/CMakeLists.txt
index 77ad19215891cba1392c637f48b8bdf8331007d1..1eaa17d5ffe6ecc03c989647c0d55140cb613d22 100644
--- a/smtk/extension/vtk/widgets/CMakeLists.txt
+++ b/smtk/extension/vtk/widgets/CMakeLists.txt
@@ -1,6 +1,8 @@
 set(classes
   vtkConeRepresentation
   vtkConeWidget
+  vtkDiskRepresentation
+  vtkDiskWidget
   vtkSBFunctionParser
   vtkSMTKArcRepresentation)
 
diff --git a/smtk/extension/vtk/widgets/vtkDiskRepresentation.cxx b/smtk/extension/vtk/widgets/vtkDiskRepresentation.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..daae7177e40485468b132f5bd6e8e74c8dfbc6d4
--- /dev/null
+++ b/smtk/extension/vtk/widgets/vtkDiskRepresentation.cxx
@@ -0,0 +1,1030 @@
+//=========================================================================
+//  Copyright (c) Kitware, Inc.
+//  All rights reserved.
+//  See LICENSE.txt for details.
+//
+//  This software is distributed WITHOUT ANY WARRANTY; without even
+//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+//  PURPOSE.  See the above copyright notice for more information.
+//=========================================================================
+#include "smtk/extension/vtk/widgets/vtkDiskRepresentation.h"
+#include "smtk/extension/vtk/source/vtkDisk.h"
+#include "smtk/extension/vtk/source/vtkImplicitDisk.h"
+
+#include "vtkActor.h"
+#include "vtkAssemblyNode.h"
+#include "vtkAssemblyPath.h"
+#include "vtkBox.h"
+#include "vtkCallbackCommand.h"
+#include "vtkCamera.h"
+#include "vtkCellArray.h"
+#include "vtkCellPicker.h"
+#include "vtkCommand.h"
+#include "vtkDiskSource.h"
+#include "vtkDoubleArray.h"
+#include "vtkGlyph3DMapper.h"
+#include "vtkHardwarePicker.h"
+#include "vtkImageData.h"
+#include "vtkInteractorObserver.h"
+#include "vtkLineSource.h"
+#include "vtkLookupTable.h"
+#include "vtkMath.h"
+#include "vtkObjectFactory.h"
+#include "vtkOutlineFilter.h"
+#include "vtkPickingManager.h"
+#include "vtkPlane.h"
+#include "vtkPointData.h"
+#include "vtkPoints.h"
+#include "vtkPolyData.h"
+#include "vtkPolyDataMapper.h"
+#include "vtkProperty.h"
+#include "vtkRenderWindow.h"
+#include "vtkRenderWindowInteractor.h"
+#include "vtkRenderer.h"
+#include "vtkSmartPointer.h"
+#include "vtkSphereSource.h"
+#include "vtkTransform.h"
+#include "vtkTubeFilter.h"
+#include "vtkVectorOperators.h"
+#include "vtkWindow.h"
+
+#include <algorithm>
+#include <cfloat> //for FLT_EPSILON
+
+vtkStandardNewMacro(vtkDiskRepresentation);
+
+vtkDiskRepresentation::vtkDiskRepresentation()
+{
+  this->HandleSize = 7.5;
+  this->Disk->SetResolution(128);
+
+  // Set up the initial properties
+  this->CreateDefaultProperties();
+
+  for (int ii = 0; ii < NumberOfElements; ++ii)
+  {
+    this->Elements[ii].Actor->SetMapper(this->Elements[ii].Mapper);
+    if (ii <= DiskFace)
+    {
+      this->Elements[ii].Actor->SetProperty(this->DiskProperty);
+    }
+    else if (ii <= DiskEdge)
+    {
+      this->Elements[ii].Actor->SetProperty(this->EdgeProperty);
+    }
+    else
+    {
+      this->Elements[ii].Actor->SetProperty(this->HandleProperty);
+    }
+  }
+
+  // Set up the pipelines for the visual elements
+  this->AxisTuber->SetInputConnection(this->Disk->GetOutputPort(vtkDisk::OutputPorts::DiskNormal));
+  this->AxisTuber->SetNumberOfSides(12);
+  this->Elements[DiskNormal].Mapper->SetInputConnection(this->AxisTuber->GetOutputPort());
+  this->Elements[DiskNormal].Actor->SetMapper(this->Elements[DiskNormal].Mapper);
+
+  this->Elements[DiskFace].Mapper->SetInputConnection(
+    this->Disk->GetOutputPort(vtkDisk::OutputPorts::DiskFace));
+
+  this->Elements[DiskEdge].Mapper->SetInputConnection(
+    this->Disk->GetOutputPort(vtkDisk::OutputPorts::DiskEdge));
+
+  // Create the endpoint geometry source
+  this->Sphere->SetThetaResolution(16);
+  this->Sphere->SetPhiResolution(8);
+
+  this->CenterVertexMapper->SetSourceConnection(this->Sphere->GetOutputPort());
+  this->CenterVertexMapper->SetInputConnection(
+    this->Disk->GetOutputPort(vtkDisk::OutputPorts::CenterVertex));
+  this->Elements[CenterVertex].Actor->SetMapper(this->CenterVertexMapper);
+
+  // Define the point coordinates
+  double bounds[6];
+  bounds[0] = -0.5;
+  bounds[1] = 0.5;
+  bounds[2] = -0.5;
+  bounds[3] = 0.5;
+  bounds[4] = -0.5;
+  bounds[5] = 0.5;
+
+  // Initial creation of the widget, serves to initialize it
+  this->PlaceWidget(bounds);
+
+  //Manage the picking stuff
+  this->Picker->SetTolerance(0.005);
+  for (int ii = DiskNormal; ii < NumberOfElements; ++ii)
+  {
+    this->Picker->AddPickList(this->Elements[ii].Actor);
+  }
+  this->Picker->PickFromListOn();
+
+  this->FacePicker->SetTolerance(0.005);
+  for (int ii = DiskFace; ii <= DiskFace; ++ii)
+  {
+    this->FacePicker->AddPickList(this->Elements[ii].Actor);
+  }
+  this->FacePicker->PickFromListOn();
+
+  this->RepresentationState = vtkDiskRepresentation::Outside;
+}
+
+vtkDiskRepresentation::~vtkDiskRepresentation() = default;
+
+bool vtkDiskRepresentation::SetCenter(double x, double y, double z)
+{
+  return this->SetCenter(vtkVector3d(x, y, z));
+}
+
+bool vtkDiskRepresentation::SetCenter(const vtkVector3d& pt)
+{
+  vtkVector3d p0(this->Disk->GetCenterPoint());
+
+  vtkVector3d temp(pt); // Because vtkSetVectorMacro is not const correct.
+  if (p0 == pt)
+  {
+    // If pt is already the existing value, do nothing.
+    return false;
+  }
+
+  this->Disk->SetCenterPoint(temp.GetData());
+  this->Modified();
+  return true;
+}
+
+vtkVector3d vtkDiskRepresentation::GetCenter() const
+{
+  return vtkVector3d(this->Disk->GetCenterPoint());
+}
+
+double* vtkDiskRepresentation::GetCenterPoint()
+{
+  return this->Disk->GetCenterPoint();
+}
+
+bool vtkDiskRepresentation::SetNormal(double x, double y, double z)
+{
+  return this->SetNormal(vtkVector3d(x, y, z));
+}
+
+bool vtkDiskRepresentation::SetNormal(const vtkVector3d& nm)
+{
+  auto* norm = this->Disk->GetNormal();
+  vtkVector3d dnorm(norm[0], norm[1], norm[2]);
+  if (dnorm == nm)
+  {
+    return false;
+  }
+  this->Disk->SetNormal(nm.GetData());
+  return true;
+}
+
+vtkVector3d vtkDiskRepresentation::GetNormal() const
+{
+  double* nm = this->Disk->GetNormal();
+  return vtkVector3d(nm[0], nm[1], nm[2]);
+}
+
+double* vtkDiskRepresentation::GetNormalVector()
+{
+  return this->Disk->GetNormal();
+}
+
+bool vtkDiskRepresentation::SetRadius(double r)
+{
+  double prev = this->Disk->GetRadius();
+  if (prev == r)
+  {
+    return false;
+  }
+  this->Disk->SetRadius(r);
+  this->Modified();
+  return true;
+}
+
+double vtkDiskRepresentation::GetRadius() const
+{
+  return this->Disk->GetRadius();
+}
+
+int vtkDiskRepresentation::ComputeInteractionState(int X, int Y, int /*modify*/)
+{
+  // See if anything has been selected
+  vtkAssemblyPath* path = this->GetAssemblyPath(X, Y, 0., this->Picker);
+
+  // The second picker may need to be called. This is done because the disk face
+  // may obstruct things that can be picked; thus the disk face is the selection
+  // of last resort. This allows users to rotate the normal vector even from behind
+  // the disk
+  if (path == nullptr)
+  {
+    this->FacePicker->Pick(X, Y, 0., this->Renderer);
+    path = this->FacePicker->GetPath();
+  }
+
+  if (path == nullptr) // Nothing picked
+  {
+    this->SetRepresentationState(vtkDiskRepresentation::Outside);
+    this->InteractionState = vtkDiskRepresentation::Outside;
+    return this->InteractionState;
+  }
+
+  // Something picked, continue
+  this->ValidPick = 1;
+
+  // Depending on the interaction state (set by the widget) we modify
+  // this state based on what is picked.
+  if (this->InteractionState == vtkDiskRepresentation::Moving)
+  {
+    vtkProp* prop = path->GetFirstNode()->GetViewProp();
+    if (prop == this->Elements[DiskNormal].Actor)
+    {
+      this->InteractionState = vtkDiskRepresentation::RotatingNormal;
+      this->SetRepresentationState(vtkDiskRepresentation::RotatingNormal);
+    }
+    else if (prop == this->Elements[DiskEdge].Actor)
+    {
+      this->InteractionState = vtkDiskRepresentation::AdjustingRadius;
+      this->SetRepresentationState(vtkDiskRepresentation::AdjustingRadius);
+    }
+    else if (prop == this->Elements[CenterVertex].Actor)
+    {
+      this->InteractionState = vtkDiskRepresentation::MovingCenterVertex;
+      this->SetRepresentationState(vtkDiskRepresentation::MovingCenterVertex);
+    }
+    // Better to push the face along the normal than translate in view plane.
+    // else if (prop == this->Elements[DiskFace].Actor)
+    // {
+    //   this->InteractionState = vtkDiskRepresentation::MovingWhole;
+    //   this->SetRepresentationState(vtkDiskRepresentation::MovingWhole);
+    // }
+    else if (prop == this->Elements[DiskFace].Actor)
+    {
+      this->InteractionState = vtkDiskRepresentation::PushingDiskFace;
+      this->SetRepresentationState(vtkDiskRepresentation::PushingDiskFace);
+    }
+    else
+    {
+      this->InteractionState = vtkDiskRepresentation::Outside;
+      this->SetRepresentationState(vtkDiskRepresentation::Outside);
+    }
+  }
+  else
+  {
+    this->InteractionState = vtkDiskRepresentation::Outside;
+  }
+
+  return this->InteractionState;
+}
+
+void vtkDiskRepresentation::SetRepresentationState(int state)
+{
+  if (this->RepresentationState == state)
+  {
+    return;
+  }
+
+  // Clamp the state
+  state =
+    (state < vtkDiskRepresentation::Outside
+       ? vtkDiskRepresentation::Outside
+       : (state > vtkDiskRepresentation::RotatingNormal ? vtkDiskRepresentation::RotatingNormal : state));
+
+  this->RepresentationState = state;
+  this->Modified();
+
+#if 0
+  // For debugging, it is handy to see state changes:
+  std::cout
+    << "   State "
+    << vtkDiskRepresentation::InteractionStateToString(this->RepresentationState)
+    << "\n";
+#endif
+
+  this->HighlightElement(NumberOfElements, 0); // Turn everything off
+  if (state == vtkDiskRepresentation::RotatingNormal)
+  {
+    this->HighlightAxis(1);
+  }
+  else if (state == vtkDiskRepresentation::PushingDiskFace)
+  {
+    this->HighlightDisk(1);
+  }
+  else if (state == vtkDiskRepresentation::AdjustingRadius)
+  {
+    this->HighlightCurve(1);
+  }
+  else if (state == vtkDiskRepresentation::MovingWhole)
+  {
+    this->HighlightCurve(1);
+    this->HighlightDisk(1);
+    this->HighlightAxis(1);
+    this->HighlightHandle(1);
+  }
+  else
+  {
+    this->HighlightAxis(0);
+    this->HighlightDisk(0);
+    // this->HighlightOutline(0);
+  }
+}
+
+void vtkDiskRepresentation::StartWidgetInteraction(double e[2])
+{
+  this->StartEventPosition[0] = e[0];
+  this->StartEventPosition[1] = e[1];
+  this->StartEventPosition[2] = 0.0;
+
+  this->LastEventPosition[0] = e[0];
+  this->LastEventPosition[1] = e[1];
+  this->LastEventPosition[2] = 0.0;
+}
+
+void vtkDiskRepresentation::WidgetInteraction(double e[2])
+{
+  // Do different things depending on state
+  // Calculations everybody does
+  double focalPoint[4], pickPoint[4], prevPickPoint[4];
+  double z, vpn[3];
+
+  vtkCamera* camera = this->Renderer->GetActiveCamera();
+  if (!camera)
+  {
+    return;
+  }
+
+  // Compute the two points defining the motion vector
+  double pos[3];
+  this->Picker->GetPickPosition(pos);
+  vtkInteractorObserver::ComputeWorldToDisplay(this->Renderer, pos[0], pos[1], pos[2], focalPoint);
+  z = focalPoint[2];
+  vtkInteractorObserver::ComputeDisplayToWorld(
+    this->Renderer, this->LastEventPosition[0], this->LastEventPosition[1], z, prevPickPoint);
+  vtkInteractorObserver::ComputeDisplayToWorld(this->Renderer, e[0], e[1], z, pickPoint);
+
+  // Process the motion
+  if (this->InteractionState == vtkDiskRepresentation::AdjustingRadius)
+  {
+    this->AdjustRadius(e[0], e[1], prevPickPoint, pickPoint);
+  }
+  else if (this->InteractionState == vtkDiskRepresentation::MovingCenterVertex)
+  {
+    this->TranslateCenterInPlane(prevPickPoint, pickPoint);
+  }
+  else if (this->InteractionState == vtkDiskRepresentation::MovingWhole)
+  {
+    this->TranslateCenter(prevPickPoint, pickPoint);
+  }
+  else if (this->InteractionState == vtkDiskRepresentation::PushingDiskFace)
+  {
+    this->PushFace(prevPickPoint, pickPoint);
+  }
+  else if (this->InteractionState == vtkDiskRepresentation::RotatingNormal)
+  {
+    camera->GetViewPlaneNormal(vpn);
+    this->Rotate(e[0], e[1], prevPickPoint, pickPoint, vpn);
+  }
+
+  this->LastEventPosition[0] = e[0];
+  this->LastEventPosition[1] = e[1];
+  this->LastEventPosition[2] = 0.0;
+}
+
+void vtkDiskRepresentation::EndWidgetInteraction(double /*newEventPos*/[2])
+{
+  this->SetRepresentationState(vtkDiskRepresentation::Outside);
+}
+
+double* vtkDiskRepresentation::GetBounds()
+{
+  this->BuildRepresentation();
+  this->BoundingBox->SetBounds(this->Elements[DiskFace].Actor->GetBounds());
+  for (int ii = DiskFace; ii < NumberOfElements; ++ii)
+  {
+    this->BoundingBox->AddBounds(this->Elements[ii].Actor->GetBounds());
+  }
+
+  return this->BoundingBox->GetBounds();
+}
+
+void vtkDiskRepresentation::GetActors(vtkPropCollection* pc)
+{
+  for (int ii = 0; ii < NumberOfElements; ++ii)
+  {
+    this->Elements[ii].Actor->GetActors(pc);
+  }
+}
+
+void vtkDiskRepresentation::ReleaseGraphicsResources(vtkWindow* w)
+{
+  for (int ii = 0; ii < NumberOfElements; ++ii)
+  {
+    this->Elements[ii].Actor->ReleaseGraphicsResources(w);
+  }
+}
+
+int vtkDiskRepresentation::RenderOpaqueGeometry(vtkViewport* v)
+{
+  int count = 0;
+  this->BuildRepresentation();
+  for (int ii = DiskNormal; ii < NumberOfElements; ++ii)
+  {
+    count += this->Elements[ii].Actor->RenderOpaqueGeometry(v);
+  }
+
+  if (this->DrawDisk)
+  {
+    for (int ii = DiskFace; ii < DiskNormal; ++ii)
+    {
+      count += this->Elements[ii].Actor->RenderOpaqueGeometry(v);
+    }
+  }
+
+  return count;
+}
+
+int vtkDiskRepresentation::RenderTranslucentPolygonalGeometry(vtkViewport* v)
+{
+  int count = 0;
+  this->BuildRepresentation();
+  for (int ii = DiskNormal; ii < NumberOfElements; ++ii)
+  {
+    count += this->Elements[ii].Actor->RenderTranslucentPolygonalGeometry(v);
+  }
+
+  if (this->DrawDisk)
+  {
+    for (int ii = DiskFace; ii < DiskNormal; ++ii)
+    {
+      count += this->Elements[ii].Actor->RenderTranslucentPolygonalGeometry(v);
+    }
+  }
+
+  return count;
+}
+
+vtkTypeBool vtkDiskRepresentation::HasTranslucentPolygonalGeometry()
+{
+  int result = 0;
+  for (int ii = DiskNormal; ii < NumberOfElements; ++ii)
+  {
+    result |= this->Elements[ii].Actor->HasTranslucentPolygonalGeometry();
+  }
+
+  if (this->DrawDisk)
+  {
+    for (int ii = DiskFace; ii < DiskNormal; ++ii)
+    {
+      result |= this->Elements[ii].Actor->HasTranslucentPolygonalGeometry();
+    }
+  }
+
+  return result;
+}
+
+void vtkDiskRepresentation::PrintSelf(ostream& os, vtkIndent indent)
+{
+  this->Superclass::PrintSelf(os, indent);
+  os << indent << "Resolution: " << this->Resolution << "\n";
+
+  if (this->HandleProperty)
+  {
+    os << indent << "Handle Property: " << this->HandleProperty << "\n";
+  }
+  else
+  {
+    os << indent << "Handle Property: (none)\n";
+  }
+  if (this->SelectedHandleProperty)
+  {
+    os << indent << "Selected Handle Property: " << this->SelectedHandleProperty << "\n";
+  }
+  else
+  {
+    os << indent << "Selected Handle Property: (none)\n";
+  }
+
+  if (this->DiskProperty)
+  {
+    os << indent << "Disk Property: " << this->DiskProperty << "\n";
+  }
+  else
+  {
+    os << indent << "Disk Property: (none)\n";
+  }
+  if (this->SelectedDiskProperty)
+  {
+    os << indent << "Selected Disk Property: " << this->SelectedDiskProperty << "\n";
+  }
+  else
+  {
+    os << indent << "Selected Disk Property: (none)\n";
+  }
+
+  if (this->EdgeProperty)
+  {
+    os << indent << "Edge Property: " << this->EdgeProperty << "\n";
+  }
+  else
+  {
+    os << indent << "Edge Property: (none)\n";
+  }
+
+  os << indent << "Along X Axis: " << (this->AlongXAxis ? "On" : "Off") << "\n";
+  os << indent << "Along Y Axis: " << (this->AlongYAxis ? "On" : "Off") << "\n";
+  os << indent << "ALong Z Axis: " << (this->AlongZAxis ? "On" : "Off") << "\n";
+
+  os << indent << "Tubing: " << (this->Tubing ? "On" : "Off") << "\n";
+  os << indent << "Draw Disk: " << (this->DrawDisk ? "On" : "Off") << "\n";
+  os << indent << "Bump Distance: " << this->BumpDistance << "\n";
+
+  os << indent << "Representation State: "
+     << vtkDiskRepresentation::InteractionStateToString(this->RepresentationState) << "\n";
+
+  // this->InteractionState is printed in superclass
+  // this is commented to avoid PrintSelf errors
+}
+
+void vtkDiskRepresentation::HighlightElement(ElementType elem, int highlight)
+{
+  switch (elem)
+  {
+    case DiskFace:
+      this->HighlightDisk(highlight);
+      break;
+    case DiskNormal:
+      this->HighlightAxis(highlight);
+      break;
+    case DiskEdge:
+      this->HighlightCurve(highlight);
+      break;
+    case CenterVertex:
+      this->HighlightHandle(highlight);
+      break;
+    case NumberOfElements:
+      // Set everything to the given highlight state.
+      this->HighlightAxis(highlight);
+      this->HighlightDisk(highlight);
+      this->HighlightCurve(highlight);
+      this->HighlightHandle(highlight);
+      break;
+  }
+}
+
+void vtkDiskRepresentation::HighlightDisk(int highlight)
+{
+  if (highlight)
+  {
+    this->Elements[DiskFace].Actor->SetProperty(this->SelectedDiskProperty);
+  }
+  else
+  {
+    this->Elements[DiskFace].Actor->SetProperty(this->DiskProperty);
+  }
+}
+
+void vtkDiskRepresentation::HighlightAxis(int highlight)
+{
+  if (highlight)
+  {
+    this->Elements[DiskNormal].Actor->SetProperty(this->SelectedEdgeProperty);
+  }
+  else
+  {
+    this->Elements[DiskNormal].Actor->SetProperty(this->EdgeProperty);
+  }
+}
+
+void vtkDiskRepresentation::HighlightCurve(int highlight)
+{
+  ElementType elem = DiskEdge;
+  if (highlight)
+  {
+    this->Elements[elem].Actor->SetProperty(this->SelectedEdgeProperty);
+  }
+  else
+  {
+    this->Elements[elem].Actor->SetProperty(this->EdgeProperty);
+  }
+}
+
+void vtkDiskRepresentation::HighlightHandle(int highlight)
+{
+  ElementType elem = CenterVertex;
+  if (highlight)
+  {
+    this->Elements[elem].Actor->SetProperty(this->SelectedHandleProperty);
+  }
+  else
+  {
+    this->Elements[elem].Actor->SetProperty(this->HandleProperty);
+  }
+}
+
+void vtkDiskRepresentation::Rotate(double X, double Y, double* p1, double* p2, double* vpn)
+{
+  double v[3];    //vector of motion
+  double axis[3]; //axis of rotation
+  double theta;   //rotation angle
+
+  // mouse motion vector in world space
+  v[0] = p2[0] - p1[0];
+  v[1] = p2[1] - p1[1];
+  v[2] = p2[2] - p1[2];
+
+  vtkVector3d cp0(this->Disk->GetCenterPoint());
+  vtkVector3d cp1(this->Disk->GetNormal());
+  vtkVector3d center = cp0;
+
+  // Create axis of rotation and angle of rotation
+  vtkMath::Cross(vpn, v, axis);
+  if (vtkMath::Normalize(axis) == 0.0)
+  {
+    return;
+  }
+  int* size = this->Renderer->GetSize();
+  double l2 = (X - this->LastEventPosition[0]) * (X - this->LastEventPosition[0]) +
+    (Y - this->LastEventPosition[1]) * (Y - this->LastEventPosition[1]);
+  theta = 360.0 * sqrt(l2 / (size[0] * size[0] + size[1] * size[1]));
+
+  // Manipulate the transform to reflect the rotation
+  this->Transform->Identity();
+  this->Transform->Translate(center[0], center[1], center[2]);
+  this->Transform->RotateWXYZ(theta, axis);
+  this->Transform->Translate(-center[0], -center[1], -center[2]);
+
+  // cp0 is the center of rotation, it will not move.
+  // this->Transform->TransformPoint(cp0.GetData(), cp0.GetData());
+  // this->Transform->TransformPoint(cp1.GetData(), cp1.GetData());
+  this->Transform->TransformVector(cp1.GetData(), cp1.GetData());
+
+  // this->Disk->SetCenterPoint(cp0.GetData());
+  // this->Disk->SetTopPoint(cp1.GetData());
+  this->Disk->SetNormal(cp1.GetData());
+}
+
+void vtkDiskRepresentation::PushFace(double* p1, double* p2)
+{
+  //Get the motion vector
+  vtkVector3d v;
+  v[0] = p2[0] - p1[0];
+  v[1] = p2[1] - p1[1];
+  v[2] = p2[2] - p1[2];
+
+  vtkVector3d cp0(this->Disk->GetCenterPoint());
+  vtkVector3d cp1(this->Disk->GetNormal());
+  vtkVector3d axis = cp1;
+  axis.Normalize();
+  double bump = v.Dot(axis);
+
+  this->Disk->SetCenterPoint((cp0 + bump * axis).GetData());
+}
+
+// Loop through all points and translate them
+void vtkDiskRepresentation::AdjustRadius(double /*X*/, double Y, double* p1, double* p2)
+{
+  if (Y == this->LastEventPosition[1])
+  {
+    return;
+  }
+
+  double dr;
+  double radius = this->Disk->GetRadius();
+  double v[3]; //vector of motion
+  v[0] = p2[0] - p1[0];
+  v[1] = p2[1] - p1[1];
+  v[2] = p2[2] - p1[2];
+  double l = sqrt(vtkMath::Dot(v, v));
+
+  dr = l / 4;
+  if (Y < this->LastEventPosition[1])
+  {
+    dr *= -1.0;
+  }
+
+  double nextRadius = radius + dr;
+
+  if (nextRadius < 1e-30)
+  {
+    nextRadius = 1e-30;
+  }
+  else if (nextRadius < 0.)
+  {
+    nextRadius = -nextRadius;
+  }
+  this->Disk->SetRadius(nextRadius);
+  this->BuildRepresentation();
+}
+
+// Loop through all points and translate them
+void vtkDiskRepresentation::TranslateCenter(double* p1, double* p2)
+{
+  //Get the motion vector
+  vtkVector3d v;
+  v[0] = p2[0] - p1[0];
+  v[1] = p2[1] - p1[1];
+  v[2] = p2[2] - p1[2];
+
+  vtkVector3d cp0(this->Disk->GetCenterPoint());
+
+  this->Disk->SetCenterPoint((cp0 + v).GetData());
+  this->BuildRepresentation();
+}
+
+// Translate the center point within the plane of the disk.
+void vtkDiskRepresentation::TranslateCenterInPlane(double* p1, double* p2)
+{
+  // Get the motion vector
+  vtkVector3d v;
+  v[0] = p2[0] - p1[0];
+  v[1] = p2[1] - p1[1];
+  v[2] = p2[2] - p1[2];
+
+  vtkVector3d cp(this->Disk->GetCenterPoint());
+  vtkVector3d norm(this->Disk->GetNormal());
+
+  v = v - v.Dot(norm) * norm;
+
+  this->Disk->SetCenterPoint((cp + v).GetData());
+  this->BuildRepresentation();
+}
+
+
+// Loop through all points and translate them
+void vtkDiskRepresentation::TranslateHandle(double* p1, double* p2)
+{
+  //Get the motion vector
+  vtkVector3d v;
+  v[0] = p2[0] - p1[0];
+  v[1] = p2[1] - p1[1];
+  v[2] = p2[2] - p1[2];
+
+  vtkVector3d handle(this->Disk->GetCenterPoint());
+
+  this->Disk->SetCenterPoint((handle + v).GetData());
+  this->BuildRepresentation();
+}
+
+void vtkDiskRepresentation::SizeHandles()
+{
+  double radius =
+    this->vtkWidgetRepresentation::SizeHandlesInPixels(1.5, this->Sphere->GetCenter());
+
+  this->Sphere->SetRadius(radius);
+  this->AxisTuber->SetRadius(0.25 * radius);
+}
+
+void vtkDiskRepresentation::CreateDefaultProperties()
+{
+  this->HandleProperty->SetColor(1., 1., 1.);
+
+  this->EdgeProperty->SetColor(1., 1., 1.);
+  this->EdgeProperty->SetLineWidth(3);
+
+  this->DiskProperty->SetColor(1., 1., 1.);
+  this->DiskProperty->SetOpacity(0.5);
+
+  this->SelectedHandleProperty->SetColor(0.0, 1.0, 0.);
+  this->SelectedHandleProperty->SetAmbient(1.0);
+
+  this->SelectedEdgeProperty->SetColor(0., 1.0, 0.0);
+  this->SelectedEdgeProperty->SetLineWidth(3);
+
+  this->SelectedDiskProperty->SetColor(0., 1., 0.);
+  this->SelectedDiskProperty->SetOpacity(0.5);
+}
+
+void vtkDiskRepresentation::PlaceWidget(double bds[6])
+{
+  vtkVector3d lo(bds[0], bds[2], bds[4]);
+  vtkVector3d hi(bds[1], bds[3], bds[5]);
+  vtkVector3d md = 0.5 * (lo + hi);
+
+  this->InitialLength = (hi - lo).Norm();
+
+  if (this->AlongYAxis)
+  {
+    this->Disk->SetCenterPoint(md[0], lo[1], md[2]);
+    double radius = hi[2] - md[2] > hi[0] - md[0] ? hi[0] - md[0] : hi[2] - md[2];
+    this->Disk->SetRadius(radius);
+  }
+  else if (this->AlongZAxis)
+  {
+    this->Disk->SetCenterPoint(md[0], md[1], lo[2]);
+    double radius = hi[0] - md[0] > hi[1] - md[1] ? hi[1] - md[1] : hi[0] - md[0];
+    this->Disk->SetRadius(radius);
+  }
+  else //default or x-normal
+  {
+    this->Disk->SetCenterPoint(lo[0], md[1], md[2]);
+    double radius = hi[2] - md[2] > hi[1] - md[1] ? hi[1] - md[1] : hi[2] - md[2];
+    this->Disk->SetRadius(radius);
+  }
+
+  this->ValidPick = 1; // since we have positioned the widget successfully
+  this->BuildRepresentation();
+}
+
+void vtkDiskRepresentation::SetDrawDisk(vtkTypeBool drawCyl)
+{
+  if (drawCyl == this->DrawDisk)
+  {
+    return;
+  }
+
+  this->Modified();
+  this->DrawDisk = drawCyl;
+  this->BuildRepresentation();
+}
+
+void vtkDiskRepresentation::SetAlongXAxis(vtkTypeBool var)
+{
+  if (this->AlongXAxis != var)
+  {
+    this->AlongXAxis = var;
+    this->Modified();
+  }
+  if (var)
+  {
+    this->AlongYAxisOff();
+    this->AlongZAxisOff();
+  }
+}
+
+void vtkDiskRepresentation::SetAlongYAxis(vtkTypeBool var)
+{
+  if (this->AlongYAxis != var)
+  {
+    this->AlongYAxis = var;
+    this->Modified();
+  }
+  if (var)
+  {
+    this->AlongXAxisOff();
+    this->AlongZAxisOff();
+  }
+}
+
+void vtkDiskRepresentation::SetAlongZAxis(vtkTypeBool var)
+{
+  if (this->AlongZAxis != var)
+  {
+    this->AlongZAxis = var;
+    this->Modified();
+  }
+  if (var)
+  {
+    this->AlongXAxisOff();
+    this->AlongYAxisOff();
+  }
+}
+
+void vtkDiskRepresentation::GetDisk(vtkImplicitDisk* disk)
+{
+  if (disk == nullptr)
+  {
+    return;
+  }
+
+  disk->SetCenterPoint(this->GetCenter());
+  disk->SetRadius(this->Disk->GetRadius());
+  disk->SetNormal(this->Disk->GetNormal());
+}
+
+void vtkDiskRepresentation::UpdatePlacement()
+{
+  this->BuildRepresentation();
+}
+
+void vtkDiskRepresentation::BumpDisk(int dir, double factor)
+{
+  // Compute the distance
+  double d = this->InitialLength * this->BumpDistance * factor;
+
+  // Push the cylinder
+  this->PushDisk((dir > 0 ? d : -d));
+}
+
+void vtkDiskRepresentation::PushDisk(double d)
+{
+  vtkCamera* camera = this->Renderer->GetActiveCamera();
+  if (!camera)
+  {
+    return;
+  }
+  vtkVector3d vpn;
+  vtkVector3d p0(this->Disk->GetCenterPoint());
+  vtkVector3d p1(this->Disk->GetNormal());
+  camera->GetViewPlaneNormal(vpn.GetData());
+
+  p0 = p0 + d * vpn;
+
+  this->Disk->SetCenterPoint(p0.GetData());
+  this->BuildRepresentation();
+}
+
+bool vtkDiskRepresentation::PickNormal(int X, int Y, bool snapToMeshPoint)
+{
+  this->HardwarePicker->SetSnapToMeshPoint(snapToMeshPoint);
+  vtkAssemblyPath* path = this->GetAssemblyPath(X, Y, 0., this->HardwarePicker);
+  if (path == nullptr) // actors of renderer were not touched
+  {
+    if (this->PickCameraFocalInfo)
+    {
+      double normal[3];
+      this->HardwarePicker->GetPickNormal(normal);
+      this->SetNormal(normal);
+      this->BuildRepresentation();
+    }
+    return this->PickCameraFocalInfo;
+  }
+  else // actors of renderer were touched
+  {
+    double normal[3];
+    this->HardwarePicker->GetPickNormal(normal);
+    if (!std::isnan(normal[0]) || !std::isnan(normal[1]) || !std::isnan(normal[2]))
+    {
+      this->SetNormal(normal);
+      this->BuildRepresentation();
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+}
+
+std::string vtkDiskRepresentation::InteractionStateToString(int state)
+{
+  switch (state)
+  {
+    case Outside:
+      return "Outside";
+      break;
+    case Moving:
+      return "Moving";
+      break;
+    case PushingDiskFace:
+      return "PushingDiskFace";
+      break;
+    case AdjustingRadius:
+      return "AdjustingRadius";
+      break;
+    case MovingCenterVertex:
+      return "MovingCenterVertex";
+      break;
+    case MovingWhole:
+      return "MovingWhole";
+      break;
+    case RotatingNormal:
+      return "RotatingNormal";
+      break;
+    default:
+      break;
+  }
+  return "Invalid";
+}
+
+void vtkDiskRepresentation::BuildRepresentation()
+{
+  if (!this->Renderer || !this->Renderer->GetRenderWindow())
+  {
+    return;
+  }
+
+  vtkInformation* info = this->GetPropertyKeys();
+  for (int ii = 0; ii < NumberOfElements; ++ii)
+  {
+    this->Elements[ii].Actor->SetPropertyKeys(info);
+  }
+
+  if (
+    this->GetMTime() > this->BuildTime || this->Disk->GetMTime() > this->BuildTime ||
+    this->Renderer->GetRenderWindow()->GetMTime() > this->BuildTime)
+  {
+    // Control the look of the edges
+    if (this->Tubing)
+    {
+      this->Elements[DiskNormal].Mapper->SetInputConnection(this->AxisTuber->GetOutputPort());
+    }
+    else
+    {
+      this->Elements[DiskNormal].Mapper->SetInputConnection(
+        this->Disk->GetOutputPort(vtkDisk::OutputPorts::DiskNormal));
+    }
+
+    this->SizeHandles();
+    this->BuildTime.Modified();
+  }
+}
+
+void vtkDiskRepresentation::RegisterPickers()
+{
+  vtkPickingManager* pm = this->GetPickingManager();
+  if (!pm)
+  {
+    return;
+  }
+  pm->AddPicker(this->Picker, this);
+}
diff --git a/smtk/extension/vtk/widgets/vtkDiskRepresentation.h b/smtk/extension/vtk/widgets/vtkDiskRepresentation.h
new file mode 100644
index 0000000000000000000000000000000000000000..b9a10f798e3027535db1ddb7d7ff8e6dddde300e
--- /dev/null
+++ b/smtk/extension/vtk/widgets/vtkDiskRepresentation.h
@@ -0,0 +1,383 @@
+//=========================================================================
+//  Copyright (c) Kitware, Inc.
+//  All rights reserved.
+//  See LICENSE.txt for details.
+//
+//  This software is distributed WITHOUT ANY WARRANTY; without even
+//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+//  PURPOSE.  See the above copyright notice for more information.
+//=========================================================================
+#ifndef vtkDiskRepresentation_h
+#define vtkDiskRepresentation_h
+
+#include "smtk/extension/vtk/widgets/vtkSMTKWidgetsExtModule.h" // For export macro
+#include "vtkNew.h"
+#include "vtkVector.h"
+#include "vtkWidgetRepresentation.h"
+
+#include <array>
+
+class vtkActor;
+class vtkPolyDataMapper;
+class vtkHardwarePicker;
+class vtkCellPicker;
+class vtkDisk;
+class vtkGlyph3DMapper;
+class vtkImplicitDisk;
+class vtkSphereSource;
+class vtkTubeFilter;
+class vtkProperty;
+class vtkImageData;
+class vtkOutlineFilter;
+class vtkFeatureEdges;
+class vtkPolyData;
+class vtkPolyDataAlgorithm;
+class vtkTransform;
+class vtkBox;
+class vtkLookupTable;
+
+#define VTK_MAX_DISK_RESOLUTION 2048
+
+/**
+ * @class   vtkDiskRepresentation
+ * @brief   defining the representation for a planar disk.
+ *
+ * This class is a concrete representation for the
+ * vtkDiskWidget. It represents a finite disk
+ * defined by a center point, a normal vector, and a radius.
+ * This disk representation can be manipulated by using the
+ * vtkDiskWidget.
+ *
+ * @sa
+ * vtkDiskWidget
+*/
+class VTKSMTKWIDGETSEXT_EXPORT vtkDiskRepresentation : public vtkWidgetRepresentation
+{
+public:
+  /**
+   * Instantiate the class.
+   */
+  static vtkDiskRepresentation* New();
+
+  //@{
+  /**
+   * Standard methods for the class.
+   */
+  vtkTypeMacro(vtkDiskRepresentation, vtkWidgetRepresentation);
+  void PrintSelf(ostream& os, vtkIndent indent) override;
+  //@}
+
+  vtkDiskRepresentation(const vtkDiskRepresentation&) = delete;
+  vtkDiskRepresentation& operator=(const vtkDiskRepresentation&) = delete;
+
+  //@{
+  /**
+   * Set/get the center of the disk.
+   */
+  bool SetCenter(double x, double y, double z);
+  bool SetCenter(const vtkVector3d& pt);
+  vtkVector3d GetCenter() const;
+  void SetCenterPoint(double* x) VTK_SIZEHINT(3) { this->SetCenter(x[0], x[1], x[2]); }
+  double* GetCenterPoint() VTK_SIZEHINT(3);
+  //@}
+
+  //@{
+  /**
+   * Set/get the normal of the disk.
+   */
+  bool SetNormal(double x, double y, double z);
+  bool SetNormal(const vtkVector3d& nm);
+  vtkVector3d GetNormal() const;
+  void SetNormal(double* x) VTK_SIZEHINT(3) { this->SetNormal(x[0], x[1], x[2]); }
+  double* GetNormalVector() VTK_SIZEHINT(3);
+  //@}
+
+  //@{
+  /**
+   * Set/get the radius of the disk.
+   *
+   * Negative values are generally a bad idea but not prohibited at this point.
+   * They will result in bad surface normals, though.
+   */
+  bool SetRadius(double r);
+  double GetRadius() const;
+  //@}
+
+  //@{
+  /**
+   * Force the disk widget's normal to be aligned with one of the x-y-z axes.
+   * If one axis is set on, the other two will be set off.
+   * Remember that when the state changes, a ModifiedEvent is invoked.
+   * This can be used to snap the disk to the axes if it is originally
+   * not aligned.
+   */
+  void SetAlongXAxis(vtkTypeBool);
+  vtkGetMacro(AlongXAxis, vtkTypeBool);
+  vtkBooleanMacro(AlongXAxis, vtkTypeBool);
+  void SetAlongYAxis(vtkTypeBool);
+  vtkGetMacro(AlongYAxis, vtkTypeBool);
+  vtkBooleanMacro(AlongYAxis, vtkTypeBool);
+  void SetAlongZAxis(vtkTypeBool);
+  vtkGetMacro(AlongZAxis, vtkTypeBool);
+  vtkBooleanMacro(AlongZAxis, vtkTypeBool);
+  //@}
+
+  //@{
+  /**
+   * Enable/disable the drawing of the disk. In some cases the disk
+   * interferes with the object that it is operating on (e.g., the
+   * disk interferes with the cut surface it produces resulting in
+   * z-buffer artifacts.) By default it is off.
+   */
+  void SetDrawDisk(vtkTypeBool drawCyl);
+  vtkGetMacro(DrawDisk, vtkTypeBool);
+  vtkBooleanMacro(DrawDisk, vtkTypeBool);
+  //@}
+
+  //@{
+  /**
+   * Set/Get the resolution of the disk. This is the number of
+   * triangles used to approximate the circular face and line
+   * segments used to approximate the circular edge.
+   * A vtkDisk is used under the hood.
+   */
+  vtkSetClampMacro(Resolution, int, 3, VTK_MAX_DISK_RESOLUTION);
+  vtkGetMacro(Resolution, int);
+  //@}
+
+  //@{
+  /**
+   * Turn on/off tubing of the wire outline of the cylinder
+   * intersection (against the bounding box). The tube thickens the
+   * line by wrapping with a vtkTubeFilter.
+   */
+  vtkSetMacro(Tubing, vtkTypeBool);
+  vtkGetMacro(Tubing, vtkTypeBool);
+  vtkBooleanMacro(Tubing, vtkTypeBool);
+  //@}
+
+  /**
+   * Get the implicit function for the disk. The user must provide an instance
+   * vtkImplicitDisk; upon return, it will be set to the difference between
+   * an infinite disk and the two cutting planes.
+   * The returned implicit can be used by a variety of filters
+   * to perform clipping, cutting, and selection of data.
+   */
+  void GetDisk(vtkImplicitDisk* disk);
+
+  /**
+   * Satisfies the superclass API.  This will change the state of the widget
+   * to match changes that have been made to the underlying PolyDataSource.
+   */
+  void UpdatePlacement();
+
+  //@{
+  /**
+   * Get the properties used to render the center point
+   * when selected or not.
+   */
+  vtkGetObjectMacro(HandleProperty, vtkProperty);
+  vtkGetObjectMacro(SelectedHandleProperty, vtkProperty);
+  //@}
+
+  //@{
+  /**
+   * Get the disk-face properties. The properties of the disk when selected
+   * and unselected can be manipulated.
+   */
+  vtkGetObjectMacro(DiskProperty, vtkProperty);
+  vtkGetObjectMacro(SelectedDiskProperty, vtkProperty);
+  //@}
+
+  //@{
+  /**
+   * Get the property of the boundary edge and normal line. (This property also
+   * applies to the edges when tubed.)
+   */
+  vtkGetObjectMacro(EdgeProperty, vtkProperty);
+  //@}
+
+  //@{
+  /**
+   * Methods to interface with the vtkDiskWidget.
+   */
+  int ComputeInteractionState(int X, int Y, int modify = 0) override;
+  void PlaceWidget(double bounds[6]) override;
+  void BuildRepresentation() override;
+  void StartWidgetInteraction(double eventPos[2]) override;
+  void WidgetInteraction(double newEventPos[2]) override;
+  void EndWidgetInteraction(double newEventPos[2]) override;
+  //@}
+
+  //@{
+  /**
+   * Methods supporting the rendering process.
+   */
+  double* GetBounds() override;
+  void GetActors(vtkPropCollection* pc) override;
+  void ReleaseGraphicsResources(vtkWindow*) override;
+  int RenderOpaqueGeometry(vtkViewport*) override;
+  int RenderTranslucentPolygonalGeometry(vtkViewport*) override;
+  vtkTypeBool HasTranslucentPolygonalGeometry() override;
+  //@}
+
+  //@{
+  /**
+   * Specify a translation distance used by the BumpDisk() method. Note that the
+   * distance is normalized; it is the fraction of the length of the bounding
+   * box of the wire outline.
+   */
+  vtkSetClampMacro(BumpDistance, double, 0.000001, 1);
+  vtkGetMacro(BumpDistance, double);
+  //@}
+
+  /**
+   * Translate the disk in the direction of the view vector by the
+   * specified BumpDistance. The dir parameter controls which
+   * direction the pushing occurs, either in the same direction as the
+   * view vector, or when negative, in the opposite direction.  The factor
+   * controls what percentage of the bump is used.
+   */
+  void BumpDisk(int dir, double factor);
+
+  /**
+   * Push the disk the distance specified along the view
+   * vector. Positive values are in the direction of the view vector;
+   * negative values are in the opposite direction. The distance value
+   * is expressed in world coordinates.
+   */
+  void PushDisk(double distance);
+
+  /**\brief Set the disk normal to either the surface normal below the pointer
+   *        or the opposite of the camera view vector (pointing back toward the camera).
+   */
+  bool PickNormal(int X, int Y, bool snapToMeshPoint);
+
+  // Manage the state of the widget
+  enum _InteractionState
+  {
+    Outside = 0,
+    Moving,
+    PushingDiskFace,
+    AdjustingRadius,
+    MovingCenterVertex,
+    MovingWhole,
+    RotatingNormal
+  };
+
+  static std::string InteractionStateToString(int);
+
+  //@{
+  /**
+   * The interaction state may be set from a widget (e.g.,
+   * vtkImplicitCylinderWidget) or other object. This controls how the
+   * interaction with the widget proceeds. Normally this method is used as
+   * part of a handshaking process with the widget: First
+   * ComputeInteractionState() is invoked that returns a state based on
+   * geometric considerations (i.e., cursor near a widget feature), then
+   * based on events, the widget may modify this further.
+   */
+  vtkSetClampMacro(InteractionState, int, Outside, RotatingNormal);
+  //@}
+
+  //@{
+  /**
+   * Sets the visual appearance of the representation based on the
+   * state it is in. This state is usually the same as InteractionState.
+   */
+  virtual void SetRepresentationState(int);
+  vtkGetMacro(RepresentationState, int);
+  //@}
+
+  /*
+  * Register internal Pickers within PickingManager
+  */
+  void RegisterPickers() override;
+
+protected:
+  vtkDiskRepresentation();
+  ~vtkDiskRepresentation() override;
+
+  /// Visual elements of the representation.
+  enum ElementType
+  {
+    DiskFace = 0,
+    DiskNormal,
+    DiskEdge,
+    CenterVertex,
+    NumberOfElements
+  };
+
+  void HighlightElement(ElementType elem, int highlight);
+
+  void HighlightDisk(int highlight);
+  void HighlightAxis(int highlight);
+  void HighlightCurve(int highlight);
+  void HighlightHandle(int highlight);
+
+  // Methods to manipulate the disk
+  void Rotate(double X, double Y, double* p1, double* p2, double* vpn);
+  void TranslateDisk(double* p1, double* p2);
+  void PushFace(double* p1, double* p2);
+  void AdjustRadius(double X, double Y, double* p1, double* p2);
+  void TranslateCenter(double* p1, double* p2);
+  void TranslateCenterInPlane(double* p1, double* p2);
+  void TranslateHandle(double* p1, double* p2);
+  void SizeHandles();
+
+  void CreateDefaultProperties();
+
+  struct Element
+  {
+    vtkNew<vtkActor> Actor;
+    vtkNew<vtkPolyDataMapper> Mapper;
+  };
+
+  // Actors and mappers for all visual elements of the representation.
+  std::array<Element, NumberOfElements> Elements;
+
+  int RepresentationState;
+  // Keep track of event positions
+  double LastEventPosition[3];
+  // Controlling the push operation
+  double BumpDistance{ 0.01 };
+  // Controlling ivars
+  vtkTypeBool AlongXAxis{ 0 };
+  vtkTypeBool AlongYAxis{ 0 };
+  vtkTypeBool AlongZAxis{ 0 };
+  // The actual disk which is being manipulated
+  vtkNew<vtkDisk> Disk;
+  // The facet resolution for rendering purposes.
+  int Resolution{ 128 };
+  vtkTypeBool DrawDisk{ 1 };
+  vtkNew<vtkTubeFilter> AxisTuber; // Used to style edges.
+  vtkTypeBool Tubing{ 1 };         //control whether tubing is on
+
+  // Source of center-point handle geometry
+  vtkNew<vtkSphereSource> Sphere;
+
+  // Do the picking
+  vtkNew<vtkCellPicker> Picker;
+  vtkNew<vtkCellPicker> FacePicker;
+  vtkNew<vtkHardwarePicker> HardwarePicker; // For picking surface normals.
+  vtkTypeBool PickCameraFocalInfo{ true }; // Allow picking the camera projection direction.
+
+  // Properties used to control the appearance of selected objects and
+  // the manipulator in general.
+  vtkNew<vtkProperty> HandleProperty;
+  vtkNew<vtkProperty> SelectedHandleProperty;
+  vtkNew<vtkProperty> DiskProperty;
+  vtkNew<vtkProperty> SelectedDiskProperty;
+  vtkNew<vtkProperty> EdgeProperty;
+  vtkNew<vtkProperty> SelectedEdgeProperty;
+
+  // Support GetBounds() method
+  vtkNew<vtkBox> BoundingBox;
+
+  // Overrides for the point-handle polydata mappers
+  vtkNew<vtkGlyph3DMapper> CenterVertexMapper;
+
+  vtkNew<vtkTransform> Transform;
+};
+
+#endif
diff --git a/smtk/extension/vtk/widgets/vtkDiskWidget.cxx b/smtk/extension/vtk/widgets/vtkDiskWidget.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..da8c53c711aefe67cf9dac6a37bcd8a89ccbdd2e
--- /dev/null
+++ b/smtk/extension/vtk/widgets/vtkDiskWidget.cxx
@@ -0,0 +1,407 @@
+//=========================================================================
+//  Copyright (c) Kitware, Inc.
+//  All rights reserved.
+//  See LICENSE.txt for details.
+//
+//  This software is distributed WITHOUT ANY WARRANTY; without even
+//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+//  PURPOSE.  See the above copyright notice for more information.
+//=========================================================================
+#include "smtk/extension/vtk/widgets/vtkDiskWidget.h"
+#include "smtk/extension/vtk/widgets/vtkDiskRepresentation.h"
+
+#include "vtkCallbackCommand.h"
+#include "vtkCamera.h"
+#include "vtkCommand.h"
+#include "vtkEvent.h"
+#include "vtkObjectFactory.h"
+#include "vtkRenderWindow.h"
+#include "vtkRenderWindowInteractor.h"
+#include "vtkRenderer.h"
+#include "vtkStdString.h"
+#include "vtkWidgetCallbackMapper.h"
+#include "vtkWidgetEvent.h"
+#include "vtkWidgetEventTranslator.h"
+
+vtkStandardNewMacro(vtkDiskWidget);
+
+//----------------------------------------------------------------------------
+vtkDiskWidget::vtkDiskWidget()
+{
+  this->WidgetState = vtkDiskWidget::Start;
+
+  // Define widget events
+  this->CallbackMapper->SetCallbackMethod(
+    vtkCommand::LeftButtonPressEvent, vtkWidgetEvent::Select, this, vtkDiskWidget::SelectAction);
+  this->CallbackMapper->SetCallbackMethod(
+    vtkCommand::LeftButtonReleaseEvent,
+    vtkWidgetEvent::EndSelect,
+    this,
+    vtkDiskWidget::EndSelectAction);
+  this->CallbackMapper->SetCallbackMethod(
+    vtkCommand::MiddleButtonPressEvent,
+    vtkWidgetEvent::Translate,
+    this,
+    vtkDiskWidget::TranslateAction);
+  this->CallbackMapper->SetCallbackMethod(
+    vtkCommand::MiddleButtonReleaseEvent,
+    vtkWidgetEvent::EndTranslate,
+    this,
+    vtkDiskWidget::EndSelectAction);
+  this->CallbackMapper->SetCallbackMethod(
+    vtkCommand::RightButtonPressEvent, vtkWidgetEvent::Scale, this, vtkDiskWidget::ScaleAction);
+  this->CallbackMapper->SetCallbackMethod(
+    vtkCommand::RightButtonReleaseEvent,
+    vtkWidgetEvent::EndScale,
+    this,
+    vtkDiskWidget::EndSelectAction);
+  this->CallbackMapper->SetCallbackMethod(
+    vtkCommand::MouseMoveEvent, vtkWidgetEvent::Move, this, vtkDiskWidget::MoveAction);
+  this->CallbackMapper->SetCallbackMethod(
+    vtkCommand::KeyPressEvent,
+    vtkEvent::AnyModifier,
+    30,
+    1,
+    "Up",
+    vtkWidgetEvent::Up,
+    this,
+    vtkDiskWidget::MoveDiskAction);
+  this->CallbackMapper->SetCallbackMethod(
+    vtkCommand::KeyPressEvent,
+    vtkEvent::AnyModifier,
+    28,
+    1,
+    "Right",
+    vtkWidgetEvent::Up,
+    this,
+    vtkDiskWidget::MoveDiskAction);
+  this->CallbackMapper->SetCallbackMethod(
+    vtkCommand::KeyPressEvent,
+    vtkEvent::AnyModifier,
+    31,
+    1,
+    "Down",
+    vtkWidgetEvent::Down,
+    this,
+    vtkDiskWidget::MoveDiskAction);
+  this->CallbackMapper->SetCallbackMethod(
+    vtkCommand::KeyPressEvent,
+    vtkEvent::AnyModifier,
+    29,
+    1,
+    "Left",
+    vtkWidgetEvent::Down,
+    this,
+    vtkDiskWidget::MoveDiskAction);
+  this->CallbackMapper->SetCallbackMethod(vtkCommand::KeyPressEvent, vtkEvent::AnyModifier, 'n', 1,
+    "n", vtkWidgetEvent::PickNormal, this, vtkDiskWidget::PickNormalAction);
+  this->CallbackMapper->SetCallbackMethod(vtkCommand::KeyPressEvent, vtkEvent::AnyModifier, 'N', 1,
+    "N", vtkWidgetEvent::PickNormal, this, vtkDiskWidget::PickNormalAction);
+  this->CallbackMapper->SetCallbackMethod(vtkCommand::KeyPressEvent, vtkEvent::AnyModifier, 14, 1,
+    "n", vtkWidgetEvent::PickNormal, this, vtkDiskWidget::PickNormalAction);
+}
+
+//----------------------------------------------------------------------------
+vtkDiskWidget::~vtkDiskWidget() = default;
+
+//----------------------------------------------------------------------
+void vtkDiskWidget::SelectAction(vtkAbstractWidget* w)
+{
+  vtkDiskWidget* self = reinterpret_cast<vtkDiskWidget*>(w);
+
+  // Get the event position
+  int X = self->Interactor->GetEventPosition()[0];
+  int Y = self->Interactor->GetEventPosition()[1];
+
+  // We want to update the radius, normal, and center as appropriate
+  reinterpret_cast<vtkDiskRepresentation*>(self->WidgetRep)
+    ->SetInteractionState(vtkDiskRepresentation::Moving);
+  int interactionState = self->WidgetRep->ComputeInteractionState(X, Y);
+  self->UpdateCursorShape(interactionState);
+
+  if (self->WidgetRep->GetInteractionState() == vtkDiskRepresentation::Outside)
+  {
+    return;
+  }
+
+  if (self->Interactor->GetControlKey() && interactionState == vtkDiskRepresentation::MovingWhole)
+  {
+    reinterpret_cast<vtkDiskRepresentation*>(self->WidgetRep)
+      ->SetInteractionState(vtkDiskRepresentation::MovingWhole);
+  }
+
+  // We are definitely selected
+  self->GrabFocus(self->EventCallbackCommand);
+  double eventPos[2];
+  eventPos[0] = static_cast<double>(X);
+  eventPos[1] = static_cast<double>(Y);
+  self->WidgetState = vtkDiskWidget::Active;
+  self->WidgetRep->StartWidgetInteraction(eventPos);
+
+  self->EventCallbackCommand->SetAbortFlag(1);
+  self->StartInteraction();
+  self->InvokeEvent(vtkCommand::StartInteractionEvent, nullptr);
+  self->Render();
+}
+
+//----------------------------------------------------------------------
+void vtkDiskWidget::TranslateAction(vtkAbstractWidget* w)
+{
+  vtkDiskWidget* self = reinterpret_cast<vtkDiskWidget*>(w);
+
+  // Get the event position
+  int X = self->Interactor->GetEventPosition()[0];
+  int Y = self->Interactor->GetEventPosition()[1];
+
+  // We want to compute an orthogonal vector to the pane that has been selected
+  reinterpret_cast<vtkDiskRepresentation*>(self->WidgetRep)
+    ->SetInteractionState(vtkDiskRepresentation::Moving);
+  int interactionState = self->WidgetRep->ComputeInteractionState(X, Y);
+  self->UpdateCursorShape(interactionState);
+
+  if (self->WidgetRep->GetInteractionState() == vtkDiskRepresentation::Outside)
+  {
+    return;
+  }
+
+  // We are definitely selected
+  self->GrabFocus(self->EventCallbackCommand);
+  double eventPos[2];
+  eventPos[0] = static_cast<double>(X);
+  eventPos[1] = static_cast<double>(Y);
+  self->WidgetState = vtkDiskWidget::Active;
+  self->WidgetRep->StartWidgetInteraction(eventPos);
+
+  self->EventCallbackCommand->SetAbortFlag(1);
+  self->StartInteraction();
+  self->InvokeEvent(vtkCommand::StartInteractionEvent, nullptr);
+  self->Render();
+}
+
+//----------------------------------------------------------------------
+void vtkDiskWidget::ScaleAction(vtkAbstractWidget* w)
+{
+  vtkDiskWidget* self = reinterpret_cast<vtkDiskWidget*>(w);
+
+  // Get the event position
+  int X = self->Interactor->GetEventPosition()[0];
+  int Y = self->Interactor->GetEventPosition()[1];
+
+  // We want to compute an orthogonal vector to the pane that has been selected
+  reinterpret_cast<vtkDiskRepresentation*>(self->WidgetRep)
+    ->SetInteractionState(vtkDiskRepresentation::AdjustingRadius);
+  int interactionState = self->WidgetRep->ComputeInteractionState(X, Y);
+  self->UpdateCursorShape(interactionState);
+
+  if (self->WidgetRep->GetInteractionState() == vtkDiskRepresentation::Outside)
+  {
+    return;
+  }
+
+  // We are definitely selected
+  self->GrabFocus(self->EventCallbackCommand);
+  double eventPos[2];
+  eventPos[0] = static_cast<double>(X);
+  eventPos[1] = static_cast<double>(Y);
+  self->WidgetState = vtkDiskWidget::Active;
+  self->WidgetRep->StartWidgetInteraction(eventPos);
+
+  self->EventCallbackCommand->SetAbortFlag(1);
+  self->StartInteraction();
+  self->InvokeEvent(vtkCommand::StartInteractionEvent, nullptr);
+  self->Render();
+}
+
+//----------------------------------------------------------------------
+void vtkDiskWidget::MoveAction(vtkAbstractWidget* w)
+{
+  vtkDiskWidget* self = reinterpret_cast<vtkDiskWidget*>(w);
+
+  // So as to change the cursor shape when the mouse is poised over
+  // the widget. Unfortunately, this results in a few extra picks
+  // due to the cell picker. However given that its picking simple geometry
+  // like the handles/arrows, this should be very quick
+  int X = self->Interactor->GetEventPosition()[0];
+  int Y = self->Interactor->GetEventPosition()[1];
+  int changed = 0;
+
+  if (self->ManagesCursor && self->WidgetState != vtkDiskWidget::Active)
+  {
+    int oldInteractionState =
+      reinterpret_cast<vtkDiskRepresentation*>(self->WidgetRep)->GetInteractionState();
+
+    reinterpret_cast<vtkDiskRepresentation*>(self->WidgetRep)
+      ->SetInteractionState(vtkDiskRepresentation::Moving);
+    int state = self->WidgetRep->ComputeInteractionState(X, Y);
+    changed = self->UpdateCursorShape(state);
+    reinterpret_cast<vtkDiskRepresentation*>(self->WidgetRep)
+      ->SetInteractionState(oldInteractionState);
+    changed = (changed || state != oldInteractionState) ? 1 : 0;
+  }
+
+  // See whether we're active
+  if (self->WidgetState == vtkDiskWidget::Start)
+  {
+    if (changed && self->ManagesCursor)
+    {
+      self->Render();
+    }
+    return;
+  }
+
+  // Okay, adjust the representation
+  double e[2];
+  e[0] = static_cast<double>(X);
+  e[1] = static_cast<double>(Y);
+  self->WidgetRep->WidgetInteraction(e);
+
+  // moving something
+  self->EventCallbackCommand->SetAbortFlag(1);
+  self->InvokeEvent(vtkCommand::InteractionEvent, nullptr);
+  self->Render();
+}
+
+//----------------------------------------------------------------------
+void vtkDiskWidget::EndSelectAction(vtkAbstractWidget* w)
+{
+  vtkDiskWidget* self = reinterpret_cast<vtkDiskWidget*>(w);
+
+  if (
+    self->WidgetState != vtkDiskWidget::Active ||
+    self->WidgetRep->GetInteractionState() == vtkDiskRepresentation::Outside)
+  {
+    return;
+  }
+
+  // Return state to not selected
+  double e[2];
+  self->WidgetRep->EndWidgetInteraction(e);
+  self->WidgetState = vtkDiskWidget::Start;
+  self->ReleaseFocus();
+
+  // Update cursor if managed
+  self->UpdateCursorShape(
+    reinterpret_cast<vtkDiskRepresentation*>(self->WidgetRep)->GetRepresentationState());
+
+  self->EventCallbackCommand->SetAbortFlag(1);
+  self->EndInteraction();
+  self->InvokeEvent(vtkCommand::EndInteractionEvent, nullptr);
+  self->Render();
+}
+
+//----------------------------------------------------------------------
+void vtkDiskWidget::MoveDiskAction(vtkAbstractWidget* w)
+{
+  vtkDiskWidget* self = reinterpret_cast<vtkDiskWidget*>(w);
+
+  reinterpret_cast<vtkDiskRepresentation*>(self->WidgetRep)
+    ->SetInteractionState(vtkDiskRepresentation::Moving);
+
+  int X = self->Interactor->GetEventPosition()[0];
+  int Y = self->Interactor->GetEventPosition()[1];
+  self->WidgetRep->ComputeInteractionState(X, Y);
+
+  // The cursor must be over part of the widget for these key presses to work
+  if (self->WidgetRep->GetInteractionState() == vtkDiskRepresentation::Outside)
+  {
+    return;
+  }
+
+  // Invoke all of the events associated with moving the cylinder
+  self->InvokeEvent(vtkCommand::StartInteractionEvent, nullptr);
+
+  // Move the cylinder
+  double factor = (self->Interactor->GetControlKey() ? 0.5 : 1.0);
+  if (
+    vtkStdString(self->Interactor->GetKeySym()) == vtkStdString("Down") ||
+    vtkStdString(self->Interactor->GetKeySym()) == vtkStdString("Left"))
+  {
+    self->GetDiskRepresentation()->BumpDisk(-1, factor);
+  }
+  else
+  {
+    self->GetDiskRepresentation()->BumpDisk(1, factor);
+  }
+  self->InvokeEvent(vtkCommand::InteractionEvent, nullptr);
+
+  self->EventCallbackCommand->SetAbortFlag(1);
+  self->InvokeEvent(vtkCommand::EndInteractionEvent, nullptr);
+  self->Render();
+}
+
+//----------------------------------------------------------------------
+void vtkDiskWidget::PickNormalAction(vtkAbstractWidget* w)
+{
+  vtkDiskWidget* self = reinterpret_cast<vtkDiskWidget*>(w);
+
+  int X = self->Interactor->GetEventPosition()[0];
+  int Y = self->Interactor->GetEventPosition()[1];
+
+  // Invoke all the events associated with moving the plane
+  self->InvokeEvent(vtkCommand::StartInteractionEvent, nullptr);
+  bool newNormalPicked = self->GetDiskRepresentation()->PickNormal(
+    X, Y, self->Interactor->GetControlKey() == 1);
+  self->InvokeEvent(vtkCommand::InteractionEvent, nullptr);
+  self->EventCallbackCommand->SetAbortFlag(1);
+  self->InvokeEvent(vtkCommand::EndInteractionEvent, nullptr);
+  if (newNormalPicked)
+  {
+    self->Render();
+  }
+}
+
+//----------------------------------------------------------------------
+void vtkDiskWidget::SetEnabled(int enabling)
+{
+  if (this->Enabled == enabling)
+  {
+    return;
+  }
+
+  Superclass::SetEnabled(enabling);
+}
+
+//----------------------------------------------------------------------
+void vtkDiskWidget::CreateDefaultRepresentation()
+{
+  if (!this->WidgetRep)
+  {
+    this->WidgetRep = vtkDiskRepresentation::New();
+  }
+}
+
+//----------------------------------------------------------------------
+void vtkDiskWidget::SetRepresentation(vtkDiskRepresentation* rep)
+{
+  this->Superclass::SetWidgetRepresentation(reinterpret_cast<vtkWidgetRepresentation*>(rep));
+}
+
+//----------------------------------------------------------------------
+int vtkDiskWidget::UpdateCursorShape(int state)
+{
+  // So as to change the cursor shape when the mouse is poised over
+  // the widget.
+  if (this->ManagesCursor)
+  {
+    if (state == vtkDiskRepresentation::Outside)
+    {
+      return this->RequestCursorShape(VTK_CURSOR_DEFAULT);
+    }
+    else if (state == vtkDiskRepresentation::AdjustingRadius)
+    {
+      return this->RequestCursorShape(VTK_CURSOR_SIZEALL);
+    }
+    else
+    {
+      return this->RequestCursorShape(VTK_CURSOR_HAND);
+    }
+  }
+
+  return 0;
+}
+
+//----------------------------------------------------------------------------
+void vtkDiskWidget::PrintSelf(ostream& os, vtkIndent indent)
+{
+  this->Superclass::PrintSelf(os, indent);
+}
diff --git a/smtk/extension/vtk/widgets/vtkDiskWidget.h b/smtk/extension/vtk/widgets/vtkDiskWidget.h
new file mode 100644
index 0000000000000000000000000000000000000000..b1a6c915189588562f42897b0b46e36eb989948e
--- /dev/null
+++ b/smtk/extension/vtk/widgets/vtkDiskWidget.h
@@ -0,0 +1,162 @@
+//=========================================================================
+//  Copyright (c) Kitware, Inc.
+//  All rights reserved.
+//  See LICENSE.txt for details.
+//
+//  This software is distributed WITHOUT ANY WARRANTY; without even
+//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+//  PURPOSE.  See the above copyright notice for more information.
+//=========================================================================
+#ifndef vtkDiskWidget_h
+#define vtkDiskWidget_h
+
+#include "smtk/extension/vtk/widgets/vtkSMTKWidgetsExtModule.h" // For export macro
+#include "vtkAbstractWidget.h"
+
+class vtkDiskRepresentation;
+
+/**
+ * @class   vtkDiskWidget
+ * @brief   3D widget for manipulating an infinite cylinder
+ *
+ * This 3D widget defines a planar, circular disk that can be
+ * interactively placed in a scene. The widget is assumed to consist
+ * of four parts: 1) a disk contained in a 2) bounding box, with a
+ * 3) normal vector, which is rooted at a 4) center point in the bounding
+ * box. (The representation paired with this widget determines the
+ * actual geometry of the widget.)
+ *
+ * To use this widget, you generally pair it with a vtkDiskRepresentation
+ * (or a subclass). Various options are available for controlling how the
+ * representation appears, and how the widget functions.
+ *
+ * @par Event Bindings:
+ * By default, the widget responds to the following VTK events (i.e., it
+ * watches the vtkRenderWindowInteractor for these events):
+ * <pre>
+ * If the normal vector is selected:
+ *   LeftButtonPressEvent - select normal
+ *   LeftButtonReleaseEvent - release (end select) normal
+ *   MouseMoveEvent - orient the normal vector
+ * If the center point (handle) is selected:
+ *   LeftButtonPressEvent - select handle (if on slider)
+ *   LeftButtonReleaseEvent - release handle (if selected)
+ *   MouseMoveEvent - move the center point (constrained to plane or on the
+ *                    axis if CTRL key is pressed)
+ * If the disk is selected:
+ *   LeftButtonPressEvent - select disk
+ *   LeftButtonReleaseEvent - release disk
+ *   MouseMoveEvent - increase/decrease disk radius
+ * If the outline is selected:
+ *   LeftButtonPressEvent - select outline
+ *   LeftButtonReleaseEvent - release outline
+ *   MouseMoveEvent - move the outline
+ * If the keypress characters are used
+ *   'Down/Left' Move disk away from viewer
+ *   'Up/Right' Move disk towards viewer
+ * In all the cases, independent of what is picked, the widget responds to the
+ * following VTK events:
+ *   MiddleButtonPressEvent - move the disk
+ *   MiddleButtonReleaseEvent - release the disk
+ *   RightButtonPressEvent - scale the widget's representation
+ *   RightButtonReleaseEvent - stop scaling the widget
+ *   MouseMoveEvent - scale (if right button) or move (if middle button) the widget
+ * </pre>
+ *
+ * @par Event Bindings:
+ * Note that the event bindings described above can be changed using this
+ * class's vtkWidgetEventTranslator. This class translates VTK events
+ * into the vtkDiskWidget's widget events:
+ * <pre>
+ *   vtkWidgetEvent::Select -- some part of the widget has been selected
+ *   vtkWidgetEvent::EndSelect -- the selection process has completed
+ *   vtkWidgetEvent::Move -- a request for widget motion has been invoked
+ *   vtkWidgetEvent::Up and vtkWidgetEvent::Down -- MoveConeAction
+ * </pre>
+ *
+ * @par Event Bindings:
+ * In turn, when these widget events are processed, the vtkDiskWidget
+ * invokes the following VTK events on itself (which observers can listen for):
+ * <pre>
+ *   vtkCommand::StartInteractionEvent (on vtkWidgetEvent::Select)
+ *   vtkCommand::EndInteractionEvent (on vtkWidgetEvent::EndSelect)
+ *   vtkCommand::InteractionEvent (on vtkWidgetEvent::Move)
+ * </pre>
+ *
+ *
+ * @sa
+ * vtk3DWidget vtkImplicitPlaneWidget
+*/
+class VTKSMTKWIDGETSEXT_EXPORT vtkDiskWidget : public vtkAbstractWidget
+{
+public:
+  /**
+   * Instantiate the object.
+   */
+  static vtkDiskWidget* New();
+
+  //@{
+  /**
+   * Standard vtkObject methods
+   */
+  vtkTypeMacro(vtkDiskWidget, vtkAbstractWidget);
+  void PrintSelf(ostream& os, vtkIndent indent) override;
+  //@}
+
+  vtkDiskWidget(const vtkDiskWidget&) = delete;
+  vtkDiskWidget& operator=(const vtkDiskWidget&) = delete;
+
+  /**
+   * Specify an instance of vtkWidgetRepresentation used to represent this
+   * widget in the scene. Note that the representation is a subclass of vtkProp
+   * so it can be added to the renderer independent of the widget.
+   */
+  void SetRepresentation(vtkDiskRepresentation* rep);
+
+  /// Control widget interactivity, allowing users to interact with the camera or other widgets.
+  ///
+  /// The camera is unobserved when the widget is disabled.
+  void SetEnabled(int enabling) override;
+
+  /**
+   * Return the representation as a vtkDiskRepresentation.
+   */
+  vtkDiskRepresentation* GetDiskRepresentation()
+  {
+    return reinterpret_cast<vtkDiskRepresentation*>(this->WidgetRep);
+  }
+
+  /**
+   * Create the default widget representation if one is not set.
+   */
+  void CreateDefaultRepresentation() override;
+
+protected:
+  vtkDiskWidget();
+  ~vtkDiskWidget() override;
+
+  // Manage the state of the widget
+  int WidgetState;
+  enum _WidgetState
+  {
+    Start = 0,
+    Active
+  };
+
+  // These methods handle events
+  static void SelectAction(vtkAbstractWidget*);
+  static void TranslateAction(vtkAbstractWidget*);
+  static void ScaleAction(vtkAbstractWidget*);
+  static void EndSelectAction(vtkAbstractWidget*);
+  static void MoveAction(vtkAbstractWidget*);
+  static void MoveDiskAction(vtkAbstractWidget*);
+  static void PickNormalAction(vtkAbstractWidget*);
+
+  /**
+   * Update the cursor shape based on the interaction state. Returns 1
+   * if the cursor shape requested is different from the existing one.
+   */
+  int UpdateCursorShape(int interactionState);
+};
+
+#endif
diff --git a/smtk/markup/CMakeLists.txt b/smtk/markup/CMakeLists.txt
index 8e0a735ea79de50c9f01fd25ab3d75ea76bd74be..d3ee3570c03583df31f921b58d1930e19a1c3a84 100644
--- a/smtk/markup/CMakeLists.txt
+++ b/smtk/markup/CMakeLists.txt
@@ -10,6 +10,7 @@ set(headers
   testing/cxx/helpers.h
 )
 set(operations
+  ChangeUnits
   Create
   CreateArc
   CreateAnalyticShape
diff --git a/smtk/markup/ImageData.h b/smtk/markup/ImageData.h
index 96469de7fa8265d58f6c3d5038b5d41c96471e72..4e5e9413e959e2ccae0b47244bb767670ba4d01b 100644
--- a/smtk/markup/ImageData.h
+++ b/smtk/markup/ImageData.h
@@ -79,7 +79,15 @@ public:
   /// Do not call this method outside of an operation and be aware
   /// that it _may_ modify m_pointIds and m_cellIds as well.
   bool setShapeData(vtkSmartPointer<vtkImageData> image, Superclass::ShapeOptions& options);
+  /// Return the geometric content for this node as a vtkImageData.
+  ///
+  /// This method is not inherited and provides output in the node's native format.
   vtkSmartPointer<vtkImageData> shapeData() const { return m_image; }
+  /// Return the geometric content for this node.
+  ///
+  /// This method is inherited from DiscreteGeometry and does not make any assumptions
+  /// about the type of data returned.
+  vtkSmartPointer<vtkDataObject> shape() const override { return m_image; }
 
   /// Assign this node's state from \a source.
   bool assign(const smtk::graph::Component::ConstPtr& source, smtk::resource::CopyOptions& options)
diff --git a/smtk/markup/Registrar.cxx b/smtk/markup/Registrar.cxx
index 47c90a2724e3f8f6f2c7c7b89dd246912f9396db..dd642236275248ba1a17548f52c19982e31e514c 100644
--- a/smtk/markup/Registrar.cxx
+++ b/smtk/markup/Registrar.cxx
@@ -11,6 +11,7 @@
 //=============================================================================
 #include "smtk/markup/Registrar.h"
 
+#include "smtk/markup/operators/ChangeUnits.h"
 #include "smtk/markup/operators/Create.h"
 #include "smtk/markup/operators/CreateAnalyticShape.h"
 #include "smtk/markup/operators/CreateArc.h"
@@ -48,6 +49,7 @@ namespace markup
 namespace
 {
 using OperationList = std::tuple<
+  ChangeUnits,
   Create,
   CreateArc,
   CreateAnalyticShape,
diff --git a/smtk/markup/Resource.cxx b/smtk/markup/Resource.cxx
index 336a3122265b8f6c67147eac083ed5ae34411693..f912ab8809ed0dd1647e6b358d5f21a9401809d0 100644
--- a/smtk/markup/Resource.cxx
+++ b/smtk/markup/Resource.cxx
@@ -23,6 +23,8 @@
 #include "smtk/common/Paths.h"
 #include "smtk/common/StringUtil.h"
 
+#include "units/System.h"
+
 namespace smtk
 {
 namespace markup
@@ -137,6 +139,29 @@ std::function<bool(const smtk::resource::Component&)> Resource::queryOperation(
   return smtk::resource::filter::Filter<smtk::graph::filter::Grammar>(query);
 }
 
+bool Resource::setLengthUnit(const std::string& unit)
+{
+  auto unitSys = this->unitSystem();
+  if (unit == m_lengthUnit || !unitSys)
+  {
+    return false;
+  }
+  bool ok = true;
+  auto uu = unitSys->unit(unit, &ok);
+  if (!ok)
+  {
+    // Unit must be a valid unit.
+    return false;
+  }
+  if (uu.dimension() != unitSys->dimension("L"))
+  {
+    // Units must be length.
+    return false;
+  }
+  m_lengthUnit = unit;
+  return true;
+}
+
 void Resource::initialize()
 {
   using namespace smtk::string::literals; // for ""_token
diff --git a/smtk/markup/Resource.h b/smtk/markup/Resource.h
index f65cbd153fd01dd0a852e42812dfbd3cbde4f6d1..6e16182dd1ddb63394124dac5f6e9977da86af45 100644
--- a/smtk/markup/Resource.h
+++ b/smtk/markup/Resource.h
@@ -98,6 +98,21 @@ public:
     */
   static DomainFactory& domainFactory() { return s_domainFactory; }
 
+  /**\brief Set/get the default units of length for geometric data in this resource.
+    *
+    * When the default is empty (which it is by default), no units are provided.
+    * Otherwise, all geometric data is assumed to be in these units.
+    *
+    * You should not modify the length unit while geometric data exists inside
+    * the resource.
+    *
+    * If there is no valid unit system or if \a unit is invalid
+    * (i.e., not a valid unit or with dimensions other than length),
+    * setLengthUnit() will fail.
+    */
+  std::string lengthUnit() const { return m_lengthUnit; }
+  bool setLengthUnit(const std::string& unit);
+
 protected:
   friend class Component;
 
@@ -111,6 +126,8 @@ protected:
 
   DomainMap m_domains;
   static DomainFactory s_domainFactory;
+
+  std::string m_lengthUnit;
 };
 
 template<typename Modifier>
diff --git a/smtk/markup/SpatialData.cxx b/smtk/markup/SpatialData.cxx
index 2b2cf3e3f58e962b760f6cf1d8602e63d493e400..267690259a221426fb4e8d1cf55a24a845c0f76b 100644
--- a/smtk/markup/SpatialData.cxx
+++ b/smtk/markup/SpatialData.cxx
@@ -47,5 +47,39 @@ AssignedIds* SpatialData::domainExtent(smtk::string::Token domainName) const
   return nullptr;
 }
 
+bool SpatialData::isBlanked() const
+{
+  const auto& boolProps = this->properties().get<bool>();
+  if (!boolProps.contains("_blanked"))
+  {
+    return false;
+  }
+  return boolProps.at("_blanked");
+}
+
+bool SpatialData::setBlanking(bool shouldBlank)
+{
+  if (!this->properties().contains<bool>("_blanked"))
+  {
+    // Already not blanked? Do nothing.
+    if (!shouldBlank) { return false; }
+    this->properties().emplace<bool>("_blanked", true);
+    return true;
+  }
+  // Already blanked? Do nothing
+  if (this->properties().at<bool>("_blanked") == shouldBlank)
+  {
+    return false;
+  }
+  if (shouldBlank)
+  {
+    this->properties().emplace<bool>("_blanked", true);
+    return true;
+  }
+  this->properties().erase<bool>("_blanked");
+  return true;
+}
+
+
 } // namespace markup
 } // namespace smtk
diff --git a/smtk/markup/SpatialData.h b/smtk/markup/SpatialData.h
index b4b62b7986ed4abb560ba22300d5adc7238d123a..ffe15a02fa102ea128bdd4fc4cd7339a339700df 100644
--- a/smtk/markup/SpatialData.h
+++ b/smtk/markup/SpatialData.h
@@ -69,6 +69,21 @@ public:
   virtual AssignedIds* domainExtent(Domain* domain) const;
   virtual AssignedIds* domainExtent(smtk::string::Token domainName) const;
 
+  /// Return whether or not this node has its geometry blanked (i.e., not rendered).
+  bool isBlanked() const;
+
+  /// Set whether or not this node has its geometry blanked.
+  ///
+  /// Blanking a node's geometry is usually performed by an operation that wishes to
+  /// keep the source geometry around but also permanently transform it in some reversible
+  /// or editable way. While the markup session also allows transform properties
+  /// attached to nodes to affect rendering, this can cause issues for downstream filters
+  /// that need access to the transformed geometry (and do not wish to perform the transform
+  /// themselves).
+  ///
+  /// This method returns true when the node's blanking state was modified.
+  bool setBlanking(bool shouldBlank);
+
 protected:
 };
 
diff --git a/smtk/markup/Traits.h b/smtk/markup/Traits.h
index f6af4bfee157e57e84674284c2459e5af2bdcac8..1edda988703a00327ac678c8f1513f5daee1fb96 100644
--- a/smtk/markup/Traits.h
+++ b/smtk/markup/Traits.h
@@ -147,6 +147,7 @@ struct SMTKMARKUP_EXPORT BoundariesToShapes
   static constexpr graph::OwnershipSemantics semantics =
     graph::OwnershipSemantics::ToNodeOwnsFromNode;
 };
+
 } // namespace arcs
 
 // Forward-declare domain types
diff --git a/smtk/markup/UnstructuredData.h b/smtk/markup/UnstructuredData.h
index 5b3c60c86d35ddef120868abcee2b18ce21dc23e..e3594bef8e87bf6e77a5a81b7ec93ecb805e4875 100644
--- a/smtk/markup/UnstructuredData.h
+++ b/smtk/markup/UnstructuredData.h
@@ -75,7 +75,16 @@ public:
 
   /// Assign the \a mesh data as this object's shape and update Field children to match.
   bool setShapeData(vtkSmartPointer<vtkDataObject> mesh, ShapeOptions& options);
+  /// Return the geometric content for this node as a vtkDataObject.
+  ///
+  /// Because unstructured data may be modeled with vtkPolyData, vtkUnstructuredGrid,
+  /// or vtkCellGrid data, there is no better type to return.
   vtkSmartPointer<vtkDataObject> shapeData() const;
+  /// Return the geometric content for this node.
+  ///
+  /// This method is inherited from DiscreteGeometry and does not make any assumptions
+  /// about the type of data returned.
+  vtkSmartPointer<vtkDataObject> shape() const override { return m_mesh; }
 
   const AssignedIds& pointIds() const { return *m_pointIds; }
   const AssignedIds& cellIds() const { return *m_cellIds; }
diff --git a/smtk/markup/json/jsonResource.cxx b/smtk/markup/json/jsonResource.cxx
index d219b27319b973d8ba3e2609108e651f72269158..ad2dea5b93edae956f69607875818c2aad68c860 100644
--- a/smtk/markup/json/jsonResource.cxx
+++ b/smtk/markup/json/jsonResource.cxx
@@ -217,6 +217,12 @@ void to_json(nlohmann::json& j, const smtk::markup::Resource::Ptr& resource)
     j["domains"] = domainData;
   }
 
+  // Record length unit (if one exists).
+  if (!resource->lengthUnit().empty())
+  {
+    j["length-unit"] = resource->lengthUnit();
+  }
+
   // Record string-token hashes.
   // Some nodes may use string tokens, so we must serialize that map if it exists.
   auto& tokenManager = smtk::string::Token::manager();
@@ -280,6 +286,13 @@ void from_json(const nlohmann::json& jj, smtk::markup::Resource::Ptr& resource)
     }
   }
 
+  // Deserialize length unit.
+  auto jLengthUnit = jj.find("length-unit");
+  if (jLengthUnit != jj.end())
+  {
+    resource->setLengthUnit(jLengthUnit->get<std::string>());
+  }
+
   // Deserialize domains
   auto jDomains = jj.find("domains");
   if (jDomains != jj.end())
diff --git a/smtk/markup/operators/ChangeUnits.cxx b/smtk/markup/operators/ChangeUnits.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..e85ca37212465e4e1d85291382877e2aa44bbe72
--- /dev/null
+++ b/smtk/markup/operators/ChangeUnits.cxx
@@ -0,0 +1,158 @@
+//=========================================================================
+//  Copyright (c) Kitware, Inc.
+//  All rights reserved.
+//  See LICENSE.txt for details.
+//
+//  This software is distributed WITHOUT ANY WARRANTY; without even
+//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+//  PURPOSE.  See the above copyright notice for more information.
+//=========================================================================
+
+#include "smtk/markup/operators/ChangeUnits.h"
+
+#include "smtk/markup/DiscreteGeometry.h"
+
+#include "smtk/operation/MarkGeometry.h"
+
+#include "smtk/attribute/Attribute.h"
+#include "smtk/attribute/ReferenceItem.h"
+#include "smtk/attribute/StringItem.h"
+
+#include "vtkImageData.h"
+#include "vtkPoints.h"
+#include "vtkPointSet.h"
+#include "vtkSMPTools.h"
+#include "vtkVector.h"
+
+#include "units/Converter.h"
+#include "units/System.h"
+#include "units/Unit.h"
+
+#include "smtk/markup/operators/ChangeUnits_xml.h"
+
+namespace smtk
+{
+namespace markup
+{
+
+bool ChangeUnits::ableToOperate()
+{
+  if (!this->Superclass::ableToOperate())
+  {
+    return false;
+  }
+
+  std::string srcLengthUnitStr = this->parameters()->findString("source units")->value();
+
+  auto assocs = this->parameters()->associations();
+  bool ok = assocs->numberOfValues() > 0;
+  for (const auto& assoc : *assocs)
+  {
+    auto* rsrc = dynamic_cast<smtk::markup::Resource*>(assoc->parentResource());
+    auto usys = rsrc ? rsrc->unitSystem() : nullptr;
+    // Every object associated must live in a resource with a unit system and default length unit
+    // that can be converted to the length unit for this operation.
+    if (!usys || rsrc->lengthUnit().empty())
+    {
+      ok = false;
+      break;
+    }
+
+    auto srcUnit = usys->unit(srcLengthUnitStr, &ok);
+    if (!ok)
+    {
+      break;
+    }
+    auto dstUnit = usys->unit(rsrc->lengthUnit(), &ok);
+    if (!ok)
+    {
+      break;
+    }
+    auto cvt = usys->convert(srcUnit, dstUnit);
+    if (!cvt)
+    {
+      ok = false;
+      break;
+    }
+  }
+  return ok;
+}
+
+ChangeUnits::Result ChangeUnits::operateInternal()
+{
+  std::string srcLengthUnitStr = this->parameters()->findString("source units")->value();
+  auto assocs = this->parameters()->associations();
+  auto result = this->createResult(ChangeUnits::Outcome::SUCCEEDED);
+  auto mod = result->findComponent("modified");
+  mod->setNumberOfValues(assocs->numberOfValues());
+  for (const auto& assoc : *assocs)
+  {
+    auto* rsrc = dynamic_cast<smtk::markup::Resource*>(assoc->parentResource());
+    auto usys = rsrc ? rsrc->unitSystem() : nullptr;
+    if (!usys) { continue; }
+    bool ok;
+    auto srcUnit = usys->unit(srcLengthUnitStr, &ok);
+    if (!ok) { continue; }
+    auto dstUnit = usys->unit(rsrc->lengthUnit(), &ok);
+    if (!ok) { continue; }
+    auto cvt = usys->convert(srcUnit, dstUnit);
+    auto spatialData = std::dynamic_pointer_cast<smtk::markup::DiscreteGeometry>(assoc);
+    auto mesh = spatialData->shape();
+    if (!cvt || !mesh)
+    {
+      ok = false;
+      break;
+    }
+    if (auto* image = vtkImageData::SafeDownCast(mesh))
+    {
+      vtkVector3d xx;
+      vtkVector3d yy;
+      image->GetOrigin(xx.GetData());
+      image->GetSpacing(yy.GetData());
+      for (int ii = 0; ii < 3; ++ii)
+      {
+        xx[ii] = cvt->transform(xx[ii]);
+        yy[ii] = cvt->transform(yy[ii]);
+      }
+      image->SetOrigin(xx.GetData());
+      image->SetSpacing(yy.GetData());
+      mesh->Modified();
+      smtk::operation::MarkGeometry().markModified(assoc);
+    }
+    else if (auto* pset = vtkPointSet::SafeDownCast(mesh))
+    {
+      auto* pts = pset->GetPoints();
+      vtkSMPTools::For(0, pset->GetNumberOfPoints(), [&](vtkIdType begin, vtkIdType end)
+        {
+          vtkVector3d xx;
+          for (vtkIdType pp = begin; pp < end; ++pp)
+          {
+            pts->GetPoint(pp, xx.GetData());
+            for (int ii = 0; ii < 3; ++ii)
+            {
+              xx[ii] = cvt->transform(xx[ii]);
+            }
+            pts->SetPoint(pp, xx.GetData());
+          }
+        }
+      );
+      mesh->Modified();
+      smtk::operation::MarkGeometry().markModified(spatialData);
+    }
+    else
+    {
+      smtkErrorMacro(this->log(),
+        "Unhandled shape type '" << mesh->GetClassName() << "' for "
+        "component '" << assoc->name() << "' (" << assoc->typeName() << ").");
+    }
+  }
+  return result;
+}
+
+const char* ChangeUnits::xmlDescription() const
+{
+  return ChangeUnits_xml;
+}
+
+} // namespace markup
+} // namespace smtk
diff --git a/smtk/markup/operators/ChangeUnits.h b/smtk/markup/operators/ChangeUnits.h
new file mode 100644
index 0000000000000000000000000000000000000000..ff909c9b76b03251e311084cd22349e4ec849457
--- /dev/null
+++ b/smtk/markup/operators/ChangeUnits.h
@@ -0,0 +1,54 @@
+//=========================================================================
+//  Copyright (c) Kitware, Inc.
+//  All rights reserved.
+//  See LICENSE.txt for details.
+//
+//  This software is distributed WITHOUT ANY WARRANTY; without even
+//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+//  PURPOSE.  See the above copyright notice for more information.
+//=========================================================================
+#ifndef smtk_markup_ChangeUnits_h
+#define smtk_markup_ChangeUnits_h
+
+#include "smtk/markup/Resource.h"
+
+#include "smtk/operation/XMLOperation.h"
+
+namespace smtk
+{
+namespace markup
+{
+
+/**\brief Scale geometric data of associated objects from one length unit to another.
+  *
+  * If the destination unit is empty (the default) and the resource has a
+  * unit system with an active context (that specifies default units for
+  * each dimension), then the default length unit will be the destination.
+  *
+  * If the input association is spatial data, then the input nodes are
+  * blanked and output versions are created with the transformed geometry.
+  *
+  * If the input association is a markup resource, this changes the
+  * default length unit for the resource (and rescales all of its spatial
+  * data components if the default length unit was different but valid;
+  * no rescaling is performed if there was no prior default length unit).
+  */
+class SMTKMARKUP_EXPORT ChangeUnits : public smtk::operation::XMLOperation
+{
+public:
+  smtkTypeMacro(smtk::markup::ChangeUnits);
+  smtkCreateMacro(ChangeUnits);
+  smtkSharedFromThisMacro(smtk::operation::Operation);
+  smtkSuperclassMacro(smtk::operation::XMLOperation);
+
+  bool ableToOperate() override;
+
+protected:
+  Result operateInternal() override;
+  const char* xmlDescription() const override;
+};
+
+} // namespace markup
+} // namespace smtk
+
+#endif // smtk_markup_ChangeUnits_h
diff --git a/smtk/markup/operators/ChangeUnits.sbt b/smtk/markup/operators/ChangeUnits.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..20be19b824b7a0987dc90aaf05741ca95ff2945b
--- /dev/null
+++ b/smtk/markup/operators/ChangeUnits.sbt
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Description of the markup resource's "create" Operation -->
+<SMTK_AttributeResource Version="3">
+  <Definitions>
+    <include href="smtk/operation/Operation.xml"/>
+    <AttDef Type="change units" Label="change units" BaseType="operation">
+      <AssociationsDef LockType="Write">
+        <Accepts>
+          <!-- Accept any spatial markup component (though only discrete handled now) -->
+          <Resource Name="smtk::markup::Resource" Filter="smtk::markup::SpatialData"/>
+        </Accepts>
+      </AssociationsDef>
+      <ItemDefinitions>
+        <String Name="source units" Label="source units">
+          <BriefDescription>
+            The length units the geometry is specified in; the geometry
+          </BriefDescription>
+          <DetailedDescription>
+            The length units the geometry is specified in; the geometry
+            will be converted from this length unit to the resource's default length units.
+            Examples include "mm", "meter", "inch", "foot", "yard", or even "furlong".
+          </DetailedDescription>
+        </String>
+      </ItemDefinitions>
+    </AttDef>
+    <!-- Result -->
+    <include href="smtk/operation/Result.xml"/>
+    <AttDef Type="result(set name)" BaseType="result"/>
+  </Definitions>
+</SMTK_AttributeResource>
diff --git a/smtk/markup/operators/Import.cxx b/smtk/markup/operators/Import.cxx
index 5770b9cbe4711c93e082860f584a99cec4330bb2..744a23f71dab7c41bca71b99b4a50176abb8ed83 100644
--- a/smtk/markup/operators/Import.cxx
+++ b/smtk/markup/operators/Import.cxx
@@ -18,6 +18,8 @@
 
 #include "smtk/extension/vtk/io/ImportAsVTKData.h"
 
+#include "smtk/view/Selection.h"
+
 #include "smtk/attribute/Attribute.h"
 #include "smtk/attribute/ComponentItem.h"
 #include "smtk/attribute/FileItem.h"
@@ -29,6 +31,7 @@
 #include "smtk/attribute/StringItem.h"
 
 #include "smtk/operation/MarkGeometry.h"
+#include "smtk/operation/Hints.h"
 
 #include "smtk/resource/Manager.h"
 
@@ -414,6 +417,11 @@ Import::Result Import::operateInternal()
   {
     m_result->findInt("outcome")->setValue(0, static_cast<int>(Import::Outcome::SUCCEEDED));
     m_result->findResource("resourcesCreated")->appendValue(resource);
+
+    auto created = m_result->findComponent("created");
+    std::set<smtk::resource::Component::Ptr> importedComponents;
+    importedComponents.insert(created->begin(), created->end());
+    smtk::operation::addSelectionHint(m_result, importedComponents, smtk::view::SelectionAction::UNFILTERED_REPLACE);
   }
   return m_result;
 }
diff --git a/smtk/markup/operators/Import.sbt b/smtk/markup/operators/Import.sbt
index d22f03234dd194dad43294b0c8e9ce0c7ff89267..06855f1bbd7703aea84a31bf8fb48c9dcfadf358 100644
--- a/smtk/markup/operators/Import.sbt
+++ b/smtk/markup/operators/Import.sbt
@@ -24,6 +24,7 @@
     </AttDef>
     <!-- Result -->
     <include href="smtk/operation/Result.xml"/>
+    <include href="smtk/operation/Hints.xml"/>
     <AttDef Type="result(import)" BaseType="result">
       <ItemDefinitions>
         <Void Name="allow camera reset" Optional="true" IsEnabledByDefault="true" AdvanceLevel="11"/>
diff --git a/smtk/markup/operators/Write.cxx b/smtk/markup/operators/Write.cxx
index 2921f59e243f2e353a4723462afeb98a2871dbad..2bdd7ffe2fa8d946b1215071875a041a8c424662 100644
--- a/smtk/markup/operators/Write.cxx
+++ b/smtk/markup/operators/Write.cxx
@@ -203,6 +203,17 @@ void Write::markModifiedResources(Write::Result& /*unused*/)
   }
 }
 
+void Write::generateSummary(Result& res)
+{
+  if (smtk::operation::outcome(res) != Outcome::SUCCEEDED)
+  {
+    this->Superclass::generateSummary(res);
+  }
+  auto resourceItem = this->parameters()->associations();
+  auto resource = std::dynamic_pointer_cast<smtk::resource::Resource>(resourceItem->value());
+  smtkInfoMacro(this->log(), "Wrote \"" << resource->location() << "\".");
+}
+
 bool Write::writeData(
   const Component* dataNode,
   const std::string& filename,
diff --git a/smtk/markup/operators/Write.h b/smtk/markup/operators/Write.h
index 0f8dd40cf1e183216cfa3994072ac85a460837f6..41e4d35cb2d63a927018f0b704c08c2ef6171499 100644
--- a/smtk/markup/operators/Write.h
+++ b/smtk/markup/operators/Write.h
@@ -38,6 +38,7 @@ protected:
   Result operateInternal() override;
   const char* xmlDescription() const override;
   void markModifiedResources(Result&) override;
+  void generateSummary(Result&) override;
 
   bool
   writeData(const Component* dataNode, const std::string& filename, smtk::string::Token mimeType);
diff --git a/smtk/markup/pybind11/CMakeLists.txt b/smtk/markup/pybind11/CMakeLists.txt
index 245c4015a5d7f1d7d41855a832164270b16de3fe..8bd8126cc7f839019aa9f637368c5f8fdc337cb9 100644
--- a/smtk/markup/pybind11/CMakeLists.txt
+++ b/smtk/markup/pybind11/CMakeLists.txt
@@ -9,7 +9,13 @@ pybind11_add_module(${library_name}
 target_include_directories(${library_name} PUBLIC
   $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}>
 )
-target_link_libraries(${library_name} LINK_PUBLIC smtkCore smtkMarkup)
+target_link_libraries(${library_name}
+  LINK_PUBLIC
+    smtkMarkup
+    smtkCore
+    VTK::CommonCore
+    VTK::WrappingPythonCore
+)
 set_target_properties(${library_name}
   PROPERTIES
     CXX_VISIBILITY_PRESET hidden
diff --git a/smtk/markup/pybind11/PybindMarkup.cxx b/smtk/markup/pybind11/PybindMarkup.cxx
index 6869fad85eec9c8892026319bb64e518479d1fb9..29ef5e1ecc710a970aa3d1229e1ef1f1de056a30 100644
--- a/smtk/markup/pybind11/PybindMarkup.cxx
+++ b/smtk/markup/pybind11/PybindMarkup.cxx
@@ -23,8 +23,10 @@ using PySharedPtrClass = py::class_<T, std::shared_ptr<T>, Args...>;
 
 #include "PybindResource.h"
 #include "PybindComponent.h"
+#include "PybindSpatialData.h"
 #include "PybindDomain.h"
 #include "PybindDomainMap.h"
+#include "PybindUnstructuredData.h"
 
 #include "smtk/resource/Manager.h"
 
@@ -44,4 +46,6 @@ PYBIND11_MODULE(_smtkPybindMarkup, markup)
   auto smtk_markup_Component = pybind11_init_smtk_markup_Component(markup);
   auto smtk_markup_Domain = pybind11_init_smtk_markup_Domain(markup);
   auto smtk_markup_DomainMap = pybind11_init_smtk_markup_DomainMap(markup);
+  auto smtk_markup_SpatialData = pybind11_init_smtk_markup_SpatialData(markup);
+  auto smtk_markup_UnstructuredData = pybind11_init_smtk_markup_UnstructuredData(markup);
 }
diff --git a/smtk/markup/pybind11/PybindResource.h b/smtk/markup/pybind11/PybindResource.h
index 9e1a119986cce19b300c343315cda126bee34598..3ee7ded19a84088feabf113532d9bc065f974c7b 100644
--- a/smtk/markup/pybind11/PybindResource.h
+++ b/smtk/markup/pybind11/PybindResource.h
@@ -27,6 +27,8 @@ inline PySharedPtrClass< smtk::markup::Resource> pybind11_init_smtk_markup_Resou
   instance
     .def_static("create", []() { return smtk::markup::Resource::create(); })
     .def("domains", [](smtk::markup::Resource& rr) { return rr.domains(); })
+    .def("lengthUnit", &smtk::markup::Resource::lengthUnit)
+    .def("setLengthUnit", &smtk::markup::Resource::setLengthUnit, py::arg("lengthUnit"))
     ;
   return instance;
 }
diff --git a/smtk/markup/pybind11/PybindSpatialData.h b/smtk/markup/pybind11/PybindSpatialData.h
new file mode 100644
index 0000000000000000000000000000000000000000..049eac0c5d0d48e17581a75a90acbb7f98fcb663
--- /dev/null
+++ b/smtk/markup/pybind11/PybindSpatialData.h
@@ -0,0 +1,35 @@
+//=========================================================================
+//  Copyright (c) Kitware, Inc.
+//  All rights reserved.
+//  See LICENSE.txt for details.
+//
+//  This software is distributed WITHOUT ANY WARRANTY; without even
+//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+//  PURPOSE.  See the above copyright notice for more information.
+//=========================================================================
+
+#ifndef pybind_smtk_markup_SpatialData_h
+#define pybind_smtk_markup_SpatialData_h
+
+#include <pybind11/pybind11.h>
+
+#include "smtk/markup/SpatialData.h"
+
+#include "smtk/common/UUID.h"
+#include "smtk/common/pybind11/PybindUUIDTypeCaster.h"
+
+namespace py = pybind11;
+
+inline PySharedPtrClass< smtk::markup::SpatialData> pybind11_init_smtk_markup_SpatialData(py::module &m)
+{
+  PySharedPtrClass< smtk::markup::SpatialData, smtk::markup::Component> instance(m, "SpatialData");
+  instance
+    .def("setBlanking", &smtk::markup::SpatialData::setBlanking, py::arg("shouldBlank"))
+    .def("isBlanked", &smtk::markup::SpatialData::isBlanked)
+    .def_static("CastTo", [](const std::shared_ptr<smtk::resource::PersistentObject>& obj)
+      { return std::dynamic_pointer_cast<smtk::markup::SpatialData>(obj); })
+    ;
+  return instance;
+}
+
+#endif
diff --git a/smtk/markup/pybind11/PybindUnstructuredData.h b/smtk/markup/pybind11/PybindUnstructuredData.h
new file mode 100644
index 0000000000000000000000000000000000000000..0541d8de087392506a1d031811e5e4fff7fa6795
--- /dev/null
+++ b/smtk/markup/pybind11/PybindUnstructuredData.h
@@ -0,0 +1,36 @@
+//=========================================================================
+//  Copyright (c) Kitware, Inc.
+//  All rights reserved.
+//  See LICENSE.txt for details.
+//
+//  This software is distributed WITHOUT ANY WARRANTY; without even
+//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+//  PURPOSE.  See the above copyright notice for more information.
+//=========================================================================
+
+#ifndef pybind_smtk_markup_UnstructuredData_h
+#define pybind_smtk_markup_UnstructuredData_h
+
+#include <pybind11/pybind11.h>
+
+#include "smtk/markup/UnstructuredData.h"
+
+#include "smtk/extension/vtk/pybind11/PybindVTKTypeCaster.h"
+
+#include "smtk/common/UUID.h"
+#include "smtk/common/pybind11/PybindUUIDTypeCaster.h"
+
+namespace py = pybind11;
+
+inline PySharedPtrClass< smtk::markup::UnstructuredData> pybind11_init_smtk_markup_UnstructuredData(py::module &m)
+{
+  PySharedPtrClass< smtk::markup::UnstructuredData, smtk::markup::SpatialData> instance(m, "UnstructuredData");
+  instance
+    .def("shape", &smtk::markup::UnstructuredData::shape)
+    .def_static("CastTo", [](const std::shared_ptr<smtk::resource::PersistentObject>& obj)
+      { return std::dynamic_pointer_cast<smtk::markup::UnstructuredData>(obj); })
+    ;
+  return instance;
+}
+
+#endif
diff --git a/smtk/markup/testing/python/markupResource.py b/smtk/markup/testing/python/markupResource.py
index e9c42e67addc835e890f0b354f53233cb5e02eb9..7a944e774eb5dabc638a2ad5233356b983e6d9dd 100644
--- a/smtk/markup/testing/python/markupResource.py
+++ b/smtk/markup/testing/python/markupResource.py
@@ -19,6 +19,25 @@ print('Node Types\n  ' + '\n  '.join([xx.data()
       for xx in resource.nodeTypes()]))
 print('Arc Types\n  ' + '\n  '.join([xx.data() for xx in resource.arcTypes()]))
 
+expectedNodeset = set([
+    'URL', 'UnstructuredData', 'Comment', 'Plane', 'Cone', 'Label', 'DiscreteGeometry',
+    'Ontology', 'Field', 'AnalyticShape', 'SideSet', 'Box', 'OntologyIdentifier',
+    'Subset', 'NodeSet', 'Landmark', 'SpatialData', 'ImageData', 'Component', 'Sphere',
+    'Feature', 'Group'])
+fqnsNodes = {smtk.string.Token('smtk::markup::' + xx) for xx in expectedNodeset}
+if fqnsNodes != set(resource.nodeTypes()):
+    print('Error: unexpected nodeset.')
+    raise RuntimeError('Unexpected nodeset')
+
+expectedArcset = set([
+  'URLsToData', 'ReferencesToPrimaries', 'LabelsToSubjects', 'OntologyIdentifiersToSubtypes',
+  'URLsToImportedData', 'OntologyIdentifiersToIndividuals', 'FieldsToShapes',
+  'OntologyToIdentifiers', 'GroupsToMembers', 'BoundariesToShapes'])
+fqnsArcs = { smtk.string.Token('smtk::markup::arcs::' + xx) for xx in expectedArcset }
+if fqnsArcs != set(resource.arcTypes()):
+    print('Error: unexpected arcset.')
+    raise RuntimeError('Unexpected arcset')
+
 # Create one node of every type
 nodes = [resource.createNodeOfType(nodeTypeName)
          for nodeTypeName in resource.nodeTypes()]
@@ -48,5 +67,15 @@ if not didConnect:
 
 
 resource.dump('')
-print(resource.domains())
-print(resource.domains().keys())
+# print(resource.domains())
+print('Domains\n  ' + '\n  '.join([xx.data() for xx in resource.domains().keys()]))
+
+print('Test blanking')
+nodeToBlank = resource.createNodeOfType(smtk.string.Token('smtk::markup::UnstructuredData'))
+bbefore = nodeToBlank.isBlanked()
+print('  Is blanked initially?', nodeToBlank.isBlanked())
+nodeToBlank.setBlanking(True)
+print('  Is blanked finally?', nodeToBlank.isBlanked())
+bafter = nodeToBlank.isBlanked()
+if bbefore or not bafter:
+    raise ('Blanking incorrect')
diff --git a/smtk/mesh/pybind11/PybindDeleteMesh.h b/smtk/mesh/pybind11/PybindDeleteMesh.h
index be400746205fc61c3e6ecc639cae85af8f394f8a..8623beca0b4f3d3db81b85196a7026685bbb0c13 100644
--- a/smtk/mesh/pybind11/PybindDeleteMesh.h
+++ b/smtk/mesh/pybind11/PybindDeleteMesh.h
@@ -23,7 +23,6 @@ inline PySharedPtrClass< smtk::mesh::DeleteMesh, smtk::operation::XMLOperation >
 {
   PySharedPtrClass< smtk::mesh::DeleteMesh, smtk::operation::XMLOperation > instance(m, "DeleteMesh");
   instance
-    .def("deepcopy", (smtk::mesh::DeleteMesh & (smtk::mesh::DeleteMesh::*)(::smtk::mesh::DeleteMesh const &)) &smtk::mesh::DeleteMesh::operator=)
     .def("ableToOperate", &smtk::mesh::DeleteMesh::ableToOperate)
     .def_static("create", (std::shared_ptr<smtk::mesh::DeleteMesh> (*)()) &smtk::mesh::DeleteMesh::create)
     .def_static("create", (std::shared_ptr<smtk::mesh::DeleteMesh> (*)(::std::shared_ptr<smtk::mesh::DeleteMesh> &)) &smtk::mesh::DeleteMesh::create, py::arg("ref"))
diff --git a/smtk/mesh/pybind11/PybindElevateMesh.h b/smtk/mesh/pybind11/PybindElevateMesh.h
index df4662f7215ee315b1ba5b50848097a149a2926f..e8548545d0a26d666fbdbbd74f3b03d94cc38dce 100644
--- a/smtk/mesh/pybind11/PybindElevateMesh.h
+++ b/smtk/mesh/pybind11/PybindElevateMesh.h
@@ -24,8 +24,6 @@ inline PySharedPtrClass< smtk::mesh::ElevateMesh, smtk::operation::XMLOperation
   PySharedPtrClass< smtk::mesh::ElevateMesh, smtk::operation::XMLOperation > instance(m, "ElevateMesh");
   instance
     .def(py::init<>())
-    .def(py::init<::smtk::mesh::ElevateMesh const &>())
-    .def("deepcopy", (smtk::mesh::ElevateMesh & (smtk::mesh::ElevateMesh::*)(::smtk::mesh::ElevateMesh const &)) &smtk::mesh::ElevateMesh::operator=)
     .def("ableToOperate", &smtk::mesh::ElevateMesh::ableToOperate)
     .def_static("create", (std::shared_ptr<smtk::mesh::ElevateMesh> (*)()) &smtk::mesh::ElevateMesh::create)
     .def_static("create", (std::shared_ptr<smtk::mesh::ElevateMesh> (*)(::std::shared_ptr<smtk::mesh::ElevateMesh> &)) &smtk::mesh::ElevateMesh::create, py::arg("ref"))
diff --git a/smtk/mesh/pybind11/PybindExport.h b/smtk/mesh/pybind11/PybindExport.h
index 11a2a8e8b2080daf513bdd78bf4ec6b6dbc9f93e..c9d7cde222829cef88d2dce6fdbfe40c5a8e7246 100644
--- a/smtk/mesh/pybind11/PybindExport.h
+++ b/smtk/mesh/pybind11/PybindExport.h
@@ -24,8 +24,6 @@ inline PySharedPtrClass< smtk::mesh::Export, smtk::operation::XMLOperation > pyb
   PySharedPtrClass< smtk::mesh::Export, smtk::operation::XMLOperation > instance(m, "Export");
   instance
     .def(py::init<>())
-    .def(py::init<::smtk::mesh::Export const &>())
-    .def("deepcopy", (smtk::mesh::Export & (smtk::mesh::Export::*)(::smtk::mesh::Export const &)) &smtk::mesh::Export::operator=)
     .def("ableToOperate", &smtk::mesh::Export::ableToOperate)
     .def_static("create", (std::shared_ptr<smtk::mesh::Export> (*)()) &smtk::mesh::Export::create)
     .def_static("create", (std::shared_ptr<smtk::mesh::Export> (*)(::std::shared_ptr<smtk::mesh::Export> &)) &smtk::mesh::Export::create, py::arg("ref"))
diff --git a/smtk/mesh/pybind11/PybindImport.h b/smtk/mesh/pybind11/PybindImport.h
index 2e54d72de5d7e7280716065aeb5e9c6c89c9fbf5..2fe8a437ac2224aa72a0d635e8ac2799f16a158c 100644
--- a/smtk/mesh/pybind11/PybindImport.h
+++ b/smtk/mesh/pybind11/PybindImport.h
@@ -24,8 +24,6 @@ inline PySharedPtrClass< smtk::mesh::Import, smtk::operation::XMLOperation > pyb
   PySharedPtrClass< smtk::mesh::Import, smtk::operation::XMLOperation > instance(m, "Import");
   instance
     .def(py::init<>())
-    .def(py::init<::smtk::mesh::Import const &>())
-    .def("deepcopy", (smtk::mesh::Import & (smtk::mesh::Import::*)(::smtk::mesh::Import const &)) &smtk::mesh::Import::operator=)
     .def("ableToOperate", &smtk::mesh::Import::ableToOperate)
     .def_static("create", (std::shared_ptr<smtk::mesh::Import> (*)()) &smtk::mesh::Import::create)
     .def_static("create", (std::shared_ptr<smtk::mesh::Import> (*)(::std::shared_ptr<smtk::mesh::Import> &)) &smtk::mesh::Import::create, py::arg("ref"))
diff --git a/smtk/mesh/pybind11/PybindInterpolateOntoMesh.h b/smtk/mesh/pybind11/PybindInterpolateOntoMesh.h
index c00203f0cf0fca5180165002c007eed065832cd3..e6374ac9b978365897c69c63dbb28b0f6e7aa247 100644
--- a/smtk/mesh/pybind11/PybindInterpolateOntoMesh.h
+++ b/smtk/mesh/pybind11/PybindInterpolateOntoMesh.h
@@ -24,8 +24,6 @@ inline PySharedPtrClass< smtk::mesh::InterpolateOntoMesh, smtk::operation::XMLOp
   PySharedPtrClass< smtk::mesh::InterpolateOntoMesh, smtk::operation::XMLOperation > instance(m, "InterpolateOntoMesh");
   instance
     .def(py::init<>())
-    .def(py::init<::smtk::mesh::InterpolateOntoMesh const &>())
-    .def("deepcopy", (smtk::mesh::InterpolateOntoMesh & (smtk::mesh::InterpolateOntoMesh::*)(::smtk::mesh::InterpolateOntoMesh const &)) &smtk::mesh::InterpolateOntoMesh::operator=)
     .def("ableToOperate", &smtk::mesh::InterpolateOntoMesh::ableToOperate)
     .def_static("create", (std::shared_ptr<smtk::mesh::InterpolateOntoMesh> (*)()) &smtk::mesh::InterpolateOntoMesh::create)
     .def_static("create", (std::shared_ptr<smtk::mesh::InterpolateOntoMesh> (*)(::std::shared_ptr<smtk::mesh::InterpolateOntoMesh> &)) &smtk::mesh::InterpolateOntoMesh::create, py::arg("ref"))
diff --git a/smtk/mesh/pybind11/PybindRead.h b/smtk/mesh/pybind11/PybindRead.h
index cd402f3f164c40d091ab919f93b820bfc39ea683..55054d38d367f7cb8c15e94af513d76a25a7031e 100644
--- a/smtk/mesh/pybind11/PybindRead.h
+++ b/smtk/mesh/pybind11/PybindRead.h
@@ -24,8 +24,6 @@ inline PySharedPtrClass< smtk::mesh::Read, smtk::operation::XMLOperation > pybin
   PySharedPtrClass< smtk::mesh::Read, smtk::operation::XMLOperation > instance(m, "Read");
   instance
     .def(py::init<>())
-    .def(py::init<::smtk::mesh::Read const &>())
-    .def("deepcopy", (smtk::mesh::Read & (smtk::mesh::Read::*)(::smtk::mesh::Read const &)) &smtk::mesh::Read::operator=)
     .def("ableToOperate", &smtk::mesh::Read::ableToOperate)
     .def_static("create", (std::shared_ptr<smtk::mesh::Read> (*)()) &smtk::mesh::Read::create)
     .def_static("create", (std::shared_ptr<smtk::mesh::Read> (*)(::std::shared_ptr<smtk::mesh::Read> &)) &smtk::mesh::Read::create, py::arg("ref"))
diff --git a/smtk/mesh/pybind11/PybindWrite.h b/smtk/mesh/pybind11/PybindWrite.h
index 83c6259021bb602afa9c75970aeedd7a064bade2..cfde610355e3b0e49dc334772c0783d5df00b102 100644
--- a/smtk/mesh/pybind11/PybindWrite.h
+++ b/smtk/mesh/pybind11/PybindWrite.h
@@ -24,8 +24,6 @@ inline PySharedPtrClass< smtk::mesh::Write, smtk::operation::XMLOperation > pybi
   PySharedPtrClass< smtk::mesh::Write, smtk::operation::XMLOperation > instance(m, "Write");
   instance
     .def(py::init<>())
-    .def(py::init<::smtk::mesh::Write const &>())
-    .def("deepcopy", (smtk::mesh::Write & (smtk::mesh::Write::*)(::smtk::mesh::Write const &)) &smtk::mesh::Write::operator=)
     .def("ableToOperate", &smtk::mesh::Write::ableToOperate)
     .def_static("create", (std::shared_ptr<smtk::mesh::Write> (*)()) &smtk::mesh::Write::create)
     .def_static("create", (std::shared_ptr<smtk::mesh::Write> (*)(::std::shared_ptr<smtk::mesh::Write> &)) &smtk::mesh::Write::create, py::arg("ref"))
diff --git a/smtk/model/pybind11/PybindAddAuxiliaryGeometry.h b/smtk/model/pybind11/PybindAddAuxiliaryGeometry.h
index 35c0d26f8bb208140a9d050fa3c566f844955022..2c7519b3f8bad68a426dff5d48c14d950e46a156 100644
--- a/smtk/model/pybind11/PybindAddAuxiliaryGeometry.h
+++ b/smtk/model/pybind11/PybindAddAuxiliaryGeometry.h
@@ -24,8 +24,6 @@ inline PySharedPtrClass< smtk::model::AddAuxiliaryGeometry, smtk::operation::XML
   PySharedPtrClass< smtk::model::AddAuxiliaryGeometry, smtk::operation::XMLOperation > instance(m, "AddAuxiliaryGeometry");
   instance
     .def(py::init<>())
-    .def(py::init<::smtk::model::AddAuxiliaryGeometry const &>())
-    .def("deepcopy", (smtk::model::AddAuxiliaryGeometry & (smtk::model::AddAuxiliaryGeometry::*)(::smtk::model::AddAuxiliaryGeometry const &)) &smtk::model::AddAuxiliaryGeometry::operator=)
     .def_static("create", (std::shared_ptr<smtk::model::AddAuxiliaryGeometry> (*)()) &smtk::model::AddAuxiliaryGeometry::create)
     .def_static("create", (std::shared_ptr<smtk::model::AddAuxiliaryGeometry> (*)(::std::shared_ptr<smtk::model::AddAuxiliaryGeometry> &)) &smtk::model::AddAuxiliaryGeometry::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<const smtk::model::AddAuxiliaryGeometry> (smtk::model::AddAuxiliaryGeometry::*)() const) &smtk::model::AddAuxiliaryGeometry::shared_from_this)
diff --git a/smtk/model/pybind11/PybindCloseModel.h b/smtk/model/pybind11/PybindCloseModel.h
index 7167f61c1b8e45bdedcda4c864265776e1231643..5fe80ad9194da63678985e524b9edc7f5c18ebc7 100644
--- a/smtk/model/pybind11/PybindCloseModel.h
+++ b/smtk/model/pybind11/PybindCloseModel.h
@@ -24,8 +24,6 @@ inline PySharedPtrClass< smtk::model::CloseModel, smtk::operation::XMLOperation
   PySharedPtrClass< smtk::model::CloseModel, smtk::operation::XMLOperation > instance(m, "CloseModel");
   instance
     .def(py::init<>())
-    .def(py::init<::smtk::model::CloseModel const &>())
-    .def("deepcopy", (smtk::model::CloseModel & (smtk::model::CloseModel::*)(::smtk::model::CloseModel const &)) &smtk::model::CloseModel::operator=)
     .def("ableToOperate", &smtk::model::CloseModel::ableToOperate)
     .def_static("create", (std::shared_ptr<smtk::model::CloseModel> (*)()) &smtk::model::CloseModel::create)
     .def_static("create", (std::shared_ptr<smtk::model::CloseModel> (*)(::std::shared_ptr<smtk::model::CloseModel> &)) &smtk::model::CloseModel::create, py::arg("ref"))
diff --git a/smtk/model/pybind11/PybindCreateInstances.h b/smtk/model/pybind11/PybindCreateInstances.h
index 9a5a13291108281a31a3212dff801227d0379eb7..decbb5e9ab0f30b9d999308acd12c7bfe1426ad4 100644
--- a/smtk/model/pybind11/PybindCreateInstances.h
+++ b/smtk/model/pybind11/PybindCreateInstances.h
@@ -24,8 +24,6 @@ inline PySharedPtrClass< smtk::model::CreateInstances, smtk::operation::XMLOpera
   PySharedPtrClass< smtk::model::CreateInstances, smtk::operation::XMLOperation > instance(m, "CreateInstances");
   instance
     .def(py::init<>())
-    .def(py::init<::smtk::model::CreateInstances const &>())
-    .def("deepcopy", (smtk::model::CreateInstances & (smtk::model::CreateInstances::*)(::smtk::model::CreateInstances const &)) &smtk::model::CreateInstances::operator=)
     .def_static("create", (std::shared_ptr<smtk::model::CreateInstances> (*)()) &smtk::model::CreateInstances::create)
     .def_static("create", (std::shared_ptr<smtk::model::CreateInstances> (*)(::std::shared_ptr<smtk::model::CreateInstances> &)) &smtk::model::CreateInstances::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<const smtk::model::CreateInstances> (smtk::model::CreateInstances::*)() const) &smtk::model::CreateInstances::shared_from_this)
diff --git a/smtk/model/pybind11/PybindDelete.h b/smtk/model/pybind11/PybindDelete.h
index 21922018b34b6f3dfebb123173b6f39919c8173f..620aef924a7cab21760ebc2e06bdc04f3dd63ced 100644
--- a/smtk/model/pybind11/PybindDelete.h
+++ b/smtk/model/pybind11/PybindDelete.h
@@ -24,8 +24,6 @@ inline PySharedPtrClass< smtk::model::Delete, smtk::operation::XMLOperation > py
   PySharedPtrClass< smtk::model::Delete, smtk::operation::XMLOperation > instance(m, "Delete");
   instance
     .def(py::init<>())
-    .def(py::init<::smtk::model::Delete const &>())
-    .def("deepcopy", (smtk::model::Delete & (smtk::model::Delete::*)(::smtk::model::Delete const &)) &smtk::model::Delete::operator=)
     .def_static("create", (std::shared_ptr<smtk::model::Delete> (*)()) &smtk::model::Delete::create)
     .def_static("create", (std::shared_ptr<smtk::model::Delete> (*)(::std::shared_ptr<smtk::model::Delete> &)) &smtk::model::Delete::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<const smtk::model::Delete> (smtk::model::Delete::*)() const) &smtk::model::Delete::shared_from_this)
diff --git a/smtk/model/pybind11/PybindExportModelJSON.h b/smtk/model/pybind11/PybindExportModelJSON.h
index 87fafcd78d307dad6f5ea10e96f5be8a43fd9a72..72a08fc51e07fbcb76af4fe9fe8fa626341fc958 100644
--- a/smtk/model/pybind11/PybindExportModelJSON.h
+++ b/smtk/model/pybind11/PybindExportModelJSON.h
@@ -24,8 +24,6 @@ inline PySharedPtrClass< smtk::model::ExportModelJSON, smtk::operation::XMLOpera
   PySharedPtrClass< smtk::model::ExportModelJSON, smtk::operation::XMLOperation > instance(m, "ExportModelJSON");
   instance
     .def(py::init<>())
-    .def(py::init<::smtk::model::ExportModelJSON const &>())
-    .def("deepcopy", (smtk::model::ExportModelJSON & (smtk::model::ExportModelJSON::*)(::smtk::model::ExportModelJSON const &)) &smtk::model::ExportModelJSON::operator=)
     .def_static("create", (std::shared_ptr<smtk::model::ExportModelJSON> (*)()) &smtk::model::ExportModelJSON::create)
     .def_static("create", (std::shared_ptr<smtk::model::ExportModelJSON> (*)(::std::shared_ptr<smtk::model::ExportModelJSON> &)) &smtk::model::ExportModelJSON::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<const smtk::model::ExportModelJSON> (smtk::model::ExportModelJSON::*)() const) &smtk::model::ExportModelJSON::shared_from_this)
diff --git a/smtk/operation/Operation.cxx b/smtk/operation/Operation.cxx
index 0f4d6dcb7c071a6ba2f3190afd25667ba0807737..01c3f2c585a0d933c97f48378967c3e0af8b5ab8 100644
--- a/smtk/operation/Operation.cxx
+++ b/smtk/operation/Operation.cxx
@@ -139,6 +139,13 @@ Operation::Result Operation::operate()
 
 Operation::Result Operation::operate(const BaseKey& key)
 {
+  std::multimap<Priority, Handler> instanceHandlers;
+  {
+    std::lock_guard<std::mutex> guard(m_handlerLock);
+    instanceHandlers = m_handlers;
+    m_handlers.clear();
+  }
+
   // Gather all requested resources and their lock types.
   const auto resourcesAndLockTypes = this->identifyLocksRequired();
 
@@ -373,6 +380,14 @@ Operation::Result Operation::operate(const BaseKey& key)
   }
 
   // Execute post-operation observation
+  // Always call handlers, but based on the \a key, observers may be skipped.
+  // Handlers will always be invoked before other observers, but in priority order.
+  // In the future, we may attempt to interleave the handler and observer calls.
+  for (const auto& entry : instanceHandlers)
+  {
+    entry.second(*this, result);
+  }
+  instanceHandlers.clear();
   if (key.m_observerOption == ObserverOption::InvokeObservers && observePostOperation && manager)
   {
     manager->observers()(*this, EventType::DID_OPERATE, result);
@@ -529,6 +544,34 @@ Operation::Result Operation::createResult(Outcome outcome)
   return result;
 }
 
+void Operation::addHandler(Handler handler, Priority priority)
+{
+  std::lock_guard<std::mutex> guard(m_handlerLock);
+  m_handlers.emplace(priority, handler);
+}
+
+bool Operation::removeHandler(Handler handler, Priority priority)
+{
+  std::lock_guard<std::mutex> guard(m_handlerLock);
+  for (auto it = m_handlers.lower_bound(priority); it != m_handlers.upper_bound(priority); ++it)
+  {
+    if (it->second.target<Handler>() == handler.target<Handler>())
+    {
+      m_handlers.erase(it);
+      return true;
+    }
+  }
+  return false;
+}
+
+bool Operation::clearHandlers()
+{
+  std::lock_guard<std::mutex> guard(m_handlerLock);
+  bool didRemove = !m_handlers.empty();
+  m_handlers.clear();
+  return didRemove;
+}
+
 void Operation::markModifiedResources(Operation::Result& result)
 {
   // Gather all requested resources and their lock types.
diff --git a/smtk/operation/Operation.h b/smtk/operation/Operation.h
index 35df8ee43762e1639b3c798b3f56485744450d5d..4489886249e910ed237af8cd340350f5d93015b1 100644
--- a/smtk/operation/Operation.h
+++ b/smtk/operation/Operation.h
@@ -18,6 +18,7 @@
 
 #include <functional>
 #include <map>
+#include <mutex>
 #include <string>
 #include <typeindex>
 #include <utility>
@@ -40,6 +41,7 @@ class Helper;
 class ImportPythonOperation;
 class Manager;
 class Operation;
+class PythonRunChild;
 
 using Handler = std::function<void(Operation&, const std::shared_ptr<smtk::attribute::Attribute>&)>;
 
@@ -66,17 +68,20 @@ class SMTKCORE_EXPORT Operation : smtkEnableSharedPtr(Operation)
 public:
   smtkTypeMacroBase(smtk::operation::Operation);
 
-  // A hash value uniquely representing the operation.
+  /// A priority for Handlers.
+  using Priority = int;
+
+  /// A hash value uniquely representing the operation.
   typedef std::size_t Index;
 
-  // An attribute describing the operation's input.
+  /// An attribute describing the operation's input.
   typedef std::shared_ptr<smtk::attribute::Attribute> Parameters;
 
-  // An attribute containing the operation's result.
+  /// An attribute containing the operation's result.
   typedef std::shared_ptr<smtk::attribute::Attribute> Result;
 
-  // An attribute resource containing the operation's execution definition
-  // result definition.
+  /// An attribute resource containing the operation's execution definition
+  /// result definition.
   typedef std::shared_ptr<smtk::attribute::Resource> Specification;
 
   typedef std::shared_ptr<smtk::attribute::Definition> Definition;
@@ -185,6 +190,33 @@ public:
   /// attribute is distinguished by its derivation from the "result" attribute.
   Result createResult(Outcome);
 
+  /// Pass a \a handler to invoke after the operation's completion (successful or not).
+  /// The \a handler is called only on this instance of the operation (as opposed to
+  /// observers of the operation manager, which are invoked for every operation).
+  ///
+  /// The \a priority is used to schedule the \a handler relative to other observers.
+  ///
+  /// Handlers are cleared each time the operation is run, so if you run the same
+  /// instance of an operation multiple times, you must add the handler before
+  /// launching the operation each time.
+  ///
+  /// Note that you are expected to avoid launching the same instance of an operation
+  /// multiple times without ensuring that the operation is not already queued.
+  /// Otherwise, handlers may be added or removed while a previous instance is running
+  /// – including removal after the first instance of the operation has completed but
+  /// before successive instances – making invocation of handlers intermittent after the
+  /// first operation.
+  ///
+  /// A handler may add itself or other handlers back to the operation instance
+  /// as the operation will copy the handler container before invoking handlers.
+  void addHandler(Handler handler, Priority priority);
+
+  /// Remove a \a handler from the operation.
+  bool removeHandler(Handler handler, Priority priority);
+
+  /// Clear all handlers from the operation.
+  bool clearHandlers();
+
   /// Operations that are managed have a non-null pointer to their manager.
   ManagerPtr manager() const { return m_manager.lock(); }
 
@@ -228,6 +260,7 @@ public:
   };
 
 protected:
+  friend class PythonRunChild;
   Operation();
 
   /// Identify resources to lock, and whether to lock them for reading or writing.
@@ -329,6 +362,8 @@ private:
   Definition m_resultDefinition;
   std::vector<std::weak_ptr<smtk::attribute::Attribute>> m_results;
   ResourceAccessMap m_lockedResources;
+  std::mutex m_handlerLock;
+  std::multimap<Priority, Handler> m_handlers;
 };
 
 /**\brief Return the outcome of an operation given its \a result object.
diff --git a/smtk/operation/operators/WriteResource.cxx b/smtk/operation/operators/WriteResource.cxx
index 2d38d771903ffde1e4d6616a6743ae3bfb6bd780..7e4c6fe3c589e8490aa970d3a89bb24ea6bfdc2d 100644
--- a/smtk/operation/operators/WriteResource.cxx
+++ b/smtk/operation/operators/WriteResource.cxx
@@ -213,8 +213,7 @@ void WriteResource::generateSummary(WriteResource::Result& res)
   }
   else if (outcome == static_cast<int>(smtk::operation::Operation::Outcome::SUCCEEDED))
   {
-    msg << ": wrote \"" << resource->location() << "\"";
-    smtkInfoMacro(this->log(), msg.str());
+    // Do nothing. Each child operation should provide a summary.
   }
   else
   {
diff --git a/smtk/operation/pybind11/PybindOperation.h b/smtk/operation/pybind11/PybindOperation.h
index b0dddeafa818bdd229e0edaadc2627c3ddac4e75..5e1775d1e8b1ec6c4bd886a8bc8149b1c6d79eb6 100644
--- a/smtk/operation/pybind11/PybindOperation.h
+++ b/smtk/operation/pybind11/PybindOperation.h
@@ -22,6 +22,43 @@
 
 #include "smtk/io/Logger.h"
 
+namespace smtk
+{
+namespace operation
+{
+
+/// A helper class that is a friend of smtk::operation::Operation so it
+/// can construct a Key to run "child" (nested) operations.
+class PythonRunChild
+{
+public:
+  using Result           = smtk::operation::Operation::Result;
+  using ObserverOption   = smtk::operation::Operation::ObserverOption;
+  using LockOption       = smtk::operation::Operation::LockOption;
+  using ParametersOption = smtk::operation::Operation::ParametersOption;
+
+  PythonRunChild(smtk::operation::Operation* self)
+    : m_self(self)
+  {
+  }
+
+  Result run(
+    const smtk::operation::Operation::Ptr& childOp,
+    ObserverOption observerOption,
+    LockOption lockOption,
+    ParametersOption paramsOption)
+  {
+    auto key = m_self->childKey(observerOption, lockOption, paramsOption);
+    return childOp->operate(key);
+  }
+
+protected:
+  smtk::operation::Operation* m_self{ nullptr };
+};
+
+} // namespace operation
+} // namespace smtk
+
 namespace py = pybind11;
 
 inline PySharedPtrClass< smtk::operation::Operation, smtk::operation::PyOperation > pybind11_init_smtk_operation_Operation(py::module &m)
@@ -46,7 +83,6 @@ inline PySharedPtrClass< smtk::operation::Operation, smtk::operation::PyOperatio
 
   instance
     .def(py::init<>())
-    .def("deepcopy", (smtk::operation::Operation & (smtk::operation::Operation::*)(::smtk::operation::Operation const &)) &smtk::operation::Operation::operator=)
     .def_static("create", &smtk::operation::PyOperation::create)
     .def("typeName", &smtk::operation::Operation::typeName)
     .def("index", &smtk::operation::Operation::index)
@@ -101,7 +137,23 @@ inline PySharedPtrClass< smtk::operation::Operation, smtk::operation::PyOperatio
     .def("createResult", &smtk::operation::Operation::createResult, py::arg("arg0"))
     .def("manager", &smtk::operation::Operation::manager)
     .def("managers", &smtk::operation::Operation::managers)
+    .def("addHandler", &smtk::operation::Operation::addHandler, py::arg("handler"), py::arg("priority"))
+    .def("removeHandler", &smtk::operation::Operation::removeHandler, py::arg("handler"), py::arg("priority"))
+    .def("clearHandlers", &smtk::operation::Operation::clearHandlers)
     .def("restoreTrace", (bool (smtk::operation::Operation::*)(::std::string const &)) &smtk::operation::Operation::restoreTrace)
+    .def("runChildOp", [](smtk::operation::Operation* self, const smtk::operation::Operation::Ptr& childOp,
+        smtk::operation::Operation::ObserverOption observerOption,
+        smtk::operation::Operation::LockOption lockOption,
+        smtk::operation::Operation::ParametersOption paramsOption)
+      {
+        smtk::operation::PythonRunChild runner(self);
+        auto result = runner.run(childOp, observerOption, lockOption, paramsOption);
+        return result;
+      },
+      py::arg("childOp"),
+      py::arg("observerOption") = smtk::operation::Operation::ObserverOption::SkipObservers,
+      py::arg("lockOption") = smtk::operation::Operation::LockOption::ParentLocksOnly,
+      py::arg("paramsOption") = smtk::operation::Operation::ParametersOption::SkipValidation)
     ;
   py::enum_<smtk::operation::Operation::Outcome>(instance, "Outcome")
     .value("UNABLE_TO_OPERATE", smtk::operation::Operation::Outcome::UNABLE_TO_OPERATE)
diff --git a/smtk/operation/pybind11/PybindReadResource.h b/smtk/operation/pybind11/PybindReadResource.h
index 22e57381016f83e841bad7100147667b484b804e..1ec92d4cf290a6a4d4331e89230fdf5a1d58a932 100644
--- a/smtk/operation/pybind11/PybindReadResource.h
+++ b/smtk/operation/pybind11/PybindReadResource.h
@@ -23,8 +23,6 @@ inline PySharedPtrClass< smtk::operation::ReadResource, smtk::operation::XMLOper
 {
   PySharedPtrClass< smtk::operation::ReadResource, smtk::operation::XMLOperation > instance(m, "ReadResource");
   instance
-    .def(py::init<::smtk::operation::ReadResource const &>())
-    .def("deepcopy", (smtk::operation::ReadResource & (smtk::operation::ReadResource::*)(::smtk::operation::ReadResource const &)) &smtk::operation::ReadResource::operator=)
     .def_static("create", (std::shared_ptr<smtk::operation::ReadResource> (*)()) &smtk::operation::ReadResource::create)
     .def_static("create", (std::shared_ptr<smtk::operation::ReadResource> (*)(::std::shared_ptr<smtk::operation::ReadResource> &)) &smtk::operation::ReadResource::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<const smtk::operation::ReadResource> (smtk::operation::ReadResource::*)() const) &smtk::operation::ReadResource::shared_from_this)
diff --git a/smtk/operation/pybind11/PybindRemoveResource.h b/smtk/operation/pybind11/PybindRemoveResource.h
index edb6de9498bac7fd369563f185d3c7f023134439..13ef374a6f521e0c5ab320507c205389d4ebb61f 100644
--- a/smtk/operation/pybind11/PybindRemoveResource.h
+++ b/smtk/operation/pybind11/PybindRemoveResource.h
@@ -23,8 +23,6 @@ inline PySharedPtrClass< smtk::operation::RemoveResource, smtk::operation::XMLOp
 {
   PySharedPtrClass< smtk::operation::RemoveResource, smtk::operation::XMLOperation > instance(m, "RemoveResource");
   instance
-    .def(py::init<::smtk::operation::RemoveResource const &>())
-    .def("deepcopy", (smtk::operation::RemoveResource & (smtk::operation::RemoveResource::*)(::smtk::operation::RemoveResource const &)) &smtk::operation::RemoveResource::operator=)
     .def_static("create", (std::shared_ptr<smtk::operation::RemoveResource> (*)()) &smtk::operation::RemoveResource::create)
     .def_static("create", (std::shared_ptr<smtk::operation::RemoveResource> (*)(::std::shared_ptr<smtk::operation::RemoveResource> &)) &smtk::operation::RemoveResource::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<const smtk::operation::RemoveResource> (smtk::operation::RemoveResource::*)() const) &smtk::operation::RemoveResource::shared_from_this)
diff --git a/smtk/operation/pybind11/PybindSetProperty.h b/smtk/operation/pybind11/PybindSetProperty.h
index 2e0d964431dd996fb3f4bdafe8ef76eb793ad58b..9aeec35f7a4f8b9357cba56bb2fca77010f42368 100644
--- a/smtk/operation/pybind11/PybindSetProperty.h
+++ b/smtk/operation/pybind11/PybindSetProperty.h
@@ -24,8 +24,6 @@ inline PySharedPtrClass< smtk::operation::SetProperty, smtk::operation::XMLOpera
   PySharedPtrClass< smtk::operation::SetProperty, smtk::operation::XMLOperation > instance(m, "SetProperty");
   instance
     .def(py::init<>())
-    .def(py::init<::smtk::operation::SetProperty const &>())
-    .def("deepcopy", (smtk::operation::SetProperty & (smtk::operation::SetProperty::*)(::smtk::operation::SetProperty const &)) &smtk::operation::SetProperty::operator=)
     .def_static("create", (std::shared_ptr<smtk::operation::SetProperty> (*)()) &smtk::operation::SetProperty::create)
     .def_static("create", (std::shared_ptr<smtk::operation::SetProperty> (*)(::std::shared_ptr<smtk::operation::SetProperty> &)) &smtk::operation::SetProperty::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<const smtk::operation::SetProperty> (smtk::operation::SetProperty::*)() const) &smtk::operation::SetProperty::shared_from_this)
diff --git a/smtk/operation/pybind11/PybindWriteResource.h b/smtk/operation/pybind11/PybindWriteResource.h
index f54bbefa6b22d4f592b53ec0e36777b26de3db1e..c679383b9429544180a13b2701cbcb8a4f9b88ff 100644
--- a/smtk/operation/pybind11/PybindWriteResource.h
+++ b/smtk/operation/pybind11/PybindWriteResource.h
@@ -23,8 +23,6 @@ inline PySharedPtrClass< smtk::operation::WriteResource, smtk::operation::XMLOpe
 {
   PySharedPtrClass< smtk::operation::WriteResource, smtk::operation::XMLOperation > instance(m, "WriteResource");
   instance
-    .def(py::init<::smtk::operation::WriteResource const &>())
-    .def("deepcopy", (smtk::operation::WriteResource & (smtk::operation::WriteResource::*)(::smtk::operation::WriteResource const &)) &smtk::operation::WriteResource::operator=)
     .def_static("create", (std::shared_ptr<smtk::operation::WriteResource> (*)()) &smtk::operation::WriteResource::create)
     .def_static("create", (std::shared_ptr<smtk::operation::WriteResource> (*)(::std::shared_ptr<smtk::operation::WriteResource> &)) &smtk::operation::WriteResource::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<const smtk::operation::WriteResource> (smtk::operation::WriteResource::*)() const) &smtk::operation::WriteResource::shared_from_this)
diff --git a/smtk/operation/pybind11/PybindXMLOperation.h b/smtk/operation/pybind11/PybindXMLOperation.h
index 5bad73ffdc45fd2d4f26a3a2491c8eccaaf8f1de..dce73b0f001301564500eab904d352e5bd5e943a 100644
--- a/smtk/operation/pybind11/PybindXMLOperation.h
+++ b/smtk/operation/pybind11/PybindXMLOperation.h
@@ -23,7 +23,6 @@ inline PySharedPtrClass< smtk::operation::XMLOperation, smtk::operation::Operati
 {
   PySharedPtrClass< smtk::operation::XMLOperation, smtk::operation::Operation > instance(m, "XMLOperation");
   instance
-    .def("deepcopy", (smtk::operation::XMLOperation & (smtk::operation::XMLOperation::*)(::smtk::operation::XMLOperation const &)) &smtk::operation::XMLOperation::operator=)
     .def("shared_from_this", (std::shared_ptr<const smtk::operation::XMLOperation> (smtk::operation::XMLOperation::*)() const) &smtk::operation::XMLOperation::shared_from_this)
     .def("shared_from_this", (std::shared_ptr<smtk::operation::XMLOperation> (smtk::operation::XMLOperation::*)()) &smtk::operation::XMLOperation::shared_from_this)
     ;
diff --git a/smtk/operation/testing/python/CMakeLists.txt b/smtk/operation/testing/python/CMakeLists.txt
index 9859534ed86424707621c97f2287698e405ded8f..218eec617485a937b647c82ff4979ac33d1ee988 100644
--- a/smtk/operation/testing/python/CMakeLists.txt
+++ b/smtk/operation/testing/python/CMakeLists.txt
@@ -6,6 +6,12 @@ set(smtkOperationPythonTests
 set(smtkOperationPythonDataTests
 )
 
+if (SMTK_ENABLE_POLYGON_SESSION)
+  list(APPEND smtkOperationPythonDataTests
+    testOperationHandler
+  )
+endif()
+
 if(SMTK_ENABLE_PARAVIEW_SUPPORT)
   list(APPEND smtkOperationPythonDataTests
     testOperationTracing
diff --git a/smtk/operation/testing/python/testOperationHandler.py b/smtk/operation/testing/python/testOperationHandler.py
new file mode 100644
index 0000000000000000000000000000000000000000..8d6fb2558c922cc6c7cecb8a1fc3095e311a98ea
--- /dev/null
+++ b/smtk/operation/testing/python/testOperationHandler.py
@@ -0,0 +1,111 @@
+# =============================================================================
+#
+#  Copyright (c) Kitware, Inc.
+#  All rights reserved.
+#  See LICENSE.txt for details.
+#
+#  This software is distributed WITHOUT ANY WARRANTY; without even
+#  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+#  PURPOSE.  See the above copyright notice for more information.
+#
+# =============================================================================
+import smtk
+import smtk.attribute
+import smtk.operation
+import smtk.session.polygon
+import smtk.testing
+
+invokedCount = 0
+message = 'test'
+addSelf = False
+
+def handler(op, result):
+    global message, invokedCount
+    invokedCount += 1
+    print('%s: Handler invoked (%d)' % (message, invokedCount))
+    if addSelf:
+        op.addHandler(handler, 0)
+
+class TestOperationHandler(smtk.testing.TestCase):
+
+    def testSingleHandler(self):
+        import os
+        global message, invokedCount
+        self.mgr = smtk.model.Resource.create()
+        fpath = [smtk.testing.DATA_DIR, 'model',
+                 '2d', 'smtk', 'epic-trex-drummer.smtk']
+        op = smtk.session.polygon.Read.create()
+        op.parameters().find('filename').setValue(os.path.join(*fpath))
+        print
+        op.addHandler(handler, 0)
+        op.addHandler(handler, 1)
+        op.removeHandler(handler, 1)
+        invokedCount = 0
+        message = 'testSingleHandler'
+        res = op.operate()
+        if res.find('outcome').value(0) != int(smtk.operation.Operation.SUCCEEDED):
+            raise RuntimeError
+        self.assertEqual(invokedCount, 1, 'Expected to be called exactly once.')
+
+    def testMultipleHandlers(self):
+        import os
+        global message, invokedCount
+        self.mgr = smtk.model.Resource.create()
+        fpath = [smtk.testing.DATA_DIR, 'model',
+                 '2d', 'smtk', 'epic-trex-drummer.smtk']
+        op = smtk.session.polygon.Read.create()
+        op.parameters().find('filename').setValue(os.path.join(*fpath))
+        op.addHandler(handler, 0)
+        op.addHandler(handler, 1)
+        invokedCount = 0
+        message = 'testMultipleHandlers'
+        res = op.operate()
+        if res.find('outcome').value(0) != int(smtk.operation.Operation.SUCCEEDED):
+            raise RuntimeError
+        self.assertEqual(invokedCount, 2, 'Expected to be called exactly twice.')
+
+    def testMultipleTimes(self):
+        import os
+        global message, invokedCount, addSelf
+        self.mgr = smtk.model.Resource.create()
+        fpath = [smtk.testing.DATA_DIR, 'model',
+                 '2d', 'smtk', 'epic-trex-drummer.smtk']
+        op = smtk.session.polygon.Read.create()
+        op.parameters().find('filename').setValue(os.path.join(*fpath))
+        op.addHandler(handler, 0)
+        op.addHandler(handler, 1)
+        addSelf = True
+        invokedCount = 0
+        message = 'testMultipleTimes (first time)'
+        res = op.operate()
+        if res.find('outcome').value(0) != int(smtk.operation.Operation.SUCCEEDED):
+            raise RuntimeError
+        self.assertEqual(invokedCount, 2, 'Expected to be called exactly twice.')
+
+        # Invoke the operation a second time.
+        invokedCount = 0
+        message = 'testMultipleTimes (second time)'
+        # Remove one of the handlers that got re-added since "addSelf" was true.
+        op.removeHandler(handler, 0)
+        addSelf = False
+        res = op.operate()
+        if res.find('outcome').value(0) != int(smtk.operation.Operation.SUCCEEDED):
+            raise RuntimeError
+        self.assertEqual(invokedCount, 1, 'Expected to be called after first operation.')
+
+        # Invoke the operation a third time (with no re-add of handler).
+        invokedCount = 0
+        message = 'testMultipleTimes (third time)'
+        addSelf = False
+        res = op.operate()
+        if res.find('outcome').value(0) != int(smtk.operation.Operation.SUCCEEDED):
+            raise RuntimeError
+        self.assertEqual(invokedCount, 0, 'Expected to be called no more after the first operation.')
+
+        # Test that no handlers remain on the operation
+        didRemove = op.clearHandlers()
+        self.assertFalse(didRemove, 'Expected no handlers remaining.')
+
+if __name__ == '__main__':
+    smtk.testing.process_arguments()
+    smtk.testing.main()
diff --git a/smtk/project/operators/Write.cxx b/smtk/project/operators/Write.cxx
index 960e67a5c83b43c8fd81e979f1e585bd3924864f..af50c76532a508dd15aae888ab0b013664b519b6 100644
--- a/smtk/project/operators/Write.cxx
+++ b/smtk/project/operators/Write.cxx
@@ -173,6 +173,17 @@ Write::Result Write::operateInternal()
   return result;
 }
 
+void Write::generateSummary(Operation::Result& res)
+{
+  if (smtk::operation::outcome(res) != Outcome::SUCCEEDED)
+  {
+    this->Superclass::generateSummary(res);
+  }
+  auto resourceItem = this->parameters()->associations();
+  auto resource = std::dynamic_pointer_cast<smtk::resource::Resource>(resourceItem->value());
+  smtkInfoMacro(this->log(), "Wrote \"" << resource->location() << "\".");
+}
+
 const char* Write::xmlDescription() const
 {
   return Write_xml;
diff --git a/smtk/project/operators/Write.h b/smtk/project/operators/Write.h
index 5a5c85f51f885db648cf64d7d26735237217cb89..e2c97f457b83d1d215b13b7cff30b46ad9a25976 100644
--- a/smtk/project/operators/Write.h
+++ b/smtk/project/operators/Write.h
@@ -34,6 +34,7 @@ public:
 
 protected:
   Result operateInternal() override;
+  void generateSummary(Operation::Result& res) override;
   const char* xmlDescription() const override;
 };
 
diff --git a/smtk/project/pybind11/PybindOperation.h b/smtk/project/pybind11/PybindOperation.h
index 306815c304ca624a9320e82234b8c0e647584a64..88667341566773b6d60c6007a3f27534b3590649 100644
--- a/smtk/project/pybind11/PybindOperation.h
+++ b/smtk/project/pybind11/PybindOperation.h
@@ -26,7 +26,6 @@ inline PySharedPtrClass< smtk::project::Operation, smtk::operation::XMLOperation
 
   PySharedPtrClass< smtk::project::Operation, smtk::operation::XMLOperation > instance(m, "Operation");
   instance
-    .def("deepcopy", (smtk::project::Operation & (smtk::project::Operation::*)(::smtk::project::Operation const &)) &smtk::project::Operation::operator=)
     .def("typeName", &smtk::project::Operation::typeName)
     .def("shared_from_this", (std::shared_ptr<smtk::project::Operation> (smtk::project::Operation::*)()) &smtk::project::Operation::shared_from_this)
     .def("shared_from_this", (std::shared_ptr<const smtk::project::Operation> (smtk::project::Operation::*)() const) &smtk::project::Operation::shared_from_this)
diff --git a/smtk/resource/Artifact.cxx b/smtk/resource/Artifact.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..bee50b7b6ecb795f6b2b06fc921d752f1e86eda7
--- /dev/null
+++ b/smtk/resource/Artifact.cxx
@@ -0,0 +1,60 @@
+//=========================================================================
+//  Copyright (c) Kitware, Inc.
+//  All rights reserved.
+//  See LICENSE.txt for details.
+//
+//  This software is distributed WITHOUT ANY WARRANTY; without even
+//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+//  PURPOSE.  See the above copyright notice for more information.
+//=========================================================================
+#include "smtk/resource/Artifact.h"
+
+namespace smtk
+{
+namespace resource
+{
+
+bool Artifact::setLocation(URL location)
+{
+  if (m_location == location)
+  {
+    return false;
+  }
+  m_location = location;
+  return true;
+}
+
+bool Artifact::setChecksum(smtk::string::Token algorithm, const std::vector<std::uint8_t>& value)
+{
+  if (m_checksumAlgorithm == algorithm && m_checksumData == value)
+  {
+    return false;
+  }
+  m_checksumAlgorithm = algorithm;
+  m_checksumData = value;
+  return true;
+}
+
+bool Artifact::setTimestamp(smtk::string::Token format, const std::vector<std::uint8_t>& value)
+{
+  if (m_timestampFormat == format && m_timestampData == value)
+  {
+    return false;
+  }
+  m_timestampFormat = format;
+  m_timestampData = value;
+  return true;
+}
+
+bool Artifact::setExtant(bool isExtant)
+{
+  if (m_extant == isExtant)
+  {
+    return false;
+  }
+  m_extant = isExtant;
+  return true;
+}
+
+} // namespace resource
+} // namespace smtk
diff --git a/smtk/resource/Artifact.h b/smtk/resource/Artifact.h
new file mode 100644
index 0000000000000000000000000000000000000000..4fa7bbac7f47f00368d81835e141e3afe2b03deb
--- /dev/null
+++ b/smtk/resource/Artifact.h
@@ -0,0 +1,121 @@
+//=========================================================================
+//  Copyright (c) Kitware, Inc.
+//  All rights reserved.
+//  See LICENSE.txt for details.
+//
+//  This software is distributed WITHOUT ANY WARRANTY; without even
+//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+//  PURPOSE.  See the above copyright notice for more information.
+//=========================================================================
+#ifndef smtk_resource_Artifact_h
+#define smtk_resource_Artifact_h
+
+#include "smtk/resource/Component.h"
+#include "smtk/common/URL.h" // For ivar and API.
+
+namespace smtk
+{
+namespace resource
+{
+
+/**\brief An artifact is a component that represents external data.
+  *
+  * Artifacts are typically used to represent files that hold opaque
+  * data relevant to a simulation. Examples include historical input
+  * decks, job logs, job results files, meshes of simulation domains.
+  *
+  * Artifacts may be stored inside or outside of a resource (i.e., they
+  * may be shared among multiple resources or they may be considered
+  * owned by a single resource).
+  *
+  * Artifacts may be small or large.
+  *
+  * What characterizes artifacts is that they reference data external
+  * to a resource. The data is referenced via an smtk::common::URL.
+  * URLs may be absolute (and must be if the data is stored external to
+  * the resource) or relative (and should be if the data is stored internal
+  * to the resource). Any relative URL is considered relative to its parent
+  * resource's location.
+  *
+  * Artifacts may also be characterized by a checksum and a timestamp.
+  * This allows resources to determine whether an artifact has been
+  * modified since the last time it was accessed.
+  *
+  * Note that this class is abstract; if you wish your resource to store
+  * instances of Artifact, you will need to subclass it to provide
+  *
+  */
+class SMTKCORE_EXPORT Artifact : public PersistentObject
+{
+public:
+  using URL = smtk::common::URL;
+
+  smtkTypeMacro(smtk::resource::Artifact);
+  smtkSuperclassMacro(smtk::resource::Component);
+  smtkSharedFromThisMacro(smtk::resource::PersistentObject);
+
+  ~Artifact() override = default;
+
+  // const smtk::common::UUID& id() const override { return m_id; }
+  // void setId(const smtk::common::UUID& uid) override { m_id = uid; }
+
+  /// Return the location of the artifact's data.
+  const URL& location() const { return m_location; }
+
+  /// Set the location of the artifact's data.
+  bool setLocation(URL location);
+
+  /// Indicate whether the artifact has a checksum provided by returning
+  /// the checksum algorithm.
+  ///
+  /// If the returned token is invalid, no checksum is available.
+  smtk::string::Token hasChecksum() const { return m_checksumAlgorithm; }
+
+  /// Return the artifact's checksum (if it has one).
+  std::vector<std::uint8_t> checksumData() const { return m_checksumData; }
+
+  /// Set the artifact's current checksum.
+  virtual bool setChecksum(smtk::string::Token algorithm, const std::vector<std::uint8_t>& value);
+
+  /// Indicate whether the artifact has a timestamp provided by returning
+  /// the timestamp format.
+  ///
+  /// If the returned token is invalid, no timestamp is available.
+  smtk::string::Token hasTimestamp() const { return m_timestampFormat; }
+
+  /// Return the artifact's timestamp (if it has one).
+  std::vector<std::uint8_t> timestampData() const { return m_timestampData; }
+
+  /// Set the artifact's current timestamp.
+  virtual bool setTimestamp(smtk::string::Token format, const std::vector<std::uint8_t>& value);
+
+  /// Return true if the artifact is still extant and false if expired.
+  ///
+  /// If an application determines an artifact is no longer accessible,
+  /// it may call setExtant(). By default, artifacts are extant upon
+  /// creation.
+  bool extant() const { return m_extant; }
+
+  /// Set whether an artifact exists at its location or not.
+  ///
+  /// Artifacts may not be extant initially (for example a log file may not
+  /// exist before a simulation job has commenced) or finally (for example a
+  /// log file may be removed after a certain time) or at other times in
+  /// the lifecycle of the artifact.
+  bool setExtant(bool isExtant);
+
+protected:
+  Artifact();
+
+  smtk::common::UUID m_id;
+  URL m_location;
+  smtk::string::Token m_checksumAlgorithm;
+  std::vector<std::uint8_t> m_checksumData;
+  smtk::string::Token m_timestampFormat;
+  std::vector<std::uint8_t> m_timestampData;
+  bool m_extant{ true };
+};
+} // namespace resource
+} // namespace smtk
+
+#endif // smtk_resource_Artifact_h
diff --git a/smtk/resource/CMakeLists.txt b/smtk/resource/CMakeLists.txt
index ea3e64e7568747fc8d34f7ae0a116f1edc581a26..82e0a32ed9ff6ac0844a958d78e1b7a71214a4d8 100644
--- a/smtk/resource/CMakeLists.txt
+++ b/smtk/resource/CMakeLists.txt
@@ -1,5 +1,6 @@
 # set up sources to build
 set(resourceSrcs
+  Artifact.cxx
   Component.cxx
   ComponentLinks.cxx
   CopyOptions.cxx
@@ -26,6 +27,7 @@ set(resourceSrcs
 )
 
 set(resourceHeaders
+  Artifact.h
   Component.h
   ComponentLinks.h
   Container.h
diff --git a/smtk/resource/Resource.cxx b/smtk/resource/Resource.cxx
index de90b2788ab405d51498c8f875620ba2e961af88..9523d7f866df462d211f0676c28787f51738262c 100644
--- a/smtk/resource/Resource.cxx
+++ b/smtk/resource/Resource.cxx
@@ -200,6 +200,39 @@ bool Resource::setLocation(const std::string& myLocation)
   return false;
 }
 
+std::string Resource::absoluteLocation(bool createDir) const
+{
+  auto url = this->location();
+  if (!smtk::common::Paths::isRelative(url))
+  {
+    if (createDir)
+    {
+      auto containingDir = smtk::common::Paths::directory(url);
+      smtk::common::Paths::createDirectory(containingDir);
+    }
+    return url;
+  }
+  const auto* parent = this->parentResource();
+  if (parent && parent != this)
+  {
+    auto parentUrl = parent->absoluteLocation();
+    // The parent URL contains the filename of the parent resource. Strip that:
+    auto parentDir = smtk::common::Paths::directory(parentUrl);
+    url = parentDir + "/" + url;
+  }
+  else
+  {
+    auto workDir = smtk::common::Paths::currentDirectory();
+    url = workDir + "/" + url;
+  }
+  if (createDir)
+  {
+    auto containingDir = smtk::common::Paths::directory(url);
+    smtk::common::Paths::createDirectory(containingDir);
+  }
+  return url;
+}
+
 std::string Resource::name() const
 {
   if (m_name.empty())
@@ -225,9 +258,9 @@ void Resource::setClean(bool state)
   m_clean = state;
 }
 
-bool Resource::setUnitsSystem(const shared_ptr<units::System>& unitsSystem)
+bool Resource::setUnitSystem(const shared_ptr<units::System>& unitSystem)
 {
-  m_unitsSystem = unitsSystem;
+  m_unitSystem = unitSystem;
   return true;
 }
 
@@ -285,15 +318,15 @@ void Resource::copyUnitSystem(
       // Do not set a unit system.
       break;
     case CopyOptions::CopyType::Shallow:
-      this->setUnitsSystem(rsrc->unitsSystem());
+      this->setUnitSystem(rsrc->unitSystem());
       break;
     case CopyOptions::CopyType::Deep:
     {
-      if (auto unitSys = rsrc->unitsSystem())
+      if (auto unitSys = rsrc->unitSystem())
       {
         nlohmann::json spec = unitSys;
         shared_ptr<units::System> unitCopy = units::System::createFromSpec(spec.dump());
-        this->setUnitsSystem(unitCopy);
+        this->setUnitSystem(unitCopy);
       }
     }
     break;
diff --git a/smtk/resource/Resource.h b/smtk/resource/Resource.h
index c7d1c9706ba33a9abab6c96fc87b19eeada70475..39c40e0897f3eda74a556ab72e8a50537f60073f 100644
--- a/smtk/resource/Resource.h
+++ b/smtk/resource/Resource.h
@@ -13,6 +13,7 @@
 
 #include "smtk/CoreExports.h"
 
+#include "smtk/common/Deprecation.h"
 #include "smtk/common/UUID.h"
 
 #include "smtk/resource/Component.h"
@@ -170,6 +171,28 @@ public:
   /// This may change when a user chooses to "Save As…" a different filename.
   const std::string& location() const { return m_location; }
   virtual bool setLocation(const std::string& location);
+
+  /// As a convenience, fetch the absolute path to a resource from its location.
+  ///
+  /// For URLs with a protocol (e.g., "https://foo.com/resource.smtk") or local
+  /// paths with a leading slash (e.g., "/foo/resource.smtk"), this is exactly
+  /// the resource's location() string.
+  ///
+  /// However, if a resource has a relative path, we must look to see whether
+  /// it has a parent resource; if so, this method is recursively called on
+  /// the parent to find its absolute path so that the relative path of the
+  /// child resource can be determined.
+  ///
+  /// When a resource (the immediate one or any parent) has a relative location
+  /// but no parent, then the current working directory is used to determine
+  /// its absolute path.
+  ///
+  /// As an added convenience, you may pass "true" as a second argument to this
+  /// method to have it create the containing directory of the resource's
+  /// location for you. This is for use by Write operations.
+  ///
+  /// \sa smtk::common::Paths::isRelative, smtk::common::Paths::currentDirectory
+  std::string absoluteLocation(bool createDir = false) const;
   ///@}
 
   ///@name Naming
@@ -331,9 +354,15 @@ public:
   /// unit systems.
 
   /// \brief Sets the system of units used by this resource.
-  virtual bool setUnitsSystem(const shared_ptr<units::System>& unitsSystem);
+  virtual bool setUnitSystem(const shared_ptr<units::System>& unitSystem);
+  SMTK_DEPRECATED_IN_NEXT("Use setUnitSystem() instead.")
+  virtual bool setUnitsSystem(const shared_ptr<units::System>& unitsSystem)
+  { return this->setUnitSystem(unitsSystem); }
+
   /// \brief Gets the system of units used by this resource.
-  const shared_ptr<units::System>& unitsSystem() const { return m_unitsSystem; }
+  const shared_ptr<units::System>& unitSystem() const { return m_unitSystem; }
+  SMTK_DEPRECATED_IN_NEXT("Use setUnitSystem() instead.")
+  const shared_ptr<units::System>& unitsSystem() const { return m_unitSystem; }
   ///@}
 
   ///@name Resource Templates
@@ -489,7 +518,7 @@ protected:
   void copyLinks(const std::shared_ptr<const Resource>& rsrc, const CopyOptions& options);
 
   WeakManagerPtr m_manager;
-  std::shared_ptr<units::System> m_unitsSystem;
+  std::shared_ptr<units::System> m_unitSystem;
 
 private:
   /// Instances of this internal class are passed to resource::Manager to
@@ -594,6 +623,7 @@ SMTKCORE_NO_EXPORT QueryType& queryForObject(const PersistentObject& object)
   }
   throw query::BadTypeError(smtk::common::typeName<QueryType>());
 }
+
 } // namespace resource
 } // namespace smtk
 
diff --git a/smtk/resource/pybind11/PybindResource.h b/smtk/resource/pybind11/PybindResource.h
index 8812f707d60393b979750acf8c8f0a6bec19cffe..9ff47c70bc970e05bd204a2347f11699b46c1215 100644
--- a/smtk/resource/pybind11/PybindResource.h
+++ b/smtk/resource/pybind11/PybindResource.h
@@ -23,6 +23,11 @@
 
 #include "smtk/resource/pybind11/PyResource.h"
 
+// For unit conversion
+#include "units/Measurement.h"
+#include "units/System.h"
+#include "units/Unit.h"
+
 namespace py = pybind11;
 
 inline PySharedPtrClass< smtk::resource::Resource, smtk::resource::PyResource, smtk::resource::PersistentObject > pybind11_init_smtk_resource_Resource(py::module &m)
@@ -150,6 +155,37 @@ inline PySharedPtrClass< smtk::resource::Resource, smtk::resource::PyResource, s
     .def("setMarkedForRemoval", &smtk::resource::Resource::setMarkedForRemoval, py::arg("val"))
     .def("setName", &smtk::resource::Resource::setName, py::arg("name"))
     .def("typeName", &smtk::resource::Resource::typeName)
+    .def("createDefaultUnitSystem", [](smtk::resource::Resource* rsrc)
+      {
+        auto sys = rsrc->unitSystem();
+        if (!sys)
+        {
+          sys = units::System::createWithDefaults();
+          rsrc->setUnitSystem(sys);
+          return true;
+        }
+        return false;
+      }
+    )
+    .def("unitConversion", [](smtk::resource::Resource* rsrc, const std::string& measurement, const std::string& unit) -> double
+      {
+        double result = std::numeric_limits<double>::quiet_NaN();
+        auto sys = rsrc->unitSystem();
+        if (!sys)
+        {
+          return result;
+        }
+        bool didConvert;
+        auto valueIn = sys->measurement(measurement, &didConvert);
+        if (!didConvert) { return result; }
+        auto unitOut = sys->unit(unit, &didConvert);
+        if (!didConvert) { return result; }
+        auto valueOut = sys->convert(valueIn, unitOut, &didConvert);
+        if (!didConvert) { return result; }
+        result = valueOut.m_value;
+        return result;
+      }, py::arg("valueWithUnitsIn"), py::arg("unitsOut")
+    )
     .def("visit", &smtk::resource::Resource::visit, py::arg("v"))
     .def_static("visuallyLinkedRole", &smtk::resource::Resource::visuallyLinkedRole)
     .def_readonly_static("VisuallyLinkedRole", &smtk::resource::Resource::VisuallyLinkedRole)
diff --git a/smtk/resource/testing/python/CMakeLists.txt b/smtk/resource/testing/python/CMakeLists.txt
index a3cff4a0a21bc78aeb776dac67e9f7f7645fd3ec..1a22079cb26041b23222c29d45a4e4de1137c865 100644
--- a/smtk/resource/testing/python/CMakeLists.txt
+++ b/smtk/resource/testing/python/CMakeLists.txt
@@ -1,5 +1,6 @@
 set(smtkResourcePythonTests
   testResource
+  testResourceUnitSystem
 )
 
 # Additional tests that require SMTK_DATA_DIR
diff --git a/smtk/resource/testing/python/testResourceUnitSystem.py b/smtk/resource/testing/python/testResourceUnitSystem.py
new file mode 100644
index 0000000000000000000000000000000000000000..e4e4ee0222c93f898d21ec8d43a940a7aea9a7e2
--- /dev/null
+++ b/smtk/resource/testing/python/testResourceUnitSystem.py
@@ -0,0 +1,34 @@
+# =============================================================================
+#
+#  Copyright (c) Kitware, Inc.
+#  All rights reserved.
+#  See LICENSE.txt for details.
+#
+#  This software is distributed WITHOUT ANY WARRANTY; without even
+#  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+#  PURPOSE.  See the above copyright notice for more information.
+#
+# =============================================================================
+
+import smtk
+import smtk.common
+import smtk.resource
+import smtk.model
+
+import math
+
+# Create a resource; by default it has no unit system.
+rsrc = smtk.model.Resource.create()
+# Test that conversion fails with no unit system.
+result = rsrc.unitConversion("1 ft", "m")
+if not math.isnan(result):
+    raise RuntimeError('Unit conversion with no unit system should fail.')
+
+rsrc.createDefaultUnitSystem()
+result = rsrc.unitConversion("1 ft", "m")
+if math.fabs(result - 0.3048) > 1e-7:
+    raise RuntimeError('Unit conversion failed.')
+
+result = rsrc.unitConversion("1 ft", "second")
+if not math.isnan(result):
+    raise RuntimeError('Unit conversion between incompatible units should fail.')
diff --git a/smtk/session/mesh/pybind11/PybindCreateUniformGrid.h b/smtk/session/mesh/pybind11/PybindCreateUniformGrid.h
index 8d307ef0eed2850d7c507b8ec5b90b8e3e2bef76..22da73495a7d38a44e8341ba3c77a721532c0021 100644
--- a/smtk/session/mesh/pybind11/PybindCreateUniformGrid.h
+++ b/smtk/session/mesh/pybind11/PybindCreateUniformGrid.h
@@ -23,9 +23,7 @@ inline PySharedPtrClass< smtk::session::mesh::CreateUniformGrid, smtk::operation
 {
   PySharedPtrClass< smtk::session::mesh::CreateUniformGrid, smtk::operation::XMLOperation > instance(m, "CreateUniformGrid");
   instance
-    .def(py::init<::smtk::session::mesh::CreateUniformGrid const &>())
     .def(py::init<>())
-    .def("deepcopy", (smtk::session::mesh::CreateUniformGrid & (smtk::session::mesh::CreateUniformGrid::*)(::smtk::session::mesh::CreateUniformGrid const &)) &smtk::session::mesh::CreateUniformGrid::operator=)
     .def_static("create", (std::shared_ptr<smtk::session::mesh::CreateUniformGrid> (*)()) &smtk::session::mesh::CreateUniformGrid::create)
     .def_static("create", (std::shared_ptr<smtk::session::mesh::CreateUniformGrid> (*)(::std::shared_ptr<smtk::session::mesh::CreateUniformGrid> &)) &smtk::session::mesh::CreateUniformGrid::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<smtk::session::mesh::CreateUniformGrid> (smtk::session::mesh::CreateUniformGrid::*)()) &smtk::session::mesh::CreateUniformGrid::shared_from_this)
diff --git a/smtk/session/mesh/pybind11/PybindEulerCharacteristicRatio.h b/smtk/session/mesh/pybind11/PybindEulerCharacteristicRatio.h
index f138013f3a82ead4dec0eb9017b65ce5986b7072..c50a531de1fba9f407058b85211c580df3922153 100644
--- a/smtk/session/mesh/pybind11/PybindEulerCharacteristicRatio.h
+++ b/smtk/session/mesh/pybind11/PybindEulerCharacteristicRatio.h
@@ -23,9 +23,7 @@ inline PySharedPtrClass< smtk::session::mesh::EulerCharacteristicRatio, smtk::op
 {
   PySharedPtrClass< smtk::session::mesh::EulerCharacteristicRatio, smtk::operation::XMLOperation > instance(m, "EulerCharacteristicRatio");
   instance
-    .def(py::init<::smtk::session::mesh::EulerCharacteristicRatio const &>())
     .def(py::init<>())
-    .def("deepcopy", (smtk::session::mesh::EulerCharacteristicRatio & (smtk::session::mesh::EulerCharacteristicRatio::*)(::smtk::session::mesh::EulerCharacteristicRatio const &)) &smtk::session::mesh::EulerCharacteristicRatio::operator=)
     .def_static("create", (std::shared_ptr<smtk::session::mesh::EulerCharacteristicRatio> (*)()) &smtk::session::mesh::EulerCharacteristicRatio::create)
     .def_static("create", (std::shared_ptr<smtk::session::mesh::EulerCharacteristicRatio> (*)(::std::shared_ptr<smtk::session::mesh::EulerCharacteristicRatio> &)) &smtk::session::mesh::EulerCharacteristicRatio::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<smtk::session::mesh::EulerCharacteristicRatio> (smtk::session::mesh::EulerCharacteristicRatio::*)()) &smtk::session::mesh::EulerCharacteristicRatio::shared_from_this)
diff --git a/smtk/session/mesh/pybind11/PybindExport.h b/smtk/session/mesh/pybind11/PybindExport.h
index 631c8e6aeb370f61a54eafb80c73361d99d7b526..a842082da32157bd52370221bb5397e9697b23e9 100644
--- a/smtk/session/mesh/pybind11/PybindExport.h
+++ b/smtk/session/mesh/pybind11/PybindExport.h
@@ -23,9 +23,7 @@ inline PySharedPtrClass< smtk::session::mesh::Export, smtk::operation::XMLOperat
 {
   PySharedPtrClass< smtk::session::mesh::Export, smtk::operation::XMLOperation > instance(m, "Export");
   instance
-    .def(py::init<::smtk::session::mesh::Export const &>())
     .def(py::init<>())
-    .def("deepcopy", (smtk::session::mesh::Export & (smtk::session::mesh::Export::*)(::smtk::session::mesh::Export const &)) &smtk::session::mesh::Export::operator=)
     .def_static("create", (std::shared_ptr<smtk::session::mesh::Export> (*)()) &smtk::session::mesh::Export::create)
     .def_static("create", (std::shared_ptr<smtk::session::mesh::Export> (*)(::std::shared_ptr<smtk::session::mesh::Export> &)) &smtk::session::mesh::Export::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<smtk::session::mesh::Export> (smtk::session::mesh::Export::*)()) &smtk::session::mesh::Export::shared_from_this)
diff --git a/smtk/session/mesh/pybind11/PybindImport.h b/smtk/session/mesh/pybind11/PybindImport.h
index 8c48f8bfaf4e4408419fd2ac27b2fc10f9a74654..7c737a9a5491803b028234f6cf0163a0900324c3 100644
--- a/smtk/session/mesh/pybind11/PybindImport.h
+++ b/smtk/session/mesh/pybind11/PybindImport.h
@@ -23,9 +23,7 @@ inline PySharedPtrClass< smtk::session::mesh::Import, smtk::operation::XMLOperat
 {
   PySharedPtrClass< smtk::session::mesh::Import, smtk::operation::XMLOperation > instance(m, "Import");
   instance
-    .def(py::init<::smtk::session::mesh::Import const &>())
     .def(py::init<>())
-    .def("deepcopy", (smtk::session::mesh::Import & (smtk::session::mesh::Import::*)(::smtk::session::mesh::Import const &)) &smtk::session::mesh::Import::operator=)
     .def_static("create", (std::shared_ptr<smtk::session::mesh::Import> (*)()) &smtk::session::mesh::Import::create)
     .def_static("create", (std::shared_ptr<smtk::session::mesh::Import> (*)(::std::shared_ptr<smtk::session::mesh::Import> &)) &smtk::session::mesh::Import::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<smtk::session::mesh::Import> (smtk::session::mesh::Import::*)()) &smtk::session::mesh::Import::shared_from_this)
diff --git a/smtk/session/mesh/pybind11/PybindRead.h b/smtk/session/mesh/pybind11/PybindRead.h
index c05001a0488d059f6f6cc8f2a222e2c4a1a637b9..6746242067299c343f49fa44ab689b36e5e87f25 100644
--- a/smtk/session/mesh/pybind11/PybindRead.h
+++ b/smtk/session/mesh/pybind11/PybindRead.h
@@ -25,9 +25,7 @@ inline PySharedPtrClass< smtk::session::mesh::Read, smtk::operation::XMLOperatio
 {
   PySharedPtrClass< smtk::session::mesh::Read, smtk::operation::XMLOperation > instance(m, "Read");
   instance
-    .def(py::init<::smtk::session::mesh::Read const &>())
     .def(py::init<>())
-    .def("deepcopy", (smtk::session::mesh::Read & (smtk::session::mesh::Read::*)(::smtk::session::mesh::Read const &)) &smtk::session::mesh::Read::operator=)
     .def_static("create", (std::shared_ptr<smtk::session::mesh::Read> (*)()) &smtk::session::mesh::Read::create)
     .def_static("create", (std::shared_ptr<smtk::session::mesh::Read> (*)(::std::shared_ptr<smtk::session::mesh::Read> &)) &smtk::session::mesh::Read::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<smtk::session::mesh::Read> (smtk::session::mesh::Read::*)()) &smtk::session::mesh::Read::shared_from_this)
diff --git a/smtk/session/mesh/pybind11/PybindWrite.h b/smtk/session/mesh/pybind11/PybindWrite.h
index ae21f08115d1bcbadf19aeb337ee8dea341aa034..98ffebe4a3b66197001bddb89d9fe9d3de5dd71d 100644
--- a/smtk/session/mesh/pybind11/PybindWrite.h
+++ b/smtk/session/mesh/pybind11/PybindWrite.h
@@ -25,9 +25,7 @@ inline PySharedPtrClass< smtk::session::mesh::Write, smtk::operation::XMLOperati
 {
   PySharedPtrClass< smtk::session::mesh::Write, smtk::operation::XMLOperation > instance(m, "Write");
   instance
-    .def(py::init<::smtk::session::mesh::Write const &>())
     .def(py::init<>())
-    .def("deepcopy", (smtk::session::mesh::Write & (smtk::session::mesh::Write::*)(::smtk::session::mesh::Write const &)) &smtk::session::mesh::Write::operator=)
     .def_static("create", (std::shared_ptr<smtk::session::mesh::Write> (*)()) &smtk::session::mesh::Write::create)
     .def_static("create", (std::shared_ptr<smtk::session::mesh::Write> (*)(::std::shared_ptr<smtk::session::mesh::Write> &)) &smtk::session::mesh::Write::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<smtk::session::mesh::Write> (smtk::session::mesh::Write::*)()) &smtk::session::mesh::Write::shared_from_this)
diff --git a/smtk/session/polygon/pybind11/PybindCreateEdge.h b/smtk/session/polygon/pybind11/PybindCreateEdge.h
index 4e727a80518eca549054bb72271bf24a609e0688..4fd8edcb5d73cdb815273f32b9d167c027e154b5 100644
--- a/smtk/session/polygon/pybind11/PybindCreateEdge.h
+++ b/smtk/session/polygon/pybind11/PybindCreateEdge.h
@@ -21,7 +21,6 @@ inline PySharedPtrClass< smtk::session::polygon::CreateEdge > pybind11_init_smtk
 {
   PySharedPtrClass< smtk::session::polygon::CreateEdge > instance(m, "CreateEdge", parent);
   instance
-    .def(py::init<::smtk::session::polygon::CreateEdge const &>())
     .def_static("create", (std::shared_ptr<smtk::session::polygon::CreateEdge> (*)()) &smtk::session::polygon::CreateEdge::create)
     ;
   return instance;
diff --git a/smtk/session/polygon/pybind11/PybindCreateEdgeFromPoints.h b/smtk/session/polygon/pybind11/PybindCreateEdgeFromPoints.h
index 1935aeaceb9ea872513c43da37b011ea823878a4..8a33f4885042ecb257a597a5398b1a2a0fc43ed8 100644
--- a/smtk/session/polygon/pybind11/PybindCreateEdgeFromPoints.h
+++ b/smtk/session/polygon/pybind11/PybindCreateEdgeFromPoints.h
@@ -23,8 +23,6 @@ inline PySharedPtrClass< smtk::session::polygon::CreateEdgeFromPoints > pybind11
 {
   PySharedPtrClass< smtk::session::polygon::CreateEdgeFromPoints > instance(m, "CreateEdgeFromPoints", parent);
   instance
-    .def(py::init<::smtk::session::polygon::CreateEdgeFromPoints const &>())
-    .def("deepcopy", (smtk::session::polygon::CreateEdgeFromPoints & (smtk::session::polygon::CreateEdgeFromPoints::*)(::smtk::session::polygon::CreateEdgeFromPoints const &)) &smtk::session::polygon::CreateEdgeFromPoints::operator=)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::CreateEdgeFromPoints> (*)()) &smtk::session::polygon::CreateEdgeFromPoints::create)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::CreateEdgeFromPoints> (*)(::std::shared_ptr<smtk::session::polygon::CreateEdgeFromPoints> &)) &smtk::session::polygon::CreateEdgeFromPoints::create, py::arg("ref"))
     .def("process", &smtk::session::polygon::CreateEdgeFromPoints::process, py::arg("pnts"), py::arg("numCoordsPerPoint"), py::arg("parentModel"))
diff --git a/smtk/session/polygon/pybind11/PybindCreateEdgeFromVertices.h b/smtk/session/polygon/pybind11/PybindCreateEdgeFromVertices.h
index 5f293fe1ff466522018c2a0dfe2207ee687410a5..3059e7a68bb86898a71f411eb015ac4d0cf670ba 100644
--- a/smtk/session/polygon/pybind11/PybindCreateEdgeFromVertices.h
+++ b/smtk/session/polygon/pybind11/PybindCreateEdgeFromVertices.h
@@ -21,8 +21,6 @@ inline PySharedPtrClass< smtk::session::polygon::CreateEdgeFromVertices > pybind
 {
   PySharedPtrClass< smtk::session::polygon::CreateEdgeFromVertices > instance(m, "CreateEdgeFromVertices", parent);
   instance
-    .def(py::init<::smtk::session::polygon::CreateEdgeFromVertices const &>())
-    .def("deepcopy", (smtk::session::polygon::CreateEdgeFromVertices & (smtk::session::polygon::CreateEdgeFromVertices::*)(::smtk::session::polygon::CreateEdgeFromVertices const &)) &smtk::session::polygon::CreateEdgeFromVertices::operator=)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::CreateEdgeFromVertices> (*)()) &smtk::session::polygon::CreateEdgeFromVertices::create)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::CreateEdgeFromVertices> (*)(::std::shared_ptr<smtk::session::polygon::CreateEdgeFromVertices> &)) &smtk::session::polygon::CreateEdgeFromVertices::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<const smtk::session::polygon::CreateEdgeFromVertices> (smtk::session::polygon::CreateEdgeFromVertices::*)() const) &smtk::session::polygon::CreateEdgeFromVertices::shared_from_this)
diff --git a/smtk/session/polygon/pybind11/PybindCreateFaces.h b/smtk/session/polygon/pybind11/PybindCreateFaces.h
index 0232a87849f887b67ab5b155169dc87af0dbf742..83e608559fbdb5bafa957c9a958e20591f836b73 100644
--- a/smtk/session/polygon/pybind11/PybindCreateFaces.h
+++ b/smtk/session/polygon/pybind11/PybindCreateFaces.h
@@ -34,8 +34,6 @@ inline PySharedPtrClass< smtk::session::polygon::CreateFaces > pybind11_init_smt
 {
   PySharedPtrClass< smtk::session::polygon::CreateFaces > instance(m, "CreateFaces", parent);
   instance
-    .def(py::init<::smtk::session::polygon::CreateFaces const &>())
-    .def("deepcopy", (smtk::session::polygon::CreateFaces & (smtk::session::polygon::CreateFaces::*)(::smtk::session::polygon::CreateFaces const &)) &smtk::session::polygon::CreateFaces::operator=)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::CreateFaces> (*)()) &smtk::session::polygon::CreateFaces::create)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::CreateFaces> (*)(::std::shared_ptr<smtk::session::polygon::CreateFaces> &)) &smtk::session::polygon::CreateFaces::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<const smtk::session::polygon::CreateFaces> (smtk::session::polygon::CreateFaces::*)() const) &smtk::session::polygon::CreateFaces::shared_from_this)
diff --git a/smtk/session/polygon/pybind11/PybindCreateModel.h b/smtk/session/polygon/pybind11/PybindCreateModel.h
index c13f00b325222890a721e8a84204e7eebe258565..d61613a838633c066ca2a0b54d143de02d55a027 100644
--- a/smtk/session/polygon/pybind11/PybindCreateModel.h
+++ b/smtk/session/polygon/pybind11/PybindCreateModel.h
@@ -21,7 +21,6 @@ inline PySharedPtrClass< smtk::session::polygon::CreateModel > pybind11_init_smt
 {
   PySharedPtrClass< smtk::session::polygon::CreateModel > instance(m, "CreateModel", parent);
   instance
-    .def(py::init<::smtk::session::polygon::CreateModel const &>())
     .def_static("create", (std::shared_ptr<smtk::session::polygon::CreateModel> (*)()) &smtk::session::polygon::CreateModel::create)
     ;
   return instance;
diff --git a/smtk/session/polygon/pybind11/PybindCreateVertices.h b/smtk/session/polygon/pybind11/PybindCreateVertices.h
index fe29abbe64c25a96908e4b3382c55e16c96dfe10..545cda15cb2b5956376a0e0c3c03cb27f0ceb104 100644
--- a/smtk/session/polygon/pybind11/PybindCreateVertices.h
+++ b/smtk/session/polygon/pybind11/PybindCreateVertices.h
@@ -23,8 +23,6 @@ inline PySharedPtrClass< smtk::session::polygon::CreateVertices> pybind11_init_s
 {
   PySharedPtrClass< smtk::session::polygon::CreateVertices > instance(m, "CreateVertices", parent);
   instance
-    .def(py::init<::smtk::session::polygon::CreateVertices const &>())
-    .def("deepcopy", (smtk::session::polygon::CreateVertices & (smtk::session::polygon::CreateVertices::*)(::smtk::session::polygon::CreateVertices const &)) &smtk::session::polygon::CreateVertices::operator=)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::CreateVertices> (*)()) &smtk::session::polygon::CreateVertices::create)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::CreateVertices> (*)(::std::shared_ptr<smtk::session::polygon::CreateVertices> &)) &smtk::session::polygon::CreateVertices::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<const smtk::session::polygon::CreateVertices> (smtk::session::polygon::CreateVertices::*)() const) &smtk::session::polygon::CreateVertices::shared_from_this)
diff --git a/smtk/session/polygon/pybind11/PybindDelete.h b/smtk/session/polygon/pybind11/PybindDelete.h
index 68c61196d272781d697cc50b199669443e33288c..c897b10b9c5beb072a48d404b22cc9760908447b 100644
--- a/smtk/session/polygon/pybind11/PybindDelete.h
+++ b/smtk/session/polygon/pybind11/PybindDelete.h
@@ -23,8 +23,6 @@ inline PySharedPtrClass< smtk::session::polygon::Delete> pybind11_init_smtk_sess
 {
   PySharedPtrClass< smtk::session::polygon::Delete > instance(m, "Delete", parent);
   instance
-    .def(py::init<::smtk::session::polygon::Delete const &>())
-    .def("deepcopy", (smtk::session::polygon::Delete & (smtk::session::polygon::Delete::*)(::smtk::session::polygon::Delete const &)) &smtk::session::polygon::Delete::operator=)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::Delete> (*)()) &smtk::session::polygon::Delete::create)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::Delete> (*)(::std::shared_ptr<smtk::session::polygon::Delete> &)) &smtk::session::polygon::Delete::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<const smtk::session::polygon::Delete> (smtk::session::polygon::Delete::*)() const) &smtk::session::polygon::Delete::shared_from_this)
diff --git a/smtk/session/polygon/pybind11/PybindDemoteVertex.h b/smtk/session/polygon/pybind11/PybindDemoteVertex.h
index 820101da1e4a4b9108d54fe5b39785f3fd357d6a..471408450d09216b912ca90ef477e60d0bb8f52c 100644
--- a/smtk/session/polygon/pybind11/PybindDemoteVertex.h
+++ b/smtk/session/polygon/pybind11/PybindDemoteVertex.h
@@ -23,8 +23,6 @@ inline PySharedPtrClass< smtk::session::polygon::DemoteVertex > pybind11_init_sm
 {
   PySharedPtrClass< smtk::session::polygon::DemoteVertex > instance(m, "DemoteVertex", parent);
   instance
-    .def(py::init<::smtk::session::polygon::DemoteVertex const &>())
-    .def("deepcopy", (smtk::session::polygon::DemoteVertex & (smtk::session::polygon::DemoteVertex::*)(::smtk::session::polygon::DemoteVertex const &)) &smtk::session::polygon::DemoteVertex::operator=)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::DemoteVertex> (*)()) &smtk::session::polygon::DemoteVertex::create)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::DemoteVertex> (*)(::std::shared_ptr<smtk::session::polygon::DemoteVertex> &)) &smtk::session::polygon::DemoteVertex::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<const smtk::session::polygon::DemoteVertex> (smtk::session::polygon::DemoteVertex::*)() const) &smtk::session::polygon::DemoteVertex::shared_from_this)
diff --git a/smtk/session/polygon/pybind11/PybindExtractContours.h b/smtk/session/polygon/pybind11/PybindExtractContours.h
index eec43fff9ead594429d9b0eb985b139ef4de1e83..46049721cec4e404e3880cb7b92d40c5391d4d1a 100644
--- a/smtk/session/polygon/pybind11/PybindExtractContours.h
+++ b/smtk/session/polygon/pybind11/PybindExtractContours.h
@@ -24,8 +24,6 @@ inline PySharedPtrClass< smtk::session::polygon::ExtractContours > pybind11_init
   PySharedPtrClass< smtk::session::polygon::ExtractContours > instance(m, "ExtractContours", parent);
   instance
     .def(py::init<>())
-    .def(py::init<::smtk::session::polygon::ExtractContours const &>())
-    .def("deepcopy", (smtk::session::polygon::ExtractContours & (smtk::session::polygon::ExtractContours::*)(::smtk::session::polygon::ExtractContours const &)) &smtk::session::polygon::ExtractContours::operator=)
     .def("ableToOperate", &smtk::session::polygon::ExtractContours::ableToOperate)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::ExtractContours> (*)()) &smtk::session::polygon::ExtractContours::create)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::ExtractContours> (*)(::std::shared_ptr<smtk::session::polygon::ExtractContours> &)) &smtk::session::polygon::ExtractContours::create, py::arg("ref"))
diff --git a/smtk/session/polygon/pybind11/PybindForceCreateFace.h b/smtk/session/polygon/pybind11/PybindForceCreateFace.h
index b380c71098c2a6cb707ade248e21c9c9b78a5598..c625a29112ab8de10d5eba6a7de2d7674403278b 100644
--- a/smtk/session/polygon/pybind11/PybindForceCreateFace.h
+++ b/smtk/session/polygon/pybind11/PybindForceCreateFace.h
@@ -24,8 +24,6 @@ inline PySharedPtrClass< smtk::session::polygon::ForceCreateFace > pybind11_init
   PySharedPtrClass< smtk::session::polygon::ForceCreateFace > instance(m, "ForceCreateFace", parent);
   instance
     .def(py::init<>())
-    .def(py::init<::smtk::session::polygon::ForceCreateFace const &>())
-    .def("deepcopy", (smtk::session::polygon::ForceCreateFace & (smtk::session::polygon::ForceCreateFace::*)(::smtk::session::polygon::ForceCreateFace const &)) &smtk::session::polygon::ForceCreateFace::operator=)
     .def("ableToOperate", &smtk::session::polygon::ForceCreateFace::ableToOperate)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::ForceCreateFace> (*)()) &smtk::session::polygon::ForceCreateFace::create)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::ForceCreateFace> (*)(::std::shared_ptr<smtk::session::polygon::ForceCreateFace> &)) &smtk::session::polygon::ForceCreateFace::create, py::arg("ref"))
diff --git a/smtk/session/polygon/pybind11/PybindImport.h b/smtk/session/polygon/pybind11/PybindImport.h
index 2b943c00d47dfb96457c533da97715bfe1ef7ca8..5b59ab9bae03665a30d5078c06b38d695cb5a2c8 100644
--- a/smtk/session/polygon/pybind11/PybindImport.h
+++ b/smtk/session/polygon/pybind11/PybindImport.h
@@ -21,8 +21,6 @@ inline PySharedPtrClass< smtk::session::polygon::Import > pybind11_init_smtk_ses
 {
   PySharedPtrClass< smtk::session::polygon::Import > instance(m, "Import", parent);
   instance
-    .def(py::init<::smtk::session::polygon::Import const &>())
-    .def("deepcopy", (smtk::session::polygon::Import & (smtk::session::polygon::Import::*)(::smtk::session::polygon::Import const &)) &smtk::session::polygon::Import::operator=)
     .def("ableToOperate", &smtk::session::polygon::Import::ableToOperate)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::Import> (*)()) &smtk::session::polygon::Import::create)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::Import> (*)(::std::shared_ptr<smtk::session::polygon::Import> &)) &smtk::session::polygon::Import::create, py::arg("ref"))
diff --git a/smtk/session/polygon/pybind11/PybindImportPPG.h b/smtk/session/polygon/pybind11/PybindImportPPG.h
index 4ff34a66529c417bf1f0c981c5eb24435f598d9c..524e0b9f950b7f0450f1e85e6ec9debafb29e73f 100644
--- a/smtk/session/polygon/pybind11/PybindImportPPG.h
+++ b/smtk/session/polygon/pybind11/PybindImportPPG.h
@@ -21,8 +21,6 @@ inline PySharedPtrClass< smtk::session::polygon::ImportPPG > pybind11_init_smtk_
 {
   PySharedPtrClass< smtk::session::polygon::ImportPPG > instance(m, "ImportPPG", parent);
   instance
-    .def(py::init<::smtk::session::polygon::ImportPPG const &>())
-    .def("deepcopy", (smtk::session::polygon::ImportPPG & (smtk::session::polygon::ImportPPG::*)(::smtk::session::polygon::ImportPPG const &)) &smtk::session::polygon::ImportPPG::operator=)
     .def("ableToOperate", &smtk::session::polygon::ImportPPG::ableToOperate)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::ImportPPG> (*)()) &smtk::session::polygon::ImportPPG::create)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::ImportPPG> (*)(::std::shared_ptr<smtk::session::polygon::ImportPPG> &)) &smtk::session::polygon::ImportPPG::create, py::arg("ref"))
diff --git a/smtk/session/polygon/pybind11/PybindLegacyRead.h b/smtk/session/polygon/pybind11/PybindLegacyRead.h
index 9850b93a68138b1a5ffb5fa02cfc364beaeec81a..e4f346bc084b2648c22acdb634ac44be7a6bd5c2 100644
--- a/smtk/session/polygon/pybind11/PybindLegacyRead.h
+++ b/smtk/session/polygon/pybind11/PybindLegacyRead.h
@@ -21,8 +21,6 @@ inline PySharedPtrClass< smtk::session::polygon::LegacyRead > pybind11_init_smtk
 {
   PySharedPtrClass< smtk::session::polygon::LegacyRead > instance(m, "LegacyRead", parent);
   instance
-    .def(py::init<::smtk::session::polygon::LegacyRead const &>())
-    .def("deepcopy", (smtk::session::polygon::LegacyRead & (smtk::session::polygon::LegacyRead::*)(::smtk::session::polygon::LegacyRead const &)) &smtk::session::polygon::LegacyRead::operator=)
     .def("ableToOperate", &smtk::session::polygon::LegacyRead::ableToOperate)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::LegacyRead> (*)()) &smtk::session::polygon::LegacyRead::create)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::LegacyRead> (*)(::std::shared_ptr<smtk::session::polygon::LegacyRead> &)) &smtk::session::polygon::LegacyRead::create, py::arg("ref"))
diff --git a/smtk/session/polygon/pybind11/PybindOperation.h b/smtk/session/polygon/pybind11/PybindOperation.h
index a5063a4587b273d6d09a462775bcc79656592c64..4d4e62bd71484915920ee26e660970b48584116c 100644
--- a/smtk/session/polygon/pybind11/PybindOperation.h
+++ b/smtk/session/polygon/pybind11/PybindOperation.h
@@ -22,9 +22,6 @@ namespace py = pybind11;
 inline PySharedPtrClass< smtk::session::polygon::Operation, smtk::operation::XMLOperation > pybind11_init_smtk_session_polygon_Operation(py::module &m)
 {
   PySharedPtrClass< smtk::session::polygon::Operation, smtk::operation::XMLOperation > instance(m, "Operation");
-  instance
-    .def("deepcopy", (smtk::session::polygon::Operation & (smtk::session::polygon::Operation::*)(::smtk::session::polygon::Operation const &)) &smtk::session::polygon::Operation::operator=)
-    ;
   return instance;
 }
 
diff --git a/smtk/session/polygon/pybind11/PybindRead.h b/smtk/session/polygon/pybind11/PybindRead.h
index cb7de145dcf7391cd364e569004f1c2a3d46cf4e..054a02c8ede7543e35ce83e7dbb270be8df73ab4 100644
--- a/smtk/session/polygon/pybind11/PybindRead.h
+++ b/smtk/session/polygon/pybind11/PybindRead.h
@@ -23,8 +23,6 @@ inline PySharedPtrClass< smtk::session::polygon::Read > pybind11_init_smtk_sessi
 {
   PySharedPtrClass< smtk::session::polygon::Read > instance(m, "Read", parent);
   instance
-    .def(py::init<::smtk::session::polygon::Read const &>())
-    .def("deepcopy", (smtk::session::polygon::Read & (smtk::session::polygon::Read::*)(::smtk::session::polygon::Read const &)) &smtk::session::polygon::Read::operator=)
     .def("ableToOperate", &smtk::session::polygon::Read::ableToOperate)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::Read> (*)()) &smtk::session::polygon::Read::create)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::Read> (*)(::std::shared_ptr<smtk::session::polygon::Read> &)) &smtk::session::polygon::Read::create, py::arg("ref"))
diff --git a/smtk/session/polygon/pybind11/PybindSplitEdge.h b/smtk/session/polygon/pybind11/PybindSplitEdge.h
index 1e6527c1ba2b1547c2a9278a390f7ad9ac287a36..7064e366adef651f4b19dba1ec235e7baaf8365e 100644
--- a/smtk/session/polygon/pybind11/PybindSplitEdge.h
+++ b/smtk/session/polygon/pybind11/PybindSplitEdge.h
@@ -23,8 +23,6 @@ inline PySharedPtrClass< smtk::session::polygon::SplitEdge > pybind11_init_smtk_
 {
   PySharedPtrClass< smtk::session::polygon::SplitEdge > instance(m, "SplitEdge", parent);
   instance
-    .def(py::init<::smtk::session::polygon::SplitEdge const &>())
-    .def("deepcopy", (smtk::session::polygon::SplitEdge & (smtk::session::polygon::SplitEdge::*)(::smtk::session::polygon::SplitEdge const &)) &smtk::session::polygon::SplitEdge::operator=)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::SplitEdge> (*)()) &smtk::session::polygon::SplitEdge::create)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::SplitEdge> (*)(::std::shared_ptr<smtk::session::polygon::SplitEdge> &)) &smtk::session::polygon::SplitEdge::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<const smtk::session::polygon::SplitEdge> (smtk::session::polygon::SplitEdge::*)() const) &smtk::session::polygon::SplitEdge::shared_from_this)
diff --git a/smtk/session/polygon/pybind11/PybindTweakEdge.h b/smtk/session/polygon/pybind11/PybindTweakEdge.h
index ec1467160f25c566440de6e4769b0b7e18f5d3dc..ce89ef45324ddda2f7a6caa4e01d1fa4a3b57b43 100644
--- a/smtk/session/polygon/pybind11/PybindTweakEdge.h
+++ b/smtk/session/polygon/pybind11/PybindTweakEdge.h
@@ -23,8 +23,6 @@ inline PySharedPtrClass< smtk::session::polygon::TweakEdge > pybind11_init_smtk_
 {
   PySharedPtrClass< smtk::session::polygon::TweakEdge > instance(m, "TweakEdge", parent);
   instance
-    .def(py::init<::smtk::session::polygon::TweakEdge const &>())
-    .def("deepcopy", (smtk::session::polygon::TweakEdge & (smtk::session::polygon::TweakEdge::*)(::smtk::session::polygon::TweakEdge const &)) &smtk::session::polygon::TweakEdge::operator=)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::TweakEdge> (*)()) &smtk::session::polygon::TweakEdge::create)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::TweakEdge> (*)(::std::shared_ptr<smtk::session::polygon::TweakEdge> &)) &smtk::session::polygon::TweakEdge::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<const smtk::session::polygon::TweakEdge> (smtk::session::polygon::TweakEdge::*)() const) &smtk::session::polygon::TweakEdge::shared_from_this)
diff --git a/smtk/session/polygon/pybind11/PybindWrite.h b/smtk/session/polygon/pybind11/PybindWrite.h
index e7bec1512a622767b3ed75bfb352b984c5b5709f..a709786dccaae65b3f91c502545de9fd53b67abf 100644
--- a/smtk/session/polygon/pybind11/PybindWrite.h
+++ b/smtk/session/polygon/pybind11/PybindWrite.h
@@ -23,8 +23,6 @@ inline PySharedPtrClass< smtk::session::polygon::Write > pybind11_init_smtk_sess
 {
   PySharedPtrClass< smtk::session::polygon::Write > instance(m, "Write", parent);
   instance
-    .def(py::init<::smtk::session::polygon::Write const &>())
-    .def("deepcopy", (smtk::session::polygon::Write & (smtk::session::polygon::Write::*)(::smtk::session::polygon::Write const &)) &smtk::session::polygon::Write::operator=)
     .def("ableToOperate", &smtk::session::polygon::Write::ableToOperate)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::Write> (*)()) &smtk::session::polygon::Write::create)
     .def_static("create", (std::shared_ptr<smtk::session::polygon::Write> (*)(::std::shared_ptr<smtk::session::polygon::Write> &)) &smtk::session::polygon::Write::create, py::arg("ref"))
diff --git a/smtk/session/vtk/pybind11/PybindExport.h b/smtk/session/vtk/pybind11/PybindExport.h
index d33d0104decf552a9e237448aeb438a313f4c325..df02089f653944cdc79f5cb9242f2b11a3f838ba 100644
--- a/smtk/session/vtk/pybind11/PybindExport.h
+++ b/smtk/session/vtk/pybind11/PybindExport.h
@@ -22,7 +22,6 @@ inline PySharedPtrClass< smtk::session::vtk::Export > pybind11_init_smtk_session
   PySharedPtrClass< smtk::session::vtk::Export > instance(m, "Export", parent);
   instance
     .def(py::init<>())
-    .def("deepcopy", (smtk::session::vtk::Export & (smtk::session::vtk::Export::*)(::smtk::session::vtk::Export const &)) &smtk::session::vtk::Export::operator=)
     .def_static("create", (std::shared_ptr<smtk::session::vtk::Export> (*)()) &smtk::session::vtk::Export::create)
     .def_static("create", (std::shared_ptr<smtk::session::vtk::Export> (*)(::std::shared_ptr<smtk::session::vtk::Export> &)) &smtk::session::vtk::Export::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<const smtk::session::vtk::Export> (smtk::session::vtk::Export::*)() const) &smtk::session::vtk::Export::shared_from_this)
diff --git a/smtk/session/vtk/pybind11/PybindImport.h b/smtk/session/vtk/pybind11/PybindImport.h
index f63fa6a205b8b22cf8c54190270396a78fb93da3..34a5121a14d722cc0534b7fcbfb0fc2d01032367 100644
--- a/smtk/session/vtk/pybind11/PybindImport.h
+++ b/smtk/session/vtk/pybind11/PybindImport.h
@@ -22,7 +22,6 @@ inline PySharedPtrClass< smtk::session::vtk::Import > pybind11_init_smtk_session
   PySharedPtrClass< smtk::session::vtk::Import > instance(m, "Import", parent);
   instance
     .def(py::init<>())
-    .def("deepcopy", (smtk::session::vtk::Import & (smtk::session::vtk::Import::*)(::smtk::session::vtk::Import const &)) &smtk::session::vtk::Import::operator=)
     .def_static("create", (std::shared_ptr<smtk::session::vtk::Import> (*)()) &smtk::session::vtk::Import::create)
     .def_static("create", (std::shared_ptr<smtk::session::vtk::Import> (*)(::std::shared_ptr<smtk::session::vtk::Import> &)) &smtk::session::vtk::Import::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<const smtk::session::vtk::Import> (smtk::session::vtk::Import::*)() const) &smtk::session::vtk::Import::shared_from_this)
diff --git a/smtk/session/vtk/pybind11/PybindLegacyRead.h b/smtk/session/vtk/pybind11/PybindLegacyRead.h
index 4e4b24dbb3c2ef3d1fb22c9307d2695e4a53e1a0..366965344144a321c6bb65935e5f5888efe7ae3f 100644
--- a/smtk/session/vtk/pybind11/PybindLegacyRead.h
+++ b/smtk/session/vtk/pybind11/PybindLegacyRead.h
@@ -22,7 +22,6 @@ inline PySharedPtrClass< smtk::session::vtk::LegacyRead > pybind11_init_smtk_ses
   PySharedPtrClass< smtk::session::vtk::LegacyRead > instance(m, "LegacyRead", parent);
   instance
     .def(py::init<>())
-    .def("deepcopy", (smtk::session::vtk::LegacyRead & (smtk::session::vtk::LegacyRead::*)(::smtk::session::vtk::LegacyRead const &)) &smtk::session::vtk::LegacyRead::operator=)
     .def_static("create", (std::shared_ptr<smtk::session::vtk::LegacyRead> (*)()) &smtk::session::vtk::LegacyRead::create)
     .def_static("create", (std::shared_ptr<smtk::session::vtk::LegacyRead> (*)(::std::shared_ptr<smtk::session::vtk::LegacyRead> &)) &smtk::session::vtk::LegacyRead::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<const smtk::session::vtk::LegacyRead> (smtk::session::vtk::LegacyRead::*)() const) &smtk::session::vtk::LegacyRead::shared_from_this)
diff --git a/smtk/session/vtk/pybind11/PybindOperation.h b/smtk/session/vtk/pybind11/PybindOperation.h
index 239ebda656124e58b9e3e8c8a9236e9897b25c0e..b929d0058d9685303e0a4e4a781be7d155fe288a 100644
--- a/smtk/session/vtk/pybind11/PybindOperation.h
+++ b/smtk/session/vtk/pybind11/PybindOperation.h
@@ -22,9 +22,6 @@ namespace py = pybind11;
 inline PySharedPtrClass< smtk::session::vtk::Operation, smtk::operation::XMLOperation > pybind11_init_smtk_session_vtk_Operation(py::module &m)
 {
   PySharedPtrClass< smtk::session::vtk::Operation, smtk::operation::XMLOperation > instance(m, "Operation");
-  instance
-    .def("deepcopy", (smtk::session::vtk::Operation & (smtk::session::vtk::Operation::*)(::smtk::session::vtk::Operation const &)) &smtk::session::vtk::Operation::operator=)
-    ;
   return instance;
 }
 
diff --git a/smtk/session/vtk/pybind11/PybindRead.h b/smtk/session/vtk/pybind11/PybindRead.h
index d4f77557f82ee400a85a6e9b9c3473aaca2023e3..cf82ecd3b8cd40047d535ebcfd9f7c2a57722ada 100644
--- a/smtk/session/vtk/pybind11/PybindRead.h
+++ b/smtk/session/vtk/pybind11/PybindRead.h
@@ -24,7 +24,6 @@ inline PySharedPtrClass< smtk::session::vtk::Read > pybind11_init_smtk_session_v
   PySharedPtrClass< smtk::session::vtk::Read > instance(m, "Read", parent);
   instance
     .def(py::init<>())
-    .def("deepcopy", (smtk::session::vtk::Read & (smtk::session::vtk::Read::*)(::smtk::session::vtk::Read const &)) &smtk::session::vtk::Read::operator=)
     .def_static("create", (std::shared_ptr<smtk::session::vtk::Read> (*)()) &smtk::session::vtk::Read::create)
     .def_static("create", (std::shared_ptr<smtk::session::vtk::Read> (*)(::std::shared_ptr<smtk::session::vtk::Read> &)) &smtk::session::vtk::Read::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<const smtk::session::vtk::Read> (smtk::session::vtk::Read::*)() const) &smtk::session::vtk::Read::shared_from_this)
diff --git a/smtk/session/vtk/pybind11/PybindWrite.h b/smtk/session/vtk/pybind11/PybindWrite.h
index 88543d10ab124af27047c65530171e96d47c1040..aeefca5ef5d045fb9ab9e1c0faa18dd57a05d93a 100644
--- a/smtk/session/vtk/pybind11/PybindWrite.h
+++ b/smtk/session/vtk/pybind11/PybindWrite.h
@@ -24,7 +24,6 @@ inline PySharedPtrClass< smtk::session::vtk::Write > pybind11_init_smtk_session_
   PySharedPtrClass< smtk::session::vtk::Write > instance(m, "Write", parent);
   instance
     .def(py::init<>())
-    .def("deepcopy", (smtk::session::vtk::Write & (smtk::session::vtk::Write::*)(::smtk::session::vtk::Write const &)) &smtk::session::vtk::Write::operator=)
     .def_static("create", (std::shared_ptr<smtk::session::vtk::Write> (*)()) &smtk::session::vtk::Write::create)
     .def_static("create", (std::shared_ptr<smtk::session::vtk::Write> (*)(::std::shared_ptr<smtk::session::vtk::Write> &)) &smtk::session::vtk::Write::create, py::arg("ref"))
     .def("shared_from_this", (std::shared_ptr<const smtk::session::vtk::Write> (smtk::session::vtk::Write::*)() const) &smtk::session::vtk::Write::shared_from_this)
diff --git a/smtk/string/pybind11/PybindToken.h b/smtk/string/pybind11/PybindToken.h
index 0dad97c200507fddbbd3e7e5799c368ae40f6160..c973046491f407411cc5518a8dd9796fd23eab01 100644
--- a/smtk/string/pybind11/PybindToken.h
+++ b/smtk/string/pybind11/PybindToken.h
@@ -22,13 +22,25 @@ inline py::class_<smtk::string::Token> pybind11_init_smtk_string_Token(py::modul
   py::class_<smtk::string::Token> instance(m, "Token");
   instance
     .def_static("manager", &smtk::string::Token::manager)
+    .def(py::init<>())
     .def(py::init<const std::string &>())
     .def(py::init<smtk::string::Hash>())
     .def("data", &smtk::string::Token::data)
     .def("id", &smtk::string::Token::id)
+    .def("valid", &smtk::string::Token::valid)
+    .def("__str__", [](const smtk::string::Token& token)
+      { return token.data(); })
     .def("__repr__", [](const smtk::string::Token& token)
       {
-        return "<smtk.string.Token '" + token.data() + "'>";
+        if (token.valid())
+        {
+          if (token.hasData())
+          {
+            return "Token('" + token.data() + "')";
+          }
+          return "Token(" + std::to_string(token.id()) + ")";
+        }
+        return std::string("Token()");
       })
     .def("__hash__", [](const smtk::string::Token& self)
       {
@@ -38,6 +50,10 @@ inline py::class_<smtk::string::Token> pybind11_init_smtk_string_Token(py::modul
       {
         return self == other;
       })
+    .def("__lt__", [](const smtk::string::Token& self, const smtk::string::Token& other)
+      {
+        return self < other;
+      })
     ;
   return instance;
 }
diff --git a/smtk/string/testing/CMakeLists.txt b/smtk/string/testing/CMakeLists.txt
index 2941d367a3cf5e1cbcdf5137f12b35096cd03df9..83305da3a2914ab6d4dec88c168fc7037bd7553e 100644
--- a/smtk/string/testing/CMakeLists.txt
+++ b/smtk/string/testing/CMakeLists.txt
@@ -1,5 +1,5 @@
 add_subdirectory(cxx)
 
-# if(SMTK_ENABLE_PYTHON_WRAPPING)
-#   add_subdirectory(python)
-# endif()
+if(SMTK_ENABLE_PYTHON_WRAPPING)
+  add_subdirectory(python)
+endif()
diff --git a/smtk/string/testing/python/CMakeLists.txt b/smtk/string/testing/python/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..0525061eb4dee28bc77d38990738a7f6f58cf308
--- /dev/null
+++ b/smtk/string/testing/python/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(smtkStringPythonTests
+  testStringToken
+)
+
+foreach (test ${smtkStringPythonTests})
+  smtk_add_test_python(${test}Py ${test}.py --src-dir=${smtk_SOURCE_DIR})
+endforeach()
diff --git a/smtk/string/testing/python/testStringToken.py b/smtk/string/testing/python/testStringToken.py
new file mode 100644
index 0000000000000000000000000000000000000000..05cb7bea7ad61eeaad2e083ff90ae7d7fddae896
--- /dev/null
+++ b/smtk/string/testing/python/testStringToken.py
@@ -0,0 +1,45 @@
+# =============================================================================
+#
+#  Copyright (c) Kitware, Inc.
+#  All rights reserved.
+#  See LICENSE.txt for details.
+#
+#  This software is distributed WITHOUT ANY WARRANTY; without even
+#  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+#  PURPOSE.  See the above copyright notice for more information.
+#
+# =============================================================================
+import os
+import sys
+import unittest
+import smtk
+import smtk.string
+import smtk.common
+from smtk import common
+import smtk.testing
+
+
+class TestStringToken(unittest.TestCase):
+
+    def test(self):
+        blank = smtk.string.Token()
+        print(blank)
+        self.assertFalse(blank.valid(), 'Default-constructed Token should be invalid.')
+        texts = ('a', 'b', 'c', 'aa')
+        allParsed = []
+        for txt in texts:
+            parsed = smtk.string.Token(txt)
+            print(parsed)
+            self.assertEqual(str(parsed), txt, 'Round-tripped Token was not identical.')
+            allParsed.append(parsed)
+        self.assertEqual(len(allParsed), len(texts), 'Expected to parse every Token.')
+        self.assertEqual(allParsed, [smtk.string.Token(x) for x in texts], \
+            'Same text in should produce identical Tokens out.')
+        s1 = set(allParsed)
+        s2 = set([smtk.string.Token(x) for x in texts])
+        print(s1)
+        self.assertEqual(s1, s2, 'Hashing should produce identical sets of Tokens.')
+
+if __name__ == '__main__':
+    smtk.testing.process_arguments()
+    unittest.main()
diff --git a/smtk/task/Manager.cxx b/smtk/task/Manager.cxx
index 8d973e38c5204e38fcb45db916a4d1c7f0bfbe65..8333f0746fc5d482ef86b8733dfa157fc393b4cc 100644
--- a/smtk/task/Manager.cxx
+++ b/smtk/task/Manager.cxx
@@ -9,6 +9,9 @@
 //=========================================================================
 
 #include "smtk/task/Manager.h"
+#include "smtk/task/ObjectsInRoles.h"
+
+#include "smtk/project/Project.h"
 
 #include "smtk/operation/Operation.h"
 
@@ -20,10 +23,124 @@
 
 #include "smtk/io/Logger.h"
 
+using namespace smtk::string::literals;
+
 namespace smtk
 {
 namespace task
 {
+namespace // anonymous
+{
+
+bool resourceMatch(
+  const std::string& filter,
+  smtk::resource::PersistentObject* object,
+  smtk::resource::Resource*& rsrc)
+{
+  if (auto* resource = dynamic_cast<smtk::resource::Resource*>(object))
+  {
+    rsrc = resource;
+    if (filter == "*" || rsrc->matchesType(filter))
+    {
+      return true;
+    }
+  }
+  else if (auto* comp = dynamic_cast<smtk::resource::Component*>(object))
+  {
+    if ((rsrc = comp->parentResource()))
+    {
+      if (filter == "*" || rsrc->matchesType(filter))
+      {
+        return true;
+      }
+    }
+  }
+  return false;
+}
+
+bool componentMatch(
+  const std::string& filter,
+  smtk::resource::PersistentObject* object,
+  smtk::resource::Resource* rsrc)
+{
+  // An empty filter string indicates only resources are allowed.
+  if (filter.empty())
+  {
+    return (object == rsrc);
+  }
+  // "*" allows any component:
+  else if (filter == "*")
+  {
+    // Assume that if object is not a pointer to the parent resource,
+    // it must be a component:
+    return (object != rsrc);
+  }
+  // We have a non-trivial filter string; we must have a component.
+  if (auto* comp = dynamic_cast<smtk::resource::Component*>(object))
+  {
+    auto query = rsrc->queryOperation(filter);
+    return query ? query(*comp) : false;
+  }
+  return false;
+}
+
+bool filterObject(const nlohmann::json::array_t& filter, smtk::resource::PersistentObject* object)
+{
+  for (const auto& filterTuple : filter)
+  {
+    try
+    {
+      if (filterTuple.is_array() && filterTuple.size() == 2)
+      {
+        smtk::resource::Resource* rsrc{ nullptr };
+        if (resourceMatch(filterTuple[0].get<std::string>(), object, rsrc))
+        {
+          auto compSpec = filterTuple[1].is_null() ? "" : filterTuple[1].get<std::string>();
+          if (componentMatch(compSpec, object, rsrc))
+          {
+            return true;
+          }
+        }
+      }
+    }
+    catch (nlohmann::json::exception& e)
+    {
+      smtkErrorMacro(
+        smtk::io::Logger::instance(),
+        "Cannot process filter \"" << filterTuple.dump() << "\";" << e.what());
+      continue;
+    }
+  }
+  return false;
+}
+
+nlohmann::json::array_t filtersForSpec(const smtk::view::Configuration::Component& spec)
+{
+  nlohmann::json::array_t filters;
+  for (const auto& filter : spec.children())
+  {
+    if (filter.name() != "Filter")
+    {
+      smtkWarningMacro(smtk::io::Logger::instance(), "Unhandled child \"" << filter.name() << "\".");
+      continue;
+    }
+    std::string rsrcMatch;
+    if (!filter.attribute("Resource", rsrcMatch))
+    {
+      smtkWarningMacro(smtk::io::Logger::instance(), "No resource in \"" << filter.name() << "\". Skipping.");
+      continue;
+    }
+    std::string compMatch;
+    filter.attribute("Component", compMatch);
+    nlohmann::json::array_t fspec{rsrcMatch};
+    //NOLINTNEXTLINE(modernize-use-emplace)
+    if (!compMatch.empty()) { fspec.push_back(compMatch); } else { fspec.push_back(std::nullptr_t()); }
+    filters.emplace_back(fspec);
+  }
+  return filters;
+}
+
+} // anonymous namespace
 
 constexpr const char* const Manager::type_name;
 
@@ -149,6 +266,232 @@ smtk::resource::Resource* Manager::resource() const
   return m_parent;
 }
 
+Manager::ResourceObjectMap Manager::workflowObjects(
+  const nlohmann::json& spec,
+  Task* task)
+{
+  // Filter objects by the \a spec.
+  ResourceObjectMap objectMap;
+
+  nlohmann::json source;
+  nlohmann::json filter;
+  if (spec.contains("source"))
+  {
+    source = spec.at("source");
+    if (!source.is_object() || !source.contains("type"))
+    {
+      smtkErrorMacro(
+        smtk::io::Logger::instance(),
+        "Spec source must be a dictionary with 'type' key, "
+        "got \""
+          << source.dump() << "\"");
+      return objectMap;
+    }
+  }
+  else
+  {
+    source = { { "type", "project resources" } };
+  }
+
+  if (spec.contains("filter"))
+  {
+    filter = spec.at("filter");
+    if (!filter.is_array())
+    {
+      smtkErrorMacro(
+        smtk::io::Logger::instance(),
+        "Spec filter must be an array, got \"" << filter.dump() << "\".");
+      return objectMap;
+    }
+  }
+  else
+  {
+    // Accept any resource or component:
+    filter = nlohmann::json::array_t({ { "*", "*" }, { "*", nullptr } });
+  }
+
+  auto sourceType = source["type"].get<smtk::string::Token>();
+  std::unordered_set<smtk::resource::PersistentObject*> objects;
+  switch (sourceType.id())
+  {
+    default:
+      smtkErrorMacro(
+        smtk::io::Logger::instance(), "Unknown source type \"" << sourceType.data() << "\".");
+      // fall through
+    case "project resources"_hash:
+    {
+      auto* project = dynamic_cast<smtk::project::Project*>(this->resource());
+      if (!project)
+      {
+        return objectMap;
+      }
+
+      for (const auto& resource : project->resources())
+      {
+        if (filterObject(filter, resource.get()))
+        {
+          objects.insert(resource.get());
+        }
+      }
+    }
+    break;
+    case "active task port"_hash:
+    {
+      auto* currentTask = task ? task : this->active().task();
+      if (!currentTask || !source.contains("port"))
+      {
+        smtkErrorMacro(smtk::io::Logger::instance(), "No active task or no \"port\" specified.");
+        return objectMap;
+      }
+
+      // We are asked to find objects on a port of the (now) active task.
+      // Determine whether we are filtering on role or not.
+      smtk::string::Token sourceRole;
+      if (source.contains("role"))
+      {
+        sourceRole = source.at("role").get<smtk::string::Token>();
+      }
+      // Find the correct port:
+      const auto& taskPortMap = currentTask->ports();
+      auto it = taskPortMap.find(source["port"]);
+      if (it == taskPortMap.end())
+      {
+        return objectMap;
+      }
+      // Fetch data from the port. We only understand ObjectsInRoles at this point:
+      auto portData = it->second->parent()->portData(it->second);
+      if (portData)
+      {
+        if (auto objectsInRoles = std::dynamic_pointer_cast<smtk::task::ObjectsInRoles>(portData))
+        {
+          for (const auto& entry : objectsInRoles->data())
+          {
+            if (sourceRole.valid() && entry.first != sourceRole)
+            { // Skip objects in unrequested roles.
+              continue;
+            }
+            // Filter objects as requested.
+            for (const auto& object : entry.second)
+            {
+              if (filterObject(filter, object))
+              {
+                objects.insert(object);
+              }
+            }
+          }
+        }
+        else
+        {
+          smtkErrorMacro(
+            smtk::io::Logger::instance(),
+            "Unhandled port data type \"" << portData->typeName() << "\".");
+        }
+      }
+    }
+    break;
+  }
+
+  for (const auto& object : objects)
+  {
+    smtk::resource::Resource* resource =dynamic_cast<smtk::resource::Resource*>(object);
+    if (auto* comp = dynamic_cast<smtk::resource::Component*>(object))
+    {
+      resource = comp->parentResource();
+    }
+    if (!resource)
+    {
+      continue;
+    }
+    objectMap[resource].insert(object == resource ? nullptr : object);
+  }
+
+  return objectMap;
+}
+
+Manager::ResourceObjectMap Manager::workflowObjects(
+  const smtk::view::Configuration::Component& spec, Task* task)
+{
+  // To avoid dueling implementations, we'll convert \a spec into JSON and pass
+  // it to the variant above.
+  nlohmann::json jsonSpec;
+  nlohmann::json::array_t controls;
+  for (const auto& entry : spec.children())
+  {
+    smtk::string::Token tagName = entry.name();
+    switch (tagName.id())
+    {
+    case "ActiveTaskPort"_hash:
+      {
+        auto portName = entry.attributeAsString("Port");
+        auto roleName = entry.attributeAsString("Role");
+        if (portName.empty())
+        {
+          smtkErrorMacro(smtk::io::Logger::instance(), "Missing a port name.");
+          continue;
+        }
+        nlohmann::json sourceSpec{{ "type", "active task port" }, {"port", portName}};
+        if (!roleName.empty())
+        {
+          sourceSpec["role"] = roleName;
+        }
+        auto filters = filtersForSpec(entry);
+        if (!filters.empty())
+        {
+          jsonSpec["filter"] = filters;
+        }
+        jsonSpec["source"] = sourceSpec;
+      }
+      break;
+    case "ProjectResources"_hash:
+      {
+        nlohmann::json sourceSpec{{ "type", "project resources" }};
+        auto filters = filtersForSpec(entry);
+        if (!filters.empty())
+        {
+          jsonSpec["filter"] = filters;
+        }
+        jsonSpec["source"] = sourceSpec;
+      }
+      break;
+    case "Control"_hash:
+      {
+        auto controlType = entry.attributeAsString("Type");
+        if (controlType.empty())
+        {
+          smtkErrorMacro(smtk::io::Logger::instance(),
+            "Control tag must provide a Type attribute.");
+          continue;
+        }
+        controls.emplace_back(controlType);
+      }
+      break;
+    default:
+      {
+        smtkWarningMacro(smtk::io::Logger::instance(),
+          "Unhandled specification tag <" << tagName.data() << ">. Skipping.");
+      }
+    }
+  }
+  if (!controls.empty())
+  {
+    jsonSpec["controls"] = controls;
+  }
+  auto objMap = this->workflowObjects(jsonSpec, task);
+  return objMap;
+}
+
+bool Manager::isResourceRelevant(
+  const std::shared_ptr<smtk::resource::Resource>& resource,
+  const nlohmann::json& filter)
+{
+  return filterObject(filter, resource.get());
+}
+
+bool Manager::isResourceRelevant(smtk::resource::Resource* resource, const nlohmann::json& filter)
+{
+  return filterObject(filter, resource);
+}
+
 int Manager::handleOperation(
   const smtk::operation::Operation& op,
   smtk::operation::EventType event,
diff --git a/smtk/task/Manager.h b/smtk/task/Manager.h
index 9dd682795e2cd0fc54b445f5e1fd8ed3a8b1a607..33fdb9a0ce6fc41b95c498436d2a177d4bef01c8 100644
--- a/smtk/task/Manager.h
+++ b/smtk/task/Manager.h
@@ -30,6 +30,8 @@
 #include "smtk/task/Task.h"
 #include "smtk/task/adaptor/Instances.h"
 
+#include "smtk/view/Configuration.h"
+
 #include "nlohmann/json.hpp"
 
 #include <array>
@@ -82,6 +84,13 @@ public:
   Manager(const Manager&) = delete;
   void operator=(const Manager&) = delete;
 
+  /// A map of objects grouped by their parent resource.
+  ///
+  /// If a resource itself is intended for selection, one
+  /// entry of its target set of objects will be the null pointer.
+  using ResourceObjectMap =
+    std::map<smtk::resource::Resource*, std::unordered_set<smtk::resource::PersistentObject*>>;
+
   /// Managed instances of Task objects (and a registry of Task classes).
   using TaskInstances = smtk::task::Instances;
 
@@ -132,6 +141,33 @@ public:
   /// If this manager is owned by a resource (typically a project), return it.
   smtk::resource::Resource* resource() const;
 
+  /// Given a filter \a spec, return a set of objects grouped by their parent resources.
+  ///
+  /// The objects are drawn – according to the \a spec – from either ports of the active
+  /// task or resources owned by the parent of this task manager (assuming it is a project).
+  /// If a resource itself is intended to be part of the result, one of the object pointers
+  /// in that resource's target set will be the null pointer.
+  ///
+  /// The variant that accepts a JSON \a spec is for use inside task-manager style.
+  /// The variant that accepts a Configuration::Component \a spec is for use inside
+  /// view-configuration XML.
+  /// The two forms of \a spec are similar but not identical in order to improve
+  /// readability and reduce maintenance overhead.
+  ResourceObjectMap workflowObjects(const nlohmann::json& spec, Task* task = nullptr);
+  ResourceObjectMap workflowObjects(
+    const smtk::view::Configuration::Component& spec, Task* task = nullptr);
+
+  /// Return true if the \a resource matches the filter \a spec provided.
+  bool isResourceRelevant(
+    const std::shared_ptr<smtk::resource::Resource>& resource, const nlohmann::json& spec);
+  bool isResourceRelevant(
+    smtk::resource::Resource* resource, const nlohmann::json& spec);
+  bool isResourceRelevant(
+    const std::shared_ptr<smtk::resource::Resource>& resource,
+    const smtk::view::Configuration::Component& spec);
+  bool isResourceRelevant(
+    smtk::resource::Resource* resource, const smtk::view::Configuration::Component& spec);
+
   /// Return a gallery of Task Worklets
   Gallery& gallery() { return m_gallery; }
   const Gallery& gallery() const { return m_gallery; }
diff --git a/smtk/task/TrivialProducerAgent.cxx b/smtk/task/TrivialProducerAgent.cxx
index 8cba737887eeeecdce191b22186914f648ee5ab1..a242df90cce0e7c854bec6ae25d499e4e53d35aa 100644
--- a/smtk/task/TrivialProducerAgent.cxx
+++ b/smtk/task/TrivialProducerAgent.cxx
@@ -28,7 +28,7 @@ TrivialProducerAgent::TrivialProducerAgent(Task* owningTask)
 
 State TrivialProducerAgent::state() const
 {
-  return State::Completable;
+  return m_internalState;
 }
 
 void TrivialProducerAgent::configure(const Configuration& config)
@@ -54,8 +54,8 @@ void TrivialProducerAgent::configure(const Configuration& config)
         for (const auto& objectSpec : entry.value())
         {
           auto* obj = helper.objectFromJSONSpec(objectSpec, "port");
-          std::cout << "Port \"" << m_name.data() << "\" add obj " << obj << " role " << entry.key()
-                    << "\n";
+          // std::cout << "Port \"" << m_name.data() << "\" add obj " << obj << " role "
+          //           << entry.key() << "\n";
           if (obj)
           {
             addedData |= m_data->addObject(obj, role);
@@ -65,6 +65,29 @@ void TrivialProducerAgent::configure(const Configuration& config)
     }
 #endif
   }
+  it = config.find("required-counts");
+  if (it != config.end())
+  {
+    m_requiredObjectCounts = it->get<std::map<smtk::string::Token, std::pair<int, int>>>();
+  }
+  else
+  {
+    m_requiredObjectCounts.clear();
+  }
+  it = config.find("internal-state");
+  if (it != config.end())
+  {
+    bool valid = false;
+    m_internalState = stateEnum(it->get<std::string>(), &valid);
+    if (!valid)
+    {
+      m_internalState = State::Completable;
+    }
+  }
+  else
+  {
+    m_internalState = State::Completable;
+  }
   it = config.find("output-port");
   if (it != config.end())
   {
@@ -83,6 +106,7 @@ void TrivialProducerAgent::configure(const Configuration& config)
 TrivialProducerAgent::Configuration TrivialProducerAgent::configuration() const
 {
   auto config = this->Superclass::configuration();
+  config["internal-state"] = stateName(m_internalState);
   if (m_outputPort)
   {
     config["output-port"] = m_outputPort->name();
@@ -95,9 +119,27 @@ TrivialProducerAgent::Configuration TrivialProducerAgent::configuration() const
   {
     config["data"] = m_data;
   }
+  if (!m_requiredObjectCounts.empty())
+  {
+    config["required-counts"] = m_requiredObjectCounts;
+  }
   return config;
 }
 
+std::string TrivialProducerAgent::troubleshoot() const
+{
+  std::ostringstream msg;
+  switch (m_internalState)
+  {
+  default:
+    break;
+  case smtk::task::State::Incomplete:
+    msg << R"html(<li>Missing objects in roles.</li>)html";
+    break;
+  }
+  return msg.str();
+}
+
 std::shared_ptr<PortData> TrivialProducerAgent::portData(const Port* port) const
 {
   if (port == m_outputPort)
@@ -123,7 +165,13 @@ bool TrivialProducerAgent::addObjectInRole(
     {
       if (trivialProducer->name() == agentName)
       {
-        return trivialProducer->m_data->addObject(object, role);
+        State prev = trivialProducer->state();
+        bool didAdd = trivialProducer->m_data->addObject(object, role);
+        if (didAdd)
+        {
+          trivialProducer->parent()->updateAgentState(trivialProducer, prev, trivialProducer->computeInternalState());
+        }
+        return didAdd;
       }
     }
   }
@@ -146,7 +194,13 @@ bool TrivialProducerAgent::addObjectInRole(
     {
       if (trivialProducer->outputPort() == port)
       {
-        return trivialProducer->m_data->addObject(object, role);
+        State prev = trivialProducer->state();
+        bool didAdd = trivialProducer->m_data->addObject(object, role);
+        if (didAdd)
+        {
+          trivialProducer->parent()->updateAgentState(trivialProducer, prev, trivialProducer->computeInternalState());
+        }
+        return didAdd;
       }
     }
   }
@@ -170,8 +224,10 @@ bool TrivialProducerAgent::removeObjectFromRole(
     {
       if (trivialProducer->name() == agentName)
       {
+        State prev = trivialProducer->state();
         if (trivialProducer->m_data->removeObject(object, role))
         {
+          trivialProducer->parent()->updateAgentState(trivialProducer, prev, trivialProducer->computeInternalState());
           return true;
         }
       }
@@ -197,8 +253,10 @@ bool TrivialProducerAgent::removeObjectFromRole(
     {
       if (trivialProducer->outputPort() == port)
       {
+        State prev = trivialProducer->state();
         if (trivialProducer->m_data->removeObject(object, role))
         {
+          trivialProducer->parent()->updateAgentState(trivialProducer, prev, trivialProducer->computeInternalState());
           return true;
         }
       }
@@ -220,7 +278,12 @@ bool TrivialProducerAgent::resetData(Task* task, const std::string& agentName)
     {
       if (trivialProducer->name() == agentName)
       {
+        State prev = trivialProducer->state();
         didChange |= trivialProducer->m_data->clear();
+        if (didChange)
+        {
+          trivialProducer->parent()->updateAgentState(trivialProducer, prev, trivialProducer->computeInternalState());
+        }
       }
     }
   }
@@ -238,11 +301,50 @@ bool TrivialProducerAgent::resetData(Task* task, Port* port)
   {
     if (auto* trivialProducer = dynamic_cast<TrivialProducerAgent*>(agent))
     {
+      State prev = trivialProducer->state();
       didChange |= trivialProducer->m_data->clear();
+      if (didChange)
+      {
+        trivialProducer->parent()->updateAgentState(trivialProducer, prev, trivialProducer->computeInternalState());
+      }
     }
   }
   return didChange;
 }
 
+State TrivialProducerAgent::computeInternalState()
+{
+  State result = State::Completable;
+  for (const auto& entry : m_requiredObjectCounts)
+  {
+    auto it = m_data->data().find(entry.first);
+    if (it == m_data->data().end())
+    {
+      result = State::Incomplete;
+      break;
+    }
+    auto numObj = static_cast<int>(it->second.size());
+    if (numObj >= entry.second.first && numObj <= entry.second.second)
+    {
+      // We are in range, skip following checks.
+      continue;
+    }
+    if (entry.second.second < 0 && numObj >= entry.second.first)
+    {
+      // We are allowed to have any number as long as we exceed the minimum.
+      continue;
+    }
+    if (entry.second.first < 0 && entry.second.second < 0 && numObj == 0)
+    {
+      // We are required to have 0 objects in this role.
+      continue;
+    }
+    result = State::Incomplete;
+    break;
+  }
+  m_internalState = result;
+  return result;
+}
+
 } // namespace task
 } // namespace smtk
diff --git a/smtk/task/TrivialProducerAgent.h b/smtk/task/TrivialProducerAgent.h
index 53677880219b6daadd78854116b91bb34bb99550..776659a10396a17e0b8db9cac2ed1296512a1570 100644
--- a/smtk/task/TrivialProducerAgent.h
+++ b/smtk/task/TrivialProducerAgent.h
@@ -25,6 +25,9 @@ class ObjectsInRoles;
 /// assign objects to a task. The downstream or child tasks of this
 /// agent's task will then be configured with the objects in the roles
 /// as configured.
+///
+/// Unless the task's configuration includes a minimum/maximum count
+/// of objects per role, the task will always be completable.
 class SMTKCORE_EXPORT TrivialProducerAgent : public Agent
 {
 public:
@@ -38,7 +41,10 @@ public:
 
   ///\brief Return the current state of the agent.
   ///
-  /// This agent will always be completable, even if no resources are assigned.
+  /// By default, this agent will always be completable, even if no resources are assigned.
+  /// However, if the agent's configuration contains minimum/maximum counts
+  /// for objects by role, the state will only be completable when the number of objects
+  /// in each specified role is in the allowed range.
   State state() const override;
 
   ///\brief Configure the agent based on a provided JSON configuration.
@@ -47,6 +53,9 @@ public:
   ///\brief Produce a JSON configuration object for the current task state.
   Configuration configuration() const override;
 
+  ///\brief Provide feedback to users on how to make this agent completable.
+  std::string troubleshoot() const override;
+
   ///\brief Return the port data from the agent.
   std::shared_ptr<PortData> portData(const Port* port) const override;
 
@@ -94,7 +103,11 @@ public:
   static bool resetData(Task* task, Port* port);
 
 protected:
+  virtual State computeInternalState();
+
+  State m_internalState{ State::Completable };
   std::shared_ptr<ObjectsInRoles> m_data;
+  std::map<smtk::string::Token, std::pair<int, int>> m_requiredObjectCounts;
   Port* m_outputPort{ nullptr };
 };
 
diff --git a/smtk/task/pybind11/PybindTask.cxx b/smtk/task/pybind11/PybindTask.cxx
index 0abf159f540cfc9a8e3672a674181946763bb8b1..3e0944bf7590c88d91c3c2935e8f494668c5fe94 100644
--- a/smtk/task/pybind11/PybindTask.cxx
+++ b/smtk/task/pybind11/PybindTask.cxx
@@ -35,6 +35,7 @@ using namespace nlohmann;
 #include "PybindState.h"
 #include "PybindSubmitOperationAgent.h"
 #include "PybindTask.h"
+#include "PybindTrivialProducerAgent.h"
 #include "PybindWorklet.h"
 #include "PybindInstances.h"
 
@@ -58,6 +59,7 @@ PYBIND11_MODULE(_smtkPybindTask, m)
   auto smtk_task_FillOutAttributesAgent = pybind11_init_smtk_task_FillOutAttributesAgent(m);
   auto smtk_task_SubmitOperationAgent = pybind11_init_smtk_task_SubmitOperationAgent(m);
   auto smtk_task_GatherObjectsAgent = pybind11_init_smtk_task_GatherObjectsAgent(m);
+  auto smtk_task_TrivialProducerAgent = pybind11_init_smtk_task_TrivialProducerAgent(m);
   auto smtk_task_Task = pybind11_init_smtk_task_Task(m);
   pybind11_init_smtk_task_State(m);
   pybind11_init_smtk_task_stateEnum(m);
diff --git a/smtk/task/pybind11/PybindTask.h b/smtk/task/pybind11/PybindTask.h
index 303a2fac91be4e7375652576aa9a34fca6e72f84..5330f3cea182ed592f021c232fc434d8c8876778 100644
--- a/smtk/task/pybind11/PybindTask.h
+++ b/smtk/task/pybind11/PybindTask.h
@@ -34,6 +34,8 @@ inline PySharedPtrClass< smtk::task::Task, smtk::resource::Component > pybind11_
     .def("setName", &smtk::task::Task::setName, py::arg("name"))
     .def("title", &smtk::task::Task::name)
     .def("setTitle", &smtk::task::Task::setName, py::arg("title"))
+    .def("ports", &smtk::task::Task::ports)
+    .def("portData", &smtk::task::Task::portData, py::arg("port"))
     .def("state", &smtk::task::Task::state)
     .def("agents", &smtk::task::Task::agents, py::return_value_policy::reference_internal)
     .def("children", &smtk::task::Task::children, py::return_value_policy::reference_internal)
diff --git a/smtk/task/pybind11/PybindTrivialProducerAgent.h b/smtk/task/pybind11/PybindTrivialProducerAgent.h
new file mode 100644
index 0000000000000000000000000000000000000000..0ec4527f82d5caa9d6ec498a53579da5cc182d4e
--- /dev/null
+++ b/smtk/task/pybind11/PybindTrivialProducerAgent.h
@@ -0,0 +1,79 @@
+//=========================================================================
+//  Copyright (c) Kitware, Inc.
+//  All rights reserved.
+//  See LICENSE.txt for details.
+//
+//  This software is distributed WITHOUT ANY WARRANTY; without even
+//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+//  PURPOSE.  See the above copyright notice for more information.
+//=========================================================================
+
+#ifndef pybind_smtk_task_TrivialProducerAgent_h
+#define pybind_smtk_task_TrivialProducerAgent_h
+
+#include <pybind11/pybind11.h>
+
+#include "smtk/task/TrivialProducerAgent.h"
+
+#include "smtk/task/Port.h"
+
+namespace py = pybind11;
+
+inline py::class_< smtk::task::TrivialProducerAgent > pybind11_init_smtk_task_TrivialProducerAgent(py::module &m)
+{
+  py::class_< smtk::task::TrivialProducerAgent, smtk::task::Agent > instance(m, "TrivialProducerAgent");
+  instance
+    .def_static("addObjectInRole", [](
+        smtk::task::Task* task, const std::string& agentName, const std::string& role, const smtk::resource::PersistentObject::Ptr& object)
+      {
+        return smtk::task::TrivialProducerAgent::addObjectInRole(task, agentName, role, object.get());
+      },
+      py::arg("task"), py::arg("agentName"), py::arg("role"), py::arg("object"))
+    .def_static("addObjectInRole", [](
+        smtk::task::Task* task, smtk::task::Port* port, const std::string& role, const smtk::resource::PersistentObject::Ptr& object)
+      {
+        return smtk::task::TrivialProducerAgent::addObjectInRole(task, port, role, object.get());
+      },
+      py::arg("task"), py::arg("port"), py::arg("role"), py::arg("object"))
+    .def_static("removeObjectFromRole", [](
+        smtk::task::Task* task, const std::string& agentName, const std::string& role, const smtk::resource::PersistentObject::Ptr& object)
+      {
+        return smtk::task::TrivialProducerAgent::removeObjectFromRole(task, agentName, role, object.get());
+      },
+      py::arg("task"), py::arg("agentName"), py::arg("role"), py::arg("object"))
+    .def_static("removeObjectFromRole", [](
+        smtk::task::Task* task, smtk::task::Port* port, const std::string& role, const smtk::resource::PersistentObject::Ptr& object)
+      {
+        return smtk::task::TrivialProducerAgent::removeObjectFromRole(task, port, role, object.get());
+      },
+      py::arg("task"), py::arg("port"), py::arg("role"), py::arg("object"))
+    .def_static("resetData", [](smtk::task::Task* task, const std::string& agentName)
+      {
+        return smtk::task::TrivialProducerAgent::resetData(task, agentName);
+      }, py::arg("task"), py::arg("agentName"))
+    .def_static("resetData", [](smtk::task::Task* task, smtk::task::Port* port)
+      {
+        return smtk::task::TrivialProducerAgent::resetData(task, port);
+      }, py::arg("task"), py::arg("port"))
+    .def("typeToken", &smtk::task::TrivialProducerAgent::typeToken)
+    .def("classHierarchy", &smtk::task::TrivialProducerAgent::classHierarchy)
+    .def("matchesType", &smtk::task::TrivialProducerAgent::matchesType, py::arg("candidate"))
+    .def("generationsFromBase", &smtk::task::TrivialProducerAgent::generationsFromBase, py::arg("base"))
+    .def("state", &smtk::task::TrivialProducerAgent::state)
+    .def("configure", [](smtk::task::TrivialProducerAgent& self, const std::string& jsonConfig)
+      {
+        auto config = nlohmann::json::parse(jsonConfig);
+        self.configure(config);
+      })
+    .def("configuration", &smtk::task::TrivialProducerAgent::configuration)
+    .def("name", &smtk::task::TrivialProducerAgent::name)
+    .def("portData", [](smtk::task::TrivialProducerAgent& self, smtk::task::Port::Ptr port)
+      {
+        return self.portData(port.get());
+      }, py::arg("port"))
+    .def("parent", &smtk::task::TrivialProducerAgent::parent, py::return_value_policy::reference_internal)
+    ;
+  return instance;
+}
+
+#endif
diff --git a/smtk/view/Configuration.h b/smtk/view/Configuration.h
index 3eaac22f721251dc815ab3a45092ed83b81a04bf..fa9b94370a587e3a978326fd66bfc9231893642a 100644
--- a/smtk/view/Configuration.h
+++ b/smtk/view/Configuration.h
@@ -63,7 +63,7 @@ public:
     /// is t or true, false if attribute is f or false and not set otherwise
     /// set value to the attribute's values.  Else it returns false
     bool attributeAsBool(const std::string& attname, bool& value) const;
-    /// Returns true if the component has an attribute called name and if it's value is
+    /// Returns true if the component has an attribute called name and if its value is
     /// either t or true (ignoring case).  Else it returns false.
     bool attributeAsBool(const std::string& attname) const;