Relate parameter values between objects

Have some feature requests, feedback, cool stuff to share, or want to know where FreeCAD is going? This is the place.
Forum rules
Be nice to others! Read the FreeCAD code of conduct!
cslarson
Posts: 30
Joined: Sun Nov 07, 2010 10:04 pm

Re: Relate parameter values between objects

Post by cslarson »

Maybe there is a way to do this that wouldn't need any changes to other elements of FreeCAD and accomplishes the following:
relate object parameters to user create model parameters
user created parameters and relations saved and retrieved with the model file
some helpful gui component

A python feature object with two dictionaries, one for parameters, one to hold the relationships that bind these parameters to other object parameters/dimensions. The keys in the relationship dictionary could be the parameters pulled from all the other model objects, no? The keys for the parameters dictionary would just be new parameters that I want to create. A script opens a gui interface that allows for friendly editing of these dictionaries as well as initially creating the object if it isn't already found in the model.

So I likely can do this myself since FreeCAD has been designed to be so extensible through Python! Thanks :) (Any tips?...)
User avatar
tanderson69
Veteran
Posts: 1626
Joined: Thu Feb 18, 2010 1:07 am

Re: Relate parameter values between objects

Post by tanderson69 »

Coming late to the party on this one, but I was hoping this would come up. In UG it was called expressions. Expressions could be used in multiple features and had the ability to be interpart(linked between files). I wrote a lot of customization code to automatically setup these links between assembly components. I agree with Jreigel that they can and do get abused, but I think they are a great feature when used with restraint.
cslarson
Posts: 30
Joined: Sun Nov 07, 2010 10:04 pm

Re: Relate parameter values between objects

Post by cslarson »

What I thought I would try, to start with, was creating an object that stores these parameters and relationships. Any ideas on anything, including naming, would be appreciated. Storing the user created parameters is done in a python dictionary, mparameters, which is an App::PropertyPythonObject of the main ModelParameters object, which is an "App::FeaturePython". What I am stuck on is how to do the relations. Particularly, how to hold the reference to the parameter we want to affect (FreeCAD.ActiveDocument.Sphere.Radius for example) in the ModelParameters object. Initially I thought I could use that actual parameter as a key in the dictionary, relations, with the value being the code to "evaluate" but keys must be immutable types like strings or numbers. Anyway, basically I think I need to create a reference to an object and store that reference in my ModelParameters object. Maybe I just do this in a string...

Code: Select all

import FreeCAD

class ModelParameters:
	def __init__(self, obj):
		obj.addProperty("App::PropertyPythonObject","Parameters").Parameters = {}
		obj.addProperty("App::PropertyPythonObject","Relations").Relations = {}                                //not sure what to do here...
		obj.Proxy = self

	def onChanged(self, fp, prop):
		return

	def execute(self, fp):
		return

def getModelParameters():
	doc = FreeCAD.ActiveDocument
	if hasattr(doc,'ModelParameters'):
		mp = doc.ModelParameters
	else:
		mp = doc.addObject("App::FeaturePython","ModelParameters")
		ModelParameters(mp)
	return mp
cslarson
Posts: 30
Joined: Sun Nov 07, 2010 10:04 pm

Re: Relate parameter values between objects

Post by cslarson »

What I am stuck on is how to do the relations.
I think the relations could be stored as tuples in a list. The first item in the tuple would be the expression as a string. The next item is the object that the parameter belongs to. Third would be that object's parameter that is to be altered, as a string. If this parameter is not a float, such as Placement, then it gets more complex... but maybe can still be handled in a special way using additional items in the tuple.
cslarson
Posts: 30
Joined: Sun Nov 07, 2010 10:04 pm

Re: Relate parameter values between objects

Post by cslarson »

