Determining if a point is inside or outside a Body2

Hi, I need to code a function that discretizes a solid body (Body2) into a 3D point cloud with a certain resolution.

The algorithm would be like this:

  1. Determine the bounding box of the body;
  2. Create a 3D point cloud array;
  3. For each point, determine if a point is inside or outside the body;
  4. Remove all the points that are outside the body.

This can be optimized, but this way it’s easier to explain. I don’t need to visualize these points or output them back to SW, just keep them in my code.

The problem is step #3. I can’t seem to find any API calls on Body2 that would check if a point at certain XYZ coordinates is inside or outside the body. It could possibly be done with rays, but I am expecting to have millions of points to check, so I need this procedure to be as fast as possible, preferably using only one fast API call per point.

Can anyone suggest how to do this?

Use IModelDocExtension::RayIntersections.
Instead of checking millions of individual points, cast rays through the body along one axis. For each ray, SOLIDWORKS can return the ENTER and EXIT intersections, so all grid points between each pair are inside the body.

Two main approaches:
RayIntersections with batched rays — easier, uses exact SOLIDWORKS body geometry, and is quite suitable for VBA.
Tessellate the body and perform point-in-mesh tests yourself — potentially faster for very large datasets, but much more complex and based on an approximated mesh.
for a SOLIDWORKS VBA macro, I would choose RayIntersections. It gives the best balance of speed, simplicity, and reliability.

For speed in VBA, use typed Double() arrays rather than Variant() arrays wherever possible—for example, one flat array storing X, Y, Z, X, Y, Z…
Also avoid repeated ReDim Preserve, because it becomes very expensive with millions of points.

batched RayIntersections + flat Double() arrays is probably the best VBA solution.


Or, instead of transmitting millions of XYZ values, you could use more compact data:
X, Y, Z_enter, Z_exit
and have the external module generate the points with the required step size, for example, in C#.

Next, everything depends on whether the points should be in an orthogonal grid; if so, there’s some tolerance for overlap. How do you choose the base plane for the ray flow, etc.?

Thank you very much! This would actually use far fewer API calls than I imagined, I had completely forgotten that RayIntersection can do multiple rays at once, so it’s actually just a few API calls for the entire body, and then lots of internal logic to figure out the points.

By the way, did you have that graphic prepared for cases such as this? It looks like I’m not the first one to ask this :slight_smile:

I’ve been using AI a lot lately—I explain the concept to it and ask it to create an illustration. It only worked on the third try, and even then, there are some inaccuracies in the diagram. But it’s easier to understand the general principle this way.

Okay, I figured it out using the ray method. Sharing the macro in case someone else needs something like this.

I didn’t use swRayPtsOptsENTRY_EXIT when calling RayIntersections because it makes this API call ~3x slower. Instead I wrote my own algorithm that checks if a point to be created is inside or outside the body by checking if it is between the odd and even index of the ray intersections (because every ray must have at least two intersections - entering and exiting the body - except for grazing cases). Pretty simple but much faster.

Unfortunately GetIntersections sometimes returns multiple intersections at the exact same or just slightly different coordinates (overlapping points), probably due to numerical errors in the kernel, so I had to write a cleanup function that eliminates duplicates. This however means that my algorithm doesn’t work for multibodies currently, I’m still trying to figure out that part.

Also, very small bodies might be an issue because there are often quite large numerical errors in the coordinates returned by RayIntersectionsPoints, so when determining duplicates I had to use rounding down, controlled with ROUNDING_DIGITS constant. By default I left it at 3 (1mm), seems to work with most models. With simple analytical shapes you can go as high as 8, non analytical shapes sometimes requires all 3. Haven’t found a way to work around it. Also you can control whether to use high precision rays in the RayIntersections method with HIGH_PRECISION_RAYS constant, but I set it to False by default because for some reason True results in less precision (have to use smaller ROUNDING_DIGITS value). No noticeable difference in performance though.

Point cloud is created by subdividing the model bounding box into approximately cubic spaces, and placing points in the vertices of these spaces, inside the model body. Approximate number of points per edge is controlled by NUM_OF_EDGE_POINTS constant at the top of the macro. Be careful with that number, as the number of point clouds grows to the third power.

