trimesh icon indicating copy to clipboard operation
trimesh copied to clipboard

docs: mesh.section returns disconnected paths

Open ita1024 opened this issue 3 years ago • 4 comments

The documentation for "mesh.section" in "trimesh/base.py" simply reads "Returns a 3D cross section of the current mesh and a plane defined by origin and normal."

It is unclear (even after looking at the code) if there is any guarantee to return a single continuous path if the faces are all connected. The documentation should at least mention that the segments may be disjoined (more than one entity is returned) if the faces do not form a continuous path (this is expected).

In the following example:

ply
format ascii 1.0
element vertex 7
property float x
property float y
property float z
element face 6
property list uchar int vertex_indices
end_header
-1 -1 0.2 
1 -1 -0.2 
1 1 0.2 
-1 1 0.2 
-1.713591 -1.700557 0.2026067 
1.672336 -1.688403 -0.472148 
-1.919373 1.393387 0.2 
3 0 1 2 
3 2 3 0 
3 0 4 1 
3 1 4 5 
3 3 6 0 
3 0 6 4 

and with such section:

import trimesh

mesh = trimesh.load_mesh('testmesh.ply')
section = mesh.section([-0.45180434118748247, 0.44599586172843414, 0.08584125159119675], [-1.71359118, -1.70055751, 0.20260674])

print("amount of entities", len(section.entities))
for i, entity in enumerate(section.entities):
        for point in entity.discrete(section.vertices):
                print(i, point)

Is it expected that two separate paths are obtained while the point (-1., -0.97717107, 0.2) is present in both? (tried on branches master/test-trimesh-3.15)

amount of entities 2
0 [-1.         -0.97717107  0.2       ]
0 [-1.00631278 -0.98356606  0.2       ]
0 [-1.71358146 -1.70054765  0.20260666]
0 [-1.71359059 -1.70055689  0.20260664]
0 [-1.71359069 -1.70055699  0.20260664]
1 [-1.         -0.97717107  0.2       ]
1 [0.9517522 1.        0.2      ]

ita1024 avatar Sep 18 '22 15:09 ita1024

Hey, you might want to look at the section.paths or section.discrete which are closed cycles. Entities aren't necessarily ordered or connected, it is a "curve soup":

In [1]: import trimesh

In [2]: m = trimesh.creation.box()

In [3]: s = m.section?
Signature: m.section(plane_normal, plane_origin, **kwargs)
Docstring:
Returns a 3D cross section of the current mesh and a plane
defined by origin and normal.

Parameters
------------
plane_normal: (3) vector for plane normal
  Normal vector of section plane
plane_origin : (3, ) float
  Point on the cross section plane

Returns
---------
intersections: Path3D or None
  Curve of intersection
File:      ~/trimesh/trimesh/base.py
Type:      method

In [5]: s = m.section(plane_normal=[0,0,1], plane_origin=[0,0,0])

In [6]: s
Out[6]: <trimesh.Path3D(vertices.shape=(8, 3), len(entities)=1)>

In [7]: s.discrete?
Type:        property
String form: <property object at 0x7f4d5ad6e660>
Docstring:  
A sequence of connected vertices in space, corresponding to
self.paths.

Returns
---------
discrete : (len(self.paths),)
    A sequence of (m*, dimension) float

In [8]: s.discrete
Out[8]: 
[TrackedArray([[ 0. , -0.5,  0. ],
               [-0.5, -0.5,  0. ],
               [-0.5,  0. ,  0. ],
               [-0.5,  0.5,  0. ],
               [ 0. ,  0.5,  0. ],
               [ 0.5,  0.5,  0. ],
               [ 0.5,  0. ,  0. ],
               [ 0.5, -0.5,  0. ],
               [ 0. , -0.5,  0. ]])]

In [10]: s.paths?
Type:        property
String form: <property object at 0x7f4d5b8cde40>
Docstring:  
Sequence of closed paths, encoded by entity index.

Returns
---------
paths : (n,) sequence of (*,) int
  Referencing self.entities

mikedh avatar Sep 21 '22 20:09 mikedh

With my example above, section.paths or section.discrete seem to return empty lists (but there are entities), so I am stitching the curve soup (or segment soup) in a custom function at this time (and this is rather difficult).

Under which conditions can section.path is supposed to return an empty list?

import trimesh

mesh = trimesh.load_mesh('testmesh.ply')
section = mesh.section([-0.45180434118748247, 0.44599586172843414, 0.08584125159119675], [-1.71359118, -1.70055751, 0.20260674])
for i, entity in enumerate(section.entities):
        for point in entity.discrete(section.vertices):
                print(i, point)
print(section.paths) # empty list
print(section.discrete) # empty list
plan_els, matrix = section.to_planar()
print(plan_els.discrete) # empty list

ita1024 avatar Sep 21 '22 21:09 ita1024

Hey, yeah section.paths will be empty if there are no closed cycles in the curve soup. If the mesh isn't watertight there may not be any closed cycles.

mikedh avatar Sep 22 '22 22:09 mikedh

Now I understand! It was really meant for watertight surfaces/closed cycles, but none of my meshes have those properties.

The points in the section also appear to differ sometimes (floating point values?). I found a way to obtain proper paths using mesh_plane directly with return_faces=True: calculating the connected faces in the faces result then helps match segments to their neighbors.

lines, faces = trimesh.intersections.mesh_plane(
  mesh=mesh,
  plane_normal=[0,0,1],
  plane_origin=[0,0,0],
  return_faces=True,
  local_faces=my_faces)

I am not familiar enough with the library or Numpy to suggest a patch, but the information above would have saved me some time if I had found it in the doc strings.

ita1024 avatar Sep 23 '22 15:09 ita1024