Python abstraction to auto-unwind transformations

1 view
Skip to first unread message

W W

unread,
Nov 5, 2025, 1:57:22 AM (yesterday) Nov 5
to PythonSCAD
Hey folks, quick cross posting from https://www.reddit.com/r/OpenPythonSCAD/comments/1oovuhg/auto_unwind_transformation_scopes_using_with/

I always had this one friction point with CSG abstractions, and I was able to solve it this week.

TLDR: By combining monads, Python context managers (with statements), and incremental transform matrices, I can now auto-unwind transforms (translate/rotate/scale) within a scoped block. This eliminates the manual, error-prone process of restoring positions in CSG-style modeling.


The Problem: 

  • In OpenSCAD/PythonSCAD, many operations (like rotate_extrude()) are origin-centric. When working with solids away from the origin, I’d manually translate, operate, then “undo” transforms. This has been a tedious and brittle process.

My Solution: 

  • Using a monadic abstraction with Python’s context manager, I record each transform matrix on a stack. When the with block exits, transforms automatically unwind. The system tracks incremental matrices per operation and even allows manual overrides when needed.

Some Challenges (mostly for retro. Can skip if you don't care): 

  • Calculating correct incremental matrices wasn’t always straightforward. Some operations (like unions) don’t yield predictable transforms. I added an “escape hatch” for manual overrides.

Enough words. Demo time:

Here's what the render looks like for the testcase with monads. 

White solid is the "original" solid. The purple "poop" is computed after /1/ some translation /2/ 2d projection /3/ rotate_extrude /4/ let the monad unwind the movements such that the rotate_extrude output is at the same location as the original solid.
  • 1.png


W W

unread,
Nov 5, 2025, 1:58:56 AM (yesterday) Nov 5
to PythonSCAD
For simplicity, here's the code snippet that uses monads to auto unwind transforms:

```py
    def compute_with_monad():
        '''
        This is "more code", however unwinding transform movements is automatic.
       
        Most of the 'effort' is one-time-price in implementing the _withdelta functions, for cases where solid.origin does not quite preserve transformation lineages completely.
       
        Thankfully, this is much easier to reason about since you only need to zero-in transformation matrix for one transform, and it is all composeable in stepwise multmatrix() and divmatrix() internally in TransformLineageMonad.
       
        Note that this is bound to happen for some operations, such as unioning two solids.
        '''
        loc = dumbbell.origin
        
        # IMPORTANT: reference must exist OUTSIDE of the with context to be able to dereference it after context unwind!
        monad = TransformLineageMonad(dumbbell)
       
        with (
            monad as dum
        ):
            # All translate/rotate/scale within the with-scope will be unwind after!
            # Good for diff/union solids around the origin, and the context will restore to original position.
           
            # Center the solid around the origin.
            # center_withdelta() is already supplied in ztools lib (early experimental).
            dum_at_origin, _ = dum.apply_mutably(lambda solid: center_withdelta(solid))
            #show(dum_at_origin.solid.color('yellow'))
   
            # Final reposition to be ready for rotate_extrude. It is a 2d projection now.
            dum_ready_for_rotate_extrude, _ = dum_at_origin.apply_mutably(lambda solid: MonadUtilities.translate_withdelta(solid, [20, 20, 20]))
            #show(dum_ready_for_rotate_extrude.solid.color('cyan'))
           
            # Perform the projection to 2d, right before rotate_extrude.
            dum_ready_for_rotate_extrude, _ = dum_ready_for_rotate_extrude.apply_mutably(lambda solid: MonadUtilities.projection_withdelta(solid))
           
            # Give it height of 1 to show in render() F6.
            #show(dum_ready_for_rotate_extrude.solid.linear_extrude(1).color('blue'))
           
            # 2-layer caricature poop emoji.
            weird_pottery_looking_thing, _ = dum_ready_for_rotate_extrude.apply_mutably(lambda shape: MonadUtilities.rotate_extrude_withdelta(shape, 180))
            #show(weird_pottery_looking_thing.solid.color('orange'))
       
        # Once context exits, all the 4x4 transform matrix will unwind.
        # The result solid will "move" to the original position and orientation before context started.
        show(monad.solid.color('magenta'))
```

W W

unread,
Nov 5, 2025, 2:00:25 AM (yesterday) Nov 5
to PythonSCAD
The monad itself is fairly short. About 200 lines.

https://github.com/wiw-pub/ztools/blob/monads/src/transformlineagemonad.py
Reply all
Reply to author
Forward
0 new messages