/* jCAE stand for Java Computer Aided Engineering. Features are : Small CAD
modeler, Finite element mesher, Plugin architecture.
Copyright (C) 2003,2006 by EADS CRC
Copyright (C) 2007-2011, by EADS France
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.jcae.mesh.amibe.algos3d;
import org.jcae.mesh.amibe.ds.AbstractHalfEdge;
import org.jcae.mesh.amibe.ds.Mesh;
import org.jcae.mesh.amibe.ds.HalfEdge;
import org.jcae.mesh.amibe.ds.Vertex;
import org.jcae.mesh.amibe.ds.Triangle;
import org.jcae.mesh.amibe.projection.MeshLiaison;
import org.jcae.mesh.xmldata.MeshReader;
import org.jcae.mesh.xmldata.MeshWriter;
import org.jcae.mesh.amibe.traits.MeshTraitsBuilder;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.lang.Math;
/**
* Remove degenerated triangles. Triangles whose edge ratio is greater than a
* given (large) tolerance are removed. Such triangles are sorted out and
* removed in turn, those of largest edge ratio processed first. The shortest
* edge of such a triangle is collapsed into its middle vertex. Works with
* non manifold edges as well.
*
* Remark: the edge ratio is the ratio between the longest edge and the
* shortest edge of a triangle.
*
* Example: if a mesh m has already been performed, the following commands will
* remove triangles of edge ratio greater than 50.0 (default value is 100.0).
* <pre>
* RemoveDegeneratedTriangles rdt = new RemoveDegeneratedTriangles(m, 50.);
* rdt.compute();
* </pre>
*
* WARNING: this algorithm is intended to clean a mesh, hence all regular
* edges (i.e. not OUTER) except IMMUTABLE are processed. Neither free edges
* nor ridges should be tagged as IMMUTABLE (unless you know what you are
* doing). This makes possible to remove small edges on ridges without changing
* actually the geometry.
*
* TODO: process flat triangles with bad aspect ratio but good edge ratio by
* first splitting them into two degenerated triangles and updating the tree.
*/
public class RemoveDegeneratedTriangles extends AbstractAlgoHalfEdge
{
private static final Logger LOGGER = Logger.getLogger(
RemoveDegeneratedTriangles.class.getName());
private Vertex collapseVertex = null;
private HalfEdge maxRatioHE = null;
private double maxRatio;
/**
* Creates a <code>RemoveDegeneratedTriangles</code> instance.
*
* @param m the <code>Mesh</code> instance to refine.
* @param options map containing key-value pairs to modify algorithm
* behaviour. Valid key is <code>rho</code>.
*/
public RemoveDegeneratedTriangles(final Mesh m, final Map<String, String> options)
{
this(m, null, options);
}
public RemoveDegeneratedTriangles(final MeshLiaison liaison, final Map<String, String> options)
{
this(liaison.getMesh(), liaison, options);
}
public RemoveDegeneratedTriangles(final Mesh m, final MeshLiaison meshLiaison, final Map<String, String> options)
{
super(m, meshLiaison);
/* Allow edges on ridges to be collapsed */
/* TODO: how to ensure it has not been done already? */
minCos = -2.0;
/* Do not swap after removing triangles */
setNoSwapAfterProcessing(true);
/* Only 'a few' triangles should be removed */
setProgressBarStatus(10);
/* Tolerance is 1 / rho * rho; default value for rho is 100 */
tolerance = 1. / 100;
/* Maximum ratio in the mesh */
maxRatio = -1.0;
for (final Map.Entry<String, String> opt: options.entrySet())
{
final String key = opt.getKey();
final String val = opt.getValue();
if (key.equals("rho"))
{
double sizeTarget = Double.valueOf(val).doubleValue();
tolerance = 1. / (sizeTarget * sizeTarget);
}
else
throw new RuntimeException("Unknown option: "+key);
}
/*
* This algorithm is intended to repair ill-shaped triangles without
* modifying the geometry. Since all regular edges are considered,
* only very small edges have to be considered. Hence set a minimal
* valid value for rho. Let this value be 2 for tests but a reasonable
* one is larger than 10.
*/
if (tolerance >= 0.25)
throw new RuntimeException("Edge ratio 'rho' must be strictly greater than 2.0");
}
@Override
public Logger thisLogger()
{
return LOGGER;
}
@Override
public void preProcessAllHalfEdges()
{
LOGGER.info("Using maximum edge ratio: "+1./Math.sqrt(tolerance));
}
/**
* Compute (square of inverse of) edge ratio. Since 'rho' is greater than
* two, 'tolerance' = 1/rho**2 is smaller than 1/4 (and positive). Hence
* the edge 'e' can be processed only if the ratio between its length and
* one of its neighbor is the inverse of the edge ratio of the triangle.
*/
@Override
protected final double cost(final HalfEdge e)
{
/* square length of 'e' */
double ae = e.origin().sqrDistance3D(e.destination());
/* max square length of the neighbors of 'e' */
double maxNeighSqrLength = -ae;
/* Neighbor of 'e' whose length is max */
HalfEdge maxNeighHE = null;
/* fan number of e (1 if manifold) */
int nfan = 0;
/* neighbors of 'e' (including non-manifold) */
for (Iterator<AbstractHalfEdge> it = e.fanIterator(); it.hasNext(); )
{
HalfEdge fan = (HalfEdge) it.next();
assert !fan.hasAttributes(AbstractHalfEdge.OUTER) : "fan edge is OUTER: "+fan;
/* Consider only non-outer edges */
if (!fan.getTri().hasAttributes(AbstractHalfEdge.OUTER)) {
HalfEdge fn = fan.next();
HalfEdge fp = fan.prev();
double an = fn.origin().sqrDistance3D(fn.destination());
double ap = fp.origin().sqrDistance3D(fp.destination());
if (an > maxNeighSqrLength) {
maxNeighSqrLength = an;
maxNeighHE = fn;
}
if (ap > maxNeighSqrLength) {
maxNeighSqrLength = ap;
maxNeighHE = fp;
}
}
else if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Triangle is OUTER: "+fan.getTri());
}
nfan++;
}
/* free edges are considered but not their symmetric (OUTER) */
if (e.hasSymmetricEdge() && !e.hasAttributes(AbstractHalfEdge.BOUNDARY))
{
HalfEdge sym = e.sym();
assert !sym.hasAttributes(AbstractHalfEdge.OUTER) : "sym edge is OUTER: "+sym;
/* Consider only non-outer edges */
if (!sym.getTri().hasAttributes(AbstractHalfEdge.OUTER)) {
HalfEdge sn = sym.next();
HalfEdge sp = sym.prev();
double an = sn.origin().sqrDistance3D(sn.destination());
double ap = sp.origin().sqrDistance3D(sp.destination());
if (an > maxNeighSqrLength) {
maxNeighSqrLength = an;
maxNeighHE = sn;
}
if (ap > maxNeighSqrLength) {
maxNeighSqrLength = ap;
maxNeighHE = sp;
}
}
else if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Triangle is OUTER: "+sym.getTri());
}
}
/* inverse of square ratio */
double inverseSqrEdgeRatio = ae/Math.abs(maxNeighSqrLength);
double ratio = 1./Math.sqrt(inverseSqrEdgeRatio);
/* debug info if candidate */
if (LOGGER.isLoggable(Level.FINE)) {
if (inverseSqrEdgeRatio <= tolerance) {
LOGGER.fine("Candidate edge: "+e);
LOGGER.fine("Candidate edge length: "+Math.sqrt(ae));
LOGGER.fine("Max neighbor length: "+Math.sqrt(maxNeighSqrLength));
LOGGER.fine("Ratio: "+ratio);
LOGGER.fine("Max neighbor edge: "+maxNeighHE);
LOGGER.fine("Nr fan: "+nfan);
}
}
if (ratio > maxRatio) {
maxRatioHE = e;
maxRatio = ratio;
}
return inverseSqrEdgeRatio;
}
@Override
void postComputeTree()
{
LOGGER.info("Maximum Ratio: "+maxRatio);
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Maximum Ratio found at edge: "+maxRatioHE);
}
}
/**
* Can process if can collapse.
*/
@Override
public boolean canProcessEdge(HalfEdge current)
{
/* ensure that current is not OUTER */
current = uniqueOrientation(current);
if (current.hasAttributes(AbstractHalfEdge.IMMUTABLE))
return false;
/* check that endpoints are writable */
Vertex v1 = current.origin();
Vertex v2 = current.destination();
if (!v1.isWritable() || !v2.isWritable())
return false;
/* create middle vertex of current edge */
collapseVertex = mesh.createVertex(0., 0., 0.);
collapseVertex.middle(v1, v2);
/* check if can collapse edge with middle vertex */
return mesh.canCollapseEdge(current, collapseVertex);
}
/**
* Update cost of edges that are still existing. The one that are removed
* during collapse are removed from tree in processEdge.
*/
private void updateTree(HalfEdge current)
{
if (current.origin().isReadable() && current.destination().isReadable())
updateCost(current, cost(current));
}
/**
* Collapse edge with its middle vertex (created in canProcessEdge).
* Remove from tree all edges belonging to the triangles about to be
* removed by the collapse.
*/
@Override
public HalfEdge processEdge(HalfEdge current, double costCurrent)
{
current = uniqueOrientation(current);
if (LOGGER.isLoggable(Level.FINE))
{
LOGGER.fine("Collapse edge: "+current+" into "+collapseVertex+" cost="+costCurrent);
if (current.hasAttributes(AbstractHalfEdge.NONMANIFOLD))
{
LOGGER.fine("Non-manifold edge:");
for (Iterator<AbstractHalfEdge> it = current.fanIterator(); it.hasNext(); )
LOGGER.fine(" --> "+it.next());
}
}
/*
* HalfEdge instances on t1 and t2 will be deleted when edge is
* contracted, and we do not know whether they appear within tree or
* their symmetric ones, so remove them now.
* (see LengthDecimateHalfEdge)
*/
for (Iterator<AbstractHalfEdge> it = current.fanIterator(); it.hasNext(); )
{
HalfEdge f = (HalfEdge) it.next();
HalfEdge h = removeOneFromTree(f);
h.clearAttributes(AbstractHalfEdge.MARKED);
if (f.getTri().isWritable())
{
nrTriangles--;
for (int i = 0; i < 2; i++)
{
f = f.next();
removeFromTree(f);
}
f = f.next();
}
}
HalfEdge sym = current.sym();
if (sym.getTri().isWritable())
{
nrTriangles--;
for (int i = 0; i < 2; i++)
{
sym = sym.next();
removeFromTree(sym);
}
sym = sym.next();
}
/* Contract (v1,v2) into collapseVertex. By convention, collapse()
* returns edge (collapseVertex, apex)
*/
assert (!current.hasAttributes(AbstractHalfEdge.OUTER));
Vertex apex = current.apex();
Vertex v1 = current.origin();
Vertex v2 = current.destination();
current = (HalfEdge) mesh.edgeCollapse(current, collapseVertex);
/* Now current == (collapseVertex*a) */
if (liaison != null)
{
liaison.removeVertex(v2);
liaison.replaceVertex(v1, collapseVertex);
}
/* Update cost of modified edges */
assert current != null : collapseVertex+" not connected to "+apex;
assert current.origin() == collapseVertex : ""+current+"\n"+collapseVertex+"\n"+apex;
assert current.apex() == apex : ""+current+"\n"+collapseVertex+"\n"+apex;
assert mesh.isValid();
if (current.origin().isManifold())
{
do
{
current = current.nextOriginLoop();
assert !current.hasAttributes(AbstractHalfEdge.NONMANIFOLD);
updateTree(current);
}
while (current.apex() != apex);
return current.next();
}
Vertex o = current.origin();
Triangle [] list = (Triangle []) o.getLink();
for (Triangle t: list)
{
HalfEdge f = (HalfEdge) t.getAbstractHalfEdge();
if (f.destination() == o)
f = f.next();
else if (f.apex() == o)
f = f.prev();
assert f.origin() == o;
Vertex d = f.destination();
do
{
f = f.nextOriginLoop();
updateTree(f);
}
while (f.destination() != d);
current = f;
}
/* Since we do not swap after collapsing this valued is ignored */
return current.next();
}
@Override
public void postProcessAllHalfEdges()
{
LOGGER.info("Number of collapsed edges: "+processed);
LOGGER.info("Total number of edges not collapsed during processing: "+notProcessed);
LOGGER.info("Total number of edges swapped to increase quality: "+swapped);
super.postProcessAllHalfEdges();
}
private final static String usageString = "<xmlDir> <rho> <brepFile> <outputDir>";
/**
*
* @param args xmlDir, rho, brepFile, output
*/
public static void main(String[] args)
{
HashMap<String, String> options = new HashMap<String, String>();
if(args.length != 4)
{
System.out.println(usageString);
return;
}
options.put("rho", args[1]);
LOGGER.info("Load geometry file");
MeshTraitsBuilder mtb = MeshTraitsBuilder.getDefault3D();
mtb.addTriangleSet();
Mesh mesh = new Mesh(mtb);
try
{
MeshReader.readObject3D(mesh, args[0]);
}
catch (IOException ex)
{
ex.printStackTrace();
throw new RuntimeException(ex);
}
new RemoveDegeneratedTriangles(mesh, options).compute();
File brepFile = new File(args[2]);
try
{
MeshWriter.writeObject3D(mesh, args[3], brepFile.getName());
}
catch (IOException ex)
{
ex.printStackTrace();
throw new RuntimeException(ex);
}
}
}