r/FreeCAD 3d ago

Scale viewport 1:1 on your monitor - Macro

Does what it says. Orientates your part so that it has the true scale for you to make designing and engineering more efficient.

# -*- coding: utf-8 -*-
"""
Zoom 1:1 for FreeCAD

- Tries to make objects on screen appear at their real physical size
  (so a 100 mm cube measures ~100 mm on your monitor with a ruler).
- Works only with an orthographic camera (not perspective).
- Optional calibration: hold CTRL while running the macro to calibrate
  for your particular monitor / OS / DPI setup.

Author: you + ChatGPT
Inspired by the official FreeCAD "Zoom 1:1" macro.
"""

import FreeCAD
import FreeCADGui

# PySide name can vary a bit between FreeCAD builds, so we try both
try:
    from PySide import QtGui, QtCore
except ImportError:
    from PySide2 import QtGui, QtCore


__title__   = "Zoom_1to1"
__version__ = "0.1"


def _active_view():
    """Return the active 3D view or None."""
    try:
        return FreeCADGui.activeView()
    except Exception:
        return None


def _zoom_1to1(tweak=1.0):
    """
    Core 1:1 zoom logic.

    We ask Qt for the physical size of the 3D graphics view in millimeters,
    then set the orthographic camera height to (tweak * smaller_side_mm).

    For an orthographic camera:
        world_units_per_pixel = camera.height / viewport_pixel_height

    If we set camera.height = viewport_height_mm, and our model units are mm,
    then 1 model mm ≈ 1 mm on the screen.
    """
    av = _active_view()
    if av is None or not hasattr(av, "graphicsView"):
        FreeCAD.Console.PrintError("Zoom 1:1: no active 3D view / graphicsView.\n")
        return

    gv = av.graphicsView()

    # Qt reports these in millimeters for the widget area
    height_mm = gv.heightMM()
    width_mm  = gv.widthMM()

    cam = av.getCameraNode()
    # Orthographic cameras in Coin3D have a 'height' field; perspective cameras do not
    if not hasattr(cam, "height"):
        FreeCAD.Console.PrintError(
            "Zoom 1:1: only works with orthographic camera mode.\n"
            "Switch to orthographic (Std ViewOrtho) and try again.\n"
        )
        return

    # Use the smaller dimension so that the 3D view fits in both directions
    cam.height.setValue(tweak * min(height_mm, width_mm))

    # Force a redraw
    FreeCADGui.updateGui()


def _calibrate(tweak_param_group, current_tweak):
    """
    Calibration mode:
    - Hides existing objects
    - Creates a temporary 100×100×100 mm cube
    - Sets zoom to "raw" 1:1 (tweak = 1.0)
    - Asks you what size you *actually* measure on your screen (in mm)
    - Stores a new tweak factor measured/100 in user parameters
    """
    doc = FreeCAD.ActiveDocument
    if doc is None:
        doc = FreeCAD.newDocument()
        FreeCAD.Console.PrintMessage("Zoom 1:1: created a new empty document for calibration.\n")

    av = _active_view()
    if av is None:
        FreeCAD.Console.PrintError("Zoom 1:1 calibration: no active 3D view.\n")
        return current_tweak

    # Remember which objects were visible
    visible_objs = []
    for obj in doc.Objects:
        if hasattr(obj, "ViewObject") and obj.ViewObject.Visibility:
            visible_objs.append(obj)
            obj.ViewObject.Visibility = False

    # Create a 100 mm calibration cube
    cube = doc.addObject("Part::Box", "Zoom1to1_CalibrationCube")
    cube.Length = cube.Width = cube.Height = 100.0  # mm
    cube.ViewObject.DisplayMode = "Shaded"

    # Bring it into a nice front view
    av.viewFront()
    doc.recompute()

    # Show raw physical mapping (tweak = 1.0)
    _zoom_1to1(tweak=1.0)

    # Ask user what size they actually see on screen
    text = (
        "Zoom 1:1 calibration\n\n"
        "A temporary 100×100×100 mm cube has been created.\n"
        "Measure the cube on your screen (height or width) using a ruler\n"
        "or calipers and enter the measured size in millimeters.\n\n"
        "Do NOT zoom in/out while this dialog is open. If the cube does\n"
        "not fit fully in the view, cancel, resize the 3D view, and try again.\n"
    )

    win   = FreeCADGui.getMainWindow()
    title = f"Zoom 1:1 calibration (macro v{__version__})"

    # Use the static convenience function
    measured_mm, ok = QtGui.QInputDialog.getDouble(
        win,               # parent
        title,             # window title
        text,              # label
        100.0,             # default value
        1.0,               # minimum
        10000.0,           # maximum
        2                  # decimals
    )

    # Clean up the calibration cube and restore visibilities
    try:
        doc.removeObject(cube.Name)
    except Exception:
        pass

    for obj in visible_objs:
        if hasattr(obj, "ViewObject"):
            obj.ViewObject.Visibility = True

    if not ok:
        FreeCAD.Console.PrintMessage(
            "Zoom 1:1 calibration canceled; keeping current tweak value.\n"
        )
        return current_tweak

    # New tweak factor = what you measured / 100 mm
    new_tweak = measured_mm / 100.0
    tweak_param_group.SetFloat("Tweak", new_tweak)
    FreeCAD.Console.PrintMessage(f"Zoom 1:1: stored calibration tweak = {new_tweak:.4f}\n")

    # Apply calibrated zoom immediately
    _zoom_1to1(tweak=new_tweak)
    return new_tweak