I included some drawing functions to visualize the ray origins (controlled by constants at the top of the macro), intersection points and the final point cloud by outputting them to a 3D sketch.

Example test part:

Point cloud (NUM_OF_EDGE_POINTS = 50, intersections found 2028, total of 12379 points):

Point creation took 0.72 s (of which 0.37s is RayIntersections call, GetRayIntersectionsPoints took 0 s, and the rest is my code), and drawing time took 12.70 s (with SketchMgr.AddToDB set to True).

So yeah, performance is not great, RayIntersections still takes ~0.49ms per intersection, but I suppose any other method would take even longer…

Anyway, just sharing these findings in case someone else needs to do a volumetric cell mesh of the model or something.

Point cloud V1.swp (177.5 KB)

So why are you ignoring useful information by just using the coordinates?

chunkedPointCoords(0) = chunkedPoint(3)
chunkedPointCoords(1) = chunkedPoint(4)
chunkedPointCoords(2) = chunkedPoint(5)

If you keep the BodyIndex and RayIndex, you can definitely tell whether it’s a duplicate or not, especially since you round the coordinates to 1 mm—you could be missing out on “good” points.

 0 = BodyIndex
 1 = RayIndex
 2 = IntersectionType
 3 = X
 4 = Y
 5 = Z

My opinion is not to round coordinates at all, but to store them with RayIndex.

And your rule with “if odd” - reduce “ROUNDING_DIGITS” - doesn’t look “nice.” We could have simply stored the IntersectionType and immediately known that we’re dealing with a Regular Face, Tangent, Edge, or Vertex.

  • FACE - regular parity processing
  • SILHOUETTE - special case / ignore for switching inside/outside
  • EDGE/VERTEX - check for duplicates with tolerance.

Oh, and you have a lot of Integers, where there are already Longs, it won’t be enough if you pile on more points. But those are minor details… very cool.

Thanks, - yeah, the BodyIndex could indeed help with multibodies, I had that idea, but didn’t get around to making it work.

As for duplicates, that’s the issue - sometimes duplicates happen with a single-body part, along the same ray. Ray enters the body, so it should be just 1 intersection there, but I get 5 or 6 sometimes, and same for the exit point. Wait, now that I think about it, perhaps this is because it strikes not just a face, but also edge/vertex, so it produces an intersection for each type… Yeah, that’s probably it. I’ll test it out now.

As for Integers/Longs - yeah, you’re right about that too, I haven’t tested this macro with larger number of points yet, so I haven’t run into that issue (yet).

Yes, I agree that the duplicate intersections can still occur on a single body and a single ray, so BodyIndex/RayIndex alone wont eliminate that problem.

But I think there is a small misunderstanding about IntersectionType. The documentation says only the ENTER/EXIT bits require swRayPtsOptsENTRY_EXIT. FACE, SILHOUETTE, EDGE and VERTEX are still returned in IntersectionType without that option.

So you can keep your fast RayIntersections(..., Options:=0, ...) call and still use IntersectionType to investigate those duplicate hits.

Also, ENTRY_EXIT does not change the result record from 6 to 9 values. It just ORs ENTER or EXIT into IntersectionType. The additional three values are the surface normal nx, ny, nz, and appear when swRayPtsOptsNORMALS is requested.

For duplicates, I would probably group by RayIndex + BodyIndex, then cluster only intersections with nearly equal Z and inspect/OR their IntersectionType flags. That seems safer than rounding the entire XYZ coordinate to 1 mm. :slightly_smiling_face:

Yeah, you’re right in your second paragraph, I misunderstood what you meant in your previous reply but didn’t edit my message fast enough :smiley:

As for using / not using swRayPtsOptsENTRY_EXIT, I beg to differ. Here is what GetRayIntersectionsPoints returns when RayIntersections was called with swRayPtsOptsENTRY_EXIT, :

And this is without (Options = 0):

As you can see, array size is different, and the chunk size is clearly different (9 vs 6). Not sure why.

