001/**
002 * J3DGenScreenNote -- A Java3D node that represents a GenScreenNote in a J3D scene graph.
003 *
004 * Copyright (C) 2014-2023, Joseph A. Huwaldt. All rights reserved.
005 *
006 * This library is free software; you can redistribute it and/or modify it under the terms
007 * of the GNU Lesser General Public License as published by the Free Software Foundation;
008 * either version 2.1 of the License, or (at your option) any later version.
009 *
010 * This library is distributed in the hope that it will be useful, but WITHOUT ANY
011 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
012 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
013 *
014 * You should have received a copy of the GNU Lesser General Public License along with
015 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place -
016 * Suite 330, Boston, MA 02111-1307, USA. Or visit: http://www.gnu.org/licenses/lgpl.html
017 */
018package geomss.j3d;
019
020import geomss.app.GeomSSCanvas3D;
021import geomss.geom.GenScreenNote;
022import geomss.geom.GeomPoint;
023import jahuwaldt.j3d.BGFGImage;
024import java.awt.*;
025import java.awt.image.BufferedImage;
026import static java.util.Objects.requireNonNull;
027import javax.measure.unit.SI;
028import org.jogamp.java3d.*;
029import org.jogamp.vecmath.*;
030
031/**
032 * A Java 3D node that represents a GenScreenNote in a Java 3D scene graph.
033 *
034 * <p> Modified by: Joseph A. Huwaldt </p>
035 *
036 * @author Joseph A. Huwaldt, Date: February 7, 2014
037 * @version June 4, 2023
038 */
039public class J3DGenScreenNote extends J3DGeomGroup<GenScreenNote> {
040
041    //  The switch for main or mirrored geometry.
042    private Switch _symmSG;
043
044    //  The overlay used to render the text to the screen.
045    private final MyOverlay _overlay;
046
047    /**
048     * Construct a J3DGenScreenNote using the specified GenScreenNote as a reference.
049     *
050     * @param canvas   The canvas that the note is rendered into.
051     * @param geometry The GeomSS geometry to be turned into a Java3D node.
052     */
053    public J3DGenScreenNote(GeomSSCanvas3D canvas, GenScreenNote geometry) {
054        super(requireNonNull(canvas), requireNonNull(geometry));
055
056        //  Create a overlay that renders the text of the note on the screen.
057        GeomPoint location = geometry.getLocation().to(SI.METER);               //      Convert all geometry to meters.
058        int dims = location.getPhyDimension();
059        double x = location.getValue(0);
060        double y = (dims > 1 ? location.getValue(1) : 0);
061        double z = (dims > 2 ? location.getValue(2) : 0);
062        Point3d location3d = new Point3d(x, y, z);
063        _overlay = new MyOverlay(location3d, geometry);
064
065        //  Add the overlay to the canvas.
066        getCanvas3D().addOverlay(_overlay);
067    }
068
069    /**
070     * Set the display of a mirrored copy of this geometry. This is called from
071     * "setDisplayed()" to turn on and off the display of mirrored geometry without
072     * changing the "mirrored state" of the object. This call does not affect the output
073     * of "isMirrored()".
074     *
075     * @param mirrored Flag indicating if the mirrored geometry should be displayed or
076     *                 not.
077     * @see #setMirrored(boolean)
078     * @see #isMirrored()
079     */
080    @Override
081    protected void internalSetMirrored(boolean mirrored) {
082        if (mirrored)
083            _symmSG.setWhichChild(Switch.CHILD_ALL);
084        else
085            _symmSG.setWhichChild(0);
086    }
087
088    /**
089     * Create a new Java 3D <code>Group</code> that contains the geometry contained in
090     * this object. This method is called from <code>createSceneGraph</code>.
091     *
092     * @return New Java 3D Group that contains the geometry in this object.
093     * @see #createSceneGraph
094     */
095    @Override
096    protected Group createGeometry() {
097        J3DRenderingPrefs drawPrefs = getRenderingPrefs();
098
099        //      Create a point array with a single point in it.
100        GenScreenNote thisNote = this.getGeomElement();
101        PointArray pointA = new PointArray(1, PointArray.COORDINATES | PointArray.COLOR_4);
102        GeomPoint location = thisNote.getLocation().to(SI.METER);               //      Convert all geometry to meters.
103        int dims = location.getPhyDimension();
104        double x = location.getValue(0);
105        double y = (dims > 1 ? location.getValue(1) : 0);
106        double z = (dims > 2 ? location.getValue(2) : 0);
107        Point3d location3d = new Point3d(x, y, z);
108        pointA.setCoordinate(0, location3d);
109        //pointA.setColor(0, drawPrefs.getPointColorJ3D());
110
111        //  Set the color to be transparent.  Makes the point invisible, but still pickable.
112        pointA.setColor(0, new Color4f(0f, 0f, 0f, 0f));
113
114        //      Create a 3D shape from the point array
115        Shape3D pointShape = new GeomShape3D(thisNote, pointA);
116        Appearance pApp = new Appearance();
117        pointShape.setAppearance(pApp);
118        pApp.setPointAttributes(new PointAttributes(drawPrefs.getPointSize(), true));
119
120        //      Add the basic unmirrored geometry to the switch.
121        _symmSG = new Switch();
122        _symmSG.setCapability(Switch.ALLOW_SWITCH_READ);
123        _symmSG.setCapability(Switch.ALLOW_SWITCH_WRITE);
124        _symmSG.addChild(pointShape);
125
126        //      Clone the basic geometry to make the mirrored geometry.
127        Node mirrored = pointShape.cloneTree();
128
129        //      Create a mirror across the XZ plane of symmetry transform.
130        Transform3D symmT = new Transform3D();
131        symmT.setScale(new Vector3d(1, -1, 1));
132        TransformGroup symmTG = new TransformGroup(symmT);
133
134        //      Add the mirrored geometry to the symmetry transform group.
135        symmTG.addChild(mirrored);
136
137        //      Add the mirrored geometry to the switch.
138        _symmSG.addChild(symmTG);
139
140        //      By default, display only the main geometry (not the mirrored).
141        _symmSG.setWhichChild(0);
142
143        return _symmSG;
144    }
145
146    /**
147     * Creates a new instance of the node. This routine is called by
148     * <code>cloneTree</code> to duplicate the current node.
149     *
150     * @param forceDuplicate when set to <code>true</code>, causes the
151     *                       <code>duplicateOnCloneTree</code> flag to be ignored. When
152     *                       <code>false</code>, the value of each node's
153     *                       <code>duplicateOnCloneTree</code> variable determines whether
154     *                       NodeComponent data is duplicated or copied.
155     * @return A new instance of this Java3D node.
156     */
157    @Override
158    public Node cloneNode(boolean forceDuplicate) {
159        J3DGenScreenNote node = new J3DGenScreenNote(getCanvas3D(), this.getGeomElement());
160        node.duplicateNode(this, forceDuplicate);
161        return node;
162    }
163
164    /**
165     * Detaches this BranchGroup from its parent. This implementation also removes the
166     * note overlay from the canvas it was rendered into.
167     */
168    @Override
169    public void detach() {
170        super.detach();
171
172        //  Remove our overlay from the rendering canvas.
173        getCanvas3D().removeOverlay(_overlay);
174
175        //  Force J3DGeomGroupFactory to create a new J3DGenScreenNote the next time
176        //  the note is drawn, by removing it from the notes meta-data.
177        getGeomElement().removeUserData(USERDATA_KEY);
178    }
179
180    /**
181     * An overlay image that contains the rendered text for the screen note.
182     */
183    private final class MyOverlay implements BGFGImage {
184
185        private final Point3d modelPoint;
186        private final String noteString;
187        private final Font noteFont;
188
189        //  The ascent and overall height of the font in pixels.
190        private int ascent, height, width;
191
192        //  The X,Y location of the model point when projected onto the screen.
193        private int px, py;
194
195        //  Stored for efficiency to prevent wasteful object creation.
196        private BufferedImage bufImg;
197        private final Rectangle imgBounds = new Rectangle();
198        private final Rectangle canvasBounds = new Rectangle();
199
200        //  Used as optimizations to get3DTo2DPoint() to prevent wastefull object creation.
201        private final Transform3D T_VL = new Transform3D();   //  Vworld wrt Local
202        private final Transform3D T_IV = new Transform3D();   //  ImagePlate wrt Vworld
203        private final Point3d _p3d = new Point3d();           //  A temporary point3d object.
204
205        public MyOverlay(Point3d point, GenScreenNote note) {
206            modelPoint = point;
207            noteString = note.getNote();
208            noteFont = note.getFont();
209        }
210
211        /**
212         * Returns the overlay used to show a drag rectangle.
213         */
214        @Override
215        public synchronized BufferedImage getImage() {
216            GeomSSCanvas3D canvas = getCanvas3D();
217            if (bufImg == null) {
218                //  The buffered image doesn't exist, so create it.
219
220                //  Measure the note string.
221                FontMetrics fm = canvas.getFontMetrics(noteFont);
222                ascent = fm.getAscent();
223                height = fm.getHeight();
224                width = fm.stringWidth(noteString);
225
226                //  TYPE_INT_ARGB is required to get transparency.  However, the buffer only gets
227                //  rendered as a square with the given width no matter the height specified.
228                //  So, this is using the max of width & height to work around this problem
229                //  which must be a bug in Java3D(?).
230                int max = Math.max(width, height);
231                bufImg = new BufferedImage(max, max, BufferedImage.TYPE_INT_ARGB);
232                Graphics2D g2d = bufImg.createGraphics();
233
234                //  Clear the buffer to transparent.
235                clearSurface(g2d, max, max);
236
237                //  Render the text into the buffered image.
238                Color textColor = SystemColor.textText;
239                g2d.setPaint(textColor);
240                g2d.setFont(noteFont);
241                g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
242                        RenderingHints.VALUE_TEXT_ANTIALIAS_GASP);
243                g2d.drawString(noteString, 0, ascent);
244                //g2d.drawLine(0,ascent,0,ascent);
245            }
246
247            //  Convert the model space 3D point to a screen pixel location.
248            Point2d p2d = get3DTo2DPoint(modelPoint);
249            px = (int)Math.round(p2d.getX());
250            py = (int)Math.round(p2d.getY());
251
252            //  Is this inside the displayed area of the canvas?
253            imgBounds.x = px;
254            imgBounds.y = py;
255            imgBounds.width = width;
256            imgBounds.height = height;
257            canvas.getBounds(canvasBounds);
258            canvasBounds.x = 0;
259            canvasBounds.y = 0;
260            if (canvasBounds.intersects(imgBounds)) {
261                //  Return the buffered image.
262                return bufImg;
263            }
264
265            //  If the point is outside the canvas, then return nothing.
266            return null;
267        }
268
269        /**
270         * Returns the X coordinate of the upper left corner of the image.
271         */
272        @Override
273        public int getImageX() {
274            return px;
275        }
276
277        /**
278         * Return the Y coordinate of the upper left corner of the drag rectangle.
279         */
280        @Override
281        public int getImageY() {
282            return py - ascent;
283        }
284
285        /**
286         * Clears the buffered image to be 100% transparent.
287         */
288        private void clearSurface(Graphics2D drawg2d, int width, int height) {
289            drawg2d.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f));
290            drawg2d.fillRect(0, 0, width + 1, height + 1);
291            drawg2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f));
292        }
293
294        /**
295         * Converts a 3D point in model space to it's projected 2D screen coordinate
296         * location.
297         *
298         * @param point3d The 3D model space point to be projected.
299         * @return the 2D projected pixel location on the screen of the 3D point
300         *         projection.
301         */
302        private Point2d get3DTo2DPoint(Point3d point3d) {
303            _p3d.set(point3d);      //  Must defensively copy the input point.
304
305            //  Get the required transforms.
306            _symmSG.getChild(0).getLocalToVworld(T_VL);
307            GeomSSCanvas3D canvas = getCanvas3D();
308            canvas.getVworldToImagePlate(T_IV);
309
310            //  Convert from local coordinates to image plate coordinates.
311            //  T_IL = T_IV * T_VL = ImagePlate wrt Local
312            T_IV.mul(T_VL);
313            T_IV.transform(_p3d);
314
315            //  Convert from image plate to AWT pixel locations.
316            Point2d point2d = new Point2d();
317            canvas.getPixelLocationFromImagePlate(_p3d, point2d);
318
319            return point2d;
320        }
321    }
322}