Well, the following has been an interesting experience for me. This script doesn't yet support many of the things it should support, such as deleting or renaming a "model parameter" via the gui, or interacting with object properties besides those that are directly float values or Placement.Base.x/y/z. As simple as it looks I still had a bit of a time figuring out why something was not working the way I wanted. This seemed much more complicated when writing something with a gui. But I think it was still a good learning experience, hey.

I call the script ModelParameters.py so if it's in my Mod/Scripts file then I can use it with the following code in the FreeCAD python interpreter:

Code: Select all

import ModelParameters
d=QtGui.QWidget()
d.ui=ModelParameters.Ui_Dialog()
d.ui.setupUi(d)
d.show()

Code: Select all

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'NewModelParameters.ui'
#
# Created: Sun Dec 05 21:12:07 2010
#	  by: PyQt4 UI code generator 4.8.1
#
# WARNING! All changes made in this file will be lost!

import FreeCAD, string
from PyQt4 import QtCore, QtGui

try:
	_fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
	_fromUtf8 = lambda s: s

class ModelParameters:
	def __init__(self, obj):
		obj.addProperty("App::PropertyPythonObject","Parameters").Parameters = {}
		obj.addProperty("App::PropertyPythonObject","Relations").Relations = {}
		obj.Proxy = self

	def onChanged(self, fp, prop):
		return

	def execute(self, fp):
		return

