From 05bf4a1171d27ebb86211fde9e7f24891014c41c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radek=20O=C5=A1lej=C5=A1ek?= <oslejsek@fi.muni.cz> Date: Sun, 6 Feb 2022 09:37:37 +0100 Subject: [PATCH] Resolve "Introduce HumanFaceUtils" --- .../cz/fidentis/analyst/face/HumanFace.java | 63 +- .../fidentis/analyst/face/HumanFaceUtils.java | 355 +++++++++ .../events/HumanFaceTransformedEvent.java | 46 ++ .../analyst/face/events/KdTreeCreated.java | 29 - .../analyst/face/events/KdTreeDestroyed.java | 24 - .../analyst/face/events/MeshChangedEvent.java | 22 - .../analyst/face/events/MeshEvent.java | 22 - .../events/SymmetryPlaneChangedEvent.java | 7 +- .../analyst/icp/IcpTransformation.java | 8 +- .../fidentis/analyst/icp/IcpTransformer.java | 42 +- .../cz/fidentis/analyst/symmetry/Plane.java | 114 ++- .../visitors/mesh/HausdorffDistance.java | 2 +- .../fidentis/analyst/symmetry/PlaneTest.java | 129 ++++ .../visitors/mesh/CrossSectionTest.java | 8 +- .../batch/ApproxHausdorffDistTask.java | 17 +- .../batch/ApproxHausdorffDistTaskGPU.java | 8 +- .../batch/CompleteHausdorffDistTask.java | 26 +- .../cz/fidentis/analyst/batch/IcpTask.java | 20 +- .../cz/fidentis/analyst/canvas/Canvas.java | 27 +- .../toolbar/SceneToolboxFaceToFace.java | 206 ++--- .../toolbar/SceneToolboxSingleFace.java | 105 ++- .../analyst/distance/DistanceAction.java | 29 +- .../registration/RegistrationAction.java | 297 ++------ .../registration/RegistrationPanel.form | 580 +++------------ .../registration/RegistrationPanel.java | 703 +++++++----------- .../cz/fidentis/analyst/scene/Drawable.java | 70 -- .../analyst/scene/DrawableCuttingPlane.java | 99 +-- .../analyst/scene/DrawableFeaturePoints.java | 4 +- .../fidentis/analyst/scene/DrawablePlane.java | 83 +-- .../java/cz/fidentis/analyst/scene/Scene.java | 33 +- .../fidentis/analyst/scene/SceneRenderer.java | 13 + .../analyst/symmetry/ProfilesAction.java | 202 ++--- .../analyst/symmetry/ProfilesPanel.form | 4 +- .../analyst/symmetry/ProfilesPanel.java | 4 +- .../analyst/symmetry/SymmetryAction.java | 15 +- .../tests/BatchSimilarityApproxHausdorff.java | 145 ---- .../tests/BatchSimilarityGroundTruth.java | 39 +- .../tests/BatchSimilarityGroundTruthOpt.java | 169 ----- .../analyst/tests/EfficiencyTests.java | 141 ---- .../cz/fidentis/analyst/tests/ICPTest.java | 82 -- .../analyst/canvas/toolbar/Bundle.properties | 1 + .../analyst/registration/Bundle.properties | 24 +- GUI/src/main/resources/na.png | Bin 0 -> 35276 bytes GUI/src/main/resources/na28x28.png | Bin 0 -> 2250 bytes .../fidentis/analyst/mesh/core/MeshFacet.java | 2 +- .../analyst/mesh/core/MeshTriangle.java | 10 +- .../analyst/mesh/core/MeshTriangleTest.java | 38 +- 47 files changed, 1660 insertions(+), 2407 deletions(-) create mode 100644 Comparison/src/main/java/cz/fidentis/analyst/face/HumanFaceUtils.java create mode 100644 Comparison/src/main/java/cz/fidentis/analyst/face/events/HumanFaceTransformedEvent.java delete mode 100644 Comparison/src/main/java/cz/fidentis/analyst/face/events/KdTreeCreated.java delete mode 100644 Comparison/src/main/java/cz/fidentis/analyst/face/events/KdTreeDestroyed.java delete mode 100644 Comparison/src/main/java/cz/fidentis/analyst/face/events/MeshChangedEvent.java delete mode 100644 Comparison/src/main/java/cz/fidentis/analyst/face/events/MeshEvent.java create mode 100644 Comparison/src/test/java/cz/fidentis/analyst/symmetry/PlaneTest.java delete mode 100644 GUI/src/main/java/cz/fidentis/analyst/tests/BatchSimilarityApproxHausdorff.java delete mode 100644 GUI/src/main/java/cz/fidentis/analyst/tests/BatchSimilarityGroundTruthOpt.java delete mode 100644 GUI/src/main/java/cz/fidentis/analyst/tests/EfficiencyTests.java delete mode 100644 GUI/src/main/java/cz/fidentis/analyst/tests/ICPTest.java create mode 100644 GUI/src/main/resources/na.png create mode 100644 GUI/src/main/resources/na28x28.png diff --git a/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFace.java b/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFace.java index 15cafbc9..8e089d9c 100644 --- a/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFace.java +++ b/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFace.java @@ -7,12 +7,7 @@ import cz.fidentis.analyst.face.events.HumanFaceEvent; import cz.fidentis.analyst.feature.FeaturePoint; import cz.fidentis.analyst.feature.services.FeaturePointImportService; import cz.fidentis.analyst.kdtree.KdTree; -import cz.fidentis.analyst.face.events.KdTreeCreated; -import cz.fidentis.analyst.face.events.KdTreeDestroyed; -import cz.fidentis.analyst.face.events.MeshChangedEvent; -import cz.fidentis.analyst.face.events.SymmetryPlaneChangedEvent; import cz.fidentis.analyst.mesh.core.MeshModel; -import cz.fidentis.analyst.mesh.core.MeshRectangleFacet; import cz.fidentis.analyst.mesh.io.MeshObjLoader; import cz.fidentis.analyst.symmetry.Plane; import cz.fidentis.analyst.visitors.face.HumanFaceVisitor; @@ -92,10 +87,7 @@ public class HumanFace implements Serializable { meshModel.simplifyModel(); this.id = file.getCanonicalPath(); - BoundingBox visitor = new BoundingBox(); - meshModel.compute(visitor); - bbox = visitor.getBoundingBox(); - + updateBoundingBox(); eventBus = new EventBus(); if (loadLandmarks) { @@ -138,13 +130,8 @@ public class HumanFace implements Serializable { throw new IllegalArgumentException("id"); } this.meshModel = model; - - BoundingBox visitor = new BoundingBox(); - meshModel.compute(visitor); - bbox = visitor.getBoundingBox(); - + updateBoundingBox(); this.id = id; - eventBus = new EventBus(); } @@ -159,7 +146,6 @@ public class HumanFace implements Serializable { /** * Sets the mesh model. - * Triggers {@link cz.fidentis.analyst.face.events.MeshChangedEvent}. * * @param meshModel new mesh model, must not be {@code null} * @throws IllegalArgumentException if new model is missing @@ -169,12 +155,8 @@ public class HumanFace implements Serializable { throw new IllegalArgumentException("meshModel"); } this.meshModel = meshModel; - - BoundingBox visitor = new BoundingBox(); - meshModel.compute(visitor); - bbox = visitor.getBoundingBox(); - - announceEvent(new MeshChangedEvent(this, getShortName(), this)); + updateBoundingBox(); + //announceEvent(new MeshChangedEvent(this, getShortName(), this)); } /** @@ -210,16 +192,26 @@ public class HumanFace implements Serializable { eventBus.post(evt); } } + + /** + * Computes and return bounding box. + * + * @return bounding box + */ + public final BBox updateBoundingBox() { + BoundingBox visitor = new BoundingBox(); + this.meshModel.compute(visitor); + this.bbox = visitor.getBoundingBox(); + return bbox; + } /** * Sets the symmetry plane. If the input argument is {@code null}, then removes the plane. - * Triggers {@link cz.fidentis.analyst.face.events.SymmetryPlaneChangedEvent}. * * @param plane The new symmetry plane; Must not be {@code null} */ public void setSymmetryPlane(Plane plane) { this.symmetryPlane = plane; - this.announceEvent(new SymmetryPlaneChangedEvent(this, getShortName(), this)); } /** @@ -234,8 +226,17 @@ public class HumanFace implements Serializable { * Returns rectangular mesh facet of the symmetry plane, if exists. * @return a rectangular mesh facet of the symmetry plane or {@code null} */ - public MeshRectangleFacet getSymmetryPlaneFacet() { - return (symmetryPlane == null) ? null : symmetryPlane.getMesh(bbox); + //@Deprecated + //public MeshRectangleFacet getSymmetryPlaneFacet() { + // return (symmetryPlane == null) ? null : symmetryPlane.getMesh(bbox); + //} + + /** + * Returns {@code true} if the face has the symmetry plane computed. + * @return {@code true} if the face has the symmetry plane computed. + */ + public boolean hasSymmetryPlane() { + return (symmetryPlane != null); } /** @@ -250,6 +251,7 @@ public class HumanFace implements Serializable { * * @param points List of feature points */ + @Deprecated public void setFeaturePoints(List<FeaturePoint> points) { featurePoints = points; } @@ -273,7 +275,8 @@ public class HumanFace implements Serializable { if (featurePoints == null) { return Collections.emptyList(); } - return Collections.unmodifiableList(featurePoints); + //return Collections.unmodifiableList(featurePoints); + return featurePoints; } /** @@ -337,9 +340,6 @@ public class HumanFace implements Serializable { public KdTree computeKdTree(boolean recompute) { if (kdTree == null || recompute) { kdTree = new KdTree(new ArrayList<>(meshModel.getFacets())); - if (eventBus != null) { // eventBus is null when the class is deserialized! - eventBus.post(new KdTreeCreated(this, this.getShortName(), this)); - } } return kdTree; } @@ -351,9 +351,6 @@ public class HumanFace implements Serializable { public KdTree removeKdTree() { KdTree ret = this.kdTree; this.kdTree = null; - if (eventBus != null) { // eventBus is null when the class is deserialized! - eventBus.post(new KdTreeDestroyed(this, this.getShortName(), this)); - } return ret; } diff --git a/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFaceUtils.java b/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFaceUtils.java new file mode 100644 index 00000000..27d43967 --- /dev/null +++ b/Comparison/src/main/java/cz/fidentis/analyst/face/HumanFaceUtils.java @@ -0,0 +1,355 @@ +package cz.fidentis.analyst.face; + +import cz.fidentis.analyst.feature.FeaturePoint; +import cz.fidentis.analyst.icp.EigenvalueDecomposition; +import cz.fidentis.analyst.icp.IcpTransformer; +import cz.fidentis.analyst.icp.NoUndersampling; +import cz.fidentis.analyst.icp.Quaternion; +import cz.fidentis.analyst.icp.RandomStrategy; +import cz.fidentis.analyst.symmetry.Plane; +import javax.vecmath.Matrix3d; +import javax.vecmath.Matrix4d; +import javax.vecmath.Point3d; +import javax.vecmath.Vector3d; + +/** + * A utility class for operations (visitors) applied onto the whole human faces. + * Currently, transformation operations that affect multiple parts of the human + * face consistently (mesh, symmetry plane, feature points, etc.) are provided. + * + * @author Radek Oslejsek + */ +public class HumanFaceUtils { + + /** + * Superimpose two faces using ICP applied on their triangular meshes. + * + * @param staticFace A face that remains unchanged. + * @param transformedFace A face to be transformed. + * @param maxIterations Maximal number of ICP iterations, bigger than zero. + * Reasonable number seems to be 10. + * @param scale Whether to scale face as well + * @param error Acceptable error (a number bigger than or equal to zero). + * @param undersampling 100 = no undersampling, 10 = undersampling into 10% of vertices. + * @param recomputeKdTree If {@code true} and the k-d tree of the {@code transformedFace} exists, + * the it automatically re-computed. Otherwise, it is simply removed. + * @return ICP visitor that holds the transformations performed on the {@code transformedFace}. + */ + public static IcpTransformer alignMeshes( + HumanFace staticFace, HumanFace transformedFace, + int maxIterations, boolean scale, double error, int undersampling, + boolean recomputeKdTree) { + + // transform mesh: + IcpTransformer icp = new IcpTransformer( + staticFace.getMeshModel(), + maxIterations, + scale, + error, + (undersampling == 100) ? new NoUndersampling() : new RandomStrategy(undersampling) + ); + transformedFace.getMeshModel().compute(icp, true); // superimpose face towards the static face + + // update k-d of transformed face: + if (transformedFace.hasKdTree()) { + if (recomputeKdTree) { + transformedFace.computeKdTree(true); + } else { + transformedFace.removeKdTree(); + } + } + + // update bounding box, which is always present: + transformedFace.updateBoundingBox(); + + // transform feature points: + if (transformedFace.hasFeaturePoints()) { + icp.getTransformations().values().forEach(trList -> { // List<IcpTransformation> + trList.forEach(tr -> { // IcpTransformation + for (int i = 0; i < transformedFace.getFeaturePoints().size(); i++) { + FeaturePoint fp = transformedFace.getFeaturePoints().get(i); + Point3d trPoint = tr.transformPoint(fp.getPosition(), scale); + transformedFace.getFeaturePoints().set(i, + new FeaturePoint(trPoint.x, trPoint.y, trPoint.z, fp.getFeaturePointType()) + ); + } + }); + }); + } + + // transform symmetry plane: + if (transformedFace.hasSymmetryPlane()) { + icp.getTransformations().values().forEach(trList -> { // List<IcpTransformation> + trList.forEach(tr -> { // IcpTransformation + transformedFace.setSymmetryPlane(transformPlane( + transformedFace.getSymmetryPlane(), + tr.getRotation(), + tr.getTranslation(), + tr.getScaleFactor())); + }); + }); + } + + return icp; + } + + /** + * Transform the face. + * + * @param face Face to be transformed. + * @param rotation Rotation vector denoting the rotation angle around axes X, Y, and Z. + * @param translation Translation vector denoting the translation in the X, Y, and Z direction. + * @param scale Scale factor (1 = no scale). + * @param recomputeKdTree If {@code true} and the k-d tree of the {@code transformedFace} exists, + * the it automatically re-computed. Otherwise, it is simply removed. + */ + public static void transformFace(HumanFace face, Vector3d rotation, Vector3d translation, double scale, boolean recomputeKdTree) { + Quaternion rot = new Quaternion(rotation.x, rotation.y, rotation.z, 1.0); + + face.getMeshModel().getFacets().stream() + .map(facet -> facet.getVertices()) + .flatMap(meshPoint -> meshPoint.parallelStream()) + .forEach(meshPoint -> { + transformPoint(meshPoint.getPosition(), rot, translation, scale); + }); + + + // update k-d of transformed face: + if (face.hasKdTree()) { + if (recomputeKdTree) { + face.computeKdTree(true); + } else { + face.removeKdTree(); + } + } + + // update bounding box, which is always present: + face.updateBoundingBox(); + + // transform feature points: + if (face.hasFeaturePoints()) { + face.getFeaturePoints().parallelStream().forEach(fp -> { + transformPoint(fp.getPosition(), rot, translation, scale); + }); + } + + // transform symmetry plane: + if (face.hasSymmetryPlane()) { + face.setSymmetryPlane(transformPlane(face.getSymmetryPlane(), rot, translation, scale)); + } + } + + /** + * Transforms one face so that its symmetry plane fits the symmetry plane of the other face. + * + * @param staticFace a human face that remains unchanged + * @param transformedFace a human face that will be transformed + * @param recomputeKdTree If {@code true} and the k-d tree of the {@code transformedFace} exists, + * the it automatically re-computed. Otherwise, it is simply removed. + */ + public static void alignSymmetryPlanes(HumanFace staticFace, HumanFace transformedFace, boolean recomputeKdTree) { + Plane statPlane = staticFace.getSymmetryPlane(); + Plane tranPlane = transformedFace.getSymmetryPlane(); + + // Get "centroids" of the planes + Point3d statCentroid = statPlane.projectToPlane(staticFace.getBoundingBox().getMidPoint()); + Point3d tranCentroid = tranPlane.projectToPlane(transformedFace.getBoundingBox().getMidPoint()); + + // Compute a 3D transformation of planes. + // However, this transforation rotates the face around the plane axis arbitrarily + Vector3d translation = new Vector3d( + statCentroid.x - tranCentroid.x, + statCentroid.y - tranCentroid.y, + statCentroid.z - tranCentroid.z + ); + Quaternion rotation = computeRotation(tranPlane.getNormal(), statPlane.getNormal()); + + // Compute rotation around plane's normal so that the face is oriented as before + + // Get a vector perpendicular to the transformed plane nornal (and then laying at the plane) + Point3d pp = getPerpendicularPoint(tranPlane); // point at the original plane + Vector3d pv = new Vector3d(pp); + pv.sub(tranCentroid); + pv.normalize(); + + // transform the point so that it lays at the target plane + transformPoint(pp, rotation, translation, 1.0); // point at the transformed plane + transformPoint(tranCentroid, rotation, translation, 1.0); // point at the transformed plane + Vector3d trPV = new Vector3d(pp); + trPV.sub(tranCentroid); + trPV.normalize(); + + // compute angle of rotation around the plane normal so that the face is oriented as before + double cosRot = pv.dot(trPV); + pv.cross(pv, trPV); + double sinRot = pv.length(); + Vector3d axis = (statPlane.getNormal().dot(tranPlane.getNormal()) < 0) + ? statPlane.getNormal() + : new Vector3d(-statPlane.getNormal().x, -statPlane.getNormal().y, -statPlane.getNormal().z); + + // get rotation matrix around the plane's normal + Matrix3d rotMat = rotMatAroundAxis(axis, sinRot, cosRot); + + // Transform mesh vertices: + transformedFace.getMeshModel().getFacets().forEach(f -> { + f.getVertices().stream().forEach(p -> { + transformPoint(p.getPosition(), rotation, translation, 1.0); + rotMat.transform(p.getPosition()); // rotate around the plane's normal + }); + }); + + // update k-d of transformed face: + if (transformedFace.hasKdTree()) { + if (recomputeKdTree) { + transformedFace.computeKdTree(true); + } else { + transformedFace.removeKdTree(); + } + } + + // update bounding box, which is always present: + transformedFace.updateBoundingBox(); + + // Transform feature points: + if (transformedFace.hasFeaturePoints()) { + transformedFace.getFeaturePoints().parallelStream().forEach(fp -> { + transformPoint(fp.getPosition(), rotation, translation, 1.0); + rotMat.transform(fp.getPosition()); // rotate around the plane's normal + }); + } + + // Transform the symmetry plane: + if (transformedFace.hasSymmetryPlane()) { + transformedFace.setSymmetryPlane(transformPlane( + transformedFace.getSymmetryPlane(), rotation, translation, 1.0) + ); + } + } + + /** + * Create rotation matrix from rotation axis and the angle + * + * @param axis rotation axis + * @param sinAngle cosine of the angle + * @param cosAngle sin of the angle + * @return rotation matrix + */ + protected static Matrix3d rotMatAroundAxis(Vector3d axis, double sinAngle, double cosAngle) { + Matrix3d matC = new Matrix3d( + 0, -axis.z, axis.y, + axis.z, 0, -axis.x, + -axis.y, axis.x, 0 + ); + + Matrix3d matCC = new Matrix3d(matC); + matCC.mul(matCC); + matCC.mul(1.0 - cosAngle); + + matC.mul(sinAngle); + + Matrix3d rotMat = new Matrix3d(); + rotMat.setIdentity(); + rotMat.add(matC); + rotMat.add(matCC); + + return rotMat; + } + + /** + * Computes a "random" point at the plan, i.e. a point, which is + * perpendicular to the plane's normal and lays at the plane. + * + * @param plane the plane + * @return point at the plane + */ + protected static Point3d getPerpendicularPoint(Plane plane) { + Point3d p; + if (plane.getNormal().x >= Math.max(plane.getNormal().y, plane.getNormal().z)) { + p = new Point3d(0, 1000, 0); + } else if (plane.getNormal().y >= Math.max(plane.getNormal().x, plane.getNormal().y)) { + p = new Point3d(1000, 0, 0); + } else { + p = new Point3d(0, 1000, 0); + } + return plane.projectToPlane(p); + } + + /** + * Computed 3D rotation matrix that transforms one vector onto another. + * @param trDir vector to be transformed + * @param targetDir target vector + * @return rotation + */ + protected static Quaternion computeRotation(Vector3d trDir, Vector3d targetDir) { + Matrix4d multipleMatrix = new Matrix4d(); + Matrix4d sumMatrixComp = new Matrix4d( + 0, -trDir.x, -trDir.y, -trDir.z, + trDir.x, 0, trDir.z, -trDir.y, + trDir.y, -trDir.z, 0, trDir.x, + trDir.z, trDir.y, -trDir.x, 0 + ); + Matrix4d sumMatrixMain = new Matrix4d( + 0, -targetDir.x, -targetDir.y, -targetDir.z, + targetDir.x, 0, -targetDir.z, targetDir.y, + targetDir.y, targetDir.z, 0, -targetDir.x, + targetDir.z, -targetDir.y, targetDir.x, 0 + ); + + multipleMatrix.mulTransposeLeft(sumMatrixComp, sumMatrixMain); + Quaternion rotation = new Quaternion(new EigenvalueDecomposition(multipleMatrix)); + rotation.normalize(); + return rotation; + } + + + /** + * Transform a single 3d point. + * + * @param point point to be transformed + * @param rotation rotation, can be {@code null} + * @param translation translation + * @param scale scale + */ + protected static void transformPoint(Point3d point, Quaternion rotation, Vector3d translation, double scale) { + Quaternion rotQuat = new Quaternion(point.x, point.y, point.z, 1); + + if (rotation != null) { + Quaternion rotationCopy = Quaternion.multiply(rotQuat, rotation.getConjugate()); + rotQuat = Quaternion.multiply(rotation, rotationCopy); + } + + point.set( + (rotQuat.x + translation.x) * scale, + (rotQuat.y + translation.y) * scale, + (rotQuat.z + translation.z) * scale + ); + } + + /** + * Transforms the whole plane, i.e., its normal and position. + * + * @param plane plane to be transformed + * @param rot rotation + * @param translation translation + * @param scale scale + * @return transformed plane + */ + protected static Plane transformPlane(Plane plane, Quaternion rot, Vector3d translation, double scale) { + Point3d point = new Point3d(plane.getNormal()); + transformPoint(point, rot, new Vector3d(0, 0, 0), 1.0); // rotate only + Plane retPlane = new Plane(point, plane.getDistance()); + + // ... then translate and scale a point projected on the rotate plane: + point.scale(retPlane.getDistance()); // point laying on the rotated plane + transformPoint(point, null, translation, scale); // translate and scale only + Vector3d normal = retPlane.getNormal(); + double dist = ((normal.x * point.x) + (normal.y * point.y) + (normal.z * point.z)) + / Math.sqrt(normal.dot(normal)); // distance of tranformed surface point in the plane's mormal direction + + return new Plane(retPlane.getNormal(), dist); + } + + + + +} diff --git a/Comparison/src/main/java/cz/fidentis/analyst/face/events/HumanFaceTransformedEvent.java b/Comparison/src/main/java/cz/fidentis/analyst/face/events/HumanFaceTransformedEvent.java new file mode 100644 index 00000000..960862ef --- /dev/null +++ b/Comparison/src/main/java/cz/fidentis/analyst/face/events/HumanFaceTransformedEvent.java @@ -0,0 +1,46 @@ +package cz.fidentis.analyst.face.events; + +import cz.fidentis.analyst.face.HumanFace; + +/** + * A human face all its components (mesh, symmetry plane, feature points, etc.) + * have been transformed in scape. + * + * @author Radek Oslejsek + */ +public class HumanFaceTransformedEvent extends HumanFaceEvent { + + private final boolean isFinished; + + /** + * Constructor of finished transformation. + * + * @param face Human face related to the event + * @param name Event name provided by issuer + * @param issuer The issuer + */ + public HumanFaceTransformedEvent(HumanFace face, String name, Object issuer) { + this(face, name, issuer, true); + } + + /** + * Constructor. + * @param face Human face related to the event + * @param name Event name provided by issuer + * @param issuer The issuer + * @param isFinished if {@code true}, then it is supposed that the transformation will continue + * (is in the progress). + */ + public HumanFaceTransformedEvent(HumanFace face, String name, Object issuer, boolean isFinished) { + super(face, name, issuer); + this.isFinished = isFinished; + } + + /** + * Returns {@code true} it the transformation is finished (is not under progress) + * @return {@code true} it the transformation is finished + */ + public boolean isFinished() { + return this.isFinished; + } +} diff --git a/Comparison/src/main/java/cz/fidentis/analyst/face/events/KdTreeCreated.java b/Comparison/src/main/java/cz/fidentis/analyst/face/events/KdTreeCreated.java deleted file mode 100644 index 52d62d03..00000000 --- a/Comparison/src/main/java/cz/fidentis/analyst/face/events/KdTreeCreated.java +++ /dev/null @@ -1,29 +0,0 @@ - -package cz.fidentis.analyst.face.events; - -import cz.fidentis.analyst.face.HumanFace; - -/** - * An event fired when a new KdTree is calculated - - * @author Matej Kovar - */ -public class KdTreeCreated extends KdTreeEvent { - - /** - * Constructor. - * @param face Human face related to the event - * @param name Event name provided by issuer - * @param issuer The issuer - */ - public KdTreeCreated(HumanFace face, String name, Object issuer) { - super(face, name, issuer); - } - - @Override - public boolean isCalculated() { - return true; - } - - -} diff --git a/Comparison/src/main/java/cz/fidentis/analyst/face/events/KdTreeDestroyed.java b/Comparison/src/main/java/cz/fidentis/analyst/face/events/KdTreeDestroyed.java deleted file mode 100644 index 30d3ffd8..00000000 --- a/Comparison/src/main/java/cz/fidentis/analyst/face/events/KdTreeDestroyed.java +++ /dev/null @@ -1,24 +0,0 @@ - -package cz.fidentis.analyst.face.events; - -import cz.fidentis.analyst.face.HumanFace; - -/** - * An event fired when a KD-tree is destroyed - - * @author Matej Kovar - */ - -public class KdTreeDestroyed extends KdTreeEvent { - - /** - * Constructor. - * @param face Human face related to the event - * @param name Event name provided by issuer - * @param issuer The issuer - */ - public KdTreeDestroyed(HumanFace face, String name, Object issuer) { - super(face, name, issuer); - } - -} diff --git a/Comparison/src/main/java/cz/fidentis/analyst/face/events/MeshChangedEvent.java b/Comparison/src/main/java/cz/fidentis/analyst/face/events/MeshChangedEvent.java deleted file mode 100644 index daa6bc3d..00000000 --- a/Comparison/src/main/java/cz/fidentis/analyst/face/events/MeshChangedEvent.java +++ /dev/null @@ -1,22 +0,0 @@ -package cz.fidentis.analyst.face.events; - -import cz.fidentis.analyst.face.HumanFace; - -/** - * An event fired when the topology or position has changed. - - * @author Radek Oslejsek - */ -public class MeshChangedEvent extends MeshEvent { - - /** - * Constructor. - * @param face Human face related to the event - * @param name Event name provided by issuer - * @param issuer The issuer - */ - public MeshChangedEvent(HumanFace face, String name, Object issuer) { - super(face, name, issuer); - } - -} diff --git a/Comparison/src/main/java/cz/fidentis/analyst/face/events/MeshEvent.java b/Comparison/src/main/java/cz/fidentis/analyst/face/events/MeshEvent.java deleted file mode 100644 index b4f0df4f..00000000 --- a/Comparison/src/main/java/cz/fidentis/analyst/face/events/MeshEvent.java +++ /dev/null @@ -1,22 +0,0 @@ -package cz.fidentis.analyst.face.events; - -import cz.fidentis.analyst.face.HumanFace; - -/** - * The root type for events related to changes in the triangular mesh. - * - * @author Radek Oslejsek - */ -public class MeshEvent extends HumanFaceEvent { - - /** - * Constructor. - * @param face Human face related to the event - * @param name Event name provided by issuer - * @param issuer The issuer - */ - public MeshEvent(HumanFace face, String name, Object issuer) { - super(face, name, issuer); - } - -} diff --git a/Comparison/src/main/java/cz/fidentis/analyst/face/events/SymmetryPlaneChangedEvent.java b/Comparison/src/main/java/cz/fidentis/analyst/face/events/SymmetryPlaneChangedEvent.java index f171633a..04f52440 100644 --- a/Comparison/src/main/java/cz/fidentis/analyst/face/events/SymmetryPlaneChangedEvent.java +++ b/Comparison/src/main/java/cz/fidentis/analyst/face/events/SymmetryPlaneChangedEvent.java @@ -3,7 +3,11 @@ package cz.fidentis.analyst.face.events; import cz.fidentis.analyst.face.HumanFace; /** - * New symmetry plane has been added or changed (transformed). + * A new symmetry plane has been added or the previous one has been recomputed. + * If the plane is transformed separately (without the transformation of mesh or + * other parts of the human face), use this event as well. + * On the contrary, if symmetry plane is transformed together with + * the mesh transformation, use the {@link HausdorffDistanceComputed} event instead. * * @author Radek Oslejsek */ @@ -11,6 +15,7 @@ public class SymmetryPlaneChangedEvent extends HumanFaceEvent { /** * Constructor. + * * @param face Human face related to the event * @param name Event name provided by issuer * @param issuer The issuer diff --git a/Comparison/src/main/java/cz/fidentis/analyst/icp/IcpTransformation.java b/Comparison/src/main/java/cz/fidentis/analyst/icp/IcpTransformation.java index fcf0fda9..363ee1e8 100644 --- a/Comparison/src/main/java/cz/fidentis/analyst/icp/IcpTransformation.java +++ b/Comparison/src/main/java/cz/fidentis/analyst/icp/IcpTransformation.java @@ -22,7 +22,7 @@ public class IcpTransformation { * @param rotation Rotation is represented by Quaternion * (x, y, z coordinate and scalar component). * @param scaleFactor ScaleFactor represents scale between two objects. - * In case there is no scale the value is 0. + * In case there is no scale the value is 1. * @param meanD MeanD represents mean distance between objects. */ public IcpTransformation(Vector3d translation, Quaternion rotation, double scaleFactor, double meanD) { @@ -82,9 +82,9 @@ public class IcpTransformation { if(scale && !Double.isNaN(getScaleFactor())) { return new Point3d( - rotQuat.x * getScaleFactor() + getTranslation().x, - rotQuat.y * getScaleFactor() + getTranslation().y, - rotQuat.z * getScaleFactor() + getTranslation().z + (rotQuat.x + getTranslation().x) * getScaleFactor(), + (rotQuat.y + getTranslation().y) * getScaleFactor(), + (rotQuat.z + getTranslation().z) * getScaleFactor() ); } else { return new Point3d( diff --git a/Comparison/src/main/java/cz/fidentis/analyst/icp/IcpTransformer.java b/Comparison/src/main/java/cz/fidentis/analyst/icp/IcpTransformer.java index 5f9d1a51..5c47b7e9 100644 --- a/Comparison/src/main/java/cz/fidentis/analyst/icp/IcpTransformer.java +++ b/Comparison/src/main/java/cz/fidentis/analyst/icp/IcpTransformer.java @@ -76,7 +76,7 @@ public class IcpTransformer extends MeshVisitor { * new transformation and applying it). A number bigger than zero. * Reasonable number seems to be 10. * @param scale If {@code true}, then the scale factor is also computed. - * @param error Acceptable error. A number bugger than or equal to zero. + * @param error Acceptable error. A number bigger than or equal to zero. * When reached, then the ICP stops. Reasonable number seems to be 0.05. * @param strategy One of the reduction strategies. If {@code null}, then {@link NoUndersampling} is used. * @throws IllegalArgumentException if some parameter is wrong @@ -295,7 +295,7 @@ public class IcpTransformer extends MeshVisitor { List<Point3d>centers = computeCenterBothFacets(nearestPoints, comparedPoints); Point3d mainCenter = centers.get(0); Point3d comparedCenter = centers.get(1); - + Matrix4d sumMatrix = new Matrix4d(); Matrix4d multipleMatrix = new Matrix4d(); @@ -330,7 +330,7 @@ public class IcpTransformer extends MeshVisitor { // END computing rotation parameter //computing SCALE parameter - double scaleFactor = 0; + double scaleFactor = 1.0; if (scale) { double sxUp = 0; double sxDown = 0; @@ -341,7 +341,7 @@ public class IcpTransformer extends MeshVisitor { } Matrix4d matrixPoint = pointToMatrix(relativeCoordinate(nearestPoints.get(i), mainCenter)); - Matrix4d matrixPointCompare = pointToMatrix(relativeCoordinate(comparedPoints.get(i).getPosition(),comparedCenter)); + Matrix4d matrixPointCompare = pointToMatrix(relativeCoordinate(comparedPoints.get(i).getPosition(), comparedCenter)); matrixPointCompare.mul(rotationMatrix); @@ -383,38 +383,28 @@ public class IcpTransformer extends MeshVisitor { * and the second one represents center of compared facet. */ private List<Point3d> computeCenterBothFacets(List<Point3d> nearestPoints, List<MeshPoint> comparedPoints) { - double xN = 0; - double yN = 0; - double zN = 0; - - double xC = 0; - double yC = 0; - double zC = 0; - + //assert(nearestPoints.size() == comparedPoints.size()); + Point3d n = new Point3d(0, 0, 0); + Point3d c = new Point3d(0, 0, 0); int countOfNotNullPoints = 0; List<Point3d> result = new ArrayList<>(2); for (int i = 0; i < nearestPoints.size(); i++) { - - if(nearestPoints.get(i) == null){ + if (nearestPoints.get(i) == null) { continue; } - - xN += nearestPoints.get(i).x; - yN += nearestPoints.get(i).y; - zN += nearestPoints.get(i).z; - - xC += comparedPoints.get(i).getPosition().x; - yC += comparedPoints.get(i).getPosition().y; - zC += comparedPoints.get(i).getPosition().z; - + n.add(nearestPoints.get(i)); + c.add(comparedPoints.get(i).getPosition()); countOfNotNullPoints ++; } - result.add(new Point3d(xN/countOfNotNullPoints, yN/countOfNotNullPoints, zN/countOfNotNullPoints)); - result.add(new Point3d(xC/countOfNotNullPoints, yC/countOfNotNullPoints, zC/countOfNotNullPoints)); + + n.scale(1.0/countOfNotNullPoints); + c.scale(1.0/countOfNotNullPoints); + result.add(n); + result.add(c); return result; } - + /** * Compute relative coordinate of given point according to given center. * Relative coordinates represents distance from the center of the mesh to vertex. diff --git a/Comparison/src/main/java/cz/fidentis/analyst/symmetry/Plane.java b/Comparison/src/main/java/cz/fidentis/analyst/symmetry/Plane.java index 80737435..2b9489bf 100644 --- a/Comparison/src/main/java/cz/fidentis/analyst/symmetry/Plane.java +++ b/Comparison/src/main/java/cz/fidentis/analyst/symmetry/Plane.java @@ -5,10 +5,11 @@ import cz.fidentis.analyst.visitors.mesh.BoundingBox.BBox; import java.io.Serializable; import java.util.List; import javax.vecmath.Point3d; +import javax.vecmath.Tuple3d; import javax.vecmath.Vector3d; /** - * Symmetry plane. + * Immutable symmetry plane. * * @author Natalia Bebjakova * @author Dominik Racek @@ -26,8 +27,8 @@ public class Plane implements Serializable { * @param dist distance * @throws IllegalArgumentException if the @code{plane} argument is null */ - public Plane(Vector3d normal, double dist) { - setNormal(new Vector3d(normal)); + public Plane(Tuple3d normal, double dist) { + setNormal(normal); setDistance(dist); } @@ -54,7 +55,7 @@ public class Plane implements Serializable { Vector3d n = new Vector3d(); double d = 0; Vector3d refDir = planes.get(0).getNormal(); - for (int i = 0; i < planes.size(); i++) { + for (int i = 1; i < planes.size(); i++) { Vector3d normDir = planes.get(i).getNormal(); if (normDir.dot(refDir) < 0) { n.sub(normDir); @@ -66,7 +67,7 @@ public class Plane implements Serializable { } setNormal(n); - setDistance(d); + setDistance(-d); normalize(); } @@ -90,11 +91,7 @@ public class Plane implements Serializable { */ @Override public String toString(){ - return "APPROXIMATE PLANE:" + System.lineSeparator() + - normal.x + System.lineSeparator() + - normal.y + System.lineSeparator() + - normal.z + System.lineSeparator() + - distance + System.lineSeparator(); + return normal + " dist " + distance; } public Vector3d getNormal() { @@ -104,14 +101,47 @@ public class Plane implements Serializable { public double getDistance() { return distance; } + + /** + * Returns a point laying at the plane + * @return a point laying at the plane + */ + public Point3d getPlanePoint() { + Point3d ret = new Point3d(normal); + ret.scale(distance); + return ret; + } /** * Translate the plane along its normal * * @param value */ - public void translate(double value) { - this.distance += value; + //public void shift(double value) { + // this.distance += value; + //} + + /** + * Translate the plane along its normal + * + * @param value a value to be added to the current plane's distance value + * @return shifted plane + */ + public Plane shift(double value) { + Plane ret = new Plane(this); + ret.distance += value; + return ret; + } + + /** + * Returns a plane with flipped direction + * @return a plane with flipped direction + */ + public Plane flip() { + Plane ret = new Plane(this); + ret.normal.scale(-1.0); + ret.distance *= -1.0; + return ret; } /** @@ -122,39 +152,50 @@ public class Plane implements Serializable { * @throws NullPointerException if the {@code point} is {@code null} */ public Point3d projectToPlane(Point3d point) { + Point3d ret = new Point3d(normal); + ret.scale(-getPointDistance(point)); + ret.add(point); + return ret; + + /* double shiftDist = ((normal.x * point.x) + (normal.y * point.y) + - (normal.z * point.z) + - distance) / normSquare; + (normal.z * point.z)) / normSquare; + shiftDist -= distance; Point3d ret = new Point3d(normal); ret.scale(-shiftDist); ret.add(point); return ret; + */ } /** * Returns a point laying on the opposite side of the plane ("mirrors" the point). - * This method implements equation (1) of - * <a href="https://link.springer.com/content/pdf/10.1007/s00371-020-02034-w.pdf">Hruda et al: Robust, fast and flexible symmetry plane detection based -on differentiable symmetry measure</a> * * @param point A 3D point to be reflected * @return a point on the opposite side of the plane. * @throws NullPointerException if the {@code point} is {@code null} */ public Point3d reflectOverPlane(Point3d point) { + Point3d ret = new Point3d(normal); + ret.scale(-2.0 * getPointDistance(point)); + ret.add(point); + return ret; + + /* double shiftDist = ((normal.x * point.x) + (normal.y * point.y) + (normal.z * point.z) + distance) / normSquare; - + //System.out.println("HHH "+ shiftDist); Point3d ret = new Point3d(normal); ret.scale(-2.0 * shiftDist); ret.add(point); return ret; + */ } /** @@ -181,7 +222,7 @@ on differentiable symmetry measure</a> * @throws IllegalArgumentException if {@code size} is <= 0 */ public MeshRectangleFacet getMesh(Point3d point, double size) { - return Plane.this.getMesh(point, size, size); + return getMesh(point, size, size); } /** @@ -193,14 +234,27 @@ on differentiable symmetry measure</a> * @throws NullPointerException if the {@code midPoint} or {@code bbox} are {@code null} */ public MeshRectangleFacet getMesh(BBox bbox) { - return Plane.this.getMesh(bbox.getMidPoint(), bbox.getDiagonalLength(), bbox.getDiagonalLength()); + return getMesh(bbox.getMidPoint(), bbox.getDiagonalLength(), bbox.getDiagonalLength()); } - protected final void setNormal(Vector3d normal) { - this.normal = normal; - this.normSquare = normal.dot(normal); + /** + * Changes the normal vector. + * @param normal new normalized normal vector + * @throw IllegalArgumentExcpetion if the normal is {@code null} + * or if length is not one. + */ + protected final void setNormal(Tuple3d normal) { + if (normal == null) { + throw new IllegalArgumentException("noraml"); + } + this.normal = new Vector3d(normal); + this.normSquare = this.normal.dot(this.normal); } + /** + * Changes the distance. + * @param dist new distance + */ protected final void setDistance(double dist) { this.distance = dist; } @@ -212,8 +266,10 @@ on differentiable symmetry measure</a> * the respect to the plane's normal, the negative distance is returned */ public double getPointDistance(Point3d point) { - return ((normal.x * point.x) + (normal.y * point.y) + (normal.z * point.z) + distance) - / Math.sqrt(normSquare); + Point3d v = new Point3d(point); + v.sub(this.getPlanePoint()); + return (normal.x * v.x) + (normal.y * v.y) + (normal.z * v.z); + //return ((normal.x * point.x) + (normal.y * point.y) + (normal.z * point.z) + distance) / Math.sqrt(normSquare); } /** @@ -226,12 +282,14 @@ on differentiable symmetry measure</a> public Point3d getIntersectionWithLine(Point3d p1, Point3d p2) { double distance1 = getPointDistance(p1); double distance2 = getPointDistance(p2); - double t = distance1 / (distance1 - distance2); - - if (distance1 * distance2 > 0) { + + if (distance1 * distance2 > 0) { // both are positive or negative return null; } + double t = distance1 / (distance1 - distance2); + + //Logger.print("EEE "+p1+ " "+getPointDistance(p1)); Point3d output = new Point3d(p2); output.sub(p1); output.scale(t); diff --git a/Comparison/src/main/java/cz/fidentis/analyst/visitors/mesh/HausdorffDistance.java b/Comparison/src/main/java/cz/fidentis/analyst/visitors/mesh/HausdorffDistance.java index 7e1f9688..ca048467 100644 --- a/Comparison/src/main/java/cz/fidentis/analyst/visitors/mesh/HausdorffDistance.java +++ b/Comparison/src/main/java/cz/fidentis/analyst/visitors/mesh/HausdorffDistance.java @@ -356,7 +356,7 @@ import javax.vecmath.Point3d; updateResults(visitor, vertices.get(i), comparedFacet); } } - + if (inParallel()) { // process asynchronous computation of distance executor.shutdown(); while (!executor.isTerminated()){} diff --git a/Comparison/src/test/java/cz/fidentis/analyst/symmetry/PlaneTest.java b/Comparison/src/test/java/cz/fidentis/analyst/symmetry/PlaneTest.java new file mode 100644 index 00000000..1e053ae7 --- /dev/null +++ b/Comparison/src/test/java/cz/fidentis/analyst/symmetry/PlaneTest.java @@ -0,0 +1,129 @@ +package cz.fidentis.analyst.symmetry; + +import javax.vecmath.Point3d; +import javax.vecmath.Vector3d; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + + +/** + * + * @author Radek Oslejsek + */ +public class PlaneTest { + + @Test + public void construction() { + Vector3d norm = new Vector3d(-1, -1, -1); + norm.normalize(); + + Plane p = new Plane(norm, -3); + Assertions.assertEquals(norm, p.getNormal()); + Assertions.assertEquals(-3, p.getDistance()); + + p = new Plane(p); + Assertions.assertEquals(norm, p.getNormal()); + Assertions.assertEquals(-3, p.getDistance()); + } + + @Test + public void modification() { + Vector3d norm = new Vector3d(-1, -1, -1); + norm.normalize(); + Plane p = new Plane(norm, -3); + + Assertions.assertTrue(p.getPlanePoint() + .epsilonEquals(new Vector3d(1.7320508075688776, 1.7320508075688776, 1.7320508075688776), 0.001)); + Assertions.assertTrue(p.shift(1) + .getPlanePoint().epsilonEquals(new Vector3d(1.1547005383792517, 1.1547005383792517, 1.1547005383792517), 0.001)); + Assertions.assertTrue(p.shift(-1) + .getPlanePoint().epsilonEquals(new Vector3d(2.3094010767585034, 2.3094010767585034, 2.3094010767585034), 0.001)); + + Vector3d n2 = new Vector3d(norm); + n2.scale(-1); + Assertions.assertEquals(n2, p.flip().getNormal()); + Assertions.assertEquals(3, p.flip().getDistance()); + } + + @Test + public void projectToPlane() { + Plane p = new Plane(new Vector3d(-1, 0, 0), -3); + Assertions.assertTrue(p.projectToPlane(new Point3d(9,9,9)) + .epsilonEquals(new Vector3d(3, 9, 9), 0.001)); + Assertions.assertTrue(p.projectToPlane(new Point3d(-9,-9,-9)) + .epsilonEquals(new Vector3d(3, -9, -9), 0.001)); + Assertions.assertTrue(p.projectToPlane(new Point3d(0,0,0)) + .epsilonEquals(new Vector3d(3, 0, 0), 0.001)); + + p = new Plane(new Vector3d(-1, 0, 0), 3); + Assertions.assertTrue(p.projectToPlane(new Point3d(9,9,9)) + .epsilonEquals(new Vector3d(-3, 9, 9), 0.001)); + Assertions.assertTrue(p.projectToPlane(new Point3d(-9,-9,-9)) + .epsilonEquals(new Vector3d(-3, -9, -9), 0.001)); + Assertions.assertTrue(p.projectToPlane(new Point3d(0,0,0)) + .epsilonEquals(new Vector3d(-3, 0, 0), 0.001)); + + Vector3d norm = new Vector3d(-1, -1, -1); + norm.normalize(); + p = new Plane(norm, -3); + Assertions.assertTrue(p.projectToPlane(new Point3d(9,9,9)) + .epsilonEquals(new Vector3d(1.7320508075688776, 1.7320508075688776, 1.7320508075688776), 0.001)); + Assertions.assertTrue(p.projectToPlane(new Point3d(-9,-9,-9)) + .epsilonEquals(new Vector3d(1.7320508075688776, 1.7320508075688776, 1.7320508075688776), 0.001)); + Assertions.assertTrue(p.projectToPlane(new Point3d(0,0,0)) + .epsilonEquals(new Vector3d(1.7320508075688776, 1.7320508075688776, 1.7320508075688776), 0.001)); + } + + @Test + public void reflectOverPlane() { + Plane p = new Plane(new Vector3d(-1, 0, 0), -3); + Assertions.assertTrue(p.reflectOverPlane(new Point3d(9,9,9)) + .epsilonEquals(new Vector3d(-3, 9, 9), 0.001)); + Assertions.assertTrue(p.reflectOverPlane(new Point3d(-9,-9,-9)) + .epsilonEquals(new Vector3d(15, -9, -9), 0.001)); + Assertions.assertTrue(p.reflectOverPlane(new Point3d(0,0,0)) + .epsilonEquals(new Vector3d(6, 0, 0), 0.001)); + + p = new Plane(new Vector3d(-1, 0, 0), 3); + Assertions.assertTrue(p.reflectOverPlane(new Point3d(9,9,9)) + .epsilonEquals(new Vector3d(-15, 9, 9), 0.001)); + Assertions.assertTrue(p.reflectOverPlane(new Point3d(-9,-9,-9)) + .epsilonEquals(new Vector3d(3, -9, -9), 0.001)); + Assertions.assertTrue(p.reflectOverPlane(new Point3d(0,0,0)) + .epsilonEquals(new Vector3d(-6, 0, 0), 0.001)); + + Vector3d norm = new Vector3d(-1, -1, -1); + norm.normalize(); + p = new Plane(norm, -3); + + Assertions.assertTrue(p.reflectOverPlane(new Point3d(9,9,9)) + .epsilonEquals(new Vector3d(-5.535898384862248, -5.535898384862248, -5.535898384862248), 0.001)); + Assertions.assertTrue(p.reflectOverPlane(new Point3d(-9,-9,-9)) + .epsilonEquals(new Vector3d(12.464101615137768, 12.464101615137768, 12.464101615137768), 0.001)); + Assertions.assertTrue(p.reflectOverPlane(new Point3d(0,0,0)) + .epsilonEquals(new Vector3d(3.464101615137756, 3.464101615137756, 3.464101615137756), 0.001)); + } + + @Test + public void getPointDistance() { + Plane p = new Plane(new Vector3d(-1, 0, 0), -3); + Assertions.assertEquals(-6, p.getPointDistance(new Point3d(9, 9, 9))); + Assertions.assertEquals(12, p.getPointDistance(new Point3d(-9, -9, -9))); + Assertions.assertEquals(3, p.getPointDistance(new Point3d(0, 0, 0))); + + p = new Plane(new Vector3d(-1, 0, 0), 3); + Assertions.assertEquals(-12, p.getPointDistance(new Point3d(9, 9, 9))); + Assertions.assertEquals(6, p.getPointDistance(new Point3d(-9, -9, -9))); + Assertions.assertEquals(-3, p.getPointDistance(new Point3d(0, 0, 0))); + + p = new Plane(new Vector3d(1, 0, 0), 3); + Assertions.assertEquals(6, p.getPointDistance(new Point3d(9, 9, 9))); + Assertions.assertEquals(-12, p.getPointDistance(new Point3d(-9, -9, -9))); + Assertions.assertEquals(-3, p.getPointDistance(new Point3d(0, 0, 0))); + + p = new Plane(new Vector3d(1, 0, 0), -3); + Assertions.assertEquals(12, p.getPointDistance(new Point3d(9, 9, 9))); + Assertions.assertEquals(-6, p.getPointDistance(new Point3d(-9, -9, -9))); + Assertions.assertEquals(3, p.getPointDistance(new Point3d(0, 0, 0))); + } +} diff --git a/Comparison/src/test/java/cz/fidentis/analyst/visitors/mesh/CrossSectionTest.java b/Comparison/src/test/java/cz/fidentis/analyst/visitors/mesh/CrossSectionTest.java index 21799570..29d32ff3 100644 --- a/Comparison/src/test/java/cz/fidentis/analyst/visitors/mesh/CrossSectionTest.java +++ b/Comparison/src/test/java/cz/fidentis/analyst/visitors/mesh/CrossSectionTest.java @@ -55,7 +55,7 @@ public class CrossSectionTest { */ @Test public void CrossSectionZigZag() { - Plane cuttingPlane = new Plane(new Vector3d(1, 0, 0), -0.5); + Plane cuttingPlane = new Plane(new Vector3d(-1, 0, 0), -0.5); MeshModel model = createModel(); CrossSectionZigZag cs = new CrossSectionZigZag(cuttingPlane); @@ -63,7 +63,7 @@ public class CrossSectionTest { CrossSectionCurve curve = cs.getCrossSectionCurve(); //They can be ordered two ways, check both - Assertions.assertEquals(curve.getSegmentSize(0), 6); + Assertions.assertEquals(6, curve.getSegmentSize(0)); if (curve.getSegment(0).get(0).equals(new Point3d(0.5, 0, 0))) { Assertions.assertEquals(curve.getSegment(0).get(0), new Point3d(0.5, 0, 0)); Assertions.assertEquals(curve.getSegment(0).get(1), new Point3d(0.5, 0.5, 0)); @@ -88,7 +88,7 @@ public class CrossSectionTest { */ @Test public void CrossSection() { - Plane cuttingPlane = new Plane(new Vector3d(1, 0, 0), -0.5); + Plane cuttingPlane = new Plane(new Vector3d(-1, 0, 0), -0.5); MeshModel model = createModel(); CrossSection cs = new CrossSection(cuttingPlane); @@ -98,7 +98,7 @@ public class CrossSectionTest { //They can be ordered two ways, check both //Flaw of the non-zigzag design: it can't detect first and last intersection, //since it only controls edges between two triangles. - Assertions.assertEquals(points.size(), 4); + Assertions.assertEquals(4, points.size()); if (points.get(0).equals(new Point3d(0.5, 0.5, 0))) { Assertions.assertEquals(points.get(0), new Point3d(0.5, 0.5, 0)); Assertions.assertEquals(points.get(1), new Point3d(0.5, 1, 0)); diff --git a/GUI/src/main/java/cz/fidentis/analyst/batch/ApproxHausdorffDistTask.java b/GUI/src/main/java/cz/fidentis/analyst/batch/ApproxHausdorffDistTask.java index 0a18717d..f7666fbf 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/batch/ApproxHausdorffDistTask.java +++ b/GUI/src/main/java/cz/fidentis/analyst/batch/ApproxHausdorffDistTask.java @@ -94,21 +94,16 @@ public class ApproxHausdorffDistTask extends SimilarityTask { HausdorffDistance hd = new HausdorffDistance( face.getKdTree(), HausdorffDistance.Strategy.POINT_TO_POINT, - true, // relative + true, // relative distance true, // parallel true // crop ); - templateFace.getMeshModel().compute(hd, true); - - // Store relative distances of individual vertices to the cache - distCache.add(hd.getDistances() - .values() - .stream() - .flatMap(List::stream) - .collect(Collectors.toList())); - face.removeKdTree(); // k-d tree construction is fast, and the memory is more valuable - + templateFace.getMeshModel().compute(hd); + distCache.add(hd.getDistances().values().stream() // Store relative distances of vertices to the cache + .flatMap(List::stream).collect(Collectors.toList())); hdComputationTime.stop(); + + face.removeKdTree(); // k-d tree construction is fast, and the memory is more valuable } else { distCache.add(DoubleStream.generate(() -> 0.0d) .limit(templateFace.getMeshModel().getNumVertices()) diff --git a/GUI/src/main/java/cz/fidentis/analyst/batch/ApproxHausdorffDistTaskGPU.java b/GUI/src/main/java/cz/fidentis/analyst/batch/ApproxHausdorffDistTaskGPU.java index b95faa4a..f6879d11 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/batch/ApproxHausdorffDistTaskGPU.java +++ b/GUI/src/main/java/cz/fidentis/analyst/batch/ApproxHausdorffDistTaskGPU.java @@ -150,11 +150,11 @@ public class ApproxHausdorffDistTaskGPU extends SimilarityTask { HausdorffDistance hd = new HausdorffDistance( face.getKdTree(), HausdorffDistance.Strategy.POINT_TO_POINT, - true, // relative + true, // relative distance true, // parallel true // crop ); - templateFace.getMeshModel().compute(hd, true); + templateFace.getMeshModel().compute(hd); // Copy commputed distances into the GPU memory: float[] auxDist = new float[numVertices]; @@ -168,9 +168,9 @@ public class ApproxHausdorffDistTaskGPU extends SimilarityTask { CL.CL_MEM_READ_ONLY | CL.CL_MEM_COPY_HOST_PTR, Sizeof.cl_float * numVertices, arrayP, null); - face.removeKdTree(); // TO BE TESTED - hdComputationTime.stop(); + + face.removeKdTree(); } else { // distance to template face itself is zero float[] auxDist = new float[numVertices]; // filled by zeros by default Pointer arrayP = Pointer.to(auxDist); diff --git a/GUI/src/main/java/cz/fidentis/analyst/batch/CompleteHausdorffDistTask.java b/GUI/src/main/java/cz/fidentis/analyst/batch/CompleteHausdorffDistTask.java index ea7eec93..c01d5330 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/batch/CompleteHausdorffDistTask.java +++ b/GUI/src/main/java/cz/fidentis/analyst/batch/CompleteHausdorffDistTask.java @@ -60,28 +60,26 @@ public class CompleteHausdorffDistTask extends SimilarityTask { //Logger.print(priFace.getShortName() + " - " + secFace.getShortName()); - // compute Huasdorff distance in both ways + // compute Huasdorff distance in both directions hdComputationTime.start(); - //priFace.computeKdTree(true); HausdorffDistance hd = new HausdorffDistance( - priFace.getMeshModel(), - HausdorffDistance.Strategy.POINT_TO_POINT, - false, // relative + priFace.getKdTree(), + HausdorffDistance.Strategy.POINT_TO_POINT, + false, // relative distance true, // parallel - true // crop + true // crop ); - secFace.getMeshModel().compute(hd, true); + secFace.getMeshModel().compute(hd); setDistSimilarity(j, i, hd.getStats().getAverage()); - //secFace.computeKdTree(true); hd = new HausdorffDistance( - secFace.getMeshModel(), - HausdorffDistance.Strategy.POINT_TO_POINT, - false, // relative + secFace.getKdTree(), + HausdorffDistance.Strategy.POINT_TO_POINT, + false, // relative distance true, // parallel - false // crop - ); - priFace.getMeshModel().compute(hd, true); + true // crop + ); + priFace.getMeshModel().compute(hd); setDistSimilarity(i, j, hd.getStats().getAverage()); hdComputationTime.stop(); diff --git a/GUI/src/main/java/cz/fidentis/analyst/batch/IcpTask.java b/GUI/src/main/java/cz/fidentis/analyst/batch/IcpTask.java index a6b375c9..d82266ed 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/batch/IcpTask.java +++ b/GUI/src/main/java/cz/fidentis/analyst/batch/IcpTask.java @@ -5,9 +5,7 @@ import cz.fidentis.analyst.canvas.Canvas; import cz.fidentis.analyst.core.ProgressDialog; import cz.fidentis.analyst.face.HumanFace; import cz.fidentis.analyst.face.HumanFaceFactory; -import cz.fidentis.analyst.icp.IcpTransformer; -import cz.fidentis.analyst.icp.NoUndersampling; -import cz.fidentis.analyst.icp.RandomStrategy; +import cz.fidentis.analyst.face.HumanFaceUtils; import cz.fidentis.analyst.mesh.core.MeshModel; import cz.fidentis.analyst.mesh.io.MeshObjExporter; import cz.fidentis.analyst.scene.DrawableFace; @@ -103,13 +101,15 @@ public class IcpTask extends SwingWorker<MeshModel, HumanFace> { if (computeICP) { // ICP registration - face is transformed! icpComputationTime.start(); - IcpTransformer icp = new IcpTransformer( - initFace.getMeshModel(), - 100, - controlPanel.scaleIcp(), - 0.3, - (undersampling == 100) ? new NoUndersampling() : new RandomStrategy(undersampling)); - face.getMeshModel().compute(icp, true); // superimpose face towards the initFace + HumanFaceUtils.alignMeshes( + initFace, + face, // is transformed + 100, // max iterations + controlPanel.scaleIcp(), + 0.3, // error + undersampling, + false // drop k-d tree, if exists + ); icpComputationTime.stop(); } diff --git a/GUI/src/main/java/cz/fidentis/analyst/canvas/Canvas.java b/GUI/src/main/java/cz/fidentis/analyst/canvas/Canvas.java index 29feb057..053f750f 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/canvas/Canvas.java +++ b/GUI/src/main/java/cz/fidentis/analyst/canvas/Canvas.java @@ -8,7 +8,12 @@ import cz.fidentis.analyst.scene.Camera; import cz.fidentis.analyst.scene.Scene; import cz.fidentis.analyst.scene.SceneRenderer; import cz.fidentis.analyst.canvas.toolbar.RenderingModeToolbox; +import cz.fidentis.analyst.face.events.HumanFaceEvent; +import cz.fidentis.analyst.face.events.HumanFaceListener; +import cz.fidentis.analyst.face.events.HumanFaceTransformedEvent; +import cz.fidentis.analyst.face.events.SymmetryPlaneChangedEvent; import cz.fidentis.analyst.scene.Drawable; +import cz.fidentis.analyst.scene.DrawableFace; import java.awt.BorderLayout; import java.awt.Color; import java.awt.event.ActionEvent; @@ -39,7 +44,7 @@ import org.openide.util.NbBundle; * @author Natalia Bebjakova * @author Radek Oslejsek */ -public class Canvas extends JPanel { +public class Canvas extends JPanel implements HumanFaceListener { private static final String RENDERING_MODE_TOOLBOX_ICON = "wireframe28x28.png"; private static final String BACKGROUND_BUTTON_ICON = "background28x28.png"; @@ -87,6 +92,7 @@ public class Canvas extends JPanel { int index = scene.getFreeSlot(); scene.setHumanFace(index, face); scene.setFaceAsPrimary(index); + face.registerListener(this); return index; } return -1; @@ -104,6 +110,7 @@ public class Canvas extends JPanel { int index = scene.getFreeSlot(); scene.setHumanFace(index, face); scene.setFaceAsSecondary(index); + face.registerListener(this); return index; } return -1; @@ -226,6 +233,24 @@ public class Canvas extends JPanel { } } + @Override + public void acceptEvent(HumanFaceEvent event) { + // The symmtery plane is replaced with new nstance during the transformation + // of a human face. Therefore, the drawable plane has to be updated. + // It is not necesary for meshes and feature points because their vertices + // are directly modified and then do not affect their drawable wrappers + if (event instanceof HumanFaceTransformedEvent || event instanceof SymmetryPlaneChangedEvent) { + final HumanFace face = event.getFace(); + final List<DrawableFace> drFaces = getScene().getDrawableFaces(); + for (int i = 0; i < drFaces.size(); i++) { + if (drFaces.get(i).getHumanFace().equals(face)) { // recomputes and replaces i-th symmetry plane + getScene().setDrawableSymmetryPlane(i, face); + break; + } + } + } + } + private ControlButtons getButtonsPanel(Canvas canvas) { diff --git a/GUI/src/main/java/cz/fidentis/analyst/canvas/toolbar/SceneToolboxFaceToFace.java b/GUI/src/main/java/cz/fidentis/analyst/canvas/toolbar/SceneToolboxFaceToFace.java index 984f9ac2..3ec0db48 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/canvas/toolbar/SceneToolboxFaceToFace.java +++ b/GUI/src/main/java/cz/fidentis/analyst/canvas/toolbar/SceneToolboxFaceToFace.java @@ -1,8 +1,12 @@ package cz.fidentis.analyst.canvas.toolbar; import cz.fidentis.analyst.canvas.Canvas; +import cz.fidentis.analyst.face.HumanFace; +import cz.fidentis.analyst.face.events.HumanFaceEvent; +import cz.fidentis.analyst.face.events.HumanFaceListener; +import cz.fidentis.analyst.face.events.SymmetryPlaneChangedEvent; import cz.fidentis.analyst.scene.DrawableFace; -import cz.fidentis.analyst.scene.DrawableFeaturePoints; +import cz.fidentis.analyst.scene.DrawablePlane; import java.awt.Color; import java.awt.FlowLayout; import java.awt.event.ActionEvent; @@ -23,8 +27,9 @@ import org.openide.util.NbBundle; * @author Richard Pajersky * @author Radek Oslejsek */ -public class SceneToolboxFaceToFace extends JPanel { +public class SceneToolboxFaceToFace extends JPanel implements HumanFaceListener { + private static final String SYMMETRY_BUTTON_ICON = "symmetry28x28.png"; private static final String PRIMARY_FACE_ICON = "primaryFace28x28.png"; private static final String SECONDARY_FACE_ICON = "secondaryFace28x28.png"; private static final String FEATURE_POINTS_ICON = "fps28x28.png"; @@ -32,10 +37,12 @@ public class SceneToolboxFaceToFace extends JPanel { private static final int TRANSPARENCY_RANGE = 50; - private JToggleButton primLandButton; + private JToggleButton priLandButton; private JToggleButton secLandButton; - private JToggleButton primFaceButton; + private JToggleButton priFaceButton; private JToggleButton secFaceButton; + private JToggleButton priSymmetryButton; + private JToggleButton secSymmetryButton; private JSlider slider; private JToggleButton secDistButton; @@ -47,50 +54,106 @@ public class SceneToolboxFaceToFace extends JPanel { */ public SceneToolboxFaceToFace(Canvas canvas) { this.canvas = canvas; - initComponents(); + initComponents(); // buttons are turned on by default + + // be informed if something changes in the faces -- see acceptEvent() + canvas.getPrimaryFace().registerListener(this); + canvas.getSecondaryFace().registerListener(this); // Change inital state: - primLandButton.setSelected(false); - DrawableFeaturePoints fp0 = canvas.getScene().getDrawableFeaturePoints(canvas.getScene().getPrimaryFaceSlot()); - if (fp0 != null) { - fp0.show(false); - } + + // show the symmetry planes automatically, but only after they are computed + priSymmetryButton.setSelected(true); + priSymmetryButton.setEnabled(false); + secSymmetryButton.setSelected(true); + secSymmetryButton.setEnabled(false); + // currently, FPs (landmarks) are either presented or not + priLandButton.setSelected(false); + if (canvas.getPrimaryFace().hasFeaturePoints()) { + canvas.getScene().getDrawableFeaturePoints(canvas.getScene().getPrimaryFaceSlot()).show(false); + } else { + priLandButton.setEnabled(false); + } secLandButton.setSelected(false); - DrawableFeaturePoints fp1 = canvas.getScene().getDrawableFeaturePoints(canvas.getScene().getSecondaryFaceSlot()); - if (fp1 != null) { - fp1.show(false); + if (canvas.getSecondaryFace().hasFeaturePoints()) { + canvas.getScene().getDrawableFeaturePoints(canvas.getScene().getSecondaryFaceSlot()).show(false); + } else { + secLandButton.setEnabled(false); } - secDistButton.setSelected(true); - canvas.getScene().getDrawableFace(canvas.getScene().getSecondaryFaceSlot()).setRenderHeatmap(true); + secDistButton.setSelected(false); + canvas.getScene().getDrawableFace(canvas.getScene().getSecondaryFaceSlot()).setRenderHeatmap(false); slider.setValue(30); canvas.getScene().getDrawableFace(canvas.getScene().getPrimaryFaceSlot()).setTransparency(30/(float)TRANSPARENCY_RANGE); canvas.getScene().getDrawableFace(canvas.getScene().getSecondaryFaceSlot()).setTransparency(1); } + + + @Override + public void acceptEvent(HumanFaceEvent event) { + if (event instanceof SymmetryPlaneChangedEvent) { + DrawablePlane dp = null; + JToggleButton button = null; + HumanFace face = event.getFace(); + + if (face.equals(canvas.getPrimaryFace())) { + dp = canvas.getScene().getDrawableSymmetryPlane(canvas.getScene().getPrimaryFaceSlot()); + button = priSymmetryButton; + } else if (face.equals(canvas.getSecondaryFace())) { + dp = canvas.getScene().getDrawableSymmetryPlane(canvas.getScene().getSecondaryFaceSlot()); + button = secSymmetryButton; + } + + if (dp != null) { // the symmetry plane is included in the scene + button.setEnabled(true); + dp.show(button.isSelected()); + } else { + button.setEnabled(false); + } + } + } + + private JToggleButton initButton(Color color, String icon, String tooltip) { + JToggleButton button = new JToggleButton(); + button.setBorder(Canvas.BUTTONS_BORDER); + if (color != null) { + button.setBackground(null); // default color + button.setUI(new MetalToggleButtonUI() { + @Override + protected Color getSelectColor() { + return color; + } + }); + } + button.setIcon(new ImageIcon(getClass().getResource("/" + icon))); + button.setFocusable(false); + button.setSelected(true); + button.setToolTipText(NbBundle.getMessage(SceneToolboxFaceToFace.class, tooltip)); + + return button; + } private void initComponents() { JPanel panel = new JPanel(); panel.setLayout(new FlowLayout()); add(panel); - primLandButton = new JToggleButton(); - primLandButton.setBorder(Canvas.BUTTONS_BORDER); - primLandButton.setBackground(DrawableFace.SKIN_COLOR_PRIMARY); - primLandButton.setUI(new MetalToggleButtonUI() { + priSymmetryButton = initButton(null, SYMMETRY_BUTTON_ICON, "SceneToolboxFaceToFace.symmetryButton.tooltip"); + panel.add(priSymmetryButton); + priSymmetryButton.addActionListener(new AbstractAction() { @Override - protected Color getSelectColor() { - return DrawableFace.SKIN_COLOR_PRIMARY.darker(); + public void actionPerformed(ActionEvent e) { + boolean onOff = ((JToggleButton) e.getSource()).isSelected(); + canvas.getScene().getDrawableSymmetryPlane(canvas.getScene().getPrimaryFaceSlot()).show(onOff); + canvas.renderScene(); } }); - primLandButton.setIcon(new ImageIcon(getClass().getResource("/" + FEATURE_POINTS_ICON))); - primLandButton.setFocusable(false); - primLandButton.setSelected(true); - primLandButton.setToolTipText(NbBundle.getMessage(SceneToolboxFaceToFace.class, "SceneToolboxFaceToFace.landButton.tooltip")); - panel.add(primLandButton); - - primLandButton.addActionListener(new AbstractAction() { + + priLandButton = initButton(null, FEATURE_POINTS_ICON, "SceneToolboxFaceToFace.landButton.tooltip"); + panel.add(priLandButton); + priLandButton.addActionListener(new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { boolean onOff = ((JToggleButton) e.getSource()).isSelected(); @@ -99,23 +162,9 @@ public class SceneToolboxFaceToFace extends JPanel { } }); - ///////////////////////// - primFaceButton = new JToggleButton(); - primFaceButton.setBorder(Canvas.BUTTONS_BORDER); - primFaceButton.setBackground(DrawableFace.SKIN_COLOR_PRIMARY); - primFaceButton.setUI(new MetalToggleButtonUI() { - @Override - protected Color getSelectColor() { - return DrawableFace.SKIN_COLOR_PRIMARY.darker(); - } - }); - primFaceButton.setIcon(new ImageIcon(getClass().getResource("/" + PRIMARY_FACE_ICON))); - primFaceButton.setFocusable(false); - primFaceButton.setSelected(true); - primFaceButton.setToolTipText(NbBundle.getMessage(SceneToolboxFaceToFace.class, "SceneToolboxFaceToFace.faceButton.tooltip")); - panel.add(primFaceButton); - - primFaceButton.addActionListener(new AbstractAction() { + priFaceButton = initButton(DrawableFace.SKIN_COLOR_PRIMARY, PRIMARY_FACE_ICON, "SceneToolboxFaceToFace.faceButton.tooltip"); + panel.add(priFaceButton); + priFaceButton.addActionListener(new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { boolean onOff = ((JToggleButton) e.getSource()).isSelected(); @@ -150,21 +199,8 @@ public class SceneToolboxFaceToFace extends JPanel { }); ///////////////////////// - secFaceButton = new JToggleButton(); - secFaceButton.setBorder(Canvas.BUTTONS_BORDER); - secFaceButton.setBackground(DrawableFace.SKIN_COLOR_SECONDARY); - secFaceButton.setUI(new MetalToggleButtonUI() { - @Override - protected Color getSelectColor() { - return DrawableFace.SKIN_COLOR_SECONDARY.darker(); - } - }); - secFaceButton.setIcon(new ImageIcon(getClass().getResource("/" + SECONDARY_FACE_ICON))); - secFaceButton.setFocusable(false); - secFaceButton.setSelected(true); - secFaceButton.setToolTipText(NbBundle.getMessage(SceneToolboxFaceToFace.class, "SceneToolboxFaceToFace.faceButton.tooltip")); + secFaceButton = initButton(DrawableFace.SKIN_COLOR_SECONDARY, SECONDARY_FACE_ICON, "SceneToolboxFaceToFace.faceButton.tooltip"); panel.add(secFaceButton); - secFaceButton.addActionListener(new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { @@ -174,23 +210,8 @@ public class SceneToolboxFaceToFace extends JPanel { } }); - - ///////////////////////// - secLandButton = new JToggleButton(); - secLandButton.setBorder(Canvas.BUTTONS_BORDER); - secLandButton.setBackground(DrawableFace.SKIN_COLOR_SECONDARY); - secLandButton.setUI(new MetalToggleButtonUI() { - @Override - protected Color getSelectColor() { - return DrawableFace.SKIN_COLOR_SECONDARY.darker(); - } - }); - secLandButton.setIcon(new ImageIcon(getClass().getResource("/" + FEATURE_POINTS_ICON))); - secLandButton.setFocusable(false); - secLandButton.setSelected(true); - secLandButton.setToolTipText(NbBundle.getMessage(SceneToolboxFaceToFace.class, "SceneToolboxFaceToFace.landButton.tooltip")); + secLandButton = initButton(null, FEATURE_POINTS_ICON, "SceneToolboxFaceToFace.landButton.tooltip"); panel.add(secLandButton); - secLandButton.addActionListener(new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { @@ -199,38 +220,27 @@ public class SceneToolboxFaceToFace extends JPanel { canvas.renderScene(); } }); - - ///////////////////// - secDistButton = new JToggleButton(); - secDistButton.setBorder(Canvas.BUTTONS_BORDER); - secDistButton.setBackground(DrawableFace.SKIN_COLOR_SECONDARY); - secDistButton.setUI(new MetalToggleButtonUI() { + + secSymmetryButton = initButton(null, SYMMETRY_BUTTON_ICON, "SceneToolboxFaceToFace.symmetryButton.tooltip"); + panel.add(secSymmetryButton); + secSymmetryButton.addActionListener(new AbstractAction() { @Override - protected Color getSelectColor() { - return DrawableFace.SKIN_COLOR_SECONDARY.darker(); + public void actionPerformed(ActionEvent e) { + boolean onOff = ((JToggleButton) e.getSource()).isSelected(); + canvas.getScene().getDrawableSymmetryPlane(canvas.getScene().getSecondaryFaceSlot()).show(onOff); + canvas.renderScene(); } }); - secDistButton.setIcon(new ImageIcon(getClass().getResource("/" + DISTANCE_BUTTON_ICON))); - secDistButton.setFocusable(false); - secDistButton.setSelected(true); - secDistButton.setToolTipText(NbBundle.getMessage(SceneToolboxFaceToFace.class, "SceneToolboxFaceToFace.distButton.tooltip")); + + secDistButton = initButton(null, DISTANCE_BUTTON_ICON, "SceneToolboxFaceToFace.distButton.tooltip"); panel.add(secDistButton); - secDistButton.addActionListener(new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { - if (((JToggleButton) e.getSource()).isSelected()) { - canvas.getScene().getDrawableFace(canvas.getScene().getSecondaryFaceSlot()).setRenderHeatmap(true); - } else { - canvas.getScene().getDrawableFace(canvas.getScene().getSecondaryFaceSlot()).setRenderHeatmap(false); - } - // Switch: - //canvas.getScene().getDrawableFace(secIndex).setRenderHeatmap( - // !canvas.getScene().getDrawableFace(secIndex).isHeatmapRendered() - //); + boolean onOff = ((JToggleButton) e.getSource()).isSelected(); + canvas.getScene().getDrawableFace(canvas.getScene().getSecondaryFaceSlot()).setRenderHeatmap(onOff); canvas.renderScene(); } }); } - } diff --git a/GUI/src/main/java/cz/fidentis/analyst/canvas/toolbar/SceneToolboxSingleFace.java b/GUI/src/main/java/cz/fidentis/analyst/canvas/toolbar/SceneToolboxSingleFace.java index b5078bcb..c27fee48 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/canvas/toolbar/SceneToolboxSingleFace.java +++ b/GUI/src/main/java/cz/fidentis/analyst/canvas/toolbar/SceneToolboxSingleFace.java @@ -1,7 +1,11 @@ package cz.fidentis.analyst.canvas.toolbar; import cz.fidentis.analyst.canvas.Canvas; +import cz.fidentis.analyst.face.events.HumanFaceEvent; +import cz.fidentis.analyst.face.events.HumanFaceListener; +import cz.fidentis.analyst.face.events.SymmetryPlaneChangedEvent; import cz.fidentis.analyst.scene.DrawableFace; +import cz.fidentis.analyst.scene.DrawablePlane; import java.awt.Color; import java.awt.FlowLayout; import java.awt.event.ActionEvent; @@ -22,15 +26,17 @@ import org.openide.util.NbBundle; * @author Richard Pajersky * @author Radek Oslejsek */ -public class SceneToolboxSingleFace extends JPanel { +public class SceneToolboxSingleFace extends JPanel implements HumanFaceListener { + private static final String SYMMETRY_BUTTON_ICON = "symmetry28x28.png"; private static final String FACE_ICON = "head28x28.png"; private static final String FEATURE_POINTS_ICON = "fps28x28.png"; private static final int TRANSPARENCY_RANGE = 100; - private JToggleButton primLandButton; - private JToggleButton primFaceButton; + private JToggleButton priLandButton; + private JToggleButton priFaceButton; + private JToggleButton priSymmetryButton; private JSlider slider; private final Canvas canvas; @@ -42,6 +48,56 @@ public class SceneToolboxSingleFace extends JPanel { public SceneToolboxSingleFace(Canvas canvas) { this.canvas = canvas; initComponents(); + + // show the symmetry planes automatically, but only after they are computed + priSymmetryButton.setSelected(true); + priSymmetryButton.setEnabled(false); + + // currently, FPs (landmarks) are either presented or not + priLandButton.setSelected(false); + if (canvas.getPrimaryFace() != null) { + if (canvas.getPrimaryFace().hasFeaturePoints()) { + canvas.getScene().getDrawableFeaturePoints(canvas.getScene().getPrimaryFaceSlot()).show(false); + } else { + priLandButton.setEnabled(false); + } + + // be informed if something changes in the faces -- see acceptEvent() + canvas.getPrimaryFace().registerListener(this); + } + } + + @Override + public void acceptEvent(HumanFaceEvent event) { + if (event instanceof SymmetryPlaneChangedEvent) { + DrawablePlane dp = canvas.getScene().getDrawableSymmetryPlane(canvas.getScene().getPrimaryFaceSlot()); + if (dp != null) { // the symmetry plane is included in the scene + priSymmetryButton.setEnabled(true); + dp.show(priSymmetryButton.isSelected()); + } else { + priSymmetryButton.setEnabled(false); + } + } + } + + private JToggleButton initButton(Color color, String icon, String tooltip) { + JToggleButton button = new JToggleButton(); + button.setBorder(Canvas.BUTTONS_BORDER); + if (color != null) { + button.setBackground(null); // default color + button.setUI(new MetalToggleButtonUI() { + @Override + protected Color getSelectColor() { + return color; + } + }); + } + button.setIcon(new ImageIcon(getClass().getResource("/" + icon))); + button.setFocusable(false); + button.setSelected(true); + button.setToolTipText(NbBundle.getMessage(SceneToolboxFaceToFace.class, tooltip)); + + return button; } private void initComponents() { @@ -49,22 +105,20 @@ public class SceneToolboxSingleFace extends JPanel { panel.setLayout(new FlowLayout()); add(panel); - primLandButton = new JToggleButton(); - primLandButton.setBorder(Canvas.BUTTONS_BORDER); - primLandButton.setBackground(DrawableFace.SKIN_COLOR_PRIMARY); - primLandButton.setUI(new MetalToggleButtonUI() { + priSymmetryButton = initButton(null, SYMMETRY_BUTTON_ICON, "SceneToolboxFaceToFace.symmetryButton.tooltip"); + panel.add(priSymmetryButton); + priSymmetryButton.addActionListener(new AbstractAction() { @Override - protected Color getSelectColor() { - return DrawableFace.SKIN_COLOR_PRIMARY.darker(); + public void actionPerformed(ActionEvent e) { + boolean onOff = ((JToggleButton) e.getSource()).isSelected(); + canvas.getScene().getDrawableSymmetryPlane(canvas.getScene().getPrimaryFaceSlot()).show(onOff); + canvas.renderScene(); } }); - primLandButton.setIcon(new ImageIcon(getClass().getResource("/" + FEATURE_POINTS_ICON))); - primLandButton.setFocusable(false); - primLandButton.setSelected(true); - primLandButton.setToolTipText(NbBundle.getMessage(SceneToolboxSingleFace.class, "SceneToolboxSingleFace.landButton.tooltip")); - panel.add(primLandButton); - primLandButton.addActionListener(new AbstractAction() { + priLandButton = initButton(null, FEATURE_POINTS_ICON, "SceneToolboxFaceToFace.landButton.tooltip"); + panel.add(priLandButton); + priLandButton.addActionListener(new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { boolean onOff = ((JToggleButton) e.getSource()).isSelected(); @@ -72,24 +126,10 @@ public class SceneToolboxSingleFace extends JPanel { canvas.renderScene(); } }); - - ///////////////////////// - primFaceButton = new JToggleButton(); - primFaceButton.setBorder(Canvas.BUTTONS_BORDER); - primFaceButton.setBackground(DrawableFace.SKIN_COLOR_PRIMARY); - primFaceButton.setUI(new MetalToggleButtonUI() { - @Override - protected Color getSelectColor() { - return DrawableFace.SKIN_COLOR_PRIMARY.darker(); - } - }); - primFaceButton.setIcon(new ImageIcon(getClass().getResource("/" + FACE_ICON))); - primFaceButton.setFocusable(false); - primFaceButton.setSelected(true); - primFaceButton.setToolTipText(NbBundle.getMessage(SceneToolboxSingleFace.class, "SceneToolboxSingleFace.faceButton.tooltip")); - panel.add(primFaceButton); - primFaceButton.addActionListener(new AbstractAction() { + priFaceButton = initButton(DrawableFace.SKIN_COLOR_PRIMARY, FACE_ICON, "SceneToolboxFaceToFace.faceButton.tooltip"); + panel.add(priFaceButton); + priFaceButton.addActionListener(new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { boolean onOff = ((JToggleButton) e.getSource()).isSelected(); @@ -98,6 +138,7 @@ public class SceneToolboxSingleFace extends JPanel { } }); + ///////////////////////// slider = new JSlider(); //slider.setMajorTickSpacing(TRANSPARENCY_RANGE/2); diff --git a/GUI/src/main/java/cz/fidentis/analyst/distance/DistanceAction.java b/GUI/src/main/java/cz/fidentis/analyst/distance/DistanceAction.java index 455ab3f8..28798ab3 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/distance/DistanceAction.java +++ b/GUI/src/main/java/cz/fidentis/analyst/distance/DistanceAction.java @@ -8,7 +8,7 @@ import cz.fidentis.analyst.core.ControlPanelAction; import cz.fidentis.analyst.face.events.HausdorffDistanceComputed; import cz.fidentis.analyst.face.events.HumanFaceEvent; import cz.fidentis.analyst.face.events.HumanFaceListener; -import cz.fidentis.analyst.face.events.MeshChangedEvent; +import cz.fidentis.analyst.face.events.HumanFaceTransformedEvent; import cz.fidentis.analyst.feature.FeaturePoint; import cz.fidentis.analyst.feature.FeaturePointType; import cz.fidentis.analyst.scene.DrawableFpWeights; @@ -203,15 +203,18 @@ public class DistanceAction extends ControlPanelAction implements HumanFaceListe @Override public void acceptEvent(HumanFaceEvent event) { - if (event instanceof MeshChangedEvent && event.getIssuer() != this) { // recompte (W)HD - hdVisitor = null; - whdVisitor = null; - computeAndUpdateHausdorffDistance(true); - // Relocate weight speheres: - final List<FeaturePoint> secondaryFPs = getSecondaryFeaturePoints().getFeaturePoints(); - final List<FeaturePoint> weightedFPs = fpSpheres.getFeaturePoints(); - for (int i = 0; i < secondaryFPs.size(); i++) { - weightedFPs.get(i).getPosition().set(secondaryFPs.get(i).getPosition()); + if (event instanceof HumanFaceTransformedEvent && event.getIssuer() != this) { // recompte (W)HD + HumanFaceTransformedEvent ftEvent = (HumanFaceTransformedEvent) event; + if (ftEvent.isFinished()) { + hdVisitor = null; + whdVisitor = null; + computeAndUpdateHausdorffDistance(true); + // Relocate weight speheres: + final List<FeaturePoint> secondaryFPs = getSecondaryFeaturePoints().getFeaturePoints(); + final List<FeaturePoint> weightedFPs = fpSpheres.getFeaturePoints(); + for (int i = 0; i < secondaryFPs.size(); i++) { + weightedFPs.get(i).getPosition().set(secondaryFPs.get(i).getPosition()); + } } } else if (event instanceof HausdorffDistanceComputed) { // update stats HausdorffDistanceComputed hdEvent = (HausdorffDistanceComputed) event; @@ -281,14 +284,16 @@ public class DistanceAction extends ControlPanelAction implements HumanFaceListe getPrimaryDrawableFace().getHumanFace().getKdTree(), useStrategy, relativeDist, - true, + true, // parallel crop ); } whdVisitor = new HausdorffDistancePrioritized(hdVisitor, featurePoints); Logger out = Logger.measureTime(); + getSecondaryDrawableFace().getHumanFace().accept(whdVisitor); + out.printDuration("Weighted Hausdorff distance for models with " + getPrimaryDrawableFace().getHumanFace().getMeshModel().getNumVertices() + "/" @@ -304,7 +309,7 @@ public class DistanceAction extends ControlPanelAction implements HumanFaceListe getSecondaryDrawableFace().getHumanFace().getShortName(), this )); - } + } /** * Updates the GUI elements of {@link DistancePanel} that display diff --git a/GUI/src/main/java/cz/fidentis/analyst/registration/RegistrationAction.java b/GUI/src/main/java/cz/fidentis/analyst/registration/RegistrationAction.java index 63982c23..a18d4d5e 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/registration/RegistrationAction.java +++ b/GUI/src/main/java/cz/fidentis/analyst/registration/RegistrationAction.java @@ -4,29 +4,19 @@ import cz.fidentis.analyst.Logger; import cz.fidentis.analyst.canvas.Canvas; import cz.fidentis.analyst.core.ControlPanelAction; import cz.fidentis.analyst.face.HumanFace; +import cz.fidentis.analyst.face.HumanFaceUtils; import cz.fidentis.analyst.face.events.HausdorffDistanceComputed; import cz.fidentis.analyst.face.events.HumanFaceEvent; import cz.fidentis.analyst.face.events.HumanFaceListener; -import cz.fidentis.analyst.face.events.MeshChangedEvent; -import cz.fidentis.analyst.feature.FeaturePoint; -import cz.fidentis.analyst.icp.IcpTransformation; -import cz.fidentis.analyst.icp.IcpTransformer; -import cz.fidentis.analyst.icp.NoUndersampling; -import cz.fidentis.analyst.icp.RandomStrategy; -import cz.fidentis.analyst.icp.UndersamplingStrategy; -import cz.fidentis.analyst.mesh.core.MeshFacet; -import cz.fidentis.analyst.mesh.core.MeshPoint; +import cz.fidentis.analyst.face.events.HumanFaceTransformedEvent; import cz.fidentis.analyst.procrustes.ProcrustesAnalysis; -import cz.fidentis.analyst.scene.DrawableFace; import java.awt.Color; import java.awt.event.ActionEvent; -import java.util.List; import java.util.zip.DataFormatException; import javax.swing.JCheckBox; -import javax.swing.JComboBox; import javax.swing.JFormattedTextField; +import javax.swing.JOptionPane; import javax.swing.JTabbedPane; -import javax.vecmath.Point3d; import javax.vecmath.Vector3d; /** @@ -61,13 +51,11 @@ public class RegistrationAction extends ControlPanelAction implements HumanFaceL private boolean scale = true; private int maxIterations = 10; private double error = 0.3; - private UndersamplingStrategy undersampling = new RandomStrategy(200); private String strategy = RegistrationPanel.STRATEGY_POINT_TO_POINT; private boolean relativeDist = false; private boolean heatmapRender = false; private boolean procrustesScalingEnabled = false; - /* * Coloring threshold and statistical values of feature point distances: */ @@ -115,100 +103,29 @@ public class RegistrationAction extends ControlPanelAction implements HumanFaceL case RegistrationPanel.ACTION_COMMAND_APPLY_ICP: applyICP(); highlightCloseFeaturePoints(); - announceMeshChange(getSecondaryDrawableFace()); - break; - case RegistrationPanel.ACTION_COMMAND_SHIFT_X: - value = ((Number) (((JFormattedTextField) ae.getSource()).getValue())).doubleValue(); - getSecondaryDrawableFace().getTranslation().x = value; - if (getSecondaryFeaturePoints() != null) { - getSecondaryFeaturePoints().getTranslation().x = value; - } - highlightCloseFeaturePoints(); - break; - case RegistrationPanel.ACTION_COMMAND_SHIFT_Y: - value = ((Number) (((JFormattedTextField) ae.getSource()).getValue())).doubleValue(); - getSecondaryDrawableFace().getTranslation().y = value; - if (getSecondaryFeaturePoints() != null) { - getSecondaryFeaturePoints().getTranslation().y = value; - } - highlightCloseFeaturePoints(); - break; - case RegistrationPanel.ACTION_COMMAND_SHIFT_Z: - value = ((Number) (((JFormattedTextField) ae.getSource()).getValue())).doubleValue(); - getSecondaryDrawableFace().getTranslation().z = value; - if (getSecondaryFeaturePoints() != null) { - getSecondaryFeaturePoints().getTranslation().z = value; - } - highlightCloseFeaturePoints(); + //announceMeshChange(getSecondaryDrawableFace()); + getCanvas().getSecondaryFace().announceEvent(new HumanFaceTransformedEvent( + getCanvas().getSecondaryFace(), "", this) + ); break; - case RegistrationPanel.ACTION_COMMAND_ROTATE_X: - value = ((Number) (((JFormattedTextField) ae.getSource()).getValue())).doubleValue(); - getSecondaryDrawableFace().getRotation().x = value; - if (getSecondaryFeaturePoints() != null) { - getSecondaryFeaturePoints().getRotation().x = value; - } + case RegistrationPanel.ACTION_COMMAND_MANUAL_TRANSFORMATION_IN_PROGRESS: + HumanFaceUtils.transformFace( + getSecondaryDrawableFace().getHumanFace(), + controlPanel.getAndClearManualRotation(), + controlPanel.getAndClearManualTranslation(), + controlPanel.getAndClearManualScale(), + false + ); highlightCloseFeaturePoints(); + getCanvas().getSecondaryFace().announceEvent(new HumanFaceTransformedEvent( + getCanvas().getSecondaryFace(), "", this, false) // transformation under progress + ); break; - case RegistrationPanel.ACTION_COMMAND_ROTATE_Y: - value = ((Number) (((JFormattedTextField) ae.getSource()).getValue())).doubleValue(); - getSecondaryDrawableFace().getRotation().y = value; - if (getSecondaryFeaturePoints() != null) { - getSecondaryFeaturePoints().getRotation().y = value; - } - highlightCloseFeaturePoints(); - break; - case RegistrationPanel.ACTION_COMMAND_ROTATE_Z: - value = ((Number) (((JFormattedTextField) ae.getSource()).getValue())).doubleValue(); - getSecondaryDrawableFace().getRotation().z = value; - if (getSecondaryFeaturePoints() != null) { - getSecondaryFeaturePoints().getRotation().z = value; - } - highlightCloseFeaturePoints(); - break; - case RegistrationPanel.ACTION_COMMAND_SCALE: - value = ((Number) (((JFormattedTextField) ae.getSource()).getValue())).doubleValue(); - getSecondaryDrawableFace().getScale().x = value; - getSecondaryDrawableFace().getScale().y = value; - getSecondaryDrawableFace().getScale().z = value; - if (getSecondaryFeaturePoints() != null) { - getSecondaryFeaturePoints().getScale().x = value; - getSecondaryFeaturePoints().getScale().y = value; - getSecondaryFeaturePoints().getScale().z = value; - } - highlightCloseFeaturePoints(); - break; - case RegistrationPanel.ACTION_COMMAND_RESET_TRANSLATION: - getSecondaryDrawableFace().setTranslation(new Vector3d(0, 0, 0)); - if (getSecondaryFeaturePoints() != null) { - getSecondaryFeaturePoints().setTranslation(new Vector3d(0, 0, 0)); - } - highlightCloseFeaturePoints(); - break; - case RegistrationPanel.ACTION_COMMAND_RESET_ROTATION: - getSecondaryDrawableFace().setRotation(new Vector3d(0, 0, 0)); - if (getSecondaryFeaturePoints() != null) { - getSecondaryFeaturePoints().setRotation(new Vector3d(0, 0, 0)); - } - highlightCloseFeaturePoints(); - break; - case RegistrationPanel.ACTION_COMMAND_RESET_SCALE: - getSecondaryDrawableFace().setScale(new Vector3d(0, 0, 0)); - if (getSecondaryFeaturePoints() != null) { - getSecondaryFeaturePoints().setScale(new Vector3d(0, 0, 0)); - } - highlightCloseFeaturePoints(); - break; - case RegistrationPanel.ACTION_COMMAND_APPLY_TRANSFORMATIONS: - transformFace(); - getSecondaryDrawableFace().setTranslation(new Vector3d(0, 0, 0)); - getSecondaryDrawableFace().setRotation(new Vector3d(0, 0, 0)); - getSecondaryDrawableFace().setScale(new Vector3d(0, 0, 0)); - if (getSecondaryFeaturePoints() != null) { - getSecondaryFeaturePoints().setTranslation(new Vector3d(0, 0, 0)); - getSecondaryFeaturePoints().setRotation(new Vector3d(0, 0, 0)); - getSecondaryFeaturePoints().setScale(new Vector3d(0, 0, 0)); - } - announceMeshChange(getSecondaryDrawableFace()); + case RegistrationPanel.ACTION_COMMAND_MANUAL_TRANSFORMATION_FINISHED: + getCanvas().getSecondaryFace().announceEvent(new HumanFaceTransformedEvent( + getCanvas().getSecondaryFace(), "", this, true) // finished transformation + ); + //announceMeshChange(getSecondaryDrawableFace()); break; case RegistrationPanel.ACTION_COMMAND_FP_CLOSENESS_THRESHOLD: fpThreshold = ((Number) (((JFormattedTextField) ae.getSource()).getValue())).doubleValue(); @@ -223,27 +140,28 @@ public class RegistrationAction extends ControlPanelAction implements HumanFaceL case RegistrationPanel.ACTION_COMMAND_ICP_ERROR: error = ((Number) (((JFormattedTextField) ae.getSource()).getValue())).doubleValue(); break; - case RegistrationPanel.ACTION_COMMAND_ICP_UNDERSAMPLING: - String item = (String) ((JComboBox) ae.getSource()).getSelectedItem(); - switch (item) { - case "None": - this.undersampling = new NoUndersampling(); - break; - case "Random 200": - this.undersampling = new RandomStrategy(200); - break; - default: - throw new UnsupportedOperationException(item); - } - break; case RegistrationPanel.ACTION_COMMAND_PROCRUSTES_SCALE: this.procrustesScalingEnabled = ((JCheckBox) ae.getSource()).isSelected(); break; case RegistrationPanel.ACTION_COMMAND_PROCRUSTES_APPLY: applyProcrustes(); highlightCloseFeaturePoints(); - announceMeshChange(getPrimaryDrawableFace()); - announceMeshChange(getSecondaryDrawableFace()); + getCanvas().getPrimaryFace().announceEvent(new HumanFaceTransformedEvent( + getCanvas().getPrimaryFace(), "", this) + ); + getCanvas().getSecondaryFace().announceEvent(new HumanFaceTransformedEvent( + getCanvas().getSecondaryFace(), "", this) + ); + //announceMeshChange(getPrimaryDrawableFace()); + //announceMeshChange(getSecondaryDrawableFace()); + break; + case RegistrationPanel.ACTION_COMMAND_ALIGN_SYMMETRY_PLANES: + alignSymmetryPlanes(); + highlightCloseFeaturePoints(); + getCanvas().getSecondaryFace().announceEvent(new HumanFaceTransformedEvent( + getCanvas().getSecondaryFace(), "", this) + ); + //announceMeshChange(getSecondaryDrawableFace()); break; default: // do nothing @@ -305,20 +223,17 @@ public class RegistrationAction extends ControlPanelAction implements HumanFaceL protected void applyICP() { Logger out = Logger.measureTime(); - - IcpTransformer visitor = new IcpTransformer(getPrimaryDrawableFace().getModel(), maxIterations, scale, error, undersampling); - getSecondaryDrawableFace().getModel().compute(visitor); // NOTE: the secondary face is physically transformed - getSecondaryDrawableFace().getHumanFace().removeKdTree(); // invalidate k-d tree, if exists - for (List<IcpTransformation> trList : visitor.getTransformations().values()) { // transform feature points - for (IcpTransformation tr : trList) { - for (int i = 0; i < getSecondaryFeaturePoints().getFeaturePoints().size(); i++) { - FeaturePoint fp = getSecondaryFeaturePoints().getFeaturePoints().get(i); - Point3d trPoint = tr.transformPoint(fp.getPosition(), scale); - getSecondaryFeaturePoints().getFeaturePoints().set(i, new FeaturePoint(trPoint.x, trPoint.y, trPoint.z, fp.getFeaturePointType())); - } - } - } - + + HumanFaceUtils.alignMeshes( + getPrimaryDrawableFace().getHumanFace(), + getSecondaryDrawableFace().getHumanFace(), // is transformed + maxIterations, + scale, + error, + controlPanel.getIcpUndersampling(), + false // drop k-d tree, if exists + ); + out.printDuration("ICP for models with " + getPrimaryDrawableFace().getHumanFace().getMeshModel().getNumVertices() + "/" @@ -346,14 +261,9 @@ public class RegistrationAction extends ControlPanelAction implements HumanFaceL double fpMinDist = Double.POSITIVE_INFINITY; double distSum = 0.0; for (int i = 0; i < getPrimaryFeaturePoints().getFeaturePoints().size(); i++) { - FeaturePoint primary = getPrimaryFeaturePoints().getFeaturePoints().get(i); - FeaturePoint secondary = getSecondaryFeaturePoints().getFeaturePoints().get(i); - Point3d transformed = new Point3d(secondary.getX(), secondary.getY(), secondary.getZ()); - transformPoint(transformed); - double distance = Math.sqrt( - Math.pow(transformed.x - primary.getX(), 2) + - Math.pow(transformed.y - primary.getY(), 2) + - Math.pow(transformed.z - primary.getZ(), 2)); + Vector3d v = new Vector3d(getPrimaryFeaturePoints().getFeaturePoints().get(i).getPosition()); + v.sub(getSecondaryFeaturePoints().getFeaturePoints().get(i).getPosition()); + double distance = v.length(); if (distance > fpThreshold) { getPrimaryFeaturePoints().resetColorToDefault(i); getSecondaryFeaturePoints().resetColorToDefault(i); @@ -369,88 +279,27 @@ public class RegistrationAction extends ControlPanelAction implements HumanFaceL // to do: show ACF dist } - /** - * Applies carried out transformations first on - * {@link #getSecondaryDrawableFace} and then on - * {@link #getSecondaryDrawableFace} feature points - */ - private void transformFace() { - for (MeshFacet transformedFacet : getSecondaryDrawableFace().getFacets()) { - for (MeshPoint comparedPoint : transformedFacet.getVertices()) { - transformPoint(comparedPoint.getPosition()); - } - } - for (int i = 0; i < getSecondaryFeaturePoints().getFeaturePoints().size(); i++) { - FeaturePoint point = getSecondaryFeaturePoints().getFeaturePoints().get(i); - Point3d transformed = new Point3d(point.getX(), point.getY(), point.getZ()); - transformPoint(transformed); - point = new FeaturePoint(transformed.x, transformed.y, transformed.z, point.getFeaturePointType()); - getSecondaryFeaturePoints().getFeaturePoints().set(i, point); - } - getSecondaryDrawableFace().getHumanFace().removeKdTree(); // invalidate k-d tree, if exists - } - - /** - * Transforms point based on transformation info from - * {@link #getSecondaryDrawableFace} - * - * @param point Point to transform - */ - private void transformPoint(Point3d point) { - if (point == null) { - throw new IllegalArgumentException("point is null"); - } - - Point3d newPoint = new Point3d(0, 0, 0); - double quotient; - - // rotate around X - quotient = Math.toRadians(getSecondaryDrawableFace().getRotation().x); - if (!Double.isNaN(quotient)) { - double cos = Math.cos(quotient); - double sin = Math.sin(quotient); - newPoint.y = point.y * cos - point.z * sin; - newPoint.z = point.z * cos + point.y * sin; - point.y = newPoint.y; - point.z = newPoint.z; - } - - // rotate around Y - quotient = Math.toRadians(getSecondaryDrawableFace().getRotation().y); - if (!Double.isNaN(quotient)) { - double cos = Math.cos(quotient); - double sin = Math.sin(quotient); - newPoint.x = point.x * cos + point.z * sin; - newPoint.z = point.z * cos - point.x * sin; - point.x = newPoint.x; - point.z = newPoint.z; - } - - // rotate around Z - quotient = Math.toRadians(getSecondaryDrawableFace().getRotation().z); - if (!Double.isNaN(quotient)) { - double cos = Math.cos(quotient); - double sin = Math.sin(quotient); - newPoint.x = point.x * cos - point.y * sin; - newPoint.y = point.y * cos + point.x * sin; - point.x = newPoint.x; - point.y = newPoint.y; - } + //protected void announceMeshChange(DrawableFace face) { + // if (face != null) { + // face.getHumanFace().announceEvent(new MeshChangedEvent(face.getHumanFace(), face.getHumanFace().getShortName(), this)); + // } + //} - // translate - point.x += getSecondaryDrawableFace().getTranslation().x; - point.y += getSecondaryDrawableFace().getTranslation().y; - point.z += getSecondaryDrawableFace().getTranslation().z; - - // scale - point.x *= 1 + getSecondaryDrawableFace().getScale().x; - point.y *= 1 + getSecondaryDrawableFace().getScale().y; - point.z *= 1 + getSecondaryDrawableFace().getScale().z; - } - - protected void announceMeshChange(DrawableFace face) { - if (face != null) { - face.getHumanFace().announceEvent(new MeshChangedEvent(face.getHumanFace(), face.getHumanFace().getShortName(), this)); + private void alignSymmetryPlanes() { + if (!getPrimaryDrawableFace().getHumanFace().hasSymmetryPlane() + || !getSecondaryDrawableFace().getHumanFace().hasSymmetryPlane()) { + + JOptionPane.showMessageDialog(controlPanel, + "Compute symmetry planes first", + "No symmerty planes", + JOptionPane.INFORMATION_MESSAGE); + return; } + + HumanFaceUtils.alignSymmetryPlanes( + getPrimaryDrawableFace().getHumanFace(), + getSecondaryDrawableFace().getHumanFace(), // is transformed + false // drops k-d tree, if exists + ); } } diff --git a/GUI/src/main/java/cz/fidentis/analyst/registration/RegistrationPanel.form b/GUI/src/main/java/cz/fidentis/analyst/registration/RegistrationPanel.form index 146a2f5a..754e92b2 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/registration/RegistrationPanel.form +++ b/GUI/src/main/java/cz/fidentis/analyst/registration/RegistrationPanel.form @@ -44,15 +44,18 @@ <Group type="102" alignment="0" attributes="0"> <EmptySpace max="-2" attributes="0"/> <Group type="103" groupAlignment="1" max="-2" attributes="0"> - <Component id="jPanel2" alignment="0" max="32767" attributes="0"/> + <Component id="jPanel1" alignment="0" max="32767" attributes="0"/> <Component id="jPanel4" alignment="0" max="32767" attributes="0"/> - <Component id="jPanel7" alignment="0" max="32767" attributes="0"/> <Component id="transformationPanel" alignment="0" min="-2" pref="585" max="-2" attributes="0"/> - <Component id="jPanel1" alignment="0" max="32767" attributes="0"/> + <Component id="jPanel7" max="32767" attributes="0"/> </Group> - <EmptySpace min="-2" pref="153" max="-2" attributes="0"/> + <EmptySpace max="32767" attributes="0"/> <Component id="jSeparator7" min="-2" pref="50" max="-2" attributes="0"/> - <EmptySpace min="0" pref="0" max="32767" attributes="0"/> + </Group> + <Group type="102" alignment="0" attributes="0"> + <EmptySpace max="-2" attributes="0"/> + <Component id="jPanel2" min="-2" max="-2" attributes="0"/> + <EmptySpace max="32767" attributes="0"/> </Group> </Group> </DimensionLayout> @@ -72,12 +75,12 @@ <EmptySpace max="-2" attributes="0"/> <Component id="jPanel1" min="-2" max="-2" attributes="0"/> <EmptySpace max="-2" attributes="0"/> - <Component id="transformationPanel" min="-2" pref="209" max="-2" attributes="0"/> + <Component id="transformationPanel" min="-2" pref="176" max="-2" attributes="0"/> <EmptySpace max="-2" attributes="0"/> <Component id="jPanel4" min="-2" max="-2" attributes="0"/> <EmptySpace max="-2" attributes="0"/> <Component id="jPanel2" min="-2" max="-2" attributes="0"/> - <EmptySpace min="-2" pref="232" max="-2" attributes="0"/> + <EmptySpace min="-2" pref="328" max="-2" attributes="0"/> <Component id="jSeparator11" min="-2" pref="10" max="-2" attributes="0"/> <EmptySpace min="-2" pref="221" max="-2" attributes="0"/> <Component id="jSeparator2" min="-2" pref="10" max="-2" attributes="0"/> @@ -93,7 +96,7 @@ <Properties> <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> <Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo"> - <TitledBorder title="Fine-tuning the mutual position:"> + <TitledBorder title="Manual alignment:"> <ResourceString PropertyName="titleX" bundle="cz/fidentis/analyst/registration/Bundle.properties" key="RegistrationPanel.transformationPanel.border.title" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> <Font PropertyName="font" name="Dialog" size="14" style="1"/> </TitledBorder> @@ -117,36 +120,16 @@ <EmptySpace min="-2" pref="580" max="-2" attributes="0"/> <Component id="shiftPanel" min="-2" max="-2" attributes="0"/> </Group> - <Group type="102" attributes="0"> - <EmptySpace min="-2" pref="349" max="-2" attributes="0"/> - <Component id="jSeparator10" min="-2" pref="15" max="-2" attributes="0"/> + <Group type="102" alignment="0" attributes="0"> + <EmptySpace max="-2" attributes="0"/> + <Component id="jButton3" min="-2" max="-2" attributes="0"/> </Group> <Group type="102" alignment="0" attributes="0"> - <Group type="103" groupAlignment="0" attributes="0"> - <Group type="102" attributes="0"> - <EmptySpace min="-2" pref="24" max="-2" attributes="0"/> - <Component id="shiftLabel" min="-2" max="-2" attributes="0"/> - <EmptySpace max="-2" attributes="0"/> - <Component id="lowShiftRB" min="-2" max="-2" attributes="0"/> - <EmptySpace max="-2" attributes="0"/> - <Component id="highShiftRB" min="-2" max="-2" attributes="0"/> - </Group> - <Component id="translationPanel" alignment="0" min="-2" pref="227" max="-2" attributes="0"/> - </Group> - <Group type="103" groupAlignment="0" attributes="0"> - <Group type="102" alignment="0" attributes="0"> - <EmptySpace min="-2" pref="24" max="-2" attributes="0"/> - <Component id="resetAllButton" min="-2" pref="128" max="-2" attributes="0"/> - <EmptySpace type="unrelated" max="-2" attributes="0"/> - <Component id="applyButton" min="-2" pref="128" max="-2" attributes="0"/> - </Group> - <Group type="102" alignment="0" attributes="0"> - <EmptySpace max="-2" attributes="0"/> - <Component id="rotationPanel" min="-2" pref="224" max="-2" attributes="0"/> - <EmptySpace max="-2" attributes="0"/> - <Component id="scalePanel" min="-2" pref="102" max="-2" attributes="0"/> - </Group> - </Group> + <Component id="translationPanel" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="rotationPanel" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="scalePanel" min="-2" max="-2" attributes="0"/> </Group> </Group> <EmptySpace min="0" pref="0" max="32767" attributes="0"/> @@ -157,24 +140,14 @@ <Group type="103" groupAlignment="0" attributes="0"> <Group type="102" attributes="0"> <EmptySpace max="-2" attributes="0"/> - <Group type="103" groupAlignment="0" attributes="0"> + <Group type="103" groupAlignment="0" max="-2" attributes="0"> <Component id="scalePanel" max="32767" attributes="0"/> - <Component id="rotationPanel" max="32767" attributes="0"/> - <Component id="translationPanel" max="32767" attributes="0"/> + <Component id="rotationPanel" alignment="0" max="32767" attributes="0"/> + <Component id="translationPanel" alignment="0" max="32767" attributes="0"/> </Group> - <EmptySpace max="-2" attributes="0"/> - <Group type="103" groupAlignment="0" attributes="0"> - <Component id="applyButton" alignment="0" min="-2" pref="26" max="-2" attributes="0"/> - <Group type="103" groupAlignment="3" attributes="0"> - <Component id="shiftLabel" alignment="3" min="-2" max="-2" attributes="0"/> - <Component id="lowShiftRB" alignment="3" min="-2" max="-2" attributes="0"/> - <Component id="highShiftRB" alignment="3" min="-2" max="-2" attributes="0"/> - <Component id="resetAllButton" alignment="3" min="-2" max="-2" attributes="0"/> - </Group> - </Group> - <EmptySpace min="-2" pref="39" max="-2" attributes="0"/> - <Component id="jSeparator10" min="-2" pref="62" max="-2" attributes="0"/> - <EmptySpace min="-2" pref="25" max="-2" attributes="0"/> + <EmptySpace max="32767" attributes="0"/> + <Component id="jButton3" min="-2" max="-2" attributes="0"/> + <EmptySpace min="-2" pref="59" max="-2" attributes="0"/> <Component id="shiftPanel" min="-2" max="-2" attributes="0"/> <EmptySpace max="32767" attributes="0"/> <Component id="jSeparator5" min="-2" pref="10" max="-2" attributes="0"/> @@ -198,36 +171,21 @@ <DimensionLayout dim="0"> <Group type="103" groupAlignment="0" attributes="0"> <Group type="102" alignment="0" attributes="0"> - <Group type="103" groupAlignment="1" max="-2" attributes="0"> - <Group type="102" alignment="1" attributes="0"> - <EmptySpace min="-2" pref="31" max="-2" attributes="0"/> - <Component id="translXLabel" min="-2" max="-2" attributes="0"/> - </Group> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="1" attributes="0"> + <Component id="translXLabel" alignment="1" min="-2" max="-2" attributes="0"/> <Group type="102" alignment="1" attributes="0"> - <EmptySpace min="-2" pref="2" max="-2" attributes="0"/> - <Component id="translationButton" min="-2" max="-2" attributes="0"/> - <EmptySpace max="32767" attributes="0"/> - <Group type="103" groupAlignment="0" attributes="0"> - <Component id="translationXFTF" alignment="0" min="-2" pref="54" max="-2" attributes="0"/> - <Group type="102" alignment="0" attributes="0"> - <Component id="leftTranslationXButton" min="-2" max="-2" attributes="0"/> - <EmptySpace max="-2" attributes="0"/> - <Component id="rightTranslationXButton" min="-2" max="-2" attributes="0"/> - </Group> - </Group> + <Component id="leftTranslationXButton" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="rightTranslationXButton" min="-2" max="-2" attributes="0"/> </Group> </Group> <Group type="103" groupAlignment="0" attributes="0"> - <Group type="102" attributes="0"> + <Group type="102" alignment="0" attributes="0"> <EmptySpace min="-2" pref="7" max="-2" attributes="0"/> - <Group type="103" groupAlignment="0" attributes="0"> - <Group type="102" alignment="0" attributes="0"> - <Component id="leftTranslationYButton" min="-2" max="-2" attributes="0"/> - <EmptySpace max="-2" attributes="0"/> - <Component id="rightTranslationYButton" min="-2" max="-2" attributes="0"/> - </Group> - <Component id="translationYFTF" min="-2" pref="54" max="-2" attributes="0"/> - </Group> + <Component id="leftTranslationYButton" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="rightTranslationYButton" min="-2" max="-2" attributes="0"/> </Group> <Group type="102" alignment="0" attributes="0"> <EmptySpace min="-2" pref="15" max="-2" attributes="0"/> @@ -238,14 +196,9 @@ <Group type="103" groupAlignment="0" attributes="0"> <Group type="102" alignment="0" attributes="0"> <EmptySpace min="-2" pref="1" max="-2" attributes="0"/> - <Group type="103" groupAlignment="0" attributes="0"> - <Group type="102" alignment="0" attributes="0"> - <Component id="leftTranslationZButton" min="-2" max="-2" attributes="0"/> - <EmptySpace max="-2" attributes="0"/> - <Component id="rightTranslationZButton" min="-2" max="-2" attributes="0"/> - </Group> - <Component id="translationZFTF" min="-2" pref="54" max="-2" attributes="0"/> - </Group> + <Component id="leftTranslationZButton" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="rightTranslationZButton" min="-2" max="-2" attributes="0"/> <EmptySpace min="-2" pref="2" max="-2" attributes="0"/> </Group> <Component id="translZLabel" alignment="1" min="-2" max="-2" attributes="0"/> @@ -265,75 +218,21 @@ </Group> <EmptySpace min="-2" pref="9" max="-2" attributes="0"/> <Group type="103" groupAlignment="0" attributes="0"> - <Group type="102" alignment="0" attributes="0"> - <Group type="103" groupAlignment="1" attributes="0"> - <Component id="rightTranslationXButton" alignment="0" min="-2" max="-2" attributes="0"/> - <Component id="leftTranslationXButton" alignment="0" min="-2" max="-2" attributes="0"/> - </Group> - <EmptySpace max="-2" attributes="0"/> - <Group type="103" groupAlignment="0" attributes="0"> - <Component id="translationXFTF" max="-2" attributes="0"/> - <Group type="102" alignment="1" attributes="0"> - <Component id="translationButton" min="-2" max="-2" attributes="0"/> - <EmptySpace min="-2" pref="6" max="-2" attributes="0"/> - </Group> - </Group> - </Group> - <Group type="102" alignment="0" attributes="0"> - <Group type="103" groupAlignment="1" attributes="0"> - <Component id="rightTranslationYButton" alignment="0" min="-2" max="-2" attributes="0"/> - <Component id="leftTranslationYButton" alignment="0" min="-2" max="-2" attributes="0"/> - </Group> - <EmptySpace max="-2" attributes="0"/> - <Component id="translationYFTF" min="-2" max="-2" attributes="0"/> - </Group> - <Group type="102" alignment="0" attributes="0"> - <Group type="103" groupAlignment="1" attributes="0"> - <Group type="102" alignment="1" attributes="0"> - <Component id="rightTranslationZButton" min="-2" max="-2" attributes="0"/> - <EmptySpace min="-2" pref="6" max="-2" attributes="0"/> - </Group> - <Group type="102" alignment="0" attributes="0"> - <Component id="leftTranslationZButton" min="-2" max="-2" attributes="0"/> - <EmptySpace max="-2" attributes="0"/> - </Group> - </Group> - <Component id="translationZFTF" min="-2" max="-2" attributes="0"/> + <Group type="103" alignment="0" groupAlignment="1" attributes="0"> + <Component id="rightTranslationZButton" alignment="1" min="-2" max="-2" attributes="0"/> + <Component id="leftTranslationZButton" alignment="0" min="-2" max="-2" attributes="0"/> </Group> + <Component id="rightTranslationXButton" alignment="0" min="-2" max="-2" attributes="0"/> + <Component id="leftTranslationXButton" alignment="0" min="-2" max="-2" attributes="0"/> + <Component id="rightTranslationYButton" alignment="0" min="-2" max="-2" attributes="0"/> + <Component id="leftTranslationYButton" alignment="0" min="-2" max="-2" attributes="0"/> </Group> - <EmptySpace min="10" pref="10" max="-2" attributes="0"/> + <EmptySpace max="32767" attributes="0"/> </Group> </Group> </DimensionLayout> </Layout> <SubComponents> - <Component class="javax.swing.JButton" name="translationButton"> - <Properties> - <Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor"> - <Image iconType="3" name="/restart-line.png"/> - </Property> - <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> - <ResourceString bundle="cz/fidentis/analyst/registration/Bundle.properties" key="RegistrationPanel.translationButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> - </Property> - <Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> - <ResourceString bundle="cz/fidentis/analyst/registration/Bundle.properties" key="RegistrationPanel.translationButton.toolTipText" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> - </Property> - <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> - <Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo"> - <TitledBorder/> - </Border> - </Property> - <Property name="cursor" type="java.awt.Cursor" editor="org.netbeans.modules.form.editors2.CursorEditor"> - <Color id="Default Cursor"/> - </Property> - <Property name="margin" type="java.awt.Insets" editor="org.netbeans.beaninfo.editors.InsetsEditor"> - <Insets value="[2, 2, 2, 2]"/> - </Property> - </Properties> - <Events> - <EventHandler event="mousePressed" listener="java.awt.event.MouseListener" parameters="java.awt.event.MouseEvent" handler="translationButtonMousePressed"/> - </Events> - </Component> <Component class="javax.swing.JButton" name="rightTranslationXButton"> <Properties> <Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor"> @@ -368,19 +267,6 @@ <EventHandler event="mouseReleased" listener="java.awt.event.MouseListener" parameters="java.awt.event.MouseEvent" handler="rightTranslationXButtonMouseReleased"/> </Events> </Component> - <Component class="javax.swing.JFormattedTextField" name="translationXFTF"> - <Properties> - <Property name="formatterFactory" type="javax.swing.JFormattedTextField$AbstractFormatterFactory" editor="org.netbeans.modules.form.editors.AbstractFormatterFactoryEditor"> - <Format subtype="0" type="0"/> - </Property> - <Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> - <ResourceString bundle="cz/fidentis/analyst/registration/Bundle.properties" key="RegistrationPanel.translationXFTF.toolTipText" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> - </Property> - <Property name="cursor" type="java.awt.Cursor" editor="org.netbeans.modules.form.editors2.CursorEditor"> - <Color id="Text Cursor"/> - </Property> - </Properties> - </Component> <Component class="javax.swing.JButton" name="leftTranslationYButton"> <Properties> <Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor"> @@ -409,13 +295,6 @@ <EventHandler event="mouseReleased" listener="java.awt.event.MouseListener" parameters="java.awt.event.MouseEvent" handler="leftTranslationYButtonMouseReleased"/> </Events> </Component> - <Component class="javax.swing.JFormattedTextField" name="translationYFTF"> - <Properties> - <Property name="formatterFactory" type="javax.swing.JFormattedTextField$AbstractFormatterFactory" editor="org.netbeans.modules.form.editors.AbstractFormatterFactoryEditor"> - <Format subtype="0" type="0"/> - </Property> - </Properties> - </Component> <Component class="javax.swing.JButton" name="rightTranslationYButton"> <Properties> <Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor"> @@ -454,13 +333,6 @@ </Property> </Properties> </Component> - <Component class="javax.swing.JFormattedTextField" name="translationZFTF"> - <Properties> - <Property name="formatterFactory" type="javax.swing.JFormattedTextField$AbstractFormatterFactory" editor="org.netbeans.modules.form.editors.AbstractFormatterFactoryEditor"> - <Format subtype="0" type="0"/> - </Property> - </Properties> - </Component> <Component class="javax.swing.JLabel" name="translZLabel"> <Properties> <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> @@ -588,36 +460,24 @@ <DimensionLayout dim="0"> <Group type="103" groupAlignment="0" attributes="0"> <Group type="102" alignment="0" attributes="0"> - <EmptySpace min="-2" pref="2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> <Group type="103" groupAlignment="0" attributes="0"> <Group type="102" alignment="0" attributes="0"> - <Component id="rotationButton" min="-2" max="-2" attributes="0"/> + <Component id="leftRotationXButton" min="-2" max="-2" attributes="0"/> <EmptySpace max="-2" attributes="0"/> - <Group type="103" groupAlignment="0" attributes="0"> - <Component id="rotationXFTF" min="-2" pref="54" max="-2" attributes="0"/> - <Group type="102" alignment="0" attributes="0"> - <Component id="leftRotationXButton" min="-2" max="-2" attributes="0"/> - <EmptySpace max="-2" attributes="0"/> - <Component id="rightRotationXButton" min="-2" max="-2" attributes="0"/> - </Group> - </Group> + <Component id="rightRotationXButton" min="-2" max="-2" attributes="0"/> </Group> <Group type="102" alignment="0" attributes="0"> - <EmptySpace min="-2" pref="55" max="-2" attributes="0"/> + <EmptySpace min="-2" pref="23" max="-2" attributes="0"/> <Component id="rotatXLabel" min="-2" pref="20" max="-2" attributes="0"/> </Group> </Group> <Group type="103" groupAlignment="0" attributes="0"> - <Group type="102" attributes="0"> + <Group type="102" alignment="0" attributes="0"> <EmptySpace min="-2" pref="6" max="-2" attributes="0"/> - <Group type="103" groupAlignment="0" attributes="0"> - <Group type="102" attributes="0"> - <Component id="leftRotationYButton" min="-2" max="-2" attributes="0"/> - <EmptySpace max="-2" attributes="0"/> - <Component id="rightRotationYButton" min="-2" max="-2" attributes="0"/> - </Group> - <Component id="rotationYFTF" min="-2" pref="54" max="-2" attributes="0"/> - </Group> + <Component id="leftRotationYButton" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="rightRotationYButton" min="-2" max="-2" attributes="0"/> </Group> <Group type="102" alignment="0" attributes="0"> <EmptySpace min="-2" pref="29" max="-2" attributes="0"/> @@ -631,14 +491,9 @@ </Group> <Group type="102" alignment="0" attributes="0"> <EmptySpace min="-2" pref="6" max="-2" attributes="0"/> - <Group type="103" groupAlignment="0" max="-2" attributes="0"> - <Component id="rotationZFTF" min="-2" pref="54" max="-2" attributes="0"/> - <Group type="102" attributes="0"> - <Component id="leftRotationZButton" min="-2" max="-2" attributes="0"/> - <EmptySpace max="-2" attributes="0"/> - <Component id="rightRotationZButton" max="32767" attributes="0"/> - </Group> - </Group> + <Component id="leftRotationZButton" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="rightRotationZButton" min="-2" max="-2" attributes="0"/> </Group> </Group> <EmptySpace max="32767" attributes="0"/> @@ -661,31 +516,12 @@ </Group> <EmptySpace max="-2" attributes="0"/> <Group type="103" groupAlignment="0" attributes="0"> - <Group type="102" attributes="0"> - <Group type="103" groupAlignment="0" attributes="0"> - <Component id="leftRotationXButton" min="-2" max="-2" attributes="0"/> - <Component id="rightRotationXButton" min="-2" max="-2" attributes="0"/> - <Component id="leftRotationYButton" min="-2" max="-2" attributes="0"/> - <Component id="rightRotationYButton" min="-2" max="-2" attributes="0"/> - <Component id="leftRotationZButton" min="-2" max="-2" attributes="0"/> - </Group> - <EmptySpace max="-2" attributes="0"/> - <Group type="103" groupAlignment="0" attributes="0"> - <Group type="103" groupAlignment="3" attributes="0"> - <Component id="rotationXFTF" alignment="3" min="-2" max="-2" attributes="0"/> - <Component id="rotationYFTF" alignment="3" min="-2" max="-2" attributes="0"/> - <Component id="rotationZFTF" alignment="3" min="-2" max="-2" attributes="0"/> - </Group> - <Group type="102" alignment="1" attributes="0"> - <Component id="rotationButton" max="-2" attributes="0"/> - <EmptySpace min="-2" pref="5" max="-2" attributes="0"/> - </Group> - </Group> - </Group> - <Group type="102" attributes="0"> - <Component id="rightRotationZButton" min="-2" pref="24" max="-2" attributes="0"/> - <EmptySpace min="-2" pref="41" max="-2" attributes="0"/> - </Group> + <Component id="rightRotationZButton" min="-2" pref="24" max="-2" attributes="0"/> + <Component id="leftRotationXButton" min="-2" max="-2" attributes="0"/> + <Component id="rightRotationXButton" min="-2" max="-2" attributes="0"/> + <Component id="leftRotationYButton" min="-2" max="-2" attributes="0"/> + <Component id="rightRotationYButton" min="-2" max="-2" attributes="0"/> + <Component id="leftRotationZButton" min="-2" max="-2" attributes="0"/> </Group> <EmptySpace max="32767" attributes="0"/> </Group> @@ -785,47 +621,6 @@ </Property> </Properties> </Component> - <Component class="javax.swing.JButton" name="rotationButton"> - <Properties> - <Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor"> - <Image iconType="3" name="/restart-line.png"/> - </Property> - <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> - <ResourceString bundle="cz/fidentis/analyst/registration/Bundle.properties" key="RegistrationPanel.rotationButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> - </Property> - <Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> - <ResourceString bundle="cz/fidentis/analyst/registration/Bundle.properties" key="RegistrationPanel.rotationButton.toolTipText" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> - </Property> - <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> - <Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo"> - <TitledBorder/> - </Border> - </Property> - <Property name="cursor" type="java.awt.Cursor" editor="org.netbeans.modules.form.editors2.CursorEditor"> - <Color id="Default Cursor"/> - </Property> - <Property name="margin" type="java.awt.Insets" editor="org.netbeans.beaninfo.editors.InsetsEditor"> - <Insets value="[2, 2, 2, 2]"/> - </Property> - </Properties> - <Events> - <EventHandler event="mousePressed" listener="java.awt.event.MouseListener" parameters="java.awt.event.MouseEvent" handler="rotationButtonMousePressed"/> - </Events> - </Component> - <Component class="javax.swing.JFormattedTextField" name="rotationZFTF"> - <Properties> - <Property name="formatterFactory" type="javax.swing.JFormattedTextField$AbstractFormatterFactory" editor="org.netbeans.modules.form.editors.AbstractFormatterFactoryEditor"> - <Format subtype="0" type="0"/> - </Property> - </Properties> - </Component> - <Component class="javax.swing.JFormattedTextField" name="rotationYFTF"> - <Properties> - <Property name="formatterFactory" type="javax.swing.JFormattedTextField$AbstractFormatterFactory" editor="org.netbeans.modules.form.editors.AbstractFormatterFactoryEditor"> - <Format subtype="0" type="0"/> - </Property> - </Properties> - </Component> <Component class="javax.swing.JButton" name="rightRotationZButton"> <Properties> <Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor"> @@ -962,16 +757,6 @@ <EventHandler event="mouseReleased" listener="java.awt.event.MouseListener" parameters="java.awt.event.MouseEvent" handler="rightRotationXButtonMouseReleased"/> </Events> </Component> - <Component class="javax.swing.JFormattedTextField" name="rotationXFTF"> - <Properties> - <Property name="formatterFactory" type="javax.swing.JFormattedTextField$AbstractFormatterFactory" editor="org.netbeans.modules.form.editors.AbstractFormatterFactoryEditor"> - <Format subtype="0" type="0"/> - </Property> - <Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> - <ResourceString bundle="cz/fidentis/analyst/registration/Bundle.properties" key="RegistrationPanel.rotationXFTF.toolTipText" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> - </Property> - </Properties> - </Component> </SubComponents> </Container> <Container class="javax.swing.JPanel" name="scalePanel"> @@ -989,38 +774,23 @@ <DimensionLayout dim="0"> <Group type="103" groupAlignment="0" attributes="0"> <Group type="102" alignment="0" attributes="0"> - <EmptySpace min="-2" pref="32" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> <Component id="scalePlusButton" min="-2" max="-2" attributes="0"/> <EmptySpace max="-2" attributes="0"/> <Component id="scaleMinusButton" min="-2" max="-2" attributes="0"/> - <EmptySpace min="0" pref="0" max="32767" attributes="0"/> - </Group> - <Group type="102" alignment="1" attributes="0"> - <EmptySpace min="-2" pref="2" max="-2" attributes="0"/> - <Component id="scaleButton" min="-2" max="-2" attributes="0"/> - <EmptySpace min="-2" pref="7" max="-2" attributes="0"/> - <Component id="scaleFTF" min="-2" pref="54" max="-2" attributes="0"/> - <EmptySpace max="32767" attributes="0"/> + <EmptySpace pref="15" max="32767" attributes="0"/> </Group> </Group> </DimensionLayout> <DimensionLayout dim="1"> <Group type="103" groupAlignment="0" attributes="0"> - <Group type="102" alignment="1" attributes="0"> - <EmptySpace max="32767" attributes="0"/> + <Group type="102" alignment="0" attributes="0"> + <EmptySpace min="-2" pref="26" max="-2" attributes="0"/> <Group type="103" groupAlignment="0" attributes="0"> <Component id="scalePlusButton" min="-2" max="-2" attributes="0"/> <Component id="scaleMinusButton" min="-2" max="-2" attributes="0"/> </Group> - <EmptySpace max="-2" attributes="0"/> - <Group type="103" groupAlignment="0" attributes="0"> - <Component id="scaleFTF" min="-2" max="-2" attributes="0"/> - <Group type="102" alignment="1" attributes="0"> - <Component id="scaleButton" max="-2" attributes="0"/> - <EmptySpace min="-2" pref="5" max="-2" attributes="0"/> - </Group> - </Group> - <EmptySpace max="-2" attributes="0"/> + <EmptySpace max="32767" attributes="0"/> </Group> </Group> </DimensionLayout> @@ -1060,46 +830,6 @@ <EventHandler event="mouseReleased" listener="java.awt.event.MouseListener" parameters="java.awt.event.MouseEvent" handler="scalePlusButtonMouseReleased"/> </Events> </Component> - <Component class="javax.swing.JFormattedTextField" name="scaleFTF"> - <Properties> - <Property name="formatterFactory" type="javax.swing.JFormattedTextField$AbstractFormatterFactory" editor="org.netbeans.modules.form.editors.AbstractFormatterFactoryEditor"> - <Format subtype="0" type="0"/> - </Property> - <Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> - <ResourceString bundle="cz/fidentis/analyst/registration/Bundle.properties" key="RegistrationPanel.scaleFTF.toolTipText" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> - </Property> - </Properties> - <Events> - <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="scaleFTFActionPerformed"/> - </Events> - </Component> - <Component class="javax.swing.JButton" name="scaleButton"> - <Properties> - <Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor"> - <Image iconType="3" name="/restart-line.png"/> - </Property> - <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> - <ResourceString bundle="cz/fidentis/analyst/registration/Bundle.properties" key="RegistrationPanel.scaleButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> - </Property> - <Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> - <ResourceString bundle="cz/fidentis/analyst/registration/Bundle.properties" key="RegistrationPanel.scaleButton.toolTipText" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> - </Property> - <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> - <Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo"> - <TitledBorder/> - </Border> - </Property> - <Property name="cursor" type="java.awt.Cursor" editor="org.netbeans.modules.form.editors2.CursorEditor"> - <Color id="Default Cursor"/> - </Property> - <Property name="margin" type="java.awt.Insets" editor="org.netbeans.beaninfo.editors.InsetsEditor"> - <Insets value="[2, 2, 2, 2]"/> - </Property> - </Properties> - <Events> - <EventHandler event="mousePressed" listener="java.awt.event.MouseListener" parameters="java.awt.event.MouseEvent" handler="scaleButtonMousePressed"/> - </Events> - </Component> <Component class="javax.swing.JButton" name="scaleMinusButton"> <Properties> <Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor"> @@ -1136,28 +866,6 @@ </Component> </SubComponents> </Container> - <Component class="javax.swing.JButton" name="resetAllButton"> - <Properties> - <Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor"> - <Image iconType="3" name="/refresh-line.png"/> - </Property> - <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> - <ResourceString bundle="cz/fidentis/analyst/registration/Bundle.properties" key="RegistrationPanel.resetAllButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> - </Property> - <Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> - <ResourceString bundle="cz/fidentis/analyst/registration/Bundle.properties" key="RegistrationPanel.resetAllButton.toolTipText" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> - </Property> - <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> - <Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo"> - <TitledBorder/> - </Border> - </Property> - <Property name="focusPainted" type="boolean" value="false"/> - </Properties> - <Events> - <EventHandler event="mousePressed" listener="java.awt.event.MouseListener" parameters="java.awt.event.MouseEvent" handler="resetAllButtonMousePressed"/> - </Events> - </Component> <Container class="javax.swing.JPanel" name="shiftPanel"> <Layout> @@ -1173,79 +881,14 @@ </DimensionLayout> </Layout> </Container> - <Component class="javax.swing.JSeparator" name="jSeparator10"> - <Properties> - <Property name="orientation" type="int" value="1"/> - </Properties> - </Component> - <Component class="javax.swing.JButton" name="applyButton"> - <Properties> - <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> - <ResourceString bundle="cz/fidentis/analyst/registration/Bundle.properties" key="RegistrationPanel.applyButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> - </Property> - <Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> - <ResourceString bundle="cz/fidentis/analyst/registration/Bundle.properties" key="RegistrationPanel.applyButton.toolTipText" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> - </Property> - <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> - <Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo"> - <TitledBorder/> - </Border> - </Property> - <Property name="cursor" type="java.awt.Cursor" editor="org.netbeans.modules.form.editors2.CursorEditor"> - <Color id="Hand Cursor"/> - </Property> - <Property name="focusPainted" type="boolean" value="false"/> - </Properties> - <Events> - <EventHandler event="mousePressed" listener="java.awt.event.MouseListener" parameters="java.awt.event.MouseEvent" handler="applyButtonMousePressed"/> - </Events> - </Component> <Component class="javax.swing.JSeparator" name="jSeparator5"> </Component> - <Component class="javax.swing.JLabel" name="shiftLabel"> - <Properties> - <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> - <Font name="Ubuntu" size="15" style="1"/> - </Property> - <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> - <ResourceString bundle="cz/fidentis/analyst/registration/Bundle.properties" key="RegistrationPanel.shiftLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> - </Property> - <Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> - <ResourceString bundle="cz/fidentis/analyst/registration/Bundle.properties" key="RegistrationPanel.shiftLabel.toolTipText" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> - </Property> - </Properties> - </Component> - <Component class="javax.swing.JRadioButton" name="lowShiftRB"> - <Properties> - <Property name="buttonGroup" type="javax.swing.ButtonGroup" editor="org.netbeans.modules.form.RADComponent$ButtonGroupPropertyEditor"> - <ComponentRef name="precisionGroup"/> - </Property> - <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> - <ResourceString bundle="cz/fidentis/analyst/registration/Bundle.properties" key="RegistrationPanel.lowShiftRB.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> - </Property> - <Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> - <ResourceString bundle="cz/fidentis/analyst/registration/Bundle.properties" key="RegistrationPanel.lowShiftRB.toolTipText" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> - </Property> - </Properties> - <Events> - <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="lowShiftRBActionPerformed"/> - </Events> - </Component> - <Component class="javax.swing.JRadioButton" name="highShiftRB"> + <Component class="javax.swing.JButton" name="jButton3"> <Properties> - <Property name="buttonGroup" type="javax.swing.ButtonGroup" editor="org.netbeans.modules.form.RADComponent$ButtonGroupPropertyEditor"> - <ComponentRef name="precisionGroup"/> - </Property> <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> - <ResourceString bundle="cz/fidentis/analyst/registration/Bundle.properties" key="RegistrationPanel.highShiftRB.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> - </Property> - <Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> - <ResourceString bundle="cz/fidentis/analyst/registration/Bundle.properties" key="RegistrationPanel.highShiftRB.toolTipText" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + <ResourceString bundle="cz/fidentis/analyst/registration/Bundle.properties" key="RegistrationPanel.jButton3.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> </Property> </Properties> - <Events> - <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="highShiftRBActionPerformed"/> - </Events> </Component> </SubComponents> </Container> @@ -1271,27 +914,31 @@ <Group type="103" groupAlignment="0" attributes="0"> <Group type="102" attributes="0"> <EmptySpace max="-2" attributes="0"/> - <Group type="103" groupAlignment="0" max="-2" attributes="0"> - <Group type="102" attributes="0"> - <Component id="jLabel8" min="-2" max="-2" attributes="0"/> - <EmptySpace max="-2" attributes="0"/> - <Component id="jComboBox1" min="-2" pref="166" max="-2" attributes="0"/> - <EmptySpace max="32767" attributes="0"/> - <Component id="jButton1" min="-2" max="-2" attributes="0"/> - </Group> + <Group type="103" groupAlignment="0" attributes="0"> <Group type="102" attributes="0"> - <Component id="jLabel7" min="-2" max="-2" attributes="0"/> - <EmptySpace max="-2" attributes="0"/> - <Component id="jCheckBox1" min="-2" max="-2" attributes="0"/> - <EmptySpace type="separate" max="-2" attributes="0"/> - <Component id="jLabel5" min="-2" max="-2" attributes="0"/> - <EmptySpace max="-2" attributes="0"/> - <Component id="jFormattedTextField1" min="-2" pref="49" max="-2" attributes="0"/> - <EmptySpace type="separate" max="-2" attributes="0"/> - <Component id="jLabel6" min="-2" max="-2" attributes="0"/> - <EmptySpace max="-2" attributes="0"/> - <Component id="jFormattedTextField2" min="-2" pref="53" max="-2" attributes="0"/> + <Group type="103" groupAlignment="0" attributes="0"> + <Group type="102" alignment="0" attributes="0"> + <Component id="jLabel7" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="jCheckBox1" min="-2" max="-2" attributes="0"/> + <EmptySpace type="separate" max="-2" attributes="0"/> + <Component id="jLabel5" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="jFormattedTextField1" min="-2" pref="49" max="-2" attributes="0"/> + </Group> + <Component id="comboSliderInteger1" alignment="0" min="-2" max="-2" attributes="0"/> + </Group> + <EmptySpace min="-2" pref="12" max="-2" attributes="0"/> + <Group type="103" groupAlignment="0" attributes="0"> + <Group type="102" attributes="0"> + <Component id="jLabel6" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="jFormattedTextField2" min="-2" pref="53" max="-2" attributes="0"/> + </Group> + <Component id="jLabel8" min="-2" max="-2" attributes="0"/> + </Group> </Group> + <Component id="jButton1" alignment="0" min="-2" max="-2" attributes="0"/> </Group> <EmptySpace max="32767" attributes="0"/> </Group> @@ -1311,14 +958,18 @@ <Component id="jFormattedTextField2" alignment="3" min="-2" pref="20" max="-2" attributes="0"/> </Group> </Group> - <EmptySpace type="separate" max="-2" attributes="0"/> <Group type="103" groupAlignment="0" attributes="0"> - <Group type="103" groupAlignment="3" attributes="0"> - <Component id="jLabel8" alignment="3" min="-2" max="-2" attributes="0"/> - <Component id="jComboBox1" alignment="3" min="-2" pref="27" max="-2" attributes="0"/> + <Group type="102" attributes="0"> + <EmptySpace min="-2" pref="5" max="-2" attributes="0"/> + <Component id="comboSliderInteger1" min="-2" max="-2" attributes="0"/> + </Group> + <Group type="102" alignment="0" attributes="0"> + <EmptySpace min="-2" pref="26" max="-2" attributes="0"/> + <Component id="jLabel8" min="-2" max="-2" attributes="0"/> </Group> - <Component id="jButton1" alignment="0" min="-2" max="-2" attributes="0"/> </Group> + <EmptySpace max="-2" attributes="0"/> + <Component id="jButton1" min="-2" max="-2" attributes="0"/> <EmptySpace max="32767" attributes="0"/> </Group> </Group> @@ -1384,19 +1035,6 @@ </Property> </Properties> </Component> - <Component class="javax.swing.JComboBox" name="jComboBox1"> - <Properties> - <Property name="model" type="javax.swing.ComboBoxModel" editor="org.netbeans.modules.form.editors2.ComboBoxModelEditor"> - <StringArray count="2"> - <StringItem index="0" value="None"/> - <StringItem index="1" value="Random 200"/> - </StringArray> - </Property> - </Properties> - <AuxValues> - <AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="<String>"/> - </AuxValues> - </Component> <Component class="javax.swing.JButton" name="jButton1"> <Properties> <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> @@ -1413,6 +1051,8 @@ <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="jButton1ActionPerformed"/> </Events> </Component> + <Component class="cz.fidentis.analyst.core.ComboSliderInteger" name="comboSliderInteger1"> + </Component> </SubComponents> </Container> <Container class="javax.swing.JPanel" name="jPanel4"> @@ -1563,12 +1203,12 @@ <Group type="103" groupAlignment="0" attributes="0"> <Group type="102" alignment="0" attributes="0"> <EmptySpace max="-2" attributes="0"/> + <Component id="jButton2" min="-2" max="-2" attributes="0"/> + <EmptySpace type="separate" max="-2" attributes="0"/> <Component id="jLabel4" min="-2" max="-2" attributes="0"/> <EmptySpace max="-2" attributes="0"/> <Component id="jCheckBox2" min="-2" max="-2" attributes="0"/> <EmptySpace max="32767" attributes="0"/> - <Component id="jButton2" min="-2" max="-2" attributes="0"/> - <EmptySpace min="-2" pref="190" max="-2" attributes="0"/> </Group> </Group> </DimensionLayout> @@ -1577,9 +1217,11 @@ <Group type="102" alignment="0" attributes="0"> <EmptySpace max="-2" attributes="0"/> <Group type="103" groupAlignment="0" max="-2" attributes="0"> - <Component id="jButton2" min="-2" max="-2" attributes="0"/> <Component id="jCheckBox2" alignment="1" max="32767" attributes="0"/> - <Component id="jLabel4" alignment="1" max="32767" attributes="0"/> + <Group type="103" alignment="1" groupAlignment="3" attributes="0"> + <Component id="jLabel4" alignment="3" max="32767" attributes="0"/> + <Component id="jButton2" alignment="3" min="-2" max="-2" attributes="0"/> + </Group> </Group> <EmptySpace max="32767" attributes="0"/> </Group> @@ -1650,7 +1292,7 @@ <Component id="jLabel2" min="-2" max="-2" attributes="0"/> <EmptySpace max="-2" attributes="0"/> <Component id="jTextField2" min="-2" pref="60" max="-2" attributes="0"/> - <EmptySpace max="32767" attributes="0"/> + <EmptySpace pref="30" max="32767" attributes="0"/> </Group> </Group> </DimensionLayout> diff --git a/GUI/src/main/java/cz/fidentis/analyst/registration/RegistrationPanel.java b/GUI/src/main/java/cz/fidentis/analyst/registration/RegistrationPanel.java index d2d56b96..d3eaa22f 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/registration/RegistrationPanel.java +++ b/GUI/src/main/java/cz/fidentis/analyst/registration/RegistrationPanel.java @@ -2,10 +2,11 @@ package cz.fidentis.analyst.registration; import cz.fidentis.analyst.canvas.Direction; import cz.fidentis.analyst.core.ControlPanel; +import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.DoubleSummaryStatistics; import javax.swing.ImageIcon; -import javax.swing.JFormattedTextField; +import javax.vecmath.Vector3d; /** * Panel used to interactively visualize two face and adjust their registration. @@ -26,53 +27,44 @@ public class RegistrationPanel extends ControlPanel { */ public static final String ACTION_COMMAND_APPLY_ICP = "apply ICP"; - public static final String ACTION_COMMAND_SHIFT_X = "shift x"; - public static final String ACTION_COMMAND_SHIFT_Y = "shift y"; - public static final String ACTION_COMMAND_SHIFT_Z = "shift z"; - public static final String ACTION_COMMAND_ROTATE_X = "rotate x"; - public static final String ACTION_COMMAND_ROTATE_Y = "rotate y"; - public static final String ACTION_COMMAND_ROTATE_Z = "rotate z"; - public static final String ACTION_COMMAND_SCALE = "scale"; - public static final String ACTION_COMMAND_RESET_ROTATION = "reset translation"; - public static final String ACTION_COMMAND_RESET_TRANSLATION = "reset rotation"; - public static final String ACTION_COMMAND_RESET_SCALE = "reset scale"; - public static final String ACTION_COMMAND_APPLY_TRANSFORMATIONS = "apply transformations"; + public static final String ACTION_COMMAND_MANUAL_TRANSFORMATION_FINISHED = "manual transformation"; + public static final String ACTION_COMMAND_MANUAL_TRANSFORMATION_IN_PROGRESS = "manual transformation in progress"; public static final String ACTION_COMMAND_FP_CLOSENESS_THRESHOLD = "fp closeness treshold"; public static final String ACTION_COMMAND_ICP_SCALE = "ICP scale"; public static final String ACTION_COMMAND_ICP_MAX_ITERATIONS = "ICP iterations"; public static final String ACTION_COMMAND_ICP_ERROR = "ICP error"; - public static final String ACTION_COMMAND_ICP_UNDERSAMPLING = "ICP undersampling"; public static final String ACTION_COMMAND_PROCRUSTES_APPLY = "Procrustes apply"; public static final String ACTION_COMMAND_PROCRUSTES_SCALE = "Procrustes scale"; + public static final String ACTION_COMMAND_ALIGN_SYMMETRY_PLANES = "align symmetry planes"; + /* * Configuration of panel-specific GUI elements */ public static final String STRATEGY_POINT_TO_POINT = "Point to point"; public static final String STRATEGY_POINT_TO_TRIANGLE = "Point to triangle"; - public static final double TRANSFORMATION_FINESS = 1.0; - /** - * Transformation shift is higher + * Animator which animates transformations */ - public static final double HIGH_SHIFT_QUOTIENT = 1.0; + private final ModelRotationAnimator animator = new ModelRotationAnimator(); - /** - * Transformation shift is lower - */ - public static final double LOW_SHIFT_QUOTIENT = 0.1; + private static final double MOVE_MODIFIER = 0.2; /** - * Animator which animates transformations + * ICP undersampling. 100 = none */ - private final ModelRotationAnimator animator = new ModelRotationAnimator(); + private int undersampling = 100; - private double moveModifier = LOW_SHIFT_QUOTIENT; + private Vector3d manualRotation = new Vector3d(); + private Vector3d manualTranslation = new Vector3d(); + private double manualScale = 1.0; + private final ActionListener action; + /** * Constructor. * @param action Action listener @@ -80,36 +72,67 @@ public class RegistrationPanel extends ControlPanel { public RegistrationPanel(ActionListener action) { this.setName(NAME); initComponents(); - setDefaults(); + this.action = action; // connect GUI element with RegistrationAction listener: jButton1.addActionListener(createListener(action, ACTION_COMMAND_APPLY_ICP)); - translationXFTF.addActionListener(createListener(action, ACTION_COMMAND_SHIFT_X)); - translationYFTF.addActionListener(createListener(action, ACTION_COMMAND_SHIFT_Y)); - translationZFTF.addActionListener(createListener(action, ACTION_COMMAND_SHIFT_Z)); - rotationXFTF.addActionListener(createListener(action, ACTION_COMMAND_ROTATE_X)); - rotationYFTF.addActionListener(createListener(action, ACTION_COMMAND_ROTATE_Y)); - rotationZFTF.addActionListener(createListener(action, ACTION_COMMAND_ROTATE_Z)); - scaleFTF.addActionListener(createListener(action, ACTION_COMMAND_SCALE)); - translationButton.addActionListener(createListener(action, ACTION_COMMAND_RESET_TRANSLATION)); - rotationButton.addActionListener(createListener(action, ACTION_COMMAND_RESET_ROTATION)); - scaleButton.addActionListener(createListener(action, ACTION_COMMAND_RESET_SCALE)); - jButton2.addActionListener(createListener(action, ACTION_COMMAND_PROCRUSTES_APPLY)); - scaleFTF.addActionListener(createListener(action, ACTION_COMMAND_PROCRUSTES_SCALE)); - - resetAllButton.addActionListener(createListener(action, ACTION_COMMAND_RESET_TRANSLATION)); - resetAllButton.addActionListener(createListener(action, ACTION_COMMAND_RESET_SCALE)); - resetAllButton.addActionListener(createListener(action, ACTION_COMMAND_RESET_ROTATION)); + jButton3.addActionListener(createListener(action, ACTION_COMMAND_ALIGN_SYMMETRY_PLANES)); - applyButton.addActionListener(createListener(action, ACTION_COMMAND_APPLY_TRANSFORMATIONS)); + thersholdFTF.setValue(5.0); thersholdFTF.addActionListener(createListener(action, ACTION_COMMAND_FP_CLOSENESS_THRESHOLD)); + jCheckBox1.addActionListener(createListener(action, ACTION_COMMAND_ICP_SCALE)); jFormattedTextField1.addActionListener(createListener(action, ACTION_COMMAND_ICP_ERROR)); jFormattedTextField2.addActionListener(createListener(action, ACTION_COMMAND_ICP_MAX_ITERATIONS)); - jComboBox1.addActionListener(createListener(action, ACTION_COMMAND_ICP_UNDERSAMPLING)); + + //comboSliderInteger1.setSliderEast(); + comboSliderInteger1.setRange(10, 100); + comboSliderInteger1.setValue(this.undersampling); + comboSliderInteger1.addInputFieldListener((ActionEvent ae) -> { // update slider when the input field changed + this.undersampling = comboSliderInteger1.getValue(); + }); + } + + /** + * Returns the manual rotation info and resents it + * @return rotation info + */ + public Vector3d getAndClearManualRotation() { + Vector3d ret = this.manualRotation; + this.manualRotation = new Vector3d(0, 0, 0); + return ret; + } + + /** + * Returns the manual translation info and resents it + * @return translation info + */ + public Vector3d getAndClearManualTranslation() { + Vector3d ret = this.manualTranslation; + this.manualTranslation = new Vector3d(0, 0, 0); + return ret; } + /** + * Returns the manual scale info and resents it + * @return scale info + */ + public double getAndClearManualScale() { + double ret = this.manualScale; + this.manualScale = 1.0; + return ret; + } + + /** + * Returns ICP undersampling parameter + * @return ICP undersampling parameter + */ + public int getIcpUndersampling() { + return undersampling; + } + + /** * Updates GUI elements that display statistical data about the calculated Hausdorff distance. * @@ -131,50 +154,56 @@ public class RegistrationPanel extends ControlPanel { public void transform(Direction dir) { switch (dir) { case TRANSLATE_LEFT: - decreaseInputField(translationXFTF); + this.manualTranslation.x -= this.MOVE_MODIFIER; break; case TRANSLATE_RIGHT: - increaseInputField(translationXFTF); + this.manualTranslation.x += this.MOVE_MODIFIER; break; case TRANSLATE_UP: - decreaseInputField(translationYFTF); + this.manualTranslation.y -= this.MOVE_MODIFIER; break; case TRANSLATE_DOWN: - increaseInputField(translationYFTF); + this.manualTranslation.y += this.MOVE_MODIFIER; break; case TRANSLATE_IN: - decreaseInputField(translationZFTF); + this.manualTranslation.z -= this.MOVE_MODIFIER; break; case TRANSLATE_OUT: - increaseInputField(translationZFTF); + this.manualTranslation.z += this.MOVE_MODIFIER; break; case ROTATE_LEFT: - decreaseInputField(rotationXFTF); + this.manualRotation.x -= this.MOVE_MODIFIER / 180.0; break; case ROTATE_RIGHT: - increaseInputField(rotationXFTF); + this.manualRotation.x += this.MOVE_MODIFIER / 180.0; break; case ROTATE_UP: - increaseInputField(rotationYFTF); + this.manualRotation.y -= this.MOVE_MODIFIER / 180.0; break; case ROTATE_DOWN: - decreaseInputField(rotationYFTF); + this.manualRotation.y += this.MOVE_MODIFIER / 180.0; break; case ROTATE_IN: - increaseInputField(rotationZFTF); + this.manualRotation.z += this.MOVE_MODIFIER / 180.0; break; case ROTATE_OUT: - decreaseInputField(rotationZFTF); + this.manualRotation.z -= this.MOVE_MODIFIER / 180.0; break; case ZOOM_OUT: - decreaseInputField(scaleFTF); + this.manualScale /= 1.005; break; case ZOOM_IN: - increaseInputField(scaleFTF); + this.manualScale *= 1.005; break; default: throw new UnsupportedOperationException(); } + + action.actionPerformed(new ActionEvent( + this, + ActionEvent.ACTION_PERFORMED, + ACTION_COMMAND_MANUAL_TRANSFORMATION_IN_PROGRESS) + ); } @Override @@ -191,37 +220,6 @@ public class RegistrationPanel extends ControlPanel { return new ImageIcon(RegistrationPanel.class.getClassLoader().getResource("/" + ICON)); } - /** - * Additional initialization of panel - */ - private void setDefaults() { - lowShiftRB.setSelected(true); - thersholdFTF.setValue(5.0); - //resetAllButtonActionPerformed(null); - - rotationXFTF.setValue(0.0); - rotationYFTF.setValue(0.0); - rotationZFTF.setValue(0.0); - translationXFTF.setValue(0.0); - translationYFTF.setValue(0.0); - translationZFTF.setValue(0.0); - scaleFTF.setValue(0.0); - } - - private void increaseInputField(JFormattedTextField textField) { - double newValue = ((Number)textField.getValue()).doubleValue() + moveModifier; - newValue *= TRANSFORMATION_FINESS; - textField.setValue(newValue); - textField.postActionEvent(); - } - - private void decreaseInputField(JFormattedTextField textField) { - double newValue = ((Number)textField.getValue()).doubleValue() - moveModifier; - newValue *= TRANSFORMATION_FINESS; - textField.setValue(newValue); - textField.postActionEvent(); - } - /** * This method is called from within the constructor to initialize the form. * WARNING: Do NOT modify this code. The content of this method is always @@ -237,14 +235,10 @@ public class RegistrationPanel extends ControlPanel { jSeparator11 = new javax.swing.JSeparator(); transformationPanel = new javax.swing.JPanel(); translationPanel = new javax.swing.JPanel(); - translationButton = new javax.swing.JButton(); rightTranslationXButton = new javax.swing.JButton(); - translationXFTF = new javax.swing.JFormattedTextField(); leftTranslationYButton = new javax.swing.JButton(); - translationYFTF = new javax.swing.JFormattedTextField(); rightTranslationYButton = new javax.swing.JButton(); translXLabel = new javax.swing.JLabel(); - translationZFTF = new javax.swing.JFormattedTextField(); translZLabel = new javax.swing.JLabel(); translYLabel = new javax.swing.JLabel(); rightTranslationZButton = new javax.swing.JButton(); @@ -256,27 +250,16 @@ public class RegistrationPanel extends ControlPanel { rotatZLabel = new javax.swing.JLabel(); rotatYLabel = new javax.swing.JLabel(); rotatXLabel = new javax.swing.JLabel(); - rotationButton = new javax.swing.JButton(); - rotationZFTF = new javax.swing.JFormattedTextField(); - rotationYFTF = new javax.swing.JFormattedTextField(); rightRotationZButton = new javax.swing.JButton(); rightRotationYButton = new javax.swing.JButton(); leftRotationZButton = new javax.swing.JButton(); rightRotationXButton = new javax.swing.JButton(); - rotationXFTF = new javax.swing.JFormattedTextField(); scalePanel = new javax.swing.JPanel(); scalePlusButton = new javax.swing.JButton(); - scaleFTF = new javax.swing.JFormattedTextField(); - scaleButton = new javax.swing.JButton(); scaleMinusButton = new javax.swing.JButton(); - resetAllButton = new javax.swing.JButton(); shiftPanel = new javax.swing.JPanel(); - jSeparator10 = new javax.swing.JSeparator(); - applyButton = new javax.swing.JButton(); jSeparator5 = new javax.swing.JSeparator(); - shiftLabel = new javax.swing.JLabel(); - lowShiftRB = new javax.swing.JRadioButton(); - highShiftRB = new javax.swing.JRadioButton(); + jButton3 = new javax.swing.JButton(); jSeparator2 = new javax.swing.JSeparator(); jPanel1 = new javax.swing.JPanel(); jLabel7 = new javax.swing.JLabel(); @@ -286,8 +269,8 @@ public class RegistrationPanel extends ControlPanel { jLabel6 = new javax.swing.JLabel(); jFormattedTextField2 = new javax.swing.JFormattedTextField(); jLabel8 = new javax.swing.JLabel(); - jComboBox1 = new javax.swing.JComboBox<>(); jButton1 = new javax.swing.JButton(); + comboSliderInteger1 = new cz.fidentis.analyst.core.ComboSliderInteger(); jPanel4 = new javax.swing.JPanel(); featurePointsLabel = new javax.swing.JLabel(); thersholdFTF = new javax.swing.JFormattedTextField(); @@ -312,18 +295,6 @@ public class RegistrationPanel extends ControlPanel { translationPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.translationPanel.border.title"))); // NOI18N - translationButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/restart-line.png"))); // NOI18N - org.openide.awt.Mnemonics.setLocalizedText(translationButton, org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.translationButton.text")); // NOI18N - translationButton.setToolTipText(org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.translationButton.toolTipText")); // NOI18N - translationButton.setBorder(javax.swing.BorderFactory.createTitledBorder("")); - translationButton.setCursor(new java.awt.Cursor(java.awt.Cursor.DEFAULT_CURSOR)); - translationButton.setMargin(new java.awt.Insets(2, 2, 2, 2)); - translationButton.addMouseListener(new java.awt.event.MouseAdapter() { - public void mousePressed(java.awt.event.MouseEvent evt) { - translationButtonMousePressed(evt); - } - }); - rightTranslationXButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/arrow-right-s-line.png"))); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(rightTranslationXButton, org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.rightTranslationXButton.text")); // NOI18N rightTranslationXButton.setBorder(javax.swing.BorderFactory.createTitledBorder("")); @@ -341,10 +312,6 @@ public class RegistrationPanel extends ControlPanel { } }); - translationXFTF.setFormatterFactory(new javax.swing.text.DefaultFormatterFactory(new javax.swing.text.NumberFormatter())); - translationXFTF.setToolTipText(org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.translationXFTF.toolTipText")); // NOI18N - translationXFTF.setCursor(new java.awt.Cursor(java.awt.Cursor.TEXT_CURSOR)); - leftTranslationYButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/arrow-left-s-line.png"))); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(leftTranslationYButton, org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.leftTranslationYButton.text")); // NOI18N leftTranslationYButton.setBorder(javax.swing.BorderFactory.createTitledBorder("")); @@ -360,8 +327,6 @@ public class RegistrationPanel extends ControlPanel { } }); - translationYFTF.setFormatterFactory(new javax.swing.text.DefaultFormatterFactory(new javax.swing.text.NumberFormatter())); - rightTranslationYButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/arrow-right-s-line.png"))); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(rightTranslationYButton, org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.rightTranslationYButton.text")); // NOI18N rightTranslationYButton.setBorder(javax.swing.BorderFactory.createTitledBorder("")); @@ -380,8 +345,6 @@ public class RegistrationPanel extends ControlPanel { translXLabel.setFont(new java.awt.Font("Tahoma", 0, 11)); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(translXLabel, org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.translXLabel.text")); // NOI18N - translationZFTF.setFormatterFactory(new javax.swing.text.DefaultFormatterFactory(new javax.swing.text.NumberFormatter())); - translZLabel.setFont(new java.awt.Font("Tahoma", 0, 11)); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(translZLabel, org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.translZLabel.text")); // NOI18N @@ -440,29 +403,19 @@ public class RegistrationPanel extends ControlPanel { translationPanelLayout.setHorizontalGroup( translationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(translationPanelLayout.createSequentialGroup() - .addGroup(translationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false) - .addGroup(translationPanelLayout.createSequentialGroup() - .addGap(31, 31, 31) - .addComponent(translXLabel)) + .addContainerGap() + .addGroup(translationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(translXLabel) .addGroup(translationPanelLayout.createSequentialGroup() - .addGap(2, 2, 2) - .addComponent(translationButton) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGroup(translationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(translationXFTF, javax.swing.GroupLayout.PREFERRED_SIZE, 54, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGroup(translationPanelLayout.createSequentialGroup() - .addComponent(leftTranslationXButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(rightTranslationXButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))))) + .addComponent(leftTranslationXButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(rightTranslationXButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) .addGroup(translationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(translationPanelLayout.createSequentialGroup() .addGap(7, 7, 7) - .addGroup(translationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(translationPanelLayout.createSequentialGroup() - .addComponent(leftTranslationYButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(rightTranslationYButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addComponent(translationYFTF, javax.swing.GroupLayout.PREFERRED_SIZE, 54, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addComponent(leftTranslationYButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(rightTranslationYButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addGroup(translationPanelLayout.createSequentialGroup() .addGap(15, 15, 15) .addComponent(translYLabel))) @@ -470,12 +423,9 @@ public class RegistrationPanel extends ControlPanel { .addGroup(translationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(translationPanelLayout.createSequentialGroup() .addGap(1, 1, 1) - .addGroup(translationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(translationPanelLayout.createSequentialGroup() - .addComponent(leftTranslationZButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(rightTranslationZButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addComponent(translationZFTF, javax.swing.GroupLayout.PREFERRED_SIZE, 54, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(leftTranslationZButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(rightTranslationZButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addGap(2, 2, 2)) .addComponent(translZLabel, javax.swing.GroupLayout.Alignment.TRAILING)) .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) @@ -490,32 +440,14 @@ public class RegistrationPanel extends ControlPanel { .addComponent(translZLabel)) .addGap(9, 9, 9) .addGroup(translationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(translationPanelLayout.createSequentialGroup() - .addGroup(translationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) - .addComponent(rightTranslationXButton, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(leftTranslationXButton, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(translationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(translationXFTF, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, translationPanelLayout.createSequentialGroup() - .addComponent(translationButton) - .addGap(6, 6, 6)))) - .addGroup(translationPanelLayout.createSequentialGroup() - .addGroup(translationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) - .addComponent(rightTranslationYButton, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(leftTranslationYButton, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(translationYFTF, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addGroup(translationPanelLayout.createSequentialGroup() - .addGroup(translationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) - .addGroup(translationPanelLayout.createSequentialGroup() - .addComponent(rightTranslationZButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(6, 6, 6)) - .addGroup(javax.swing.GroupLayout.Alignment.LEADING, translationPanelLayout.createSequentialGroup() - .addComponent(leftTranslationZButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED))) - .addComponent(translationZFTF, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) - .addGap(10, 10, 10)) + .addGroup(translationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(rightTranslationZButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(leftTranslationZButton, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(rightTranslationXButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(leftTranslationXButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(rightTranslationYButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(leftTranslationYButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); rotationPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.rotationPanel.border.title"))); // NOI18N @@ -561,22 +493,6 @@ public class RegistrationPanel extends ControlPanel { rotatXLabel.setFont(new java.awt.Font("Tahoma", 0, 11)); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(rotatXLabel, org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.rotatXLabel.text")); // NOI18N - rotationButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/restart-line.png"))); // NOI18N - org.openide.awt.Mnemonics.setLocalizedText(rotationButton, org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.rotationButton.text")); // NOI18N - rotationButton.setToolTipText(org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.rotationButton.toolTipText")); // NOI18N - rotationButton.setBorder(javax.swing.BorderFactory.createTitledBorder("")); - rotationButton.setCursor(new java.awt.Cursor(java.awt.Cursor.DEFAULT_CURSOR)); - rotationButton.setMargin(new java.awt.Insets(2, 2, 2, 2)); - rotationButton.addMouseListener(new java.awt.event.MouseAdapter() { - public void mousePressed(java.awt.event.MouseEvent evt) { - rotationButtonMousePressed(evt); - } - }); - - rotationZFTF.setFormatterFactory(new javax.swing.text.DefaultFormatterFactory(new javax.swing.text.NumberFormatter())); - - rotationYFTF.setFormatterFactory(new javax.swing.text.DefaultFormatterFactory(new javax.swing.text.NumberFormatter())); - rightRotationZButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/arrow-right-s-line.png"))); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(rightRotationZButton, org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.rightRotationZButton.text")); // NOI18N rightRotationZButton.setBorder(javax.swing.BorderFactory.createTitledBorder("")); @@ -645,37 +561,26 @@ public class RegistrationPanel extends ControlPanel { } }); - rotationXFTF.setFormatterFactory(new javax.swing.text.DefaultFormatterFactory(new javax.swing.text.NumberFormatter())); - rotationXFTF.setToolTipText(org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.rotationXFTF.toolTipText")); // NOI18N - javax.swing.GroupLayout rotationPanelLayout = new javax.swing.GroupLayout(rotationPanel); rotationPanel.setLayout(rotationPanelLayout); rotationPanelLayout.setHorizontalGroup( rotationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(rotationPanelLayout.createSequentialGroup() - .addGap(2, 2, 2) + .addContainerGap() .addGroup(rotationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(rotationPanelLayout.createSequentialGroup() - .addComponent(rotationButton) + .addComponent(leftRotationXButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(rotationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(rotationXFTF, javax.swing.GroupLayout.PREFERRED_SIZE, 54, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGroup(rotationPanelLayout.createSequentialGroup() - .addComponent(leftRotationXButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(rightRotationXButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)))) + .addComponent(rightRotationXButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addGroup(rotationPanelLayout.createSequentialGroup() - .addGap(55, 55, 55) + .addGap(23, 23, 23) .addComponent(rotatXLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 20, javax.swing.GroupLayout.PREFERRED_SIZE))) .addGroup(rotationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(rotationPanelLayout.createSequentialGroup() .addGap(6, 6, 6) - .addGroup(rotationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(rotationPanelLayout.createSequentialGroup() - .addComponent(leftRotationYButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(rightRotationYButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addComponent(rotationYFTF, javax.swing.GroupLayout.PREFERRED_SIZE, 54, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addComponent(leftRotationYButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(rightRotationYButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addGroup(rotationPanelLayout.createSequentialGroup() .addGap(29, 29, 29) .addComponent(rotatYLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 17, javax.swing.GroupLayout.PREFERRED_SIZE))) @@ -685,12 +590,9 @@ public class RegistrationPanel extends ControlPanel { .addComponent(rotatZLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 21, javax.swing.GroupLayout.PREFERRED_SIZE)) .addGroup(rotationPanelLayout.createSequentialGroup() .addGap(6, 6, 6) - .addGroup(rotationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) - .addComponent(rotationZFTF, javax.swing.GroupLayout.PREFERRED_SIZE, 54, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGroup(rotationPanelLayout.createSequentialGroup() - .addComponent(leftRotationZButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(rightRotationZButton, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))))) + .addComponent(leftRotationZButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(rightRotationZButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); rotationPanelLayout.setVerticalGroup( @@ -706,25 +608,12 @@ public class RegistrationPanel extends ControlPanel { .addComponent(rotatYLabel)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(rotationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(rotationPanelLayout.createSequentialGroup() - .addGroup(rotationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(leftRotationXButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(rightRotationXButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(leftRotationYButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(rightRotationYButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(leftRotationZButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(rotationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(rotationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(rotationXFTF, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(rotationYFTF, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(rotationZFTF, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, rotationPanelLayout.createSequentialGroup() - .addComponent(rotationButton) - .addGap(5, 5, 5)))) - .addGroup(rotationPanelLayout.createSequentialGroup() - .addComponent(rightRotationZButton, javax.swing.GroupLayout.PREFERRED_SIZE, 24, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(41, 41, 41))) + .addComponent(rightRotationZButton, javax.swing.GroupLayout.PREFERRED_SIZE, 24, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(leftRotationXButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(rightRotationXButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(leftRotationYButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(rightRotationYButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(leftRotationZButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); @@ -747,26 +636,6 @@ public class RegistrationPanel extends ControlPanel { } }); - scaleFTF.setFormatterFactory(new javax.swing.text.DefaultFormatterFactory(new javax.swing.text.NumberFormatter())); - scaleFTF.setToolTipText(org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.scaleFTF.toolTipText")); // NOI18N - scaleFTF.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - scaleFTFActionPerformed(evt); - } - }); - - scaleButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/restart-line.png"))); // NOI18N - org.openide.awt.Mnemonics.setLocalizedText(scaleButton, org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.scaleButton.text")); // NOI18N - scaleButton.setToolTipText(org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.scaleButton.toolTipText")); // NOI18N - scaleButton.setBorder(javax.swing.BorderFactory.createTitledBorder("")); - scaleButton.setCursor(new java.awt.Cursor(java.awt.Cursor.DEFAULT_CURSOR)); - scaleButton.setMargin(new java.awt.Insets(2, 2, 2, 2)); - scaleButton.addMouseListener(new java.awt.event.MouseAdapter() { - public void mousePressed(java.awt.event.MouseEvent evt) { - scaleButtonMousePressed(evt); - } - }); - scaleMinusButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/subtract-line.png"))); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(scaleMinusButton, org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.scaleMinusButton.text")); // NOI18N scaleMinusButton.setBorder(javax.swing.BorderFactory.createTitledBorder("")); @@ -789,45 +658,22 @@ public class RegistrationPanel extends ControlPanel { scalePanelLayout.setHorizontalGroup( scalePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(scalePanelLayout.createSequentialGroup() - .addGap(32, 32, 32) + .addContainerGap() .addComponent(scalePlusButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(scaleMinusButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(0, 0, Short.MAX_VALUE)) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, scalePanelLayout.createSequentialGroup() - .addGap(2, 2, 2) - .addComponent(scaleButton) - .addGap(7, 7, 7) - .addComponent(scaleFTF, javax.swing.GroupLayout.PREFERRED_SIZE, 54, javax.swing.GroupLayout.PREFERRED_SIZE) - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addContainerGap(15, Short.MAX_VALUE)) ); scalePanelLayout.setVerticalGroup( scalePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, scalePanelLayout.createSequentialGroup() - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(scalePanelLayout.createSequentialGroup() + .addGap(26, 26, 26) .addGroup(scalePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(scalePlusButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(scaleMinusButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(scalePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(scaleFTF, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, scalePanelLayout.createSequentialGroup() - .addComponent(scaleButton) - .addGap(5, 5, 5))) - .addContainerGap()) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); - resetAllButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/refresh-line.png"))); // NOI18N - org.openide.awt.Mnemonics.setLocalizedText(resetAllButton, org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.resetAllButton.text")); // NOI18N - resetAllButton.setToolTipText(org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.resetAllButton.toolTipText")); // NOI18N - resetAllButton.setBorder(javax.swing.BorderFactory.createTitledBorder("")); - resetAllButton.setFocusPainted(false); - resetAllButton.addMouseListener(new java.awt.event.MouseAdapter() { - public void mousePressed(java.awt.event.MouseEvent evt) { - resetAllButtonMousePressed(evt); - } - }); - javax.swing.GroupLayout shiftPanelLayout = new javax.swing.GroupLayout(shiftPanel); shiftPanel.setLayout(shiftPanelLayout); shiftPanelLayout.setHorizontalGroup( @@ -839,40 +685,7 @@ public class RegistrationPanel extends ControlPanel { .addGap(0, 84, Short.MAX_VALUE) ); - jSeparator10.setOrientation(javax.swing.SwingConstants.VERTICAL); - - org.openide.awt.Mnemonics.setLocalizedText(applyButton, org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.applyButton.text")); // NOI18N - applyButton.setToolTipText(org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.applyButton.toolTipText")); // NOI18N - applyButton.setBorder(javax.swing.BorderFactory.createTitledBorder("")); - applyButton.setCursor(new java.awt.Cursor(java.awt.Cursor.HAND_CURSOR)); - applyButton.setFocusPainted(false); - applyButton.addMouseListener(new java.awt.event.MouseAdapter() { - public void mousePressed(java.awt.event.MouseEvent evt) { - applyButtonMousePressed(evt); - } - }); - - shiftLabel.setFont(new java.awt.Font("Ubuntu", 1, 15)); // NOI18N - org.openide.awt.Mnemonics.setLocalizedText(shiftLabel, org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.shiftLabel.text")); // NOI18N - shiftLabel.setToolTipText(org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.shiftLabel.toolTipText")); // NOI18N - - precisionGroup.add(lowShiftRB); - org.openide.awt.Mnemonics.setLocalizedText(lowShiftRB, org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.lowShiftRB.text")); // NOI18N - lowShiftRB.setToolTipText(org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.lowShiftRB.toolTipText")); // NOI18N - lowShiftRB.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - lowShiftRBActionPerformed(evt); - } - }); - - precisionGroup.add(highShiftRB); - org.openide.awt.Mnemonics.setLocalizedText(highShiftRB, org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.highShiftRB.text")); // NOI18N - highShiftRB.setToolTipText(org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.highShiftRB.toolTipText")); // NOI18N - highShiftRB.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - highShiftRBActionPerformed(evt); - } - }); + org.openide.awt.Mnemonics.setLocalizedText(jButton3, org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.jButton3.text")); // NOI18N javax.swing.GroupLayout transformationPanelLayout = new javax.swing.GroupLayout(transformationPanel); transformationPanel.setLayout(transformationPanelLayout); @@ -887,50 +700,27 @@ public class RegistrationPanel extends ControlPanel { .addGap(580, 580, 580) .addComponent(shiftPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addGroup(transformationPanelLayout.createSequentialGroup() - .addGap(349, 349, 349) - .addComponent(jSeparator10, javax.swing.GroupLayout.PREFERRED_SIZE, 15, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addContainerGap() + .addComponent(jButton3)) .addGroup(transformationPanelLayout.createSequentialGroup() - .addGroup(transformationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(transformationPanelLayout.createSequentialGroup() - .addGap(24, 24, 24) - .addComponent(shiftLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(lowShiftRB) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(highShiftRB)) - .addComponent(translationPanel, javax.swing.GroupLayout.PREFERRED_SIZE, 227, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addGroup(transformationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(transformationPanelLayout.createSequentialGroup() - .addGap(24, 24, 24) - .addComponent(resetAllButton, javax.swing.GroupLayout.PREFERRED_SIZE, 128, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(applyButton, javax.swing.GroupLayout.PREFERRED_SIZE, 128, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addGroup(transformationPanelLayout.createSequentialGroup() - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(rotationPanel, javax.swing.GroupLayout.PREFERRED_SIZE, 224, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(scalePanel, javax.swing.GroupLayout.PREFERRED_SIZE, 102, javax.swing.GroupLayout.PREFERRED_SIZE))))) + .addComponent(translationPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(rotationPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(scalePanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) .addGap(0, 0, Short.MAX_VALUE)) ); transformationPanelLayout.setVerticalGroup( transformationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(transformationPanelLayout.createSequentialGroup() .addContainerGap() - .addGroup(transformationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(transformationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) .addComponent(scalePanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(rotationPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(translationPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(transformationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(applyButton, javax.swing.GroupLayout.PREFERRED_SIZE, 26, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGroup(transformationPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(shiftLabel) - .addComponent(lowShiftRB) - .addComponent(highShiftRB) - .addComponent(resetAllButton))) - .addGap(39, 39, 39) - .addComponent(jSeparator10, javax.swing.GroupLayout.PREFERRED_SIZE, 62, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(25, 25, 25) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jButton3) + .addGap(59, 59, 59) .addComponent(shiftPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(jSeparator5, javax.swing.GroupLayout.PREFERRED_SIZE, 10, javax.swing.GroupLayout.PREFERRED_SIZE)) @@ -961,8 +751,6 @@ public class RegistrationPanel extends ControlPanel { org.openide.awt.Mnemonics.setLocalizedText(jLabel8, org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.jLabel8.text")); // NOI18N - jComboBox1.setModel(new javax.swing.DefaultComboBoxModel<>(new String[] { "None", "Random 200" })); - jButton1.setFont(new java.awt.Font("Ubuntu", 1, 15)); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(jButton1, org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.jButton1.text")); // NOI18N jButton1.setToolTipText(org.openide.util.NbBundle.getMessage(RegistrationPanel.class, "RegistrationPanel.jButton1.toolTipText")); // NOI18N @@ -978,25 +766,26 @@ public class RegistrationPanel extends ControlPanel { jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(jPanel1Layout.createSequentialGroup() .addContainerGap() - .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) - .addGroup(jPanel1Layout.createSequentialGroup() - .addComponent(jLabel8) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(jComboBox1, javax.swing.GroupLayout.PREFERRED_SIZE, 166, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(jButton1)) + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(jPanel1Layout.createSequentialGroup() - .addComponent(jLabel7) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(jCheckBox1) - .addGap(18, 18, 18) - .addComponent(jLabel5) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(jFormattedTextField1, javax.swing.GroupLayout.PREFERRED_SIZE, 49, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(18, 18, 18) - .addComponent(jLabel6) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(jFormattedTextField2, javax.swing.GroupLayout.PREFERRED_SIZE, 53, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel1Layout.createSequentialGroup() + .addComponent(jLabel7) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jCheckBox1) + .addGap(18, 18, 18) + .addComponent(jLabel5) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jFormattedTextField1, javax.swing.GroupLayout.PREFERRED_SIZE, 49, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(comboSliderInteger1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGap(12, 12, 12) + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel1Layout.createSequentialGroup() + .addComponent(jLabel6) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jFormattedTextField2, javax.swing.GroupLayout.PREFERRED_SIZE, 53, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(jLabel8))) + .addComponent(jButton1)) .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); jPanel1Layout.setVerticalGroup( @@ -1011,12 +800,15 @@ public class RegistrationPanel extends ControlPanel { .addComponent(jFormattedTextField1, javax.swing.GroupLayout.PREFERRED_SIZE, 22, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(jLabel6) .addComponent(jFormattedTextField2, javax.swing.GroupLayout.PREFERRED_SIZE, 20, javax.swing.GroupLayout.PREFERRED_SIZE))) - .addGap(18, 18, 18) .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(jLabel8) - .addComponent(jComboBox1, javax.swing.GroupLayout.PREFERRED_SIZE, 27, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addComponent(jButton1)) + .addGroup(jPanel1Layout.createSequentialGroup() + .addGap(5, 5, 5) + .addComponent(comboSliderInteger1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(jPanel1Layout.createSequentialGroup() + .addGap(26, 26, 26) + .addComponent(jLabel8))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jButton1) .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); @@ -1111,21 +903,22 @@ public class RegistrationPanel extends ControlPanel { jPanel7Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(jPanel7Layout.createSequentialGroup() .addContainerGap() + .addComponent(jButton2) + .addGap(18, 18, 18) .addComponent(jLabel4) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(jCheckBox2) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(jButton2) - .addGap(190, 190, 190)) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); jPanel7Layout.setVerticalGroup( jPanel7Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(jPanel7Layout.createSequentialGroup() .addContainerGap() .addGroup(jPanel7Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) - .addComponent(jButton2) .addComponent(jCheckBox2, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(jLabel4, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel7Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(jLabel4, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jButton2))) .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); @@ -1154,7 +947,7 @@ public class RegistrationPanel extends ControlPanel { .addComponent(jLabel2) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(jTextField2, javax.swing.GroupLayout.PREFERRED_SIZE, 60, javax.swing.GroupLayout.PREFERRED_SIZE) - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addContainerGap(30, Short.MAX_VALUE)) ); jPanel2Layout.setVerticalGroup( jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) @@ -1183,14 +976,16 @@ public class RegistrationPanel extends ControlPanel { .addGroup(layout.createSequentialGroup() .addContainerGap() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false) - .addComponent(jPanel2, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jPanel1, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(jPanel4, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(jPanel7, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(transformationPanel, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.PREFERRED_SIZE, 585, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(jPanel1, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - .addGap(153, 153, 153) - .addComponent(jSeparator7, javax.swing.GroupLayout.PREFERRED_SIZE, 50, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(0, 0, Short.MAX_VALUE)) + .addComponent(jPanel7, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jSeparator7, javax.swing.GroupLayout.PREFERRED_SIZE, 50, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(jPanel2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) @@ -1205,12 +1000,12 @@ public class RegistrationPanel extends ControlPanel { .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(jPanel1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(transformationPanel, javax.swing.GroupLayout.PREFERRED_SIZE, 209, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(transformationPanel, javax.swing.GroupLayout.PREFERRED_SIZE, 176, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(jPanel4, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(jPanel2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(232, 232, 232) + .addGap(328, 328, 328) .addComponent(jSeparator11, javax.swing.GroupLayout.PREFERRED_SIZE, 10, javax.swing.GroupLayout.PREFERRED_SIZE) .addGap(221, 221, 221) .addComponent(jSeparator2, javax.swing.GroupLayout.PREFERRED_SIZE, 10, javax.swing.GroupLayout.PREFERRED_SIZE) @@ -1220,7 +1015,6 @@ public class RegistrationPanel extends ControlPanel { private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton1ActionPerformed - resetAllButtonMousePressed(null); }//GEN-LAST:event_jButton1ActionPerformed private void jCheckBox1ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jCheckBox1ActionPerformed @@ -1239,38 +1033,26 @@ public class RegistrationPanel extends ControlPanel { thersholdFTF.postActionEvent(); }//GEN-LAST:event_thersholdUpButtonActionPerformed - private void highShiftRBActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_highShiftRBActionPerformed - this.moveModifier = HIGH_SHIFT_QUOTIENT; - }//GEN-LAST:event_highShiftRBActionPerformed - - private void lowShiftRBActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_lowShiftRBActionPerformed - this.moveModifier = LOW_SHIFT_QUOTIENT; - }//GEN-LAST:event_lowShiftRBActionPerformed - - private void applyButtonMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_applyButtonMousePressed - resetAllButtonMousePressed(evt); - }//GEN-LAST:event_applyButtonMousePressed - - private void resetAllButtonMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_resetAllButtonMousePressed - translationButtonMousePressed(evt); - rotationButtonMousePressed(evt); - scaleButtonMousePressed(evt); - }//GEN-LAST:event_resetAllButtonMousePressed - private void scaleMinusButtonMouseReleased(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_scaleMinusButtonMouseReleased animator.stopModelAnimation(this); + action.actionPerformed(new ActionEvent( + evt, + ActionEvent.ACTION_PERFORMED, + ACTION_COMMAND_MANUAL_TRANSFORMATION_FINISHED) + ); }//GEN-LAST:event_scaleMinusButtonMouseReleased private void scaleMinusButtonMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_scaleMinusButtonMousePressed animator.startModelAnimation(Direction.ZOOM_OUT, this); }//GEN-LAST:event_scaleMinusButtonMousePressed - private void scaleButtonMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_scaleButtonMousePressed - scaleFTF.setValue(0.0); - }//GEN-LAST:event_scaleButtonMousePressed - private void scalePlusButtonMouseReleased(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_scalePlusButtonMouseReleased animator.stopModelAnimation(this); + action.actionPerformed(new ActionEvent( + evt, + ActionEvent.ACTION_PERFORMED, + ACTION_COMMAND_MANUAL_TRANSFORMATION_FINISHED) + ); }//GEN-LAST:event_scalePlusButtonMouseReleased private void scalePlusButtonMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_scalePlusButtonMousePressed @@ -1279,6 +1061,11 @@ public class RegistrationPanel extends ControlPanel { private void rightRotationXButtonMouseReleased(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_rightRotationXButtonMouseReleased animator.stopModelAnimation(this); + action.actionPerformed(new ActionEvent( + evt, + ActionEvent.ACTION_PERFORMED, + ACTION_COMMAND_MANUAL_TRANSFORMATION_FINISHED) + ); }//GEN-LAST:event_rightRotationXButtonMouseReleased private void rightRotationXButtonMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_rightRotationXButtonMousePressed @@ -1287,6 +1074,11 @@ public class RegistrationPanel extends ControlPanel { private void leftRotationZButtonMouseReleased(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_leftRotationZButtonMouseReleased animator.stopModelAnimation(this); + action.actionPerformed(new ActionEvent( + evt, + ActionEvent.ACTION_PERFORMED, + ACTION_COMMAND_MANUAL_TRANSFORMATION_FINISHED) + ); }//GEN-LAST:event_leftRotationZButtonMouseReleased private void leftRotationZButtonMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_leftRotationZButtonMousePressed @@ -1295,6 +1087,11 @@ public class RegistrationPanel extends ControlPanel { private void rightRotationYButtonMouseReleased(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_rightRotationYButtonMouseReleased animator.stopModelAnimation(this); + action.actionPerformed(new ActionEvent( + evt, + ActionEvent.ACTION_PERFORMED, + ACTION_COMMAND_MANUAL_TRANSFORMATION_FINISHED) + ); }//GEN-LAST:event_rightRotationYButtonMouseReleased private void rightRotationYButtonMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_rightRotationYButtonMousePressed @@ -1303,20 +1100,24 @@ public class RegistrationPanel extends ControlPanel { private void rightRotationZButtonMouseReleased(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_rightRotationZButtonMouseReleased animator.stopModelAnimation(this); + action.actionPerformed(new ActionEvent( + evt, + ActionEvent.ACTION_PERFORMED, + ACTION_COMMAND_MANUAL_TRANSFORMATION_FINISHED) + ); }//GEN-LAST:event_rightRotationZButtonMouseReleased private void rightRotationZButtonMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_rightRotationZButtonMousePressed animator.startModelAnimation(Direction.ROTATE_OUT, this); }//GEN-LAST:event_rightRotationZButtonMousePressed - private void rotationButtonMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_rotationButtonMousePressed - rotationXFTF.setValue(0.0); - rotationYFTF.setValue(0.0); - rotationZFTF.setValue(0.0); - }//GEN-LAST:event_rotationButtonMousePressed - private void leftRotationXButtonMouseReleased(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_leftRotationXButtonMouseReleased animator.stopModelAnimation(this); + action.actionPerformed(new ActionEvent( + evt, + ActionEvent.ACTION_PERFORMED, + ACTION_COMMAND_MANUAL_TRANSFORMATION_FINISHED) + ); }//GEN-LAST:event_leftRotationXButtonMouseReleased private void leftRotationXButtonMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_leftRotationXButtonMousePressed @@ -1325,6 +1126,11 @@ public class RegistrationPanel extends ControlPanel { private void leftRotationYButtonMouseReleased(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_leftRotationYButtonMouseReleased animator.stopModelAnimation(this); + action.actionPerformed(new ActionEvent( + evt, + ActionEvent.ACTION_PERFORMED, + ACTION_COMMAND_MANUAL_TRANSFORMATION_FINISHED) + ); }//GEN-LAST:event_leftRotationYButtonMouseReleased private void leftRotationYButtonMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_leftRotationYButtonMousePressed @@ -1333,6 +1139,11 @@ public class RegistrationPanel extends ControlPanel { private void leftTranslationZButtonMouseReleased(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_leftTranslationZButtonMouseReleased animator.stopModelAnimation(this); + action.actionPerformed(new ActionEvent( + evt, + ActionEvent.ACTION_PERFORMED, + ACTION_COMMAND_MANUAL_TRANSFORMATION_FINISHED) + ); }//GEN-LAST:event_leftTranslationZButtonMouseReleased private void leftTranslationZButtonMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_leftTranslationZButtonMousePressed @@ -1341,6 +1152,11 @@ public class RegistrationPanel extends ControlPanel { private void leftTranslationXButtonMouseReleased(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_leftTranslationXButtonMouseReleased animator.stopModelAnimation(this); + action.actionPerformed(new ActionEvent( + evt, + ActionEvent.ACTION_PERFORMED, + ACTION_COMMAND_MANUAL_TRANSFORMATION_FINISHED) + ); }//GEN-LAST:event_leftTranslationXButtonMouseReleased private void leftTranslationXButtonMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_leftTranslationXButtonMousePressed @@ -1349,6 +1165,11 @@ public class RegistrationPanel extends ControlPanel { private void rightTranslationZButtonMouseReleased(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_rightTranslationZButtonMouseReleased animator.stopModelAnimation(this); + action.actionPerformed(new ActionEvent( + evt, + ActionEvent.ACTION_PERFORMED, + ACTION_COMMAND_MANUAL_TRANSFORMATION_FINISHED) + ); }//GEN-LAST:event_rightTranslationZButtonMouseReleased private void rightTranslationZButtonMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_rightTranslationZButtonMousePressed @@ -1357,6 +1178,11 @@ public class RegistrationPanel extends ControlPanel { private void rightTranslationYButtonMouseReleased(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_rightTranslationYButtonMouseReleased animator.stopModelAnimation(this); + action.actionPerformed(new ActionEvent( + evt, + ActionEvent.ACTION_PERFORMED, + ACTION_COMMAND_MANUAL_TRANSFORMATION_FINISHED) + ); }//GEN-LAST:event_rightTranslationYButtonMouseReleased private void rightTranslationYButtonMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_rightTranslationYButtonMousePressed @@ -1365,6 +1191,11 @@ public class RegistrationPanel extends ControlPanel { private void leftTranslationYButtonMouseReleased(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_leftTranslationYButtonMouseReleased animator.stopModelAnimation(this); + action.actionPerformed(new ActionEvent( + evt, + ActionEvent.ACTION_PERFORMED, + ACTION_COMMAND_MANUAL_TRANSFORMATION_FINISHED) + ); }//GEN-LAST:event_leftTranslationYButtonMouseReleased private void leftTranslationYButtonMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_leftTranslationYButtonMousePressed @@ -1373,22 +1204,17 @@ public class RegistrationPanel extends ControlPanel { private void rightTranslationXButtonMouseReleased(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_rightTranslationXButtonMouseReleased animator.stopModelAnimation(this); + action.actionPerformed(new ActionEvent( + evt, + ActionEvent.ACTION_PERFORMED, + ACTION_COMMAND_MANUAL_TRANSFORMATION_FINISHED) + ); }//GEN-LAST:event_rightTranslationXButtonMouseReleased private void rightTranslationXButtonMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_rightTranslationXButtonMousePressed animator.startModelAnimation(Direction.TRANSLATE_RIGHT, this); }//GEN-LAST:event_rightTranslationXButtonMousePressed - private void translationButtonMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_translationButtonMousePressed - translationXFTF.setValue(0.0); - translationYFTF.setValue(0.0); - translationZFTF.setValue(0.0); - }//GEN-LAST:event_translationButtonMousePressed - - private void scaleFTFActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_scaleFTFActionPerformed - // TODO add your handling code here: - }//GEN-LAST:event_scaleFTFActionPerformed - private void jButton2ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton2ActionPerformed // TODO add your handling code here: }//GEN-LAST:event_jButton2ActionPerformed @@ -1399,14 +1225,13 @@ public class RegistrationPanel extends ControlPanel { // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JButton applyButton; + private cz.fidentis.analyst.core.ComboSliderInteger comboSliderInteger1; private javax.swing.JLabel featurePointsLabel; - private javax.swing.JRadioButton highShiftRB; private javax.swing.JButton jButton1; private javax.swing.JButton jButton2; + private javax.swing.JButton jButton3; private javax.swing.JCheckBox jCheckBox1; private javax.swing.JCheckBox jCheckBox2; - private javax.swing.JComboBox<String> jComboBox1; private javax.swing.JFormattedTextField jFormattedTextField1; private javax.swing.JFormattedTextField jFormattedTextField2; private javax.swing.JLabel jLabel1; @@ -1420,7 +1245,6 @@ public class RegistrationPanel extends ControlPanel { private javax.swing.JPanel jPanel2; private javax.swing.JPanel jPanel4; private javax.swing.JPanel jPanel7; - private javax.swing.JSeparator jSeparator10; private javax.swing.JSeparator jSeparator11; private javax.swing.JSeparator jSeparator2; private javax.swing.JSeparator jSeparator5; @@ -1433,10 +1257,8 @@ public class RegistrationPanel extends ControlPanel { private javax.swing.JButton leftTranslationXButton; private javax.swing.JButton leftTranslationYButton; private javax.swing.JButton leftTranslationZButton; - private javax.swing.JRadioButton lowShiftRB; private javax.swing.ButtonGroup precisionGroup; private javax.swing.ButtonGroup primaryRenderModeGroup; - private javax.swing.JButton resetAllButton; private javax.swing.JButton rightRotationXButton; private javax.swing.JButton rightRotationYButton; private javax.swing.JButton rightRotationZButton; @@ -1446,18 +1268,11 @@ public class RegistrationPanel extends ControlPanel { private javax.swing.JLabel rotatXLabel; private javax.swing.JLabel rotatYLabel; private javax.swing.JLabel rotatZLabel; - private javax.swing.JButton rotationButton; private javax.swing.JPanel rotationPanel; - private javax.swing.JFormattedTextField rotationXFTF; - private javax.swing.JFormattedTextField rotationYFTF; - private javax.swing.JFormattedTextField rotationZFTF; - private javax.swing.JButton scaleButton; - private javax.swing.JFormattedTextField scaleFTF; private javax.swing.JButton scaleMinusButton; private javax.swing.JPanel scalePanel; private javax.swing.JButton scalePlusButton; private javax.swing.ButtonGroup secondaryRenerModeGroup; - private javax.swing.JLabel shiftLabel; private javax.swing.JPanel shiftPanel; private javax.swing.JFormattedTextField thersholdFTF; private javax.swing.JButton thersholdUpButton; @@ -1466,10 +1281,6 @@ public class RegistrationPanel extends ControlPanel { private javax.swing.JLabel translXLabel; private javax.swing.JLabel translYLabel; private javax.swing.JLabel translZLabel; - private javax.swing.JButton translationButton; private javax.swing.JPanel translationPanel; - private javax.swing.JFormattedTextField translationXFTF; - private javax.swing.JFormattedTextField translationYFTF; - private javax.swing.JFormattedTextField translationZFTF; // End of variables declaration//GEN-END:variables } diff --git a/GUI/src/main/java/cz/fidentis/analyst/scene/Drawable.java b/GUI/src/main/java/cz/fidentis/analyst/scene/Drawable.java index 90193a7f..3b60dee7 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/scene/Drawable.java +++ b/GUI/src/main/java/cz/fidentis/analyst/scene/Drawable.java @@ -4,7 +4,6 @@ import com.jogamp.opengl.GL; import com.jogamp.opengl.GL2; import java.awt.Color; import java.util.Comparator; -import javax.vecmath.Vector3d; /** * A drawable object, i.e., an object with drawing state and capable to @@ -22,12 +21,6 @@ public abstract class Drawable { private float transparency = 1; // 0 = off, 1 = max private Color highlights = new Color(0, 0, 0, 1); - /* transformation */ - private Vector3d translation = new Vector3d(0, 0, 0); - private Vector3d rotation = new Vector3d(0, 0, 0); - private Vector3d scale = new Vector3d(0, 0, 0); - - /* rendering mode */ /** * Render mode to use, one from {@code GL_FILL}, {@code GL_LINE}, {@code GL_POINT} */ @@ -49,9 +42,6 @@ public abstract class Drawable { this.color = drawable.color; this.highlights = drawable.highlights; this.transparency = drawable.transparency; - this.translation = new Vector3d(drawable.translation); - this.rotation = new Vector3d(drawable.rotation); - this.scale = new Vector3d(drawable.scale); } /** @@ -78,22 +68,7 @@ public abstract class Drawable { */ public void render(GL2 gl) { initRendering(gl); - - gl.glPushMatrix(); - - // rotate - gl.glRotated(getRotation().x, 1, 0, 0); - gl.glRotated(getRotation().y, 0, 1, 0); - gl.glRotated(getRotation().z, 0, 0, 1); - // move - gl.glTranslated(getTranslation().x, getTranslation().y, getTranslation().z); - // scale - gl.glScaled(1 + getScale().x, 1 + getScale().y, 1 + getScale().z); - renderObject(gl); - - gl.glPopMatrix(); - finishRendering(gl); } @@ -175,51 +150,6 @@ public abstract class Drawable { this.highlights = highlights; } - /** - * @return Current translation - */ - public Vector3d getTranslation() { - return translation; - } - - /** - * Sets tranlation - * @param translation Translation - */ - public void setTranslation(Vector3d translation) { - this.translation = translation; - } - - /** - * @return Current rotation - */ - public Vector3d getRotation() { - return rotation; - } - - /** - * Sets rotation - * @param rotation - */ - public void setRotation(Vector3d rotation) { - this.rotation = rotation; - } - - /** - * @return Current scale - */ - public Vector3d getScale() { - return scale; - } - - /** - * Sets scale - * @param scale Scale - */ - public void setScale(Vector3d scale) { - this.scale = scale; - } - /** * @return Value of {@link #renderMode} */ diff --git a/GUI/src/main/java/cz/fidentis/analyst/scene/DrawableCuttingPlane.java b/GUI/src/main/java/cz/fidentis/analyst/scene/DrawableCuttingPlane.java index c4f334e1..2a8dab3b 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/scene/DrawableCuttingPlane.java +++ b/GUI/src/main/java/cz/fidentis/analyst/scene/DrawableCuttingPlane.java @@ -1,10 +1,8 @@ package cz.fidentis.analyst.scene; -import cz.fidentis.analyst.mesh.core.MeshFacet; -import cz.fidentis.analyst.mesh.core.MeshFacetImpl; +import com.jogamp.opengl.GL2; import cz.fidentis.analyst.symmetry.Plane; -import javax.vecmath.Point3d; -import javax.vecmath.Vector3d; +import cz.fidentis.analyst.visitors.mesh.BoundingBox.BBox; /** * Drawable plane with the possibility to shift it along the normal and, @@ -16,6 +14,17 @@ import javax.vecmath.Vector3d; public class DrawableCuttingPlane extends DrawablePlane { private double shift = 0.0; + private boolean showMirror = false; + + /** + * Constructor. + * + * @param plane plane + * @param bbox bounding box used to estimate cutting pale shape (rectangle) + */ + public DrawableCuttingPlane(Plane plane, BBox bbox) { + super(plane, bbox); + } /** * Copy constructor. @@ -28,58 +37,26 @@ public class DrawableCuttingPlane extends DrawablePlane { } /** - * Constructor. - * @param facet Mesh facet of the plane - * @param plane The plane - */ - public DrawableCuttingPlane(MeshFacet facet, Plane plane) { - super(facet, plane); - } - - /** - * Constructor. - * - * @param plane The plane - * @param midPoint A 3D point, which is projected to the plane and used as a center of the rectangular facet - * @param width Width - * @param height Height - * @throws NullPointerException if the {@code plane} or {@code midPoint} are {@code null} - * @throws IllegalArgumentException if {@code width} or {@code height} are <= 0 - */ - public DrawableCuttingPlane(Plane plane, Point3d midPoint, double width, double height) { - super(plane, midPoint, width, height); - } - - /** - * Moves the plane in the plane's normal direction. + * Moves the plane in the plane's normal direction relatively to the original position. * * @param dist Distance. If positive, then the movements is in the direction of the normal vector. * Otherwise, the movement is against the normal direction. */ public void shift(double dist) { - shift += dist; - - Vector3d move = getPlane().getNormal(); - move.scale(dist); - for (int i = 0; i < getFacets().get(0).getNumberOfVertices(); ++i) { - getFacets().get(0).getVertex(i).getPosition().sub(move); - } - - if (isMirrorPlaneShown()) { - move.scale(-1.0); - for (int i = 0; i < getFacets().get(1).getNumberOfVertices(); ++i) { - getFacets().get(1).getVertex(i).getPosition().sub(move); - } - } + this.shift = dist; } - /** - * Returns shifted cutting plane. - * @return shifted cutting plane. - */ @Override public Plane getPlane() { - return new Plane(super.getPlane().getNormal(), super.getPlane().getDistance() + shift); + return super.getPlane().shift(this.shift); + } + + /** + * Returns original non-shifted plane + * @return original non-shifted plane + */ + public Plane getNonShiftedPlane() { + return super.getPlane(); } /** @@ -87,7 +64,7 @@ public class DrawableCuttingPlane extends DrawablePlane { * @return the cutting plane shifted to opposite direction */ public Plane getMirrorPlane() { - return new Plane(super.getPlane().getNormal(), super.getPlane().getDistance() - shift); + return super.getPlane().shift(-this.shift); } /** @@ -95,20 +72,7 @@ public class DrawableCuttingPlane extends DrawablePlane { * @param show Shows if {@code true}, hides otherwise */ public void showMirrorPlane(boolean show) { - if (show) { - if (isMirrorPlaneShown()) { // already shown - return; - } - MeshFacetImpl mirror = new MeshFacetImpl(getModel().getFacets().get(0)); - Vector3d move = new Vector3d(super.getPlane().getNormal()); - move.scale(-2.0 * shift); - for (int i = 0; i < mirror.getNumberOfVertices(); ++i) { - mirror.getVertex(i).getPosition().sub(move); - } - getModel().addFacet(mirror); - } else if (isMirrorPlaneShown()) { - getModel().removeFacet(1); - } + this.showMirror = show; } /** @@ -116,6 +80,15 @@ public class DrawableCuttingPlane extends DrawablePlane { * @return {@code true} if the mirror planes are set to be shown. */ public boolean isMirrorPlaneShown() { - return getModel().getFacets().size() > 1; + return this.showMirror; + //return getModel().getFacets().size() > 1; + } + + @Override + protected void renderObject(GL2 gl) { + renderObject(gl, getPlane().getMesh(getBBox())); + if (showMirror) { + renderObject(gl, getMirrorPlane().getMesh(getBBox())); + } } } diff --git a/GUI/src/main/java/cz/fidentis/analyst/scene/DrawableFeaturePoints.java b/GUI/src/main/java/cz/fidentis/analyst/scene/DrawableFeaturePoints.java index 3018786e..d0d7723c 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/scene/DrawableFeaturePoints.java +++ b/GUI/src/main/java/cz/fidentis/analyst/scene/DrawableFeaturePoints.java @@ -5,7 +5,6 @@ import com.jogamp.opengl.glu.GLU; import com.jogamp.opengl.glu.GLUquadric; import cz.fidentis.analyst.feature.FeaturePoint; import java.awt.Color; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -60,7 +59,8 @@ public class DrawableFeaturePoints extends Drawable { * @param defaultPerimeter Default perimeter */ public DrawableFeaturePoints(List<FeaturePoint> featurePoints, Color defaultColor, double defaultPerimeter) { - this.featurePoints = new ArrayList<>(featurePoints); + //this.featurePoints = new ArrayList<>(featurePoints); + this.featurePoints = featurePoints; this.defaultPerimeter = defaultPerimeter; setColor(defaultColor); } diff --git a/GUI/src/main/java/cz/fidentis/analyst/scene/DrawablePlane.java b/GUI/src/main/java/cz/fidentis/analyst/scene/DrawablePlane.java index 7999836c..0796f9d7 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/scene/DrawablePlane.java +++ b/GUI/src/main/java/cz/fidentis/analyst/scene/DrawablePlane.java @@ -1,20 +1,35 @@ package cz.fidentis.analyst.scene; -import cz.fidentis.analyst.mesh.core.MeshFacet; -import cz.fidentis.analyst.mesh.core.MeshModel; +import com.jogamp.opengl.GL2; +import cz.fidentis.analyst.mesh.core.MeshRectangleFacet; import cz.fidentis.analyst.symmetry.Plane; +import cz.fidentis.analyst.visitors.mesh.BoundingBox.BBox; import javax.vecmath.Point3d; /** * A plane to be shown as a rectangular mesh facet. + * The rectangular shape is generated at run-time from the associated plane. * * @author Radek Oslejsek * @author Dominik Racek */ -public class DrawablePlane extends DrawableMesh { +public class DrawablePlane extends Drawable { - private final Plane plane; + private Plane plane; + private final BBox bbox; + + /** + * Constructor. + * + * @param plane plane + * @param bbox bounding box used to estimate cutting pale shape (rectangle) + */ + public DrawablePlane(Plane plane, BBox bbox) { + this.plane = plane; + this.bbox = bbox; + } + /** * Copy constructor. * @param drPlane Original plane @@ -23,51 +38,35 @@ public class DrawablePlane extends DrawableMesh { public DrawablePlane(DrawablePlane drPlane) { super(drPlane); this.plane = new Plane(drPlane.getPlane()); + this.bbox = drPlane.bbox; } - /** - * Constructor. - * @param model Mesh model of the plane - * @param plane The plane - */ - public DrawablePlane(MeshModel model, Plane plane) { - super(model); - if (plane == null) { - throw new IllegalArgumentException("plane"); - } + public Plane getPlane() { + return plane; + } + + protected void setPlane(Plane plane) { this.plane = plane; } + + @Override + protected void renderObject(GL2 gl) { + renderObject(gl, getPlane().getMesh(bbox)); + } - /** - * Constructor. - * @param facet Mesh facet of the plane - * @param plane The plane - */ - public DrawablePlane(MeshFacet facet, Plane plane) { - super(facet); - if (plane == null) { - throw new IllegalArgumentException("plane"); + protected void renderObject(GL2 gl, MeshRectangleFacet facet) { + gl.glBegin(GL2.GL_TRIANGLES); //vertices are rendered as triangles + // get the normal and tex coords indicies for face i + for (int v = 0; v < facet.getCornerTable().getSize(); v++) { + Point3d vert = facet.getVertices().get(facet.getCornerTable().getRow(v).getVertexIndex()).getPosition(); + if (v == 0) { + } + gl.glVertex3d(vert.x, vert.y, vert.z); } - this.plane = new Plane(plane); + gl.glEnd(); } - /** - * Constructor. - * - * @param plane The plane - * @param midPoint A 3D point, which is projected to the plane and used as a center of the rectangular facet - * @param width Width - * @param height Height - * @throws NullPointerException if the {@code plane} or {@code midPoint} are {@code null} - * @throws IllegalArgumentException if {@code width} or {@code height} are <= 0 - */ - public DrawablePlane(Plane plane, Point3d midPoint, double width, double height) { - super(plane.getMesh(midPoint, width, height)); - this.plane = new Plane(plane); + protected BBox getBBox() { + return this.bbox; } - - public Plane getPlane() { - return plane; - } - } diff --git a/GUI/src/main/java/cz/fidentis/analyst/scene/Scene.java b/GUI/src/main/java/cz/fidentis/analyst/scene/Scene.java index 28613b1a..22fbdc2f 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/scene/Scene.java +++ b/GUI/src/main/java/cz/fidentis/analyst/scene/Scene.java @@ -3,12 +3,13 @@ package cz.fidentis.analyst.scene; import cz.fidentis.analyst.face.HumanFace; import java.awt.Color; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** * A simple 3D scene. The structure is as follows: * Drawable components of a single human face (mesh, symmetry plane, feature points. etc.) - * are always stored at the same index. One face can be labeled as "primary", one as "secondary". + * are always stored at the same index (slot). One face can be labeled as "primary", one as "secondary". * Other faces have no label. * Then, there is a special list of so-called other drawables. The size of this list can differ. * @@ -170,7 +171,6 @@ public class Scene { } drawableFaces.set(slot, new DrawableFace(face)); - return true; } @@ -184,7 +184,7 @@ public class Scene { public boolean setDrawableFeaturePoints(int slot, HumanFace face) { if (slot < 0 || slot >= Scene.MAX_FACES_IN_SCENE) { return false; - } else if (face == null) { // remove + } else if (face == null || face.getFeaturePoints() == null) { // remove if (slot >= getArraySize()){ return false; } else { @@ -195,13 +195,7 @@ public class Scene { return false; } - drawableFeaturePoints.set( - slot, - (face.getFeaturePoints() != null) - ? new DrawableFeaturePoints(face.getFeaturePoints()) - : null - ); - + drawableFeaturePoints.set(slot, new DrawableFeaturePoints(face.getFeaturePoints())); return true; } @@ -215,7 +209,7 @@ public class Scene { public boolean setDrawableSymmetryPlane(int slot, HumanFace face) { if (slot < 0 || slot >= Scene.MAX_FACES_IN_SCENE) { return false; - } else if (face == null) { // remove + } else if (face == null || face.getSymmetryPlane() == null) { // remove if (slot >= getArraySize()){ return false; } else { @@ -224,14 +218,13 @@ public class Scene { } } else if (!prepareArrays(slot, face)) { return false; - } + } - drawableSymmetryPlanes.set( - slot, - (face.getSymmetryPlane() != null && face.getSymmetryPlaneFacet() != null) - ? new DrawablePlane(face.getSymmetryPlaneFacet(), face.getSymmetryPlane()) - : null - ); + if (drawableSymmetryPlanes.get(slot) == null) { // add new + drawableSymmetryPlanes.set(slot, new DrawablePlane(face.getSymmetryPlane(), face.getBoundingBox())); + } else { // replace existing + drawableSymmetryPlanes.get(slot).setPlane(face.getSymmetryPlane()); + } return true; } @@ -376,6 +369,10 @@ public class Scene { return ret; } + public List<DrawableFace> getDrawableFaces() { + return Collections.unmodifiableList(this.drawableFaces); + } + protected Color getColorOfFeaturePoints(Color origColor) { return new Color( Math.min(255, origColor.getRed() + 40), diff --git a/GUI/src/main/java/cz/fidentis/analyst/scene/SceneRenderer.java b/GUI/src/main/java/cz/fidentis/analyst/scene/SceneRenderer.java index cbd6d37c..2e45de1e 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/scene/SceneRenderer.java +++ b/GUI/src/main/java/cz/fidentis/analyst/scene/SceneRenderer.java @@ -127,6 +127,19 @@ public class SceneRenderer { } } + // draw coordinates: + /* + gl.glMaterialfv(GL2.GL_FRONT_AND_BACK, GL2.GL_DIFFUSE, new float[]{255,255,255,255}, 0); + gl.glBegin(GL2.GL_LINES); //vertices are rendered as triangles + gl.glVertex3d(-30, 0, 0); + gl.glVertex3d(30, 0, 0); + gl.glVertex3d(0, -30, 0); + gl.glVertex3d(0, 30, 0); + gl.glVertex3d(0, 0, -30); + gl.glVertex3d(0, 0, 30); + gl.glEnd(); + */ + gl.glFlush(); //out.printDuration("Scene rendering"); diff --git a/GUI/src/main/java/cz/fidentis/analyst/symmetry/ProfilesAction.java b/GUI/src/main/java/cz/fidentis/analyst/symmetry/ProfilesAction.java index 6eab131f..50c730c0 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/symmetry/ProfilesAction.java +++ b/GUI/src/main/java/cz/fidentis/analyst/symmetry/ProfilesAction.java @@ -6,9 +6,7 @@ import cz.fidentis.analyst.core.ControlPanelAction; import cz.fidentis.analyst.face.HumanFace; import cz.fidentis.analyst.face.events.HumanFaceEvent; import cz.fidentis.analyst.face.events.HumanFaceListener; -import cz.fidentis.analyst.face.events.SymmetryPlaneChangedEvent; -import cz.fidentis.analyst.mesh.core.MeshFacet; -import cz.fidentis.analyst.mesh.core.MeshFacetImpl; +import cz.fidentis.analyst.face.events.HumanFaceTransformedEvent; import cz.fidentis.analyst.scene.DrawableCuttingPlane; import cz.fidentis.analyst.visitors.mesh.BoundingBox.BBox; import cz.fidentis.analyst.visitors.mesh.CrossSectionZigZag; @@ -24,7 +22,6 @@ import java.io.PrintWriter; import java.util.List; import java.util.stream.Collectors; import javax.swing.JComboBox; -import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.vecmath.Vector3d; @@ -52,11 +49,10 @@ public class ProfilesAction extends ControlPanelAction implements HumanFaceListe private CrossSectionCurve primaryMirrorCurve; private CrossSectionCurve secondaryMirrorCurve; - private double lastSliderValue = 0.5; private boolean cuttingPlaneFromSymmetry; - private int priCuttingPlaneIndex = -1; - private int secCuttingPlaneIndex = -1; + private int priCuttingPlaneSlot = -1; + private int secCuttingPlaneSlot = -1; /** * Constructor. @@ -68,9 +64,9 @@ public class ProfilesAction extends ControlPanelAction implements HumanFaceListe super(canvas, topControlPanel); this.topControlPanel = topControlPanel; - computeOrthogonalCuttingPlanes(false); + computeVerticalCuttingPlanes(); // compute default cutting planes - //recomputePrimaryProfile(); + //recomputePrimaryProfile(); if (getSecondaryDrawableFace() != null) { //recomputeSecondaryProfile(); @@ -121,18 +117,17 @@ public class ProfilesAction extends ControlPanelAction implements HumanFaceListe } } break; - case ProfilesPanel.ACTION_OFFSET_CUTTING_PLANE: - double auxVal = ((ComboSliderDouble) ae.getSource()).getValue(); - double value = auxVal - lastSliderValue; - double multiplier = -150; - - setCuttingPlaneOffset(priCuttingPlaneIndex, multiplier * value); - + case ProfilesPanel.ACTION_SHIFT_CUTTING_PLANE: + double sliderVal = ((ComboSliderDouble) ae.getSource()).getValue(); + double xExtent = maxExtentX(); + double shift = (sliderVal >= 0.5) + ? ( (sliderVal - 0.5) / 0.5) * xExtent // move right + : -((0.5 - sliderVal) / 0.5) * xExtent; // move left + getDrawableCuttingPlane(priCuttingPlaneSlot).shift(shift); if (getSecondaryDrawableFace() != null) { - setCuttingPlaneOffset(secCuttingPlaneIndex, multiplier * value); + getDrawableCuttingPlane(secCuttingPlaneSlot).shift(shift); } recomputeProfiles(); - lastSliderValue = auxVal; break; case ProfilesPanel.ACTION_CHANGE_CUTTING_PLANE: String option = (String)((JComboBox) ae.getSource()).getSelectedItem(); @@ -140,9 +135,9 @@ public class ProfilesAction extends ControlPanelAction implements HumanFaceListe if (option.equals(ProfilesPanel.OPTION_SYMMETRY_PLANE)) { if (getScene().getDrawableSymmetryPlane(0) == null) { JOptionPane.showMessageDialog( - new JFrame(), + controlPanel, "Compute a symmetry plane at the Symmetry tab first.", - "Dialog", + "No symmetry plane", JOptionPane.INFORMATION_MESSAGE ); controlPanel.setDefaultPlaneSelection(); @@ -152,7 +147,7 @@ public class ProfilesAction extends ControlPanelAction implements HumanFaceListe computeCuttingPlanesFromSymmetry(); } else if (option.equals(ProfilesPanel.OPTION_VERTICAL_PLANE)) { controlPanel.resetSliderSilently(); - computeOrthogonalCuttingPlanes(false); + computeVerticalCuttingPlanes(); } showCuttingPlanes(true, controlPanel.isMirrorCutsChecked()); @@ -170,10 +165,19 @@ public class ProfilesAction extends ControlPanelAction implements HumanFaceListe @Override public void acceptEvent(HumanFaceEvent event) { - if (event instanceof SymmetryPlaneChangedEvent && cuttingPlaneFromSymmetry) { + // If some human face is transformed, then cutting planes have to updated. + if (event instanceof HumanFaceTransformedEvent) { controlPanel.resetSliderSilently(); - computeCuttingPlanesFromSymmetry(); // recompute cutting planes - //getCanvas().getScene().showCuttingPlanes(false, controlPanel.isMirrorCutsChecked()); + if (cuttingPlaneFromSymmetry) { + computeCuttingPlanesFromSymmetry(); // recompute cutting planes + //getCanvas().getScene().showCuttingPlanes(false, controlPanel.isMirrorCutsChecked()); + } else { + computeVerticalCuttingPlanes(); + } + getDrawableCuttingPlane(priCuttingPlaneSlot).show(false); // will be shown on the panel focus + if (getDrawableCuttingPlane(secCuttingPlaneSlot) != null) { + getDrawableCuttingPlane(secCuttingPlaneSlot).show(false); + } } } @@ -214,27 +218,27 @@ public class ProfilesAction extends ControlPanelAction implements HumanFaceListe System.out.println("ERROR writing to a file: " + ex); } } - + private void recomputePrimaryProfile() { //Main profile - CrossSectionZigZag cs = new CrossSectionZigZag(getDrawableCuttingPlane(priCuttingPlaneIndex).getPlane()); + CrossSectionZigZag cs = new CrossSectionZigZag(getDrawableCuttingPlane(priCuttingPlaneSlot).getPlane()); getPrimaryDrawableFace().getModel().compute(cs); this.primaryCurve = cs.getCrossSectionCurve(); //Mirror profile - CrossSectionZigZag mcs = new CrossSectionZigZag(getDrawableCuttingPlane(priCuttingPlaneIndex).getMirrorPlane()); + CrossSectionZigZag mcs = new CrossSectionZigZag(getDrawableCuttingPlane(priCuttingPlaneSlot).getMirrorPlane()); getPrimaryDrawableFace().getModel().compute(mcs); this.primaryMirrorCurve = mcs.getCrossSectionCurve(); } private void recomputeSecondaryProfile() { //Main profile - CrossSectionZigZag cs = new CrossSectionZigZag(getDrawableCuttingPlane(secCuttingPlaneIndex).getPlane()); + CrossSectionZigZag cs = new CrossSectionZigZag(getDrawableCuttingPlane(secCuttingPlaneSlot).getPlane()); getSecondaryDrawableFace().getModel().compute(cs); this.secondaryCurve = cs.getCrossSectionCurve(); //Mirror profile - CrossSectionZigZag mcs = new CrossSectionZigZag(getDrawableCuttingPlane(secCuttingPlaneIndex).getMirrorPlane()); + CrossSectionZigZag mcs = new CrossSectionZigZag(getDrawableCuttingPlane(secCuttingPlaneSlot).getMirrorPlane()); getSecondaryDrawableFace().getModel().compute(mcs); this.secondaryMirrorCurve = mcs.getCrossSectionCurve(); } @@ -244,11 +248,11 @@ public class ProfilesAction extends ControlPanelAction implements HumanFaceListe projectCloseFeaturePoints( primaryCurve, getCanvas().getPrimaryFace(), - getDrawableCuttingPlane(priCuttingPlaneIndex).getPlane()); + getDrawableCuttingPlane(priCuttingPlaneSlot).getPlane()); projectCloseFeaturePoints( primaryMirrorCurve, getCanvas().getPrimaryFace(), - getDrawableCuttingPlane(priCuttingPlaneIndex).getPlane()); + getDrawableCuttingPlane(priCuttingPlaneSlot).getPlane()); controlPanel.getProfileRenderingPanel().setPrimarySegments(this.primaryCurve); controlPanel.getProfileRenderingPanel().setPrimaryMirrorSegments(this.primaryMirrorCurve); @@ -257,94 +261,73 @@ public class ProfilesAction extends ControlPanelAction implements HumanFaceListe projectCloseFeaturePoints( secondaryCurve, getCanvas().getSecondaryFace(), - getDrawableCuttingPlane(secCuttingPlaneIndex).getPlane()); + getDrawableCuttingPlane(secCuttingPlaneSlot).getPlane()); projectCloseFeaturePoints( secondaryMirrorCurve, getCanvas().getSecondaryFace(), - getDrawableCuttingPlane(secCuttingPlaneIndex).getPlane()); + getDrawableCuttingPlane(secCuttingPlaneSlot).getPlane()); controlPanel.getProfileRenderingPanel().setSecondarySegments(this.secondaryCurve); controlPanel.getProfileRenderingPanel().setSecondaryMirrorSegments(this.secondaryMirrorCurve); } } - private void setCuttingPlaneOffset(int index, double value) { - //Move cutting planes left and mirror planes right - //If normal is negative, need to negate value - if (getDrawableCuttingPlane(index).getPlane().getNormal().x < 0) { - getDrawableCuttingPlane(index).shift(-value); - } else { - getDrawableCuttingPlane(index).shift(value); - } - } - - /** - * Creates vertical or horizontal cutting and mirror planes from bounding box. - * - * @param horizontal - */ - protected void computeOrthogonalCuttingPlanes(boolean horizontal) { - DrawableCuttingPlane drPlane = getDrawableOrthogonalPlane(horizontal); - if (priCuttingPlaneIndex == -1) { - priCuttingPlaneIndex = getCanvas().getScene().getFreeSlotForOtherDrawables(); + protected void computeVerticalCuttingPlanes() { + BBox bbox = getCanvas().getPrimaryFace().getBoundingBox(); + Plane plane = new Plane(new Vector3d(1,0,0), bbox.getMidPoint().x); + DrawableCuttingPlane cuttingPlane = new DrawableCuttingPlane(plane, bbox); + cuttingPlane.setTransparency(0.5f); + if (priCuttingPlaneSlot == -1) { + priCuttingPlaneSlot = getCanvas().getScene().getFreeSlotForOtherDrawables(); } - getCanvas().getScene().setOtherDrawable(priCuttingPlaneIndex, drPlane); + getCanvas().getScene().setOtherDrawable(priCuttingPlaneSlot, cuttingPlane); if (getSecondaryDrawableFace() != null) { - if (secCuttingPlaneIndex == -1) { - secCuttingPlaneIndex = getCanvas().getScene().getFreeSlotForOtherDrawables(); + bbox = getCanvas().getSecondaryFace().getBoundingBox(); + plane = new Plane(new Vector3d(1,0,0), bbox.getMidPoint().x); + cuttingPlane = new DrawableCuttingPlane(plane, bbox); + cuttingPlane.setTransparency(0.5f); + if (secCuttingPlaneSlot == -1) { + secCuttingPlaneSlot = getCanvas().getScene().getFreeSlotForOtherDrawables(); } - getCanvas().getScene().setOtherDrawable(secCuttingPlaneIndex, new DrawableCuttingPlane(drPlane)); + getCanvas().getScene().setOtherDrawable(secCuttingPlaneSlot, new DrawableCuttingPlane(cuttingPlane)); } + cuttingPlaneFromSymmetry = false; } - protected DrawableCuttingPlane getDrawableOrthogonalPlane(boolean horizontal) { - BBox bbox = getCanvas().getPrimaryFace().getBoundingBox(); // use BBox of the first face for size and position estimation - Plane plane = new Plane( - (horizontal) ? new Vector3d(0,1,0) : new Vector3d(-1,0,0), - (horizontal) ? bbox.getMidPoint().y : bbox.getMidPoint().x - ); - DrawableCuttingPlane cuttingPlane = new DrawableCuttingPlane( - plane, - bbox.getMidPoint(), - bbox.getDiagonalLength(), - bbox.getDiagonalLength() - ); - cuttingPlane.setTransparency(0.5f); - return cuttingPlane; - } - - /** - * Copy rectangle from the symmetry plane. - * - * @param faceIndex - * @return - */ - protected DrawableCuttingPlane getCuttingPlaneFromSymmetry(int faceIndex) { - MeshFacet symmetryFacet = getCanvas().getHumanFace(faceIndex).getSymmetryPlaneFacet(); - Plane symmetryPlane = getCanvas().getHumanFace(faceIndex).getSymmetryPlane(); - return new DrawableCuttingPlane(new MeshFacetImpl(symmetryFacet), new Plane(symmetryPlane)); - } - /** * Creates cutting and mirror planes by copying the rectangle from the symmetry plane. */ protected void computeCuttingPlanesFromSymmetry() { - DrawableCuttingPlane cuttingPlane = getCuttingPlaneFromSymmetry(getCanvas().getPrimaryFaceIndex()); + Plane symmetryPlane = getCanvas().getPrimaryFace().getSymmetryPlane(); + if (symmetryPlane.getNormal().x < 0) { // orient to the right-hand direction + symmetryPlane = symmetryPlane.flip(); + } + DrawableCuttingPlane cuttingPlane = new DrawableCuttingPlane( + new Plane(symmetryPlane), + getCanvas().getPrimaryFace().getBoundingBox() + ); cuttingPlane.setTransparency(0.5f); - if (priCuttingPlaneIndex == -1) { - priCuttingPlaneIndex = getCanvas().getScene().getFreeSlotForOtherDrawables(); + if (priCuttingPlaneSlot == -1) { + priCuttingPlaneSlot = getCanvas().getScene().getFreeSlotForOtherDrawables(); } - getCanvas().getScene().setOtherDrawable(priCuttingPlaneIndex, cuttingPlane); + getCanvas().getScene().setOtherDrawable(priCuttingPlaneSlot, cuttingPlane); recomputePrimaryProfile(); - if (getSecondaryDrawableFace() != null) { - cuttingPlane = getCuttingPlaneFromSymmetry(getCanvas().getSecondaryFaceIndex()); + if (getCanvas().getSecondaryFace() != null) { + symmetryPlane = getCanvas().getSecondaryFace().getSymmetryPlane(); + if (symmetryPlane.getNormal().x < 0) { // orient to the right-hand direction + symmetryPlane = symmetryPlane.flip(); + } + cuttingPlane = new DrawableCuttingPlane( + new Plane(symmetryPlane), + getCanvas().getSecondaryFace().getBoundingBox() + ); cuttingPlane.setTransparency(0.5f); - if (secCuttingPlaneIndex == -1) { - secCuttingPlaneIndex = getCanvas().getScene().getFreeSlotForOtherDrawables(); + if (secCuttingPlaneSlot == -1) { + secCuttingPlaneSlot = getCanvas().getScene().getFreeSlotForOtherDrawables(); } - getCanvas().getScene().setOtherDrawable(secCuttingPlaneIndex, cuttingPlane); + getCanvas().getScene().setOtherDrawable(secCuttingPlaneSlot, cuttingPlane); recomputeSecondaryProfile(); } cuttingPlaneFromSymmetry = true; @@ -367,13 +350,34 @@ public class ProfilesAction extends ControlPanelAction implements HumanFaceListe } protected void showCuttingPlanes(boolean show, boolean showMirrors) { - if (getDrawableCuttingPlane(priCuttingPlaneIndex) != null) { - getDrawableCuttingPlane(priCuttingPlaneIndex).show(show); - getDrawableCuttingPlane(priCuttingPlaneIndex).showMirrorPlane(showMirrors); + if (getDrawableCuttingPlane(priCuttingPlaneSlot) != null) { + getDrawableCuttingPlane(priCuttingPlaneSlot).show(show); + getDrawableCuttingPlane(priCuttingPlaneSlot).showMirrorPlane(showMirrors); } - if (getDrawableCuttingPlane(secCuttingPlaneIndex) != null) { - getDrawableCuttingPlane(secCuttingPlaneIndex).show(show); - getDrawableCuttingPlane(secCuttingPlaneIndex).showMirrorPlane(showMirrors); + if (getDrawableCuttingPlane(secCuttingPlaneSlot) != null) { + getDrawableCuttingPlane(secCuttingPlaneSlot).show(show); + getDrawableCuttingPlane(secCuttingPlaneSlot).showMirrorPlane(showMirrors); } } + + protected double maxExtentX() { + double max = Double.NEGATIVE_INFINITY; + + double planeX = getDrawableCuttingPlane(priCuttingPlaneSlot).getNonShiftedPlane().getPlanePoint().x; + double bboxX = getCanvas().getPrimaryFace().getBoundingBox().getMaxPoint().x; + max = Math.max(max, Math.abs(bboxX - planeX)); + bboxX = getCanvas().getPrimaryFace().getBoundingBox().getMinPoint().x; + max = Math.max(max, Math.abs(bboxX - planeX)); + + if (getCanvas().getSecondaryFace() != null) { + planeX = getDrawableCuttingPlane(secCuttingPlaneSlot).getNonShiftedPlane().getPlanePoint().x; + bboxX = getCanvas().getSecondaryFace().getBoundingBox().getMaxPoint().x; + max = Math.max(max, Math.abs(bboxX - planeX)); + bboxX = getCanvas().getSecondaryFace().getBoundingBox().getMinPoint().x; + max = Math.max(max, Math.abs(bboxX - planeX)); + } + + return max; + } + } diff --git a/GUI/src/main/java/cz/fidentis/analyst/symmetry/ProfilesPanel.form b/GUI/src/main/java/cz/fidentis/analyst/symmetry/ProfilesPanel.form index affc6fe6..652258b3 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/symmetry/ProfilesPanel.form +++ b/GUI/src/main/java/cz/fidentis/analyst/symmetry/ProfilesPanel.form @@ -20,7 +20,7 @@ <EmptySpace max="-2" attributes="0"/> <Group type="103" groupAlignment="1" max="-2" attributes="0"> <Component id="jPanel1" max="32767" attributes="0"/> - <Component id="polylinePanel1" max="32767" attributes="0"/> + <Component id="polylinePanel1" pref="500" max="32767" attributes="0"/> </Group> <EmptySpace max="32767" attributes="0"/> </Group> @@ -132,7 +132,7 @@ </DimensionLayout> <DimensionLayout dim="1"> <Group type="103" groupAlignment="0" attributes="0"> - <EmptySpace min="0" pref="498" max="32767" attributes="0"/> + <EmptySpace min="0" pref="398" max="32767" attributes="0"/> </Group> </DimensionLayout> </Layout> diff --git a/GUI/src/main/java/cz/fidentis/analyst/symmetry/ProfilesPanel.java b/GUI/src/main/java/cz/fidentis/analyst/symmetry/ProfilesPanel.java index e9ac0b55..347c46ea 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/symmetry/ProfilesPanel.java +++ b/GUI/src/main/java/cz/fidentis/analyst/symmetry/ProfilesPanel.java @@ -21,7 +21,7 @@ public class ProfilesPanel extends ControlPanel { * Handled actions */ public static final String ACTION_COMMAND_EXPORT = "export"; - public static final String ACTION_OFFSET_CUTTING_PLANE = "offset plane"; + public static final String ACTION_SHIFT_CUTTING_PLANE = "shift plane"; public static final String ACTION_MIRROR_CUTS = "mirror-cuts"; public static final String ACTION_CHANGE_CUTTING_PLANE = "change cutting plane"; @@ -60,7 +60,7 @@ public class ProfilesPanel extends ControlPanel { comboSliderDouble1.setRange(0, 1, 2); comboSliderDouble1.setValue(0.5); - comboSliderDouble1.addListener(createListener(action, ACTION_OFFSET_CUTTING_PLANE)); + comboSliderDouble1.addListener(createListener(action, ACTION_SHIFT_CUTTING_PLANE)); jCheckBox1.addActionListener(createListener(action, ACTION_MIRROR_CUTS)); jButton1.addActionListener(createListener(action, ACTION_COMMAND_EXPORT)); diff --git a/GUI/src/main/java/cz/fidentis/analyst/symmetry/SymmetryAction.java b/GUI/src/main/java/cz/fidentis/analyst/symmetry/SymmetryAction.java index a044a14b..c6a46ba1 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/symmetry/SymmetryAction.java +++ b/GUI/src/main/java/cz/fidentis/analyst/symmetry/SymmetryAction.java @@ -6,7 +6,6 @@ import cz.fidentis.analyst.core.ControlPanelAction; import cz.fidentis.analyst.face.HumanFace; import cz.fidentis.analyst.face.events.HumanFaceEvent; import cz.fidentis.analyst.face.events.HumanFaceListener; -import cz.fidentis.analyst.face.events.MeshChangedEvent; import cz.fidentis.analyst.face.events.SymmetryPlaneChangedEvent; import cz.fidentis.analyst.feature.FeaturePoint; import cz.fidentis.analyst.mesh.core.MeshFacet; @@ -54,6 +53,7 @@ public class SymmetryAction extends ControlPanelAction implements HumanFaceListe // Place control panel to the topControlPanel this.topControlPanel.addTab(controlPanel.getName(), controlPanel.getIcon(), controlPanel); + /* this.topControlPanel.addChangeListener(e -> { // If the symmetry panel is focused... if (((JTabbedPane) e.getSource()).getSelectedComponent() instanceof SymmetryPanel) { @@ -65,6 +65,7 @@ public class SymmetryAction extends ControlPanelAction implements HumanFaceListe getCanvas().getScene().showSymmetryPlane(getCanvas().getScene().getSecondaryFaceSlot(), false); } }); + */ // Be informed about changes in faces perfomed by other GUI elements getPrimaryDrawableFace().getHumanFace().registerListener(this); @@ -96,6 +97,7 @@ public class SymmetryAction extends ControlPanelAction implements HumanFaceListe @Override public void acceptEvent(HumanFaceEvent event) { + /* symmetry plane is transformed together with the mesh if (event instanceof MeshChangedEvent) { // recompute symmetry plane, if necessary switch (sPlane) { case 0: // none @@ -113,6 +115,7 @@ public class SymmetryAction extends ControlPanelAction implements HumanFaceListe default: } } + */ if (event instanceof SymmetryPlaneChangedEvent) { updatePrecision(event.getFace()); } @@ -127,6 +130,7 @@ public class SymmetryAction extends ControlPanelAction implements HumanFaceListe face.setSymmetryPlane(estimator.getSymmetryPlane()); log.printDuration("Symmetry plane estimation from mesh for " + face.getShortName()); setDrawablePlane(face, index); + face.announceEvent(new SymmetryPlaneChangedEvent(face, "", this)); } } @@ -169,6 +173,7 @@ public class SymmetryAction extends ControlPanelAction implements HumanFaceListe face.setSymmetryPlane(new Plane(planes)); // creates an average plane setDrawablePlane(face, index); + face.announceEvent(new SymmetryPlaneChangedEvent(face, "", this)); } protected void setDrawablePlane(HumanFace face, int index) { @@ -186,7 +191,7 @@ public class SymmetryAction extends ControlPanelAction implements HumanFaceListe MeshModel clone = new MeshModel(face.getMeshModel()); - for (MeshFacet facet: clone.getFacets()) { + for (MeshFacet facet: clone.getFacets()) { // invert mesh facet.getVertices().parallelStream().forEach(v -> { v.getPosition().set(face.getSymmetryPlane().reflectOverPlane(v.getPosition())); }); @@ -196,9 +201,9 @@ public class SymmetryAction extends ControlPanelAction implements HumanFaceListe HausdorffDistance visitor = new HausdorffDistance( face.getKdTree(), Strategy.POINT_TO_POINT_DISTANCE_ONLY, - false, - true, - false + false, // relative + true, // parallel + false // crop ); clone.compute(visitor); controlPanel.updateHausdorffDistanceStats(visitor.getStats(), getCanvas().getHumanFace(0) == face); diff --git a/GUI/src/main/java/cz/fidentis/analyst/tests/BatchSimilarityApproxHausdorff.java b/GUI/src/main/java/cz/fidentis/analyst/tests/BatchSimilarityApproxHausdorff.java deleted file mode 100644 index 7cc8988b..00000000 --- a/GUI/src/main/java/cz/fidentis/analyst/tests/BatchSimilarityApproxHausdorff.java +++ /dev/null @@ -1,145 +0,0 @@ -package cz.fidentis.analyst.tests; - -import cz.fidentis.analyst.face.HumanFace; -import cz.fidentis.analyst.icp.IcpTransformer; -import cz.fidentis.analyst.icp.NoUndersampling; -import cz.fidentis.analyst.visitors.mesh.HausdorffDistance; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.DoubleSummaryStatistics; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * A class for testing the efficiency of batch processing algorithms. - * The goal of this tool is to measure time and precision of - * approximated Hausdorff distance (HD). - * - * - First, HD of the first face (primary face) with other faces is computed - * - Each secondary face is superimposed using ICP - * - Then it is stored in k-d tree - * - Relative (!) HD from the primary to secondary face is computed and stored. - * The order cannot be reverted because we need HD matrices of the same size. - * - TODO: First face should be replaced with average face - * - Then, the mutual distances are approximated: - * - For each pair of faces A and B, the distance is computed as the difference - * between distances A-P and B-P, where P is the primary face. - * - * @author Radek Oslejsek - */ -public class BatchSimilarityApproxHausdorff { - private static final String DATA_DIR = "../../analyst-data-antropologie/_ECA"; - private static final String OUTPUT_FILE = "../../SIMILARITY_APPROX_HAUSDORFF.csv"; - - /** - * Main method - * @param args Input arguments - * @throws IOException on IO error - */ - public static void main(String[] args) throws IOException, ClassNotFoundException, Exception { - List<Path> faces = Files.list(new File(DATA_DIR).toPath()) - .filter(f -> f.toString().endsWith(".obj")) - .sorted() - .limit(100) - .collect(Collectors.toList()); - - - Map<Integer, List<Double>> distances = new HashMap<>(); - - for (int i = 1; i < faces.size(); i++) { - HumanFace priFace = new HumanFace(faces.get(0).toFile()); - HumanFace secFace = new HumanFace(faces.get(i).toFile()); - - secFace.computeKdTree(false); - - System.out.println(i + "/" + (faces.size()-1) + " (" + priFace.getShortName() + "/" + secFace.getShortName() + ")"); - - IcpTransformer icp = new IcpTransformer(priFace.getMeshModel(), 50, true, 0.3, new NoUndersampling()); - secFace.getMeshModel().compute(icp, true); - - HausdorffDistance hd = new HausdorffDistance(secFace.getKdTree(), HausdorffDistance.Strategy.POINT_TO_POINT, true, true, true); - priFace.getMeshModel().compute(hd, true); - - //System.out.println(hd.getDistances().get(priFace.getMeshModel().getFacets().get(0)).size()); - distances.put(i, hd.getDistances().get(priFace.getMeshModel().getFacets().get(0))); - } - - BufferedWriter bfw = new BufferedWriter(new FileWriter(OUTPUT_FILE)); - bfw.write("PRI FACE;SEC FACE;MIN;MAX;AVG"); - bfw.newLine(); - - for (int i = 0; i < faces.size(); i++) { - for (int j = 0; j < faces.size(); j++) { - if (i == j) { - continue; - } - - DoubleSummaryStatistics stats; - if (i == 0 || j == 0) { - stats = getStats(distances.get((i == 0) ? j : i)); - } else { - stats = getStats(distances.get(i), distances.get(j)); - } - - bfw.write(getShortName(faces.get(i).toFile())+";"); - bfw.write(getShortName(faces.get(j).toFile())+";"); - if (stats != null) { - bfw.write(String.format("%.20f", stats.getMin())+";"); - bfw.write(String.format("%.20f", stats.getMax())+";"); - bfw.write(String.format("%.20f", stats.getAverage())+""); - bfw.newLine(); - } else { - bfw.write(String.format("%.20f", Double.NaN)+";"); - bfw.write(String.format("%.20f", Double.NaN)+";"); - bfw.write(String.format("%.20f", Double.NaN)+""); - bfw.newLine(); - } - bfw.flush(); - } - } - - bfw.close(); - - } - - private static DoubleSummaryStatistics getStats(List<Double> d1, List<Double> d2) { - if (d1.size() != d2.size()) { - return null; - } - - List<Double> ret = new ArrayList<>(d1.size()); - for (int i = 0; i < d1.size(); i++) { - ret.add(Math.abs(d2.get(i) - d1.get(i))); - } - - return ret.stream() - .mapToDouble(Double::doubleValue) - .summaryStatistics(); - } - - private static DoubleSummaryStatistics getStats(List<Double> d1) { - List<Double> ret = new ArrayList<>(d1.size()); - for (int i = 0; i < d1.size(); i++) { - ret.add(Math.abs(d1.get(i))); - } - - return d1.stream() - .mapToDouble(v -> Math.abs(v)) - .summaryStatistics(); - } - - private static String getShortName(File file) throws IOException { - String id = file.getCanonicalPath(); - String name = id.substring(0, id.lastIndexOf('.')); // remove extention - name = name.substring(id.lastIndexOf('/')+1, name.length()); - name = name.substring(name.lastIndexOf(File.separatorChar) + 1, name.length()); - return name; - } -} diff --git a/GUI/src/main/java/cz/fidentis/analyst/tests/BatchSimilarityGroundTruth.java b/GUI/src/main/java/cz/fidentis/analyst/tests/BatchSimilarityGroundTruth.java index d6c43c62..f8dd484f 100644 --- a/GUI/src/main/java/cz/fidentis/analyst/tests/BatchSimilarityGroundTruth.java +++ b/GUI/src/main/java/cz/fidentis/analyst/tests/BatchSimilarityGroundTruth.java @@ -1,10 +1,8 @@ package cz.fidentis.analyst.tests; import cz.fidentis.analyst.face.HumanFace; -import cz.fidentis.analyst.icp.IcpTransformer; -import cz.fidentis.analyst.icp.NoUndersampling; +import cz.fidentis.analyst.face.HumanFaceUtils; import cz.fidentis.analyst.visitors.mesh.HausdorffDistance; -import cz.fidentis.analyst.visitors.mesh.HausdorffDistance.Strategy; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; @@ -77,22 +75,41 @@ public class BatchSimilarityGroundTruth { // transform secondary face if (i != j) { // register only different faces long icpTime = System.currentTimeMillis(); - IcpTransformer icp = new IcpTransformer(priFace.getMeshModel(), 100, false, 0.3, new NoUndersampling()); - secFace.getMeshModel().compute(icp, true); + HumanFaceUtils.alignMeshes( + priFace, + secFace, // is transformed + 100, // max iterations + false,// scale + 0.3, // error + 100, // no undersampling + false // drop k-d tree, if exists + ); icpComputationTime += System.currentTimeMillis() - icpTime; } long hdTime = System.currentTimeMillis(); // compute HD from secondary to primary: - priFace.computeKdTree(true); - HausdorffDistance hd = new HausdorffDistance(priFace.getKdTree(), Strategy.POINT_TO_POINT, false, true, CROP_HD); - secFace.getMeshModel().compute(hd, true); + HausdorffDistance hd = new HausdorffDistance( + priFace.getKdTree(), + HausdorffDistance.Strategy.POINT_TO_POINT, + false, // relative distance + true, // parallel + CROP_HD // crop + ); + secFace.getMeshModel().compute(hd); distances[j][i] = hd.getStats().getAverage(); + // compute HD from primary to secondary: - secFace.computeKdTree(true); - hd = new HausdorffDistance(secFace.getKdTree(), Strategy.POINT_TO_POINT, false, true, CROP_HD); - priFace.getMeshModel().compute(hd, true); + hd = new HausdorffDistance( + secFace.getKdTree(), + HausdorffDistance.Strategy.POINT_TO_POINT, + false, // relative distance + true, // parallel + CROP_HD // crop + ); + priFace.getMeshModel().compute(hd); distances[i][j] = hd.getStats().getAverage(); + hdComputationTime += System.currentTimeMillis() - hdTime; } } diff --git a/GUI/src/main/java/cz/fidentis/analyst/tests/BatchSimilarityGroundTruthOpt.java b/GUI/src/main/java/cz/fidentis/analyst/tests/BatchSimilarityGroundTruthOpt.java deleted file mode 100644 index c28f3832..00000000 --- a/GUI/src/main/java/cz/fidentis/analyst/tests/BatchSimilarityGroundTruthOpt.java +++ /dev/null @@ -1,169 +0,0 @@ -package cz.fidentis.analyst.tests; - -import cz.fidentis.analyst.visitors.kdtree.AvgFaceConstructor; -import cz.fidentis.analyst.face.HumanFace; -import cz.fidentis.analyst.face.HumanFaceFactory; -import cz.fidentis.analyst.icp.IcpTransformer; -import cz.fidentis.analyst.icp.NoUndersampling; -import cz.fidentis.analyst.mesh.io.MeshObjExporter; -import cz.fidentis.analyst.visitors.mesh.HausdorffDistance; -import cz.fidentis.analyst.visitors.mesh.HausdorffDistance.Strategy; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.Duration; -import java.util.List; -import java.util.stream.Collectors; - -/** - * A class for testing the efficiency of batch processing algorithms. - * It works in the same way as {@link BatchSimilarityGroundTruth} with the following changes: - * <ul> - * <li>All faces are transformed (ICP) to the very first face of the collection. They are not transformed mutually. - * It enables us to compute ICP only once for every face, but with possible loss of precision. - * Moreover, {@code HumanFaceFactory} is used to quickly load/swap faces when used multiple times.</li> - * </ul> - * Stats for 100 faces WITH CROP: - * <pre> - * Time of AVG face computation time: 00:00:19,529 - * ICP computation time: 00:05:19,096 - * HD computation time: 03:17:29,446 - * Total computation time: 03:32:30,671 - * </pre> - * Stats for 100 faces WITHOUT CROP: - * <pre> - * Time of AVG face computation time: 00:00:19,386 - * ICP computation time: 00:05:39,318 - * HD computation time: 03:11:49,226 - * Total computation time: 03:25:50,957 - * </pre> - * - * @author Radek Oslejsek - */ -public class BatchSimilarityGroundTruthOpt { - - private static final String DATA_DIR = "../../analyst-data-antropologie/_ECA"; - private static final String OUTPUT_FILE = "../../SIMILARITY_GROUND_TRUTH_OPT.csv"; - private static final String TEMPLATE_FACE_PATH = "../../analyst-data-antropologie/template_face.obj"; - private static final int MAX_SAMPLES = 100; - private static final boolean CROP_HD = false; - - /** - * Main method - * @param args Input arguments - * @throws IOException on IO error - */ - public static void main(String[] args) throws IOException, ClassNotFoundException, Exception { - List<Path> faces = Files.list(new File(DATA_DIR).toPath()) - .filter(f -> f.toString().endsWith(".obj")) - .sorted() - .limit(MAX_SAMPLES) - .collect(Collectors.toList()); - - double[][] distances = new double[faces.size()][faces.size()]; - String[] names = new String[faces.size()]; - - long totalTime = System.currentTimeMillis(); - long avgFaceComputationTime = 0; - long icpComputationTime = 0; - long hdComputationTime = 0; - - AvgFaceConstructor avgFaceConstructor = new AvgFaceConstructor(new HumanFace(faces.get(0).toFile()).getMeshModel()); - - HumanFaceFactory factory = new HumanFaceFactory(); - factory.setReuseDumpFile(true); - factory.setStrategy(HumanFaceFactory.Strategy.MRU); - - int counter = 0; - for (int i = 0; i < faces.size(); i++) { - String priFaceId = factory.loadFace(faces.get(i).toFile()); - HumanFace priFace = factory.getFace(priFaceId); - names[i] = priFace.getShortName().replaceAll("_01_ECA", ""); - - for (int j = i; j < faces.size(); j++) { // starts with "i"! - priFace = factory.getFace(priFaceId); // re-read if dumped, at leat update the access time - - String secFaceId = factory.loadFace(faces.get(j).toFile()); - HumanFace secFace = factory.getFace(secFaceId); - - System.out.print(++counter + ": " + priFace.getShortName() + " - " + secFace.getShortName()); - - // transform secondary face, but only once. Transformed faces are stored in the HumanFaceFactory! Don't transform the same face - if (i == 0 && i != j) { - System.out.print(", ICP"); - long icpTime = System.currentTimeMillis(); - IcpTransformer icp = new IcpTransformer(priFace.getMeshModel(), 100, true, 0.3, new NoUndersampling()); - secFace.getMeshModel().compute(icp, true); - icpComputationTime += System.currentTimeMillis() - icpTime; - } - - long hdTime = System.currentTimeMillis(); - // compute HD from secondary to primary: - priFace.computeKdTree(true); - HausdorffDistance hd = new HausdorffDistance(priFace.getKdTree(), Strategy.POINT_TO_POINT, false, true, CROP_HD); - secFace.getMeshModel().compute(hd, true); - distances[j][i] = hd.getStats().getAverage(); - // compute HD from primary to secondary: - secFace.computeKdTree(true); - hd = new HausdorffDistance(secFace.getKdTree(), Strategy.POINT_TO_POINT, false, true, CROP_HD); - priFace.getMeshModel().compute(hd, true); - distances[i][j] = hd.getStats().getAverage(); - hdComputationTime += System.currentTimeMillis() - hdTime; - - // Compute AVG face. Use each tranfromed face only once. Skip the very first face - if (i == 0 && j != 0) { - System.out.print(", AVG"); - long avgFaceTime = System.currentTimeMillis(); - priFace.getKdTree().accept(avgFaceConstructor); - avgFaceComputationTime += System.currentTimeMillis() - avgFaceTime; - } - - System.out.println(", " + factory.toString()); - } - - factory.removeFace(priFaceId); // the face is no longer needed - } - - MeshObjExporter exp = new MeshObjExporter(avgFaceConstructor.getAveragedMeshModel()); - exp.exportModelToObj(new File(TEMPLATE_FACE_PATH)); - - BufferedWriter w = new BufferedWriter(new FileWriter(OUTPUT_FILE)); - w.write("PRI FACE;SEC FACE;AVG HD from PRI to SEC;AVG HD from SEC to PRI"); - w.newLine(); - for (int i = 0; i < faces.size(); i++) { - for (int j = i; j < faces.size(); j++) { - w.write(names[i] + ";"); - w.write(names[j] + ";"); - w.write(String.format("%.8f", distances[i][j]) + ";"); - w.write(String.format("%.8f", distances[j][i]) + ";"); - if (distances[i][j] > distances[j][i]) { - w.write(String.format("%.8f", distances[i][j]) + ";"); - w.write(String.format("%.8f", distances[j][i]) + ""); - } else { - w.write(String.format("%.8f", distances[j][i]) + ";"); - w.write(String.format("%.8f", distances[i][j]) + ""); - } - w.newLine(); - } - } - w.close(); - - System.out.println(); - Duration duration = Duration.ofMillis(avgFaceComputationTime); - System.out.println("Time of AVG face computation time: " + - String.format("%02d:%02d:%02d,%03d", duration.toHoursPart(), duration.toMinutesPart(), duration.toSecondsPart(), duration.toMillisPart())); - duration = Duration.ofMillis(icpComputationTime); - System.out.println("ICP computation time: " + - String.format("%02d:%02d:%02d,%03d", duration.toHoursPart(), duration.toMinutesPart(), duration.toSecondsPart(), duration.toMillisPart())); - duration = Duration.ofMillis(hdComputationTime); - System.out.println("HD computation time: " + - String.format("%02d:%02d:%02d,%03d", duration.toHoursPart(), duration.toMinutesPart(), duration.toSecondsPart(), duration.toMillisPart())); - duration = Duration.ofMillis(System.currentTimeMillis() - totalTime); - System.out.println("Total computation time: " + - String.format("%02d:%02d:%02d,%03d", duration.toHoursPart(), duration.toMinutesPart(), duration.toSecondsPart(), duration.toMillisPart())); - } - -} diff --git a/GUI/src/main/java/cz/fidentis/analyst/tests/EfficiencyTests.java b/GUI/src/main/java/cz/fidentis/analyst/tests/EfficiencyTests.java deleted file mode 100644 index 1de0990d..00000000 --- a/GUI/src/main/java/cz/fidentis/analyst/tests/EfficiencyTests.java +++ /dev/null @@ -1,141 +0,0 @@ -package cz.fidentis.analyst.tests; - -import cz.fidentis.analyst.face.HumanFace; -import cz.fidentis.analyst.kdtree.KdTree; -import cz.fidentis.analyst.mesh.core.MeshFacet; -import cz.fidentis.analyst.symmetry.SymmetryConfig; -import cz.fidentis.analyst.symmetry.Plane; -import cz.fidentis.analyst.symmetry.SymmetryEstimator; -import cz.fidentis.analyst.visitors.mesh.HausdorffDistance; -import cz.fidentis.analyst.visitors.mesh.HausdorffDistance.Strategy; -import java.io.File; -import java.io.IOException; -import java.util.List; -import java.util.Map; - -/** - * A class for testing the efficiency of algorithms. - * - * @author Radek Oslejsek - */ -public class EfficiencyTests { - - private static File girlFile = new File("src/test/resources/cz/fidentis/analyst/average_girl_17-20.obj"); - private static File boyFile = new File("src/test/resources/cz/fidentis/analyst/average_boy_17-20.obj"); - private static File faceFile2 = new File("src/test/resources/cz/fidentis/analyst/00002_01_ECA.obj"); - private static File faceFile4 = new File("/home/oslejsek/GIT/HCI/analyst2/GUI/src/test/resources/cz/fidentis/analyst/00004_01_ECA.obj"); - private static File basicFaceFile = new File("src/test/resources/cz/fidentis/analyst/basic-model-04.obj"); - - private static HumanFace face1; - private static HumanFace face2; - - /** - * Main method - * @param args Input arguments - * @throws IOException on IO error - */ - public static void main(String[] args) throws IOException, ClassNotFoundException, Exception { - face1 = printFaceLoad(faceFile4); - face2 = printFaceLoad(faceFile2); - - face1.getMeshModel().simplifyModel(); - face2.getMeshModel().simplifyModel(); - - //for (int i = 0; i < 10; i++) { - // printFaceDump(face1); - //} - - boolean relativeDist = false; - boolean printDetails = false; - - - //face1.getMeshModel().compute(new GaussianCurvature()); // initialize everything, then measure - - System.out.println("Symmetry plane calculation:"); - //printSymmetryPlane(face1, true, CurvatureAlg.MEAN); - //printSymmetryPlane(face1, true, CurvatureAlg.GAUSSIAN); - //printSymmetryPlane(face1, false, CurvatureAlg.MEAN); - //printSymmetryPlane(face1, false, CurvatureAlg.GAUSSIAN); - - System.out.println(); - System.out.println(measureKdTreeCreation(face1) + "\tmsec:\tKd-tree creation of first face"); - System.out.println(measureKdTreeCreation(face2) + "\tmsec:\tKd-tree creation of second face"); - - System.out.println(); - System.out.println("Hausdorff distance sequentially:"); - testAndPrint(face1, new HausdorffDistance(face2.getMeshModel(), Strategy.POINT_TO_POINT, relativeDist, false, false), printDetails); - testAndPrint(face1, new HausdorffDistance(face2.getMeshModel(), Strategy.POINT_TO_POINT_DISTANCE_ONLY, false, false, false), printDetails); - testAndPrint(face1, new HausdorffDistance(face2.getMeshModel(), Strategy.POINT_TO_POINT, relativeDist, false, false), printDetails); - testAndPrint(face1, new HausdorffDistance(face2.getMeshModel(), Strategy.POINT_TO_TRIANGLE_APPROXIMATE, relativeDist, false, false), printDetails); - - System.out.println(); - System.out.println("Hausdorff distance concurrently:"); - testAndPrint(face1, new HausdorffDistance(face2.getMeshModel(), Strategy.POINT_TO_POINT_DISTANCE_ONLY, false, true, false), printDetails); - testAndPrint(face1, new HausdorffDistance(face2.getMeshModel(), Strategy.POINT_TO_POINT, relativeDist, true, false), printDetails); - testAndPrint(face1, new HausdorffDistance(face2.getMeshModel(), Strategy.POINT_TO_TRIANGLE_APPROXIMATE, relativeDist, true, false), printDetails); - } - - protected static void testAndPrint(HumanFace face, HausdorffDistance vis, boolean printDetails) { - long startTime = System.currentTimeMillis(); - face.getMeshModel().compute(vis); - long endTime = System.currentTimeMillis(); - - System.out.println( - (endTime-startTime) + - "\tmsec: " + - //"\t" + (vis.inParallel() ? "concurrently" : "sequentially") + - "\t" + (vis.relativeDistance()? "relative" : "absolute") + - "\t" + vis.getStrategy() - ); - - if (printDetails) { - Map<MeshFacet, List<Double>> results = vis.getDistances(); - for (MeshFacet facet: results.keySet()) { - System.out.println(results.get(facet)); - } - } - } - - private static long measureKdTreeCreation(HumanFace face) { - long startTime = System.currentTimeMillis(); - KdTree kdTree = new KdTree(face.getMeshModel().getFacets()); - return System.currentTimeMillis() - startTime; - } - - private static void printSymmetryPlane(HumanFace face, boolean concurrently) { - long startTime = System.currentTimeMillis(); - SymmetryEstimator est = new SymmetryEstimator(new SymmetryConfig()); - face.getMeshModel().compute(est, false); - Plane plane = est.getSymmetryPlane(concurrently); - long endTime = System.currentTimeMillis(); - - System.out.println( - (endTime-startTime) + - "\tmsec: " + - "\t" + (concurrently ? "concurrently" : "sequentially") + -// "\t" + curvatureAlg + " curvature " + - "\t" + plane.getNormal() + ", " + plane.getDistance() - ); - } - - private static void printFaceDump(HumanFace face) throws IOException, ClassNotFoundException, Exception { - long startTime = System.currentTimeMillis(); - File file = face.dumpToFile(); - long endTime = System.currentTimeMillis(); - System.out.println("Human face dumping:\t " +(endTime-startTime) + " msec"); - - startTime = System.currentTimeMillis(); - HumanFace f = HumanFace.restoreFromFile(file); - endTime = System.currentTimeMillis(); - System.out.println("Human face restore:\t " +(endTime-startTime) + " msec"); - } - - private static HumanFace printFaceLoad(File file) throws IOException { - long startTime = System.currentTimeMillis(); - HumanFace face = new HumanFace(file); - long endTime = System.currentTimeMillis(); - System.out.println("Human face loading:\t " +(endTime-startTime) + " msec"); - return face; - } - -} diff --git a/GUI/src/main/java/cz/fidentis/analyst/tests/ICPTest.java b/GUI/src/main/java/cz/fidentis/analyst/tests/ICPTest.java deleted file mode 100644 index 13fa8816..00000000 --- a/GUI/src/main/java/cz/fidentis/analyst/tests/ICPTest.java +++ /dev/null @@ -1,82 +0,0 @@ -package cz.fidentis.analyst.tests; - -import cz.fidentis.analyst.face.HumanFace; -import cz.fidentis.analyst.icp.IcpTransformer; -import cz.fidentis.analyst.icp.NoUndersampling; -import cz.fidentis.analyst.kdtree.KdTree; -import cz.fidentis.analyst.mesh.core.MeshFacet; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -/** - * - * @author Radek Oslejsek - */ -public class ICPTest { - - private static final String DATA_DIR = "../../analyst-data/multi-scan-models-anonymized-fixed/"; - - private static String[] faceFiles = new String[] { - DATA_DIR + "02/00002_01_ECA.obj", - DATA_DIR + "04/00004_01_ECA.obj", - DATA_DIR + "06/00006_01_ECA.obj", - DATA_DIR + "07/00007_01_ECA.obj", - DATA_DIR + "10/00010_01_ECA.obj" - }; - - private static int tests = 0; - private static int iters = 0; - - /** - * - * @param args - * @throws IOException - */ - public static void main(String[] args) throws IOException { - long time = 0; - for (int i = 0; i < faceFiles.length; i++) { - time += test(i); - } - System.out.println("tests: " + tests); - System.out.println("iters: " + iters); - System.out.println("time: " + time + " ms"); - System.out.println("AVG iters: " + ((double)iters/tests)); - System.out.println("AVG time: " + ((double)time/tests) + " ms"); - } - - /** - * - * @param index - * @return - * @throws IOException - */ - public static long test(int index) throws IOException { - List<HumanFace> faces = new ArrayList<>(); - for (int i = 0; i < faceFiles.length; i++) { - faces.add(new HumanFace(new File(faceFiles[i]))); - } - HumanFace prim = faces.remove(index); - KdTree primKdTree = prim.computeKdTree(true); - - - System.out.println(); - System.out.println(prim.getId()); - long startTime = System.currentTimeMillis(); - for (HumanFace sec: faces) { - tests++; - IcpTransformer icpTransformer = new IcpTransformer(primKdTree, 10, false, 0.05, new NoUndersampling()); - System.out.println(sec.getId()); - sec.getMeshModel().compute(icpTransformer); - - for (MeshFacet f: icpTransformer.getTransformations().keySet()) { - //System.out.println("AAA"+icpTransformer.getTransformations().get(f).size()); - iters += icpTransformer.getTransformations().get(f).size(); - } - } - long endTime = System.currentTimeMillis(); - - return endTime - startTime; - } -} diff --git a/GUI/src/main/resources/cz/fidentis/analyst/canvas/toolbar/Bundle.properties b/GUI/src/main/resources/cz/fidentis/analyst/canvas/toolbar/Bundle.properties index 2aebc13c..9ccc35ef 100644 --- a/GUI/src/main/resources/cz/fidentis/analyst/canvas/toolbar/Bundle.properties +++ b/GUI/src/main/resources/cz/fidentis/analyst/canvas/toolbar/Bundle.properties @@ -8,3 +8,4 @@ SceneToolboxSingleFace.landButton.tooltip=show-hide feature points SceneToolboxSingleFace.faceButton.tooltip=show-hide face SceneToolboxSingleFace.slider.tooltip=change face transparency SceneToolboxFaceToFace.distButton.tooltip=show-hide distance heatmap +SceneToolboxFaceToFace.symmetryButton.tooltip=show-hide symmetry plane diff --git a/GUI/src/main/resources/cz/fidentis/analyst/registration/Bundle.properties b/GUI/src/main/resources/cz/fidentis/analyst/registration/Bundle.properties index c9ef96eb..d966ae39 100644 --- a/GUI/src/main/resources/cz/fidentis/analyst/registration/Bundle.properties +++ b/GUI/src/main/resources/cz/fidentis/analyst/registration/Bundle.properties @@ -5,30 +5,15 @@ RegistrationPanel.jLabel5.text=min. error: RegistrationPanel.jFormattedTextField2.text=50 RegistrationPanel.jLabel6.text=iterations: RegistrationPanel.jLabel7.text=scale: -RegistrationPanel.jLabel8.text=undersampling: +RegistrationPanel.jLabel8.text=undersampling (100% = none) RegistrationPanel.thersholdUpButton.text= RegistrationPanel.thresholdDownButton.text= RegistrationPanel.featurePointsLabel.text=Highlight feature point pairs closer than: 2 -RegistrationPanel.translationButton.text= RegistrationPanel.translationPanel.border.title=translation -RegistrationPanel.highShiftRB.toolTipText=set high shifting amount -RegistrationPanel.highShiftRB.text=big -RegistrationPanel.lowShiftRB.toolTipText=set low shifting amount -RegistrationPanel.lowShiftRB.text=small -RegistrationPanel.shiftLabel.toolTipText=transformation amount -RegistrationPanel.shiftLabel.text=step: -RegistrationPanel.applyButton.toolTipText=apply transformations -RegistrationPanel.applyButton.text=Apply changes -RegistrationPanel.resetAllButton.toolTipText=reset all transformations -RegistrationPanel.resetAllButton.text=reset all RegistrationPanel.scaleMinusButton.text= -RegistrationPanel.scaleButton.toolTipText=reset scale -RegistrationPanel.scaleButton.text= -RegistrationPanel.scaleFTF.toolTipText= RegistrationPanel.scalePlusButton.text= RegistrationPanel.scalePanel.border.title=scale -RegistrationPanel.rotationXFTF.toolTipText= # To change this license header, choose License Headers in Project Properties. # To change this template file, choose Tools | Templates # and open the template in the editor. @@ -36,8 +21,6 @@ RegistrationPanel.rightRotationXButton.text= RegistrationPanel.leftRotationZButton.text= RegistrationPanel.rightRotationYButton.text= RegistrationPanel.rightRotationZButton.text= -RegistrationPanel.rotationButton.toolTipText=reset rotation -RegistrationPanel.rotationButton.text= RegistrationPanel.rotatXLabel.text=x RegistrationPanel.rotatYLabel.text=y RegistrationPanel.rotatZLabel.text=z @@ -52,9 +35,7 @@ RegistrationPanel.translZLabel.text=front-back RegistrationPanel.translXLabel.text=horizontal RegistrationPanel.rightTranslationYButton.text= RegistrationPanel.leftTranslationYButton.text= -RegistrationPanel.translationXFTF.toolTipText= RegistrationPanel.rightTranslationXButton.text= -RegistrationPanel.translationButton.toolTipText=reset translation RegistrationPanel.jButton2.text=Apply RegistrationPanel.jButton1.toolTipText=Apply ICP RegistrationPanel.jButton2.toolTipText=Apply Procrustes @@ -62,7 +43,7 @@ RegistrationPanel.jLabel4.text=scale: RegistrationPanel.jCheckBox2.text= RegistrationPanel.jPanel7.border.title=Feature points alignment (Procrustes): RegistrationPanel.jPanel1.border.title=Mesh alignment (ICP): -RegistrationPanel.transformationPanel.border.title=Fine-tuning the mutual position: +RegistrationPanel.transformationPanel.border.title=Manual alignment: RegistrationPanel.jPanel4.border.title=View: RegistrationPanel.jLabel1.text=Hausdorff distance: RegistrationPanel.jPanel2.border.title=Measurements: @@ -72,3 +53,4 @@ RegistrationPanel.jTextField1.text= BatchRegistrationPanel.jPanel2.border.title=Dataset BatchRegistrationPanel.jCheckBox1.text=compute average face from BatchRegistrationPanel.jPanel3.border.title=Similarity +RegistrationPanel.jButton3.text=Aling symmetry planes diff --git a/GUI/src/main/resources/na.png b/GUI/src/main/resources/na.png new file mode 100644 index 0000000000000000000000000000000000000000..d1ea8cf0ab404a5ee1ea10a211027d143e64f677 GIT binary patch literal 35276 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�OLiPG=R4*%n@R-#_0Q+g|YDj7CK>4esMc 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 AasU7T literal 0 HcmV?d00001 diff --git a/GUI/src/main/resources/na28x28.png b/GUI/src/main/resources/na28x28.png new file mode 100644 index 0000000000000000000000000000000000000000..07fac02a43b648c2960bc751dbe07ff7737fb97c GIT binary patch literal 2250 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 YAJ|G^=fdzh*8l(j07*qoM6N<$f}2lqTmS$7 literal 0 HcmV?d00001 diff --git a/MeshModel/src/main/java/cz/fidentis/analyst/mesh/core/MeshFacet.java b/MeshModel/src/main/java/cz/fidentis/analyst/mesh/core/MeshFacet.java index c9f81ed9..d590c86f 100644 --- a/MeshModel/src/main/java/cz/fidentis/analyst/mesh/core/MeshFacet.java +++ b/MeshModel/src/main/java/cz/fidentis/analyst/mesh/core/MeshFacet.java @@ -67,7 +67,7 @@ public interface MeshFacet extends Iterable<MeshTriangle>, Serializable { * @return number of triangles */ int getNumTriangles(); - + /** * Return triangle instances. The order corresponds with with the corner * table, i.e., the i-th returned triangle corresponds to i-th triangle in diff --git a/MeshModel/src/main/java/cz/fidentis/analyst/mesh/core/MeshTriangle.java b/MeshModel/src/main/java/cz/fidentis/analyst/mesh/core/MeshTriangle.java index baaddfb4..bb0e75f7 100644 --- a/MeshModel/src/main/java/cz/fidentis/analyst/mesh/core/MeshTriangle.java +++ b/MeshModel/src/main/java/cz/fidentis/analyst/mesh/core/MeshTriangle.java @@ -428,7 +428,7 @@ public class MeshTriangle implements Iterable<MeshPoint> { * * @param point vertex from which the projection is created * @return projection to plane of triangle - * @throws {@code NullPointerException} if the point is {@code null} + * @throws NullPointerException if the point is {@code null} */ protected Point3d getProjectionToTrianglePlane(Point3d point) { Vector3d normal = computeNormal(); @@ -559,10 +559,10 @@ public class MeshTriangle implements Iterable<MeshPoint> { * @return true if the triangle is intersected by the plane */ public boolean checkIntersectionWithPlane(Vector3d normal, double d) { - double p1 = (normal.x * getVertex1().x) + (normal.y * getVertex1().y) + (normal.z * getVertex1().z) + d; - double p2 = (normal.x * getVertex2().x) + (normal.y * getVertex2().y) + (normal.z * getVertex2().z) + d; - double p3 = (normal.x * getVertex3().x) + (normal.y * getVertex3().y) + (normal.z * getVertex3().z) + d; - + double p1 = (normal.x * getVertex1().x) + (normal.y * getVertex1().y) + (normal.z * getVertex1().z) - d; + double p2 = (normal.x * getVertex2().x) + (normal.y * getVertex2().y) + (normal.z * getVertex2().z) - d; + double p3 = (normal.x * getVertex3().x) + (normal.y * getVertex3().y) + (normal.z * getVertex3().z) - d; + //If one point is on one side of the plane and the other on the other side return ((p1 <= 0 || p2 <= 0 || p3 <= 0) && (p1 >= 0 || p2 >= 0 || p3 >= 0)); } diff --git a/MeshModel/src/test/java/cz/fidentis/analyst/mesh/core/MeshTriangleTest.java b/MeshModel/src/test/java/cz/fidentis/analyst/mesh/core/MeshTriangleTest.java index b7e2b12b..65eba7e8 100644 --- a/MeshModel/src/test/java/cz/fidentis/analyst/mesh/core/MeshTriangleTest.java +++ b/MeshModel/src/test/java/cz/fidentis/analyst/mesh/core/MeshTriangleTest.java @@ -23,22 +23,25 @@ public class MeshTriangleTest { ); return triangle; } + */ @Test public void getClosestPointTest() { - MeshTriangle tri = new MeshTriangle( - new MeshPointImpl(new Vector3d(1, 0, 0), null, null), - new MeshPointImpl(new Vector3d(1, 2, 0), null, null), - new MeshPointImpl(new Vector3d(2, 0, 0), null, null), - 0, 1, 2 - ); - Assertions.assertEquals(new Vector3d(1, 0, 0), tri.getClosestPoint(new Vector3d(0, 0, 0))); - Assertions.assertEquals(new Vector3d(1, 1, 0), tri.getClosestPoint(new Vector3d(0, 1, 0))); - Assertions.assertEquals(new Vector3d(1, 2, 0), tri.getClosestPoint(new Vector3d(0, 3, 0))); - Assertions.assertEquals(new Vector3d(1, 0, 0), tri.getClosestPoint(new Vector3d(1, 0, -1))); - Assertions.assertEquals(new Vector3d(1.2, 0.2, 0), tri.getClosestPoint(new Vector3d(1.2, 0.2, -1))); + MeshFacet facet = createFacet(); + MeshTriangle tri = new MeshTriangle(facet, 0, 1, 2); + + Assertions.assertEquals(new Point3d(0.25, 0.25, 0), tri.getClosestPoint(new Point3d(0, 0.5, -1))); + Assertions.assertEquals(new Point3d(0.25, 0.25, 0), tri.getClosestPoint(new Point3d(0, 0.5, 0))); + Assertions.assertEquals(new Point3d(0.25, 0.25, 0), tri.getClosestPoint(new Point3d(0, 0.5, 1))); + + Assertions.assertEquals(new Point3d(1, 1, 0), tri.getClosestPoint(new Point3d(1, 1, -1))); + Assertions.assertEquals(new Point3d(1, 1, 0), tri.getClosestPoint(new Point3d(1, 1, 0))); + Assertions.assertEquals(new Point3d(1, 1, 0), tri.getClosestPoint(new Point3d(1, 1, 1))); + + Assertions.assertEquals(new Point3d(0.7, 0.7, 0), tri.getClosestPoint(new Point3d(0.7, 0.7, -1))); + Assertions.assertEquals(new Point3d(0.7, 0.7, 0), tri.getClosestPoint(new Point3d(0.7, 0.7, 0))); + Assertions.assertEquals(new Point3d(0.7, 0.7, 0), tri.getClosestPoint(new Point3d(0.7, 0.7, 1))); } - */ private MeshFacet createFacet() { MeshFacet facet = new MeshFacetImpl(); @@ -114,8 +117,8 @@ public class MeshTriangleTest { MeshFacet facet = createFacet(); Vector3d normal = new Vector3d(1, 0, 0); - double d1 = -1; - double d2 = -0.5; + double d1 = 1; + double d2 = 0.5; MeshTriangle tri1 = new MeshTriangle(facet, 0, 1, 2); MeshTriangle tri2 = new MeshTriangle(facet, 1, 2, 3); @@ -131,5 +134,12 @@ public class MeshTriangleTest { Assertions.assertFalse(tri2.checkIntersectionWithPlane(normal, d2)); Assertions.assertFalse(tri3.checkIntersectionWithPlane(normal, d2)); Assertions.assertFalse(tri4.checkIntersectionWithPlane(normal, d2)); + + Vector3d n2 = new Vector3d(-1, -1, 0); + n2.normalize(); + Assertions.assertTrue(tri1.checkIntersectionWithPlane(n2, -0.5)); + Assertions.assertFalse(tri2.checkIntersectionWithPlane(n2, -0.5)); + Assertions.assertFalse(tri3.checkIntersectionWithPlane(n2, -0.5)); + Assertions.assertFalse(tri4.checkIntersectionWithPlane(n2, -0.5)); } } -- GitLab