#!/usr/bin/python # -*- coding: utf-8 -*- ##################################### # Copyright (c) 2026 # Licensed under LGPL v2 # # This macro resets the origin of selected Mesh objects to the world # origin, WITHOUT moving the geometry. The current placement transform # is baked into the mesh vertex coordinates, then the Placement property # is reset to identity (0,0,0 / 0 deg). The mesh stays exactly where it # seats in the world, but its origin now coincides with the world origin. # # Useful after using alignment tools (e.g. Mesh Alignment) to # consolidate transformations without relying on a parent container. # Take a look at the example given on my GitHub page for a common use case: # https://github.com/de-seingalt/MeshResetOrigin/tree/main#example-common-use-case # # Only Mesh::Feature objects are processed. Non-mesh objects and meshes # that live inside a parent container (App::Part) are skipped with a # console warning. A single undo transaction wraps all changes. # # Version history: # *0.2 : added multiple mesh selection support and tested on FreeCAD 1.1.1 # *0.1 : initial draft # ##################################### __Name__ = 'MeshResetOrigin' __Comment__ = 'Reset selected mesh origins to global document origin without moving geometry' __Author__ = 'de-seingalt' __Version__ = '0.2' __Date__ = '2026-06-09' __License__ = 'LGPL v2' __Web__ = 'https://github.com/de-seingalt/MeshResetOrigin' __Wiki__ = 'https://wiki.freecad.org/Macro_MeshResetOrigin' __Icon__ = 'https://wiki.freecad.org/images/3/3f/Macro_MeshResetOrigin.svg' __Xpm__ = '' __Help__ = ('Select one or more Mesh objects, then run the macro. ' 'The origin of each mesh is set to global document origin' 'while the geometry stays in place.') __Status__ = 'Beta' __Requires__ = 'FreeCAD >= 1.1.1' __Communication__ = 'https://github.com/de-seingalt/MeshResetOrigin/issues' __Files__ = '' import FreeCAD import FreeCADGui def reset_mesh_origin(obj): """ Bake the placement of a Mesh::Feature into its vertex coordinates so that its local origin coincides with the world origin, WITHOUT moving the geometry visually. How it works: - We read the current Placement matrix. - We apply that matrix to a copy of the mesh: every vertex is moved into the position it currently occupies in world space. - We reassign the transformed mesh and reset Placement to identity. Net effect: the mesh stays exactly where it was on screen, but its Placement now reads (0,0,0) / 0 deg, because the transform lives in the geometry instead of the Placement property. Note: this only handles meshes with no parent container. A parented mesh is filtered out earlier in run(), because baking the LOCAL placement would leave the parent transform still composed on top. """ placement = obj.Placement if placement.isIdentity(): return False # nothing to do, already at origin # Work on a copy so the operation is atomic and undoable mesh = obj.Mesh.copy() mesh.transform(placement.toMatrix()) obj.Mesh = mesh obj.Placement = FreeCAD.Placement() # identity: (0,0,0) / 0 deg return True def run(): doc = FreeCAD.ActiveDocument if doc is None: FreeCAD.Console.PrintError( 'MeshResetOrigin: no active document.\n' ) return selection = FreeCADGui.Selection.getSelection() if not selection: FreeCAD.Console.PrintWarning( 'MeshResetOrigin: no object selected. ' 'Please select one or more Mesh objects first.\n' ) return # Filter: keep only Mesh::Feature objects meshes = [obj for obj in selection if obj.isDerivedFrom('Mesh::Feature')] skipped = len(selection) - len(meshes) if not meshes: FreeCAD.Console.PrintWarning( 'MeshResetOrigin: no Mesh object in the selection ' '({} non-mesh object(s) ignored).\n'.format(len(selection)) ) return # Exclude meshes that have a parent GeoFeature container (e.g. App::Part). # Reassigning the global placement while the mesh is still parented would # cause FreeCAD to compose the parent transform on top of it, moving the # geometry to an unintended position. safe_meshes = [] parented = [] for obj in meshes: if obj.getParentGeoFeatureGroup() is not None: parented.append(obj) else: safe_meshes.append(obj) for obj in parented: FreeCAD.Console.PrintWarning( "MeshResetOrigin: '{}' has a parent container — skipped. " "Remove it from its container before running this macro.\n" .format(obj.Label) ) if not safe_meshes: FreeCAD.Console.PrintWarning( 'MeshResetOrigin: no eligible mesh to process ' '(all selected meshes have a parent container).\n' ) return doc.openTransaction('Reset Mesh Origin') try: processed = 0 already_ok = 0 for obj in safe_meshes: changed = reset_mesh_origin(obj) if changed: processed += 1 FreeCAD.Console.PrintMessage( "MeshResetOrigin: '{}' origin reset to world origin.\n" .format(obj.Label) ) else: already_ok += 1 FreeCAD.Console.PrintMessage( "MeshResetOrigin: '{}' already at origin — unchanged.\n" .format(obj.Label) ) doc.commitTransaction() doc.recompute() summary = '{} mesh(es) reset'.format(processed) if already_ok: summary += ', {} already at origin'.format(already_ok) if skipped: summary += ', {} non-mesh object(s) skipped'.format(skipped) if parented: summary += ', {} parented mesh(es) skipped'.format(len(parented)) FreeCAD.Console.PrintMessage( 'MeshResetOrigin: done — {}.\n'.format(summary) ) except Exception as e: doc.abortTransaction() FreeCAD.Console.PrintError( 'MeshResetOrigin: error — {}. Transaction aborted.\n'.format(e) ) run()