001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.geometry.euclidean.threed;
018
019import java.util.Objects;
020
021import org.apache.commons.geometry.core.Transform;
022import org.apache.commons.geometry.core.partitioning.EmbeddingHyperplane;
023import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
024import org.apache.commons.geometry.euclidean.twod.AffineTransformMatrix2D;
025import org.apache.commons.geometry.euclidean.twod.Vector2D;
026import org.apache.commons.numbers.core.Precision;
027
028/** Extension of the {@link Plane} class that supports embedding of 2D subspaces in the plane.
029 * This is accomplished by defining two additional vectors, {@link #getU() u} and {@link #getV() v},
030 * that define the {@code x} and {@code y} axes respectively of the embedded subspace. For completeness,
031 * an additional vector {@link #getW()} is defined, which is simply an alias for the plane normal.
032 * Together, the vectors {@code u}, {@code v}, and {@code w} form a right-handed orthonormal basis.
033 *
034 * <p>The additional {@code u} and {@code v} vectors are not required to fulfill the contract of
035 * {@link org.apache.commons.geometry.core.partitioning.Hyperplane Hyperplane}. Therefore, they
036 * are not considered when using instances of this type purely as a hyperplane. For example, the
037 * {@link Plane#eq(Plane, Precision.DoubleEquivalence) eq} and
038 * {@link Plane#similarOrientation(org.apache.commons.geometry.core.partitioning.Hyperplane) similiarOrientation}
039 * methods do not consider them.</p>
040 */
041public final class EmbeddingPlane extends Plane implements EmbeddingHyperplane<Vector3D, Vector2D> {
042    /** First normalized vector of the plane frame (in plane). */
043    private final Vector3D.Unit u;
044
045    /** Second normalized vector of the plane frame (in plane). */
046    private final Vector3D.Unit v;
047
048    /** Construct a new instance from an orthonormal set of basis vectors and an origin offset.
049     * @param u first vector of the basis (in plane)
050     * @param v second vector of the basis (in plane)
051     * @param w third vector of the basis (plane normal)
052     * @param originOffset offset of the origin with respect to the plane.
053     * @param precision precision context used for floating point comparisons
054     */
055    EmbeddingPlane(final Vector3D.Unit u, final Vector3D.Unit v, final Vector3D.Unit w, final double originOffset,
056                   final Precision.DoubleEquivalence precision) {
057        super(w, originOffset, precision);
058
059        this.u = u;
060        this.v = v;
061    }
062
063    /** Get the plane first canonical vector.
064     * <p>
065     * The frame defined by ({@link #getU u}, {@link #getV v},
066     * {@link #getW w}) is a right-handed orthonormalized frame).
067     * </p>
068     * @return normalized first canonical vector
069     * @see #getV
070     * @see #getW
071     * @see #getNormal
072     */
073    public Vector3D.Unit getU() {
074        return u;
075    }
076
077    /** Get the plane second canonical vector.
078     * <p>
079     * The frame defined by ({@link #getU u}, {@link #getV v},
080     * {@link #getW w}) is a right-handed orthonormalized frame).
081     * </p>
082     * @return normalized second canonical vector
083     * @see #getU
084     * @see #getW
085     * @see #getNormal
086     */
087    public Vector3D.Unit getV() {
088        return v;
089    }
090
091    /** Get the plane third canonical vector, ie, the plane normal. This
092     * method is simply an alias for {@link #getNormal()}.
093     * <p>
094     * The frame defined by {@link #getU() u}, {@link #getV() v},
095     * {@link #getW() w} is a right-handed orthonormalized frame.
096     * </p>
097     * @return normalized normal vector
098     * @see #getU()
099     * @see #getV()
100     * @see #getNormal()
101     */
102    public Vector3D.Unit getW() {
103        return getNormal();
104    }
105
106    /** Return the current instance.
107     */
108    @Override
109    public EmbeddingPlane getEmbedding() {
110        return this;
111    }
112
113    /** Transform a 3D space point into an in-plane point.
114     * @param point point of the space
115     * @return in-plane point
116     * @see #toSpace
117     */
118    @Override
119    public Vector2D toSubspace(final Vector3D point) {
120        return Vector2D.of(point.dot(u), point.dot(v));
121    }
122
123    /** Transform an in-plane point into a 3D space point.
124     * @param point in-plane point
125     * @return 3D space point
126     * @see #toSubspace(Vector3D)
127     */
128    @Override
129    public Vector3D toSpace(final Vector2D point) {
130        return Vector3D.Sum.create()
131                .addScaled(point.getX(), u)
132                .addScaled(point.getY(), v)
133                .addScaled(-getOriginOffset(), getNormal()).get();
134    }
135
136    /** Get one point from the 3D-space.
137     * @param inPlane desired in-plane coordinates for the point in the plane
138     * @param offset  desired offset for the point
139     * @return one point in the 3D-space, with given coordinates and offset relative
140     *         to the plane
141     */
142    public Vector3D pointAt(final Vector2D inPlane, final double offset) {
143        return Vector3D.Sum.create()
144                .addScaled(inPlane.getX(), u)
145                .addScaled(inPlane.getY(), v)
146                .addScaled(offset - getOriginOffset(), getNormal()).get();
147    }
148
149    /** Build a new reversed version of this plane, with opposite orientation.
150     * <p>
151     * The new plane frame is chosen in such a way that a 3D point that had
152     * {@code (x, y)} in-plane coordinates and {@code z} offset with respect to the
153     * plane and is unaffected by the change will have {@code (y, x)} in-plane
154     * coordinates and {@code -z} offset with respect to the new plane. This means
155     * that the {@code u} and {@code v} vectors returned by the {@link #getU} and
156     * {@link #getV} methods are exchanged, and the {@code w} vector returned by the
157     * {@link #getNormal} method is reversed.
158     * </p>
159     * @return a new reversed plane
160     */
161    @Override
162    public EmbeddingPlane reverse() {
163        return new EmbeddingPlane(v, u, getNormal().negate(), -getOriginOffset(), getPrecision());
164    }
165
166    /** {@inheritDoc} */
167    @Override
168    public EmbeddingPlane transform(final Transform<Vector3D> transform) {
169        final Vector3D origin = getOrigin();
170        final Vector3D plusU = origin.add(u);
171        final Vector3D plusV = origin.add(v);
172
173        final Vector3D tOrigin = transform.apply(origin);
174        final Vector3D tPlusU = transform.apply(plusU);
175        final Vector3D tPlusV = transform.apply(plusV);
176
177        final Vector3D.Unit tU = tOrigin.directionTo(tPlusU);
178        final Vector3D.Unit tV = tOrigin.directionTo(tPlusV);
179        final Vector3D.Unit tW = tU.cross(tV).normalize();
180
181        final double tOriginOffset = -tOrigin.dot(tW);
182
183        return new EmbeddingPlane(tU, tV, tW, tOriginOffset, getPrecision());
184    }
185
186    /** Translate the plane by the specified amount.
187     * @param translation translation to apply
188     * @return a new plane
189     */
190    @Override
191    public EmbeddingPlane translate(final Vector3D translation) {
192        final Vector3D tOrigin = getOrigin().add(translation);
193
194        return Planes.fromPointAndPlaneVectors(tOrigin, u, v, getPrecision());
195    }
196
197    /** Rotate the plane around the specified point.
198     * @param center rotation center
199     * @param rotation 3-dimensional rotation
200     * @return a new rotated plane
201     */
202    @Override
203    public EmbeddingPlane rotate(final Vector3D center, final QuaternionRotation rotation) {
204        final Vector3D delta = getOrigin().subtract(center);
205        final Vector3D tOrigin = center.add(rotation.apply(delta));
206        final Vector3D.Unit tU = rotation.apply(u).normalize();
207        final Vector3D.Unit tV = rotation.apply(v).normalize();
208
209        return Planes.fromPointAndPlaneVectors(tOrigin, tU, tV, getPrecision());
210    }
211
212    /** {@inheritDoc} */
213    @Override
214    public int hashCode() {
215        return Objects.hash(getNormal(), getOriginOffset(), u, v, getPrecision());
216    }
217
218    /** {@inheritDoc} */
219    @Override
220    public boolean equals(final Object obj) {
221        if (this == obj) {
222            return true;
223        } else if (obj == null || obj.getClass() != EmbeddingPlane.class) {
224            return false;
225        }
226
227        final EmbeddingPlane other = (EmbeddingPlane) obj;
228
229        return Objects.equals(this.getNormal(), other.getNormal()) &&
230                Double.compare(this.getOriginOffset(), other.getOriginOffset()) == 0 &&
231                Objects.equals(this.u, other.u) &&
232                Objects.equals(this.v, other.v) &&
233                Objects.equals(this.getPrecision(), other.getPrecision());
234    }
235
236    /** {@inheritDoc} */
237    @Override
238    public String toString() {
239        final StringBuilder sb = new StringBuilder();
240        sb.append(getClass().getSimpleName())
241            .append("[origin= ")
242            .append(getOrigin())
243            .append(", u= ")
244            .append(u)
245            .append(", v= ")
246            .append(v)
247            .append(", w= ")
248            .append(getNormal())
249            .append(']');
250
251        return sb.toString();
252    }
253
254    /** Get an object containing the current plane transformed by the argument along with a
255     * 2D transform that can be applied to subspace points. The subspace transform transforms
256     * subspace points such that their 3D location in the transformed plane is the same as their
257     * 3D location in the original plane after the 3D transform is applied. For example, consider
258     * the code below:
259     * <pre>
260     *      SubspaceTransform st = plane.subspaceTransform(transform);
261     *
262     *      Vector2D subPt = Vector2D.of(1, 1);
263     *
264     *      Vector3D a = transform.apply(plane.toSpace(subPt)); // transform in 3D space
265     *      Vector3D b = st.getPlane().toSpace(st.getTransform().apply(subPt)); // transform in 2D space
266     * </pre>
267     * At the end of execution, the points {@code a} (which was transformed using the original
268     * 3D transform) and {@code b} (which was transformed in 2D using the subspace transform)
269     * are equivalent.
270     *
271     * @param transform the transform to apply to this instance
272     * @return an object containing the transformed plane along with a transform that can be applied
273     *      to subspace points
274     * @see #transform(Transform)
275     */
276    public SubspaceTransform subspaceTransform(final Transform<Vector3D> transform) {
277        final Vector3D origin = getOrigin();
278
279        final Vector3D tOrigin = transform.apply(origin);
280        final Vector3D tPlusU = transform.apply(origin.add(u));
281        final Vector3D tPlusV = transform.apply(origin.add(v));
282
283        final EmbeddingPlane tPlane = Planes.fromPointAndPlaneVectors(
284                tOrigin,
285                tOrigin.vectorTo(tPlusU),
286                tOrigin.vectorTo(tPlusV),
287                getPrecision());
288
289        final Vector2D tSubspaceOrigin = tPlane.toSubspace(tOrigin);
290        final Vector2D tSubspaceU = tSubspaceOrigin.vectorTo(tPlane.toSubspace(tPlusU));
291        final Vector2D tSubspaceV = tSubspaceOrigin.vectorTo(tPlane.toSubspace(tPlusV));
292
293        final AffineTransformMatrix2D subspaceTransform =
294                AffineTransformMatrix2D.fromColumnVectors(tSubspaceU, tSubspaceV, tSubspaceOrigin);
295
296        return new SubspaceTransform(tPlane, subspaceTransform);
297    }
298
299    /** Class containing a transformed plane instance along with a subspace (2D) transform. The subspace
300     * transform produces the equivalent of the 3D transform in 2D.
301     */
302    public static final class SubspaceTransform {
303        /** The transformed plane. */
304        private final EmbeddingPlane plane;
305
306        /** The subspace transform instance. */
307        private final AffineTransformMatrix2D transform;
308
309        /** Simple constructor.
310         * @param plane the transformed plane
311         * @param transform 2D transform that can be applied to subspace points
312         */
313        public SubspaceTransform(final EmbeddingPlane plane, final AffineTransformMatrix2D transform) {
314            this.plane = plane;
315            this.transform = transform;
316        }
317
318        /** Get the transformed plane instance.
319         * @return the transformed plane instance
320         */
321        public EmbeddingPlane getPlane() {
322            return plane;
323        }
324
325        /** Get the 2D transform that can be applied to subspace points. This transform can be used
326         * to perform the equivalent of the 3D transform in 2D space.
327         * @return the subspace transform instance
328         */
329        public AffineTransformMatrix2D getTransform() {
330            return transform;
331        }
332    }
333}