I will try to rewrite the duplicate/entry/exit finding function… Will take me a bit of time, you are clearly much more experienced than I am :slight_smile:

Give me your test model .sdlprt for testing.

Here is one I’m using, just a bunch of weird geometry to try and provoke edge cases (pun intended)

PointCloudTest sw2024.SLDPRT (142.5 KB)

EDIT: I realize with the discovery of how these duplicate points happen, this is not the best model to test for it, I will soon make another.

I rewrote the macro a bit, added debugging and used RayIndex and BodyIndex.
from Debug:

==============================================================
SUSPICIOUS RAY / BODY
RayIndex  = 199
BodyIndex = 0
Ray origin [mm]: X=763,513514  Y=583,000000  Z=-924,066924
Raw hits        = 9
Clusters        = 2
Parity crossings= 2

--- RAW HITS ---
raw[0]  XYZ[mm]=(763,513514, 583,000000, -842,342342)  dZ=-  Type=4 [&H4]  EDGE
raw[1]  XYZ[mm]=(763,513514, 583,000000, -842,342342)  dZ=0,000000  Type=4 [&H4]  EDGE
raw[2]  XYZ[mm]=(763,513514, 583,000000, -842,342342)  dZ=0,000000  Type=4 [&H4]  EDGE
raw[3]  XYZ[mm]=(763,513514, 583,000000, -842,342342)  dZ=0,000000  Type=8 [&H8]  VERTEX
raw[4]  XYZ[mm]=(763,513514, 583,000000, -842,342342)  dZ=0,000000  Type=1 [&H1]  FACE
raw[5]  XYZ[mm]=(763,513514, 583,000000, -416,666667)  dZ=425,675676  Type=1 [&H1]  FACE
raw[6]  XYZ[mm]=(763,513514, 583,000000, -416,666667)  dZ=0,000000  Type=4 [&H4]  EDGE
raw[7]  XYZ[mm]=(763,513514, 583,000000, -416,666667)  dZ=0,000000  Type=4 [&H4]  EDGE
raw[8]  XYZ[mm]=(763,513514, 583,000000, -416,666667)  dZ=0,000000  Type=8 [&H8]  VERTEX

--- CLUSTERS ---
cluster[0]  Z=-842,342342 mm  members=5  Type=13 [&HD]  FACE|EDGE|VERTEX  -> TOGGLE
cluster[1]  Z=-416,666667 mm  members=4  Type=13 [&HD]  FACE|EDGE|VERTEX  -> TOGGLE
==============================================================

That is, one physical entry point generated 5 records because the ray hit several topological objects simultaneously.
Z = -842.342342 mm
EDGE + EDGE + EDGE + VERTEX + FACE
The output is the same - FACE, EDGE, EDGE, VERTEX
This is very good; I didn’t see any direct kernel errors (although I’m not ruling them out).
Your “ROUNDING_DIGITS = 3” in a significant number of cases actually didn’t correct the numerical error, but simply accidentally merged multiple results from the same intersection.
** You can see that “dZ=0.000000”
even when outputting to the sixth decimal place. That is, the difference is less than approximately 0.0000005 mm, if it exists at all.
Therefore, a 1 mm tolerance is completely excessive here. I tested (in my code) DUPLICATE_Z_TOL_M As Double = 0.001 and now you can make 0.000001 m = 0.001 mm = 1 µm and retest, well, play around.

Also very interesting are these:

==============================================================
SUSPICIOUS RAY / BODY
RayIndex  = 195
BodyIndex = 0
Ray origin [mm]: X=763,513514  Y=235,000000  Z=-924,066924
Raw hits        = 7
Clusters        = 4
Parity crossings= 4