class Ui_Dialog(object):

	def get_model_parameters():
		if FreeCAD.ActiveDocument == None:
			FreeCAD.newDocument()
		doc = FreeCAD.ActiveDocument
		if hasattr(doc,'ModelParameters'):
			mp = doc.ModelParameters
		else:
			mp = doc.addObject("App::FeaturePython","ModelParameters")
			ModelParameters(mp)
		return mp
	
	mp = get_model_parameters()
	
	relations_tree_o_items = {}														# map tree top level item to model object
	relations_tree_p_items = {}														# map tree child item to object property tuple (object,property)

	def setupUi(self, Dialog):
		Dialog.setObjectName(_fromUtf8("Dialog"))
		Dialog.resize(631, 541)
		self.verticalLayoutWidget = QtGui.QWidget(Dialog)
		self.verticalLayoutWidget.setGeometry(QtCore.QRect(9, 9, 611, 521))
		self.verticalLayoutWidget.setObjectName(_fromUtf8("verticalLayoutWidget"))
		self.vertical_layout = QtGui.QVBoxLayout(self.verticalLayoutWidget)
		self.vertical_layout.setObjectName(_fromUtf8("vertical_layout"))
		self.label_model_parameters = QtGui.QLabel(self.verticalLayoutWidget)
		self.label_model_parameters.setObjectName(_fromUtf8("label_model_parameters"))
		self.vertical_layout.addWidget(self.label_model_parameters)
		self.table_model_parameters = QtGui.QTableWidget(self.verticalLayoutWidget)
		self.table_model_parameters.setObjectName(_fromUtf8("table_model_parameters"))
		self.table_model_parameters.setColumnCount(2)
		self.table_model_parameters.setRowCount(0)
		self.header_item_name = QtGui.QTableWidgetItem()
		self.table_model_parameters.setHorizontalHeaderItem(0, self.header_item_name)
		self.header_item_value = QtGui.QTableWidgetItem()
		self.table_model_parameters.setHorizontalHeaderItem(1, self.header_item_value)
		self.vertical_layout.addWidget(self.table_model_parameters)
		self.label_relations = QtGui.QLabel(self.verticalLayoutWidget)
		self.label_relations.setObjectName(_fromUtf8("label_relations"))
		self.vertical_layout.addWidget(self.label_relations)
		self.tree_relations = QtGui.QTreeWidget(self.verticalLayoutWidget)
		self.tree_relations.setObjectName(_fromUtf8("tree_relations"))
		self.vertical_layout.addWidget(self.tree_relations)

		self.retranslateUi(Dialog)
		self.refresh_mp_table()
		self.refresh_relations_tree()
		QtCore.QObject.connect(self.table_model_parameters,QtCore.SIGNAL("cellChanged(int,int)"),self.model_parameters_changed)
		self.tree_relations.itemChanged.connect(self.relation_changed)
		QtCore.QMetaObject.connectSlotsByName(Dialog)

	def retranslateUi(self, Dialog):
		Dialog.setWindowTitle(QtGui.QApplication.translate("Dialog", "Dialog", None, QtGui.QApplication.UnicodeUTF8))
		self.label_model_parameters.setText(QtGui.QApplication.translate("Dialog", "Model Parameters", None, QtGui.QApplication.UnicodeUTF8))
		self.table_model_parameters.horizontalHeaderItem(0).setText(QtGui.QApplication.translate("Dialog", "Name", None, QtGui.QApplication.UnicodeUTF8))
		self.table_model_parameters.horizontalHeaderItem(1).setText(QtGui.QApplication.translate("Dialog", "Value", None, QtGui.QApplication.UnicodeUTF8))
		self.label_relations.setText(QtGui.QApplication.translate("Dialog", "Relations", None, QtGui.QApplication.UnicodeUTF8))
		self.tree_relations.headerItem().setText(0, QtGui.QApplication.translate("Dialog", "Object Parameter", None, QtGui.QApplication.UnicodeUTF8))
		self.tree_relations.headerItem().setText(1, QtGui.QApplication.translate("Dialog", "Expression", None, QtGui.QApplication.UnicodeUTF8))
		self.tree_relations.headerItem().setText(2, QtGui.QApplication.translate("Dialog", "Evaluates to:", None, QtGui.QApplication.UnicodeUTF8))

	def mp_name_ok(self,text):
		if text == "":
			return False
		l_count = 0
		for l in text:
			if l in string.letters:
				l_count = l_count+1
		if l_count == 0:
			QtGui.QMessageBox.critical(None,"Problem with name","Name should contain a letter")
			return False
		else:
			return True

	def mp_value_ok(self,value):
		try:
			value = float(value)
		except ValueError:
			QtGui.QMessageBox.critical(None,"Problem with value","Value should be a number")
			return False
		return True

	def model_parameters_changed(self):
		item = self.table_model_parameters.currentItem()
		if item.row() == 0 and item.column() == 0:							# new model parameter name, wait for value before saving
			return
		if item.row() == 0 and item.column() == 1:							# got value, save new model parameter and add new empty row
			name_item = self.table_model_parameters.item(item.row(),0)
			name = str(name_item.text())
			value = item.text()
			if self.mp_name_ok(name) == True & self.mp_value_ok(value) == True:
				value = float(value)
			self.mp.Parameters[name]=value
			self.refresh_mp_table()
			return
		if item.column() == 0:															# rename or delete model parameter
			name = str(item.text())
			if name == "":
				# delete that model parameter
				# use dialog to confirm
				return
			elif self.mp_name_ok(name) == True:
				# rename that model parameter
				return
		elif item.column() == 1:														# update model parameter value
			value = str(item.text())
			if self.mp_value_ok(value) == True:
				name_item = self.table_model_parameters.item(item.row(),0)
				name = str(name_item.text())
				self.mp.Parameters[name]=value
				self.refresh_relations_tree()
				return

	def relation_changed(self):
		item = self.tree_relations.currentItem()
		expression = str(item.text(1))
		if expression == "":
			return
		if expression.startswith("="):
			expression=expression.replace("=","")
		op_tuple = self.relations_tree_p_items[item]
		self.mp.Relations[self.relations_tree_p_items[item]] = expression
		evaluates_to = self.evaluate_expression(expression)
		self.execute_expression(evaluates_to,op_tuple,item)

	def by_length(self, word1, word2):											# for sorting parameter names from longest to shortest to help prevent
		return len(word2) - len(word1)											# errors when replacing names with their values in the expression

	def evaluate_expression(self,expression):
		mp_names = self.mp.Parameters.keys()
		mp_names.sort(cmp=self.by_length)
		for mp_name in mp_names:
			expression = expression.replace(mp_name,str(self.mp.Parameters[mp_name]))
		evaluates_to = float(eval(expression))
		return evaluates_to
		
	def execute_expression(self,evaluates_to,op_tuple,item):
		object = op_tuple[0]
		property = op_tuple[1]
		prefix = "FreeCAD.ActiveDocument."
		if property == "Placement Base x":
			axis = "x"
			self.change_placement(object,axis,evaluates_to)
			return
		elif property == "Placement Base y":
			axis = "y"
			self.change_placement(object,axis,evaluates_to)
			return
		elif property == "Placement Base z":
			axis = "z"
			self.change_placement(object,axis,evaluates_to)
			return
		try:
			exec(prefix + object.Name + "." + property + "=" + str(evaluates_to))
		except:
			QtGui.QMessageBox.critical(None,"Some Problem","Expression: "+expression)
			raise
			return
		item.setText(2,QtCore.QString(str(evaluates_to)))

	def refresh_mp_table(self):
		self.table_model_parameters.clear()
		self.mp.Parameters['hello'] = 50.0
		self.mp.Parameters['peach'] = 1028.08
		self.mp.Parameters['bananas'] = 43.5
		row_count = len(self.mp.Parameters.items())+1
		self.table_model_parameters.setRowCount(row_count)
		empty_item_name = QtGui.QTableWidgetItem(QtCore.QString(""))
		empty_item_value = QtGui.QTableWidgetItem(QtCore.QString(""))
		self.table_model_parameters.setItem(0,0,empty_item_name)
		self.table_model_parameters.setItem(0,1,empty_item_value)
		for row_i,(name,value) in enumerate(self.mp.Parameters.items()):
			item_name = QtGui.QTableWidgetItem(QtCore.QString(name))
			item_value = QtGui.QTableWidgetItem(QtCore.QString(str(value)))
			self.table_model_parameters.setItem(row_i+1,0,item_name)
			self.table_model_parameters.setItem(row_i+1,1,item_value)

	def refresh_relations_tree(self):
		self.tree_relations.clear()
		self.relations_tree_o_items.clear()
		self.relations_tree_p_items.clear()
		for obj in self.mp.Document.Objects:
			op_name_list = []
			for op_name in obj.PropertiesList:
				if type(obj.getPropertyByName(op_name)) == type(float()):
					op_name_list.append(op_name)
				elif op_name == "Placement":
					op_name_list.append("Placement Base x")
					op_name_list.append("Placement Base y")
					op_name_list.append("Placement Base z")
			if op_name_list != []:
				new_o_item = QtGui.QTreeWidgetItem(self.tree_relations)
				new_o_item.setText(0,QtCore.QString(obj.Label))
				new_o_item.setFlags(QtCore.Qt.ItemIsEnabled)
				self.relations_tree_o_items[new_o_item] = obj
				for op_name in op_name_list:
					new_p_item = QtGui.QTreeWidgetItem(new_o_item)
					new_p_item.setText(0,QtCore.QString(op_name))
					new_p_item.setFlags(new_p_item.flags() | QtCore.Qt.ItemIsEditable)
					if (obj,op_name) in self.mp.Relations:
						expression = self.mp.Relations[(obj,op_name)]
						new_p_item.setText(1,QtCore.QString(expression))
						new_p_item.setText(2,QtCore.QString(str(self.evaluate_expression(expression))))
					self.relations_tree_p_items[new_p_item] = (obj,op_name)

	def change_placement(self,object,axis,value):
		current_matrix = object.Placement.toMatrix()
		new_matrix = current_matrix
		if axis == "x":
			new_matrix.A14 = value
		elif axis == "y":
			new_matrix.A24 = value
		elif axis == "z":
			new_matrix.A34 = value
		object.Placement = new_matrix
Post Reply