Edit: New development of the Variant Link feature can be found here.
Since this is the development section, this post will be aimed more for developers. I'll post a video later more suitable for end users.
The PR contains a few changes in the core that are necessary for the end product, a configuration table defined in a spreadsheet that allows user to dynamically switch between multiple sets of parameters for one or multiple objects.
First, 'Expression...' option is now always available in property viewer context menu. Previously it is hidden unless 'Show all' option is activated. This makes it easy for user to use expression on any type of property.
The user can now rename the group name of a dynamic property. The menu action is only shown when 'Show all' is activated. Note that group renaming is no undoable. Because dynamic properties can only be shown in alphabetic orders, this action allows user to customize the property grouping.
PropertyEnumeration is modified to allow user to customize its list. It is exposed through sub path named 'Enum', and is only shown in the property viewer when 'Show all' option is activated. The 'Enum' sub path accepts expression binding as well. This provides the possibility to dynamically create a property for user to choose the configuration. The following screencast shows how to add a dynamic PropertyEnumeration in property viewer.
PropertySheetPy now supports mapping protocol for user to extract contents of a range of cells as a Python list. It can be used in both Python and Expression, e.g. in expression,
Code: Select all
# return a list of cell content in order A1, B1, A2, B2. Empty cells will correspond to a None item.
Sheet.cells[<<A1:B2>>]
# return the contents of row A starting from A1 until the first empty cell
Sheet.cells[<<A1:->>]
# return the contents of column A starting from A1 until the first empty cell
Sheet.cells[<<A1:|>>]
New expression built-in function str(arg) is added to convert anything to string, and another more important function href(arg), which hides any object reference inside the argument to work around cyclic dependency error. You need to be careful when using href() as it may give unexpected result. As a rule of thumb, it will be safe if href() is refering (directly or indirectly) to some property that will only be changed by user instead of calculated by the object (e.g. with other expressions). When used right, href() can be a powerful tool. You'll see later that it is an essential component for Configuration Table to work.
Exposed a few convenience context menu in Spreadsheet view.
The 'Recompute' menu action above is to recompute the selected cells without recompute its dependencies first. Because of the introduction of href(), you may encounter chicken and egg dilemma. And this menu action offers one way to break the cycle.
'Cut/Copy/Delete/Paste' has been enhanced to support format copying and auto repeat. Another thing to note is that the normal form of cell address (e.g. A1) is considered as relative reference, and will be auto adjusted when pasted. If you do not want this, use absolute reference (e.g. $A1, $B$1, C$1).
The 'Bind' action is to bind a range of cells to another range of cells in possibly another spreadsheet in maybe an externaldocument. The following screencast shows how it works. Select the target range first, and then optionally the source range. Once bound, the cell in target range will mirror what's in the source range. Bound cells are shown with blue border, and are not editable. Double clicking any bound cells to edit or discard the binding.
The cell binding is implemented using PropertyExpressionEngine and PropertySheet::setPathValue() with a special path. For example, to bind cells B3:D3 to B5:D5 of the same spreadsheet,
Code: Select all
Spreadsheet.setExpression('.cells.Bind.B3.D3', 'tuple(.cells, <<B5>>, <<D5>>)')
The 'Configuration Table' menu action is, of course, to actually create the table. The following screencast starts by showing a common workflow to create a list of parameters and bind them to various object properties. Using alias is not mandatory, but make it easy to move the cells around later on. Once you've tested that the parameters work well, you can duplicate the parameter row to create multiple configurations. You must name each of your configurations in the leading column. The first row is not part of the configuration list. It is considered as the 'current setting', and will be dynamically bind to the user's choice. The configuration table is created by select the top left corner cell, and activate the menu action. Enter an expression to refer to an object to publish the configurations using a PropertyEnumeration. The property group name is optional.
As you can see in the above screencast, the configuration property is published into a parent object. In other word, the spreadsheet dynamic cell binding will have to refer to its parent, which would normally be a cyclic reference. This is why we need href(). The expression used for cell binding is something like this,
Code: Select all
Spreadsheet.setExpression('.cells.Bind.B1.ZZ1', 'tuple(.cells, <<B>> + str(href(Part.Configuration)+2), <<ZZ>> + str(href(Part.Configuration)+2))')
In short, each cell in the 'current setting' row is dynamically bound to a cell in some row below, with the row offset calculated using the current value of the published PropertyEnumeration named 'Configuration' in object 'Part'. There is another cyclic reference here. Part.Configuration.Enum binds to the configuration name column in the spreadsheet, while the 'current setting' row's binding refers to the Part.Configuration. And it works because of href().
There is no limitation on how many tables you can define in one spreadsheet. You can bind the published PropertyEnumeration of some object and use it as part of a higher level configuration.
The published PropertyEnumeration has a special mark called 'CopyOnChange' using the same status bitset that defines 'ReadOnly', 'Touched', etc. The Link has special handling when seeing object with these marked properties. The special handling can be enabled by setting the 'LinkCopyOnChange' property to 'Enabled'. Once enabled, the Link will first duplicate those 'CopyOnChange' properties from the linked object and inject to itself. When the user changes any of these properties, the link will make a deep copy of the linked object, apply the change to it, and change the link to point to the copied object. It will also switch the 'LinkCopyOnChange' to 'Owned' so that later changes to 'CopyOnChange' properties will not trigger further copy. One thing to note that, any property can be marked as 'CopyOnChange' through the property view menu, not just the one published by Spreadsheet. In other words, you can use other property for configuration purpose without using spreadsheet.
Link array mode support 'CopyOnChange' as well. Each element of the array can individually toggle 'LinkCopyOnChange', making a heterogeneous array. Note that this is only supported when the array elements are expanded as individual objects, not when collpased. Draft LinkArray support 'CopyOnChange' too.
The SubShapeBinder from PartDesign takes another approach to support 'CopyOnChange'. It is enabled by property 'BindCopyOnChange'. SubShapeBinder can bind to multiple objects, but 'CopyOnChange' can only works when binding to one object. Instead of duplicating the linked object with all its hierarchy, the binder will make a flattened copy of the mutated object. Another difference to Link is that the binder will sync any changes of the original object back to the copy even if the configurations are different, while for Link, once copied, the two objects become independent. See the screencast below and notice how the binder syncs the change of the cyclinder height, which is not part of the configuration.
Behind the scene, SubShapeBinder actually makes a deep copy of the bound object just like Link, but with a twist. The copy is hidden in a temporary document. The core now supports creating a 'Temporary Document'. App.newDocument() accepts a new named argument 'temp', when set to True will create a temporary document that won't show up in the tree view. Another named argument 'hidden' controls whether to show 3D view of the new document. The temporary document is created with undo/redo disabled. It will not be auto saved. When closing all documents or exit application, the user will not be prompt for saving of any temporary document, although the document can still be saved by explictly calling its save() method in Python console. All binder shares a single temporary document behind the scene. The copied objects are temporary, too, so it does not take extra space when saved. The binder is still bound to the original object.