iTwin / itwinjs-core

Monorepo for iTwin.js Library
https://www.itwinjs.org
MIT License
615 stars 210 forks source link

Create 3D element based on non planar closed loops with holes defined as closed loops ? #1330

Closed griemens closed 3 years ago

griemens commented 3 years ago

I am using iTwinJS api to convert a CAD file to a BIM file to eventually push to an iModel on iModelHub. To check correct conversion I am using the Desktop starter application for the time being. So far all works great boxes, cones, triangle meshes and rotationalsweeps.

Now I have a 3D element described as an outer contour and inner contours generally defining holes. But inner contours can also intersect and the overlapping part should be rendered as a surface.

Is there something in the Geometry core API I can use for describing such elements or tesselate them ? Typically I would use GluTess (GLU Tesselation) for such polygons.

Thank you for any tips you can provide.

pmconne commented 3 years ago

@EarlinLutz

EarlinLutz commented 3 years ago

If the loops are in a ParityRegion, the faceted parity region will be handled as you describe -- a singly-overlapped part is "in" because it is part of 3 loops (the outer and a pair of inners.)

EarlinLutz commented 3 years ago

(I'm wary of the "non-planar" point -- please show some snips to get a feel for that ....)

griemens commented 3 years ago

Thank you for the tip will try out the ParityRegion.

Related to the none planar issue. Imagine you have a curved wall, and in the wall a curved window is added. The frame of the window is created with outer closed loop and an inner closed loop. => Although is an exceptional case I have seen this in some CAD data described as inner and outer loops. Other examples I have encountered are with curved metal grids, where the holes are created with inner loops.

But if ParityRegion can not handle such cases, will add a test if points/curves are on plane. If not fall back to different algorithm. Maybe using a library like "tess2-ts"

EarlinLutz commented 3 years ago

image When you say inner loops can intersect, is it something like the outer+2Inners on the right of this png?

griemens commented 3 years ago

Yes correct. Where expected result in displayed view would be innerloop_boolean

EarlinLutz commented 3 years ago

That is the behavior we expect from a ParityRegion.

griemens commented 3 years ago

Thanks for the help EarlinLutz it is working great using ParityRegion.

Code snipper I am using

     const builder = new GeometryStreamBuilder();
     ...
     for(var surface of element.surfaces) {
         const loops: Loop[] = [];
         for(var contour of surface.contours) {
             const linestring = LineString3d.create(
                    contour.vertices.map(v => Point3d.create(v.x,v.y,v.z)));
             linestring.addClosurePoint();
             const loop = Loop.create(linestring);
             loops.push(loop);
         }
         const region = ParityRegion.createLoops(loops);
         builder.appendGeometry(region);
     }

Where element is of type

const element = { 
     surfaces: {
        contours: {
            vertices: Vector3d[],
        }[],
     }[],
}
EarlinLutz commented 3 years ago

Hi -- It's great to hear that the parityregion works nicely.

Here's something you might want to use for the truly non-planar wall case.

First, build the original (windowless) wall as a mesh.

Second, build a ConvexClipSet for each window. (Ask if you need some code fragments for doing that. Getting orientation right can be confusing.)

Then send the array of ConvexClipPlaneSet into the function dParityClipMultipleSteps included at the bottom of this. It returns "inside" and "oustide" parts.

Here's an example of a curved with 4 window punches . . .

image

// Use a ConvexClipPlaneSet to clip 2 meshes gathered as 2-element array.
// * Return another array of two meshes.
//    * resultMesh[0] contains (a) the parts inputMesh[0] INSIDE the clipper and (b) parts of inputMesh[1] OUTSIDE the clipper.
//    * resultMesh[1] contains (a) the parts of inputMesh[0] OUTSIDE the clipper and (b) parts of inputMesh[2] INSIDE the clipper.
// * To compute the XOR of an initial mesh against many clippers:
//    * The input array for the first call is [originalMesh, undefined]
//    * pass the array of 2 output from each XOR step on to the next step.
// Calling this repeatedly produces the XOR (i.e. parity clip) of the complete set of clippers.
function doParityClipSingleStep(inputMesh: (IndexedPolyface | undefined)[], clipper: ConvexClipPlaneSet): (IndexedPolyface | undefined)[]{
  const outputHandler = ClippedPolyfaceBuilders.create(true, true, false);
  if (inputMesh[0] !== undefined)
    PolyfaceClip.clipPolyfaceConvexClipPlaneSetToBuilders(inputMesh[0], clipper, outputHandler);
  // swap the builders to get parity effects on the other part of the input.
  const a = outputHandler.builderA;
  outputHandler.builderA = outputHandler.builderB;
  outputHandler.builderB = a;
  if (inputMesh[1] !== undefined)
    PolyfaceClip.clipPolyfaceConvexClipPlaneSetToBuilders(inputMesh[1], clipper, outputHandler);

  return [outputHandler.builderB?.claimPolyface(), outputHandler.builderA?.claimPolyface()];
}
// Apply multiple clippers to a mesh, passing results forward as XOR (aka parity) logic.
// Return a 2-element array with the final IN and OUT parts
function dParityClipMultipleSteps(fullWallMesh: IndexedPolyface, clipperArray: ConvexClipPlaneSet[]): (IndexedPolyface | undefined)[]{
let currentPair = [fullWallMesh, undefined];
for (const clipper of clipperArray) {
  currentPair = doParityClipSingleStep(currentPair, clipper);
  }
  return currentPair;
}
griemens commented 3 years ago

Excellent !! Thank you for the help and example. I should be able to figure out how to build a ConvexClipSet and handle the orientation properly.