/* 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,2008,2009,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.Mesh; import org.jcae.mesh.amibe.ds.HalfEdge; import org.jcae.mesh.amibe.ds.Triangle; import org.jcae.mesh.amibe.ds.Vertex; import org.jcae.mesh.amibe.ds.AbstractHalfEdge; import org.jcae.mesh.amibe.projection.MeshLiaison; import org.jcae.mesh.xmldata.MeshReader; import org.jcae.mesh.xmldata.MeshWriter; import java.io.IOException; import java.io.File; import java.util.Collection; import java.util.Map; import java.util.HashMap; import java.util.Iterator; import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; import org.jcae.mesh.amibe.metrics.MetricSupport; public class LengthDecimateHalfEdge extends AbstractAlgoHalfEdge { private static final Logger LOGGER=Logger.getLogger(LengthDecimateHalfEdge.class.getName()); private Vertex v3; private boolean freeEdgesOnly = false; private final double freeEdgeFactor; private double maxEdgeLength = -1; private final MetricSupport metrics; /** * Creates a <code>LengthDecimateHalfEdge</code> instance. * * @param m the <code>Mesh</code> instance to refine. * @param options map containing key-value pairs to modify algorithm * behaviour. Valid keys are <code>size</code>, * <code>placement</code> and <code>maxtriangles</code>. */ public LengthDecimateHalfEdge(final Mesh m, final Map<String, String> options) { this(m, null, options); } public LengthDecimateHalfEdge(final MeshLiaison meshLiaison, final Map<String, String> options) { this(meshLiaison.getMesh(), meshLiaison, options); } private LengthDecimateHalfEdge(final Mesh m, final MeshLiaison meshLiaison, final Map<String, String> options) { super(m, meshLiaison); v3 = null; m.createVertex(0.0, 0.0, 0.0); double freeEdgeTol = Double.NaN; tolerance = 1; metrics = new MetricSupport(mesh, options, "size"); for (final Map.Entry<String, String> opt: options.entrySet()) { String key = opt.getKey(); String val = opt.getValue(); if ("maxtriangles".equals(key)) { nrFinal = Integer.parseInt(val); LOGGER.fine("Nr max triangles: "+nrFinal); } else if ("maxlength".equals(key)) { maxEdgeLength = Double.parseDouble(val); LOGGER.fine("Max edge length: "+maxEdgeLength); } else if ("freeEdgesOnly".equals(key)) { freeEdgesOnly = Boolean.parseBoolean(val); LOGGER.fine("freeEdgesOnly: "+freeEdgesOnly); } else if ("freeEdgeTol".equals(key)) { freeEdgeTol = Double.parseDouble(val); freeEdgeTol = freeEdgeTol*freeEdgeTol; } else if (key.equals("coplanarity")) { minCos = Double.parseDouble(val); LOGGER.fine("Minimum dot product of face normals allowed for swapping an edge: "+minCos); } else if(!metrics.isKnownOption(key)) throw new RuntimeException("Unknown option: "+key); } if(Double.isNaN(freeEdgeTol)) freeEdgeFactor = 1.0; else freeEdgeFactor = tolerance / freeEdgeTol; if (meshLiaison == null) mesh.buildRidges(minCos); if (freeEdgesOnly) setNoSwapAfterProcessing(true); } public void setAnalyticMetric(MetricSupport.AnalyticMetricInterface m) { metrics.setAnalyticMetric(m); } public void setAnalyticMetric(int groupId, MetricSupport.AnalyticMetricInterface m) { metrics.setAnalyticMetric(groupId, m); } @Override public Logger thisLogger() { return LOGGER; } @Override protected void preProcessAllHalfEdges() { metrics.compute(); } @Override protected final double cost(final HalfEdge e) { //Ensure that boundary and non manifold edges are never processed if (freeEdgesOnly && !e.hasAttributes(AbstractHalfEdge.IMMUTABLE | AbstractHalfEdge.BOUNDARY | AbstractHalfEdge.NONMANIFOLD)) return 4.0 * tolerance; double toReturn = metrics.interpolatedDistance(e.origin(), e.destination()); //Handle the case of specific tolerance for free edges if(freeEdgeFactor != 1.0 && e.origin().getRef()>0 && e.destination().getRef()>0) toReturn = toReturn * freeEdgeFactor; return toReturn; } @Override public boolean canProcessEdge(HalfEdge current) { if (freeEdgesOnly && !current.hasAttributes(AbstractHalfEdge.IMMUTABLE | AbstractHalfEdge.SHARP | AbstractHalfEdge.BOUNDARY | AbstractHalfEdge.NONMANIFOLD)) return false; current = uniqueOrientation(current); Vertex v1 = current.origin(); Vertex v2 = current.destination(); assert v1 != v2 : current; // If an endpoint is not writable, its neighborhood is // not fully determined and contraction must not be // performed. if (!v1.isWritable() || !v2.isWritable()) return false; v3 = optimalPlacementGroups(v1, v2); if(v3 == null) return false; if (!mesh.canCollapseEdge(current, v3)) return false; if (maxEdgeLength > 0.0) { metrics.put(v3); for (Iterator<Vertex> itnv = v1.getNeighbourIteratorVertex(); itnv.hasNext(); ) { Vertex n = itnv.next(); if (n != mesh.outerVertex && (!freeEdgesOnly || n.isManifold())) if(metrics.interpolatedDistance(n, v3) > maxEdgeLength) return false; } for (Iterator<Vertex> itnv = v2.getNeighbourIteratorVertex(); itnv.hasNext(); ) { Vertex n = itnv.next(); if (n != mesh.outerVertex && (!freeEdgesOnly || n.isManifold())) if(metrics.interpolatedDistance(n, v3) > maxEdgeLength) return false; } } return true; } /** * Return true if the vertex is on a boundary edge */ private boolean isBoundary(Vertex v) { Iterator<AbstractHalfEdge> it = v.getNeighbourIteratorAbstractHalfEdge(); while(it.hasNext()) { AbstractHalfEdge e = it.next(); if(e.hasAttributes(AbstractHalfEdge.BOUNDARY)) return true; } return false; } /** * Compute the optimal point if we are on differents borders of groups. * Delegate to optimalPlacement in other cases. */ private Vertex optimalPlacementGroups(Vertex v1, Vertex v2) { Vertex toReturn; if(!v1.isMutable() && !v2.isMutable()) toReturn = null; else if(v1.isManifold() && v2.isManifold()) toReturn = optimalPlacement(v1, v2); else { TreeSet<Integer> grps1 = new TreeSet<Integer>(); TreeSet<Integer> grps2 = new TreeSet<Integer>(); v1.getGroups(grps1); v2.getGroups(grps2); if(grps1.containsAll(grps2)) { if(grps1.size() == grps2.size()) { //both points are on the same group border so we delegate to //optimalPlacement as if it was manifold toReturn = optimalPlacement(v1, v2); } else toReturn = v1; } else if(grps2.containsAll(grps1)) toReturn = v2; else //group set are disjoin so collapse is forbidden toReturn = null; } // Do not break polyline ends if(toReturn == v1 && (!v2.isMutable() || !v2.isManifold() && isBoundary(v2))) toReturn = null; else if(toReturn == v2 && (!v1.isMutable() || !v1.isManifold() && isBoundary(v1))) toReturn = null; return toReturn; } private Vertex optimalPlacement(Vertex v1, Vertex v2) { if (v1.getRef() > 0 || !v1.isMutable()) return v1; else if (v2.getRef() != 0 || !v2.isMutable()) return v2; Vertex toReturn = mesh.createVertex(0, 0, 0); toReturn.middle(v1, v2); return toReturn; } @Override public HalfEdge processEdge(HalfEdge current, double costCurrent) { current = uniqueOrientation(current); if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Contract edge: "+current+" into "+v3+" 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. 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 v3 // By convention, collapse() returns edge (v3, apex) assert (!current.hasAttributes(AbstractHalfEdge.OUTER)); Vertex apex = current.apex(); Vertex v1 = current.origin(); Vertex v2 = current.destination(); current = (HalfEdge) mesh.edgeCollapse(current, v3); // Now current == (v3*a) if (liaison != null) { liaison.removeVertex(v2); liaison.replaceVertex(v1, v3); } metrics.put(v3); // Update edge costs assert current != null : v3+" not connected to "+apex; assert current.origin() == v3 : ""+current+"\n"+v3+"\n"+apex; assert current.apex() == apex : ""+current+"\n"+v3+"\n"+apex; if (current.origin().isManifold()) { do { current = current.nextOriginLoop(); assert !current.hasAttributes(AbstractHalfEdge.NONMANIFOLD); if (current.destination().isReadable() && current.origin().isReadable()) updateCost(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(); if (f.destination().isReadable() && f.origin().isReadable()) updateCost(f); } while (f.destination() != d); current = f; } return current.next(); } @Override public void postProcessAllHalfEdges() { LOGGER.info("Number of contracted edges: "+processed); LOGGER.info("Total number of edges not contracted during processing: "+notProcessed); LOGGER.info("Total number of edges swapped to increase quality: "+swapped); super.postProcessAllHalfEdges(); } private final static String usageString = "<xmlDir> <-t tolerance | -n nrTriangles> <brepFile> <outputDir>"; /** * * @param args xmlDir, -t tolerance | -n triangle, brepFile, output */ public static void main(final String[] args) { HashMap<String, String> options = new HashMap<String, String>(); if(args.length != 5) { System.out.println(usageString); return; } if(args[1].equals("-n")) options.put("maxtriangles", args[2]); else if(args[1].equals("-t")) options.put("size", args[2]); else { System.out.println(usageString); return; } LOGGER.info("Load geometry file"); Mesh mesh = new Mesh(); try { MeshReader.readObject3D(mesh, args[0]); } catch (IOException ex) { ex.printStackTrace(); throw new RuntimeException(ex); } new LengthDecimateHalfEdge(mesh, options).compute(); File brepFile=new File(args[3]); try { MeshWriter.writeObject3D(mesh, args[4], brepFile.getName()); } catch (IOException ex) { ex.printStackTrace(); throw new RuntimeException(ex); } } }