From df5dc74692446de8393b138f2a30ae3fdd0ea89f Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Fri, 21 Jul 2023 16:29:09 +0200 Subject: [PATCH 01/49] Add error message to Define Joint operator --- phobos/blender/operators/editing.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/phobos/blender/operators/editing.py b/phobos/blender/operators/editing.py index 344da0f3..fcaa5edd 100644 --- a/phobos/blender/operators/editing.py +++ b/phobos/blender/operators/editing.py @@ -1610,8 +1610,9 @@ def execute(self, context): if validInput: for joint in (obj for obj in context.selected_objects if obj.phobostype == 'link'): context.view_layer.objects.active = joint - assert joint.parent is not None and joint.parent.phobostype == "link", \ - f"You need to have a link parented to {joint.name} before you can create a joint" + if joint.parent is None or joint.parent.phobostype != "link": + ErrorMessageWithBox(f"Link {joint.name} has to be parented to another link before you can define a joint") + return {'CANCELLED'} if len(self.name) > 0: joint["joint/name"] = self.name jUtils.setJointConstraints( From 0d5f40f008721cbf7adea66df3a718cd72153f7d Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Mon, 24 Jul 2023 15:52:14 +0200 Subject: [PATCH 02/49] Add missing sensors to defaults.json --- phobos/data/defaults.json | 78 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/phobos/data/defaults.json b/phobos/data/defaults.json index 694d5b31..34994eb3 100644 --- a/phobos/data/defaults.json +++ b/phobos/data/defaults.json @@ -1,6 +1,8 @@ { "sensors": { - "Joint6DOF": {}, + "Joint6DOF": { + "default": {} + }, "RotatingRaySensor": { "default": { "bands": 16, @@ -67,6 +69,80 @@ }, "NodeRotation": { "default": {} + }, + "ScanningSonar": { + "default": { + "width": 64, + "height": 512, + "resolution": 0.1, + "min_distance": 0, + "max_distance": 100.0, + "hud_idx": 0, + "update_rate": 10, + "gain": 1, + "show_cam": false, + "only_ray": false, + "extension": [ + 0.01, + 0.004, + 0.004 + ], + "left_limit": 180, + "right_limit": -180, + "ping_pong_mode": false + } + }, + "MultiLevelLaserRangeFinder": { + "default": { + "num_rays_vertical": 32, + "num_rays_horizontal": 1900, + "rtt_resolution_x": 512, + "rtt_resolution_y": 256, + "vertical_opening_angle": 40, + "horizontal_opening_angle": "2*1899/1900", + "max_distance": 100.0 + } + }, + "LogicalCamera": { + "default": { + "opening_height": 90, + "opening_width": 90} + }, + "JointLoad": { + "default": {} + }, + "JointTorque": { + "default": {} + }, + "JointAVGTorque": { + "default": {} + }, + "Contact": { + "default": {} + }, + "NodeVelocity": { + "default": {} + }, + "NodeAngularVelocity": { + "default": {} + }, + "Altimeter": { + "default": {} + }, + "Magnetometer": { + "default": {} + }, + "GPS": { + "default": {} + }, + "WirelessTransceiver": { + "default": {} + }, + "RFIDSensor": { + "default": {} + }, + "RFIDTag": { + "default": {} } }, "motors": { From acc5cce5bb14c299693b9dd23332379617dce0d5 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Mon, 24 Jul 2023 15:52:28 +0200 Subject: [PATCH 03/49] Update Add Sensor operator ui --- phobos/blender/operators/editing.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/phobos/blender/operators/editing.py b/phobos/blender/operators/editing.py index fcaa5edd..cf241a80 100644 --- a/phobos/blender/operators/editing.py +++ b/phobos/blender/operators/editing.py @@ -2078,11 +2078,6 @@ class AddSensorOperator(Operator): """Add a sensor at the position of the selected object. It is possible to create a new link for the sensor on the fly. Otherwise, the next link in the hierarchy will be used to parent the sensor to. - - Args: - - Returns: - """ bl_idname = "phobos.add_sensor" @@ -2189,7 +2184,7 @@ def draw(self, context): # use the dynamic props name in the GUI, but without the type id self.sensorProperties[i].draw(layout, name) layout.label(text="You can add custom properties under") - layout.label(text="Object Properties > Custom Properties") + layout.label(text="Object Properties > Custom Properties", icon="OBJECT_DATA") def invoke(self, context, event): """ From 2334ccef7fecf61b8a0b5d769d208b157095c9b8 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Tue, 1 Aug 2023 12:07:33 +0200 Subject: [PATCH 04/49] Add dynamicLabel Adds a dynamicLabel with automatic line breaks --- phobos/blender/phobosgui.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/phobos/blender/phobosgui.py b/phobos/blender/phobosgui.py index 8fd2ef20..ceec1f51 100644 --- a/phobos/blender/phobosgui.py +++ b/phobos/blender/phobosgui.py @@ -1623,6 +1623,31 @@ def draw(self, context): dc2.prop(wm, 'phobos_msg_count') dc2.prop(wm, 'phobos_msg_offset') +def dynamicLabel(text, uiLayout, context, icon=None): + panelWidth = context.region.width + print(panelWidth) + uiScale = bpy.context.preferences.view.ui_scale + #margin left, margin right, difference panelWidth -> actual panel width + margin = uiScale*20+uiScale*30+uiScale*72 + letterWidth = uiScale*10.9 + lettersPerLine = (panelWidth - margin) / letterWidth + iconWidth = uiScale*50 + lettersPerIconLine = (panelWidth - margin - iconWidth) / letterWidth + firstLine = True + nextLine = "" + for word in text.split(): + nextLineUpdated = word if nextLine == "" else nextLine + " " + word + if firstLine and icon and len(nextLineUpdated) > lettersPerIconLine: + uiLayout.label(text=nextLine, icon=icon) + nextLine = word + firstLine = False + elif len(nextLineUpdated) > lettersPerLine: + uiLayout.label(text=nextLine) + nextLine = word + else: + nextLine = nextLineUpdated + if nextLine != "": + uiLayout.label(text=nextLine) REGISTER_CLASSES = [ ModelPoseProp, From b9a137da086a9d0b1453137a52cf1e0be45c5c4a Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Tue, 1 Aug 2023 12:14:49 +0200 Subject: [PATCH 05/49] Add documentation to dynamicLabel --- phobos/blender/phobosgui.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/phobos/blender/phobosgui.py b/phobos/blender/phobosgui.py index ceec1f51..e55269cd 100644 --- a/phobos/blender/phobosgui.py +++ b/phobos/blender/phobosgui.py @@ -1624,11 +1624,22 @@ def draw(self, context): dc2.prop(wm, 'phobos_msg_offset') def dynamicLabel(text, uiLayout, context, icon=None): + """ + Prints multiline text to uiLayout.label() + + Args: + text: + uiLayout: bpy.types.UILayout + context: + icon: optional, blender icon name + + Returns: + + """ panelWidth = context.region.width - print(panelWidth) uiScale = bpy.context.preferences.view.ui_scale #margin left, margin right, difference panelWidth -> actual panel width - margin = uiScale*20+uiScale*30+uiScale*72 + margin = uiScale*(20+30+72) letterWidth = uiScale*10.9 lettersPerLine = (panelWidth - margin) / letterWidth iconWidth = uiScale*50 From c6a48425d1186d6900e59e94583980c49d533b68 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Tue, 1 Aug 2023 13:46:15 +0200 Subject: [PATCH 06/49] Fix icon not showing without line break --- phobos/blender/phobosgui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/phobos/blender/phobosgui.py b/phobos/blender/phobosgui.py index e55269cd..725f3f18 100644 --- a/phobos/blender/phobosgui.py +++ b/phobos/blender/phobosgui.py @@ -1640,7 +1640,7 @@ def dynamicLabel(text, uiLayout, context, icon=None): uiScale = bpy.context.preferences.view.ui_scale #margin left, margin right, difference panelWidth -> actual panel width margin = uiScale*(20+30+72) - letterWidth = uiScale*10.9 + letterWidth = uiScale*10.7 lettersPerLine = (panelWidth - margin) / letterWidth iconWidth = uiScale*50 lettersPerIconLine = (panelWidth - margin - iconWidth) / letterWidth @@ -1658,7 +1658,7 @@ def dynamicLabel(text, uiLayout, context, icon=None): else: nextLine = nextLineUpdated if nextLine != "": - uiLayout.label(text=nextLine) + uiLayout.label(text=nextLine, icon=icon if icon and firstLine else "NONE") REGISTER_CLASSES = [ ModelPoseProp, From 6fddd9b7be8a8821400f68a8c39edf620d68b1db Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Wed, 2 Aug 2023 13:56:00 +0200 Subject: [PATCH 07/49] Updates DynamicLabel for operator windows --- phobos/blender/phobosgui.py | 54 ++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/phobos/blender/phobosgui.py b/phobos/blender/phobosgui.py index 725f3f18..3c8a74b0 100644 --- a/phobos/blender/phobosgui.py +++ b/phobos/blender/phobosgui.py @@ -1623,42 +1623,52 @@ def draw(self, context): dc2.prop(wm, 'phobos_msg_count') dc2.prop(wm, 'phobos_msg_offset') -def dynamicLabel(text, uiLayout, context, icon=None): + +def dynamicLabel(text, uiLayout, context=None, width=300, icon=None): """ - Prints multiline text to uiLayout.label() - + Prints multiline text to uiLayout.label(). + Pass context for labels in the phobos side panel and width for operator windows + Args: text: uiLayout: bpy.types.UILayout context: + width: Width passed to context.window_manager.invoke_props_dialog(), default 300 icon: optional, blender icon name Returns: """ - panelWidth = context.region.width + assert context is not None or width > 0 uiScale = bpy.context.preferences.view.ui_scale - #margin left, margin right, difference panelWidth -> actual panel width - margin = uiScale*(20+30+72) - letterWidth = uiScale*10.7 + + panelWidth = 2 * width * uiScale + margin = 2 * uiScale + if context is not None: + panelWidth = context.region.width-uiScale*72 + # margin left, margin right + margin = uiScale * (20 + 30) + letterWidth = uiScale * 10.9 lettersPerLine = (panelWidth - margin) / letterWidth - iconWidth = uiScale*50 + iconWidth = uiScale * 50 lettersPerIconLine = (panelWidth - margin - iconWidth) / letterWidth firstLine = True - nextLine = "" - for word in text.split(): - nextLineUpdated = word if nextLine == "" else nextLine + " " + word - if firstLine and icon and len(nextLineUpdated) > lettersPerIconLine: - uiLayout.label(text=nextLine, icon=icon) - nextLine = word - firstLine = False - elif len(nextLineUpdated) > lettersPerLine: - uiLayout.label(text=nextLine) - nextLine = word - else: - nextLine = nextLineUpdated - if nextLine != "": - uiLayout.label(text=nextLine, icon=icon if icon and firstLine else "NONE") + for line in text.split("\n"): + nextLine = "" + for word in line.split(): + nextLineUpdated = word if nextLine == "" else nextLine + " " + word + if firstLine and icon and len(nextLineUpdated) > lettersPerIconLine: + uiLayout.label(text=nextLine, icon=icon) + nextLine = word + firstLine = False + elif len(nextLineUpdated) > lettersPerLine: + uiLayout.label(text=nextLine) + nextLine = word + else: + nextLine = nextLineUpdated + if nextLine != "": + uiLayout.label(text=nextLine, icon=icon if icon and firstLine else "NONE") + REGISTER_CLASSES = [ ModelPoseProp, From 936174c91899b7827130161736b034987f5eca24 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Wed, 2 Aug 2023 14:57:45 +0200 Subject: [PATCH 08/49] Update dynamicLabel --- phobos/blender/phobosgui.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/phobos/blender/phobosgui.py b/phobos/blender/phobosgui.py index 3c8a74b0..ed103cd8 100644 --- a/phobos/blender/phobosgui.py +++ b/phobos/blender/phobosgui.py @@ -1642,15 +1642,17 @@ def dynamicLabel(text, uiLayout, context=None, width=300, icon=None): assert context is not None or width > 0 uiScale = bpy.context.preferences.view.ui_scale - panelWidth = 2 * width * uiScale - margin = 2 * uiScale if context is not None: - panelWidth = context.region.width-uiScale*72 + panelWidth = context.region.width/uiScale-72 # margin left, margin right - margin = uiScale * (20 + 30) - letterWidth = uiScale * 10.9 + margin = 60 + else: + panelWidth = 2 * width + margin = 12 + + letterWidth = 10.7 lettersPerLine = (panelWidth - margin) / letterWidth - iconWidth = uiScale * 50 + iconWidth = 50 lettersPerIconLine = (panelWidth - margin - iconWidth) / letterWidth firstLine = True for line in text.split("\n"): From eac959451e624aa28082dd4817d46c68d1e254d3 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Mon, 7 Aug 2023 12:30:13 +0200 Subject: [PATCH 09/49] Fix annotation object creation --- phobos/blender/io/phobos2blender.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/phobos/blender/io/phobos2blender.py b/phobos/blender/io/phobos2blender.py index f70a19fc..171fba2f 100644 --- a/phobos/blender/io/phobos2blender.py +++ b/phobos/blender/io/phobos2blender.py @@ -497,9 +497,9 @@ def createAnnotation(ga: representation.GenericAnnotation, parent=None, size=0.1 annot_obj = bUtils.createPrimitive( f"{ga.GA_category}:{ga.GA_name if ga.GA_name is not None else 'unnamed'}", 'box', - [1, 1, 1], + (1, 1, 1), player=defs.layerTypes['annotation'], - plocation=None if parent is None else parent.matrix_world.to_translation(), + #plocation=None if parent is None else parent.matrix_world.to_translation(), #TODO phobostype='annotation' ) annot_obj.scale = (size,) * 3 @@ -525,7 +525,7 @@ def createAnnotation(ga: representation.GenericAnnotation, parent=None, size=0.1 else: annot_obj["$include_parent"] = False - annot_obj["$include_transform"] = ga.GA_transform is None + annot_obj["$include_transform"] = ga._GA_transform is None props = ga.to_yaml() From 07962f7d2c4fdca20ab4d8a16c3d043641b4be10 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Mon, 7 Aug 2023 12:37:32 +0200 Subject: [PATCH 10/49] Update Add annotations menu Add visual size setting Add custom properties --- phobos/blender/operators/editing.py | 3 +- phobos/blender/operators/generic.py | 80 ++++++++++++++++++++++++++--- 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/phobos/blender/operators/editing.py b/phobos/blender/operators/editing.py index 2faff48d..d49a4278 100644 --- a/phobos/blender/operators/editing.py +++ b/phobos/blender/operators/editing.py @@ -43,7 +43,7 @@ from ..model import links as modellinks from ..phobosgui import prev_collections from ..phoboslog import log, ErrorMessageWithBox, WarnMessageWithBox -from ..operators.generic import addObjectFromYaml, DynamicProperty +from ..operators.generic import addObjectFromYaml, DynamicProperty, AddAnnotationsOperator from ..utils import blender as bUtils from ..utils import editing as eUtils from ..utils import general as gUtils @@ -3559,6 +3559,7 @@ def poll(self, context): classes = ( DynamicProperty, + AddAnnotationsOperator, SafelyRemoveObjectsFromSceneOperator, MoveToSceneOperator, SortObjectsToLayersOperator, diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index 4e1c95b4..9da8be98 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -19,6 +19,7 @@ EnumProperty, CollectionProperty, StringProperty, + FloatProperty ) from bpy.types import Operator, PropertyGroup from numpy import isin @@ -31,6 +32,7 @@ from ..utils import io as ioUtils from ..utils import selection as sUtils from ...io import representation +from ..phobosgui import dynamicLabel def linkObjectLists(annotation, objectlist): @@ -448,6 +450,12 @@ class AddAnnotationsOperator(bpy.types.Operator): bl_region_type = 'FILE' bl_options = {'REGISTER', 'UNDO'} + ADD_PROPERTY_TEXT = "Select to add" + + TYPES = [(ADD_PROPERTY_TEXT, -1), ("Text", DynamicProperty.STRING), + ("Number", DynamicProperty.FLOAT), + ("Boolean", DynamicProperty.BOOL)] + category : StringProperty( name="Annotation Type", description="Annotation Types" ) @@ -473,6 +481,33 @@ class AddAnnotationsOperator(bpy.types.Operator): name="Annotation name", description="This name will be used for the annotation, if there are multiple annotations in this category." ) + visual_size : FloatProperty( + name="Visual size", default=1.0 + ) + + def propertyTypes(self, context): + items = [] + for name, id in AddAnnotationsOperator.TYPES: + items.append((name, name, name)) + return items + + add_property_name : StringProperty(name="Name", description="Property name") + + add_property : EnumProperty(name="Type", items=propertyTypes, description='Add property') + + custom_properties: CollectionProperty(type=DynamicProperty) + + def getPropertyTypeID(self, name): + for n, id in AddAnnotationsOperator.TYPES: + if name == n: + return id + + def getPropertyByName(self, name): + for i in range(len(self.custom_properties)): + n = self.custom_properties[i].name + if n == name: + return self.custom_properties[i] + def invoke(self, context, event): """ @@ -500,14 +535,44 @@ def draw(self, context): layout.prop(self, 'multiple') layout.prop(self, 'name') + layout.prop(self, 'visual_size') + layout.prop(self, 'include_parent') layout.prop(self, 'include_transform') - layout.label(text="After you have created the annotation object, you can:\n" - "- Position it in the 3d view\n" - "- Parent it to other objects (as Bone Relative)\n" - "- Define its properties in the custom property panel\n" - " To create nested entries use the prop/nest/key syntax for the property name") + dynamicLabel(text="After you have created the annotation object, you can: \n" + "- Position it in the 3d view \n" + "- Parent it to other objects \n" + "- Define its properties in the custom property panel \n" + " To create nested entries use the prop/nest/key syntax for the property name", + uiLayout=layout) + + layout.separator() + layout.label(text="Custom properties") + + c = layout.split() + c1, c2 = c.column(), c.column() + + if self.add_property != self.ADD_PROPERTY_TEXT: + id = self.getPropertyTypeID(self.add_property) + newName = self.add_property_name + #Check if a property with this name already exists + if newName and self.getPropertyByName(newName) is None: + new_prop = self.custom_properties.add() + new_prop.valueType = id + new_prop.name = newName + else: + c1.alert = True + self.add_property = self.ADD_PROPERTY_TEXT + + c1.prop(self, 'add_property_name') + c2.prop(self, 'add_property') + + for i in range(len(self.custom_properties)): + name = self.custom_properties[i].name + self.custom_properties[i].draw(layout, name) + + def execute(self, context): """ @@ -532,7 +597,8 @@ def execute(self, context): GA_parent_type=parent.phobostype if parent else None, GA_transform=blender2phobos.deriveObjectPose(context.active_object) if context.active_object is not None and self.include_transform else None - ) + ), + size=self.visual_size ) bUtils.toggleLayer('annotation', value=True) return {'FINISHED'} @@ -540,9 +606,7 @@ def execute(self, context): def register(): """TODO Missing documentation""" - bpy.utils.register_class(AddAnnotationsOperator) def unregister(): """TODO Missing documentation""" - bpy.utils.unregister_class(AddAnnotationsOperator) From 8c0a16e466b593396f82c9707b65f7ab4c133022 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Mon, 7 Aug 2023 15:31:09 +0200 Subject: [PATCH 11/49] Add Edit Annotations Operator --- phobos/blender/operators/editing.py | 5 ++++- phobos/blender/operators/generic.py | 35 ++++++++++++++++++++--------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/phobos/blender/operators/editing.py b/phobos/blender/operators/editing.py index d49a4278..a5c5424f 100644 --- a/phobos/blender/operators/editing.py +++ b/phobos/blender/operators/editing.py @@ -43,7 +43,8 @@ from ..model import links as modellinks from ..phobosgui import prev_collections from ..phoboslog import log, ErrorMessageWithBox, WarnMessageWithBox -from ..operators.generic import addObjectFromYaml, DynamicProperty, AddAnnotationsOperator +from ..operators.generic import addObjectFromYaml, DynamicProperty, AddAnnotationsOperator, \ + EditAnnotationsOperator, AnnotationsOperator from ..utils import blender as bUtils from ..utils import editing as eUtils from ..utils import general as gUtils @@ -3559,7 +3560,9 @@ def poll(self, context): classes = ( DynamicProperty, + AnnotationsOperator, AddAnnotationsOperator, + EditAnnotationsOperator, SafelyRemoveObjectsFromSceneOperator, MoveToSceneOperator, SortObjectsToLayersOperator, diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index 9da8be98..b981c8cc 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -34,6 +34,7 @@ from ...io import representation from ..phobosgui import dynamicLabel +annotationEditOnly = False def linkObjectLists(annotation, objectlist): """Recursively adds the objects of the specified list to an annotation dictionary. @@ -441,11 +442,11 @@ def execute(self, context): return operatorBlenderId -class AddAnnotationsOperator(bpy.types.Operator): +class AnnotationsOperator(bpy.types.Operator): """Add annotations defined by the Phobos definitions""" - bl_idname = "phobos.add_annotations" - bl_label = "Add Annotations" + bl_idname = "phobos.annotations" + bl_label = "Annotations" bl_space_type = 'VIEW_3D' bl_region_type = 'FILE' bl_options = {'REGISTER', 'UNDO'} @@ -485,9 +486,11 @@ class AddAnnotationsOperator(bpy.types.Operator): name="Visual size", default=1.0 ) + objectReady = False + def propertyTypes(self, context): items = [] - for name, id in AddAnnotationsOperator.TYPES: + for name, id in AnnotationsOperator.TYPES: items.append((name, name, name)) return items @@ -497,6 +500,11 @@ def propertyTypes(self, context): custom_properties: CollectionProperty(type=DynamicProperty) + modify : BoolProperty( + name="modify", + default=False + ) + def getPropertyTypeID(self, name): for n, id in AddAnnotationsOperator.TYPES: if name == n: @@ -518,6 +526,11 @@ def invoke(self, context, event): Returns: """ + global annotationEditOnly + self.modify = annotationEditOnly + if self.modify: + # TODO collect data + self.objectReady = True return context.window_manager.invoke_props_dialog(self, width=500) def draw(self, context): @@ -540,12 +553,14 @@ def draw(self, context): layout.prop(self, 'include_parent') layout.prop(self, 'include_transform') - dynamicLabel(text="After you have created the annotation object, you can: \n" - "- Position it in the 3d view \n" - "- Parent it to other objects \n" - "- Define its properties in the custom property panel \n" - " To create nested entries use the prop/nest/key syntax for the property name", - uiLayout=layout) + if not self.objectReady: + + dynamicLabel(text="After you have created the annotation object, you can: \n" + "- Position it in the 3d view \n" + "- Parent it to other objects \n" + "- Define its properties in the custom property panel\n" + " To create nested entries use the prop/nest/key syntax for the property name", + uiLayout=layout, width=500) layout.separator() layout.label(text="Custom properties") From 5f659e0241b0a362ca33ee90210b6bb2021660bd Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Mon, 7 Aug 2023 15:52:58 +0200 Subject: [PATCH 12/49] Add Edit Annotations Operator Write custom properties to annotation object --- phobos/blender/operators/generic.py | 84 ++++++++++++++++++++++------- 1 file changed, 66 insertions(+), 18 deletions(-) diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index b981c8cc..0cd16f0e 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -506,7 +506,7 @@ def propertyTypes(self, context): ) def getPropertyTypeID(self, name): - for n, id in AddAnnotationsOperator.TYPES: + for n, id in AnnotationsOperator.TYPES: if name == n: return id @@ -599,23 +599,71 @@ def execute(self, context): """ - parent = None - if not hasattr(context.active_object, "phobostype"): - log("Annotation will not be parented to the active object, as it is no phobos object", "WARN") - elif self.include_parent: - parent = context.active_object - phobos2blender.createAnnotation( - representation.GenericAnnotation( - GA_category=self.category, - GA_name=self.name, - GA_parent=parent if parent else None, - GA_parent_type=parent.phobostype if parent else None, - GA_transform=blender2phobos.deriveObjectPose(context.active_object) - if context.active_object is not None and self.include_transform else None - ), - size=self.visual_size - ) - bUtils.toggleLayer('annotation', value=True) + if not self.modify: + parent = None + if not hasattr(context.active_object, "phobostype"): + log("Annotation will not be parented to the active object, as it is no phobos object", "WARN") + elif self.include_parent: + parent = context.active_object + ob = phobos2blender.createAnnotation( + representation.GenericAnnotation( + GA_category=self.category, + GA_name=self.name, + GA_parent=parent if parent else None, + GA_parent_type=parent.phobostype if parent else None, + GA_transform=blender2phobos.deriveObjectPose(context.active_object) + if context.active_object is not None and self.include_transform else None + ), + size=self.visual_size + ) + bUtils.toggleLayer('annotation', value=True) + + else: + ob = context.active_object + + for i in range(len(self.custom_properties)): + prop = self.custom_properties[i] + name = prop.name + value = prop.getValue() + ob[name] = value + + + self.objectReady = True + return {'FINISHED'} + +class EditAnnotationsOperator(bpy.types.Operator): + """Modify annotations""" + + bl_idname = "phobos.edit_annotations" + bl_label = "Edit Annotations" + bl_space_type = 'VIEW_3D' + bl_region_type = 'FILE' + bl_options = {'REGISTER', 'UNDO'} + + def invoke(self, context, event): + global annotationEditOnly + annotationEditOnly = True + bpy.ops.phobos.annotations('INVOKE_DEFAULT') + return {'FINISHED'} + + @classmethod + def poll(cls, context): + return context.active_object and context.active_object.phobostype == "annotation" + + +class AddAnnotationsOperator(bpy.types.Operator): + """Modify annotations""" + + bl_idname = "phobos.add_annotations" + bl_label = "Add Annotations" + bl_space_type = 'VIEW_3D' + bl_region_type = 'FILE' + bl_options = {'REGISTER', 'UNDO'} + + def invoke(self, context, event): + global annotationEditOnly + annotationEditOnly = False + bpy.ops.phobos.annotations('INVOKE_DEFAULT') return {'FINISHED'} From 45c2e95a0812be49daf79e9794175bc4540836a9 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Tue, 8 Aug 2023 11:50:54 +0200 Subject: [PATCH 13/49] Remember value types --- phobos/blender/operators/generic.py | 36 ++++++++++++++++++++++++++--- phobos/blender/phobosgui.py | 1 + 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index 0cd16f0e..46ab4c79 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -453,6 +453,8 @@ class AnnotationsOperator(bpy.types.Operator): ADD_PROPERTY_TEXT = "Select to add" + ANNOTATION_PREFIX = "anno/" + TYPES = [(ADD_PROPERTY_TEXT, -1), ("Text", DynamicProperty.STRING), ("Number", DynamicProperty.FLOAT), ("Boolean", DynamicProperty.BOOL)] @@ -529,7 +531,31 @@ def invoke(self, context, event): global annotationEditOnly self.modify = annotationEditOnly if self.modify: - # TODO collect data + ob = context.active_object + + # Read name and category + + self.name = ob["GA_name"] + self.category = ob["GA_category"] + + # Read visual size + scale = ob.scale + if len(set(scale)): # All values are equal + self.visual_size = scale[0] + else: + self.visual_size = 0 + + # Read custom properties + for key, value in ob.items(): + if key.startswith(self.ANNOTATION_PREFIX): + valueType, name = key[len(self.ANNOTATION_PREFIX):].split("/", 1) + valueType = int(valueType) + new_prop = self.custom_properties.add() + if valueType == DynamicProperty.FLOAT: + value = float(value) + elif valueType == DynamicProperty.BOOL: + value = bool(value) + new_prop.assignValue(name, value) self.objectReady = True return context.window_manager.invoke_props_dialog(self, width=500) @@ -543,7 +569,7 @@ def draw(self, context): """ layout = self.layout - layout.label(text="The category for your annotation") + layout.label(text="Type (category) and name of your annotation") layout.prop(self, 'category') layout.prop(self, 'multiple') layout.prop(self, 'name') @@ -620,10 +646,14 @@ def execute(self, context): else: ob = context.active_object + # Rescale + if self.visual_size > 0: + ob.scale = (self.visual_size, ) * 3 + # Write custom properties to object for i in range(len(self.custom_properties)): prop = self.custom_properties[i] - name = prop.name + name = self.ANNOTATION_PREFIX+str(prop.valueType)+"/"+prop.name value = prop.getValue() ob[name] = value diff --git a/phobos/blender/phobosgui.py b/phobos/blender/phobosgui.py index ed103cd8..b8fc6d88 100644 --- a/phobos/blender/phobosgui.py +++ b/phobos/blender/phobosgui.py @@ -1168,6 +1168,7 @@ def draw(self, context): c2.operator('phobos.batch_property', text="Edit", icon='GREASEPENCIL') #todo: c2.operator('phobos.copy_props', text="Copy", icon='GHOST') c2.operator("phobos.add_annotations") + c2.operator("phobos.edit_annotations") # Kinematics layout.separator() From e8df7c4379ad3113bc27e17430384bac79e8f041 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Tue, 8 Aug 2023 19:54:38 +0200 Subject: [PATCH 14/49] Allow adding dictionaries in ui --- phobos/blender/operators/editing.py | 3 +- phobos/blender/operators/generic.py | 96 ++++++++++++++++++++++------- 2 files changed, 76 insertions(+), 23 deletions(-) diff --git a/phobos/blender/operators/editing.py b/phobos/blender/operators/editing.py index a5c5424f..a4a0d9fd 100644 --- a/phobos/blender/operators/editing.py +++ b/phobos/blender/operators/editing.py @@ -2197,8 +2197,7 @@ def draw(self, context): for i in range(len(self.sensorProperties)): name = self.sensorProperties[i].name.replace('_', ' ') - # use the dynamic props name in the GUI, but without the type id - self.sensorProperties[i].draw(layout, name) + self.sensorProperties[i].draw(layout, self.sensorProperties) layout.label(text="You can add custom properties under") layout.label(text="Object Properties > Custom Properties") diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index 46ab4c79..af142a07 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -237,12 +237,29 @@ def assignDict(addfunc, dictionary, ignore=[]): return unsupported - def draw(self, layout, name): + @classmethod + def drawAll(cls, properties, layout): """ Args: - layout: - name: + properties: List of DynamicProperties + layout: + + Returns: + + """ + + for i in range(len(properties)): + prop = properties[i] + if not prop.isDictElement(): + prop.draw(layout, properties) + + def draw(self, layout, properties=[]): + """ + + Args: + layout: UILayout to draw to + properties: List of DynamicProperties, to draw children of this property Returns: @@ -255,19 +272,27 @@ def draw(self, layout, name): else: row = layout - if len(self.dictName) > 0: - row.separator(factor=0.03) + # if len(self.dictName) > 0: + # row = row.split(factor=0.03) + # row.separator() if self.valueType == self.INT: - row.prop(self, 'intProp', text=name) + row.prop(self, 'intProp', text=self.name) elif self.valueType == self.BOOL: - row.prop(self, 'boolProp', text=name) + row.prop(self, 'boolProp', text=self.name) elif self.valueType == self.STRING: - row.prop(self, 'stringProp', text=name) + row.prop(self, 'stringProp', text=self.name) elif self.valueType == self.FLOAT: - row.prop(self, 'floatProp', text=name) + row.prop(self, 'floatProp', text=self.name) elif self.valueType == self.DICT: - row.label(text=name+":") + row = row.box() + row.label(text=self.name+":") + numElem = 0 + for prop in properties: + if prop.dictName == self.name: + prop.draw(row, properties) + numElem += 1 + row.label(text=f"{numElem} elements") @staticmethod def collectDict(properties): @@ -287,6 +312,7 @@ def collectDict(properties): return result +# TODO: Delete? def addObjectFromYaml(name, phobtype, presetname, execute_func, *args, hideprops=[]): """This registers a temporary Operator. The data for the properties is provided by the parsed yaml files of the @@ -455,9 +481,12 @@ class AnnotationsOperator(bpy.types.Operator): ANNOTATION_PREFIX = "anno/" + ANNOTATION_ROOT = "Annotation root" + TYPES = [(ADD_PROPERTY_TEXT, -1), ("Text", DynamicProperty.STRING), - ("Number", DynamicProperty.FLOAT), - ("Boolean", DynamicProperty.BOOL)] + ("Number", DynamicProperty.FLOAT), ("Boolean", DynamicProperty.BOOL)] + + TYPES_ROOT = TYPES + [("Dictionary", DynamicProperty.DICT)] category : StringProperty( name="Annotation Type", description="Annotation Types" @@ -492,10 +521,25 @@ class AnnotationsOperator(bpy.types.Operator): def propertyTypes(self, context): items = [] - for name, id in AnnotationsOperator.TYPES: + for name, id in (AnnotationsOperator.TYPES_ROOT if self.add_property_root == + AnnotationsOperator.ANNOTATION_ROOT else AnnotationsOperator.TYPES): items.append((name, name, name)) return items + def propertyRoots(self, context): + roots = [(AnnotationsOperator.ANNOTATION_ROOT, AnnotationsOperator.ANNOTATION_ROOT, + AnnotationsOperator.ANNOTATION_ROOT)] + for i in range(len(self.custom_properties)): + prop = self.custom_properties[i] + if prop.valueType == DynamicProperty.DICT: + roots.append((prop.name, prop.name, "Dictionary "+prop.name)) + return roots + + + add_property_root : EnumProperty(name="Property root", items=propertyRoots, + description='The new property will be added to this dictionary, ' + 'list or the annotation root') + add_property_name : StringProperty(name="Name", description="Property name") add_property : EnumProperty(name="Type", items=propertyTypes, description='Add property') @@ -508,14 +552,18 @@ def propertyTypes(self, context): ) def getPropertyTypeID(self, name): - for n, id in AnnotationsOperator.TYPES: + for n, id in AnnotationsOperator.TYPES_ROOT: if name == n: return id - def getPropertyByName(self, name): + def getPropertyByName(self, name, root): + if root == self.ANNOTATION_ROOT: + root = "" for i in range(len(self.custom_properties)): - n = self.custom_properties[i].name - if n == name: + prop = self.custom_properties[i] + n = prop.name + r = prop.dictName + if n == name and r == root: return self.custom_properties[i] def invoke(self, context, event): @@ -591,6 +639,9 @@ def draw(self, context): layout.separator() layout.label(text="Custom properties") + #if len(self.propertyRoots(context)) > 1: + layout.prop(self, 'add_property_root') + c = layout.split() c1, c2 = c.column(), c.column() @@ -598,10 +649,14 @@ def draw(self, context): id = self.getPropertyTypeID(self.add_property) newName = self.add_property_name #Check if a property with this name already exists - if newName and self.getPropertyByName(newName) is None: + if newName and self.getPropertyByName(newName, self.add_property_root) is None: new_prop = self.custom_properties.add() new_prop.valueType = id new_prop.name = newName + # Assign dict + if self.add_property_root != self.ANNOTATION_ROOT: + new_prop.assignParent(self.add_property_root) + else: c1.alert = True self.add_property = self.ADD_PROPERTY_TEXT @@ -609,9 +664,8 @@ def draw(self, context): c1.prop(self, 'add_property_name') c2.prop(self, 'add_property') - for i in range(len(self.custom_properties)): - name = self.custom_properties[i].name - self.custom_properties[i].draw(layout, name) + DynamicProperty.drawAll(self.custom_properties, layout) + From 41e984de0e3fa6db79e51ae7fd6cd07d3888897c Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Wed, 9 Aug 2023 13:33:58 +0200 Subject: [PATCH 15/49] Store booleans as strings --- phobos/blender/operators/generic.py | 67 +++++++++++++++++------------ 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index af142a07..67c5e54f 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -122,11 +122,12 @@ def assignParent(self, name): """ self.dictName = name - def getValue(self, properties=[]): + def getValue(self, properties=[], boolAsString=False): """ Args: properties: List of all DynamicProperties. Pass this to a dict property. Allows it to collect the data + boolAsString: Returns: This property's value @@ -134,7 +135,10 @@ def getValue(self, properties=[]): if self.valueType == self.INT: return self.intProp elif self.valueType == self.BOOL: - return self.boolProp + if boolAsString: + return "True" if self.boolProp else "False" + else: + return self.boolProp elif self.valueType == self.STRING: return self.stringProp elif self.valueType == self.FLOAT: @@ -143,7 +147,7 @@ def getValue(self, properties=[]): result = {} for prop in properties: if prop.dictName == self.name: - result[prop.name] = prop.getValue() + result[prop.name] = prop.getValue(boolAsString=boolAsString) return result def isDictElement(self): @@ -167,7 +171,7 @@ def allowDisabling(self): """ self.isEnabledOption = True - def assignValue(self, name, value): + def assignValue(self, name, value, boolAsString=False): """ Args: @@ -184,8 +188,12 @@ def assignValue(self, name, value): self.boolProp = value self.valueType = self.BOOL elif isinstance(value, str): - self.stringProp = value - self.valueType = self.STRING + if boolAsString and value in ["True", "False"]: + self.boolProp = True if value == "True" else False + self.valueType = self.BOOL + else: + self.stringProp = value + self.valueType = self.STRING elif isinstance(value, int): self.intProp = value self.valueType = self.INT @@ -208,7 +216,7 @@ def assignValue(self, name, value): self.name = name @staticmethod - def assignDict(addfunc, dictionary, ignore=[]): + def assignDict(addfunc, dictionary, ignore=[], boolAsString=False): """ Args: @@ -225,14 +233,14 @@ def assignDict(addfunc, dictionary, ignore=[]): continue subprop = addfunc() - subprop.assignValue(propname, dictionary[propname]) + subprop.assignValue(propname, dictionary[propname], boolAsString=boolAsString) # add subcategories if isinstance(dictionary[propname], dict): unsupported[propname] = dictionary[propname] for name, value in dictionary[propname].items(): dictprop = addfunc() - dictprop.assignValue(name, value) + dictprop.assignValue(name, value, boolAsString=boolAsString) dictprop.assignParent(propname) return unsupported @@ -479,10 +487,11 @@ class AnnotationsOperator(bpy.types.Operator): ADD_PROPERTY_TEXT = "Select to add" - ANNOTATION_PREFIX = "anno/" - ANNOTATION_ROOT = "Annotation root" + PARAMS = ["$include_parent", "$include_transform", "GA_category", "GA_name", "phobosmatrixinfo", + "phobostype"] + TYPES = [(ADD_PROPERTY_TEXT, -1), ("Text", DynamicProperty.STRING), ("Number", DynamicProperty.FLOAT), ("Boolean", DynamicProperty.BOOL)] @@ -594,16 +603,20 @@ def invoke(self, context, event): self.visual_size = 0 # Read custom properties + data = {} for key, value in ob.items(): - if key.startswith(self.ANNOTATION_PREFIX): - valueType, name = key[len(self.ANNOTATION_PREFIX):].split("/", 1) - valueType = int(valueType) - new_prop = self.custom_properties.add() - if valueType == DynamicProperty.FLOAT: + if key not in self.PARAMS: + if isinstance(value, (int, float)): value = float(value) - elif valueType == DynamicProperty.BOOL: - value = bool(value) - new_prop.assignValue(name, value) + elif isinstance(value, str): + pass + elif isinstance(value, list): + pass + else: + value = value.to_dict() + + data[key] = value + DynamicProperty.assignDict(self.custom_properties.add, data, boolAsString=True) self.objectReady = True return context.window_manager.invoke_props_dialog(self, width=500) @@ -619,7 +632,7 @@ def draw(self, context): layout = self.layout layout.label(text="Type (category) and name of your annotation") layout.prop(self, 'category') - layout.prop(self, 'multiple') + #layout.prop(self, 'multiple_entries') layout.prop(self, 'name') layout.prop(self, 'visual_size') @@ -646,12 +659,12 @@ def draw(self, context): c1, c2 = c.column(), c.column() if self.add_property != self.ADD_PROPERTY_TEXT: - id = self.getPropertyTypeID(self.add_property) + ID = self.getPropertyTypeID(self.add_property) newName = self.add_property_name - #Check if a property with this name already exists + # Check if a property with this name already exists if newName and self.getPropertyByName(newName, self.add_property_root) is None: new_prop = self.custom_properties.add() - new_prop.valueType = id + new_prop.valueType = ID new_prop.name = newName # Assign dict if self.add_property_root != self.ANNOTATION_ROOT: @@ -707,10 +720,10 @@ def execute(self, context): # Write custom properties to object for i in range(len(self.custom_properties)): prop = self.custom_properties[i] - name = self.ANNOTATION_PREFIX+str(prop.valueType)+"/"+prop.name - value = prop.getValue() - ob[name] = value - + if not prop.isDictElement(): + name = prop.name + value = prop.getValue(self.custom_properties, boolAsString=True) + ob[name] = value self.objectReady = True return {'FINISHED'} From c7d12599a6702e617ecb3457462a3c98e4fbbc67 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Wed, 9 Aug 2023 13:57:01 +0200 Subject: [PATCH 16/49] Read/write parameters --- phobos/blender/operators/generic.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index 67c5e54f..1fa0048c 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -590,10 +590,12 @@ def invoke(self, context, event): if self.modify: ob = context.active_object - # Read name and category + # Read parameters self.name = ob["GA_name"] self.category = ob["GA_category"] + self.include_parent = ob["$include_parent"] + self.include_transform = ob["$include_transform"] # Read visual size scale = ob.scale @@ -717,6 +719,12 @@ def execute(self, context): if self.visual_size > 0: ob.scale = (self.visual_size, ) * 3 + # Update parameters + ob["GA_category"] = self.category + ob["GA_name"] = self.name + ob["$include_parent"] = self.include_parent + ob["$include_transform"] = self.include_transform + # Write custom properties to object for i in range(len(self.custom_properties)): prop = self.custom_properties[i] From 3ae569cf56e20f0fe0ed97dd7dfa8bbef4baa878 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Wed, 9 Aug 2023 14:08:05 +0200 Subject: [PATCH 17/49] Hide most properties when closing the pop up --- phobos/blender/operators/generic.py | 71 +++++++++++++++-------------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index 1fa0048c..66b781db 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -508,7 +508,7 @@ class AnnotationsOperator(bpy.types.Operator): ) include_transform : BoolProperty( - name="Includes transformation", default=False, + name="Include transformation", default=False, description="By using the string key $transform you can include the name of the parent link in your annotations.\n" "Using &transform.matrix/position/rotation_euler/quaternion let's you choose in which way it is stored." ) @@ -528,6 +528,8 @@ class AnnotationsOperator(bpy.types.Operator): objectReady = False + isPopUp = True + def propertyTypes(self, context): items = [] for name, id in (AnnotationsOperator.TYPES_ROOT if self.add_property_root == @@ -639,47 +641,49 @@ def draw(self, context): layout.prop(self, 'visual_size') - layout.prop(self, 'include_parent') - layout.prop(self, 'include_transform') + if self.isPopUp: - if not self.objectReady: + layout.prop(self, 'include_parent') + layout.prop(self, 'include_transform') - dynamicLabel(text="After you have created the annotation object, you can: \n" - "- Position it in the 3d view \n" - "- Parent it to other objects \n" - "- Define its properties in the custom property panel\n" - " To create nested entries use the prop/nest/key syntax for the property name", - uiLayout=layout, width=500) + if not self.objectReady: - layout.separator() - layout.label(text="Custom properties") + dynamicLabel(text="After you have created the annotation object, you can: \n" + "- Position it in the 3d view \n" + "- Parent it to other objects \n" + "- Define its properties in the custom property panel\n" + " To create nested entries use the prop/nest/key syntax for the property name", + uiLayout=layout, width=500) - #if len(self.propertyRoots(context)) > 1: - layout.prop(self, 'add_property_root') + layout.separator() + layout.label(text="Custom properties") - c = layout.split() - c1, c2 = c.column(), c.column() + #if len(self.propertyRoots(context)) > 1: + layout.prop(self, 'add_property_root') - if self.add_property != self.ADD_PROPERTY_TEXT: - ID = self.getPropertyTypeID(self.add_property) - newName = self.add_property_name - # Check if a property with this name already exists - if newName and self.getPropertyByName(newName, self.add_property_root) is None: - new_prop = self.custom_properties.add() - new_prop.valueType = ID - new_prop.name = newName - # Assign dict - if self.add_property_root != self.ANNOTATION_ROOT: - new_prop.assignParent(self.add_property_root) + c = layout.split() + c1, c2 = c.column(), c.column() - else: - c1.alert = True - self.add_property = self.ADD_PROPERTY_TEXT + if self.add_property != self.ADD_PROPERTY_TEXT: + ID = self.getPropertyTypeID(self.add_property) + newName = self.add_property_name + # Check if a property with this name already exists + if newName and self.getPropertyByName(newName, self.add_property_root) is None: + new_prop = self.custom_properties.add() + new_prop.valueType = ID + new_prop.name = newName + # Assign dict + if self.add_property_root != self.ANNOTATION_ROOT: + new_prop.assignParent(self.add_property_root) + + else: + c1.alert = True + self.add_property = self.ADD_PROPERTY_TEXT - c1.prop(self, 'add_property_name') - c2.prop(self, 'add_property') + c1.prop(self, 'add_property_name') + c2.prop(self, 'add_property') - DynamicProperty.drawAll(self.custom_properties, layout) + DynamicProperty.drawAll(self.custom_properties, layout) @@ -734,6 +738,7 @@ def execute(self, context): ob[name] = value self.objectReady = True + self.isPopUp = False return {'FINISHED'} class EditAnnotationsOperator(bpy.types.Operator): From c8392f2d53cc43905c8df2e774d8623f80b317d0 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Wed, 9 Aug 2023 17:14:43 +0200 Subject: [PATCH 18/49] Add warning if annotation name in use --- phobos/blender/operators/generic.py | 38 ++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index 66b781db..fa94a6aa 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -577,6 +577,13 @@ def getPropertyByName(self, name, root): if n == name and r == root: return self.custom_properties[i] + @staticmethod + def isObjectNameInUse(name): + for ob in bpy.context.scene.objects: + if ob.name == name: + return True + return False + def invoke(self, context, event): """ @@ -635,9 +642,14 @@ def draw(self, context): """ layout = self.layout layout.label(text="Type (category) and name of your annotation") - layout.prop(self, 'category') + # Check if an annotation with this name already exists + localColumn = layout.column() + if self.isObjectNameInUse(f"{self.category}:{self.name}"): + localColumn.alert = True + localColumn.label(text="An annotation with the same type and name already exists") + localColumn.prop(self, 'category') + localColumn.prop(self, 'name') #layout.prop(self, 'multiple_entries') - layout.prop(self, 'name') layout.prop(self, 'visual_size') @@ -656,14 +668,22 @@ def draw(self, context): uiLayout=layout, width=500) layout.separator() - layout.label(text="Custom properties") + layout.label(text="Add custom properties") + + addBox = layout.box() - #if len(self.propertyRoots(context)) > 1: - layout.prop(self, 'add_property_root') + addBox.prop(self, 'add_property_root') - c = layout.split() + c = addBox.split() c1, c2 = c.column(), c.column() + c1.prop(self, 'add_property_name') + c2.prop(self, 'add_property') + + layout.separator() + + layout.label(text="Custom properties") + if self.add_property != self.ADD_PROPERTY_TEXT: ID = self.getPropertyTypeID(self.add_property) newName = self.add_property_name @@ -680,9 +700,6 @@ def draw(self, context): c1.alert = True self.add_property = self.ADD_PROPERTY_TEXT - c1.prop(self, 'add_property_name') - c2.prop(self, 'add_property') - DynamicProperty.drawAll(self.custom_properties, layout) @@ -704,6 +721,9 @@ def execute(self, context): log("Annotation will not be parented to the active object, as it is no phobos object", "WARN") elif self.include_parent: parent = context.active_object + if self.isObjectNameInUse(f"{self.category}:{self.name}"): + log("Cannot create annotation, name in use", "WARN") + return {'CANCELLED'} ob = phobos2blender.createAnnotation( representation.GenericAnnotation( GA_category=self.category, From 63df02bfdb61cb00536cd6d0cc80c14b446155d4 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Tue, 15 Aug 2023 11:45:53 +0200 Subject: [PATCH 19/49] Hide duplicate name warning when editing --- phobos/blender/operators/generic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index fa94a6aa..50487b11 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -577,11 +577,11 @@ def getPropertyByName(self, name, root): if n == name and r == root: return self.custom_properties[i] - @staticmethod - def isObjectNameInUse(name): + def isObjectNameInUse(self, name): for ob in bpy.context.scene.objects: if ob.name == name: - return True + if ob is not bpy.context.active_object or not self.modify: + return True return False def invoke(self, context, event): From 30b7bff7265d1ca9de40a03d648421cd2e07ef4d Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Tue, 15 Aug 2023 14:47:48 +0200 Subject: [PATCH 20/49] Add makros to ui and object --- phobos/blender/operators/generic.py | 53 +++++++++++++++++++++-------- phobos/io/representation.py | 3 +- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index 50487b11..8e6982db 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -22,7 +22,7 @@ FloatProperty ) from bpy.types import Operator, PropertyGroup -from numpy import isin +import json from .. import defs as defs from ..io import phobos2blender, blender2phobos @@ -93,6 +93,7 @@ class DynamicProperty(PropertyGroup): """A support class to handle dynamic properties in a temporary operator.""" name : bpy.props.StringProperty() + displayName : bpy.props.StringProperty() intProp : bpy.props.IntProperty() boolProp : bpy.props.BoolProperty() stringProp : bpy.props.StringProperty() @@ -214,6 +215,7 @@ def assignValue(self, name, value, boolAsString=False): # TODO what about lists? self.name = name + self.displayName = name @staticmethod def assignDict(addfunc, dictionary, ignore=[], boolAsString=False): @@ -280,21 +282,20 @@ def draw(self, layout, properties=[]): else: row = layout - # if len(self.dictName) > 0: - # row = row.split(factor=0.03) - # row.separator() + if not self.displayName: + self.displayName = self.name if self.valueType == self.INT: - row.prop(self, 'intProp', text=self.name) + row.prop(self, 'intProp', text=self.displayName) elif self.valueType == self.BOOL: - row.prop(self, 'boolProp', text=self.name) + row.prop(self, 'boolProp', text=self.displayName) elif self.valueType == self.STRING: - row.prop(self, 'stringProp', text=self.name) + row.prop(self, 'stringProp', text=self.displayName) elif self.valueType == self.FLOAT: - row.prop(self, 'floatProp', text=self.name) + row.prop(self, 'floatProp', text=self.displayName) elif self.valueType == self.DICT: row = row.box() - row.label(text=self.name+":") + row.label(text=self.displayName+":") numElem = 0 for prop in properties: if prop.dictName == self.name: @@ -490,10 +491,11 @@ class AnnotationsOperator(bpy.types.Operator): ANNOTATION_ROOT = "Annotation root" PARAMS = ["$include_parent", "$include_transform", "GA_category", "GA_name", "phobosmatrixinfo", - "phobostype"] + "phobostype", "GA_makros"] TYPES = [(ADD_PROPERTY_TEXT, -1), ("Text", DynamicProperty.STRING), - ("Number", DynamicProperty.FLOAT), ("Boolean", DynamicProperty.BOOL)] + ("Makro", DynamicProperty.STRING), ("Number", DynamicProperty.FLOAT), + ("Boolean", DynamicProperty.BOOL)] TYPES_ROOT = TYPES + [("Dictionary", DynamicProperty.DICT)] @@ -530,6 +532,8 @@ class AnnotationsOperator(bpy.types.Operator): isPopUp = True + makros = [] + def propertyTypes(self, context): items = [] for name, id in (AnnotationsOperator.TYPES_ROOT if self.add_property_root == @@ -596,6 +600,8 @@ def invoke(self, context, event): """ global annotationEditOnly self.modify = annotationEditOnly + self.objectReady = False + self.makros = [] if self.modify: ob = context.active_object @@ -628,6 +634,18 @@ def invoke(self, context, event): data[key] = value DynamicProperty.assignDict(self.custom_properties.add, data, boolAsString=True) + + # Read makros + self.makros = ob["GA_makros"] + if isinstance(self.makros, str): + self.makros = json.loads(self.makros) + for makro in self.makros: + parent, name = makro + # Find custom property + for prop in self.custom_properties: + if prop.dictName == parent and prop.name == name: + prop.displayName = name+" (Makro)" + self.objectReady = True return context.window_manager.invoke_props_dialog(self, width=500) @@ -684,6 +702,7 @@ def draw(self, context): layout.label(text="Custom properties") + # The user selected a property type if self.add_property != self.ADD_PROPERTY_TEXT: ID = self.getPropertyTypeID(self.add_property) newName = self.add_property_name @@ -696,15 +715,17 @@ def draw(self, context): if self.add_property_root != self.ANNOTATION_ROOT: new_prop.assignParent(self.add_property_root) + # Set as makro + if self.add_property == "Makro": + root = self.add_property_root if self.add_property_root != self.ANNOTATION_ROOT else "" + self.makros.append((root, self.add_property_name)) + new_prop.displayName = new_prop.name+" (Makro)" else: c1.alert = True self.add_property = self.ADD_PROPERTY_TEXT DynamicProperty.drawAll(self.custom_properties, layout) - - - def execute(self, context): """ @@ -731,7 +752,8 @@ def execute(self, context): GA_parent=parent if parent else None, GA_parent_type=parent.phobostype if parent else None, GA_transform=blender2phobos.deriveObjectPose(context.active_object) - if context.active_object is not None and self.include_transform else None + if context.active_object is not None and self.include_transform else None, + GA_makros=self.makros ), size=self.visual_size ) @@ -748,6 +770,7 @@ def execute(self, context): ob["GA_name"] = self.name ob["$include_parent"] = self.include_parent ob["$include_transform"] = self.include_transform + ob["GA_makros"] = self.makros # Write custom properties to object for i in range(len(self.custom_properties)): diff --git a/phobos/io/representation.py b/phobos/io/representation.py index aaf33851..ed49fa56 100644 --- a/phobos/io/representation.py +++ b/phobos/io/representation.py @@ -2186,7 +2186,7 @@ class GenericAnnotation(Representation, SmurfBase): } def __init__(self, GA_category, GA_name=None, GA_parent=None, GA_parent_type=None, GA_transform: Pose=None, - **annotations): + GA_makros="", **annotations): assert (GA_parent is None and GA_parent_type is None) \ or GA_parent_type in ["GA_related_"+str(v) for v in self._class_variables],\ "Unknown GA_parent_type="+str(GA_parent_type) @@ -2196,6 +2196,7 @@ def __init__(self, GA_category, GA_name=None, GA_parent=None, GA_parent_type=Non self._GA_transform = GA_transform self.GA_category = GA_category self.GA_name = GA_name + self.GA_makros = GA_makros for k, v in annotations.items(): setattr(self, "_"+k, v) From 4fa825d9c33d9c2cae1b6e429e96aef3252ff6b2 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Tue, 15 Aug 2023 15:35:03 +0200 Subject: [PATCH 21/49] Fix cannot create annotation without parent link --- phobos/blender/operators/generic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index 8e6982db..d65182ae 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -584,7 +584,7 @@ def getPropertyByName(self, name, root): def isObjectNameInUse(self, name): for ob in bpy.context.scene.objects: if ob.name == name: - if ob is not bpy.context.active_object or not self.modify: + if ob is not bpy.context.active_object or not self.objectReady: return True return False @@ -739,11 +739,11 @@ def execute(self, context): if not self.modify: parent = None if not hasattr(context.active_object, "phobostype"): - log("Annotation will not be parented to the active object, as it is no phobos object", "WARN") + log("Annotation will not be parented to the active object, as it is no phobos object", "WARNING") elif self.include_parent: parent = context.active_object if self.isObjectNameInUse(f"{self.category}:{self.name}"): - log("Cannot create annotation, name in use", "WARN") + log("Cannot create annotation, name in use", "WARNING") return {'CANCELLED'} ob = phobos2blender.createAnnotation( representation.GenericAnnotation( From 604a78561d9b741fe65d45efbf745601a86bb40f Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Tue, 15 Aug 2023 16:03:36 +0200 Subject: [PATCH 22/49] Allow copy of custom properties Allows to copy custom properties of annotations with the same type --- phobos/blender/operators/generic.py | 89 +++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 25 deletions(-) diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index d65182ae..b16ceb67 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -566,6 +566,18 @@ def propertyRoots(self, context): default=False ) + yes : BoolProperty( + name="Yes", + default=False + ) + + no : BoolProperty( + name="No", + default=False + ) + + copyPropertiesAsked = False + def getPropertyTypeID(self, name): for n, id in AnnotationsOperator.TYPES_ROOT: if name == n: @@ -588,6 +600,40 @@ def isObjectNameInUse(self, name): return True return False + def getAnnotationWithType(self, category): + for ob in bpy.context.scene.objects: + if ob.phobostype == "annotation": + if "GA_category" in ob: + if ob["GA_category"] == category: + return ob + + def readCustomProperties(self, ob): + data = {} + for key, value in ob.items(): + if key not in self.PARAMS: + if isinstance(value, (int, float)): + value = float(value) + elif isinstance(value, str): + pass + elif isinstance(value, list): + pass + else: + value = value.to_dict() + + data[key] = value + DynamicProperty.assignDict(self.custom_properties.add, data, boolAsString=True) + + # Read makros + self.makros = ob["GA_makros"] + if isinstance(self.makros, str): + self.makros = json.loads(self.makros) + for makro in self.makros: + parent, name = makro + # Find custom property + for prop in self.custom_properties: + if prop.dictName == parent and prop.name == name: + prop.displayName = name+" (Makro)" + def invoke(self, context, event): """ @@ -602,6 +648,7 @@ def invoke(self, context, event): self.modify = annotationEditOnly self.objectReady = False self.makros = [] + self.copyPropertiesAsked = False if self.modify: ob = context.active_object @@ -620,31 +667,7 @@ def invoke(self, context, event): self.visual_size = 0 # Read custom properties - data = {} - for key, value in ob.items(): - if key not in self.PARAMS: - if isinstance(value, (int, float)): - value = float(value) - elif isinstance(value, str): - pass - elif isinstance(value, list): - pass - else: - value = value.to_dict() - - data[key] = value - DynamicProperty.assignDict(self.custom_properties.add, data, boolAsString=True) - - # Read makros - self.makros = ob["GA_makros"] - if isinstance(self.makros, str): - self.makros = json.loads(self.makros) - for makro in self.makros: - parent, name = makro - # Find custom property - for prop in self.custom_properties: - if prop.dictName == parent and prop.name == name: - prop.displayName = name+" (Makro)" + self.readCustomProperties(ob) self.objectReady = True return context.window_manager.invoke_props_dialog(self, width=500) @@ -669,6 +692,22 @@ def draw(self, context): localColumn.prop(self, 'name') #layout.prop(self, 'multiple_entries') + if self.isPopUp: + + if not self.copyPropertiesAsked: + ancestor = self.getAnnotationWithType(self.category) + if self.yes: + # Copy stuff + self.readCustomProperties(ancestor) + self.copyPropertiesAsked = True + elif self.no: + self.copyPropertiesAsked = True + if ancestor is not None: + layout.label(text="An annotation with this type already exists. Copy parameters?") + split = layout.split() + split.prop(self, 'yes') + split.prop(self, 'no') + layout.prop(self, 'visual_size') if self.isPopUp: From 51710714e6868ba4cf3c990893c9e59e899d4f9c Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Tue, 15 Aug 2023 16:21:28 +0200 Subject: [PATCH 23/49] Update dynamicLabel --- phobos/blender/operators/generic.py | 2 +- phobos/blender/phobosgui.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index b16ceb67..3a9676ba 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -722,7 +722,7 @@ def draw(self, context): "- Parent it to other objects \n" "- Define its properties in the custom property panel\n" " To create nested entries use the prop/nest/key syntax for the property name", - uiLayout=layout, width=500) + uiLayout=layout, width=1000) layout.separator() layout.label(text="Add custom properties") diff --git a/phobos/blender/phobosgui.py b/phobos/blender/phobosgui.py index b8fc6d88..921eddc4 100644 --- a/phobos/blender/phobosgui.py +++ b/phobos/blender/phobosgui.py @@ -1634,13 +1634,12 @@ def dynamicLabel(text, uiLayout, context=None, width=300, icon=None): text: uiLayout: bpy.types.UILayout context: - width: Width passed to context.window_manager.invoke_props_dialog(), default 300 + width: Window width, default 300 icon: optional, blender icon name Returns: """ - assert context is not None or width > 0 uiScale = bpy.context.preferences.view.ui_scale if context is not None: @@ -1648,7 +1647,7 @@ def dynamicLabel(text, uiLayout, context=None, width=300, icon=None): # margin left, margin right margin = 60 else: - panelWidth = 2 * width + panelWidth = width margin = 12 letterWidth = 10.7 From 870eb9c0bcfdfdc7c60679e8472fd7dd9b2e60e4 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Mon, 21 Aug 2023 14:12:29 +0200 Subject: [PATCH 24/49] Export annotations --- phobos/blender/io/blender2phobos.py | 9 +++---- phobos/blender/operators/generic.py | 3 ++- phobos/core/robot.py | 42 +++++++++++++++-------------- phobos/io/representation.py | 15 +++++++---- 4 files changed, 37 insertions(+), 32 deletions(-) diff --git a/phobos/blender/io/blender2phobos.py b/phobos/blender/io/blender2phobos.py index 519db885..0c9637ef 100644 --- a/phobos/blender/io/blender2phobos.py +++ b/phobos/blender/io/blender2phobos.py @@ -541,12 +541,9 @@ def deriveAnnotation(obj): props = misc.deepen_dict(props) - name = obj.split(":")[1] return representation.GenericAnnotation( - GA_category=obj.split(":")[0], - GA_name=name if not name.startswith("unnamed") else None, - GA_parent=obj.parent.get("link/name", obj.parent.name), - GA_parent_type=obj.parent.phobostype, + GA_parent=obj.parent.get("link/name", obj.parent.name) if obj.parent else None, + GA_parent_type=obj.parent.phobostype if obj.parent else None, GA_transform=deriveObjectPose(obj), **props ) @@ -831,7 +828,7 @@ def deriveRobot(root, name='', objectlist=None): # [TODO v2.1.0] Re-add lights and SRDF support for named_annotation in [deriveAnnotation(obj) for obj in objectlist if obj.phobostype == 'annotation']: - robot.add_categorized_annotation(named_annotation["$name"], {k: v for k, v in named_annotation.items() if k.startswith("$")}) + robot.categorized_annotations.append(named_annotation) robot.assert_validity() return robot diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index 3a9676ba..968e8279 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -708,7 +708,8 @@ def draw(self, context): split.prop(self, 'yes') split.prop(self, 'no') - layout.prop(self, 'visual_size') + else: + layout.prop(self, 'visual_size') if self.isPopUp: diff --git a/phobos/core/robot.py b/phobos/core/robot.py index ff84986b..58e0e27d 100755 --- a/phobos/core/robot.py +++ b/phobos/core/robot.py @@ -345,10 +345,10 @@ def export_smurf(self, outputdir=None, outputfile=None, robotfile=None, export_files.append(os.path.split(stream.name)[-1]) # further annotations - for k, v in self.annotations.items(): - if k not in self.smurf_annotation_keys: - with open(os.path.join(smurf_dir, "{}_{}.yml".format(self.name.replace('/','_'), k)), "w+") as stream: - stream.write(dump_json({k: v}, default_style=False)) + for category, annos in self.annotations.items(): + if category not in self.smurf_annotation_keys: + with open(os.path.join(smurf_dir, "{}_{}.yml".format(self.name.replace('/','_'), category)), "w+") as stream: + stream.write(dump_json({category: annos}, default_style=False)) export_files.append(os.path.split(stream.name)[-1]) # submodel list @@ -368,29 +368,31 @@ def export_smurf(self, outputdir=None, outputfile=None, robotfile=None, temp_generic_annotations[ga.GA_category] = [] temp_generic_annotations[ga.GA_category].append({ga.GA_name: ga.to_yaml()} if ga.GA_name is not None else ga.to_yaml()) # clean-up the temporary lists - for k, v in temp_generic_annotations.items(): - if len(v) == 1: - temp_generic_annotations[k] = v[0] - elif len(v) > 1 and all(type(x) == dict and len(x.keys()) == 1 for x in v): - for sub_dict in v: - temp_generic_annotations[k].update(sub_dict) - for k, v in temp_generic_annotations.items(): + for category, annos in temp_generic_annotations.items(): + # If there is only one annotation of this category + if len(annos) == 1: + temp_generic_annotations[category] = annos[0] + # Elif there are more than one and all annotations have a name + elif len(annos) > 1 and all(type(x) == dict and len(x.keys()) == 1 for x in annos): + for sub_dict in annos: + temp_generic_annotations[category].update(sub_dict) + for category, annos in temp_generic_annotations.items(): # deal with existing names - if os.path.isfile(os.path.join(smurf_dir, "{}_{}.yml".format(self.name.replace('/','_'), k))): - k = "generic_annotation_"+k - new_k = k + if os.path.isfile(os.path.join(smurf_dir, "{}_{}.yml".format(self.name.replace('/','_'), category))): + category = "generic_annotation_"+category + new_k = category i = 0 while os.path.isfile(os.path.join(smurf_dir, "{}_{}.yml".format(self.name.replace('/','_'), new_k))): i += 1 - new_k = f"{k}_{i}" - k = new_k + new_k = f"{category}_{i}" + category = new_k # write - if len(v) > 0 and k not in self.smurf_annotation_keys: - with open(os.path.join(smurf_dir, "{}_{}.yml".format(self.name.replace('/','_'), k)), "w") as stream: - stream.write(dump_json({k: v}, default_style=False)) + if len(annos) > 0 and category not in self.smurf_annotation_keys: + with open(os.path.join(smurf_dir, "{}_{}.yml".format(self.name.replace('/','_'), category)), "w") as stream: + stream.write(dump_json({category: annos}, default_style=False)) export_files.append(os.path.split(stream.name)[-1]) - # Create the smurf file itsself + # Create the smurf file itself annotation_dict = { 'modelname': self.name, # 'date': datetime.datetime.now().strftime("%Y%m%d_%H:%M"), diff --git a/phobos/io/representation.py b/phobos/io/representation.py index ed49fa56..ece5c1b6 100644 --- a/phobos/io/representation.py +++ b/phobos/io/representation.py @@ -2188,7 +2188,7 @@ class GenericAnnotation(Representation, SmurfBase): def __init__(self, GA_category, GA_name=None, GA_parent=None, GA_parent_type=None, GA_transform: Pose=None, GA_makros="", **annotations): assert (GA_parent is None and GA_parent_type is None) \ - or GA_parent_type in ["GA_related_"+str(v) for v in self._class_variables],\ + or GA_parent_type in [self._type_dict[v] for v in self._class_variables],\ "Unknown GA_parent_type="+str(GA_parent_type) setattr(self, "GA_related_"+str(GA_parent_type), GA_parent) self._GA_parent_var = "GA_related_"+str(GA_parent_type) @@ -2199,8 +2199,14 @@ def __init__(self, GA_category, GA_name=None, GA_parent=None, GA_parent_type=Non self.GA_makros = GA_makros for k, v in annotations.items(): - setattr(self, "_"+k, v) - + setattr(self, k, v) + + # TODO: Is this required? + # In case it is, replace line above + # setattr(self, k, v) + # with + # setattr(self, "_"+k, v) + """ def _getter(instance, varname=k): value = getattr(instance, "_" + varname) if not self._GA_parent_var.endswith("None") and type(value) == str and value.startswith("$parent."): @@ -2217,8 +2223,7 @@ def _setter(instance, value, varname=k): log.warning(f'{varname} uses the literal: {getattr(instance, "_"+varname)},' f' but you are overriding it with a non literal value: {value}') setattr(self, "_"+k, v) - - setattr(self, k, property(_getter, _setter)) + """ SmurfBase.__init__(self, returns=list(annotations.keys())) From 1e10fe365dcbc5e177baf647517e4c6579a18bae Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Tue, 22 Aug 2023 12:13:55 +0200 Subject: [PATCH 25/49] Fix missing root arg for canonical link --- phobos/io/xmlrobot.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/phobos/io/xmlrobot.py b/phobos/io/xmlrobot.py index 0c81ef9e..20717d04 100644 --- a/phobos/io/xmlrobot.py +++ b/phobos/io/xmlrobot.py @@ -26,7 +26,7 @@ def __init__(self, name=None, version=None, links: List[representation.Link] = N joints: List[representation.Joint] = None, materials: List[representation.Material] = None, transmissions: List[representation.Transmission] = None, - sensors=None, motors=None, plugins=None, + sensors=None, motors=None, plugins=None, root=None, is_human=False, urdf_version=None, xmlfile=None, _xmlfile=None): self._related_robot_instance = self super().__init__() @@ -75,6 +75,9 @@ def __init__(self, name=None, version=None, links: List[representation.Link] = N self.regenerate_tree_maps() if self.links: self.link_entities() + if root is not None: + assert root in [str(l) for l in self.links], "root specified in xml is no link in the robot" + assert root == str(self.get_root()), "root specified in xml is not root of the robot" def __str__(self): return self.name From 19c7fbbbacd0a8273fc72be6cffb7b56e2eb60e4 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Wed, 30 Aug 2023 10:53:11 +0200 Subject: [PATCH 26/49] Allows importing annotations --- phobos/blender/io/phobos2blender.py | 6 +++- phobos/blender/operators/generic.py | 10 +++--- phobos/io/representation.py | 2 +- phobos/io/smurfrobot.py | 47 ++++++++++++++++++++++++++--- 4 files changed, 55 insertions(+), 10 deletions(-) diff --git a/phobos/blender/io/phobos2blender.py b/phobos/blender/io/phobos2blender.py index d13fddb9..0394eb48 100644 --- a/phobos/blender/io/phobos2blender.py +++ b/phobos/blender/io/phobos2blender.py @@ -533,7 +533,7 @@ def createAnnotation(ga: representation.GenericAnnotation, parent=None, size=0.1 props = ga.to_yaml() - for k, v in misc.flatten_dict(props).items(): + for k, v in props.items(): annot_obj[k] = v return annot_obj @@ -587,6 +587,10 @@ def createRobot(robot: core.Robot): for interface in robot.interfaces: newobjects.append(createInterface(interface, newlinks[interface.parent])) + log("Creating annotations...", 'INFO') + for anno in robot.generic_annotations: + newobjects.append(createAnnotation(anno)) + # [TODO v2.1.0] Re-Add SRDF support # log("Creating groups...", 'INFO') # if 'groups' in model and model['groups']: diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index 968e8279..31eb1b1e 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -624,9 +624,11 @@ def readCustomProperties(self, ob): DynamicProperty.assignDict(self.custom_properties.add, data, boolAsString=True) # Read makros - self.makros = ob["GA_makros"] - if isinstance(self.makros, str): - self.makros = json.loads(self.makros) + self.makros = [] + if "GA_makros" in ob: + self.makros = ob["GA_makros"] + if isinstance(self.makros, str): + self.makros = json.loads(self.makros) for makro in self.makros: parent, name = makro # Find custom property @@ -694,7 +696,7 @@ def draw(self, context): if self.isPopUp: - if not self.copyPropertiesAsked: + if not self.copyPropertiesAsked and not self.modify: ancestor = self.getAnnotationWithType(self.category) if self.yes: # Copy stuff diff --git a/phobos/io/representation.py b/phobos/io/representation.py index 8aa23e1e..2fb8616e 100644 --- a/phobos/io/representation.py +++ b/phobos/io/representation.py @@ -2189,7 +2189,7 @@ class GenericAnnotation(Representation, SmurfBase): } def __init__(self, GA_category, GA_name=None, GA_parent=None, GA_parent_type=None, GA_transform: Pose=None, - GA_makros="", **annotations): + GA_makros=[], **annotations): assert (GA_parent is None and GA_parent_type is None) \ or GA_parent_type in [self._type_dict[v] for v in self._class_variables],\ "Unknown GA_parent_type="+str(GA_parent_type) diff --git a/phobos/io/smurfrobot.py b/phobos/io/smurfrobot.py index d65452ca..1687a19c 100644 --- a/phobos/io/smurfrobot.py +++ b/phobos/io/smurfrobot.py @@ -318,6 +318,7 @@ def _init_annotations(self): pop_annotations.append(k) log.info(f"Adding generic annotation of category {k}") if type(v) == list: + raise NotImplementedError for a in v: self.add_aggregate("generic_annotations", representation.GenericAnnotation( GA_category=k, @@ -325,10 +326,48 @@ def _init_annotations(self): **a )) elif type(v) == dict: - self.add_aggregate("generic_annotations", representation.GenericAnnotation( - GA_category=k, - **v - )) + singleAnnotation = False + # Is this a single annotation without a name? + """ + { + "category": { + "keys": values, ... + } + } + """ + # Or is it a set of named annotations? + """ + { + "category": { + "name": { + "keys": values, ... + }, ... + } + } + """ + for name, anno in v.items(): + # If any of these items is not a dict, this is a single unnamed annotation + # Otherwise we assume it is a set of named annotations + if type(anno) != dict: + singleAnnotation = True + if singleAnnotation: + anno = v + anno.pop("GA_category", None) + anno.pop("GA_name", None) + self.add_aggregate("generic_annotations", representation.GenericAnnotation( + GA_category=k, + GA_name="", + **anno + )) + else: + for name, anno in v.items(): + anno.pop("GA_category", None) + anno.pop("GA_name", None) + self.add_aggregate("generic_annotations", representation.GenericAnnotation( + GA_category=k, + GA_name=name, + **anno + )) for k in pop_annotations: self.annotations.pop(k) From 073fcd75f94a4effcb1551d018c92cf9048dba37 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Wed, 30 Aug 2023 11:50:15 +0200 Subject: [PATCH 27/49] Add lists --- phobos/blender/operators/generic.py | 59 +++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index 31eb1b1e..c4ffdb36 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -104,6 +104,7 @@ class DynamicProperty(PropertyGroup): BOOL = 3 FLOAT = 4 DICT = 5 + LIST = 6 valueType : bpy.props.IntProperty() isEnabled : bpy.props.BoolProperty() @@ -128,7 +129,7 @@ def getValue(self, properties=[], boolAsString=False): Args: properties: List of all DynamicProperties. Pass this to a dict property. Allows it to collect the data - boolAsString: + boolAsString: If True, boolean values are converted to strings, "True" or "False" Returns: This property's value @@ -150,6 +151,13 @@ def getValue(self, properties=[], boolAsString=False): if prop.dictName == self.name: result[prop.name] = prop.getValue(boolAsString=boolAsString) return result + elif self.valueType == self.LIST: + result = [] + for prop in properties: + if prop.dictName == self.name: + result.append(prop.getValue(boolAsString=boolAsString)) + return result + def isDictElement(self): """ @@ -176,8 +184,9 @@ def assignValue(self, name, value, boolAsString=False): """ Args: - name: - value: + name: Name of this property + value: Value of this property + boolAsString: If True, the strings "True" and "False" are converted to booleans Returns: @@ -208,11 +217,12 @@ def assignValue(self, name, value, boolAsString=False): self.allowDisabling() elif isinstance(value, dict): self.valueType = self.DICT + elif isinstance(value, list): + self.valueType = self.LIST else: print("DynamicProperty - Unknown type:") print(type(value)) print(value) - # TODO what about lists? self.name = name self.displayName = name @@ -239,11 +249,15 @@ def assignDict(addfunc, dictionary, ignore=[], boolAsString=False): # add subcategories if isinstance(dictionary[propname], dict): - unsupported[propname] = dictionary[propname] for name, value in dictionary[propname].items(): dictprop = addfunc() dictprop.assignValue(name, value, boolAsString=boolAsString) dictprop.assignParent(propname) + elif isinstance(dictionary[propname], list): + for value in dictionary[propname]: + listprop = addfunc() + listprop.assignValue("", value, boolAsString=boolAsString) + listprop.assignParent(propname) return unsupported @@ -301,7 +315,21 @@ def draw(self, layout, properties=[]): if prop.dictName == self.name: prop.draw(row, properties) numElem += 1 - row.label(text=f"{numElem} elements") + row.label(text=f"{numElem} element"+ ("" if numElem == 1 else "s")) + elif self.valueType == self.LIST: + row = row.box() + row.label(text=self.displayName+":") + numElem = 0 + print("Drawing list", self.name) + for prop in properties: + print("Iterating",prop.dictName) + if prop.dictName == self.name: + print("Elem",numElem) + prop.name = str(numElem) + prop.displayName = str(numElem) + prop.draw(row, properties) + numElem += 1 + row.label(text=f"{numElem} element"+ ("" if numElem == 1 else "s")) @staticmethod def collectDict(properties): @@ -497,7 +525,7 @@ class AnnotationsOperator(bpy.types.Operator): ("Makro", DynamicProperty.STRING), ("Number", DynamicProperty.FLOAT), ("Boolean", DynamicProperty.BOOL)] - TYPES_ROOT = TYPES + [("Dictionary", DynamicProperty.DICT)] + TYPES_ROOT = TYPES + [("Dictionary", DynamicProperty.DICT), ("List", DynamicProperty.LIST)] category : StringProperty( name="Annotation Type", description="Annotation Types" @@ -548,6 +576,8 @@ def propertyRoots(self, context): prop = self.custom_properties[i] if prop.valueType == DynamicProperty.DICT: roots.append((prop.name, prop.name, "Dictionary "+prop.name)) + elif prop.valueType == DynamicProperty.LIST: + roots.append((prop.name, prop.name, "List "+prop.name)) return roots @@ -674,6 +704,15 @@ def invoke(self, context, event): self.objectReady = True return context.window_manager.invoke_props_dialog(self, width=500) + def rootType(self): + for i in range(len(self.custom_properties)): + prop = self.custom_properties[i] + if prop.name == self.add_property_root and not prop.isDictElement(): + if prop.valueType == DynamicProperty.DICT: + return dict + if prop.valueType == DynamicProperty.LIST: + return list + def draw(self, context): """ @@ -737,6 +776,9 @@ def draw(self, context): c = addBox.split() c1, c2 = c.column(), c.column() + if self.rootType() == list: + c1.enabled = False + c1.prop(self, 'add_property_name') c2.prop(self, 'add_property') @@ -752,7 +794,8 @@ def draw(self, context): if newName and self.getPropertyByName(newName, self.add_property_root) is None: new_prop = self.custom_properties.add() new_prop.valueType = ID - new_prop.name = newName + if not self.rootType() == list: + new_prop.name = newName # Assign dict if self.add_property_root != self.ANNOTATION_ROOT: new_prop.assignParent(self.add_property_root) From f7aefb5033b58d7e34af41e54f7adfc6ad7aeb03 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Mon, 4 Sep 2023 13:19:44 +0200 Subject: [PATCH 28/49] Allow deletion of custom properties --- phobos/blender/operators/generic.py | 106 ++++++++++++++++++++++------ 1 file changed, 84 insertions(+), 22 deletions(-) diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index c4ffdb36..92c947fb 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -110,6 +110,9 @@ class DynamicProperty(PropertyGroup): isEnabled : bpy.props.BoolProperty() isEnabledOption : bpy.props.BoolProperty() # Whether this property can be disabled + delete : bpy.props.BoolProperty() + deleteOption : bpy.props.BoolProperty() + dictName: bpy.props.StringProperty() def assignParent(self, name): @@ -180,6 +183,16 @@ def allowDisabling(self): """ self.isEnabledOption = True + def allowDeletion(self): + """ + Call to add a button to delete this property + + The functionality has to be implemented by the operator using DynamicProperties + + self.delete is True if the user wants to delete this DynamicProperty + """ + self.deleteOption = True + def assignValue(self, name, value, boolAsString=False): """ @@ -296,6 +309,12 @@ def draw(self, layout, properties=[]): else: row = layout + self.deleteOption = True + if self.deleteOption: + line = layout.split(factor=0.9) + row = line.row() + line.prop(self, 'delete', text="", icon="X", icon_only=True) + if not self.displayName: self.displayName = self.name @@ -608,6 +627,10 @@ def propertyRoots(self, context): copyPropertiesAsked = False + # Used to store properties that were deleted + # These values will be remove from the object when pressing OK + deletedProperties = [] + def getPropertyTypeID(self, name): for n, id in AnnotationsOperator.TYPES_ROOT: if name == n: @@ -713,6 +736,58 @@ def rootType(self): if prop.valueType == DynamicProperty.LIST: return list + def deleteCustomProperties(self): + """Delete properties that are set to be deleted""" + for i in range(len(self.custom_properties)): + prop = self.custom_properties[i] + if prop.delete: + # Remove makro if exists + parent = prop.dictName + name = prop.name + self.removeMakro(parent, name) + # Remember to remove this value from the object + if self.modify and not parent: + self.deletedProperties.append(name) + self.custom_properties.remove(i) + self.deleteCustomProperties() + break + + def removeMakro(self, parent, name): + for i in range(len(self.makros)): + makro = self.makros[i] + p, n = makro + if p == parent and n == name: + del self.makros[i] + break + + def addProperty(self, c1): + # The user selected a property type + if self.add_property != self.ADD_PROPERTY_TEXT: + ID = self.getPropertyTypeID(self.add_property) + newName = self.add_property_name + # Check if a property with this name already exists + if newName and self.getPropertyByName(newName, self.add_property_root) is None: + new_prop = self.custom_properties.add() + new_prop.valueType = ID + if not self.rootType() == list: + new_prop.name = newName + # Assign dict + if self.add_property_root != self.ANNOTATION_ROOT: + new_prop.assignParent(self.add_property_root) + + # Set as makro + if self.add_property == "Makro": + root = self.add_property_root if self.add_property_root != self.ANNOTATION_ROOT else "" + self.makros.append((root, self.add_property_name)) + new_prop.displayName = new_prop.name + " (Makro)" + + # In case this property was deleted before, forget + if self.add_property_root == self.ANNOTATION_ROOT: + self.deletedProperties.remove(newName) + else: + c1.alert = True + self.add_property = self.ADD_PROPERTY_TEXT + def draw(self, context): """ @@ -786,28 +861,9 @@ def draw(self, context): layout.label(text="Custom properties") - # The user selected a property type - if self.add_property != self.ADD_PROPERTY_TEXT: - ID = self.getPropertyTypeID(self.add_property) - newName = self.add_property_name - # Check if a property with this name already exists - if newName and self.getPropertyByName(newName, self.add_property_root) is None: - new_prop = self.custom_properties.add() - new_prop.valueType = ID - if not self.rootType() == list: - new_prop.name = newName - # Assign dict - if self.add_property_root != self.ANNOTATION_ROOT: - new_prop.assignParent(self.add_property_root) - - # Set as makro - if self.add_property == "Makro": - root = self.add_property_root if self.add_property_root != self.ANNOTATION_ROOT else "" - self.makros.append((root, self.add_property_name)) - new_prop.displayName = new_prop.name+" (Makro)" - else: - c1.alert = True - self.add_property = self.ADD_PROPERTY_TEXT + self.deleteCustomProperties() + + self.addProperty(c1) DynamicProperty.drawAll(self.custom_properties, layout) @@ -857,6 +913,12 @@ def execute(self, context): ob["$include_transform"] = self.include_transform ob["GA_makros"] = self.makros + # Remove deleted properties + for i in range(len(self.deletedProperties)): + name = self.deletedProperties[i] + del ob[name] + self.deletedProperties = [] + # Write custom properties to object for i in range(len(self.custom_properties)): prop = self.custom_properties[i] From 02d3077c64dca6cd0bd08684222df7a2d00a4f65 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Mon, 4 Sep 2023 13:30:52 +0200 Subject: [PATCH 29/49] Update UI --- phobos/blender/operators/generic.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index 92c947fb..d53554e9 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -625,6 +625,8 @@ def propertyRoots(self, context): default=False ) + # If this is a new annotation and the type already exists, + # offer to copy the properties from another object once copyPropertiesAsked = False # Used to store properties that were deleted @@ -821,8 +823,8 @@ def draw(self, context): if ancestor is not None: layout.label(text="An annotation with this type already exists. Copy parameters?") split = layout.split() - split.prop(self, 'yes') - split.prop(self, 'no') + split.prop(self, 'yes', icon="CHECKMARK", icon_only=True) + split.prop(self, 'no', icon="CANCEL", icon_only=True) else: layout.prop(self, 'visual_size') From e38e49ab2c57744359af9a033d721fd518f649fc Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Tue, 5 Sep 2023 12:20:28 +0200 Subject: [PATCH 30/49] Resolve $parent in makros --- phobos/blender/io/blender2phobos.py | 30 ++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/phobos/blender/io/blender2phobos.py b/phobos/blender/io/blender2phobos.py index 0c9637ef..04366a64 100644 --- a/phobos/blender/io/blender2phobos.py +++ b/phobos/blender/io/blender2phobos.py @@ -524,6 +524,32 @@ def deriveInterface(obj): **annotations ) +def deriveAnnotationHelper(value, name, parent, obj): + """ + Resolves makros + """ + if "GA_makros" in obj: + if [parent, name] in obj["GA_makros"]: # This is a makro + effParent = sUtils.getEffectiveParent(obj) + if obj["$include_parent"] and effParent: + while "$parent" in value: + index = value.index("$parent") + propertyIndex = index+len("$parent") + # $parent.{prop}, print property value or null + if propertyIndex+2 < len(value) and value[propertyIndex:propertyIndex+2] == ".{": + propertyEndIndex = value.index("}", propertyIndex) + prop = value[propertyIndex+2:propertyEndIndex].strip() + endReplace = propertyEndIndex + if prop in effParent: + replace = str(effParent[prop]) + else: + replace = "null" + else: # $parent without property, print name + endReplace = propertyIndex-1 + replace = effParent.name + value = value[:index]+replace+value[endReplace+1:] + + return value def deriveAnnotation(obj): """Derives the annotation info of an annotation object. @@ -536,7 +562,9 @@ def deriveAnnotation(obj): if hasattr(v, "to_list"): v = v.to_list() elif "PropertyGroup" in repr(type(v)): - v = {_k: _v for _k, _v in v.items()} + v = {_k: deriveAnnotationHelper(_v, _k, k, obj) for _k, _v in v.items()} + else: + v = deriveAnnotationHelper(v, k, "", obj) props[k] = v props = misc.deepen_dict(props) From cebb445f65bced301c43089f3814c7a3069a01ed Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Mon, 18 Sep 2023 12:30:47 +0200 Subject: [PATCH 31/49] Resolve $transform in macros --- phobos/blender/io/blender2phobos.py | 21 +++++++++++++++++++++ phobos/blender/operators/generic.py | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/phobos/blender/io/blender2phobos.py b/phobos/blender/io/blender2phobos.py index 04366a64..28857f8d 100644 --- a/phobos/blender/io/blender2phobos.py +++ b/phobos/blender/io/blender2phobos.py @@ -531,6 +531,7 @@ def deriveAnnotationHelper(value, name, parent, obj): if "GA_makros" in obj: if [parent, name] in obj["GA_makros"]: # This is a makro effParent = sUtils.getEffectiveParent(obj) + # $parent if obj["$include_parent"] and effParent: while "$parent" in value: index = value.index("$parent") @@ -548,6 +549,26 @@ def deriveAnnotationHelper(value, name, parent, obj): endReplace = propertyIndex-1 replace = effParent.name value = value[:index]+replace+value[endReplace+1:] + # $transform + if obj["$include_transform"] and effParent: + while "$transform" in value: + index = value.index("$transform") + indexTail = index+len("$transform") + split = value[indexTail:].split(maxsplit=1) + tail = split[0] + pose = deriveObjectPose(obj, effParent) + if tail[0] != ".": + tail = ".xyz" + replaceEnd = indexTail + else: + replaceEnd = indexTail+len(tail)+1 + try: + replace = getattr(pose, tail[1:]) + except Exception as e: + print(f"Unknown tail {tail} for $transform") + replace = f"transform{tail} (unknown)" + + value = value[:index] + str(replace) + value[replaceEnd:] return value diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index d53554e9..193ffc86 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -784,7 +784,7 @@ def addProperty(self, c1): new_prop.displayName = new_prop.name + " (Makro)" # In case this property was deleted before, forget - if self.add_property_root == self.ANNOTATION_ROOT: + if self.add_property_root == self.ANNOTATION_ROOT and newName in self.deletedProperties: self.deletedProperties.remove(newName) else: c1.alert = True From cb36fe50b3db1aa48873baec7cc138247015ed96 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Mon, 18 Sep 2023 12:42:22 +0200 Subject: [PATCH 32/49] Replace makro by macro --- phobos/blender/io/blender2phobos.py | 6 ++-- phobos/blender/operators/generic.py | 52 ++++++++++++++--------------- phobos/io/representation.py | 4 +-- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/phobos/blender/io/blender2phobos.py b/phobos/blender/io/blender2phobos.py index 28857f8d..71695b06 100644 --- a/phobos/blender/io/blender2phobos.py +++ b/phobos/blender/io/blender2phobos.py @@ -526,10 +526,10 @@ def deriveInterface(obj): def deriveAnnotationHelper(value, name, parent, obj): """ - Resolves makros + Resolves macros """ - if "GA_makros" in obj: - if [parent, name] in obj["GA_makros"]: # This is a makro + if "GA_macros" in obj: + if [parent, name] in obj["GA_macros"]: # This is a macro effParent = sUtils.getEffectiveParent(obj) # $parent if obj["$include_parent"] and effParent: diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index 193ffc86..d29891fd 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -538,10 +538,10 @@ class AnnotationsOperator(bpy.types.Operator): ANNOTATION_ROOT = "Annotation root" PARAMS = ["$include_parent", "$include_transform", "GA_category", "GA_name", "phobosmatrixinfo", - "phobostype", "GA_makros"] + "phobostype", "GA_macros"] TYPES = [(ADD_PROPERTY_TEXT, -1), ("Text", DynamicProperty.STRING), - ("Makro", DynamicProperty.STRING), ("Number", DynamicProperty.FLOAT), + ("Macro", DynamicProperty.STRING), ("Number", DynamicProperty.FLOAT), ("Boolean", DynamicProperty.BOOL)] TYPES_ROOT = TYPES + [("Dictionary", DynamicProperty.DICT), ("List", DynamicProperty.LIST)] @@ -579,7 +579,7 @@ class AnnotationsOperator(bpy.types.Operator): isPopUp = True - makros = [] + macros = [] def propertyTypes(self, context): items = [] @@ -678,18 +678,18 @@ def readCustomProperties(self, ob): data[key] = value DynamicProperty.assignDict(self.custom_properties.add, data, boolAsString=True) - # Read makros - self.makros = [] - if "GA_makros" in ob: - self.makros = ob["GA_makros"] - if isinstance(self.makros, str): - self.makros = json.loads(self.makros) - for makro in self.makros: - parent, name = makro + # Read macros + self.macroList = [] + if "GA_macros" in ob: + self.macroList = ob["GA_macros"] + if isinstance(self.macroList, str): + self.macroList = json.loads(self.macroList) + for macro in self.macroList: + parent, name = macro # Find custom property for prop in self.custom_properties: if prop.dictName == parent and prop.name == name: - prop.displayName = name+" (Makro)" + prop.displayName = name+" (Macro)" def invoke(self, context, event): """ @@ -704,7 +704,7 @@ def invoke(self, context, event): global annotationEditOnly self.modify = annotationEditOnly self.objectReady = False - self.makros = [] + self.macroList = [] self.copyPropertiesAsked = False if self.modify: ob = context.active_object @@ -743,10 +743,10 @@ def deleteCustomProperties(self): for i in range(len(self.custom_properties)): prop = self.custom_properties[i] if prop.delete: - # Remove makro if exists + # Remove macro if exists parent = prop.dictName name = prop.name - self.removeMakro(parent, name) + self.removeMacro(parent, name) # Remember to remove this value from the object if self.modify and not parent: self.deletedProperties.append(name) @@ -754,12 +754,12 @@ def deleteCustomProperties(self): self.deleteCustomProperties() break - def removeMakro(self, parent, name): - for i in range(len(self.makros)): - makro = self.makros[i] - p, n = makro + def removeMacro(self, parent, name): + for i in range(len(self.macroList)): + macro = self.macroList[i] + p, n = macro if p == parent and n == name: - del self.makros[i] + del self.macroList[i] break def addProperty(self, c1): @@ -777,11 +777,11 @@ def addProperty(self, c1): if self.add_property_root != self.ANNOTATION_ROOT: new_prop.assignParent(self.add_property_root) - # Set as makro - if self.add_property == "Makro": + # Set as macro + if self.add_property == "Macro": root = self.add_property_root if self.add_property_root != self.ANNOTATION_ROOT else "" - self.makros.append((root, self.add_property_name)) - new_prop.displayName = new_prop.name + " (Makro)" + self.macroList.append((root, self.add_property_name)) + new_prop.displayName = new_prop.name + " (Macro)" # In case this property was deleted before, forget if self.add_property_root == self.ANNOTATION_ROOT and newName in self.deletedProperties: @@ -896,7 +896,7 @@ def execute(self, context): GA_parent_type=parent.phobostype if parent else None, GA_transform=blender2phobos.deriveObjectPose(context.active_object) if context.active_object is not None and self.include_transform else None, - GA_makros=self.makros + GA_macros=self.macroList ), size=self.visual_size ) @@ -913,7 +913,7 @@ def execute(self, context): ob["GA_name"] = self.name ob["$include_parent"] = self.include_parent ob["$include_transform"] = self.include_transform - ob["GA_makros"] = self.makros + ob["GA_macros"] = self.macroList # Remove deleted properties for i in range(len(self.deletedProperties)): diff --git a/phobos/io/representation.py b/phobos/io/representation.py index 2fb8616e..c34585fe 100644 --- a/phobos/io/representation.py +++ b/phobos/io/representation.py @@ -2189,7 +2189,7 @@ class GenericAnnotation(Representation, SmurfBase): } def __init__(self, GA_category, GA_name=None, GA_parent=None, GA_parent_type=None, GA_transform: Pose=None, - GA_makros=[], **annotations): + GA_macros=[], **annotations): assert (GA_parent is None and GA_parent_type is None) \ or GA_parent_type in [self._type_dict[v] for v in self._class_variables],\ "Unknown GA_parent_type="+str(GA_parent_type) @@ -2199,7 +2199,7 @@ def __init__(self, GA_category, GA_name=None, GA_parent=None, GA_parent_type=Non self._GA_transform = GA_transform self.GA_category = GA_category self.GA_name = GA_name - self.GA_makros = GA_makros + self.GA_macros = GA_macros for k, v in annotations.items(): setattr(self, k, v) From a5dca0e9a92ad8bd0f60b7710bd2a24fe01540c4 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Mon, 18 Sep 2023 15:39:51 +0200 Subject: [PATCH 33/49] Remove annotations SDF icon from resources --- phobos/data/blender/resources.blend | Bin 1879108 -> 1832072 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/phobos/data/blender/resources.blend b/phobos/data/blender/resources.blend index 56c1a3e16603ce9ba8216519ae491e1e5d95d3eb..2c922f2fae8366c1622d26d3235755692f86ee7a 100644 GIT binary patch delta 103393 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
  • 0Dpky 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 delta 142823 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 From 895cbfc5e335c465e32be030799e5c9f75820705 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Tue, 19 Sep 2023 16:44:04 +0200 Subject: [PATCH 34/49] Prevents renaming of generic annotation categories Overwrites existing files --- phobos/core/robot.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/phobos/core/robot.py b/phobos/core/robot.py index 58e0e27d..158c96ea 100755 --- a/phobos/core/robot.py +++ b/phobos/core/robot.py @@ -377,18 +377,9 @@ def export_smurf(self, outputdir=None, outputfile=None, robotfile=None, for sub_dict in annos: temp_generic_annotations[category].update(sub_dict) for category, annos in temp_generic_annotations.items(): - # deal with existing names - if os.path.isfile(os.path.join(smurf_dir, "{}_{}.yml".format(self.name.replace('/','_'), category))): - category = "generic_annotation_"+category - new_k = category - i = 0 - while os.path.isfile(os.path.join(smurf_dir, "{}_{}.yml".format(self.name.replace('/','_'), new_k))): - i += 1 - new_k = f"{category}_{i}" - category = new_k # write if len(annos) > 0 and category not in self.smurf_annotation_keys: - with open(os.path.join(smurf_dir, "{}_{}.yml".format(self.name.replace('/','_'), category)), "w") as stream: + with open(os.path.join(smurf_dir, "{}_generic_annotation_{}.yml".format(self.name.replace('/','_'), category)), "w") as stream: stream.write(dump_json({category: annos}, default_style=False)) export_files.append(os.path.split(stream.name)[-1]) From 0db8b33bfe94d146b775264807b4ec3ca054949a Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Wed, 20 Sep 2023 11:40:49 +0200 Subject: [PATCH 35/49] Fix export of multiple annotations in same category --- phobos/core/robot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/phobos/core/robot.py b/phobos/core/robot.py index 158c96ea..9b51de60 100755 --- a/phobos/core/robot.py +++ b/phobos/core/robot.py @@ -374,6 +374,7 @@ def export_smurf(self, outputdir=None, outputfile=None, robotfile=None, temp_generic_annotations[category] = annos[0] # Elif there are more than one and all annotations have a name elif len(annos) > 1 and all(type(x) == dict and len(x.keys()) == 1 for x in annos): + temp_generic_annotations[category] = {} for sub_dict in annos: temp_generic_annotations[category].update(sub_dict) for category, annos in temp_generic_annotations.items(): From 88f9a053a9cedd580f1e5593bbe47a5d4f3a16e7 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Wed, 20 Sep 2023 14:22:27 +0200 Subject: [PATCH 36/49] Remove "generic_annotation" from annotation file names --- phobos/core/robot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phobos/core/robot.py b/phobos/core/robot.py index 9b51de60..27f49ca9 100755 --- a/phobos/core/robot.py +++ b/phobos/core/robot.py @@ -380,7 +380,7 @@ def export_smurf(self, outputdir=None, outputfile=None, robotfile=None, for category, annos in temp_generic_annotations.items(): # write if len(annos) > 0 and category not in self.smurf_annotation_keys: - with open(os.path.join(smurf_dir, "{}_generic_annotation_{}.yml".format(self.name.replace('/','_'), category)), "w") as stream: + with open(os.path.join(smurf_dir, "{}_{}.yml".format(self.name.replace('/','_'), category)), "w") as stream: stream.write(dump_json({category: annos}, default_style=False)) export_files.append(os.path.split(stream.name)[-1]) From 8d1f75259e6aff94bf2afbf2b6a00d8ce73090f3 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Wed, 20 Sep 2023 17:28:12 +0200 Subject: [PATCH 37/49] Parent annotations on import --- phobos/core/robot.py | 7 ++++++- phobos/io/representation.py | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/phobos/core/robot.py b/phobos/core/robot.py index 27f49ca9..b93e4152 100755 --- a/phobos/core/robot.py +++ b/phobos/core/robot.py @@ -366,7 +366,12 @@ def export_smurf(self, outputdir=None, outputfile=None, robotfile=None, for ga in self.categorized_annotations: if ga.GA_category not in temp_generic_annotations: temp_generic_annotations[ga.GA_category] = [] - temp_generic_annotations[ga.GA_category].append({ga.GA_name: ga.to_yaml()} if ga.GA_name is not None else ga.to_yaml()) + gaYaml = ga.to_yaml() + # Add parent and parent type to export + # TODO: Check if type has to be stored + gaYaml["GA_parent"] = ga.GA_parent + gaYaml["GA_parent_type"] = ga.GA_parent_type + temp_generic_annotations[ga.GA_category].append({ga.GA_name: gaYaml} if ga.GA_name is not None else gaYaml) # clean-up the temporary lists for category, annos in temp_generic_annotations.items(): # If there is only one annotation of this category diff --git a/phobos/io/representation.py b/phobos/io/representation.py index c34585fe..2ee0d2b4 100644 --- a/phobos/io/representation.py +++ b/phobos/io/representation.py @@ -2200,6 +2200,9 @@ def __init__(self, GA_category, GA_name=None, GA_parent=None, GA_parent_type=Non self.GA_category = GA_category self.GA_name = GA_name self.GA_macros = GA_macros + # Store parent type for export + # TODO: Check if type has to be stored + self.GA_parent_type = GA_parent_type for k, v in annotations.items(): setattr(self, k, v) From b8241d5b9ec6135d239f86e59f57b6f4d679dfe4 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Wed, 20 Sep 2023 17:58:20 +0200 Subject: [PATCH 38/49] Don't save annotation values that are not required --- phobos/blender/operators/generic.py | 2 +- phobos/blender/reserved_keys.py | 2 +- phobos/core/robot.py | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index d29891fd..c5e3ec84 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -538,7 +538,7 @@ class AnnotationsOperator(bpy.types.Operator): ANNOTATION_ROOT = "Annotation root" PARAMS = ["$include_parent", "$include_transform", "GA_category", "GA_name", "phobosmatrixinfo", - "phobostype", "GA_macros"] + "phobostype", "GA_macros", "GA_parent_type"] TYPES = [(ADD_PROPERTY_TEXT, -1), ("Text", DynamicProperty.STRING), ("Macro", DynamicProperty.STRING), ("Number", DynamicProperty.FLOAT), diff --git a/phobos/blender/reserved_keys.py b/phobos/blender/reserved_keys.py index 5fa8373c..bca5c17d 100644 --- a/phobos/blender/reserved_keys.py +++ b/phobos/blender/reserved_keys.py @@ -7,7 +7,7 @@ MOTOR_KEYS = ["name", "type", "joint", "maxSpeed", "maxValue", "maxEffort", "minValue"] INTERFACE_KEYS = ["name", "type", "direction", "parent", "origin", "position", "rotation"] INTERNAL_KEYS = ["phobostype", "phobosmatrixinfo"] -ANNOTATION_KEYS = ["$name", "$category", "$include_parent", "$include_transform", "$transform"] +ANNOTATION_KEYS = ["GA_name", "GA_category", "$include_parent", "$include_transform", "GA_macros"] SUBMECHANISM_KEYS = ["type", "jointnames_spanningtree", "jointnames_active", "jointnames_independent", "jointnames"] SENSOR_KEYS = ["name", "position_offset", "orientation_offset", "origin"] diff --git a/phobos/core/robot.py b/phobos/core/robot.py index b93e4152..8f23846a 100755 --- a/phobos/core/robot.py +++ b/phobos/core/robot.py @@ -19,6 +19,7 @@ from ..utils.transform import create_transformation, inv, get_adjoint, round_array from ..utils.tree import find_close_ancestor_links, get_joints from ..utils.xml import transform_object, get_joint_info_dict +from ..blender import reserved_keys log = get_logger(__name__) @@ -371,6 +372,9 @@ def export_smurf(self, outputdir=None, outputfile=None, robotfile=None, # TODO: Check if type has to be stored gaYaml["GA_parent"] = ga.GA_parent gaYaml["GA_parent_type"] = ga.GA_parent_type + # Remove keys that are not required + for key in reserved_keys.ANNOTATION_KEYS: + gaYaml.pop(key, None) temp_generic_annotations[ga.GA_category].append({ga.GA_name: gaYaml} if ga.GA_name is not None else gaYaml) # clean-up the temporary lists for category, annos in temp_generic_annotations.items(): From 737d814191c443dbdea7e7257888d6a565d90c02 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Wed, 20 Sep 2023 18:35:00 +0200 Subject: [PATCH 39/49] Fix cannot parent annotation on creation --- phobos/blender/operators/generic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index c5e3ec84..1873260d 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -803,7 +803,7 @@ def draw(self, context): layout.label(text="Type (category) and name of your annotation") # Check if an annotation with this name already exists localColumn = layout.column() - if self.isObjectNameInUse(f"{self.category}:{self.name}"): + if not self.objectReady and self.isObjectNameInUse(f"{self.category}:{self.name}"): localColumn.alert = True localColumn.label(text="An annotation with the same type and name already exists") localColumn.prop(self, 'category') @@ -883,7 +883,7 @@ def execute(self, context): parent = None if not hasattr(context.active_object, "phobostype"): log("Annotation will not be parented to the active object, as it is no phobos object", "WARNING") - elif self.include_parent: + else: parent = context.active_object if self.isObjectNameInUse(f"{self.category}:{self.name}"): log("Cannot create annotation, name in use", "WARNING") @@ -892,7 +892,7 @@ def execute(self, context): representation.GenericAnnotation( GA_category=self.category, GA_name=self.name, - GA_parent=parent if parent else None, + GA_parent=parent.name if parent else None, GA_parent_type=parent.phobostype if parent else None, GA_transform=blender2phobos.deriveObjectPose(context.active_object) if context.active_object is not None and self.include_transform else None, From 9fd9148a86cd648d863b304409e28fda1307c659 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Wed, 20 Sep 2023 18:36:08 +0200 Subject: [PATCH 40/49] Rename operators --- phobos/blender/operators/generic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index 1873260d..379d275f 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -937,7 +937,7 @@ class EditAnnotationsOperator(bpy.types.Operator): """Modify annotations""" bl_idname = "phobos.edit_annotations" - bl_label = "Edit Annotations" + bl_label = "Edit Annotation" bl_space_type = 'VIEW_3D' bl_region_type = 'FILE' bl_options = {'REGISTER', 'UNDO'} @@ -957,7 +957,7 @@ class AddAnnotationsOperator(bpy.types.Operator): """Modify annotations""" bl_idname = "phobos.add_annotations" - bl_label = "Add Annotations" + bl_label = "Add Annotation" bl_space_type = 'VIEW_3D' bl_region_type = 'FILE' bl_options = {'REGISTER', 'UNDO'} From 22c455ddfda84250f0d0fbfb2f81859fb97567a9 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Wed, 20 Sep 2023 18:51:34 +0200 Subject: [PATCH 41/49] Fixes --- phobos/blender/io/phobos2blender.py | 3 +++ phobos/blender/operators/generic.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/phobos/blender/io/phobos2blender.py b/phobos/blender/io/phobos2blender.py index 0394eb48..fb3d8441 100644 --- a/phobos/blender/io/phobos2blender.py +++ b/phobos/blender/io/phobos2blender.py @@ -533,6 +533,9 @@ def createAnnotation(ga: representation.GenericAnnotation, parent=None, size=0.1 props = ga.to_yaml() + # TODO: Check if type has to be stored + props.pop("GA_parent_type", None) + for k, v in props.items(): annot_obj[k] = v diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index 379d275f..9b92de6e 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -918,7 +918,8 @@ def execute(self, context): # Remove deleted properties for i in range(len(self.deletedProperties)): name = self.deletedProperties[i] - del ob[name] + if name in ob: + del ob[name] self.deletedProperties = [] # Write custom properties to object From e51e216a00b3211507f78fda4cd7d145a8eade4d Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Mon, 25 Sep 2023 12:26:33 +0200 Subject: [PATCH 42/49] Format transform macro --- phobos/blender/io/blender2phobos.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/phobos/blender/io/blender2phobos.py b/phobos/blender/io/blender2phobos.py index 71695b06..e644c019 100644 --- a/phobos/blender/io/blender2phobos.py +++ b/phobos/blender/io/blender2phobos.py @@ -561,10 +561,19 @@ def deriveAnnotationHelper(value, name, parent, obj): tail = ".xyz" replaceEnd = indexTail else: - replaceEnd = indexTail+len(tail)+1 + replaceEnd = indexTail+len(tail) try: replace = getattr(pose, tail[1:]) - except Exception as e: + if type(replace) == tuple: + string = "" + for i in range(len(replace)): + v = replace[i] + v = str(v) + if i > 0: + string += ", " + string += v + replace = "(" + string + ")" + except AttributeError as e: print(f"Unknown tail {tail} for $transform") replace = f"transform{tail} (unknown)" From 7c5e783f148bce7285da79745df5c7652ba7a4a4 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Mon, 25 Sep 2023 15:07:30 +0200 Subject: [PATCH 43/49] Update Annotation panel text --- phobos/blender/operators/generic.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index 9b92de6e..bac5f537 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -839,8 +839,7 @@ def draw(self, context): dynamicLabel(text="After you have created the annotation object, you can: \n" "- Position it in the 3d view \n" "- Parent it to other objects \n" - "- Define its properties in the custom property panel\n" - " To create nested entries use the prop/nest/key syntax for the property name", + "Define custom properties in the panel below", uiLayout=layout, width=1000) layout.separator() From 76fdf77b1a13cf56c5ec33636227b1bfd85e31db Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Mon, 25 Sep 2023 15:11:59 +0200 Subject: [PATCH 44/49] Only show visual size after creation --- phobos/blender/operators/generic.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index bac5f537..d72f36e0 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -800,17 +800,17 @@ def draw(self, context): """ layout = self.layout - layout.label(text="Type (category) and name of your annotation") - # Check if an annotation with this name already exists - localColumn = layout.column() - if not self.objectReady and self.isObjectNameInUse(f"{self.category}:{self.name}"): - localColumn.alert = True - localColumn.label(text="An annotation with the same type and name already exists") - localColumn.prop(self, 'category') - localColumn.prop(self, 'name') - #layout.prop(self, 'multiple_entries') if self.isPopUp: + layout.label(text="Type (category) and name of your annotation") + # Check if an annotation with this name already exists + localColumn = layout.column() + if not self.objectReady and self.isObjectNameInUse(f"{self.category}:{self.name}"): + localColumn.alert = True + localColumn.label(text="An annotation with the same type and name already exists") + localColumn.prop(self, 'category') + localColumn.prop(self, 'name') + #layout.prop(self, 'multiple_entries') if not self.copyPropertiesAsked and not self.modify: ancestor = self.getAnnotationWithType(self.category) @@ -827,6 +827,7 @@ def draw(self, context): split.prop(self, 'no', icon="CANCEL", icon_only=True) else: + layout.label(text="Modify the visual size of this annotation") layout.prop(self, 'visual_size') if self.isPopUp: From 70a68bf9b068d5009894209dede1be4582a0c6f4 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Mon, 25 Sep 2023 16:36:52 +0200 Subject: [PATCH 45/49] Remove GPS duplicate --- phobos/data/defaults.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/phobos/data/defaults.json b/phobos/data/defaults.json index aa816e49..c5ba47db 100644 --- a/phobos/data/defaults.json +++ b/phobos/data/defaults.json @@ -109,7 +109,8 @@ "LogicalCamera": { "default": { "opening_height": 90, - "opening_width": 90} + "opening_width": 90 + } }, "JointLoad": { "default": {} @@ -135,9 +136,6 @@ "Magnetometer": { "default": {} }, - "GPS": { - "default": {} - }, "WirelessTransceiver": { "default": {} }, From 15ef4a83cfb034174e1de8be39ba8e0900239624 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Tue, 26 Sep 2023 12:33:26 +0200 Subject: [PATCH 46/49] Update $transform tooltip --- phobos/blender/operators/generic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/phobos/blender/operators/generic.py b/phobos/blender/operators/generic.py index d72f36e0..14d91398 100644 --- a/phobos/blender/operators/generic.py +++ b/phobos/blender/operators/generic.py @@ -558,8 +558,8 @@ class AnnotationsOperator(bpy.types.Operator): include_transform : BoolProperty( name="Include transformation", default=False, - description="By using the string key $transform you can include the name of the parent link in your annotations.\n" - "Using &transform.matrix/position/rotation_euler/quaternion let's you choose in which way it is stored." + description="By using the string key $transform you can include the transform of the parent link in your annotations.\n" + "Using $transform.xyz/rpy/angle_axis/quaternion let's you choose in which way it is stored." ) multiple_entries : BoolProperty( From f88362d42ecaec2296e86f953f44161060230955 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Tue, 26 Sep 2023 12:37:21 +0200 Subject: [PATCH 47/49] Fix AddSensor tooltip --- phobos/blender/operators/editing.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/phobos/blender/operators/editing.py b/phobos/blender/operators/editing.py index 9652b079..02c985a5 100644 --- a/phobos/blender/operators/editing.py +++ b/phobos/blender/operators/editing.py @@ -2092,8 +2092,7 @@ def draw(self, context): class AddSensorOperator(Operator): """Add a sensor at the position of the selected object. It is possible to create a new link for the sensor on the fly. Otherwise, - the next link in the hierarchy will be used to parent the sensor to. - """ + the next link in the hierarchy will be used to parent the sensor to""" bl_idname = "phobos.add_sensor" bl_label = "Add Sensor" From 6904ebb63791686094b06b9bba59ca6b1c22a7d3 Mon Sep 17 00:00:00 2001 From: Alex Zastrow Date: Tue, 26 Sep 2023 13:12:37 +0200 Subject: [PATCH 48/49] Add frame, link and joint to reserved sensor keys --- phobos/blender/reserved_keys.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phobos/blender/reserved_keys.py b/phobos/blender/reserved_keys.py index 5fa8373c..953d6817 100644 --- a/phobos/blender/reserved_keys.py +++ b/phobos/blender/reserved_keys.py @@ -10,4 +10,4 @@ ANNOTATION_KEYS = ["$name", "$category", "$include_parent", "$include_transform", "$transform"] SUBMECHANISM_KEYS = ["type", "jointnames_spanningtree", "jointnames_active", "jointnames_independent", "jointnames"] -SENSOR_KEYS = ["name", "position_offset", "orientation_offset", "origin"] +SENSOR_KEYS = ["name", "position_offset", "orientation_offset", "origin", "frame", "link", "joint"] From aa328a8fde0100f9b9206c19674650e64f4c6401 Mon Sep 17 00:00:00 2001 From: Henning Wiedemann Date: Tue, 26 Sep 2023 16:10:33 +0200 Subject: [PATCH 49/49] Bugfixes for CI --- phobos/ci/base_model.py | 4 ++++ phobos/core/robot.py | 3 +++ phobos/io/representation.py | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/phobos/ci/base_model.py b/phobos/ci/base_model.py index 5a723f67..45caad6d 100644 --- a/phobos/ci/base_model.py +++ b/phobos/ci/base_model.py @@ -424,6 +424,9 @@ def process(self): if "joint" not in config: raise KeyError(f"Frame {linkname} can't be defined without a joint definiton. Links that are already in the robot:\n" + str([str(l) for l in self.robot.links])) _joint_def = config.pop("joint") + if "type" not in _joint_def: + log.debug("No joint type for {linkname} specified assuming fixed.") + _joint_def["type"] = "fixed" _joint_def = misc.merge_default(_joint_def, resources.get_default_joint(_joint_def["type"])) parent_link = _joint_def.pop("parent") parent_joint = self.robot.get_parent(parent_link) @@ -732,6 +735,7 @@ def process(self): def export(self): self.robot.link_entities() + self.robot.submodel_defs = {} # we define these here per model ros_pkg_name = self.robot.export(outputdir=self.exportdir, export_config=self.export_config, rel_mesh_pathes=self.export_meshes, ros_pkg_later=True) for vc in self.robot.collisions + self.robot.visuals: diff --git a/phobos/core/robot.py b/phobos/core/robot.py index 8595cd24..e07a9fd7 100755 --- a/phobos/core/robot.py +++ b/phobos/core/robot.py @@ -1170,6 +1170,9 @@ def instantiate_submodel(self, name=None, start=None, stop=None, robotname=None, for vc in link.collisions+link.visuals: if submodel.get_link(vc.origin.relative_to) is None and submodel.get_joint(vc.origin.relative_to) is None: vc.origin = representation.Pose.from_matrix(self.get_transformation(end=vc.origin.relative_to, start=link), relative_to=link).dot(vc.origin) + for joint in submodel.joints: + if submodel.get_link(joint.origin.relative_to) is None and submodel.get_joint(joint.origin.relative_to) is None: + joint.origin = representation.Pose.from_matrix(self.get_transformation(end=vc.origin.relative_to, start=link), relative_to=joint.parent).dot(vc.origin) submodel.export_pdf("test.pdf") submodel.link_entities() diff --git a/phobos/io/representation.py b/phobos/io/representation.py index d1c9ab75..03a1efd0 100644 --- a/phobos/io/representation.py +++ b/phobos/io/representation.py @@ -1735,7 +1735,7 @@ def __init__(self, name=None, parent=None, child=None, joint_type=None, else: self.axis = None if origin is None and cut_joint is False: - log.warn(f"Created joint {name} without specified origin assuming zero-transformation") + log.debug(f"Created joint {name} without specified origin assuming zero-transformation") origin = Pose(xyz=[0, 0, 0], rpy=[0, 0, 0], relative_to=self.parent) self.origin = _singular(origin) if self.origin.relative_to is None: