wassimj / topologicpy

The python bindings for topologic
MIT License
73 stars 19 forks source link

A method to add color to ExportToOBJ #60

Open gaoxipeng opened 1 week ago

gaoxipeng commented 1 week ago

Dear @wassimj , Hello, The current ExportToOBJ only supports exporting geometric data without colors. I attempted to modify the existing function to accept multiple topology elements, colors, and transparency. Possibly due to changes in the base functions, the current version of the Roof function is incorrect. Therefore, I am using version 0.6.0 of topologicpy with a test roof function. This method can produce an OBJ file with colors, but it does not support the current ByOBJFile function. I am unsure if this method is worth further discussion. I look forward to your testing and reply.

from topologicpy.Vertex import Vertex
from topologicpy.Wire import Wire
from topologicpy.Cell import Cell
from topologicpy.Cluster import Cluster
from topologicpy.Topology import Topology
from topologicpy.Face import Face
from topologicpy.Shell import Shell

def ExportToOBJ(topologies, colors, faceOpacity, path, transposeAxes: bool = True, mode: int = 0,
                meshSize: float = None,
                overwrite: bool = False, mantissa: int = 6, tolerance: float = 0.0001):
    from os.path import exists

    if len(topologies) != len(colors):
        print("Error: the number of topologies and colors must be the same. Returning None.")
        return None

    if not overwrite and exists(path):
        print(
            "Topology.ExportToOBJ - Error: a file already exists at the specified path and overwrite is set to False. Returning None.")
        return None

    # Make sure the file extension is .obj
    ext = path[len(path) - 4:len(path)]
    if ext.lower() != ".obj":
        path = path + ".obj"
    status = False
    objString = ""
    mtlPath = path[:-4] + ".mtl"
    with open(mtlPath, "w") as mtlFile:
        for i in range(len(colors)):
            mtlFile.write("newmtl color" + str(i) + "\n")
            mtlFile.write("Kd " + ' '.join(map(str, colors[i])) + "\n")
            mtlFile.write("d " + str(faceOpacity[i]) + "\n")
    vertexIndex = 1  # global vertex index counter
    for i in range(len(topologies)):
        if isinstance(topologies[i], list) and len(topologies[i]) == 0:  # Skip if the topology is an empty list
            continue
        result = OBJString(topologies[i], "color" + str(i), vertexIndex, transposeAxes=transposeAxes, mode=mode,
                           meshSize=meshSize,
                           mantissa=mantissa, tolerance=tolerance)
        objString += result[0]
        vertexIndex += result[1]
    with open(path, "w") as f:
        f.write("mtllib " + mtlPath.split('/')[-1] + "\n")  # reference the MTL file
        f.writelines(objString)
        f.close()
        status = True
    return status

def OBJString(topology, color, vertexIndex, transposeAxes: bool = True, mode: int = 0, meshSize: float = None,
              mantissa: int = 6,
              tolerance: float = 0.0001):
    from topologicpy.Helper import Helper

    if not Topology.IsInstance(topology, "Topology"):
        print("Topology.ExportToOBJ - Error: the input topology parameter is not a valid topology. Returning None.")
        return None

    lines = []
    version = Helper.Version()
    lines.append("# topologicpy " + version)
    topology = Topology.Triangulate(topology, mode=mode, meshSize=meshSize, tolerance=tolerance)
    d = Topology.Geometry(topology, mantissa=mantissa)
    vertices = d['vertices']
    faces = d['faces']
    tVertices = []
    if transposeAxes:
        for v in vertices:
            tVertices.append([v[0], v[2], v[1]])
        vertices = tVertices
    for v in vertices:
        lines.append("v " + str(v[0]) + " " + str(v[1]) + " " + str(v[2]))
    for f in faces:
        line = "usemtl " + str(color) + "\nf"  # reference the material name
        for j in f:
            line = line + " " + str(j + vertexIndex)
        lines.append(line)
    finalLines = lines[0]
    for i in range(1, len(lines)):
        finalLines = finalLines + "\n" + lines[i]
    return finalLines, len(vertices)

def Roof(face, degree=45, tolerance=0.1):
    def nearest_vertex_2d(v, vertices, tolerance=0.1):
        for vertex in vertices:
            x2 = Vertex.X(vertex)
            y2 = Vertex.Y(vertex)
            temp_v = Vertex.ByCoordinates(x2, y2, Vertex.Z(v))
            if Vertex.Distance(v, temp_v) <= tolerance:
                return vertex
        return None

    originalface = face
    degree = abs(degree)
    if degree >= 90 - tolerance:
        return None
    if degree < tolerance:
        return None
    roof = Wire.Roof(face, degree)
    if not roof:
        return None
    shell = Shell.Skeleton(face)
    faces = Shell.Faces(shell)

    if not faces:
        return None

    roof_vertices = Topology.Vertices(roof)

    final_faces = []
    for face in faces:
        wire = Face.Wire(face)
        face_vertices = Wire.Vertices(wire)
        top_vertices = []
        for sv in face_vertices:
            temp = nearest_vertex_2d(sv, roof_vertices, tolerance=tolerance)
            if temp:
                top_vertices.append(temp)
            else:
                top_vertices.append(sv)
        topface = Face.ByVertices(top_vertices)
        final_faces.append(topface)

    shell = Cluster.ByTopologies(final_faces)

    shell = Topology.RemoveCoplanarFaces(shell, epsilon=1)
    faces = Topology.Faces(shell) + [originalface]
    cell = Cell.ByFaces(faces)
    if not cell:
        return Cluster.ByTopologies(faces)
    return cell

f1 = Face.Star(radiusA=100, radiusB=200)
cell = Cell.ByThickenedFace(face=f1, thickness=100, bothSides=False)
f2 = Topology.Translate(f1, z=100)
roof = Roof(f2)
Topology.Show(roof,renderer='offline')
ExportToOBJ(topologies=[cell, roof], colors=[[0.5, 0.5, 0.5], [1, 0, 0]], faceOpacity=[0.5, 1], path='test.obj',
            overwrite=True)

image

d3a8ae2824897241b4c8d14b608b9dc
wassimj commented 1 week ago

Thanks. First I would like to correct the roof method. Please give me details on what is not working about it? Thanks

gaoxipeng commented 1 week ago

Sorry not next to the computer right now, my earlier attempt was probably because of the Polyskel.skeletonize(eb_polygonxy, ib_polygonsxy) two parameters

wassimj commented 1 week ago

No rush. When you can please let me know where and how it fails so I can fix it. Thanks

gaoxipeng commented 1 week ago

Dear @wassimj , Here is the code that may cause the issue:

from topologicpy.Cell import Cell
from topologicpy.Topology import Topology
from topologicpy.Face import Face
from topologicpy.Shell import Shell

f1 = Face.Rectangle(width=100, length=100)
cell = Cell.ByThickenedFace(face=f1, thickness=100, bothSides=False)
f2 = Topology.Translate(f1, z=100)
roof = Cell.Roof(f2)
Topology.Show(roof, renderer='offline')

in Shell.Roof line 1370: origin = Topology.Centroid(face) normal = Face.Normal(face, mantissa=mantissa) flat_face = Topology.Flatten(face, origin=origin, direction=normal) but not UnFlatten,so use ‘faces = Topology.Faces(shell) + [face] cell = Cell.ByFaces(faces, tolerance=tolerance) ’in Cell.Roof in line 1850 cannot creat a cell.

Still the above test code, if run multiple times, in Shell.py line 1524: br = wire. BoundingRectangle(roof) #This works even if it is a Cluster not a Wire in Wire.py in line 207:w = Wire.ByVertices(vList) f = Face.ByWire(w, tolerance=tolerance), It is still possible to select three collinear vertices and create a Wire that cannot create a face. Also, I've been confused about why in Wire.py line227 use factor = (round(((11-optimize)/30 + 0.57), 2),What was the reason for choosing these values. In my process, I usually only work on horizontal 2D planes, so I use the following approach:

def BoundingRectangle(topology):
    z = Topology.Vertices(topology)[0].Z()

    def br(topology):
        vertices = []
        _ = topology.Vertices(None, vertices)
        x = []
        y = []
        for aVertex in vertices:
            x.append(aVertex.X())
            y.append(aVertex.Y())
        minX = min(x)
        minY = min(y)
        maxX = max(x)
        maxY = max(y)
        return [minX, minY, maxX, maxY]

    boundingRectangle = br(topology)
    minX = boundingRectangle[0]
    minY = boundingRectangle[1]
    maxX = boundingRectangle[2]
    maxY = boundingRectangle[3]
    w = abs(maxX - minX)
    l = abs(maxY - minY)
    orig_area = l * w
    origin = Topology.Centroid(topology)

    min_area = orig_area
    best_angle = 0

    # Rotate from 0 to 90 degrees
    for angle in range(0, 91, 1):
        t = Topology.Rotate(topology, origin=origin, angle=angle)
        minX, minY, maxX, maxY = br(t)
        w = abs(maxX - minX)
        l = abs(maxY - minY)
        area = l * w

        # Update the minimum area and best angle
        if area < min_area:
            min_area = area
            best_angle = angle

    # Now, best_angle is the rotation angle that gives the minimum area
    best_br = br(Topology.Rotate(topology, origin=origin, angle=best_angle))

    minX, minY, maxX, maxY = best_br
    vb1 = Vertex.ByCoordinates(minX, minY, z)
    vb2 = Vertex.ByCoordinates(maxX, minY, z)
    vb3 = Vertex.ByCoordinates(maxX, maxY, z)
    vb4 = Vertex.ByCoordinates(minX, maxY, z)

    boundingRectangle = Wire.ByVertices([vb1, vb2, vb3, vb4], close=True)
    boundingRectangle = Topology.Rotate(boundingRectangle, origin=origin, angle=-best_angle)

    return boundingRectangle
wassimj commented 1 week ago

First things first. Shell.Roof was not Unflattening the resulting shell. This has been corrected and now Cell.Roof works correctly. This will be included in the next release.

image
wassimj commented 1 week ago

Second: You wrote:

Still the above test code, if run multiple times, in Shell.py line 1524: br = wire. BoundingRectangle(roof) #This works even if it is a Cluster not a Wire
in Wire.py in line 207:w = Wire.ByVertices(vList)
f = Face.ByWire(w, tolerance=tolerance), It is still possible to select three collinear vertices and create a Wire that cannot create a face.

Not sure I understand fully. Do you mean that I should first check if the vertices are collinear and thus I should reject that and return None because no bounding rectangle can be created from collinear vertices? If so, I have implemented that and it will be part of the next release. If you meant something else, please explain further. Thank you!

gaoxipeng commented 1 week ago

Yes, there's a chance that three collinear vertices could be randomly selected to form a wire, but they cannot further form a face.

wassimj commented 1 week ago

Third: You wrote: Also, I've been confused about why in Wire.py line227 use factor = (round(((11-optimize)/30 + 0.57), 2),What was the reason for choosing these values.

That is truly the weirdest line of code in my software. I cannot tell why I wrote it that way! I must've had a reason, but now I cannot read it back. I will take a look at your code and see if I can use it instead.

wassimj commented 1 week ago

Yes, there's a chance that three collinear vertices could be randomly selected to form a wire, but they cannot further form a face.

In the latest version, I believe I keep selecting vertices until I find three that are not collinear.

wassimj commented 1 week ago

Please test v0.7.21 to see if the issues with skeleton and roof are corrected. I will then start on OBJ import/export with colour. Thanks!

gaoxipeng commented 1 week ago

The current version of the roof is correct, thank you for the repair🫡

wassimj commented 5 days ago

Dear @gaoxipeng , Topologicpy version 0.7.22 now supports color and opacity export and import. Please test and let me know if you find any bugs. The API for these methods has changed so please consult documentation for the correct input parameters and expected output. Thanks!

wassimj commented 5 days ago
prism01 prism02 prism03 prism04 prism05
gaoxipeng commented 5 days ago

Dear @wassimj , Thanks for your modification, it is working normally, You may need to modify the comment section at the beginning of the code.

wassimj commented 4 days ago

Dear @gaoxipeng, Thanks for spotting the wrong comments. I have updated them and issued a new version (0.7.23)