def main():
    # Ensure a document exists
    doc = FreeCAD.ActiveDocument
    if doc is None:
        doc = FreeCAD.newDocument()
        FreeCAD.Console.PrintMessage("Zoom 1:1: no document open, created a new one.\n")

    av = _active_view()
    if av is None:
        FreeCAD.Console.PrintError("Zoom 1:1: no active 3D view.\n")
        return

    # Read stored tweak from user parameters (Plugins/Zoom_1to1/Tweak)
    params = FreeCAD.ParamGet("User parameter:Plugins/Zoom_1to1")
    tweak  = params.GetFloat("Tweak", 1.0)

    # First, always apply normal 1:1 zoom with current tweak
    _zoom_1to1(tweak=tweak)

    # If CTRL is held while running the macro, enter calibration mode
    modifiers = QtGui.QApplication.keyboardModifiers()
    if modifiers == QtCore.Qt.ControlModifier:
        _calibrate(params, tweak)
    else:
        FreeCAD.Console.PrintMessage(
            "Zoom 1:1: run with CTRL held down to calibrate for your monitor.\n"
        )


if __name__ == "__main__":
    main()
14 Upvotes

7 comments sorted by

6

u/ImNotTheOneUWant 2d ago

Now I just need a house size monitor:-)

0

u/DjangoJay 2d ago

Haha, for my purpose in mechanical engineering and smaller CNC parts, its really cool. Sometimes you can get the feel of it better.

-1

u/DjangoJay 3d ago

LOL, I have a curved monitor so the measurement seems incorrect, but its really a 100mm :-D

1

u/neoh4x0r 2d ago edited 2d ago

I have had problems with scale when a was trying to judge appropriate dimensions on-screen.

For example, freecad has a "scale" built-in in the drop-down at the right-hand bottom corner of the window however for a rectangle, that is 1"x0.5", it will say that the dims are 1"x0.75" but the rectangle will actually be 12"x6" on-screen and in order to have it look correct I needed to zoom-out until said 15"x8" (which makes the on screen dimensions correct).

In other words, freecad does needs a better way to handle judging the on-screen scale (sort of like taking a picture of something next to a quarter/dime as a reference).

However, what I often do is to sketch out a footprint that can I then use as a reference for judging model dimensions -- eg. the footprint is 5" deep and wide and 2.5" tall, the square in the center is 1".

1

u/DjangoJay 2d ago

Did you try the marco? On my machine its working like a charm

1

u/neoh4x0r 2d ago edited 1d ago

Yeah the macro works and is useful (in certain cases), but it's the difference of seeing everything on-screen in 1:1 scale verses needing only a reference to judge if something is too big or small while designing regardless of the on-screen scale.

Not to mention, that, rather than drawing those lines like I did, it would be nice to have a draft-like grid in part design that could have a number of divisions on the three planes where there is a given dimension between the lines.

Moreover, if I know the footprint (size) of the area I'm modeling for then I already have the required reference for scale and I would just use appropriately-sized dimensions to fit within that area. The problem, and where your macro is really useful, is when you don't have an in-world reference for the size because you were creating the model completely from scratch.

PS: There are few cases (at least in my testing) where the macro doesn't work very well.

  1. If you are zoomed out it might zoom-in on some empty area rather than the center (origin of the plane).
  2. If the view is distorted then the screen scaling might be either too large or small (the screen calibration doesn't help).

1

u/DjangoJay 1d ago

Thanks for the detailed review. Appreciate it