Skip to content

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:

bool ApplyToTargets(std::string *error) override;   // write my result into them

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

effective = P(pivot) · TRS · P(-pivot)

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

local  <-  effective · local

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:

frame = parentWorld · P(pivot) · TRS          (GetPivotFrameWorld)

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.