Modifiers¶
uLib::Modifier: an algorithm whose result can replace its input¶
A modifier (src/Core/Modifier.h) is an algorithm like any other — objects go
in, and its output is those objects seen through whatever it does to them —
with one verb the others do not have: Apply. Applying says stop showing me
the result and make it the thing itself: the effect is written into the
objects that were fed in, and the modifier, having nothing left to do, removes
itself.
scene scene
modifier Apply box <- carries the result now
box -------> cyl
cyl (the modifier is gone)
Everything up to that point is a view, undone by deleting the modifier; applying is the one act that is not. A subclass supplies exactly one thing:
Everything else — holding the objects, a port per object, Enabled, releasing
them and dissolving on Apply — is the base class, because it is the same for
every modifier there will be. Apply() bakes first and only then moves and
dissolves, so a bake that fails leaves the tree exactly as it was.
The verb is reachable three ways: the Apply button beside Play in gcompose
(armed whenever the selected object is a modifier with something in it),
invoke with the action "apply" over the remote protocol, and
Modifier::Apply() in C++ — which may destroy the object before it returns.
uLib::TransformModifier: the transform flavour¶
src/Math/TransformModifier.h is a filter over the objects it holds. Nest objects in it and they are moved, turned and scaled by
its transform exactly as if it were their parent — because it is: a modifier is
an Algorithm and a TRS, so it stands in the parent chain and every child
composes with it the way a child of an Assembly does.
scene
modifier Position / Rotation / Scale, about Pivot
box drawn at modifier · box
cylinder drawn at modifier · cylinder
untouched a modifier filters what it holds, not the scene
Nothing is copied and nothing is written into the children while the modifier is there, so taking an object back out leaves it exactly as it was.
Ports (on every modifier)¶
Every nested object gets an input port of its own, plus one spare at the end
(Input0, Input1, …). The ports are a second view of the nesting rather than
a separate thing to keep in step:
- wiring an object into the spare port moves it inside the modifier;
- clearing a port takes that object back out, to wherever the modifier itself lives.
So the node graph and the context tree say the same thing, whichever one is used to say it.
The pivot¶
What the rotation and scale turn about:
| PivotMode | where |
|---|---|
Custom |
wherever the Pivot property says |
ObjectCenter |
the median origin of the nested objects — a rotation spins the group in place |
ParentCenter |
the origin of whatever the modifier hangs from |
WorldCenter |
the world origin |
All four resolve to a point in the modifier's own parent space — the space
its Position is expressed in — so a pivot and a position always mean the same
thing by the same numbers. Pivot is editable only in Custom mode; elsewhere
it is derived.
What a child composes with is therefore
folded into GetWorldMatrix(), which is what the parent chain reads. That one
override is what makes a modifier a modifier.
Enabling and applying¶
Disabling (Enabled = false) makes the effective transform the identity
without touching anything: the children sit where their own placements put
them, and turning it back on puts the group back.
Applying is Blender's Apply. TransformModifier::ApplyToTargets writes
into each nested object's own matrix — exactly the composition the parent chain
was doing on the fly — so every object stays where it is drawn and no longer
needs the modifier to keep it there. Modifier::Apply then moves them out and
removes the modifier. Nothing on screen moves; that is the point of it.
An object with no placement of its own is left alone and simply moves out, which is the honest result of applying a transform to something that has none.
In the viewport¶
A modifier has no body, so uLib::Vtk::TransformModifier draws its pivot: a
small crosshair with a ball at its centre, in the spirit of Blender's 3D
cursor. MarkerSize (a display property, 0.5 scene units by default) sets how
big it is drawn, and the marker keeps its shape whatever the group does — it is
placed by the inverse of the frame's own rotation and scale, so a group scaled
by four does not come with a crosshair scaled by four.
The handle sits on the pivot, because that is where the transform is applied:
Its origin is the pivot carried by the transform — the one point a rotation
leaves standing still — so turning the gizmo turns the group about the gizmo
and scaling it scales the group about the gizmo, which is the only reading of a
handle that is not a surprise. Dragging it moves the whole nested group, and
the handle follows. A drag is solved straight back into the modifier's own
transform (SetPivotFrameWorld), with the pivot and the parent chain folded
out of the world matrix it arrives as.
Note what the frame is not: the modifier's own world matrix, which is
P(p)·TRS·P(-p) and whose origin is the modifier's origin carried by the
transform. The two differ by exactly the P(-p) that the frame leaves off.
Moving the pivot itself is the EditPivot display property: with it on, a
drag places the pivot instead of transforming the group (and switches the mode
to Custom, since placing one by hand is what Custom means), so the point
everything turns about can be put where it is wanted without leaving the
viewport. Off by default — moving the group is the common act, and a handle
that silently did something else would be a trap. The pivot can equally be
typed into the Properties panel, or set from code with SetPivotFromWorld().
In gcompose¶
Adding a modifier from Add ▸ Modifiers with an object selected nests that object in it — an empty modifier transforms nothing, and that is rarely what was meant. Nest In Modifier on any object's context menu puts it into an existing one, and Apply Modifier is on the modifier's own menu as well as on the toolbar button.
Why Algorithm's Object base is virtual¶
A modifier is an algorithm and a transform at once, and TRS reaches Object
virtually through AffineTransform. For the two to meet in one class,
Algorithm had to inherit Object virtually as well — otherwise the class
would carry two Object subobjects, which is not one class so much as two
objects sharing a name. The cost is that a copy constructor below Algorithm
must name Object(other) itself; see the comment on uLib::Algorithm.
Tests¶
src/Core/testing/ModifierTest.cpp (ctest: ModifierTest) pins the base class
against a deliberately trivial subclass — one that adds a number to every
object it holds — so that what is tested is the contract and not any one
flavour: the ports in both directions, applying writing the result into the
objects and dissolving the modifier, a refused bake changing nothing, the
"apply" action, and un-nesting.
app/gcompose/testing/FileNewAndAddMenuTest.cpp covers the button: adding a
modifier with an object selected nests it, selecting the modifier arms Apply,
clicking it bakes the transform into the object and removes the modifier, and
the button disarms with it.
src/Math/testing/TransformModifierTest.cpp (ctest: TransformModifierTest)
covers nesting and what it does to where an object is drawn, the ports in both
directions, all four pivot modes (including the median point of a group),
scale, disable/enable, un-nesting, applying, the action verb, modifiers nested
in each other, and a document round trip.
src/Vtk/Math/testing/vtkTransformModifierTest.cpp (ctest:
vtkTransformModifierTest) covers the viewport half: the prop exists, a nested
object keeps its own prop and is drawn through the modifier's transform, the
handle sits on the pivot in every mode, turning the handle turns the group
about the handle (and leaves the handle where it is), dragging it moves the
group, the pivot itself can be dragged with the frame origin following, and the
prop goes when the modifier does.