--- RAW HITS ---
raw[0]  XYZ[mm]=(763,513514, 235,000000, -842,342342)  dZ=-  Type=1 [&H1]  FACE
raw[1]  XYZ[mm]=(763,513514, 235,000000, -842,342342)  dZ=0,000000  Type=4 [&H4]  EDGE
raw[2]  XYZ[mm]=(763,513514, 235,000000, -680,653741)  dZ=161,688602  Type=4 [&H4]  EDGE
raw[3]  XYZ[mm]=(763,513514, 235,000000, -680,653741)  dZ=0,000000  Type=1 [&H1]  FACE
raw[4]  XYZ[mm]=(763,513514, 235,000000, -549,578715)  dZ=131,075025  Type=1 [&H1]  FACE
raw[5]  XYZ[mm]=(763,513514, 235,000000, -416,666667)  dZ=132,912049  Type=1 [&H1]  FACE
raw[6]  XYZ[mm]=(763,513514, 235,000000, -416,666667)  dZ=0,000000  Type=4 [&H4]  EDGE

--- CLUSTERS ---
cluster[0]  Z=-842,342342 mm  members=2  Type=5 [&H5]  FACE|EDGE  -> TOGGLE
cluster[1]  Z=-680,653741 mm  members=2  Type=5 [&H5]  FACE|EDGE  -> TOGGLE
cluster[2]  Z=-549,578715 mm  members=1  Type=1 [&H1]  FACE  -> TOGGLE
cluster[3]  Z=-416,666667 mm  members=2  Type=5 [&H5]  FACE|EDGE  -> TOGGLE
==============================================================

OUTSIDE-INSIDE-OUTSIDE-INSIDE-OUTSIDE

At the very beginning, you said that you don’t need to display the points graphically in the application itself - they can be passed on.
This, of course, does not include full debug output, only execution information.

======================================
Point creation: 0,461 s
Rays:           200
Raw hits:       552
Cloud points:   891
======================================
Sketch 'Point Cloud' contains 891 points.
Drawing:        1,367 s

I will try many more bodies and many more points.

This is where things get interesting: we’ve entered one body and hit a tangent to the edge of another—meaning we’ve been in the spatial frame for some time, and a point has appeared from which there’s no entry or exit. I think we’ll need to use the “expensive” Entry/Exit call here, which will show what’s happening, but only for these “special” points.


Sure — I updated the macro to make the ray-based approach more robust and multibody-safe.

Main changes: it now keeps BodyIndex, RayIndex, and IntersectionType instead of only XYZ coordinates; intersections are grouped by RayIndex + BodyIndex; coincident FACE/EDGE/VERTEX hits are clustered into one geometric event; ambiguous edge/vertex cases are resolved with an ENTRY_EXIT fallback only for the affected ray; multibody intervals are merged correctly; and the main arrays now use Long and flat Double() storage for better scalability.

I also added debug output for suspicious rays, so cases with 5–6 intersections at the same location can be inspected directly. In testing, these turned out to be multiple topological hits at exactly the same geometric point, e.g. FACE + EDGE + VERTEX, rather than coordinate noise.

Point cloud V2.1.swp (399.5 KB)

Thank you very much, mikhlov, for the time you invested into this, and all the interesting edge cases you found. I am checking the updated macro, and I can see that nearly all of it was generated/rewritten by AI. Hope you don’t take this personally, but I am strictly against any AI written code because as a self-taught developer I find it dangerously unreliable, difficult to understand and dumbing down my own skills. Also part of your reply seems to be copy-pasted from AI generated text, which is really weird. So rather than using that macro directly for my use case, I’ll try to use that new algorithm as an idea/suggestion to improve my original version in a way that I can fully understand. I’ll get on it right away. Thank you once again.

Maybe it’s because I’m a virtual persona and completely AI-controlled to monitor the engineering community. :robot: :police_car_light:

English isn’t my native language, so I use AI to translate my code so it’s in context. Most of the helper functions in the macro are easily written by AI, and I always run the code through AI for verification and formatting.
It seems I’m AI-friendly. :mouse_trap:
I initially consider myself a designer; the SW-API is a hobby.
You have a very interesting task. I hope you’re not writing software for combat lidars that will reliably determine a person’s silhouette? :face_with_monocle:

I see, thank you for being open about it :slight_smile: No, it’s for a personal/commercial project where I am trying to offload complex volumetric body simulations from SW API to much faster internal code. Not combat lidars, but still very important to get this right, lives do depend on it in a sense. So you can understand why I wouldn’t trust AI generated code for it. Still, you gave me a plenty of ideas to try out :slight_smile: