diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py index 36f440e80d..d0fbd59431 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -1003,15 +1003,12 @@ def matchText(self, text: str) -> bool: isDefault = Property(bool, lambda self: all(v.isDefault for v in self.value), notify=Attribute.valueChanged) -class ShapeAttribute(GroupAttribute): +class GeometryAttribute(GroupAttribute): """ - GroupAttribute subtype tailored for shape-specific handling. + GroupAttribute subtype tailored for geometry-specific handling. """ - def __init__(self, node, attributeDesc: desc.Shape, isOutput: bool, - root=None, parent=None): - self._visible = True - self._color = "#2A82DA" # default shape color + def __init__(self, node, attributeDesc: desc.Geometry, isOutput: bool, root=None, parent=None): super().__init__(node, attributeDesc, isOutput, root, parent) # Override @@ -1038,20 +1035,20 @@ def upgradeValue(self, exportedValue): def getSerializedValue(self): if self.isLink: return self._getInputLink().asLinkExpr() - return {key: attr.getSerializedValue() for key, attr in self._value.objects.items()} + return super().getSerializedValue() def getValueAsDict(self) -> dict: """ - Return the shape attribute value as dict. - For not keyable shape, this is the same as getSerializedValue(). - For keyable shape, the dict is indexed by key. + Return the geometry attribute value as dict. + For not keyable geometry, this is the same as getSerializedValue(). + For keyable geometry, the dict is indexed by key. """ from collections import defaultdict outValue = defaultdict(dict) - if not self.shapeKeyable: + if not self.observationKeyable: return super().getSerializedValue() for attribute in self.value: - if isinstance(attribute, ShapeAttribute): + if isinstance(attribute, GeometryAttribute): attributeDict = attribute.getValueAsDict() if attributeDict: for key, value in attributeDict.items(): @@ -1061,115 +1058,57 @@ def getValueAsDict(self) -> dict: outValue[str(pair.key)][attribute.name] = pair.value return dict(outValue) - def getShapeAsDict(self) -> dict: - """ - Return the shape attribute as dict with the shape file structure. - """ - outDict = { - "name" : self.rootName, - "type" : self.type, - "properties" : { "color": self._color } - } - - if not self.shapeKeyable: - # Not keyable shape, use properties. - outDict.get("properties").update(super().getSerializedValue()) - else: - # Keyable shape, use observations. - from collections import defaultdict - outObservations = defaultdict(dict) - for attribute in self.value: - if isinstance(attribute, ShapeAttribute): - attributeDict = attribute.getValueAsDict() - if attributeDict: - for key, value in attributeDict.items(): - outObservations[key][attribute.name] = value - else: - for pair in attribute.keyValues.pairs: - outObservations[str(pair.key)][attribute.name] = pair.value - outDict.update({ "observations" : dict(outObservations)}) - return outDict - - def _getVisible(self) -> bool: - """ - Return whether the shape attribute is visible for display. - """ - return self._visible - - def _setVisible(self, visible:bool): - """ - Set the shape attribute visibility for display. - """ - self._visible = visible - self.shapeChanged.emit() - - def _getColor(self) -> str: - """ - Return the shape attribute color for display. - """ - if self.isLink: - return self.inputLink.shapeColor - return self._color - - @raiseIfLink - def _setColor(self, color: str): - """ - Set the shape attribute color for display. - """ - self._color = color - self.shapeChanged.emit() - def _hasKeyableChilds(self) -> bool: """ Whether all child attributes are keyable. """ - return all((isinstance(attribute, ShapeAttribute) and attribute.shapeKeyable) or + return all((isinstance(attribute, GeometryAttribute) and attribute.observationKeyable) or attribute.keyable for attribute in self.value) def _getNbObservations(self) -> int: """ - Return the shape attribute number of observations. + Return the geometry attribute number of observations. Note: Observation is a value defined across all child attributes for a specific key. """ - if self.shapeKeyable: + if self.observationKeyable: firstAttribute = next(iter(self.value.values())) - if isinstance(firstAttribute, ShapeAttribute): + if isinstance(firstAttribute, GeometryAttribute): return firstAttribute.nbObservations return len(firstAttribute.keyValues.pairs) return 1 def _getObservationKeys(self) -> list: """ - Return the shape attribute list of observation keys. + Return the geometry attribute list of observation keys. Note: Observation is a value defined across all child attributes for a specific key. """ - if not self.shapeKeyable: + if not self.observationKeyable: return [] firstAttribute = next(iter(self.value.values())) - if isinstance(firstAttribute, ShapeAttribute): + if isinstance(firstAttribute, GeometryAttribute): return firstAttribute.observationKeys return firstAttribute.keyValues.getKeys() @Slot(str, result=bool) def hasObservation(self, key: str) -> bool: """ - Whether the shape attribute has an observation for the given key. + Whether the geometry attribute has an observation for the given key. Note: Observation is a value defined across all child attributes for a specific key. """ - if not self.shapeKeyable: + if not self.observationKeyable: return True - return all((isinstance(attribute, ShapeAttribute) and attribute.hasObservation(key)) or - (not isinstance(attribute, ShapeAttribute) and attribute.keyValues.hasKey(key)) + return all((isinstance(attribute, GeometryAttribute) and attribute.hasObservation(key)) or + (not isinstance(attribute, GeometryAttribute) and attribute.keyValues.hasKey(key)) for attribute in self.value) @raiseIfLink def removeObservation(self, key: str): """ - Remove the shape attribute observation for the given key. + Remove the geometry attribute observation for the given key. Note: Observation is a value defined across all child attributes for a specific key. """ for attribute in self.value: - if isinstance(attribute, ShapeAttribute): + if isinstance(attribute, GeometryAttribute): attribute.removeObservation(key) else: if attribute.keyable: @@ -1181,15 +1120,15 @@ def removeObservation(self, key: str): @raiseIfLink def setObservation(self, key: str, observation: Variant): """ - Set the shape attribute observation for the given key with the given observation. + Set the geometry attribute observation for the given key with the given observation. Note: Observation is a value defined across all child attributes for a specific key. """ for attributeStr, value in observation.items(): attribute = self.childAttribute(attributeStr) if attribute is None: - raise RuntimeError(f"Cannot set shape observation for attribute {self._getFullName()} \ + raise RuntimeError(f"Cannot set geometry observation for attribute {self._getFullName()} \ observation is incorrect.") - if isinstance(attribute, ShapeAttribute): + if isinstance(attribute, GeometryAttribute): attribute.setObservation(key, value) else: if attribute.keyable: @@ -1201,17 +1140,17 @@ def setObservation(self, key: str, observation: Variant): @Slot(str, result=Variant) def getObservation(self, key: str) -> Variant: """ - Return the shape attribute observation for the given key. + Return the geometry attribute observation for the given key. Note: Observation is a value defined across all child attributes for a specific key. """ observation = {} for attribute in self.value: - if isinstance(attribute, ShapeAttribute): - shapeObservation = attribute.getObservation(key) - if shapeObservation is None: + if isinstance(attribute, GeometryAttribute): + geoObservation = attribute.getObservation(key) + if geoObservation is None: return None else : - observation[attribute.name] = shapeObservation + observation[attribute.name] = geoObservation else: if attribute.keyable: if attribute.keyValues.hasKey(key): @@ -1222,43 +1161,135 @@ def getObservation(self, key: str) -> Variant: observation[attribute.name] = attribute.value return observation + # Properties and signals + # Emitted when a geometry observation changed. + observationsChanged = Signal() + # Whether the geometry attribute childs are keyable. + observationKeyable = Property(bool,_hasKeyableChilds, constant=True) + # The list of geometry observation keys. + observationKeys = Property(Variant, _getObservationKeys, notify=observationsChanged) + # The number of geometry observation defined. + nbObservations = Property(int, _getNbObservations, notify=observationsChanged) + + + +class ShapeAttribute(GroupAttribute): + """ + GroupAttribute subtype tailored for shape-specific handling. + """ + + def __init__(self, node, attributeDesc: desc.Shape, isOutput: bool, root=None, parent=None): + super().__init__(node, attributeDesc, isOutput, root, parent) + self._visible = True + + # Override + # Connect geometry attribute valueChanged to emit geometryChanged signal. + def _initValue(self): + super()._initValue() + # Using Attribute.valueChanged for the userName, userColor, geometry properties results in a segmentation fault. + # As a workaround, we manually connect valueChanged to shapeChanged or geometryChanged. + self.value.get("userName").valueChanged.connect(self._onShapeChanged) + self.value.get("userColor").valueChanged.connect(self._onShapeChanged) + self.geometry.valueChanged.connect(self._onGeometryChanged) + + # Override + # Fix missing link expression serialization. + # Should be remove if link expression serialization is added in GroupAttribute. + def getSerializedValue(self): + if self.isLink: + return self._getInputLink().asLinkExpr() + return super().getSerializedValue() + + def getShapeAsDict(self) -> dict: + """ + Return the shape attribute as dict with the shape file structure. + """ + outDict = { + "name" : self.userName if self.userName else self.rootName, + "type" : self.type, + "properties" : { "color": self.userColor } + } + if not self.geometry.observationKeyable: + # Not keyable geometry, use properties. + outDict.get("properties").update(self.geometry.getSerializedValue()) + else: + # Keyable geometry, use observations. + outDict.update({ "observations" : self.geometry.getValueAsDict()}) + return outDict + + def _getVisible(self) -> bool: + """ + Return whether the shape attribute is visible for display. + """ + return self._visible + + def _setVisible(self, visible:bool): + """ + Set the shape attribute visibility for display. + """ + self._visible = visible + self.shapeChanged.emit() + + def _getUserName(self) -> str: + """ + Return the shape attribute user name for display. + """ + return self.value.get("userName").value + + def _getUserColor(self) -> str: + """ + Return the shape attribute user color for display. + """ + return self.value.get("userColor").value + + @Slot() + def _onShapeChanged(self): + """ + Emit shapeChanged signal. + Used when shape userName or userColor value changed. + """ + self.shapeChanged.emit() + + @Slot() + def _onGeometryChanged(self): + """ + Emit geometryChanged signal. + Used when geometry attribute value changed. + """ + self.geometryChanged.emit() + # Properties and signals # Emitted when a shape related property changed (color, visibility). shapeChanged = Signal() # Emitted when a shape observation changed. - observationsChanged = Signal() + geometryChanged = Signal() # Whether the shape is displayable. isVisible = Property(bool, _getVisible, _setVisible, notify=shapeChanged) - # The shape color for display. - shapeColor = Property(str, _getColor, _setColor, notify=shapeChanged) - # The shape list of observation keys. - observationKeys = Property(Variant, _getObservationKeys, notify=observationsChanged) - # The number of observation defined. - nbObservations = Property(int, _getNbObservations, notify=observationsChanged) - # Whether the shape attribute childs are keyable. - shapeKeyable = Property(bool,_hasKeyableChilds, constant=True) + # The shape user name for display. + userName = Property(str, _getUserName, notify=shapeChanged) + # The shape user color for display. + userColor = Property(str, _getUserColor, notify=shapeChanged) + # The shape geometry group attribute. + geometry = Property(Variant, lambda self: self.value.get("geometry"), notify=geometryChanged) # Override hasDisplayableShape property. hasDisplayableShape = Property(bool, lambda self: True, constant=True) - # Override value property. - value = Property(Variant, Attribute._getValue, _setValue, notify=Attribute.valueChanged) class ShapeListAttribute(ListAttribute): """ ListAttribute subtype tailored for shape-specific handling. """ - def __init__(self, node, attributeDesc: desc.ShapeList, isOutput: bool, - root=None, parent=None): - self._visible = True + def __init__(self, node, attributeDesc: desc.ShapeList, isOutput: bool, root=None, parent=None): super().__init__(node, attributeDesc, isOutput, root, parent) + self._visible = True - def getValuesAsDicts(self): + def getGeometriesAsDict(self): """ - Return the values of the children of the shape list attribute. + Return the geometries values of the children of the shape list attribute. """ - return [shapeAttribute.getValueAsDict() for shapeAttribute in self.value] + return [shapeAttribute.geometry.getValueAsDict() for shapeAttribute in self.value] - def getShapesAsDicts(self): + def getShapesAsDict(self): """ Return the children of the shape list attribute. """ diff --git a/meshroom/core/desc/__init__.py b/meshroom/core/desc/__init__.py index 466d8daa77..b3614cbfa1 100644 --- a/meshroom/core/desc/__init__.py +++ b/meshroom/core/desc/__init__.py @@ -11,10 +11,14 @@ PushButtonParam, StringParam, ) +from .geometryAttribute import ( + Geometry, + Size2d, + Vec2d, +) from .shapeAttribute import ( Shape, ShapeList, - Size2d, Point2d, Line2d, Rectangle, diff --git a/meshroom/core/desc/geometryAttribute.py b/meshroom/core/desc/geometryAttribute.py new file mode 100644 index 0000000000..d54ab3c73a --- /dev/null +++ b/meshroom/core/desc/geometryAttribute.py @@ -0,0 +1,61 @@ +from meshroom.core.desc import GroupAttribute, FloatParam + +class Geometry(GroupAttribute): + """ + Base attribute for all Geometry attribute. + Countains several attributes (inherit from GroupAttribute). + """ + def __init__(self, groupDesc, name, label, description, group="allParams", advanced=False, semantic="", + enabled=True, visible=True, exposed=False): + # GroupAttribute constructor + super(Geometry, self).__init__(groupDesc=groupDesc, name=name, label=label, description=description, + group=group, advanced=advanced, semantic=semantic, + enabled=enabled, visible=visible, exposed=exposed) + + def getInstanceType(self): + """ + Return the correct Attribute instance corresponding to the description. + """ + # Import within the method to prevent cyclic dependencies + from meshroom.core.attribute import GeometryAttribute + return GeometryAttribute + +class Size2d(Geometry): + """ + Size2d is a Geometry attribute that allows to specify a 2d size. + """ + def __init__(self, name, label, description, width, height, widthRange=None, heightRange=None, + keyable=False, keyType=None, group="allParams", advanced=False, semantic="", + enabled=True, visible=True, exposed=False): + # Geometry group desciption + groupDesc = [ + FloatParam(name="width", label="Width", description="Width size.", value=width, range=widthRange, + keyable=keyable, keyType=keyType, group=group, advanced=advanced, + enabled=enabled, visible=visible, exposed=exposed), + FloatParam(name="height", label="Height", description="Height size.", value=height, range=heightRange, + keyable=keyable, keyType=keyType, group=group, advanced=advanced, + enabled=enabled, visible=visible, exposed=exposed) + ] + # GeometryAttribute constructor + super(Size2d, self).__init__(groupDesc, name, label, description, group=None, advanced=advanced, + semantic=semantic, enabled=enabled, visible=visible, exposed=exposed) + +class Vec2d(Geometry): + """ + Vec2d is a Geometry attribute that allows to specify a 2d vector. + """ + def __init__(self, name, label, description, x, y, xRange=None, yRange=None, + keyable=False, keyType=None, group="allParams", advanced=False, semantic="", + enabled=True, visible=True, exposed=False): + # Geometry group desciption + groupDesc = [ + FloatParam(name="x", label="X", description="X coordinate.", value=x, range=xRange, + keyable=keyable, keyType=keyType, group=group, advanced=advanced, + enabled=enabled, visible=visible, exposed=exposed), + FloatParam(name="y", label="Y", description="Y coordinate.", value=y, range=yRange, + keyable=keyable, keyType=keyType, group=group, advanced=advanced, + enabled=enabled, visible=visible, exposed=exposed) + ] + # GeometryAttribute constructor + super(Vec2d, self).__init__(groupDesc, name, label, description, group=None, advanced=advanced, + semantic=semantic, enabled=enabled, visible=visible, exposed=exposed) \ No newline at end of file diff --git a/meshroom/core/desc/shapeAttribute.py b/meshroom/core/desc/shapeAttribute.py index a678bcc09a..58a3e48020 100644 --- a/meshroom/core/desc/shapeAttribute.py +++ b/meshroom/core/desc/shapeAttribute.py @@ -1,12 +1,21 @@ -from meshroom.core.desc import ListAttribute, GroupAttribute, FloatParam +from meshroom.core.desc import ListAttribute, GroupAttribute, StringParam, FloatParam, Geometry, Size2d, Vec2d class Shape(GroupAttribute): """ Base attribute for all Shape attribute. Countains several attributes (inherit from GroupAttribute). """ - def __init__(self, groupDesc, name, label, description, group="allParams", advanced=False, semantic="", + def __init__(self, geometryGroupDesc, name, label, description, group="allParams", advanced=False, semantic="", enabled=True, visible=True, exposed=False): + # Shape group desciption + groupDesc = [ + StringParam(name="userName", label="User Name", description="User shape name.", value="", + group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed), + StringParam(name="userColor", label="User Color", description="User shape color.", value="#2a82da", + group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed), + Geometry(geometryGroupDesc, name="geometry", label="Geometry", description="Shape geometry.", + group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed) + ] # GroupAttribute constructor super(Shape, self).__init__(groupDesc=groupDesc, name=name, label=label, description=description, group=group, advanced=advanced, semantic=semantic, @@ -40,25 +49,6 @@ def getInstanceType(self): from meshroom.core.attribute import ShapeListAttribute return ShapeListAttribute -class Size2d(Shape): - """ - Size2d is a Shape attribute that allows to specify a 2d size. - Note: This attribute is not displayable. - """ - def __init__(self, name, label, description, keyable=False, keyType=None, - group="allParams", advanced=False, semantic="", - enabled=True, visible=True, exposed=False): - # Shape group desciption - groupDesc = [ - FloatParam(name="width", label="Width", description="Width size.", value=-1.0, keyable=keyable, keyType=keyType, - group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed), - FloatParam(name="height", label="Height", description="Height size.", value=-1.0, keyable=keyable, keyType=keyType, - group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed) - ] - # ShapeAttribute constructor - super(Size2d, self).__init__(groupDesc, name, label, description, group=None, advanced=advanced, semantic=semantic, - enabled=enabled, visible=visible, exposed=exposed) - class Point2d(Shape): """ Point2d is a Shape attribute that allows to display and modify a 2d point. @@ -66,16 +56,16 @@ class Point2d(Shape): def __init__(self, name, label, description, keyable=False, keyType=None, group="allParams", advanced=False, semantic="", enabled=True, visible=True, exposed=False): - # Shape group desciption - groupDesc = [ + # Geometry group desciption + geometryGroupDesc = [ FloatParam(name="x", label="X", description="X coordinate.", value=-1.0, keyable=keyable, keyType=keyType, group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed), FloatParam(name="y", label="Y", description="Y coordinate.", value=-1.0, keyable=keyable, keyType=keyType, group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed) ] # ShapeAttribute constructor - super(Point2d, self).__init__(groupDesc, name, label, description, group=None, advanced=advanced, semantic=semantic, - enabled=enabled, visible=visible, exposed=exposed) + super(Point2d, self).__init__(geometryGroupDesc, name, label, description, group=None, advanced=advanced, + semantic=semantic, enabled=enabled, visible=visible, exposed=exposed) class Line2d(Shape): """ @@ -84,16 +74,16 @@ class Line2d(Shape): def __init__(self, name, label, description, keyable=False, keyType=None, group="allParams", advanced=False, semantic="", enabled=True, visible=True, exposed=False): - # Shape group desciption - groupDesc = [ - Point2d(name="a", label="A", description="Line A point.", keyable=keyable, keyType=keyType, - group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed), - Point2d(name="b", label="B", description="Line B point.", keyable=keyable, keyType=keyType, - group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed) + # Geometry group desciption + geometryGroupDesc = [ + Vec2d(name="a", label="A", description="Line A point.", x=-1.0, y=-1.0, keyable=keyable, keyType=keyType, + group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed), + Vec2d(name="b", label="B", description="Line B point.", x=-1.0, y=-1.0, keyable=keyable, keyType=keyType, + group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed) ] # ShapeAttribute constructor - super(Line2d, self).__init__(groupDesc, name, label, description, group=None, advanced=advanced, semantic=semantic, - enabled=enabled, visible=visible, exposed=exposed) + super(Line2d, self).__init__(geometryGroupDesc, name, label, description, group=None, advanced=advanced, + semantic=semantic, enabled=enabled, visible=visible, exposed=exposed) class Rectangle(Shape): """ @@ -102,16 +92,18 @@ class Rectangle(Shape): def __init__(self, name, label, description, keyable=False, keyType=None, group="allParams", advanced=False, semantic="", enabled=True, visible=True, exposed=False): - # Shape group desciption - groupDesc = [ - Point2d(name="center", label="Center", description="Rectangle center.", keyable=keyable, keyType=keyType, - group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed), - Size2d(name="size", label="Size", description="Rectangle size.", keyable=keyable, keyType=keyType, - group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed) + # Geometry group desciption + geometryGroupDesc = [ + Vec2d(name="center", label="Center", description="Rectangle center.", x=-1.0, y=-1.0, + keyable=keyable, keyType=keyType, group=group, advanced=advanced, + enabled=enabled, visible=visible, exposed=exposed), + Size2d(name="size", label="Size", description="Rectangle size.", width=-1.0, height=-1.0, + keyable=keyable, keyType=keyType, group=group, advanced=advanced, + enabled=enabled, visible=visible, exposed=exposed) ] # ShapeAttribute constructor - super(Rectangle, self).__init__(groupDesc, name, label, description, group=None, advanced=advanced, semantic=semantic, - enabled=enabled, visible=visible, exposed=exposed) + super(Rectangle, self).__init__(geometryGroupDesc, name, label, description, group=None, advanced=advanced, + semantic=semantic, enabled=enabled, visible=visible, exposed=exposed) class Circle(Shape): """ @@ -120,13 +112,15 @@ class Circle(Shape): def __init__(self, name, label, description, keyable=False, keyType=None, group="allParams", advanced=False, semantic="", enabled=True, visible=True, exposed=False): - # Shape group desciption - groupDesc = [ - Point2d(name="center", label="Center", description="Circle center.", keyable=keyable, keyType=keyType, - group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed), - FloatParam(name="radius", label="Radius", description="Circle radius.", value=-1.0, keyable=keyable, keyType=keyType, - group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed) + # Geometry group desciption + geometryGroupDesc = [ + Vec2d(name="center", label="Center", description="Circle center.", x=-1.0, y=-1.0, + keyable=keyable, keyType=keyType, group=group, advanced=advanced, + enabled=enabled, visible=visible, exposed=exposed), + FloatParam(name="radius", label="Radius", description="Circle radius.", value=-1.0, + keyable=keyable, keyType=keyType, group=group, advanced=advanced, + enabled=enabled, visible=visible, exposed=exposed) ] # ShapeAttribute constructor - super(Circle, self).__init__(groupDesc, name, label, description, group=None, advanced=advanced, semantic=semantic, - enabled=enabled, visible=visible, exposed=exposed) \ No newline at end of file + super(Circle, self).__init__(geometryGroupDesc, name, label, description, group=None, advanced=advanced, + semantic=semantic, enabled=enabled, visible=visible, exposed=exposed) \ No newline at end of file diff --git a/meshroom/ui/commands.py b/meshroom/ui/commands.py index 2fdfebf6fa..48944abb22 100755 --- a/meshroom/ui/commands.py +++ b/meshroom/ui/commands.py @@ -377,27 +377,27 @@ def __init__(self, graph, attribute, key, observation, parent=None): self.attrName = attribute.fullName self.key = key self.observation = observation.toVariant() - self.oldObservation = attribute.getObservation(key) + self.oldObservation = attribute.geometry.getObservation(key) self.setText(f"Set observation for shape attribute '{attribute.fullName}' at key: '{key}'") def redoImpl(self): if self.graph.attribute(self.attrName) is not None: - self.graph.attribute(self.attrName).setObservation(self.key, self.observation) + self.graph.attribute(self.attrName).geometry.setObservation(self.key, self.observation) else: - self.graph.internalAttribute(self.attrName).setObservation(self.key, self.observation) + self.graph.internalAttribute(self.attrName).geometry.setObservation(self.key, self.observation) return True def undoImpl(self): if self.graph.attribute(self.attrName) is not None: if self.oldObservation is None: - self.graph.attribute(self.attrName).removeObservation(self.key) + self.graph.attribute(self.attrName).geometry.removeObservation(self.key) else: - self.graph.attribute(self.attrName).setObservation(self.key, self.oldObservation) + self.graph.attribute(self.attrName).geometry.setObservation(self.key, self.oldObservation) else: if self.oldObservation is None: - self.graph.internalAttribute(self.attrName).removeObservation(self.key) + self.graph.internalAttribute(self.attrName).geometry.removeObservation(self.key) else: - self.graph.internalAttribute(self.attrName).setObservation(self.key, self.oldObservation) + self.graph.internalAttribute(self.attrName).geometry.setObservation(self.key, self.oldObservation) return True class RemoveObservationCommand(GraphCommand): @@ -405,21 +405,21 @@ def __init__(self, graph, attribute, key, parent=None): super().__init__(graph, parent) self.attrName = attribute.fullName self.key = key - self.oldObservation = attribute.getObservation(key) + self.oldObservation = attribute.geometry.getObservation(key) self.setText(f"Remove observation for shape attribute '{attribute.fullName}' at key: '{key}'") def redoImpl(self): if self.graph.attribute(self.attrName) is not None: - self.graph.attribute(self.attrName).removeObservation(self.key) + self.graph.attribute(self.attrName).geometry.removeObservation(self.key) else: - self.graph.internalAttribute(self.attrName).removeObservation(self.key) + self.graph.internalAttribute(self.attrName).geometry.removeObservation(self.key) return True def undoImpl(self): if self.graph.attribute(self.attrName) is not None: - self.graph.attribute(self.attrName).setObservation(self.key, self.oldObservation) + self.graph.attribute(self.attrName).geometry.setObservation(self.key, self.oldObservation) else: - self.graph.internalAttribute(self.attrName).setObservation(self.key, self.oldObservation) + self.graph.internalAttribute(self.attrName).geometry.setObservation(self.key, self.oldObservation) return True class AddEdgeCommand(GraphCommand): diff --git a/meshroom/ui/components/shapes/shapeFile.py b/meshroom/ui/components/shapes/shapeFile.py index 2895c214ac..592b9139c0 100644 --- a/meshroom/ui/components/shapes/shapeFile.py +++ b/meshroom/ui/components/shapes/shapeFile.py @@ -84,16 +84,16 @@ def hasObservation(self, key: str) -> bool: label = Property(str, lambda self: self._name, constant=True) # The shape type (Point2d, Line2d, Rectangle, Circle, etc.). type = Property(str, lambda self: self._type, constant=True) - # Whether the shape is keyabale (multiple observations). - shapeKeyable = Property(bool,lambda self: self._keyable, constant=True) # The shape properties (color, stroke, etc.). properties = Property(Variant, lambda self: self._properties, constant=True) # The shape current observation. observation = Property(Variant, _getObservation, notify=viewIdChanged) + # Whether the shape is keyabale (multiple observations). + observationKeyable = Property(bool,lambda self: self._keyable, constant=True) # The shape list of observation keys. observationKeys = Property(Variant, lambda self: [key for key in self._observations], constant=True) # The number of observation defined. - nbObservations = Property(int, _getNbObservations, constant=True) + nbObservations = Property(int, _getNbObservations, constant=True) # Whether the shape is displayable. isVisible = Property(bool, _getVisible, _setVisible, notify=visibleChanged) diff --git a/meshroom/ui/qml/Shapes/Editor/Items/ShapeAttributeItem.qml b/meshroom/ui/qml/Shapes/Editor/Items/ShapeAttributeItem.qml index 9529a36a5c..aa59c2beef 100644 --- a/meshroom/ui/qml/Shapes/Editor/Items/ShapeAttributeItem.qml +++ b/meshroom/ui/qml/Shapes/Editor/Items/ShapeAttributeItem.qml @@ -19,29 +19,11 @@ Column { property var shapeAttribute property alias isNeasted: itemHeader.isNeasted property alias isLinkChild: itemHeader.isLinkChild - - - function hasCurrentObservation() { - return shapeAttribute ? shapeAttribute.hasObservation(_reconstruction ? _reconstruction.selectedViewId : "-1") : false - } - - // Reload hasObservation property - // When shape attribute observations changed (signal) - Connections { - target: shapeAttribute - function onObservationsChanged() { itemHeader.hasShapeObservation = hasCurrentObservation() } - } - // When reconstruction view id changed (signal) - Connections { - target: _reconstruction - function onSelectedViewIdChanged() { itemHeader.hasShapeObservation = hasCurrentObservation() } - } // Item Header ItemUtils.ItemHeader { id: itemHeader model: shapeAttribute - hasShapeObservation: hasCurrentObservation() isShape: true isAttribute: true } diff --git a/meshroom/ui/qml/Shapes/Editor/Items/ShapeDataItem.qml b/meshroom/ui/qml/Shapes/Editor/Items/ShapeDataItem.qml index 34a3161bec..c3a0057839 100644 --- a/meshroom/ui/qml/Shapes/Editor/Items/ShapeDataItem.qml +++ b/meshroom/ui/qml/Shapes/Editor/Items/ShapeDataItem.qml @@ -23,7 +23,6 @@ Column { ItemUtils.ItemHeader { id: itemHeader model: shapeData - hasShapeObservation: shapeData.hasObservation(_reconstruction.selectedViewId) isShape: true isAttribute: false } diff --git a/meshroom/ui/qml/Shapes/Editor/Items/Utils/ItemHeader.qml b/meshroom/ui/qml/Shapes/Editor/Items/Utils/ItemHeader.qml index 0e56f85da7..a3be18556c 100644 --- a/meshroom/ui/qml/Shapes/Editor/Items/Utils/ItemHeader.qml +++ b/meshroom/ui/qml/Shapes/Editor/Items/Utils/ItemHeader.qml @@ -13,7 +13,6 @@ import Utils 1.0 * @param model - the given model (provide by the current node or ShapeFilesHelper) * @param isShape - whether the model is a shape (ShapeAttribute or ShapeData) * @param isAttribute - whether the model is an attribute (ShapeAttribute or ShapeListAttribute) -* @param hasShapeObservation - whether the model is a shape with a current observation * @param isNeasted - whether the header is neasted * @param isLinkChild - Whether the model is a child attribute of a linked attribute * @param isExpanded - whether the heder is expanded @@ -27,7 +26,6 @@ Pane { property bool isShape: false property bool isAttribute: false property bool isLinkChild: false - property bool hasShapeObservation: false // Header properties property bool isNeasted: false @@ -35,14 +33,14 @@ Pane { // Read-only properties readonly property bool isAttributeSelected: isAttribute ? (ShapeViewerHelper.selectedShapeName === model.fullName) : false - readonly property bool isAttributeInitialized: isAttribute ? !model.isDefault : false + readonly property bool isAttributeInitialized: isAttribute ? (isShape ? !model.geometry.isDefault : !model.isDefault) : false readonly property bool isAttributeEnabled: isAttribute ? (model.enabled && !model.isLink && !isLinkChild) : false // Padding topPadding: 2 bottomPadding: 2 - rightPadding: 5 - leftPadding: 5 + rightPadding: 6 + leftPadding: 6 // Background background: Rectangle { @@ -200,7 +198,7 @@ Pane { enabled: isAttributeEnabled contentItem: Rectangle { anchors.centerIn: parent - color: isAttribute ? model.shapeColor : model.properties.color || "black" + color: isAttribute ? model.userColor : model.properties.color || "black" width: materialMetrics.height height: materialMetrics.height } @@ -217,9 +215,9 @@ Pane { active: isShape && isAttributeEnabled sourceComponent: ColorDialog { title: "Edit " + model.label + " color" - selectedColor: model.shapeColor + selectedColor: model.userColor onAccepted: { - model.shapeColor = selectedColor + _reconstruction.setAttribute(model.childAttribute("userColor"), selectedColor.toString()) close() } onRejected: close() @@ -250,17 +248,24 @@ Pane { } // Shape name - Label { - text: model.label + TextField { font.pointSize: 8 - } - - // Shape index - Loader { - active: isAttribute && model.root && (model.root.type === "ShapeList") - sourceComponent: Label { - text: "[" + index + "]" - font.pointSize: 8 + background: Rectangle { color: "transparent" } + palette.text: parent.palette.text + maximumLength: 40 + selectByMouse: true + persistentSelection: false + text: { + if(isAttribute && isShape && model.userName) + return model.userName + if(isAttribute && model.root && (model.root.type === "ShapeList")) + return model.rootName + return model.label + } + enabled: isAttributeEnabled && model.root && (model.root.type === "ShapeList") + onEditingFinished: { + _reconstruction.setAttribute(model.childAttribute("userName"), text) + focus = false } } @@ -275,9 +280,9 @@ Pane { // Shape number of observations Loader { - active: isShape && model.shapeKeyable + active: isShape && (isAttribute ? model.geometry.observationKeyable : model.observationKeyable) sourceComponent: Label { - text: "(" + model.nbObservations + ")" + text: "(" + (isAttribute ? model.geometry.nbObservations : model.nbObservations) + ")" font.pointSize: 8 } } @@ -290,9 +295,9 @@ Pane { RowLayout { spacing: 0 - // Shape not keyable, set/remove observation + // Static shape, set/remove observation Loader { - active: isShape && isAttribute && !model.shapeKeyable + active: isShape && isAttribute && !model.geometry.observationKeyable sourceComponent: MaterialToolButton { font.pointSize: 11 padding: 2 @@ -309,7 +314,8 @@ Pane { else { // add key - _reconstruction.setObservation(model, _reconstruction.selectedViewId, ShapeViewerHelper.getDefaultObservation(model.type)) + _reconstruction.setObservation(model, _reconstruction.selectedViewId, + ShapeViewerHelper.getDefaultObservation(model.type)) ShapeViewerHelper.selectedShapeName = model.fullName } } @@ -321,9 +327,15 @@ Pane { // Shape keyable, set/remove observation Loader { - active: isShape && model.shapeKeyable + active: isShape && (isAttribute ? model.geometry.observationKeyable : model.observationKeyable) sourceComponent: RowLayout { spacing: 0 + property var keys: isAttribute ? model.geometry.observationKeys : model.observationKeys + property bool hasCurrentKey: { + if(isAttribute) + return model.geometry.hasObservation(_reconstruction.selectedViewId) + return model.hasObservation(_reconstruction.selectedViewId) + } function getViewPath(viewId) { for (var i = 0; i < _reconstruction.viewpoints.count; i++) @@ -355,7 +367,7 @@ Pane { // Previous key MaterialToolButton { - property string prevViewId: getPrevViewId(model.observationKeys, _reconstruction.selectedViewId) + property string prevViewId: getPrevViewId(keys, _reconstruction.selectedViewId) font.pointSize: 11 padding: 2 text: MaterialIcons.keyboard_arrow_left @@ -372,11 +384,11 @@ Pane { font.pointSize: 11 padding: 2 text: MaterialIcons.noise_control_off - checkable: model.shapeKeyable - checked: model.shapeKeyable ? hasShapeObservation : false + checkable: true + checked: hasCurrentKey enabled: isAttributeEnabled onClicked: { - if(hasShapeObservation) + if(hasCurrentKey) { // remove key _reconstruction.removeObservation(model, _reconstruction.selectedViewId) @@ -385,18 +397,19 @@ Pane { else { // add key - _reconstruction.setObservation(model, _reconstruction.selectedViewId, ShapeViewerHelper.getDefaultObservation(model.type)) + _reconstruction.setObservation(model, _reconstruction.selectedViewId, + ShapeViewerHelper.getDefaultObservation(model.type)) ShapeViewerHelper.selectedShapeName = model.fullName } } - ToolTip.text: hasShapeObservation ? "Remove current key" : "Set current key" + ToolTip.text: checked ? "Remove current key" : "Set current key" ToolTip.visible: hovered ToolTip.delay: 800 } // Next key MaterialToolButton { - property string nextViewId: getNextViewId(model.observationKeys, _reconstruction.selectedViewId) + property string nextViewId: getNextViewId(keys, _reconstruction.selectedViewId) font.pointSize: 11 padding: 2 text: MaterialIcons.keyboard_arrow_right diff --git a/meshroom/ui/qml/Shapes/Viewer/Layers/BaseLayer.qml b/meshroom/ui/qml/Shapes/Viewer/Layers/BaseLayer.qml index 0085f1ee69..9811b61515 100644 --- a/meshroom/ui/qml/Shapes/Viewer/Layers/BaseLayer.qml +++ b/meshroom/ui/qml/Shapes/Viewer/Layers/BaseLayer.qml @@ -43,14 +43,9 @@ Item { ShapeViewerHelper.selectedShapeName = name } - // Helper function to get scaled point size - function getScaledPointSize() { - return Math.max(0.5, (baseLayer.properties.size || 10.0) * baseLayer.scaleRatio) - } - // Helper function to get scaled handle size function getScaledHandleSize() { - return Math.max(1.0, 8.0 * scaleRatio) + return Math.max(0.5, 8.0 * scaleRatio) } // Helper function to get scaled stroke width @@ -65,6 +60,6 @@ Item { // Helper function to get scaled font size function getScaledFontSize() { - return Math.max(4.0, (baseLayer.properties.fontSize || 10.0) * baseLayer.scaleRatio) + return Math.max(1.0, (baseLayer.properties.fontSize || 10.0) * baseLayer.scaleRatio) } } \ No newline at end of file diff --git a/meshroom/ui/qml/Shapes/Viewer/Layers/PointLayer.qml b/meshroom/ui/qml/Shapes/Viewer/Layers/PointLayer.qml index 216647d7a5..817d178aa7 100644 --- a/meshroom/ui/qml/Shapes/Viewer/Layers/PointLayer.qml +++ b/meshroom/ui/qml/Shapes/Viewer/Layers/PointLayer.qml @@ -1,4 +1,7 @@ import QtQuick +import QtQuick.Shapes + +import "Utils" as LayerUtils /** * PointLayer @@ -15,65 +18,81 @@ import QtQuick BaseLayer { id: pointLayer - // Point size from scaled properties.size - property real pointSize: pointLayer.getScaledPointSize() + // Point size and half size + property real pointSize: Math.max(1.0, 12.0 * scaleRatio) + property real pointHalfSize: pointSize * 0.5 // Point shape - Rectangle { - id: draggablePoint - x: pointLayer.observation.x - (pointSize * 0.5) - y: pointLayer.observation.y - (pointSize * 0.5) - width: pointSize - height: width - color: selected ? "#ffffff" : pointLayer.properties.color || pointLayer.defaultColor - - // Selection click - TapHandler { + Shape { + id: draggableShape + + // Center cross path + ShapePath { + fillColor: "transparent" + strokeColor: selected ? "#ffffff" : pointLayer.properties.color || pointLayer.defaultColor + strokeWidth: getScaledStrokeWidth() + + PathMove { x: pointLayer.observation.x - pointSize; y: pointLayer.observation.y } + PathLine { x: pointLayer.observation.x + pointSize; y: pointLayer.observation.y } + PathMove { x: pointLayer.observation.x; y: pointLayer.observation.y - pointSize } + PathLine { x: pointLayer.observation.x; y: pointLayer.observation.y + pointSize } + } + + // Selection area + MouseArea { + x: handleCenter.x - pointSize + y: handleCenter.y - pointSize + width: pointSize * 2 + height: pointSize * 2 acceptedButtons: Qt.LeftButton - gesturePolicy: TapHandler.WithinBounds - grabPermissions: PointerHandler.CanTakeOverFromAnything - margin: pointSize - onTapped: selectionRequested() + cursorShape: pointLayer.editable ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: selectionRequested() enabled: pointLayer.editable && !pointLayer.selected } - - // Selection hover - HoverHandler { - cursorShape: pointLayer.selected ? Qt.SizeAllCursor : Qt.PointingHandCursor - grabPermissions: PointerHandler.CanTakeOverFromAnything - margin: pointSize - enabled: pointLayer.editable - } - // Drag - DragHandler { - target: draggablePoint + // Handle for point center + LayerUtils.Handle { + id: handleCenter + x: pointLayer.observation.x || 0 + y: pointLayer.observation.y || 0 + size: getScaledHandleSize() + target: draggableShape cursorShape: Qt.SizeAllCursor - enabled: pointLayer.editable && pointLayer.selected - onActiveChanged: { - if (!active) { - _reconstruction.setObservationFromName(pointLayer.name, _reconstruction.selectedViewId, { - x: draggablePoint.x + pointSize * 0.5, - y: draggablePoint.y + pointSize * 0.5 - }) - } + visible: pointLayer.editable && pointLayer.selected + onMoved: { + _reconstruction.setObservationFromName(pointLayer.name, _reconstruction.selectedViewId, { + x: handleCenter.x + draggableShape.x, + y: handleCenter.y + draggableShape.y + }) } } // Point name - Text { - x: pointSize - y: pointSize - text: { - const lastDotIndex = pointLayer.name.lastIndexOf('.') - if(lastDotIndex < 0) - return pointLayer.name - return pointLayer.name.substring(lastDotIndex + 1); + Rectangle { + x: (pointLayer.observation.x || 0) + pointHalfSize + y: (pointLayer.observation.y || 0) + pointHalfSize + width: pointName.width + height: pointName.height + visible: pointLayer.editable && scaleRatio > 0.2 + color: selected ? palette.shadow : palette.window + + Text { + id: pointName + text: { + if(pointLayer.properties.userName && pointLayer.properties.userName.length > 0) + return pointLayer.properties.userName + const lastDotIndex = pointLayer.name.lastIndexOf('.') + if(lastDotIndex < 0) + return pointLayer.name + return pointLayer.name.substring(lastDotIndex + 1); + } + color: selected ? palette.highlightedText : palette.text + padding: 0 + rightPadding: Math.max(1, 2 * scaleRatio) + leftPadding: rightPadding + wrapMode: Text.NoWrap + font.pixelSize: getScaledFontSize() } - color: draggablePoint.color - wrapMode: Text.NoWrap - font.pixelSize: getScaledFontSize() - visible: pointLayer.editable && scaleRatio > 0.1 } } } diff --git a/meshroom/ui/qml/Shapes/Viewer/Layers/Utils/Handle.qml b/meshroom/ui/qml/Shapes/Viewer/Layers/Utils/Handle.qml index 51d05ffdcf..54a7b86976 100644 --- a/meshroom/ui/qml/Shapes/Viewer/Layers/Utils/Handle.qml +++ b/meshroom/ui/qml/Shapes/Viewer/Layers/Utils/Handle.qml @@ -64,4 +64,14 @@ Rectangle { height: root.size color: "#ffffff" } + + // Handle outline + Rectangle { + x: width * -0.5 + y: height * -0.5 + width: 1.5 * root.size + height: 1.5 * root.size + color: "#66666666" + z: -1 + } } \ No newline at end of file diff --git a/meshroom/ui/qml/Shapes/Viewer/ShapeViewerAttributeLayer.qml b/meshroom/ui/qml/Shapes/Viewer/ShapeViewerAttributeLayer.qml index d535a7da05..f571be9056 100644 --- a/meshroom/ui/qml/Shapes/Viewer/ShapeViewerAttributeLayer.qml +++ b/meshroom/ui/qml/Shapes/Viewer/ShapeViewerAttributeLayer.qml @@ -22,7 +22,7 @@ Loader { // When attribute observations changed (signal) // For now, ShapeLayer should be re-build when observation changed Connections { - target: shapeAttribute + target: shapeAttribute.geometry function onObservationsChanged() { sourceComponent = null sourceComponent = shapeAttributeLayerComponent @@ -37,8 +37,8 @@ Loader { scaleRatio: shapeViewer.scaleRatio name: shapeAttribute.fullName type: shapeAttribute.type - properties: ({"color" : shapeAttribute.shapeColor}) - observation: shapeAttribute.getObservation(_reconstruction ? _reconstruction.selectedViewId : "-1") + properties: ({"color" : shapeAttribute.userColor, "userName" : shapeAttribute.userName}) + observation: shapeAttribute.geometry.getObservation(_reconstruction ? _reconstruction.selectedViewId : "-1") editable: shapeAttribute.enabled && !shapeAttribute.isLink && !isLinkChild } } diff --git a/meshroom/ui/qml/Shapes/Viewer/ShapeViewerAttributeLoader.qml b/meshroom/ui/qml/Shapes/Viewer/ShapeViewerAttributeLoader.qml index cc076e87ed..00ffc03484 100644 --- a/meshroom/ui/qml/Shapes/Viewer/ShapeViewerAttributeLoader.qml +++ b/meshroom/ui/qml/Shapes/Viewer/ShapeViewerAttributeLoader.qml @@ -15,8 +15,8 @@ Loader { property real scaleRatio: 1.0 // Attribute should be shape or shape list - // Attribute should be visible and not default - active: attribute.hasDisplayableShape && attribute.isVisible && !attribute.isDefault + // Attribute should be visible + active: attribute.hasDisplayableShape && attribute.isVisible // Source component sourceComponent: { @@ -29,6 +29,7 @@ Loader { Component { id: shapeAttributeComponent ShapeViewerAttributeLayer { + active: !attribute.geometry.isDefault shapeAttribute: attribute scaleRatio: attributeLoader.scaleRatio } @@ -40,7 +41,7 @@ Loader { Repeater { model: attribute.value delegate: ShapeViewerAttributeLayer { - active: object.isVisible && !object.isDefault + active: object.isVisible && !object.geometry.isDefault shapeAttribute: object isLinkChild: attribute.isLink scaleRatio: attributeLoader.scaleRatio diff --git a/tests/test_attributeShape.py b/tests/test_attributeShape.py index 5dbeb12c40..ebb37fde7b 100644 --- a/tests/test_attributeShape.py +++ b/tests/test_attributeShape.py @@ -129,32 +129,32 @@ def test_initialization(self): assert node.keyableRectangle.type == "Rectangle" assert node.keyableCircle.type == "Circle" - # Check attribute number of observations + # Check attribute geometry number of observations # Should be 1 for static shape (default) - assert node.point.nbObservations == 1 - assert node.line.nbObservations == 1 - assert node.rectangle.nbObservations == 1 - assert node.circle.nbObservations == 1 + assert node.point.geometry.nbObservations == 1 + assert node.line.geometry.nbObservations == 1 + assert node.rectangle.geometry.nbObservations == 1 + assert node.circle.geometry.nbObservations == 1 # Should be 0 for keyable shape - assert node.keyablePoint.nbObservations == 0 - assert node.keyableLine.nbObservations == 0 - assert node.keyableRectangle.nbObservations == 0 - assert node.keyableCircle.nbObservations == 0 + assert node.keyablePoint.geometry.nbObservations == 0 + assert node.keyableLine.geometry.nbObservations == 0 + assert node.keyableRectangle.geometry.nbObservations == 0 + assert node.keyableCircle.geometry.nbObservations == 0 - # Check attribute shape keyable + # Check shape attribute geometry observation keyable # Should be false for static shape - assert not node.point.shapeKeyable - assert not node.line.shapeKeyable - assert not node.rectangle.shapeKeyable - assert not node.circle.shapeKeyable + assert not node.point.geometry.observationKeyable + assert not node.line.geometry.observationKeyable + assert not node.rectangle.geometry.observationKeyable + assert not node.circle.geometry.observationKeyable # Should be true for keyable shape - assert node.keyablePoint.shapeKeyable - assert node.keyableLine.shapeKeyable - assert node.keyableRectangle.shapeKeyable - assert node.keyableCircle.shapeKeyable + assert node.keyablePoint.geometry.observationKeyable + assert node.keyableLine.geometry.observationKeyable + assert node.keyableRectangle.geometry.observationKeyable + assert node.keyableCircle.geometry.observationKeyable - def test_staticShape(self): + def test_staticShapeGeometry(self): graph = Graph("") node = graph.addNewNode(NodeWithShapeAttributes.__name__) @@ -163,63 +163,63 @@ def test_staticShape(self): observationRectangle = {"center" : {"x" : 10, "y" : 10}, "size" : {"width" : 20, "height" : 20}} observationCircle = {"center" : {"x" : 10, "y" : 10}, "radius" : 20} - # Check attribute has observation, should be true (default) - assert node.point.hasObservation("0") - assert node.line.hasObservation("0") - assert node.rectangle.hasObservation("0") - assert node.circle.hasObservation("0") + # Check static shape has observation, should be true (default) + assert node.point.geometry.hasObservation("0") + assert node.line.geometry.hasObservation("0") + assert node.rectangle.geometry.hasObservation("0") + assert node.circle.geometry.hasObservation("0") - # Check attribute get observation, should be default value - assert node.point.getObservation("0") == node.point.getDefaultValue() - assert node.line.getObservation("0") == node.line.getDefaultValue() - assert node.rectangle.getObservation("0") == node.rectangle.getDefaultValue() - assert node.circle.getObservation("0") == node.circle.getDefaultValue() + # Check static shape get observation, should be default value + assert node.point.geometry.getObservation("0") == node.point.geometry.getDefaultValue() + assert node.line.geometry.getObservation("0") == node.line.geometry.getDefaultValue() + assert node.rectangle.geometry.getObservation("0") == node.rectangle.geometry.getDefaultValue() + assert node.circle.geometry.getObservation("0") == node.circle.geometry.getDefaultValue() # Create observation at key "0" - # Attribute are not keyable, key has no effect - node.point.setObservation("0", observationPoint) - node.line.setObservation("0", observationLine) - node.rectangle.setObservation("0", observationRectangle) - node.circle.setObservation("0", observationCircle) - - # Check attribute has observation, should be true - assert node.point.hasObservation("0") - assert node.line.hasObservation("0") - assert node.rectangle.hasObservation("0") - assert node.circle.hasObservation("0") - - # Check attribute get observation, should be created observation - assert node.point.getObservation("0") == observationPoint - assert node.line.getObservation("0") == observationLine - assert node.rectangle.getObservation("0") == observationRectangle - assert node.circle.getObservation("0") == observationCircle - - # Update attribute observation - node.point.setObservation("0", {"x" : 2}) - node.line.setObservation("0", {"a" : {"x" : 2, "y": 2}}) - node.rectangle.setObservation("0", {"center" : {"x" : 20, "y" : 20}}) - node.circle.setObservation("0", {"radius" : 40}) - - # Check attribute get observation, should be updated observation - assert node.point.getObservation("0").get("x") == 2 - assert node.line.getObservation("0").get("a") == {"x" : 2, "y": 2} - assert node.rectangle.getObservation("0").get("center") == {"x" : 20, "y" : 20} - assert node.circle.getObservation("0").get("radius") == 40 - - # Reset attribute value - node.point.resetToDefaultValue() - node.line.resetToDefaultValue() - node.rectangle.resetToDefaultValue() - node.circle.resetToDefaultValue() - - # Check attribute get observation, should be default value - assert node.point.getObservation("0") == node.point.getDefaultValue() - assert node.line.getObservation("0") == node.line.getDefaultValue() - assert node.rectangle.getObservation("0") == node.rectangle.getDefaultValue() - assert node.circle.getObservation("0") == node.circle.getDefaultValue() - - - def test_keyableShape(self): + # For static shape key has no effect + node.point.geometry.setObservation("0", observationPoint) + node.line.geometry.setObservation("0", observationLine) + node.rectangle.geometry.setObservation("0", observationRectangle) + node.circle.geometry.setObservation("0", observationCircle) + + # Check static shape has observation, should be true + assert node.point.geometry.hasObservation("0") + assert node.line.geometry.hasObservation("0") + assert node.rectangle.geometry.hasObservation("0") + assert node.circle.geometry.hasObservation("0") + + # Check static shape get observation, should be created observation + assert node.point.geometry.getObservation("0") == observationPoint + assert node.line.geometry.getObservation("0") == observationLine + assert node.rectangle.geometry.getObservation("0") == observationRectangle + assert node.circle.geometry.getObservation("0") == observationCircle + + # Update static shape observation + node.point.geometry.setObservation("0", {"x" : 2}) + node.line.geometry.setObservation("0", {"a" : {"x" : 2, "y": 2}}) + node.rectangle.geometry.setObservation("0", {"center" : {"x" : 20, "y" : 20}}) + node.circle.geometry.setObservation("0", {"radius" : 40}) + + # Check static shape get observation, should be updated observation + assert node.point.geometry.getObservation("0").get("x") == 2 + assert node.line.geometry.getObservation("0").get("a") == {"x" : 2, "y": 2} + assert node.rectangle.geometry.getObservation("0").get("center") == {"x" : 20, "y" : 20} + assert node.circle.geometry.getObservation("0").get("radius") == 40 + + # Reset static shape geometry + node.point.geometry.resetToDefaultValue() + node.line.geometry.resetToDefaultValue() + node.rectangle.geometry.resetToDefaultValue() + node.circle.geometry.resetToDefaultValue() + + # Check static shape get observation, should be default value + assert node.point.geometry.getObservation("0") == node.point.geometry.getDefaultValue() + assert node.line.geometry.getObservation("0") == node.line.geometry.getDefaultValue() + assert node.rectangle.geometry.getObservation("0") == node.rectangle.geometry.getDefaultValue() + assert node.circle.geometry.getObservation("0") == node.circle.geometry.getDefaultValue() + + + def test_keyableShapeGeometry(self): graph = Graph("") node = graph.addNewNode(NodeWithShapeAttributes.__name__) @@ -228,106 +228,106 @@ def test_keyableShape(self): observationRectangle = {"center" : {"x" : 10, "y" : 10}, "size" : {"width" : 20, "height" : 20}} observationCircle = {"center" : {"x" : 10, "y" : 10}, "radius" : 20} - # Check attribute has observation at key "0", should be false - assert not node.keyablePoint.hasObservation("0") - assert not node.keyableLine.hasObservation("0") - assert not node.keyableRectangle.hasObservation("0") - assert not node.keyableCircle.hasObservation("0") + # Check keyable shape has observation at key "0", should be false + assert not node.keyablePoint.geometry.hasObservation("0") + assert not node.keyableLine.geometry.hasObservation("0") + assert not node.keyableRectangle.geometry.hasObservation("0") + assert not node.keyableCircle.geometry.hasObservation("0") - # Check attribute get observation at key "0", should be None (no observation) - assert node.keyablePoint.getObservation("0") == None - assert node.keyableLine.getObservation("0") == None - assert node.keyableRectangle.getObservation("0") == None - assert node.keyableCircle.getObservation("0") == None + # Check keyable shape get observation at key "0", should be None (no observation) + assert node.keyablePoint.geometry.getObservation("0") == None + assert node.keyableLine.geometry.getObservation("0") == None + assert node.keyableRectangle.geometry.getObservation("0") == None + assert node.keyableCircle.geometry.getObservation("0") == None # Create observation at key "0" - node.keyablePoint.setObservation("0", observationPoint) - node.keyableLine.setObservation("0", observationLine) - node.keyableRectangle.setObservation("0", observationRectangle) - node.keyableCircle.setObservation("0", observationCircle) + node.keyablePoint.geometry.setObservation("0", observationPoint) + node.keyableLine.geometry.setObservation("0", observationLine) + node.keyableRectangle.geometry.setObservation("0", observationRectangle) + node.keyableCircle.geometry.setObservation("0", observationCircle) - # Check attribute number of observations, should be 1 - assert node.keyablePoint.nbObservations == 1 - assert node.keyableLine.nbObservations == 1 - assert node.keyableRectangle.nbObservations == 1 - assert node.keyableCircle.nbObservations == 1 + # Check keyable shape number of observations, should be 1 + assert node.keyablePoint.geometry.nbObservations == 1 + assert node.keyableLine.geometry.nbObservations == 1 + assert node.keyableRectangle.geometry.nbObservations == 1 + assert node.keyableCircle.geometry.nbObservations == 1 # Create observation at key "1" - node.keyablePoint.setObservation("1", observationPoint) - node.keyableLine.setObservation("1", observationLine) - node.keyableRectangle.setObservation("1", observationRectangle) - node.keyableCircle.setObservation("1", observationCircle) - - # Check attribute number of observations, should be 2 - assert node.keyablePoint.nbObservations == 2 - assert node.keyableLine.nbObservations == 2 - assert node.keyableRectangle.nbObservations == 2 - assert node.keyableCircle.nbObservations == 2 - - # Check attribute has observation, should be true - assert node.keyablePoint.hasObservation("0") - assert node.keyablePoint.hasObservation("1") - assert node.keyableLine.hasObservation("0") - assert node.keyableLine.hasObservation("1") - assert node.keyableRectangle.hasObservation("0") - assert node.keyableRectangle.hasObservation("1") - assert node.keyableCircle.hasObservation("0") - assert node.keyableCircle.hasObservation("1") - - # Check attribute get observation at key "0", should be created observation - assert node.keyablePoint.getObservation("0") == observationPoint - assert node.keyableLine.getObservation("0") == observationLine - assert node.keyableRectangle.getObservation("0") == observationRectangle - assert node.keyableCircle.getObservation("0") == observationCircle - - # Update attribute observation at key "1" - node.keyablePoint.setObservation("1", {"x" : 2}) - node.keyableLine.setObservation("1", {"a" : {"x" : 2, "y": 2}}) - node.keyableRectangle.setObservation("1", {"center" : {"x" : 20, "y" : 20}}) - node.keyableCircle.setObservation("1", {"radius" : 40}) - - # Check attribute get observation at key "1", should be updated observation - assert node.keyablePoint.getObservation("1").get("x") == 2 - assert node.keyableLine.getObservation("1").get("a") == {"x" : 2, "y": 2} - assert node.keyableRectangle.getObservation("1").get("center") == {"x" : 20, "y" : 20} - assert node.keyableCircle.getObservation("1").get("radius") == 40 - - # Remove attribute observation at key "0" - node.keyablePoint.removeObservation("0") - node.keyableLine.removeObservation("0") - node.keyableRectangle.removeObservation("0") - node.keyableCircle.removeObservation("0") - - # Check attribute has observation at key "0", should be false - assert not node.keyablePoint.hasObservation("0") - assert not node.keyableLine.hasObservation("0") - assert not node.keyableRectangle.hasObservation("0") - assert not node.keyableCircle.hasObservation("0") - - # Reset attribute value - node.keyablePoint.resetToDefaultValue() - node.keyableLine.resetToDefaultValue() - node.keyableRectangle.resetToDefaultValue() - node.keyableCircle.resetToDefaultValue() - - # Check attribute has observation at key "1", should be false - assert not node.keyablePoint.hasObservation("0") - assert not node.keyableLine.hasObservation("0") - assert not node.keyableRectangle.hasObservation("0") - assert not node.keyableCircle.hasObservation("0") - - # Check attribute number of observations, should be 0 - assert node.keyablePoint.nbObservations == 0 - assert node.keyableLine.nbObservations == 0 - assert node.keyableRectangle.nbObservations == 0 - assert node.keyableCircle.nbObservations == 0 + node.keyablePoint.geometry.setObservation("1", observationPoint) + node.keyableLine.geometry.setObservation("1", observationLine) + node.keyableRectangle.geometry.setObservation("1", observationRectangle) + node.keyableCircle.geometry.setObservation("1", observationCircle) + + # Check keyable shape number of observations, should be 2 + assert node.keyablePoint.geometry.nbObservations == 2 + assert node.keyableLine.geometry.nbObservations == 2 + assert node.keyableRectangle.geometry.nbObservations == 2 + assert node.keyableCircle.geometry.nbObservations == 2 + + # Check keyable shape has observation, should be true + assert node.keyablePoint.geometry.hasObservation("0") + assert node.keyablePoint.geometry.hasObservation("1") + assert node.keyableLine.geometry.hasObservation("0") + assert node.keyableLine.geometry.hasObservation("1") + assert node.keyableRectangle.geometry.hasObservation("0") + assert node.keyableRectangle.geometry.hasObservation("1") + assert node.keyableCircle.geometry.hasObservation("0") + assert node.keyableCircle.geometry.hasObservation("1") + + # Check keyable shape get observation at key "0", should be created observation + assert node.keyablePoint.geometry.getObservation("0") == observationPoint + assert node.keyableLine.geometry.getObservation("0") == observationLine + assert node.keyableRectangle.geometry.getObservation("0") == observationRectangle + assert node.keyableCircle.geometry.getObservation("0") == observationCircle + + # Update keyable shape observation at key "1" + node.keyablePoint.geometry.setObservation("1", {"x" : 2}) + node.keyableLine.geometry.setObservation("1", {"a" : {"x" : 2, "y": 2}}) + node.keyableRectangle.geometry.setObservation("1", {"center" : {"x" : 20, "y" : 20}}) + node.keyableCircle.geometry.setObservation("1", {"radius" : 40}) + + # Check keyable shape get observation at key "1", should be updated observation + assert node.keyablePoint.geometry.getObservation("1").get("x") == 2 + assert node.keyableLine.geometry.getObservation("1").get("a") == {"x" : 2, "y": 2} + assert node.keyableRectangle.geometry.getObservation("1").get("center") == {"x" : 20, "y" : 20} + assert node.keyableCircle.geometry.getObservation("1").get("radius") == 40 + + # Remove keyable shape observation at key "0" + node.keyablePoint.geometry.removeObservation("0") + node.keyableLine.geometry.removeObservation("0") + node.keyableRectangle.geometry.removeObservation("0") + node.keyableCircle.geometry.removeObservation("0") + + # Check keyable shape has observation at key "0", should be false + assert not node.keyablePoint.geometry.hasObservation("0") + assert not node.keyableLine.geometry.hasObservation("0") + assert not node.keyableRectangle.geometry.hasObservation("0") + assert not node.keyableCircle.geometry.hasObservation("0") + + # Reset keyable shape geometry + node.keyablePoint.geometry.resetToDefaultValue() + node.keyableLine.geometry.resetToDefaultValue() + node.keyableRectangle.geometry.resetToDefaultValue() + node.keyableCircle.geometry.resetToDefaultValue() + + # Check keyable shape has observation at key "1", should be false + assert not node.keyablePoint.geometry.hasObservation("0") + assert not node.keyableLine.geometry.hasObservation("0") + assert not node.keyableRectangle.geometry.hasObservation("0") + assert not node.keyableCircle.geometry.hasObservation("0") + + # Check keyable shape number of observations, should be 0 + assert node.keyablePoint.geometry.nbObservations == 0 + assert node.keyableLine.geometry.nbObservations == 0 + assert node.keyableRectangle.geometry.nbObservations == 0 + assert node.keyableCircle.geometry.nbObservations == 0 def test_shapeList(self): graph = Graph("") node = graph.addNewNode(NodeWithShapeAttributes.__name__) - pointValue = {"x" : 1, "y" : 1} - keyablePointValue = {} + pointValue = {"userName" : "testPoint", "userColor" : "#fff", "geometry" : {"x" : 1, "y" : 1}} + keyablePointValue = {"userName" : "testKeyablePoint", "userColor" : "#fff", "geometry" : {}} # Check visibility assert node.pointList.isVisible @@ -350,8 +350,8 @@ def test_shapeList(self): assert len(node.keyablePointList) == 3 # Check attribute second element - assert node.pointList.at(1).getValueAsDict() == pointValue - assert node.keyablePointList.at(1).getValueAsDict() == keyablePointValue + assert node.pointList.at(1).geometry.getValueAsDict() == pointValue.get("geometry") + assert node.keyablePointList.at(1).geometry.getValueAsDict() == keyablePointValue.get("geometry") # Change visibility node.pointList.isVisible = False @@ -379,7 +379,8 @@ def test_linkAttribute(self): nodeA = graph.addNewNode(NodeWithShapeAttributes.__name__) nodeB = graph.addNewNode(NodeWithShapeAttributes.__name__) - pointValue = {"x" : 1, "y" : 1} + pointGeometryValue = {"x" : 1, "y" : 1} + pointValue = {"userName" : "testPoint", "userColor" : "#fff", "geometry" : pointGeometryValue} # Add link: # nodeB.pointList is a link for nodeA.pointList @@ -394,28 +395,28 @@ def test_linkAttribute(self): assert nodeB.point.inputLink == nodeA.point # Set observation for nodeA.point - nodeA.point.setObservation("0", pointValue) + nodeA.point.geometry.setObservation("0", pointGeometryValue) # Add 3 shape to nodeA.pointList nodeA.pointList.append(pointValue) nodeA.pointList.append(pointValue) nodeA.pointList.append(pointValue) - # Check nodeB.point - assert nodeB.point.getObservation(0) == pointValue + # Check nodeB.point geometry + assert nodeB.point.geometry.getObservation(0) == pointGeometryValue - # Check nodeB.pointList + # Check nodeB.pointList geometry assert len(nodeB.pointList) == 3 - assert nodeB.pointList.at(0).getValueAsDict() == pointValue - assert nodeB.pointList.at(1).getValueAsDict() == pointValue - assert nodeB.pointList.at(2).getValueAsDict() == pointValue + assert nodeB.pointList.at(0).geometry.getValueAsDict() == pointGeometryValue + assert nodeB.pointList.at(1).geometry.getValueAsDict() == pointGeometryValue + assert nodeB.pointList.at(2).geometry.getValueAsDict() == pointGeometryValue - # Update nodeA.point and nodeA.pointList[1] - nodeA.point.setObservation("0", {"x" : 2}) - nodeA.pointList.at(1).setObservation("0", {"x" : 2}) + # Update nodeA.point and nodeA.pointList[1] geometry + nodeA.point.geometry.setObservation("0", {"x" : 2}) + nodeA.pointList.at(1).geometry.setObservation("0", {"x" : 2}) - # Check nodeB second shape - assert nodeB.point.getObservation("0").get("x") == 2 - assert nodeB.pointList.at(1).getObservation("0").get("x") == 2 + # Check nodeB second shape geometry + assert nodeB.point.geometry.getObservation("0").get("x") == 2 + assert nodeB.pointList.at(1).geometry.getObservation("0").get("x") == 2 # Check serialized value assert nodeB.point.getSerializedValue() == nodeA.point.asLinkExpr() @@ -430,87 +431,90 @@ def test_exportDict(self): observationLine = {"a" : {"x" : 1, "y" : 1}, "b" : {"x" : 2, "y" : 2}} observationRectangle = {"center" : {"x" : 10, "y" : 10}, "size" : {"width" : 20, "height" : 20}} observationCircle = {"center" : {"x" : 10, "y" : 10}, "radius" : 20} - keyablePointValue = {"x" : {"0" : observationPoint.get("x")}, "y" : {"0" : observationPoint.get("y")}} + + pointValue = {"userName" : "testPoint", "userColor" : "#fff", "geometry" : observationPoint} + keyablePointGeometryValue = {"x" : {"0" : observationPoint.get("x")}, "y" : {"0" : observationPoint.get("y")}} + keyablePointValue = {"userName" : "testKeyablePoint", "userColor" : "#fff", "geometry" : keyablePointGeometryValue} # Check uninitialized shape attribute # Shape list attribute should be empty list - assert node.pointList.getValuesAsDicts() == [] - assert node.keyablePointList.getValuesAsDicts() == [] - assert node.pointList.getShapesAsDicts() == [] - assert node.keyablePointList.getShapesAsDicts() == [] - # Not keyable shape attribute should be default - assert node.point.getValueAsDict() == {"x" : -1, "y" : -1} - assert node.line.getValueAsDict() == {"a" : {"x" : -1, "y" : -1}, "b" : {"x" : -1, "y" : -1}} - assert node.rectangle.getValueAsDict() == {"center" : {"x" : -1, "y" : -1}, "size" : {"width" : -1, "height" : -1}} - assert node.circle.getValueAsDict() == {"center" : {"x" : -1, "y" : -1}, "radius" : -1} + assert node.pointList.getGeometriesAsDict() == [] + assert node.keyablePointList.getGeometriesAsDict() == [] + assert node.pointList.getShapesAsDict() == [] + assert node.keyablePointList.getShapesAsDict() == [] + # Static shape attribute should be default + assert node.point.geometry.getValueAsDict() == {"x" : -1, "y" : -1} + assert node.line.geometry.getValueAsDict() == {"a" : {"x" : -1, "y" : -1}, "b" : {"x" : -1, "y" : -1}} + assert node.rectangle.geometry.getValueAsDict() == {"center" : {"x" : -1, "y" : -1}, "size" : {"width" : -1, "height" : -1}} + assert node.circle.geometry.getValueAsDict() == {"center" : {"x" : -1, "y" : -1}, "radius" : -1} assert node.point.getShapeAsDict() == {"name" : node.point.rootName, "type" : node.point.type, - "properties" : {"color" : node.point.shapeColor, "x" : -1, "y" : -1}} + "properties" : {"color" : node.point.userColor, "x" : -1, "y" : -1}} assert node.line.getShapeAsDict() == {"name" : node.line.rootName, "type" : node.line.type, - "properties" : {"color" : node.line.shapeColor, "a" : {"x" : -1, "y" : -1}, "b" : {"x" : -1, "y" : -1}}} + "properties" : {"color" : node.line.userColor, "a" : {"x" : -1, "y" : -1}, "b" : {"x" : -1, "y" : -1}}} assert node.rectangle.getShapeAsDict() == {"name" : node.rectangle.rootName, "type" : node.rectangle.type, - "properties" : {"color" : node.rectangle.shapeColor, "center" : {"x" : -1, "y" : -1}, "size" : {"width" : -1, "height" : -1}}} + "properties" : {"color" : node.rectangle.userColor, "center" : {"x" : -1, "y" : -1}, "size" : {"width" : -1, "height" : -1}}} assert node.circle.getShapeAsDict() == {"name" : node.circle.rootName, "type" : node.circle.type, - "properties" : {"color" : node.circle.shapeColor, "center" : {"x" : -1, "y" : -1}, "radius" : -1}} + "properties" : {"color" : node.circle.userColor, "center" : {"x" : -1, "y" : -1}, "radius" : -1}} # Keyable shape attribute should be empty dict - assert node.keyablePoint.getValueAsDict() == {} - assert node.keyableLine.getValueAsDict() == {} - assert node.keyableRectangle.getValueAsDict() == {} - assert node.keyableCircle.getValueAsDict() == {} + assert node.keyablePoint.geometry.getValueAsDict() == {} + assert node.keyableLine.geometry.getValueAsDict() == {} + assert node.keyableRectangle.geometry.getValueAsDict() == {} + assert node.keyableCircle.geometry.getValueAsDict() == {} assert node.keyablePoint.getShapeAsDict() == {"name" : node.keyablePoint.rootName, "type" : node.keyablePoint.type, - "properties" : {"color" : node.keyablePoint.shapeColor}, + "properties" : {"color" : node.keyablePoint.userColor}, "observations" : {}} assert node.keyableLine.getShapeAsDict() == {"name" : node.keyableLine.rootName, "type" : node.keyableLine.type, - "properties" : {"color" : node.keyableLine.shapeColor}, + "properties" : {"color" : node.keyableLine.userColor}, "observations" : {}} assert node.keyableRectangle.getShapeAsDict() == {"name" : node.keyableRectangle.rootName, "type" : node.keyableRectangle.type, - "properties" : {"color" : node.keyableRectangle.shapeColor}, + "properties" : {"color" : node.keyableRectangle.userColor}, "observations" : {}} assert node.keyableCircle.getShapeAsDict() == {"name" : node.keyableCircle.rootName, "type" : node.keyableCircle.type, - "properties" : {"color" : node.keyableCircle.shapeColor}, + "properties" : {"color" : node.keyableCircle.userColor}, "observations" : {}} # Add one shape with an observation - node.pointList.append(observationPoint) + node.pointList.append(pointValue) node.keyablePointList.append(keyablePointValue) # Add one observation - node.point.setObservation("0", observationPoint) - node.keyablePoint.setObservation("0", observationPoint) - node.line.setObservation("0", observationLine) - node.keyableLine.setObservation("0", observationLine) - node.rectangle.setObservation("0", observationRectangle) - node.keyableRectangle.setObservation("0", observationRectangle) - node.circle.setObservation("0", observationCircle) - node.keyableCircle.setObservation("0", observationCircle) + node.point.geometry.setObservation("0", observationPoint) + node.keyablePoint.geometry.setObservation("0", observationPoint) + node.line.geometry.setObservation("0", observationLine) + node.keyableLine.geometry.setObservation("0", observationLine) + node.rectangle.geometry.setObservation("0", observationRectangle) + node.keyableRectangle.geometry.setObservation("0", observationRectangle) + node.circle.geometry.setObservation("0", observationCircle) + node.keyableCircle.geometry.setObservation("0", observationCircle) # Check shape attribute # Shape list attribute should be empty dict - assert node.pointList.getValuesAsDicts() == [observationPoint] - assert node.keyablePointList.getValuesAsDicts() == [{"0" : observationPoint}] - assert node.pointList.getShapesAsDicts()[0].get("properties") == {"color" : node.keyablePoint.shapeColor} | observationPoint - assert node.keyablePointList.getShapesAsDicts()[0].get("observations") == {"0" : observationPoint} + assert node.pointList.getGeometriesAsDict() == [observationPoint] + assert node.keyablePointList.getGeometriesAsDict() == [{"0" : observationPoint}] + assert node.pointList.getShapesAsDict()[0].get("properties") == {"color" : pointValue.get("userColor")} | observationPoint + assert node.keyablePointList.getShapesAsDict()[0].get("observations") == {"0" : observationPoint} # Not keyable shape attribute should be default - assert node.point.getValueAsDict() == observationPoint - assert node.line.getValueAsDict() == observationLine - assert node.rectangle.getValueAsDict() == observationRectangle - assert node.circle.getValueAsDict() == observationCircle - assert node.point.getShapeAsDict().get("properties") == {"color" : node.point.shapeColor} | observationPoint - assert node.line.getShapeAsDict().get("properties") == {"color" : node.line.shapeColor} | observationLine - assert node.rectangle.getShapeAsDict().get("properties") == {"color" : node.rectangle.shapeColor} | observationRectangle - assert node.circle.getShapeAsDict().get("properties") == {"color" : node.circle.shapeColor} | observationCircle + assert node.point.geometry.getValueAsDict() == observationPoint + assert node.line.geometry.getValueAsDict() == observationLine + assert node.rectangle.geometry.getValueAsDict() == observationRectangle + assert node.circle.geometry.getValueAsDict() == observationCircle + assert node.point.getShapeAsDict().get("properties") == {"color" : node.point.userColor} | observationPoint + assert node.line.getShapeAsDict().get("properties") == {"color" : node.line.userColor} | observationLine + assert node.rectangle.getShapeAsDict().get("properties") == {"color" : node.rectangle.userColor} | observationRectangle + assert node.circle.getShapeAsDict().get("properties") == {"color" : node.circle.userColor} | observationCircle # Keyable shape attribute should be empty dict - assert node.keyablePoint.getValueAsDict() == {"0" : observationPoint} - assert node.keyableLine.getValueAsDict() == {"0" : observationLine} - assert node.keyableRectangle.getValueAsDict() == {"0" : observationRectangle} - assert node.keyableCircle.getValueAsDict() == {"0" : observationCircle} + assert node.keyablePoint.geometry.getValueAsDict() == {"0" : observationPoint} + assert node.keyableLine.geometry.getValueAsDict() == {"0" : observationLine} + assert node.keyableRectangle.geometry.getValueAsDict() == {"0" : observationRectangle} + assert node.keyableCircle.geometry.getValueAsDict() == {"0" : observationCircle} assert node.keyablePoint.getShapeAsDict().get("observations") == {"0" : observationPoint} assert node.keyableLine.getShapeAsDict().get("observations") == {"0" : observationLine} assert node.keyableRectangle.getShapeAsDict().get("observations") == {"0" : observationRectangle}