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.examples.tutorials.teapot;
018
019import java.io.IOException;
020import java.nio.file.Files;
021import java.nio.file.Path;
022import java.nio.file.Paths;
023import java.util.Arrays;
024import java.util.HashMap;
025import java.util.LinkedHashMap;
026import java.util.Map;
027import java.util.function.DoubleFunction;
028import java.util.function.UnaryOperator;
029
030import org.apache.commons.geometry.euclidean.threed.AffineTransformMatrix3D;
031import org.apache.commons.geometry.euclidean.threed.Bounds3D;
032import org.apache.commons.geometry.euclidean.threed.Plane;
033import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
034import org.apache.commons.geometry.euclidean.threed.Planes;
035import org.apache.commons.geometry.euclidean.threed.RegionBSPTree3D;
036import org.apache.commons.geometry.euclidean.threed.Vector3D;
037import org.apache.commons.geometry.euclidean.threed.mesh.SimpleTriangleMesh;
038import org.apache.commons.geometry.euclidean.threed.mesh.TriangleMesh;
039import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
040import org.apache.commons.geometry.euclidean.threed.shape.Sphere;
041import org.apache.commons.geometry.euclidean.twod.Vector2D;
042import org.apache.commons.geometry.io.euclidean.threed.IO3D;
043import org.apache.commons.geometry.io.euclidean.threed.obj.ObjWriter;
044import org.apache.commons.numbers.angle.Angle;
045import org.apache.commons.numbers.core.Precision;
046
047/** Class used to construct a simple 3D teapot shape using the
048 * {@code commons-geometry-euclidean} module.
049 */
050public class TeapotBuilder {
051
052    /** Name used to identify the teapot body geometry. */
053    private static final String BODY_NAME = "body";
054
055    /** Name used to identify the teapot lid geometry. */
056    private static final String LID_NAME = "lid";
057
058    /** Name used to identify the teapot handle geometry. */
059    private static final String HANDLE_NAME = "handle";
060
061    /** Name used to identify the teapot spout geometry. */
062    private static final String SPOUT_NAME = "spout";
063
064    /** Precision context used during region construction. */
065    private final Precision.DoubleEquivalence precision;
066
067    /** Construct a new build instance.
068     * @param precision precision context to use during region construction
069     */
070    public TeapotBuilder(final Precision.DoubleEquivalence precision) {
071        this.precision = precision;
072    }
073
074    /** Build a teapot as a {@link RegionBSPTree3D}.
075     * @return teapot as a BSP tree
076     */
077    public RegionBSPTree3D buildTeapot() {
078        return buildTeapot(null);
079    }
080
081    /** Build a teapot as a {@link RegionBSPTree3D}.
082     * @param debugOutputs if not null, important geometries used during the construction of the
083     *      teapot will be placed in this map, keyed by part name
084     * @return teapot as a BSP tree
085     */
086    public RegionBSPTree3D buildTeapot(final Map<String, RegionBSPTree3D> debugOutputs) {
087        // build the parts
088        final RegionBSPTree3D body = buildBody(1);
089        final RegionBSPTree3D lid = buildLid(body);
090        final RegionBSPTree3D handle = buildHandle();
091        final RegionBSPTree3D spout = buildSpout(1);
092
093        // combine into the final region
094        final RegionBSPTree3D teapot = RegionBSPTree3D.empty();
095        teapot.union(body, lid);
096        teapot.union(handle);
097        teapot.union(spout);
098
099        // subtract scaled-down versions of the body and spout to
100        // create the hollow interior
101        teapot.difference(buildBody(0.9));
102        teapot.difference(buildSpout(0.8));
103
104        // add debug outputs if needed
105        if (debugOutputs != null) {
106            debugOutputs.put(BODY_NAME, body);
107            debugOutputs.put(LID_NAME, lid);
108            debugOutputs.put(HANDLE_NAME, handle);
109            debugOutputs.put(SPOUT_NAME, spout);
110        }
111
112        return teapot;
113    }
114
115    /** Build a teapot separated into its component parts. The keys of the returned map are
116     * the component names and the values are the regions.
117     * @return map of teapot component names to regions
118     */
119    public Map<String, RegionBSPTree3D> buildSeparatedTeapot() {
120        // construct the single-piece teapot
121        final RegionBSPTree3D teapot = buildTeapot();
122
123        // create a region to extract the lid
124        final AffineTransformMatrix3D innerCylinderTransform = AffineTransformMatrix3D.createScale(0.4, 0.4, 1)
125                .translate(0, 0, 0.5);
126        final RegionBSPTree3D innerCylinder = buildUnitCylinderMesh(1, 20, innerCylinderTransform).toTree();
127
128        final AffineTransformMatrix3D outerCylinderTransform = AffineTransformMatrix3D.createScale(0.5, 0.5, 10);
129        final RegionBSPTree3D outerCylinder = buildUnitCylinderMesh(1, 20, outerCylinderTransform).toTree();
130
131        final Plane step = Planes.fromPointAndNormal(Vector3D.of(0, 0, 0.63), Vector3D.Unit.MINUS_Z, precision);
132
133        final RegionBSPTree3D extractor = RegionBSPTree3D.from(Arrays.asList(step.span()));
134        extractor.union(innerCylinder);
135        extractor.intersection(outerCylinder);
136
137        // extract the lid
138        final RegionBSPTree3D lid = RegionBSPTree3D.empty();
139        lid.intersection(teapot, extractor);
140
141        // remove the lid from the body
142        final RegionBSPTree3D body = RegionBSPTree3D.empty();
143        body.difference(teapot, extractor);
144
145        // build the output
146        final Map<String, RegionBSPTree3D> result = new LinkedHashMap<>();
147        result.put(LID_NAME, lid);
148        result.put(BODY_NAME, body);
149
150        return result;
151    }
152
153    /** Build the teapot body.
154     * @param initialRadius radius of the sphere used as the first step in body construction
155     * @return teapot body
156     */
157    private RegionBSPTree3D buildBody(final double initialRadius) {
158        // construct a BSP tree sphere approximation
159        final Sphere sphere = Sphere.from(Vector3D.ZERO, initialRadius, precision);
160        final RegionBSPTree3D body = sphere.toTree(4);
161
162        // squash it a little bit along the z-axis
163        final AffineTransformMatrix3D t = AffineTransformMatrix3D.createScale(1, 1, 0.75);
164        body.transform(t);
165
166        // cut off part of the bottom to make it flat
167        final Plane bottomPlane = Planes.fromPointAndNormal(
168                Vector3D.of(0, 0, -0.6 * initialRadius),
169                Vector3D.Unit.PLUS_Z,
170                precision);
171        final PlaneConvexSubset bottom = bottomPlane.span();
172        body.difference(RegionBSPTree3D.from(Arrays.asList(bottom)));
173
174        return body;
175    }
176
177    /** Build the lid of the teapot.
178     * @param body region representing the teapot lid
179     * @return teapot lid
180     */
181    private RegionBSPTree3D buildLid(final RegionBSPTree3D body) {
182        // make a copy of the body so that we match its curve exactly
183        final RegionBSPTree3D lid = body.copy();
184
185        // translate the lid to be above the body
186        final AffineTransformMatrix3D t = AffineTransformMatrix3D.createTranslation(0, 0, 0.03);
187        lid.transform(t);
188
189        // intersect the translated body with a cylinder
190        final TriangleMesh cylinder =
191                buildUnitCylinderMesh(1, 20, AffineTransformMatrix3D.createScale(0.5, 0.5, 10));
192        lid.intersection(cylinder.toTree());
193
194        // add a small squashed sphere on top; use the bounds of the top in order to place
195        // the sphere at the correct position
196        final Sphere sphere = Sphere.from(Vector3D.of(0, 0, 0), 0.15, precision);
197        final RegionBSPTree3D sphereTree = sphere.toTree(2);
198
199        final Bounds3D lidBounds = lid.getBounds();
200        final double sphereZ = lidBounds.getMax().getZ() + 0.075;
201        sphereTree.transform(AffineTransformMatrix3D.createScale(1, 1, 0.75)
202                .translate(0, 0, sphereZ));
203
204        // make the small sphere a part of the top
205        lid.union(sphereTree);
206
207        return lid;
208    }
209
210    /** Build the handle of the teapot.
211     * @return teapot handle
212     */
213    private RegionBSPTree3D buildHandle() {
214        final double handleRadius = 0.1;
215        final double height = 1 - (2 * handleRadius);
216
217        final AffineTransformMatrix3D scale =
218                AffineTransformMatrix3D.createScale(handleRadius, handleRadius, height);
219
220        final QuaternionRotation startRotation =
221                QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Y, -Angle.PI_OVER_TWO);
222        final QuaternionRotation endRotation =
223                QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Y, Angle.PI_OVER_TWO);
224        final DoubleFunction<QuaternionRotation> slerp = startRotation.slerp(endRotation);
225
226        final Vector3D curveCenter = Vector3D.of(0.5 * height, 0, 0);
227
228        final AffineTransformMatrix3D translation =
229                AffineTransformMatrix3D.createTranslation(Vector3D.of(-1.38, 0, 0));
230
231        final UnaryOperator<Vector3D> vertexTransform = v -> {
232            final double t = v.getZ();
233
234            final Vector3D scaled = scale.apply(v);
235
236            final QuaternionRotation rot = slerp.apply(t);
237            final AffineTransformMatrix3D mat = AffineTransformMatrix3D.createRotation(curveCenter, rot);
238
239            final Vector3D rotated = mat.apply(Vector3D.of(scaled.getX(), scaled.getY(), 0));
240
241            final Vector3D result = (t > 0 && t < 1) ?
242                    rotated :
243                    rotated.add(Vector3D.Unit.PLUS_X);
244
245            return translation.apply(result);
246        };
247
248        return buildUnitCylinderMesh(10, 14, vertexTransform).toTree();
249    }
250
251    /** Build the teapot spout.
252     * @param initialRadius radius of the cylinder used as the first step in spout
253     *      construction
254     * @return teapot spout
255     */
256    private RegionBSPTree3D buildSpout(final double initialRadius) {
257        final Vector2D baseScale = Vector2D.of(0.4, 0.2).multiply(initialRadius);
258        final Vector2D topScale = baseScale.multiply(0.6);
259        final double shearZ = 0.9;
260
261        final AffineTransformMatrix3D translation =
262                AffineTransformMatrix3D.createTranslation(Vector3D.of(0.25, 0, -0.4));
263
264        final UnaryOperator<Vector3D> vertexTransform = v -> {
265            final Vector2D scale = baseScale.lerp(topScale, v.getZ());
266
267            final Vector3D tv = Vector3D.of(
268                        (v.getX() * scale.getX()) + (v.getZ() * shearZ),
269                        v.getY() * scale.getY(),
270                        v.getZ()
271                    );
272
273            return translation.apply(tv);
274        };
275
276        return buildUnitCylinderMesh(1, 14, vertexTransform).toTree();
277    }
278
279    /** Construct a triangle mesh approximating a cylinder of radius one and length one oriented with its
280     * based on the origin and extending along the positive z-axis. The vertices may be transformed by
281     * {@code vertexTransform} during construction, meaning that the resulting mesh may no longer represent
282     * a cylinder.
283     * @param segments number of vertical segments used in the cylinder
284     * @param circleVertexCount number of vertices used to approximate the outside circle
285     * @param vertexTransform function used to transform each computed vertex from its position on the unit
286     *      cylinder to its position in the returned mesh
287     * @return triangle mesh
288     */
289    private TriangleMesh buildUnitCylinderMesh(final int segments, final int circleVertexCount,
290            final UnaryOperator<Vector3D> vertexTransform) {
291
292        final SimpleTriangleMesh.Builder builder = SimpleTriangleMesh.builder(precision);
293
294        // add the cylinder vertices
295        final double zDelta = 1.0 / segments;
296        double zValue;
297
298        final double azDelta = Angle.TWO_PI / circleVertexCount;
299        double az;
300
301        Vector3D vertex;
302        for (int i = 0; i <= segments; ++i) {
303            zValue = i * zDelta;
304
305            for (int v = 0; v < circleVertexCount; ++v) {
306                az = v * azDelta;
307
308                vertex = Vector3D.of(
309                        Math.cos(az),
310                        Math.sin(az),
311                        zValue);
312                builder.addVertex(vertexTransform.apply(vertex));
313            }
314        }
315
316        // add the bottom faces using a triangle fan, making sure
317        // that the triangles are oriented so that the face normal
318        // points down
319        for (int i = 1; i < circleVertexCount - 1; ++i) {
320            builder.addFace(0, i + 1, i);
321        }
322
323        // add the side faces
324        int circleStart;
325        int v1;
326        int v2;
327        int v3;
328        int v4;
329        for (int s = 0; s < segments; ++s) {
330            circleStart = s * circleVertexCount;
331
332            for (int i = 0; i < circleVertexCount; ++i) {
333                v1 = i + circleStart;
334                v2 = ((i + 1) % circleVertexCount) + circleStart;
335                v3 = v2 + circleVertexCount;
336                v4 = v1 + circleVertexCount;
337
338                builder
339                    .addFace(v1, v2, v3)
340                    .addFace(v1, v3, v4);
341            }
342        }
343
344        // add the top faces using a triangle fan
345        final int lastCircleStart = circleVertexCount * segments;
346        for (int i = 1 + lastCircleStart; i < builder.getVertexCount() - 1; ++i) {
347            builder.addFace(lastCircleStart, i, i + 1);
348        }
349
350        return builder.build();
351    }
352
353    /** Entry point for command-line execution of the {@link TeapotBuilder} class. Two positional
354     * arguments are supported:
355     * <ol>
356     *  <li><em>outputFile</em> - File to write the constructed teapot to.</li>
357     *  <li><em>debugDir</em> - (Optional) Directory to write geometry files for important
358     *      components used in the construction of the teapot.</li>
359     * </ol>
360     * @param args argument array
361     * @throws IOException if an I/O error occurs
362     */
363    public static void main(final String[] args) throws IOException {
364        if (args.length < 1) {
365            throw new IllegalArgumentException("Output file argument is required");
366        }
367
368        final Path outputFile = Paths.get(args[0]);
369        final Precision.DoubleEquivalence precision = Precision.doubleEquivalenceOfEpsilon(1e-10);
370
371        final TeapotBuilder builder = new TeapotBuilder(precision);
372
373        final Map<String, RegionBSPTree3D> debugOutputs = new HashMap<>();
374
375        final RegionBSPTree3D teapot = builder.buildTeapot(debugOutputs);
376
377        IO3D.write(teapot, outputFile);
378
379        if (args.length > 1) {
380            // write additional files to the debug dir
381            final Path debugDir = Paths.get(args[1]);
382            Files.createDirectories(debugDir);
383
384            // build and write teapot components
385            final Map<String, RegionBSPTree3D> partMap = builder.buildSeparatedTeapot();
386            try (ObjWriter writer = new ObjWriter(Files.newBufferedWriter(debugDir.resolve("separated-teapot.obj")))) {
387
388                for (Map.Entry<String, RegionBSPTree3D> entry : partMap.entrySet()) {
389                    writer.writeObjectName(entry.getKey());
390                    writer.writeBoundaries(entry.getValue());
391                }
392            }
393
394            // write debug outputs
395            for (Map.Entry<String, RegionBSPTree3D> entry : debugOutputs.entrySet()) {
396                IO3D.write(entry.getValue(), debugDir.resolve(entry.getKey() + ".stl"));
397            }
398        }
399    }
400}