Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Request: add support for scaling imported STLs #874

Open
MatthiasJ1 opened this issue Jan 17, 2025 · 5 comments
Open

Feature Request: add support for scaling imported STLs #874

MatthiasJ1 opened this issue Jan 17, 2025 · 5 comments
Labels
enhancement New feature or request

Comments

@MatthiasJ1
Copy link
Contributor

Most 3d scans and software like Blender use meters as the base unit while B3D uses mm. This causes most models to be imported way too small. This can be fixed by either pre-processing which is an unnecessary extra step that needs to be done every time, or by using Mesher.import() which can take minutes to import the model.

@gumyr gumyr added the enhancement New feature or request label Jan 17, 2025
@gumyr gumyr added this to the Not Gating Release 1.0.0 milestone Jan 17, 2025
@gumyr
Copy link
Owner

gumyr commented Jan 17, 2025

How about this?

from build123d import *
from ocp_vscode import show
from os import PathLike, fsdecode

from OCP.RWStl import RWStl
from OCP.gp import gp_Pnt, gp_Trsf
from OCP.Poly import Poly_Triangulation
from OCP.TopoDS import TopoDS_Face
from OCP.BRep import BRep_Builder
from OCP.TColgp import TColgp_Array1OfPnt


def import_stl(file_name: PathLike | str | bytes, model_unit: Unit = Unit.MM) -> Face:
    """import_stl

    Extract shape from an STL file and return it as a Face reference object.

    Note that importing with this method and creating a reference is very fast while
    creating an editable model (with Mesher) may take minutes depending on the size
    of the STL file.

    Args:
        file_name (Union[PathLike, str, bytes]): file path of STL file to import
        model_unit (Unit, optional): the default unit used when creating the model. For
            example, Blender defaults to Unit.M. Defaults to Unit.MM.

    Raises:
        ValueError: Could not import file
        ValueError: Invalid model_unit

    Returns:
        Face: STL model
    """
    # Read STL file
    reader = RWStl.ReadFile_s(fsdecode(file_name))

    # Check for any required scaling
    if model_unit == Unit.MM:
        face = TopoDS_Face()
        BRep_Builder().MakeFace(face, reader)
    else:
        conversion_factor = {
            Unit.MC: MC,  # MICRO
            Unit.MM: MM,  # MILLIMETER
            Unit.CM: CM,  # CENTIMETER
            Unit.M: M,  # METER
            Unit.IN: IN,  # INCH
            Unit.FT: FT,  # FOOT
        }
        try:
            scale_factor = conversion_factor[model_unit]
        except KeyError:
            raise ValueError(
                f"model_scale must one a valid unit: {Unit._member_names_}"
            )

        # Apply scaling transformation
        trsf = gp_Trsf()
        trsf.SetScaleFactor(scale_factor)

        # Access the nodes (vertices) of the triangulation
        num_nodes = reader.NbNodes()
        nodes = [reader.Node(i) for i in range(1, num_nodes + 1)]

        # Create a new array for the transformed nodes
        scaled_nodes = TColgp_Array1OfPnt(1, num_nodes)

        for i in range(1, num_nodes + 1):
            # Apply the scaling transformation to each node
            point = nodes[i - 1]
            scaled_point = gp_Pnt(point.XYZ())
            scaled_point.Transform(trsf)
            scaled_nodes.SetValue(i, scaled_point)

        # Create a new Poly_Triangulation with the scaled nodes
        scaled_triangulation = Poly_Triangulation(scaled_nodes, reader.Triangles())

        # Create TopoDS_Face from transformed triangulation
        face = TopoDS_Face()
        BRep_Builder().MakeFace(face, scaled_triangulation)

    return Face.cast(face)


benchy = import_stl("3DBenchy.stl")
big_benchy = import_stl("3DBenchy.stl", Unit.IN)
show(benchy, big_benchy)

Image

The SVG/DXF exporter uses Unit in a similar way so this would be consistent. The importer is fast even when scaling but displaying the benchy takes a while.

@jdegenstein
Copy link
Collaborator

I did some experimentation and came to a similar solution as @gumyr (and reused some of gumyr's code)

def import_stl_jdegenstein(file_name: PathLike | str | bytes, model_unit: Unit = Unit.MM) -> Face:
    """import_stl

    Extract shape from an STL file and return it as a Face reference object.

    Note that importing with this method and creating a reference is very fast while
    creating an editable model (with Mesher) may take minutes depending on the size
    of the STL file.

    Args:
        file_name (Union[PathLike, str, bytes]): file path of STL file to import
        model_unit (Unit, optional): the default unit used when creating the model. For
            example, Blender defaults to Unit.M. Defaults to Unit.MM.

    Raises:
        ValueError: Could not import file
        ValueError: Invalid model_unit

    Returns:
        Face: STL model
    """
    # Read STL file
    reader = RWStl.ReadFile_s(fsdecode(file_name))
    
    # Check for any required scaling
    if model_unit == Unit.MM:
        pass
    else:
        conversion_factor = {
            # Unit.MC: MC,  # MICRO
            Unit.MM: MM,  # MILLIMETER
            Unit.CM: CM,  # CENTIMETER
            Unit.M: M,  # METER
            Unit.IN: IN,  # INCH
            Unit.FT: FT,  # FOOT
        }
        try:
            scale_factor = conversion_factor[model_unit]
        except KeyError:
            raise ValueError(
                f"model_scale must one a valid unit: {Unit._member_names_}"
            )
        transformation = gp_Trsf()
        transformation.SetScaleFactor(scale_factor)
        
        node_arr = reader.InternalNodes()

        for i in range(reader.NbNodes()):
            node_arr.SetValue(i, node_arr.Value(i).Transformed(transformation))

    face = TopoDS_Face()
    BRep_Builder().MakeFace(face, reader)
    return Face.cast(face)

benchmark:

from time import time
fn = "flake.stl" # 5.6 million triangles
t1 = time()
a = import_stl_gumyr(fn, model_unit=Unit.MM)
print(time()-t1)
t1 = time()
a = import_stl_gumyr(fn, model_unit=Unit.IN)
print(time()-t1)
t1 = time()
b = import_stl_jdegenstein(fn, model_unit=Unit.MM)
print(time()-t1)
t1 = time()
b = import_stl_jdegenstein(fn, model_unit=Unit.IN)
print(time()-t1)

returns:

1.917168378829956
16.6307954788208
1.9274089336395264
9.02062463760376

My version is about 1.8x faster than gumyr's. I was hoping to avoid this method of transforming each gp_Pnt but it does not appear possible in OCCT currently.

@gumyr
Copy link
Owner

gumyr commented Jan 17, 2025

Nice!

@snoyer
Copy link
Contributor

snoyer commented Jan 18, 2025

@jdegenstein would transforming in-place save you a few milliseconds?

        for i in range(1, reader.NbNodes() + 1):
            p = reader.Node(i)
            p.Transform(transformation)
            reader.SetNode(i, p)

@jdegenstein
Copy link
Collaborator

@snoyer yes, great suggestion -- makes this version ~2.1x faster

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

4 participants