diff options
Diffstat (limited to 'src/graphui/classes/com')
22 files changed, 5812 insertions, 0 deletions
diff --git a/src/graphui/classes/com/jogamp/graph/ui/Container.java b/src/graphui/classes/com/jogamp/graph/ui/Container.java new file mode 100644 index 000000000..845d41c0c --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/Container.java @@ -0,0 +1,116 @@ +/** + * Copyright 2023 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package com.jogamp.graph.ui; + +import java.util.Collection; +import java.util.Comparator; +import java.util.List; + +import com.jogamp.graph.ui.Shape.Visitor2; +import com.jogamp.graph.ui.Shape.Visitor1; +import com.jogamp.opengl.fixedfunc.GLMatrixFunc; +import com.jogamp.opengl.math.geom.AABBox; +import com.jogamp.opengl.util.PMVMatrix; + +/** + * Container interface of UI {@link Shape}s + * @see Scene + * @see Shape + */ +public interface Container { + + List<Shape> getShapes(); + + void addShape(Shape s); + + /** + * Removes given shape, w/o {@link Shape#destroy(com.jogamp.opengl.GL2ES2, com.jogamp.graph.curve.opengl.RegionRenderer) destroying} them. + * @return the removed shape or null if not contained + */ + Shape removeShape(Shape s); + + /** + * Removes shape at given index, w/o {@link Shape#destroy(com.jogamp.opengl.GL2ES2, com.jogamp.graph.curve.opengl.RegionRenderer) destroying} them. + * @return the removed shape + * @throws IndexOutOfBoundsException if index is out of bounds, i.e. (index < 0 || index >= size()) + */ + Shape removeShape(final int idx); + + void addShapes(Collection<? extends Shape> shapes); + + /** Removes all given shapes, w/o {@link Shape#destroy(com.jogamp.opengl.GL2ES2, com.jogamp.graph.curve.opengl.RegionRenderer) destroying} them. */ + void removeShapes(Collection<? extends Shape> shapes); + + /** Removes all contained shapes, w/o {@link Shape#destroy(com.jogamp.opengl.GL2ES2, com.jogamp.graph.curve.opengl.RegionRenderer) destroying} them. */ + void removeAllShapes(); + + boolean contains(Shape s); + + AABBox getBounds(final PMVMatrix pmv, Shape shape); + + /** Enable or disable {@link PMVMatrix#getFrustum()} culling per {@link Shape}. Default is disabled. */ + void setFrustumCullingEnabled(final boolean v); + + /** Return whether {@link #setFrustumCullingEnabled(boolean) frustum culling} is enabled. */ + boolean isFrustumCullingEnabled(); + + /** + * Traverses through the graph up until {@code shape} and apply {@code action} on it. + * @param pmv + * @param shape + * @param action + * @return true to signal operation complete, i.e. {@code shape} found, otherwise false + */ + boolean forOne(final PMVMatrix pmv, final Shape shape, final Runnable action); + + /** + * Traverses through the graph and apply {@link Visitor1#visit(Shape)} for each, stop if it returns true. + * @param v + * @return true to signal operation complete and to stop traversal, i.e. {@link Visitor1#visit(Shape)} returned true, otherwise false + */ + boolean forAll(Visitor1 v); + + /** + * Traverses through the graph and apply {@link Visitor2#visit(Shape, PMVMatrix)} for each, stop if it returns true. + * @param pmv + * @param v + * @return true to signal operation complete and to stop traversal, i.e. {@link Visitor2#visit(Shape, PMVMatrix)} returned true, otherwise false + */ + boolean forAll(final PMVMatrix pmv, Visitor2 v); + + /** + * Traverses through the graph and apply {@link Visitor#visit(Shape, PMVMatrix)} for each, stop if it returns true. + * + * Each {@link Container} level is sorted using {@code sortComp} + * @param sortComp + * @param pmv + * @param v + * @return true to signal operation complete and to stop traversal, i.e. {@link Visitor2#visit(Shape, PMVMatrix)} returned true, otherwise false + */ + boolean forSortedAll(final Comparator<Shape> sortComp, final PMVMatrix pmv, final Visitor2 v); +}
\ No newline at end of file diff --git a/src/graphui/classes/com/jogamp/graph/ui/GraphShape.java b/src/graphui/classes/com/jogamp/graph/ui/GraphShape.java new file mode 100644 index 000000000..7dccd41a0 --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/GraphShape.java @@ -0,0 +1,271 @@ +/** + * Copyright 2010-2023 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package com.jogamp.graph.ui; + +import java.util.ArrayList; +import java.util.List; + +import com.jogamp.graph.curve.OutlineShape; +import com.jogamp.graph.curve.Region; +import com.jogamp.graph.curve.opengl.GLRegion; +import com.jogamp.graph.curve.opengl.RegionRenderer; +import com.jogamp.graph.ui.layout.Padding; +import com.jogamp.opengl.GL2ES2; +import com.jogamp.opengl.GLProfile; +import com.jogamp.opengl.math.Vec3f; +import com.jogamp.opengl.math.Vec4f; +import com.jogamp.opengl.util.texture.TextureSequence; + +/** + * Graph based {@link GLRegion} {@link Shape} + * <p> + * GraphUI is GPU based and resolution independent. + * </p> + * <p> + * GraphUI is intended to become an immediate- and retained-mode API. + * </p> + * @see Scene + */ +public abstract class GraphShape extends Shape { + protected final int renderModes; + protected GLRegion region = null; + protected float oshapeSharpness = OutlineShape.DEFAULT_SHARPNESS; + private int regionQuality = Region.MAX_QUALITY; + private final List<GLRegion> dirtyRegions = new ArrayList<GLRegion>(); + + /** + * Create a Graph based {@link GLRegion} UI {@link Shape}. + * + * @param renderModes Graph's {@link Region} render modes, see {@link GLRegion#create(GLProfile, int, TextureSequence) create(..)}. + */ + public GraphShape(final int renderModes) { + super(); + this.renderModes = renderModes; + } + + /** Return Graph's {@link Region} render modes, see {@link GLRegion#create(GLProfile, int, TextureSequence) create(..)}. */ + public final int getRenderModes() { return renderModes; } + + /** + * Sets the shape's Graph {@link Region}'s quality parameter. Default is {@link Region#MAX_QUALITY}. + * @param q Graph {@link Region}'s quality parameter, default is {@link Region#MAX_QUALITY}. + * @return this shape for chaining. + */ + public final GraphShape setQuality(final int q) { + this.regionQuality = q; + if( null != region ) { + region.setQuality(q); + } + return this; + } + /** + * Return the shape's Graph {@link Region}'s quality parameter. + * @see #setQuality(int) + */ + public final int getQuality() { return regionQuality; } + + /** + * Sets the shape's Graph {@link OutlineShape}'s sharpness parameter. Default is {@link OutlineShape#DEFAULT_SHARPNESS}. + * + * Method issues {@link #markShapeDirty()}. + * + * @param sharpness Graph {@link OutlineShape}'s sharpness value, default is {@link OutlineShape#DEFAULT_SHARPNESS}. + * @return this shape for chaining. + */ + public final GraphShape setSharpness(final float sharpness) { + this.oshapeSharpness = sharpness; + markShapeDirty(); + return this; + } + /** + * Return the shape's Graph {@link OutlineShape}'s sharpness value. + * @see #setSharpness(float) + */ + public final float getSharpness() { + return oshapeSharpness; + } + + @Override + public boolean hasColorChannel() { + return Region.hasColorChannel(renderModes) || Region.hasColorTexture(renderModes); + } + + private final void clearDirtyRegions(final GL2ES2 gl) { + for(final GLRegion r : dirtyRegions) { + r.destroy(gl); + } + dirtyRegions.clear(); + } + + @Override + protected final void clearImpl0(final GL2ES2 gl, final RegionRenderer renderer) { + clearImpl(gl, renderer); + clearDirtyRegions(gl); + if( null != region ) { + region.clear(gl); + } + } + + @Override + protected final void destroyImpl0(final GL2ES2 gl, final RegionRenderer renderer) { + destroyImpl(gl, renderer); + clearDirtyRegions(gl); + if( null != region ) { + region.destroy(gl); + region = null; + } + } + + @Override + protected final void drawImpl0(final GL2ES2 gl, final RegionRenderer renderer, final int[] sampleCount, final Vec4f rgba) { + if( null != rgba ) { + renderer.getRenderState().setColorStatic(rgba); + } + region.draw(gl, renderer, sampleCount); + } + + /** + * Reset the {@link GLRegion} and reserving its buffers to have a free capacity for `vertexCount` and `indexCount` elements. + * + * In case {@link GLRegion} is `null`, a new instance is being created. + * + * In case the {@link GLRegion} already exists, it will be either {@link GLRegion#clear(GL2ES2) cleared} if the {@link GL2ES2} `gl` + * instance is not `null` or earmarked for deletion at a later time and a new instance is being created. + * + * Method shall be invoked by the {@link #addShapeToRegion(GLProfile, GL2ES2)} implementation + * before actually adding the {@link OutlineShape} to the {@link GLRegion}. + * + * {@link #addShapeToRegion(GLProfile, GL2ES2)} is capable to determine initial `vertexCount` and `indexCount` buffer sizes, + * as it composes the {@link OutlineShape}s to be added. + * + * {@link #resetGLRegion(GLProfile, GL2ES2, TextureSequence, OutlineShape)} maybe used for convenience. + * + * @param glp the used GLProfile, never `null` + * @param gl the optional current {@link GL2ES2} instance, maybe `null`. + * @param colorTexSeq optional {@link TextureSequence} for {@link Region#COLORTEXTURE_RENDERING_BIT} rendering mode. + * @param vertexCount the initial {@link GLRegion} vertex buffer size + * @param indexCount the initial {@link GLRegion} index buffer size + * @see #resetGLRegion(GLProfile, GL2ES2, TextureSequence, OutlineShape) + */ + protected void resetGLRegion(final GLProfile glp, final GL2ES2 gl, final TextureSequence colorTexSeq, int vertexCount, int indexCount) { + if( hasBorder() ) { + vertexCount += 8; + indexCount += 24; + } + if( null == region ) { + region = GLRegion.create(glp, renderModes, colorTexSeq, vertexCount, indexCount); + } else if( null == gl ) { + dirtyRegions.add(region); + region = GLRegion.create(glp, renderModes, colorTexSeq, vertexCount, indexCount); + } else { + region.clear(gl); + region.setBufferCapacity(vertexCount, indexCount); + } + } + /** + * Convenient {@link #resetGLRegion(GLProfile, GL2ES2, TextureSequence, int, int)} variant determining initial + * {@link GLRegion} buffer sizes via {@link Region#countOutlineShape(OutlineShape, int[])}. + * + * @param glp the used GLProfile, never `null` + * @param gl the optional current {@link GL2ES2} instance, maybe `null`. + * @param colorTexSeq optional {@link TextureSequence} for {@link Region#COLORTEXTURE_RENDERING_BIT} rendering mode. + * @param shape the {@link OutlineShape} used to determine {@link GLRegion}'s buffer sizes via {@link Region#countOutlineShape(OutlineShape, int[])} + * @see #resetGLRegion(GLProfile, GL2ES2, TextureSequence, int, int) + */ + protected void resetGLRegion(final GLProfile glp, final GL2ES2 gl, final TextureSequence colorTexSeq, final OutlineShape shape) { + final int[/*2*/] vertIndexCount = Region.countOutlineShape(shape, new int[2]); + resetGLRegion(glp, gl, colorTexSeq, vertIndexCount[0], vertIndexCount[1]); + } + + @Override + protected final void validateImpl(final GLProfile glp, final GL2ES2 gl) { + if( null != gl ) { + clearDirtyRegions(gl); + } + if( isShapeDirty() ) { + // box has been reset + addShapeToRegion(glp, gl); // calls updateGLRegion(..) + if( hasBorder() ) { + // Also takes padding into account + addBorderOutline(); + } else if( hasPadding() ) { + final Padding p = getPadding(); + final Vec3f l = box.getLow(); + final Vec3f h = box.getHigh(); + box.resize(l.x() - p.left, l.y() - p.bottom, l.z()); + box.resize(h.x() + p.right, h.y() + p.top, l.z()); + setRotationPivot( box.getCenter() ); + } + region.setQuality(regionQuality); + } else if( isStateDirty() ) { + region.markStateDirty(); + } + } + + protected void addBorderOutline() { + final OutlineShape shape = new OutlineShape(); + final Padding dist = null != getPadding() ? getPadding() : new Padding(); + final float x1 = box.getMinX() - dist.left; + final float x2 = box.getMaxX() + dist.right; + final float y1 = box.getMinY() - dist.bottom; + final float y2 = box.getMaxY() + dist.top; + final float z = box.getCenter().z(); // 0; // box.getMinZ() + 0.025f; + { + // Outer OutlineShape as Winding.CCW. + shape.moveTo(x1, y1, z); + shape.lineTo(x2, y1, z); + shape.lineTo(x2, y2, z); + shape.lineTo(x1, y2, z); + shape.lineTo(x1, y1, z); + shape.closeLastOutline(true); + shape.addEmptyOutline(); + } + { + // Inner OutlineShape as Winding.CW. + final float dxy = getBorderThickness(); + shape.moveTo(x1+dxy, y1+dxy, z); + shape.lineTo(x1+dxy, y2-dxy, z); + shape.lineTo(x2-dxy, y2-dxy, z); + shape.lineTo(x2-dxy, y1+dxy, z); + shape.lineTo(x1+dxy, y1+dxy, z); + shape.closeLastOutline(true); + } + shape.setIsQuadraticNurbs(); + shape.setSharpness(oshapeSharpness); + region.addOutlineShape(shape, null, getBorderColor()); + box.resize(shape.getBounds()); // border <-> shape = padding, and part of shape size + setRotationPivot( box.getCenter() ); + } + + protected void clearImpl(final GL2ES2 gl, final RegionRenderer renderer) { } + + protected void destroyImpl(final GL2ES2 gl, final RegionRenderer renderer) { } + + protected abstract void addShapeToRegion(GLProfile glp, GL2ES2 gl); + +} diff --git a/src/graphui/classes/com/jogamp/graph/ui/Group.java b/src/graphui/classes/com/jogamp/graph/ui/Group.java new file mode 100644 index 000000000..d82df3a52 --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/Group.java @@ -0,0 +1,342 @@ +/** + * Copyright 2023 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package com.jogamp.graph.ui; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; + +import com.jogamp.graph.curve.Region; +import com.jogamp.graph.curve.opengl.GLRegion; +import com.jogamp.graph.curve.opengl.RegionRenderer; +import com.jogamp.graph.ui.layout.Padding; +import com.jogamp.graph.ui.shapes.Rectangle; +import com.jogamp.opengl.GL2ES2; +import com.jogamp.opengl.GLProfile; +import com.jogamp.opengl.math.Vec3f; +import com.jogamp.opengl.math.Vec4f; +import com.jogamp.opengl.math.geom.AABBox; +import com.jogamp.opengl.util.PMVMatrix; + +import jogamp.graph.ui.TreeTool; + +/** + * Group of {@link Shape}s, optionally utilizing a {@link Group.Layout}. + * @see Scene + * @see Shape + * @see Group.Layout + */ +public class Group extends Shape implements Container { + /** Layout for the GraphUI {@link Group}, called @ {@link Shape#validate(GL2ES2)} or {@link Shape#validate(GLProfile)}. */ + public static interface Layout { + /** + * Performing the layout of {@link Group#getShapes()}, called @ {@link Shape#validate(GL2ES2)} or {@link Shape#validate(GLProfile)}. + * <p> + * According to the implemented layout, method + * - may scale the {@Link Shape}s + * - may move the {@Link Shape}s + * - may reuse the given {@link PMVMatrix} `pmv` + * - must update the given {@link AABBox} `box` + * </p> + * @param g the {@link Group} to layout + * @param box the bounding box of {@link Group} to be updated by this method. + * @param pmv a {@link PMVMatrix} which can be reused. + */ + void layout(final Group g, final AABBox box, final PMVMatrix pmv); + } + + private final List<Shape> shapes = new ArrayList<Shape>(); + private Layout layouter; + private Rectangle border = null; + + /** + * Create a Graph based {@link GLRegion} UI {@link Shape}. + * <p> + * Default is non-interactive, see {@link #setInteractive(boolean)}. + * </p> + */ + public Group() { + this(null); + } + + /** + * Create a Graph based {@link GLRegion} UI {@link Shape} w/ given {@link Group.Layour}. + * <p> + * Default is non-interactive, see {@link #setInteractive(boolean)}. + * </p> + */ + public Group(final Layout l) { + super(); + this.layouter = l; + this.setInteractive(false); + } + + /** Return current {@link Group.Layout}. */ + public Layout getLayout() { return layouter; } + + /** Set {@link Group.Layout}. */ + public Group setLayout(final Layout l) { layouter = l; return this; } + + @Override + public List<Shape> getShapes() { + return shapes; + } + @Override + public void addShape(final Shape s) { + shapes.add(s); + markShapeDirty(); + } + + /** Removes given shape, keeps it alive. */ + @Override + public Shape removeShape(final Shape s) { + final Shape r = shapes.remove(s) ? s : null; + markShapeDirty(); + return r; + } + + @Override + public Shape removeShape(final int idx) { + final Shape r = shapes.remove(idx); + markShapeDirty(); + return r; + } + + /** Removes given shape and destroy it. */ + public void removeShape(final GL2ES2 gl, final RegionRenderer renderer, final Shape s) { + shapes.remove(s); + s.destroy(gl, renderer); + } + + @Override + public void addShapes(final Collection<? extends Shape> shapes) { + for(final Shape s : shapes) { + addShape(s); + } + } + /** Removes all given shapes, keeps them alive. */ + @Override + public void removeShapes(final Collection<? extends Shape> shapes) { + for(final Shape s : shapes) { + removeShape(s); + } + } + /** Removes all given shapes and destroys them. */ + public void removeShapes(final GL2ES2 gl, final RegionRenderer renderer, final Collection<? extends Shape> shapes) { + for(final Shape s : shapes) { + removeShape(gl, renderer, s); + } + } + + @Override + public void removeAllShapes() { + shapes.clear(); + } + + /** Removes all given shapes and destroys them. */ + public void removeAllShapes(final GL2ES2 gl, final RegionRenderer renderer) { + final int count = shapes.size(); + for(int i=count-1; i>=0; --i) { + removeShape(gl, renderer, shapes.get(i)); + } + } + + @Override + public boolean hasColorChannel() { + return false; // FIXME + } + + @Override + protected final void clearImpl0(final GL2ES2 gl, final RegionRenderer renderer) { + for(final Shape s : shapes) { + // s.clearImpl0(gl, renderer);; + s.clear(gl, renderer);; + } + } + + @Override + protected final void destroyImpl0(final GL2ES2 gl, final RegionRenderer renderer) { + for(final Shape s : shapes) { + // s.destroyImpl0(gl, renderer); + s.destroy(gl, renderer);; + } + if( null != border ) { + border.destroy(gl, renderer); + border = null; + } + } + + private boolean doFrustumCulling = false; + + @Override + public final void setFrustumCullingEnabled(final boolean v) { doFrustumCulling = v; } + + @Override + public final boolean isFrustumCullingEnabled() { return doFrustumCulling; } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + protected final void drawImpl0(final GL2ES2 gl, final RegionRenderer renderer, final int[] sampleCount, final Vec4f rgba) { + final PMVMatrix pmv = renderer.getMatrix(); + final Object[] shapesS = shapes.toArray(); + Arrays.sort(shapesS, (Comparator)Shape.ZAscendingComparator); + + final int shapeCount = shapesS.length; + for(int i=0; i<shapeCount; i++) { + final Shape shape = (Shape) shapesS[i]; + if( shape.isEnabled() ) { + pmv.glPushMatrix(); + shape.setTransform(pmv); + + if( !doFrustumCulling || !pmv.getFrustum().isAABBoxOutside( shape.getBounds() ) ) { + if( null == rgba ) { + shape.drawToSelect(gl, renderer, sampleCount); + } else { + shape.draw(gl, renderer, sampleCount); + } + } + pmv.glPopMatrix(); + } + } + if( null != border ) { + if( null == rgba ) { + border.drawToSelect(gl, renderer, sampleCount); + } else { + border.draw(gl, renderer, sampleCount); + } + } + } + + @Override + protected void validateImpl(final GLProfile glp, final GL2ES2 gl) { + if( isShapeDirty() ) { + // box has been reset + final PMVMatrix pmv = new PMVMatrix(); + if( null != layouter ) { + for(final Shape s : shapes) { + if( null != gl ) { + s.validate(gl); + } else { + s.validate(glp); + } + } + layouter.layout(this, box, pmv); + } else { + final AABBox tsbox = new AABBox(); + for(final Shape s : shapes) { + if( null != gl ) { + s.validate(gl); + } else { + s.validate(glp); + } + pmv.glPushMatrix(); + s.setTransform(pmv); + s.getBounds().transformMv(pmv, tsbox); + pmv.glPopMatrix(); + box.resize(tsbox); + } + } + if( hasPadding() ) { + final Padding p = getPadding(); + final Vec3f l = box.getLow(); + final Vec3f h = box.getHigh(); + box.resize(l.x() - p.left, l.y() - p.bottom, l.z()); + box.resize(h.x() + p.right, h.y() + p.top, l.z()); + setRotationPivot( box.getCenter() ); + } + if( hasBorder() ) { + if( null == border ) { + border = new Rectangle(Region.VBAA_RENDERING_BIT, box, getBorderThickness()); + } else { + border.setEnabled(true); + border.setBounds(box, getBorderThickness()); + } + border.setColor(getBorderColor()); + } else if( null != border ) { + border.setEnabled(false); + } + } + } + + @Override + public boolean contains(final Shape s) { + if( shapes.contains(s) ) { + return true; + } + for(final Shape shape : shapes) { + if( shape instanceof Container ) { + if( ((Container)shape).contains(s) ) { + return true; + } + } + } + return false; + } + + @Override + public AABBox getBounds(final PMVMatrix pmv, final Shape shape) { + pmv.reset(); + setTransform(pmv); + final AABBox res = new AABBox(); + if( null == shape ) { + return res; + } + forOne(pmv, shape, () -> { + shape.getBounds().transformMv(pmv, res); + }); + return res; + } + + @Override + public String getSubString() { + return super.getSubString()+", shapes "+shapes.size(); + } + + @Override + public boolean forOne(final PMVMatrix pmv, final Shape shape, final Runnable action) { + return TreeTool.forOne(shapes, pmv, shape, action); + } + + @Override + public boolean forAll(final Visitor1 v) { + return TreeTool.forAll(shapes, v); + } + + @Override + public boolean forAll(final PMVMatrix pmv, final Visitor2 v) { + return TreeTool.forAll(shapes, pmv, v); + } + + @Override + public boolean forSortedAll(final Comparator<Shape> sortComp, final PMVMatrix pmv, final Visitor2 v) { + return TreeTool.forSortedAll(sortComp, shapes, pmv, v); + } +} + diff --git a/src/graphui/classes/com/jogamp/graph/ui/Scene.java b/src/graphui/classes/com/jogamp/graph/ui/Scene.java new file mode 100644 index 000000000..c501b8eb8 --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/Scene.java @@ -0,0 +1,1171 @@ +/** + * Copyright 2010-2023 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package com.jogamp.graph.ui; + +import java.io.File; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; + +import com.jogamp.opengl.FPSCounter; +import com.jogamp.opengl.GL; +import com.jogamp.opengl.GL2ES2; +import com.jogamp.opengl.GLAutoDrawable; +import com.jogamp.opengl.GLCapabilitiesImmutable; +import com.jogamp.opengl.GLEventListener; +import com.jogamp.opengl.GLException; +import com.jogamp.opengl.GLProfile; +import com.jogamp.opengl.GLRunnable; +import com.jogamp.opengl.fixedfunc.GLMatrixFunc; +import com.jogamp.common.nio.Buffers; +import com.jogamp.graph.curve.Region; +import com.jogamp.graph.curve.opengl.GLRegion; +import com.jogamp.graph.curve.opengl.RegionRenderer; +import com.jogamp.graph.curve.opengl.RenderState; +import com.jogamp.graph.ui.Shape.Visitor2; +import com.jogamp.graph.ui.Shape.Visitor1; +import com.jogamp.newt.event.GestureHandler; +import com.jogamp.newt.event.InputEvent; +import com.jogamp.newt.event.MouseEvent; +import com.jogamp.newt.event.MouseListener; +import com.jogamp.newt.event.PinchToZoomGesture; +import com.jogamp.newt.event.GestureHandler.GestureEvent; +import com.jogamp.newt.opengl.GLWindow; +import com.jogamp.opengl.math.FloatUtil; +import com.jogamp.opengl.math.Ray; +import com.jogamp.opengl.math.Recti; +import com.jogamp.opengl.math.Vec2f; +import com.jogamp.opengl.math.Vec3f; +import com.jogamp.opengl.math.geom.AABBox; +import com.jogamp.opengl.util.GLPixelStorageModes; +import com.jogamp.opengl.util.GLReadBufferUtil; +import com.jogamp.opengl.util.PMVMatrix; +import com.jogamp.opengl.util.texture.TextureSequence; + +import jogamp.graph.ui.TreeTool; + +/** + * GraphUI Scene + * <p> + * GraphUI is GPU based and resolution independent. + * </p> + * <p> + * GraphUI is intended to become an immediate- and retained-mode API. + * </p> + * <p> + * To utilize a Scene instance directly as a {@link GLEventListener}, + * user needs to {@link #setClearParams(float[], int)}. + * + * Otherwise user may just call provided {@link GLEventListener} from within their own workflow + * - {@link GLEventListener#init(GLAutoDrawable)} + * - {@link GLEventListener#reshape(GLAutoDrawable, int, int, int, int)} + * - {@link GLEventListener#display(GLAutoDrawable)} + * - {@link GLEventListener#dispose(GLAutoDrawable)} + * </p> + * <p> + * {@link #setPMVMatrixSetup(PMVMatrixSetup)} maybe used to provide a custom {@link PMVMatrix} setup. + * </p> + * @see Shape + */ +public final class Scene implements Container, GLEventListener { + /** Default scene distance on z-axis to projection is -1/5f. */ + public static final float DEFAULT_SCENE_DIST = -1/5f; + /** Default projection angle in degrees value is 45.0. */ + public static final float DEFAULT_ANGLE = 45.0f; + /** Default projection z-near value is 0.1. */ + public static final float DEFAULT_ZNEAR = 0.1f; + /** Default projection z-far value is 7000. */ + public static final float DEFAULT_ZFAR = 7000.0f; + + @SuppressWarnings("unused") + private static final boolean DEBUG = false; + + private final List<Shape> shapes = new ArrayList<Shape>(); + private float dbgBorderThickness = 0f; + private boolean doFrustumCulling = false; + + private float[] clearColor = null; + private int clearMask; + + private final RegionRenderer renderer; + + private final int[] sampleCount = new int[1]; + + /** Describing the bounding box in shape's object model-coordinates of the near-plane parallel at its scene-distance, post {@link #translate(PMVMatrix)} */ + private final AABBox planeBox = new AABBox(0f, 0f, 0f, 0f, 0f, 0f); + + private volatile Shape activeShape = null; + + private SBCMouseListener sbcMouseListener = null; + private SBCGestureListener sbcGestureListener = null; + private PinchToZoomGesture pinchToZoomGesture = null; + + final GLReadBufferUtil screenshot; + + private GLAutoDrawable cDrawable = null; + + private static RegionRenderer createRenderer() { + return RegionRenderer.create(RegionRenderer.defaultBlendEnable, RegionRenderer.defaultBlendDisable); + } + + /** + * Create a new scene with an internally created RegionRenderer + * and using default values {@link #DEFAULT_SCENE_DIST}, {@link #DEFAULT_ANGLE}, {@link #DEFAULT_ZNEAR} and {@link #DEFAULT_ZFAR}. + */ + public Scene() { + this(createRenderer()); + } + + /** + * Create a new scene taking ownership of the given RegionRenderer + * and using default values {@link #DEFAULT_SCENE_DIST}, {@link #DEFAULT_ANGLE}, {@link #DEFAULT_ZNEAR} and {@link #DEFAULT_ZFAR}. + */ + public Scene(final RegionRenderer renderer) { + if( null == renderer ) { + throw new IllegalArgumentException("Null RegionRenderer"); + } + this.renderer = renderer; + this.sampleCount[0] = 4; + this.screenshot = new GLReadBufferUtil(false, false); + } + + /** Returns the associated RegionRenderer */ + public RegionRenderer getRenderer() { return renderer; } + + /** Returns the associated RegionRenderer's RenderState. */ + public RenderState getRenderState() { return renderer.getRenderState(); } + + /** + * Sets the clear parameter for {@link GL#glClearColor(float, float, float, float) glClearColor(..)} and {@link GL#glClear(int) glClear(..)} + * to be issued at {@link #display(GLAutoDrawable)}. + * + * Without setting these parameter, user has to issue + * {@link GL#glClearColor(float, float, float, float) glClearColor(..)} and {@link GL#glClear(int) glClear(..)} + * before calling {@link #display(GLAutoDrawable)}. + * + * @param clearColor {@link GL#glClearColor(float, float, float, float) glClearColor(..)} arguments + * @param clearMask {@link GL#glClear(int) glClear(..)} mask, default is {@link GL#GL_COLOR_BUFFER_BIT} | {@link GL#GL_DEPTH_BUFFER_BIT} + */ + public final void setClearParams(final float[] clearColor, final int clearMask) { this.clearColor = clearColor; this.clearMask = clearMask; } + + /** Returns the {@link GL#glClearColor(float, float, float, float) glClearColor(..)} arguments, see {@link #setClearParams(float[], int)}. */ + public final float[] getClearColor() { return clearColor; } + + /** Returns the {@link GL#glClear(int) glClear(..)} mask, see {@link #setClearParams(float[], int)}. */ + public final int getClearMask() { return clearMask; } + + @Override + public final void setFrustumCullingEnabled(final boolean v) { doFrustumCulling = v; } + + @Override + public final boolean isFrustumCullingEnabled() { return doFrustumCulling; } + + public void attachInputListenerTo(final GLWindow window) { + if(null == sbcMouseListener) { + sbcMouseListener = new SBCMouseListener(); + window.addMouseListener(sbcMouseListener); + sbcGestureListener = new SBCGestureListener(); + window.addGestureListener(sbcGestureListener); + pinchToZoomGesture = new PinchToZoomGesture(window.getNativeSurface(), false); + window.addGestureHandler(pinchToZoomGesture); + } + } + + public void detachInputListenerFrom(final GLWindow window) { + if(null != sbcMouseListener) { + window.removeMouseListener(sbcMouseListener); + sbcMouseListener = null; + window.removeGestureListener(sbcGestureListener); + sbcGestureListener = null; + window.removeGestureHandler(pinchToZoomGesture); + pinchToZoomGesture = null; + } + } + + @Override + public List<Shape> getShapes() { + return shapes; + } + @Override + public void addShape(final Shape s) { + s.setBorder(dbgBorderThickness); + shapes.add(s); + } + @Override + public Shape removeShape(final Shape s) { + s.setBorder(0f); + return shapes.remove(s) ? s : null; + } + @Override + public Shape removeShape(final int idx) { + return shapes.remove(idx); + } + + /** Removes given shape and destroy it. */ + public void removeShape(final GL2ES2 gl, final Shape s) { + s.setBorder(0f); + shapes.remove(s); + s.destroy(gl, renderer); + } + @Override + public void addShapes(final Collection<? extends Shape> shapes) { + for(final Shape s : shapes) { + addShape(s); + } + } + @Override + public void removeShapes(final Collection<? extends Shape> shapes) { + for(final Shape s : shapes) { + removeShape(s); + } + } + /** Removes all given shapes and destroys them. */ + public void removeShapes(final GL2ES2 gl, final Collection<? extends Shape> shapes) { + for(final Shape s : shapes) { + removeShape(gl, s); + } + } + @Override + public void removeAllShapes() { + shapes.clear(); + } + /** Removes all given shapes and destroys them. */ + public void removeAllShapes(final GL2ES2 gl) { + final int count = shapes.size(); + for(int i=count-1; i>=0; --i) { + removeShape(gl, shapes.get(i)); + } + } + + @Override + public boolean contains(final Shape s) { + return false; + } + public Shape getShapeByIdx(final int id) { + if( 0 > id ) { + return null; + } + return shapes.get(id); + } + public Shape getShapeByName(final int name) { + for(final Shape b : shapes) { + if(b.getName() == name ) { + return b; + } + } + return null; + } + + public int getSampleCount() { return sampleCount[0]; } + public int setSampleCount(final int v) { + sampleCount[0] = Math.min(8, Math.max(v, 0)); // clip + markAllShapesDirty(); + return sampleCount[0]; + } + + public void setAllShapesQuality(final int q) { + for(int i=0; i<shapes.size(); i++) { + final Shape shape = shapes.get(i); + if( shape instanceof GraphShape ) { + ((GraphShape)shape).setQuality(q); + } + } + } + public void setAllShapesSharpness(final float sharpness) { + for(int i=0; i<shapes.size(); i++) { + final Shape shape = shapes.get(i); + if( shape instanceof GraphShape ) { + ((GraphShape)shape).setSharpness(sharpness); + } + } + } + public void markAllShapesDirty() { + for(int i=0; i<shapes.size(); i++) { + shapes.get(i).markShapeDirty(); + } + } + + /** + * Sets the debug {@link Shape#setBorder(float) border} thickness for all existing or added shapes, zero for no debug border (default). + * @param v thickness debug border, zero for no border + */ + public final void setDebugBorderBox(final float v) { + dbgBorderThickness = v; + for(int i=0; i<shapes.size(); i++) { + shapes.get(i).setBorder(v); + } + } + + @Override + public void init(final GLAutoDrawable drawable) { + cDrawable = drawable; + renderer.init(drawable.getGL().getGL2ES2()); + } + + /** + * Reshape scene using {@link #setupMatrix(PMVMatrix, int, int, int, int)} using {@link PMVMatrixSetup}. + * <p> + * {@inheritDoc} + * </p> + * @see PMVMatrixSetup + * @see #setPMVMatrixSetup(PMVMatrixSetup) + * @see #setupMatrix(PMVMatrix, int, int, int, int) + * @see #getBounds() + * @see #getBoundsCenter() + */ + @Override + public void reshape(final GLAutoDrawable drawable, final int x, final int y, final int width, final int height) { + renderer.reshapeNotify(x, y, width, height); + + setupMatrix(renderer.getMatrix(), renderer.getViewport()); + pmvMatrixSetup.setPlaneBox(planeBox, renderer.getMatrix(), renderer.getViewport()); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public void display(final GLAutoDrawable drawable) { + final Object[] shapesS = shapes.toArray(); + Arrays.sort(shapesS, (Comparator)Shape.ZAscendingComparator); + + display(drawable, shapesS, false); + } + + private static final int[] sampleCountGLSelect = { -1 }; + + private void display(final GLAutoDrawable drawable, final Object[] shapes, final boolean glSelect) { + final GL2ES2 gl = drawable.getGL().getGL2ES2(); + + final int[] sampleCount0; + if( glSelect ) { + gl.glClearColor(0f, 0f, 0f, 1f); + sampleCount0 = sampleCountGLSelect; + gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT); + } else { + if( null != clearColor ) { + gl.glClearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]); + gl.glClear(clearMask); + } + sampleCount0 = sampleCount; + } + + final PMVMatrix pmv = renderer.getMatrix(); + pmv.glMatrixMode(GLMatrixFunc.GL_MODELVIEW); + + if( glSelect ) { + renderer.enable(gl, true, RegionRenderer.defaultBlendDisable, RegionRenderer.defaultBlendDisable); + } else { + renderer.enable(gl, true); + } + + //final int shapeCount = shapes.size(); + final int shapeCount = shapes.length; + for(int i=0; i<shapeCount; i++) { + // final Shape shape = shapes.get(i); + final Shape shape = (Shape)shapes[i]; + // System.err.println("Id "+i+": "+uiShape); + if( shape.isEnabled() ) { + pmv.glPushMatrix(); + shape.setTransform(pmv); + + if( !doFrustumCulling || !pmv.getFrustum().isAABBoxOutside( shape.getBounds() ) ) { + if( glSelect ) { + final float color = ( i + 1f ) / ( shapeCount + 2f ); + // FIXME + // System.err.printf("drawGL: color %f, index %d of [0..%d[%n", color, i, shapeCount); + renderer.getRenderState().setColorStatic(color, color, color, 1f); + shape.drawToSelect(gl, renderer, sampleCount0); + } else { + shape.draw(gl, renderer, sampleCount0); + } + } + pmv.glPopMatrix(); + } + } + if( glSelect ) { + renderer.enable(gl, false, RegionRenderer.defaultBlendDisable, RegionRenderer.defaultBlendDisable); + } else { + renderer.enable(gl, false); + } + synchronized ( syncDisplayedOnce ) { + displayedOnce = true; + syncDisplayedOnce.notifyAll(); + } + } + + private volatile boolean displayedOnce = false; + private final Object syncDisplayedOnce = new Object(); + + /** Blocks until first {@link #display(GLAutoDrawable)} has completed after construction or {@link #dispose(GLAutoDrawable). */ + public void waitUntilDisplayed() { + synchronized( syncDisplayedOnce ) { + while( !displayedOnce ) { + try { + syncDisplayedOnce.wait(); + } catch (final InterruptedException e) { } + } + } + } + + /** + * Disposes all {@link #addShape(Shape) added} {@link Shape}s. + * <p> + * Implementation also issues {@link RegionRenderer#destroy(GL2ES2)} if set + * and {@link #detachInputListenerFrom(GLWindow)} in case the drawable is of type {@link GLWindow}. + * </p> + * <p> + * {@inheritDoc} + * </p> + */ + @Override + public void dispose(final GLAutoDrawable drawable) { + synchronized ( syncDisplayedOnce ) { + displayedOnce = false; + syncDisplayedOnce.notifyAll(); + } + if( drawable instanceof GLWindow ) { + final GLWindow glw = (GLWindow) drawable; + detachInputListenerFrom(glw); + } + final GL2ES2 gl = drawable.getGL().getGL2ES2(); + for(int i=0; i<shapes.size(); i++) { + shapes.get(i).destroy(gl, renderer); + } + shapes.clear(); + cDrawable = null; + renderer.destroy(gl); + screenshot.dispose(gl); + } + + /** + * Attempt to pick a {@link Shape} using the window coordinates and contained {@ling Shape}'s {@link AABBox} {@link Shape#getBounds() bounds} + * using a ray-intersection algorithm. + * <p> + * If {@link Shape} was found the given action is performed. + * </p> + * <p> + * Method performs on current thread and returns after probing every {@link Shape}. + * </p> + * @param pmv a new {@link PMVMatrix} which will {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti) be setup}, + * {@link Shape#setTransform(PMVMatrix) shape-transformed} and can be reused by the caller and runnable. + * @param glWinX window X coordinate, bottom-left origin + * @param glWinY window Y coordinate, bottom-left origin + * @param objPos storage for found object position in model-space of found {@link Shape} + * @param shape storage for found {@link Shape} or null + * @param runnable the action to perform if {@link Shape} was found + * @return picked Shape if any or null as stored in {@code shape} + */ + public Shape pickShape(final PMVMatrix pmv, final int glWinX, final int glWinY, final Vec3f objPos, final Shape[] shape, final Runnable runnable) { + setupMatrix(pmv); + + final float winZ0 = 0f; + final float winZ1 = 0.3f; + /** + final FloatBuffer winZRB = Buffers.newDirectFloatBuffer(1); + gl.glReadPixels( x, y, 1, 1, GL2ES2.GL_DEPTH_COMPONENT, GL.GL_FLOAT, winZRB); + winZ1 = winZRB.get(0); // dir + */ + final Recti viewport = getViewport(); + final Ray ray = new Ray(); + shape[0] = null; + + forSortedAll(Shape.ZAscendingComparator, pmv, (final Shape s, final PMVMatrix pmv2) -> { + final boolean ok = s.isInteractive() && pmv.gluUnProjectRay(glWinX, glWinY, winZ0, winZ1, viewport, ray); + if( ok ) { + final AABBox sbox = s.getBounds(); + if( sbox.intersectsRay(ray) ) { + // System.err.printf("Pick.0: shape %d, [%d, %d, %f/%f] -> %s%n", i, glWinX, glWinY, winZ0, winZ1, ray); + if( null == sbox.getRayIntersection(objPos, ray, FloatUtil.EPSILON, true) ) { + throw new InternalError("Ray "+ray+", box "+sbox); + } + // System.err.printf("Pick.1: shape %d @ [%f, %f, %f], within %s%n", i, objPos[0], objPos[1], objPos[2], uiShape.getBounds()); + shape[0] = s; + runnable.run(); + return true; + } + } + return false; + }); + return shape[0]; + } + + /** + * Attempt to pick a {@link Shape} using the OpenGL false color rendering. + * <p> + * If {@link Shape} was found the given action is performed on the rendering thread. + * </p> + * <p> + * Method is non blocking and performs on rendering-thread, it returns immediately. + * </p> + * @param glWinX window X coordinate, bottom-left origin + * @param glWinY window Y coordinate, bottom-left origin + * @param objPos storage for found object position in model-space of found {@link Shape} + * @param shape storage for found {@link Shape} or null + * @param runnable the action to perform if {@link Shape} was found + */ + public void pickShapeGL(final int glWinX, final int glWinY, final Vec3f objPos, final Shape[] shape, final Runnable runnable) { + if( null == cDrawable ) { + return; + } + cDrawable.invoke(false, new GLRunnable() { + @Override + public boolean run(final GLAutoDrawable drawable) { + final Shape s = pickShapeGLImpl(drawable, glWinX, glWinY); + shape[0] = s; + if( null != s ) { + final PMVMatrix pmv = renderer.getMatrix(); + pmv.glPushMatrix(); + s.setTransform(pmv); + final boolean ok = null != shape[0].winToShapeCoord(getMatrix(), getViewport(), glWinX, glWinY, objPos); + pmv.glPopMatrix(); + if( ok ) { + runnable.run(); + } + } + return false; // needs to re-render to wash away our false-color glSelect + } } ); + } + @SuppressWarnings({ "unchecked", "rawtypes" }) + private Shape pickShapeGLImpl(final GLAutoDrawable drawable, final int glWinX, final int glWinY) { + final Object[] shapesS = shapes.toArray(); + Arrays.sort(shapesS, (Comparator)Shape.ZAscendingComparator); + + final GLPixelStorageModes psm = new GLPixelStorageModes(); + final ByteBuffer pixel = Buffers.newDirectByteBuffer(4); + + final GL2ES2 gl = drawable.getGL().getGL2ES2(); + + display(drawable, shapesS, true); + + psm.setPackAlignment(gl, 4); + // psm.setUnpackAlignment(gl, 4); + try { + // gl.glReadPixels(glWinX, getHeight() - glWinY, 1, 1, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, pixel); + gl.glReadPixels(glWinX, glWinY, 1, 1, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, pixel); + } catch(final GLException gle) { + gle.printStackTrace(); + return null; + } + psm.restore(gl); + + // final float color = ( i + 1f ) / ( shapeCount + 2f ); + final int shapeCount = shapes.size(); + final int qp = pixel.get(0) & 0xFF; + final float color = qp / 255.0f; + final int index = Math.round( ( color * ( shapeCount + 2f) ) - 1f ); + + // FIXME drawGL: color 0.333333, index 0 of [0..1[ + System.err.printf("pickGL: glWin %d / %d, byte %d, color %f, index %d of [0..%d[%n", + glWinX, glWinY, qp, color, index, shapeCount); + + if( 0 <= index && index < shapeCount ) { + return (Shape)shapesS[index]; + } else { + return null; + } + } + + /** + * Calling {@link Shape#winToObjCoord(Scene, int, int, float[])}, retrieving its Shape object position. + * @param shape + * @param glWinX in GL window coordinates, origin bottom-left + * @param glWinY in GL window coordinates, origin bottom-left + * @param pmv a new {@link PMVMatrix} which will {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti) be setup}, + * {@link Shape#setTransform(PMVMatrix) shape-transformed} and can be reused by the caller and runnable. + * @param objPos resulting object position + * @param runnable action + */ + public void winToShapeCoord(final Shape shape, final int glWinX, final int glWinY, final PMVMatrix pmv, final Vec3f objPos, final Runnable runnable) { + if( null == shape ) { + return; + } + final Recti viewport = getViewport(); + setupMatrix(pmv); + forOne(pmv, shape, () -> { + if( null != shape.winToShapeCoord(pmv, viewport, glWinX, glWinY, objPos) ) { + runnable.run(); + } + }); + } + + @Override + public AABBox getBounds(final PMVMatrix pmv, final Shape shape) { + final AABBox res = new AABBox(); + if( null == shape ) { + return res; + } + setupMatrix(pmv); + forOne(pmv, shape, () -> { + shape.getBounds().transformMv(pmv, res); + }); + return res; + } + + /** + * Traverses through the graph up until {@code shape} and apply {@code action} on it. + * @param pmv {@link PMVMatrix}, which shall be properly initialized, e.g. via {@link Scene#setupMatrix(PMVMatrix)} + * @param shape + * @param action + * @return true to signal operation complete, i.e. {@code shape} found, otherwise false + */ + @Override + public boolean forOne(final PMVMatrix pmv, final Shape shape, final Runnable action) { + return TreeTool.forOne(shapes, pmv, shape, action); + } + + /** + * Traverses through the graph and apply {@link Visitor2#visit(Shape, PMVMatrix)} for each, stop if it returns true. + * @param pmv {@link PMVMatrix}, which shall be properly initialized, e.g. via {@link Scene#setupMatrix(PMVMatrix)} + * @param v + * @return true to signal operation complete and to stop traversal, i.e. {@link Visitor2#visit(Shape, PMVMatrix)} returned true, otherwise false + */ + @Override + public boolean forAll(final PMVMatrix pmv, final Visitor2 v) { + return TreeTool.forAll(shapes, pmv, v); + } + + /** + * Traverses through the graph and apply {@link Visitor1#visit(Shape)} for each, stop if it returns true. + * @param v + * @return true to signal operation complete and to stop traversal, i.e. {@link Visitor1#visit(Shape)} returned true, otherwise false + */ + @Override + public boolean forAll(final Visitor1 v) { + return TreeTool.forAll(shapes, v); + } + + /** + * Traverses through the graph and apply {@link Visitor#visit(Shape, PMVMatrix)} for each, stop if it returns true. + * + * Each {@link Container} level is sorted using {@code sortComp} + * @param sortComp + * @param pmv + * @param v + * @return true to signal operation complete and to stop traversal, i.e. {@link Visitor2#visit(Shape, PMVMatrix)} returned true, otherwise false + */ + @Override + public boolean forSortedAll(final Comparator<Shape> sortComp, final PMVMatrix pmv, final Visitor2 v) { + return TreeTool.forSortedAll(sortComp, shapes, pmv, v); + } + + /** + * Interface providing {@link #set(PMVMatrix, Recti) a method} to + * setup {@link PMVMatrix}'s {@link GLMatrixFunc#GL_PROJECTION} and {@link GLMatrixFunc#GL_MODELVIEW}. + * <p> + * At the end of operations, the {@link GLMatrixFunc#GL_MODELVIEW} matrix has to be selected. + * </p> + * <p> + * Implementation is being called by {@link Scene#setupMatrix(PMVMatrix, int, int, int, int)} + * and hence {@link Scene#reshape(GLAutoDrawable, int, int, int, int)}. + * </p> + * <p> + * Custom implementations can be set via {@link Scene#setPMVMatrixSetup(PMVMatrixSetup)}. + * </p> + * <p> + * The default implementation is described below: + * <ul> + * <li>{@link GLMatrixFunc#GL_PROJECTION} Matrix + * <ul> + * <li>Identity</li> + * <li>Perspective {@link Scene#DEFAULT_ANGLE} with {@link Scene#DEFAULT_ZNEAR} and {@link Scene#DEFAULT_ZFAR}</li> + * <li>Translated to given {@link Scene#DEFAULT_SCENE_DIST}</li> + * <li>Scale (back) to have normalized {@link Scene#getBounds() plane dimensions}, 1 for the greater of width and height.</li> + * </ul></li> + * <li>{@link GLMatrixFunc#GL_MODELVIEW} Matrix + * <ul> + * <li>identity</li> + * </ul></li> + * </ul> + * </p> + */ + public static interface PMVMatrixSetup { + /** + * Setup {@link PMVMatrix}'s {@link GLMatrixFunc#GL_PROJECTION} and {@link GLMatrixFunc#GL_MODELVIEW}. + * <p> + * See {@link PMVMatrixSetup} for details. + * </p> + * <p> + * At the end of operations, the {@link GLMatrixFunc#GL_MODELVIEW} matrix is selected. + * </p> + * @param pmv the {@link PMVMatrix} to setup + * @param viewport Rect4i viewport + */ + void set(PMVMatrix pmv, Recti viewport); + + /** + * Optional method to set the {@link Scene#getBounds()} {@link AABBox}, maybe a {@code nop} if not desired. + * <p> + * Will be called by {@link Scene#reshape(GLAutoDrawable, int, int, int, int)} after {@link #set(PMVMatrix, Recti)}. + * </p> + * @param planeBox the {@link AABBox} to define + * @param pmv the {@link PMVMatrix}, already setup via {@link #set(PMVMatrix, Recti)}. + * @param viewport Rect4i viewport + */ + void setPlaneBox(final AABBox planeBox, final PMVMatrix pmv, Recti viewport); + } + + /** Return the default or {@link #setPMVMatrixSetup(PMVMatrixSetup)} {@link PMVMatrixSetup}. */ + public final PMVMatrixSetup getPMVMatrixSetup() { return pmvMatrixSetup; } + + /** Set a custom {@link PMVMatrixSetup}. */ + public final void setPMVMatrixSetup(final PMVMatrixSetup setup) { pmvMatrixSetup = setup; } + + /** Return the default {@link PMVMatrixSetup}. */ + public static PMVMatrixSetup getDefaultPMVMatrixSetup() { return defaultPMVMatrixSetup; } + + /** + * Setup {@link PMVMatrix} {@link GLMatrixFunc#GL_PROJECTION} and {@link GLMatrixFunc#GL_MODELVIEW} + * by calling {@link #getPMVMatrixSetup()}'s {@link PMVMatrixSetup#set(PMVMatrix, Recti)}. + * @param pmv the {@link PMVMatrix} to setup + * @param Recti viewport + */ + public void setupMatrix(final PMVMatrix pmv, final Recti viewport) { + pmvMatrixSetup.set(pmv, viewport); + } + + /** + * Setup {@link PMVMatrix} {@link GLMatrixFunc#GL_PROJECTION} and {@link GLMatrixFunc#GL_MODELVIEW} + * using implicit {@link #getViewport()} surface dimension by calling {@link #getPMVMatrixSetup()}'s {@link PMVMatrixSetup#set(PMVMatrix, Recti)}. + * @param pmv the {@link PMVMatrix} to setup + */ + public void setupMatrix(final PMVMatrix pmv) { + final Recti viewport = renderer.getViewport(); + setupMatrix(pmv, viewport); + } + + /** Copies the current int[4] viewport in given target and returns it for chaining. It is set after initial {@link #reshape(GLAutoDrawable, int, int, int, int)}. */ + public final Recti getViewport(final Recti target) { return renderer.getViewport(target); } + + /** Borrows the current int[4] viewport w/o copying. It is set after initial {@link #reshape(GLAutoDrawable, int, int, int, int)}. */ + public Recti getViewport() { return renderer.getViewport(); } + + /** Returns the {@link #getViewport()}'s width, set after initial {@link #reshape(GLAutoDrawable, int, int, int, int)}. */ + public int getWidth() { return renderer.getWidth(); } + /** Returns the {@link #getViewport()}'s height, set after initial {@link #reshape(GLAutoDrawable, int, int, int, int)}. */ + public int getHeight() { return renderer.getHeight(); } + + /** Borrow the current {@link PMVMatrix}. */ + public PMVMatrix getMatrix() { return renderer.getMatrix(); } + + /** + * Describing the scene's object model-dimensions of the plane at scene-distance covering the visible viewport rectangle. + * <p> + * The value is evaluated at {@link #reshape(GLAutoDrawable, int, int, int, int)} via {@link } + * </p> + * <p> + * {@link AABBox#getWidth()} and {@link AABBox#getHeight()} define scene's dimension covered by surface size. + * </p> + * <p> + * {@link AABBox} is setup via {@link #getPMVMatrixSetup()}'s {@link PMVMatrixSetup#setPlaneBox(AABBox, PMVMatrix, Recti)}. + * </p> + * <p> + * The default {@link PMVMatrixSetup} implementation scales to normalized plane dimensions, 1 for the greater of width and height. + * </p> + */ + public AABBox getBounds() { return planeBox; } + + /** + * + * @param pmv + * @param viewport + * @param zNear + * @param zFar + * @param winX + * @param winY + * @param objOrthoZ + * @param objPos float[3] storage for object coord result + * @param winZ + */ + public static void winToPlaneCoord(final PMVMatrix pmv, final Recti viewport, + final float zNear, final float zFar, + final float winX, final float winY, final float objOrthoZ, + final Vec3f objPos) { + final float winZ = FloatUtil.getOrthoWinZ(objOrthoZ, zNear, zFar); + pmv.gluUnProject(winX, winY, winZ, viewport, objPos); + } + + /** + * Map given window surface-size to object coordinates relative to this scene using + * the give projection parameters. + * @param viewport viewport rectangle + * @param zNear custom {@link #DEFAULT_ZNEAR} + * @param zFar custom {@link #DEFAULT_ZFAR} + * @param objOrthoDist custom {@link #DEFAULT_SCENE_DIST} + * @param objSceneSize Vec2f storage for object surface size result + */ + public void surfaceToPlaneSize(final Recti viewport, final float zNear, final float zFar, final float objOrthoDist, final Vec2f objSceneSize) { + final PMVMatrix pmv = new PMVMatrix(); + setupMatrix(pmv, viewport); + { + final Vec3f obj00Coord = new Vec3f(); + final Vec3f obj11Coord = new Vec3f(); + + winToPlaneCoord(pmv, viewport, DEFAULT_ZNEAR, DEFAULT_ZFAR, viewport.x(), viewport.y(), objOrthoDist, obj00Coord); + winToPlaneCoord(pmv, viewport, DEFAULT_ZNEAR, DEFAULT_ZFAR, viewport.width(), viewport.height(), objOrthoDist, obj11Coord); + objSceneSize.set( obj11Coord.x() - obj00Coord.x(), + obj11Coord.y() - obj00Coord.y() ); + } + } + + /** + * Map given window surface-size to object coordinates relative to this scene using + * the default {@link PMVMatrixSetup}, i.e. {@link #DEFAULT_ZNEAR}, {@link #DEFAULT_ZFAR} and {@link #DEFAULT_SCENE_DIST} + * @param viewport viewport rectangle + * @param objSceneSize Vec2f storage for object surface size result + */ + public void surfaceToPlaneSize(final Recti viewport, final Vec2f objSceneSize) { + surfaceToPlaneSize(viewport, DEFAULT_ZNEAR, DEFAULT_ZFAR, -DEFAULT_SCENE_DIST, objSceneSize); + } + + public final Shape getActiveShape() { + return activeShape; + } + + public void releaseActiveShape() { + activeShape = null; + } + private void setActiveShape(final Shape shape) { + activeShape = shape; + } + + private final class SBCGestureListener implements GestureHandler.GestureListener { + @Override + public void gestureDetected(final GestureEvent gh) { + if( null != activeShape ) { + // gesture .. delegate to active shape! + final InputEvent orig = gh.getTrigger(); + if( orig instanceof MouseEvent ) { + final Shape shape = activeShape; + if( shape.isInteractive() ) { + final MouseEvent e = (MouseEvent) orig; + // flip to GL window coordinates + final int glWinX = e.getX(); + final int glWinY = getHeight() - e.getY() - 1; + final PMVMatrix pmv = new PMVMatrix(); + final Vec3f objPos = new Vec3f(); + winToShapeCoord(shape, glWinX, glWinY, pmv, objPos, () -> { + shape.dispatchGestureEvent(gh, glWinX, glWinY, pmv, renderer.getViewport(), objPos); + }); + } + } + } + } + } + + /** + * Dispatch mouse event, either directly sending to activeShape or picking one + * @param e original Newt {@link MouseEvent} + * @param glWinX in GL window coordinates, origin bottom-left + * @param glWinY in GL window coordinates, origin bottom-left + */ + final void dispatchMouseEvent(final MouseEvent e, final int glWinX, final int glWinY) { + if( null == activeShape ) { + dispatchMouseEventPickShape(e, glWinX, glWinY); + } else if( activeShape.isInteractive() ) { + dispatchMouseEventForShape(activeShape, e, glWinX, glWinY); + } + } + /** + * Pick the shape using the event coordinates + * @param e original Newt {@link MouseEvent} + * @param glWinX in GL window coordinates, origin bottom-left + * @param glWinY in GL window coordinates, origin bottom-left + */ + final void dispatchMouseEventPickShape(final MouseEvent e, final int glWinX, final int glWinY) { + final PMVMatrix pmv = new PMVMatrix(); + final Vec3f objPos = new Vec3f(); + final Shape[] shape = { null }; + if( null == pickShape(pmv, glWinX, glWinY, objPos, shape, () -> { + setActiveShape(shape[0]); + shape[0].dispatchMouseEvent(e, glWinX, glWinY, objPos); + } ) ) + { + releaseActiveShape(); + } + } + /** + * Dispatch event to shape + * @param shape target active shape of event + * @param e original Newt {@link MouseEvent} + * @param glWinX in GL window coordinates, origin bottom-left + * @param glWinY in GL window coordinates, origin bottom-left + */ + final void dispatchMouseEventForShape(final Shape shape, final MouseEvent e, final int glWinX, final int glWinY) { + final PMVMatrix pmv = new PMVMatrix(); + final Vec3f objPos = new Vec3f(); + winToShapeCoord(shape, glWinX, glWinY, pmv, objPos, () -> { shape.dispatchMouseEvent(e, glWinX, glWinY, objPos); }); + } + + private class SBCMouseListener implements MouseListener { + int lx=-1, ly=-1, lId=-1; + + void clear() { + lx = -1; ly = -1; lId = -1; + } + + @Override + public void mousePressed(final MouseEvent e) { + if( -1 == lId || e.getPointerId(0) == lId ) { + lx = e.getX(); + ly = e.getY(); + lId = e.getPointerId(0); + } + // flip to GL window coordinates, origin bottom-left + final int glWinX = e.getX(); + final int glWinY = getHeight() - e.getY() - 1; + dispatchMouseEvent(e, glWinX, glWinY); + } + + @Override + public void mouseReleased(final MouseEvent e) { + // flip to GL window coordinates, origin bottom-left + final int glWinX = e.getX(); + final int glWinY = getHeight() - e.getY() - 1; + dispatchMouseEvent(e, glWinX, glWinY); + if( 1 == e.getPointerCount() ) { + // Release active shape: last pointer has been lifted! + releaseActiveShape(); + clear(); + } + } + + @Override + public void mouseClicked(final MouseEvent e) { + // flip to GL window coordinates + final int glWinX = e.getX(); + final int glWinY = getHeight() - e.getY() - 1; + // activeId should have been released by mouseRelease() already! + dispatchMouseEventPickShape(e, glWinX, glWinY); + // Release active shape: last pointer has been lifted! + releaseActiveShape(); + clear(); + } + + @Override + public void mouseDragged(final MouseEvent e) { + // drag activeShape, if no gesture-activity, only on 1st pointer + if( null != activeShape && !pinchToZoomGesture.isWithinGesture() && e.getPointerId(0) == lId ) { + lx = e.getX(); + ly = e.getY(); + + // dragged .. delegate to active shape! + // flip to GL window coordinates, origin bottom-left + final int glWinX = lx; + final int glWinY = getHeight() - ly - 1; + dispatchMouseEventForShape(activeShape, e, glWinX, glWinY); + } + } + + @Override + public void mouseWheelMoved(final MouseEvent e) { + // flip to GL window coordinates + final int glWinX = lx; + final int glWinY = getHeight() - ly - 1; + dispatchMouseEvent(e, glWinX, glWinY); + } + + @Override + public void mouseMoved(final MouseEvent e) { + if( -1 == lId || e.getPointerId(0) == lId ) { + lx = e.getX(); + ly = e.getY(); + lId = e.getPointerId(0); + } + final int glWinX = lx; + final int glWinY = getHeight() - ly - 1; + // dispatchMouseEvent(e, glWinX, glWinY); + dispatchMouseEventPickShape(e, glWinX, glWinY); + } + @Override + public void mouseEntered(final MouseEvent e) { } + @Override + public void mouseExited(final MouseEvent e) { + releaseActiveShape(); + clear(); + } + } + + /** + * Return a formatted status string containing avg fps and avg frame duration. + * @param glad GLAutoDrawable instance for FPSCounter, its chosen GLCapabilities and its GL's swap-interval + * @param renderModes render modes for {@link Region#getRenderModeString(int, int, int)} + * @param quality the Graph-Curve quality setting or -1 to be ignored + * @param dpi the monitor's DPI (vertical preferred) + * @return formatted status string + */ + public String getStatusText(final GLAutoDrawable glad, final int renderModes, final int quality, final float dpi) { + final FPSCounter fpsCounter = glad.getAnimator(); + final float lfps, tfps, td; + if( null != fpsCounter ) { + lfps = fpsCounter.getLastFPS(); + tfps = fpsCounter.getTotalFPS(); + td = (float)fpsCounter.getLastFPSPeriod() / (float)fpsCounter.getUpdateFPSFrames(); + } else { + lfps = 0f; + tfps = 0f; + td = 0f; + } + final GLCapabilitiesImmutable caps = glad.getChosenGLCapabilities(); + final String modeS = Region.getRenderModeString(renderModes, getSampleCount(), caps.getNumSamples()); + final String qualityStr, blendStr; + if( 0 <= quality ) { + qualityStr = ", q "+quality; + } else { + qualityStr = ""; + } + if( getRenderState().isHintMaskSet(RenderState.BITHINT_BLENDING_ENABLED) ) { + blendStr = ", blend"; + } else { + blendStr = ""; + } + return String.format("%03.1f/%03.1f fps, %.1f ms/f, vsync %d, dpi %.1f, %s%s%s, a %d", + lfps, tfps, td, glad.getGL().getSwapInterval(), dpi, modeS, qualityStr, blendStr, caps.getAlphaBits()); + } + + /** + * Return a formatted status string containing avg fps and avg frame duration. + * @param fpsCounter the counter, must not be null + * @return formatted status string + */ + public static String getStatusText(final FPSCounter fpsCounter) { + final float lfps = fpsCounter.getLastFPS(); + final float tfps = fpsCounter.getTotalFPS(); + final float td = (float)fpsCounter.getLastFPSPeriod() / (float)fpsCounter.getUpdateFPSFrames(); + return String.format("%03.1f/%03.1f fps, %.1f ms/f", lfps, tfps, td); + } + + /** + * Return the unique next technical screenshot PNG {@link File} instance as follows: + * <pre> + * filename = [{dir}][{prefix}-]{@link Region#getRenderModeString(int, int, int)}[-{contentDetails}]-snap{screenShotCount}-{resolution}.png + * </pre> + * Implementation increments {@link #getScreenshotCount()}. + * + * @param dir the target directory, may be `null` or an empty string + * @param prefix the prefix, may be `null` or an empty string + * @param renderModes the used Graph renderModes, see {@link GLRegion#create(GLProfile, int, TextureSequence) create(..)} + * @param caps the used {@link GLCapabilitiesImmutable} used to retrieved the full-screen AA (fsaa) {@link GLCapabilitiesImmutable#getNumSamples()} + * @param contentDetail user content details to be added at the end but before {@link #getScreenshotCount()}, may be `null` or an empty string + * @return a unique descriptive screenshot filename + * @see #screenshot(GL, File) + * @see #screenshot(boolean, File) + * @see #getScreenshotCount() + */ + public File nextScreenshotFile(final String dir, final String prefix, final int renderModes, final GLCapabilitiesImmutable caps, final String contentDetail) { + final String dir2 = ( null != dir && dir.length() > 0 ) ? dir : ""; + final String prefix2 = ( null != prefix && prefix.length() > 0 ) ? prefix+"-" : ""; + final RegionRenderer renderer = getRenderer(); + final String modeS = Region.getRenderModeString(renderModes, getSampleCount(), caps.getNumSamples()); + final String contentDetail2 = ( null != contentDetail && contentDetail.length() > 0 ) ? contentDetail+"-" : ""; + return new File( String.format((Locale)null, "%s%s%s-%ssnap%02d-%04dx%04d.png", + dir2, prefix2, modeS, contentDetail2, + screenShotCount++, renderer.getWidth(), renderer.getHeight() ) ); + } + private int screenShotCount = 0; + + /** Return the number of {@link #nextScreenshotFile(String, String, int, GLCapabilitiesImmutable, String)} calls. */ + public int getScreenshotCount() { return screenShotCount; } + + /** + * Write current read drawable (screen) to a file. + * <p> + * Best to be {@link GLAutoDrawable#invoke(boolean, GLRunnable) invoked on the display call}, + * see {@link #screenshot(boolean, String)}. + * </p> + * @param gl current GL object + * @param file the target file to be used, consider using {@link #nextScreenshotFile(String, String, int, GLCapabilitiesImmutable, String)} + * @see #nextScreenshotFile(String, String, int, GLCapabilitiesImmutable, String) + * @see #getScreenshotCount() + * @see #screenshot(boolean, File) + */ + public void screenshot(final GL gl, final File file) { + if(screenshot.readPixels(gl, false)) { + screenshot.write(file); + System.err.println("Wrote: "+file); + } + } + + /** + * Write current read drawable (screen) to a file on {@link GLAutoDrawable#invoke(boolean, GLRunnable) on the display call}. + * + * @param wait if true block until execution of screenshot {@link GLRunnable} is finished, otherwise return immediately w/o waiting + * @param file the target file to be used, consider using {@link #nextScreenshotFile(String, String, int, GLCapabilitiesImmutable, String)} + * @see #nextScreenshotFile(String, String, int, GLCapabilitiesImmutable, String) + * @see #getScreenshotCount() + * @see #screenshot(GL, File) + */ + public void screenshot(final boolean wait, final File file) { + if( null != cDrawable ) { + cDrawable.invoke(wait, (drawable) -> { + screenshot(drawable.getGL(), file); + return true; + }); + } + } + + private static final PMVMatrixSetup defaultPMVMatrixSetup = new PMVMatrixSetup() { + @Override + public void set(final PMVMatrix pmv, final Recti viewport) { + final float ratio = (float)viewport.width()/(float)viewport.height(); + pmv.glMatrixMode(GLMatrixFunc.GL_PROJECTION); + pmv.glLoadIdentity(); + pmv.gluPerspective(DEFAULT_ANGLE, ratio, DEFAULT_ZNEAR, DEFAULT_ZFAR); + pmv.glTranslatef(0f, 0f, DEFAULT_SCENE_DIST); + + pmv.glMatrixMode(GLMatrixFunc.GL_MODELVIEW); + pmv.glLoadIdentity(); + + // Scale (back) to have normalized plane dimensions, 1 for the greater of width and height. + final AABBox planeBox0 = new AABBox(); + setPlaneBox(planeBox0, pmv, viewport); + final float sx = planeBox0.getWidth(); + final float sy = planeBox0.getHeight(); + final float sxy = sx > sy ? sx : sy; + pmv.glMatrixMode(GLMatrixFunc.GL_PROJECTION); + pmv.glScalef(sxy, sxy, 1f); + pmv.glMatrixMode(GLMatrixFunc.GL_MODELVIEW); + } + + @Override + public void setPlaneBox(final AABBox planeBox, final PMVMatrix pmv, final Recti viewport) { + final float orthoDist = -DEFAULT_SCENE_DIST; + final Vec3f obj00Coord = new Vec3f(); + final Vec3f obj11Coord = new Vec3f(); + + winToPlaneCoord(pmv, viewport, DEFAULT_ZNEAR, DEFAULT_ZFAR, viewport.x(), viewport.y(), orthoDist, obj00Coord); + winToPlaneCoord(pmv, viewport, DEFAULT_ZNEAR, DEFAULT_ZFAR, viewport.width(), viewport.height(), orthoDist, obj11Coord); + + planeBox.setSize( obj00Coord, obj11Coord ); + } + }; + private PMVMatrixSetup pmvMatrixSetup = defaultPMVMatrixSetup; + +} diff --git a/src/graphui/classes/com/jogamp/graph/ui/Shape.java b/src/graphui/classes/com/jogamp/graph/ui/Shape.java new file mode 100644 index 000000000..ba1e50b1c --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/Shape.java @@ -0,0 +1,1398 @@ +/** + * Copyright 2010-2023 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package com.jogamp.graph.ui; + +import java.util.ArrayList; +import java.util.Comparator; + +import com.jogamp.nativewindow.NativeWindowException; +import com.jogamp.opengl.GL2ES2; +import com.jogamp.opengl.GLProfile; +import com.jogamp.opengl.fixedfunc.GLMatrixFunc; +import com.jogamp.graph.curve.opengl.RegionRenderer; +import com.jogamp.graph.ui.layout.Padding; +import com.jogamp.newt.event.GestureHandler.GestureEvent; +import com.jogamp.newt.event.GestureHandler.GestureListener; +import com.jogamp.newt.event.MouseAdapter; +import com.jogamp.newt.event.NEWTEvent; +import com.jogamp.newt.event.PinchToZoomGesture; +import com.jogamp.newt.event.MouseEvent; +import com.jogamp.newt.event.MouseListener; +import com.jogamp.opengl.math.FloatUtil; +import com.jogamp.opengl.math.Matrix4f; +import com.jogamp.opengl.math.Quaternion; +import com.jogamp.opengl.math.Recti; +import com.jogamp.opengl.math.Vec2f; +import com.jogamp.opengl.math.Vec3f; +import com.jogamp.opengl.math.Vec4f; +import com.jogamp.opengl.math.geom.AABBox; +import com.jogamp.opengl.util.PMVMatrix; + +/** + * Generic Shape, potentially using a Graph via {@link GraphShape} or other means of representing content. + * <p> + * A shape includes the following build-in user-interactions + * - drag shape w/ 1-pointer click, see {@link #setDraggable(boolean)} + * - resize shape w/ 1-pointer click and drag in 1/4th bottom-left and bottom-right corner, see {@link #setResizable(boolean)}. + * </p> + * <p> + * A shape is expected to have its 0/0 origin in its bottom-left corner, otherwise the drag-zoom sticky-edge will not work as expected. + * </p> + * <p> + * A shape's {@link #getBounds()} includes its optional {@link #getPadding()} and optional {@link #getBorderThickness()}. + * </p> + * <p> + * GraphUI is GPU based and resolution independent. + * </p> + * <p> + * GraphUI is intended to become an immediate- and retained-mode API. + * </p> + * <p> + * Default colors (toggle-off is full color): + * - non-toggle: 0.6 * color, static -> 0.6 + * - pressed: 0.8 * color, static -> 0.5 + * - toggle-off: 1.0 * color, static -> 0.6 + * - toggle-on: 0.8 * color + * </p> + * @see Scene + */ +public abstract class Shape { + /** + * General {@link Shape} visitor + */ + public static interface Visitor1 { + /** + * Visitor method + * @param s the {@link Shape} to process + * @return true to signal operation complete and to stop traversal, otherwise false + */ + boolean visit(Shape s); + } + + /** + * General {@link Shape} visitor + */ + public static interface Visitor2 { + /** + * Visitor method + * @param s the {@link Shape} to process + * @param pmv the {@link PMVMatrix} setup from the {@link Scene} down to the {@link Shape} + * @return true to signal operation complete and to stop traversal, otherwise false + */ + boolean visit(Shape s, final PMVMatrix pmv); + } + + /** + * General {@link Shape} listener action + */ + public static interface Listener { + void run(final Shape shape); + } + /** + * {@link Shape} listener action returning a boolean value + */ + public static interface ListenerBool { + boolean run(final Shape shape); + } + protected static final boolean DEBUG_DRAW = false; + private static final boolean DEBUG = false; + + private static final int DIRTY_SHAPE = 1 << 0 ; + private static final int DIRTY_STATE = 1 << 1 ; + + protected final AABBox box; + + private final Vec3f position = new Vec3f(); + private final Quaternion rotation = new Quaternion(); + private Vec3f rotPivot = null; + private final Vec3f scale = new Vec3f(1f, 1f, 1f); + + private volatile int dirty = DIRTY_SHAPE | DIRTY_STATE; + private final Object dirtySync = new Object(); + + /** Default base-color w/o color channel, will be modulated w/ pressed- and toggle color */ + protected final Vec4f rgbaColor = new Vec4f(0.60f, 0.60f, 0.60f, 1.0f); + /** Default pressed color-factor (darker and slightly transparent), modulated base-color. ~0.65 (due to alpha) */ + protected final Vec4f pressedRGBAModulate = new Vec4f(0.70f, 0.70f, 0.70f, 0.8f); + /** Default toggle color-factor (darkest), modulated base-color. 0.60 * 0.83 ~= 0.50 */ + protected final Vec4f toggleOnRGBAModulate = new Vec4f(0.83f, 0.83f, 0.83f, 1.0f); + /** Default toggle color-factor, modulated base-color. 0.60 * 1.00 ~= 0.60 */ + protected final Vec4f toggleOffRGBAModulate = new Vec4f(1.00f, 1.00f, 1.00f, 1.0f); + + private final Vec4f rgba_tmp = new Vec4f(0, 0, 0, 1); + private final Vec4f cWhite = new Vec4f(1, 1, 1, 1); + + private int name = -1; + + private boolean down = false; + private boolean toggle = false; + private boolean toggleable = false; + private boolean draggable = true; + private boolean resizable = true; + private boolean interactive = true; + private boolean enabled = true; + private float borderThickness = 0f; + private Padding padding = null; + private final Vec4f borderColor = new Vec4f(0.0f, 0.0f, 0.0f, 1.0f); + private ArrayList<MouseGestureListener> mouseListeners = new ArrayList<MouseGestureListener>(); + + private ListenerBool onInitListener = null; + private Listener onMoveListener = null; + private Listener onToggleListener = null; + private Listener onClickedListener = null; + + public Shape() { + this.box = new AABBox(); + } + + /** Set a symbolic name for this shape for identification. Default is -1 for noname. */ + public final Shape setName(final int name) { this.name = name; return this; } + /** Return the optional symbolic name for this shape. */ + public final int getName() { return this.name; } + + /** Returns true if this shape is enabled and hence visible, otherwise false. */ + public final boolean isEnabled() { return enabled; } + /** Enable or disable this shape, i.e. its visibility. */ + public final Shape setEnabled(final boolean v) { enabled = v; return this; } + + /** + * Sets the padding for this shape, which is included in {@link #getBounds()B} and also includes the border. Default is zero. + * + * Method issues {@link #markShapeDirty()}. + * + * @param padding distance of shape to the border, i.e. padding + * @return this shape for chaining + * @see #getPadding() + * @see #hasPadding() + */ + public final Shape setPaddding(final Padding padding) { + this.padding = padding; + markShapeDirty(); + return this; + } + + /** + * Returns {@link Padding} of this shape, which is included in {@link #getBounds()B} and also includes the border. Default is zero. + * @see #setPaddding(Padding) + * @see #hasPadding() + */ + public Padding getPadding() { return padding; } + + /** Returns true if {@link #setPaddding(Padding)} added a non {@link Padding#zeroSumSize()} spacing to this shape. */ + public boolean hasPadding() { return null != padding && !padding.zeroSumSize(); } + + /** + * Sets the thickness of the border, which is included in {@link #getBounds()} and is outside of {@link #getPadding()}. Default is zero for no border. + * + * Method issues {@link #markShapeDirty()}. + * + * @param thickness border thickness, zero for no border + * @return this shape for chaining + */ + public final Shape setBorder(final float thickness) { + borderThickness = Math.max(0f, thickness); + markShapeDirty(); + return this; + } + /** Returns true if a border has been enabled via {@link #setBorder(float, Padding)}. */ + public final boolean hasBorder() { return !FloatUtil.isZero(borderThickness); } + + /** Returns the border thickness, see {@link #setBorder(float, Padding)}. */ + public final float getBorderThickness() { return borderThickness; } + + /** + * Clears all data and reset all states as if this instance was newly created + * @param gl TODO + * @param renderer TODO + */ + public final void clear(final GL2ES2 gl, final RegionRenderer renderer) { + synchronized ( dirtySync ) { + clearImpl0(gl, renderer); + position.set(0f, 0f, 0f); + rotation.setIdentity(); + rotPivot = null; + scale.set(1f, 1f, 1f); + box.reset(); + markShapeDirty(); + } + } + + /** + * Destroys all data + * @param gl + * @param renderer + */ + public final void destroy(final GL2ES2 gl, final RegionRenderer renderer) { + destroyImpl0(gl, renderer); + position.set(0f, 0f, 0f); + rotation.setIdentity(); + rotPivot = null; + scale.set(1f, 1f, 1f); + box.reset(); + markShapeDirty(); + } + + /** + * Set a user one-shot initializer callback. + * <p> + * {@link ListenerBool#run(Shape)} will be called + * after each {@link #draw(GL2ES2, RegionRenderer, int[])} + * until it returns true, signaling user initialization is completed. + * </p> + * @param l callback, which shall return true signaling user initialization is done + */ + public final void onInit(final ListenerBool l) { onInitListener = l; } + public final void onMove(final Listener l) { onMoveListener = l; } + public final void onToggle(final Listener l) { onToggleListener = l; } + public final void onClicked(final Listener l) { onClickedListener = l; } + + /** Move to scaled position. Position ends up in PMVMatrix unmodified. */ + public final Shape moveTo(final float tx, final float ty, final float tz) { + position.set(tx, ty, tz); + if( null != onMoveListener ) { + onMoveListener.run(this); + } + return this; + } + + /** Move to scaled position. Position ends up in PMVMatrix unmodified. */ + public final Shape moveTo(final Vec3f t) { + position.set(t); + if( null != onMoveListener ) { + onMoveListener.run(this); + } + return this; + } + + /** Move about scaled distance. Position ends up in PMVMatrix unmodified. */ + public final Shape move(final float dtx, final float dty, final float dtz) { + position.add(dtx, dty, dtz); + if( null != onMoveListener ) { + onMoveListener.run(this); + } + return this; + } + + /** Move about scaled distance. Position ends up in PMVMatrix unmodified. */ + public final Shape move(final Vec3f dt) { + position.add(dt); + if( null != onMoveListener ) { + onMoveListener.run(this); + } + return this; + } + + /** Returns position, i.e. scaled translation as set via {@link #moveTo(float, float, float) or {@link #move(float, float, float)}}. */ + public final Vec3f getPosition() { return position; } + + /** Returns {@link Quaternion} for rotation. */ + public final Quaternion getRotation() { return rotation; } + /** Return unscaled rotation origin, aka pivot. Null if not set via {@link #getRotationPivot()}. */ + public final Vec3f getRotationPivot() { return rotPivot; } + /** + * Set unscaled rotation origin, aka pivot. Usually the {@link #getBounds()} center and should be set while {@link #validateImpl(GLProfile, GL2ES2)}. + * @return this shape for chaining + */ + public final Shape setRotationPivot(final float px, final float py, final float pz) { + rotPivot = new Vec3f(px, py, pz); + return this; + } + /** + * Set unscaled rotation origin, aka pivot. Usually the {@link #getBounds()} center and should be set while {@link #validateImpl(GLProfile, GL2ES2)}. + * @param pivot rotation origin + * @return this shape for chaining + */ + public final Shape setRotationPivot(final Vec3f pivot) { + rotPivot = new Vec3f(pivot); + return this; + } + + /** + * Set scale factor to given scale. + * @see #scale(float, float, float) + * @see #getScale() + */ + public final Shape setScale(final float sx, final float sy, final float sz) { + scale.set(sx, sy, sz); + return this; + } + /** + * Multiply current scale factor by given scale. + * @see #setScale(float, float, float) + * @see #getScale() + */ + public final Shape scale(final float sx, final float sy, final float sz) { + scale.scale(sx, sy, sz); + return this; + } + /** + * Returns scale factors. + * @see #setScale(float, float, float) + * @see #scale(float, float, float) + */ + public final Vec3f getScale() { return scale; } + + /** + * Marks the shape dirty, causing next {@link #draw(GL2ES2, RegionRenderer, int[]) draw()} + * to recreate the Graph shape and reset the region. + */ + public final void markShapeDirty() { + synchronized ( dirtySync ) { + dirty |= DIRTY_SHAPE; + } + } + + /** + * Marks the rendering state dirty, causing next {@link #draw(GL2ES2, RegionRenderer, int[]) draw()} + * to notify the Graph region to reselect shader and repaint potentially used FBOs. + */ + public final void markStateDirty() { + synchronized ( dirtySync ) { + dirty |= DIRTY_STATE; + } + } + + protected final boolean isShapeDirty() { + return 0 != ( dirty & DIRTY_SHAPE ) ; + } + protected final boolean isStateDirty() { + return 0 != ( dirty & DIRTY_STATE ) ; + } + + /** + * Returns the unscaled bounding {@link AABBox} for this shape, borrowing internal instance. + * + * The returned {@link AABBox} will cover the unscaled shape + * as well as its optional {@link #getPadding()} and optional {@link #getBorderThickness()}. + * + * The returned {@link AABBox} is only valid after an initial call to {@link #draw(GL2ES2, RegionRenderer, int[]) draw(..)} + * or {@link #validate(GL2ES2)}. + * + * @see #getBounds(GLProfile) + */ + public final AABBox getBounds() { return box; } + + /** + * Returns the scaled width of the bounding {@link AABBox} for this shape. + * + * The returned width will cover the scaled shape + * as well as its optional scaled {@link #getPadding()} and optional scaled {@link #getBorderThickness()}. + * + * The returned width is only valid after an initial call to {@link #draw(GL2ES2, RegionRenderer, int[]) draw(..)} + * or {@link #validate(GL2ES2)}. + * + * @see #getBounds() + */ + public final float getScaledWidth() { + return box.getWidth() * getScale().x(); + } + + /** + * Returns the scaled height of the bounding {@link AABBox} for this shape. + * + * The returned height will cover the scaled shape + * as well as its optional scaled {@link #getPadding()} and optional scaled {@link #getBorderThickness()}. + * + * The returned height is only valid after an initial call to {@link #draw(GL2ES2, RegionRenderer, int[]) draw(..)} + * or {@link #validate(GL2ES2)}. + * + * @see #getBounds() + */ + public final float getScaledHeight() { + return box.getHeight() * getScale().y(); + } + + /** + * Returns the unscaled bounding {@link AABBox} for this shape. + * + * This variant differs from {@link #getBounds()} as it + * returns a valid {@link AABBox} even before {@link #draw(GL2ES2, RegionRenderer, int[]) draw(..)} + * and having an OpenGL instance available. + * + * @see #getBounds() + */ + public final AABBox getBounds(final GLProfile glp) { + validate(glp); + return box; + } + + /** Experimental selection draw command used by {@link Scene}. */ + public void drawToSelect(final GL2ES2 gl, final RegionRenderer renderer, final int[] sampleCount) { + synchronized ( dirtySync ) { + validate(gl); + drawImpl0(gl, renderer, sampleCount, null); + } + } + + /** + * Renders the shape. + * <p> + * {@link #setTransform(PMVMatrix)} is expected to be completed beforehand. + * </p> + * @param gl the current GL object + * @param renderer {@link RegionRenderer} which might be used for Graph Curve Rendering, also source of {@link RegionRenderer#getMatrix()} and {@link RegionRenderer#getViewport()}. + * @param sampleCount sample count if used by Graph renderModes + */ + public void draw(final GL2ES2 gl, final RegionRenderer renderer, final int[] sampleCount) { + final boolean isPressed = isPressed(), isToggleOn = isToggleOn(); + final Vec4f rgba; + if( hasColorChannel() ) { + if( isPressed ) { + rgba = pressedRGBAModulate; + } else if( isToggleable() ) { + if( isToggleOn ) { + rgba = toggleOnRGBAModulate; + } else { + rgba = toggleOffRGBAModulate; + } + } else { + rgba = cWhite; + } + } else { + rgba = rgba_tmp; + if( isPressed ) { + rgba.mul(rgbaColor, pressedRGBAModulate); + } else if( isToggleable() ) { + if( isToggleOn ) { + rgba.mul(rgbaColor, toggleOnRGBAModulate); + } else { + rgba.mul(rgbaColor, toggleOffRGBAModulate); + } + } else { + rgba.set(rgbaColor); + } + } + synchronized ( dirtySync ) { + validate(gl); + drawImpl0(gl, renderer, sampleCount, rgba); + } + if( null != onInitListener ) { + if( onInitListener.run(this) ) { + onInitListener = null; + } + } + } + + /** + * Validates the shape's underlying {@link GLRegion}. + * <p> + * If the region is dirty, it gets {@link GLRegion#clear(GL2ES2) cleared} and is reused. + * </p> + * @param gl current {@link GL2ES2} object + * @see #validate(GLProfile) + */ + public final Shape validate(final GL2ES2 gl) { + synchronized ( dirtySync ) { + if( isShapeDirty() ) { + box.reset(); + } + validateImpl(gl.getGLProfile(), gl); + dirty = 0; + } + return this; + } + + /** + * Validates the shape's underlying {@link GLRegion} w/o a current {@link GL2ES2} object + * <p> + * If the region is dirty a new region is created + * and the old one gets pushed to a dirty-list to get disposed when a GL context is available. + * </p> + * @see #validate(GL2ES2) + */ + public final Shape validate(final GLProfile glp) { + synchronized ( dirtySync ) { + if( isShapeDirty() ) { + box.reset(); + } + validateImpl(glp, null); + dirty = 0; + } + return this; + } + + /** + * Setup the pre-selected {@link GLMatrixFunc#GL_MODELVIEW} {@link PMVMatrix} for this object. + * - Scale shape from its center position + * - Rotate shape around optional scaled pivot, see {@link #setRotationPivot(float[])}), otherwise rotate around its scaled center (default) + * <p> + * Shape's origin should be bottom-left @ 0/0 to have build-in drag-zoom work properly. + * </p> + * @param pmv the matrix + * @see #setRotationPivot(float[]) + * @see #getRotation() + * @see #moveTo(float, float, float) + * @see #setScale(float, float, float) + */ + public void setTransform(final PMVMatrix pmv) { + final boolean hasScale = !scale.isEqual(Vec3f.ONE); + final boolean hasRotate = !rotation.isIdentity(); + final boolean hasRotPivot = null != rotPivot; + final Vec3f ctr = box.getCenter(); + final boolean sameScaleRotatePivot = hasScale && hasRotate && ( !hasRotPivot || rotPivot.isEqual(ctr) ); + + pmv.glTranslatef(position.x(), position.y(), position.z()); // translate, scaled + + if( sameScaleRotatePivot ) { + // Scale shape from its center position and rotate around its center + pmv.glTranslatef(ctr.x()*scale.x(), ctr.y()*scale.y(), ctr.z()*scale.z()); // add-back center, scaled + pmv.glRotate(rotation); + pmv.glScalef(scale.x(), scale.y(), scale.z()); + pmv.glTranslatef(-ctr.x(), -ctr.y(), -ctr.z()); // move to center + } else if( hasRotate || hasScale ) { + if( hasRotate ) { + if( hasRotPivot ) { + // Rotate shape around its scaled pivot + pmv.glTranslatef(rotPivot.x()*scale.x(), rotPivot.y()*scale.y(), rotPivot.z()*scale.z()); // pivot back from rot-pivot, scaled + pmv.glRotate(rotation); + pmv.glTranslatef(-rotPivot.x()*scale.x(), -rotPivot.y()*scale.y(), -rotPivot.z()*scale.z()); // pivot to rot-pivot, scaled + } else { + // Rotate shape around its scaled center + pmv.glTranslatef(ctr.x()*scale.x(), ctr.y()*scale.y(), ctr.z()*scale.z()); // pivot back from center-pivot, scaled + pmv.glRotate(rotation); + pmv.glTranslatef(-ctr.x()*scale.x(), -ctr.y()*scale.y(), -ctr.z()*scale.z()); // pivot to center-pivot, scaled + } + } + if( hasScale ) { + // Scale shape from its center position + pmv.glTranslatef(ctr.x()*scale.x(), ctr.y()*scale.y(), ctr.z()*scale.z()); // add-back center, scaled + pmv.glScalef(scale.x(), scale.y(), scale.z()); + pmv.glTranslatef(-ctr.x(), -ctr.y(), -ctr.z()); // move to center + } + } + } + + /** + * Retrieve surface (view) size of this shape. + * <p> + * The given {@link PMVMatrix} has to be setup properly for this object, + * i.e. its {@link GLMatrixFunc#GL_PROJECTION} and {@link GLMatrixFunc#GL_MODELVIEW} for the surrounding scene + * including this shape's {@link #setTransform(PMVMatrix)}. + * </p> + * @param pmv well formed {@link PMVMatrix}, e.g. could have been setup via {@link Scene#setupMatrix(PMVMatrix) setupMatrix(..)} and {@link #setTransform(PMVMatrix)}. + * @param viewport the int[4] viewport + * @param surfaceSize int[2] target surface size + * @return given int[2] {@code surfaceSize} for successful gluProject(..) operation, otherwise {@code null} + * @see #getSurfaceSize(com.jogamp.graph.ui.Scene.PMVMatrixSetup, Recti, PMVMatrix, int[]) + * @see #getSurfaceSize(Scene, PMVMatrix, int[]) + */ + public int[/*2*/] getSurfaceSize(final PMVMatrix pmv, final Recti viewport, final int[/*2*/] surfaceSize) { + // System.err.println("Shape::getSurfaceSize.VP "+viewport[0]+"/"+viewport[1]+" "+viewport[2]+"x"+viewport[3]); + final Vec3f winCoordHigh = new Vec3f(); + final Vec3f winCoordLow = new Vec3f(); + final Vec3f high = box.getHigh(); + final Vec3f low = box.getLow(); + + final Matrix4f matPMv = pmv.getPMvMat(); + if( Matrix4f.mapObjToWin(high, matPMv, viewport, winCoordHigh) ) { + if( Matrix4f.mapObjToWin(low, matPMv, viewport, winCoordLow) ) { + surfaceSize[0] = (int)Math.abs(winCoordHigh.x() - winCoordLow.x()); + surfaceSize[1] = (int)Math.abs(winCoordHigh.y() - winCoordLow.y()); + return surfaceSize; + } + } + return null; + } + + /** + * Retrieve surface (view) size of this shape. + * <p> + * The given {@link PMVMatrix} will be {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti) setup} properly for this shape + * including this shape's {@link #setTransform(PMVMatrix)}. + * </p> + * @param pmvMatrixSetup {@link Scene.PMVMatrixSetup} to {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti) setup} given {@link PMVMatrix} {@code pmv}. + * @param viewport used viewport for {@link PMVMatrix#gluProject(float, float, float, int[], float[])} + * @param pmv a new {@link PMVMatrix} which will {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti) be setup}, + * {@link #setTransform(PMVMatrix) shape-transformed} and can be reused by the caller. + * @param surfaceSize int[2] target surface size + * @return given int[2] {@code surfaceSize} for successful gluProject(..) operation, otherwise {@code null} + * @see #getSurfaceSize(PMVMatrix, Recti, int[]) + * @see #getSurfaceSize(Scene, PMVMatrix, int[]) + */ + public int[/*2*/] getSurfaceSize(final Scene.PMVMatrixSetup pmvMatrixSetup, final Recti viewport, final PMVMatrix pmv, final int[/*2*/] surfaceSize) { + pmvMatrixSetup.set(pmv, viewport); + setTransform(pmv); + return getSurfaceSize(pmv, viewport, surfaceSize); + } + + /** + * Retrieve surface (view) size of this shape. + * <p> + * The given {@link PMVMatrix} will be {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti) setup} properly for this shape + * including this shape's {@link #setTransform(PMVMatrix)}. + * </p> + * @param scene {@link Scene} to retrieve {@link Scene.PMVMatrixSetup} and the viewport. + * @param pmv a new {@link PMVMatrix} which will {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti) be setup}, + * {@link #setTransform(PMVMatrix) shape-transformed} and can be reused by the caller. + * @param surfaceSize int[2] target surface size + * @return given int[2] {@code surfaceSize} for successful gluProject(..) operation, otherwise {@code null} + * @see #getSurfaceSize(PMVMatrix, Recti, int[]) + * @see #getSurfaceSize(com.jogamp.graph.ui.Scene.PMVMatrixSetup, Recti, PMVMatrix, int[]) + */ + public int[/*2*/] getSurfaceSize(final Scene scene, final PMVMatrix pmv, final int[/*2*/] surfaceSize) { + return getSurfaceSize(scene.getPMVMatrixSetup(), scene.getViewport(), pmv, surfaceSize); + } + + /** + * Retrieve pixel per scaled shape-coordinate unit, i.e. [px]/[obj]. + * <p> + * The given {@link PMVMatrix} will be {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti) setup} properly for this shape + * including this shape's {@link #setTransform(PMVMatrix)}. + * </p> + * @param scene {@link Scene} to retrieve {@link Scene.PMVMatrixSetup} and the viewport. + * @param pmv a new {@link PMVMatrix} which will {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti) be setup}, + * {@link #setTransform(PMVMatrix) shape-transformed} and can be reused by the caller. + * @param pixPerShape float[2] pixel per scaled shape-coordinate unit result storage + * @return given float[2] {@code pixPerShape} for successful gluProject(..) operation, otherwise {@code null} + * @see #getPixelPerShapeUnit(int[], float[]) + * @see #getSurfaceSize(Scene, PMVMatrix, int[]) + * @see #getScaledWidth() + * @see #getScaledHeight() + */ + public float[] getPixelPerShapeUnit(final Scene scene, final PMVMatrix pmv, final float[] pixPerShape) { + final int[] shapeSizePx = new int[2]; + if( null != getSurfaceSize(scene, new PMVMatrix(), shapeSizePx) ) { + return getPixelPerShapeUnit(shapeSizePx, pixPerShape); + } else { + return null; + } + } + + /** + * Retrieve pixel per scaled shape-coordinate unit, i.e. [px]/[obj]. + * @param shapeSizePx int[2] shape size in pixel as retrieved via e.g. {@link #getSurfaceSize(com.jogamp.graph.ui.Scene.PMVMatrixSetup, Recti, PMVMatrix, int[])} + * @param pixPerShape float[2] pixel scaled per shape-coordinate unit result storage + * @return given float[2] {@code pixPerShape} + * @see #getPixelPerShapeUnit(Scene, PMVMatrix, float[]) + * @see #getSurfaceSize(com.jogamp.graph.ui.Scene.PMVMatrixSetup, Recti, PMVMatrix, int[]) + * @see #getScaledWidth() + * @see #getScaledHeight() + */ + public float[] getPixelPerShapeUnit(final int[] shapeSizePx, final float[] pixPerShape) { + pixPerShape[0] = shapeSizePx[0] / getScaledWidth(); + pixPerShape[0] = shapeSizePx[1] / getScaledHeight(); + return pixPerShape; + } + + /** + * Map given object coordinate relative to this shape to window coordinates. + * <p> + * The given {@link PMVMatrix} has to be setup properly for this object, + * i.e. its {@link GLMatrixFunc#GL_PROJECTION} and {@link GLMatrixFunc#GL_MODELVIEW} for the surrounding scene + * including this shape's {@link #setTransform(PMVMatrix)}. + * </p> + * @param pmv well formed {@link PMVMatrix}, e.g. could have been setup via {@link Scene#setupMatrix(PMVMatrix) setupMatrix(..)} and {@link #setTransform(PMVMatrix)}. + * @param viewport the viewport + * @param objPos object position relative to this shape's center + * @param glWinPos int[2] target window position of objPos relative to this shape + * @return given int[2] {@code glWinPos} for successful gluProject(..) operation, otherwise {@code null} + * @see #shapeToWinCoord(com.jogamp.graph.ui.Scene.PMVMatrixSetup, Recti, float[], PMVMatrix, int[]) + * @see #shapeToWinCoord(Scene, float[], PMVMatrix, int[]) + */ + public int[/*2*/] shapeToWinCoord(final PMVMatrix pmv, final Recti viewport, final Vec3f objPos, final int[/*2*/] glWinPos) { + // System.err.println("Shape::objToWinCoordgetSurfaceSize.VP "+viewport[0]+"/"+viewport[1]+" "+viewport[2]+"x"+viewport[3]); + final Vec3f winCoord = new Vec3f(); + + if( pmv.gluProject(objPos, viewport, winCoord) ) { + glWinPos[0] = (int)(winCoord.x()); + glWinPos[1] = (int)(winCoord.y()); + return glWinPos; + } + return null; + } + + /** + * Map given object coordinate relative to this shape to window coordinates. + * <p> + * The given {@link PMVMatrix} will be {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti) setup} properly for this shape + * including this shape's {@link #setTransform(PMVMatrix)}. + * </p> + * @param pmvMatrixSetup {@link Scene.PMVMatrixSetup} to {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti) setup} given {@link PMVMatrix} {@code pmv}. + * @param viewport used viewport for {@link PMVMatrix#gluProject(Vec3f, Recti, Vec3f)} + * @param objPos object position relative to this shape's center + * @param pmv a new {@link PMVMatrix} which will {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti) be setup}, + * {@link #setTransform(PMVMatrix) shape-transformed} and can be reused by the caller. + * @param glWinPos int[2] target window position of objPos relative to this shape + * @return given int[2] {@code glWinPos} for successful gluProject(..) operation, otherwise {@code null} + * @see #shapeToWinCoord(PMVMatrix, Recti, float[], int[]) + * @see #shapeToWinCoord(Scene, float[], PMVMatrix, int[]) + */ + public int[/*2*/] shapeToWinCoord(final Scene.PMVMatrixSetup pmvMatrixSetup, final Recti viewport, final Vec3f objPos, final PMVMatrix pmv, final int[/*2*/] glWinPos) { + pmvMatrixSetup.set(pmv, viewport); + setTransform(pmv); + return this.shapeToWinCoord(pmv, viewport, objPos, glWinPos); + } + + /** + * Map given object coordinate relative to this shape to window coordinates. + * <p> + * The given {@link PMVMatrix} will be {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti) setup} properly for this shape + * including this shape's {@link #setTransform(PMVMatrix)}. + * </p> + * @param scene {@link Scene} to retrieve {@link Scene.PMVMatrixSetup} and the viewport. + * @param objPos object position relative to this shape's center + * @param pmv a new {@link PMVMatrix} which will {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti) be setup}, + * {@link #setTransform(PMVMatrix) shape-transformed} and can be reused by the caller. + * @param glWinPos int[2] target window position of objPos relative to this shape + * @return given int[2] {@code glWinPos} for successful gluProject(..) operation, otherwise {@code null} + * @see #shapeToWinCoord(PMVMatrix, Recti, float[], int[]) + * @see #shapeToWinCoord(com.jogamp.graph.ui.Scene.PMVMatrixSetup, Recti, float[], PMVMatrix, int[]) + */ + public int[/*2*/] shapeToWinCoord(final Scene scene, final Vec3f objPos, final PMVMatrix pmv, final int[/*2*/] glWinPos) { + return this.shapeToWinCoord(scene.getPMVMatrixSetup(), scene.getViewport(), objPos, pmv, glWinPos); + } + + /** + * Map given gl-window-coordinates to object coordinates relative to this shape and its z-coordinate. + * <p> + * The given {@link PMVMatrix} has to be setup properly for this object, + * i.e. its {@link GLMatrixFunc#GL_PROJECTION} and {@link GLMatrixFunc#GL_MODELVIEW} for the surrounding scene + * including this shape's {@link #setTransform(PMVMatrix)}. + * </p> + * @param pmv well formed {@link PMVMatrix}, e.g. could have been setup via {@link Scene#setupMatrix(PMVMatrix) setupMatrix(..)} and {@link #setTransform(PMVMatrix)}. + * @param viewport the Rect4i viewport + * @param glWinX in GL window coordinates, origin bottom-left + * @param glWinY in GL window coordinates, origin bottom-left + * @param objPos target object position of glWinX/glWinY relative to this shape + * @return given {@code objPos} for successful gluProject(..) and gluUnProject(..) operation, otherwise {@code null} + * @see #winToShapeCoord(com.jogamp.graph.ui.Scene.PMVMatrixSetup, Recti, int, int, PMVMatrix, float[]) + * @see #winToShapeCoord(Scene, int, int, PMVMatrix, float[]) + */ + public Vec3f winToShapeCoord(final PMVMatrix pmv, final Recti viewport, final int glWinX, final int glWinY, final Vec3f objPos) { + final Vec3f ctr = box.getCenter(); + + if( Matrix4f.mapObjToWin(ctr, pmv.getPMvMat(), viewport, objPos) ) { + final float winZ = objPos.z(); + if( Matrix4f.mapWinToObj(glWinX, glWinY, winZ, pmv.getPMviMat(), viewport, objPos) ) { + return objPos; + } + } + return null; + } + + /** + * Map given gl-window-coordinates to object coordinates relative to this shape and its z-coordinate. + * <p> + * The given {@link PMVMatrix} will be {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti) setup} properly for this shape + * including this shape's {@link #setTransform(PMVMatrix)}. + * </p> + * @param pmvMatrixSetup {@link Scene.PMVMatrixSetup} to {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti) setup} given {@link PMVMatrix} {@code pmv}. + * @param viewport used viewport for {@link PMVMatrix#gluUnProject(float, float, float, Recti, Vec3f)} + * @param glWinX in GL window coordinates, origin bottom-left + * @param glWinY in GL window coordinates, origin bottom-left + * @param pmv a new {@link PMVMatrix} which will {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti) be setup}, + * {@link #setTransform(PMVMatrix) shape-transformed} and can be reused by the caller. + * @param objPos target object position of glWinX/glWinY relative to this shape + * @return given {@code objPos} for successful gluProject(..) and gluUnProject(..) operation, otherwise {@code null} + * @see #winToShapeCoord(PMVMatrix, Recti, int, int, float[]) + * @see #winToShapeCoord(Scene, int, int, PMVMatrix, float[]) + */ + public Vec3f winToShapeCoord(final Scene.PMVMatrixSetup pmvMatrixSetup, final Recti viewport, final int glWinX, final int glWinY, final PMVMatrix pmv, final Vec3f objPos) { + pmvMatrixSetup.set(pmv, viewport); + setTransform(pmv); + return this.winToShapeCoord(pmv, viewport, glWinX, glWinY, objPos); + } + + /** + * Map given gl-window-coordinates to object coordinates relative to this shape and its z-coordinate. + * <p> + * The given {@link PMVMatrix} will be {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti) setup} properly for this shape + * including this shape's {@link #setTransform(PMVMatrix)}. + * </p> + * @param scene {@link Scene} to retrieve {@link Scene.PMVMatrixSetup} and the viewport. + * @param glWinX in GL window coordinates, origin bottom-left + * @param glWinY in GL window coordinates, origin bottom-left + * @param pmv a new {@link PMVMatrix} which will {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti) be setup}, + * {@link #setTransform(PMVMatrix) shape-transformed} and can be reused by the caller. + * @param objPos target object position of glWinX/glWinY relative to this shape + * @return given {@code objPos} for successful gluProject(..) and gluUnProject(..) operation, otherwise {@code null} + * @see #winToShapeCoord(PMVMatrix, Recti, int, int, float[]) + * @see #winToShapeCoord(com.jogamp.graph.ui.Scene.PMVMatrixSetup, Recti, int, int, PMVMatrix, float[]) + */ + public Vec3f winToShapeCoord(final Scene scene, final int glWinX, final int glWinY, final PMVMatrix pmv, final Vec3f objPos) { + return this.winToShapeCoord(scene.getPMVMatrixSetup(), scene.getViewport(), glWinX, glWinY, pmv, objPos); + } + + public Vec4f getColor() { return rgbaColor; } + + /** + * Set base color. + * <p> + * Default base-color w/o color channel, will be modulated w/ pressed- and toggle color + * </p> + */ + public final Shape setColor(final float r, final float g, final float b, final float a) { + this.rgbaColor.set(r, g, b, a); + return this; + } + + /** + * Set base color. + * <p> + * Default base-color w/o color channel, will be modulated w/ pressed- and toggle color + * </p> + */ + public final Shape setColor(final Vec4f c) { + this.rgbaColor.set(c); + return this; + } + + /** + * Set pressed color. + * <p> + * Default pressed color-factor w/o color channel, modulated base-color. 0.75 * 1.2 = 0.9 + * </p> + */ + public final Shape setPressedColorMod(final float r, final float g, final float b, final float a) { + this.pressedRGBAModulate.set(r, g, b, a); + return this; + } + + /** + * Set toggle-on color. + * <p> + * Default toggle-on color-factor w/o color channel, modulated base-color. 0.75 * 1.13 ~ 0.85 + * </p> + */ + public final Shape setToggleOnColorMod(final float r, final float g, final float b, final float a) { + this.toggleOnRGBAModulate.set(r, g, b, a); + return this; + } + + /** + * Set toggle-off color. + * <p> + * Default toggle-off color-factor w/o color channel, modulated base-color. 0.75 * 0.86 ~ 0.65 + * </p> + */ + public final Shape setToggleOffColorMod(final float r, final float g, final float b, final float a) { + this.toggleOffRGBAModulate.set(r, g, b, a); + return this; + } + + public Vec4f getBorderColor() { return borderColor; } + + /** Set border color. */ + public final Shape setBorderColor(final float r, final float g, final float b, final float a) { + this.borderColor.set(r, g, b, a); + return this; + } + + /** Set border color. */ + public final Shape setBorderColor(final Vec4f c) { + this.borderColor.set(c); + return this; + } + + @Override + public final String toString() { + return getClass().getSimpleName()+"["+getSubString()+"]"; + } + + public String getSubString() { + final String pivotS; + if( null != rotPivot ) { + pivotS = "pivot["+rotPivot+"], "; + } else { + pivotS = ""; + } + final String scaleS; + if( !scale.isEqual( Vec3f.ONE ) ) { + scaleS = "scale["+scale+"], "; + } else { + scaleS = "scale 1, "; + } + final String rotateS; + if( !rotation.isIdentity() ) { + final Vec3f euler = rotation.toEuler(new Vec3f()); + rotateS = "rot["+euler+"], "; + } else { + rotateS = ""; + } + final String ps = hasPadding() ? padding.toString()+", " : ""; + final String bs = hasBorder() ? "Border "+getBorderThickness()+", " : ""; + return "enabled "+enabled+", toggle[able "+toggleable+", state "+toggle+ + "], able[iactive "+isInteractive()+", resize "+isResizable()+", move "+this.isDraggable()+ + "], pos["+position+"], "+pivotS+scaleS+rotateS+ + ps+bs+"box"+box; + } + + // + // Input + // + + public Shape setPressed(final boolean b) { + this.down = b; + markStateDirty(); + return this; + } + public boolean isPressed() { + return this.down; + } + + /** + * + * @param toggleable + * @see #isInteractive() + */ + public Shape setToggleable(final boolean toggleable) { + this.toggleable = toggleable; + return this; + } + + /** + * Returns true if this shape is toggable, + * i.e. rendered w/ {@link #setToggleOnColorMod(float, float, float, float)} or {@link #setToggleOffColorMod(float, float, float, float)}. + * @see #isInteractive() + */ + public boolean isToggleable() { + return toggleable; + } + public Shape setToggle(final boolean v) { + toggle = v; + markStateDirty(); + return this; + } + public Shape toggle() { + if( isToggleable() ) { + toggle = !toggle; + if( null != onToggleListener ) { + onToggleListener.run(this); + } + markStateDirty(); + } + return this; + } + public boolean isToggleOn() { return toggle; } + + /** + * Set whether this shape is interactive, + * i.e. any user interaction like + * - {@link #isToggleable()} + * - {@link #isDraggable()} + * - {@link #isResizable()} + * but excluding programmatic changes. + * @param v new value for {@link #isInteractive()} + */ + public Shape setInteractive(final boolean v) { interactive = v; return this; } + /** + * Returns if this shape allows user interaction, see {@link #setInteractive(boolean)} + * @see #setInteractive(boolean) + */ + public boolean isInteractive() { return interactive; } + + /** + * Set whether this shape is draggable, + * i.e. translated by 1-pointer-click and drag. + * <p> + * Default draggable is true. + * </p> + * @see #isInteractive() + */ + public Shape setDraggable(final boolean draggable) { + this.draggable = draggable; + return this; + } + /** + * Returns if this shape is draggable, a user interaction. + * @see #isInteractive() + */ + public boolean isDraggable() { + return draggable; + } + + /** + * Set whether this shape is resizable, + * i.e. zoomed by 1-pointer-click and drag in 1/4th bottom-left and bottom-right corner. + * <p> + * Default resizable is true. + * </p> + * @see #isInteractive() + */ + public Shape setResizable(final boolean resizable) { + this.resizable = resizable; + return this; + } + /** + * Returns if this shape is resiable, a user interaction. + * @see #isInteractive() + */ + public boolean isResizable() { + return resizable; + } + + /** + * Set whether this shape is draggable and resizable. + * <p> + * Default draggable and resizable is true. + * </p> + * @see #setDraggable(boolean) + * @see #setResizable(boolean) + * @see #isInteractive() + */ + public Shape setDragAndResizeable(final boolean v) { + this.draggable = v; + this.resizable = v; + return this; + } + + public final Shape addMouseListener(final MouseGestureListener l) { + if(l == null) { + return this; + } + @SuppressWarnings("unchecked") + final ArrayList<MouseGestureListener> clonedListeners = (ArrayList<MouseGestureListener>) mouseListeners.clone(); + clonedListeners.add(l); + mouseListeners = clonedListeners; + return this; + } + + public final Shape removeMouseListener(final MouseGestureListener l) { + if (l == null) { + return this; + } + @SuppressWarnings("unchecked") + final ArrayList<MouseGestureListener> clonedListeners = (ArrayList<MouseGestureListener>) mouseListeners.clone(); + clonedListeners.remove(l); + mouseListeners = clonedListeners; + return this; + } + + /** + * Combining {@link MouseListener} and {@link GestureListener} + */ + public static interface MouseGestureListener extends MouseListener, GestureListener { + } + + /** + * Convenient adapter combining dummy implementation for {@link MouseListener} and {@link GestureListener} + */ + public static abstract class MouseGestureAdapter extends MouseAdapter implements MouseGestureListener { + @Override + public void gestureDetected(final GestureEvent gh) { + } + } + + /** + * {@link Shape} event info for propagated {@link NEWTEvent}s + * containing reference of {@link #shape the intended shape} as well as + * the {@link #objPos rotated relative position} to this shape. + * The latter is normalized to bottom-left zero origin, allowing easier usage. + */ + public static class EventInfo { + /** The associated {@link Shape} for this event */ + public final Shape shape; + /** The relative object coordinate of glWinX/glWinY to the associated {@link Shape}. */ + public final Vec3f objPos; + /** The GL window coordinates, origin bottom-left */ + public final int[] winPos; + /** The drag delta of the relative object coordinate of glWinX/glWinY to the associated {@link Shape}. */ + public final Vec2f objDrag = new Vec2f(); + /** The drag delta of GL window coordinates, origin bottom-left */ + public final int[] winDrag = { 0, 0 }; + + /** + * Ctor + * @param glWinX in GL window coordinates, origin bottom-left + * @param glWinY in GL window coordinates, origin bottom-left + * @param shape associated shape + * @param objPos relative object coordinate of glWinX/glWinY to the associated shape. + */ + EventInfo(final int glWinX, final int glWinY, final Shape shape, final Vec3f objPos) { + this.winPos = new int[] { glWinX, glWinY }; + this.shape = shape; + this.objPos = objPos; + } + + @Override + public String toString() { + return "EventInfo[winPos ["+winPos[0]+", "+winPos[1]+"], objPos ["+objPos+"], "+shape+"]"; + } + } + + private boolean dragFirst = false; + private final Vec2f objDraggedFirst = new Vec2f(); // b/c its relative to Shape and we stick to it + private final int[] winDraggedLast = { 0, 0 }; // b/c its absolute window pos + private boolean inMove = false; + private int inResize = 0; // 1 br, 2 bl + private static final float resize_sxy_min = 1f/200f; // 1/2% - TODO: Maybe customizable? + private static final float resize_section = 1f/5f; // resize action in a corner + + /** + * Dispatch given NEWT mouse event to this shape + * @param e original Newt {@link MouseEvent} + * @param glWinX in GL window coordinates, origin bottom-left + * @param glWinY in GL window coordinates, origin bottom-left + * @param objPos object position of mouse event relative to this shape + */ + /* pp */ final void dispatchMouseEvent(final MouseEvent e, final int glWinX, final int glWinY, final Vec3f objPos) { + final Shape.EventInfo shapeEvent = new EventInfo(glWinX, glWinY, this, objPos); + + final short eventType = e.getEventType(); + if( 1 == e.getPointerCount() ) { + switch( eventType ) { + case MouseEvent.EVENT_MOUSE_CLICKED: + toggle(); + if( null != onClickedListener ) { + onClickedListener.run(this); + } + break; + case MouseEvent.EVENT_MOUSE_PRESSED: + dragFirst = true; + setPressed(true); + break; + case MouseEvent.EVENT_MOUSE_RELEASED: + // Release active shape: last pointer has been lifted! + setPressed(false); + inMove = false; + inResize = 0; + break; + } + } + switch( eventType ) { + case MouseEvent.EVENT_MOUSE_DRAGGED: { + // adjust for rotation + final Vec3f euler = rotation.toEuler(new Vec3f()); + final boolean x_flip, y_flip; + { + final float x_rot = Math.abs(euler.x()); + final float y_rot = Math.abs(euler.y()); + x_flip = 1f*FloatUtil.HALF_PI <= y_rot && y_rot <= 3f*FloatUtil.HALF_PI; + y_flip = 1f*FloatUtil.HALF_PI <= x_rot && x_rot <= 3f*FloatUtil.HALF_PI; + } + // 1 pointer drag and potential drag-resize + if(dragFirst) { + objDraggedFirst.set(objPos); + winDraggedLast[0] = glWinX; + winDraggedLast[1] = glWinY; + dragFirst=false; + + final float ix = x_flip ? box.getWidth() - objPos.x() : objPos.x(); + final float iy = y_flip ? box.getHeight() - objPos.y() : objPos.y(); + final float minx_br = box.getMaxX() - box.getWidth() * resize_section; + final float miny_br = box.getMinY(); + final float maxx_br = box.getMaxX(); + final float maxy_br = box.getMinY() + box.getHeight() * resize_section; + if( minx_br <= ix && ix <= maxx_br && + miny_br <= iy && iy <= maxy_br ) { + if( interactive && resizable ) { + inResize = 1; // bottom-right + } + } else { + final float minx_bl = box.getMinX(); + final float miny_bl = box.getMinY(); + final float maxx_bl = box.getMinX() + box.getWidth() * resize_section; + final float maxy_bl = box.getMinY() + box.getHeight() * resize_section; + if( minx_bl <= ix && ix <= maxx_bl && + miny_bl <= iy && iy <= maxy_bl ) { + if( interactive && resizable ) { + inResize = 2; // bottom-left + } + } else { + inMove = interactive && draggable; + } + } + if( DEBUG ) { + System.err.printf("DragFirst: drag %b, resize %d, obj[%s], flip[x %b, y %b]%n", + inMove, inResize, objPos, x_flip, y_flip); + System.err.printf("DragFirst: %s%n", this); + } + return; + } + shapeEvent.objDrag.set( objPos.x() - objDraggedFirst.x(), + objPos.y() - objDraggedFirst.y() ); + shapeEvent.objDrag.scale(x_flip ? -1f : 1f, y_flip ? -1f : 1f); + + shapeEvent.winDrag[0] = glWinX - winDraggedLast[0]; + shapeEvent.winDrag[1] = glWinY - winDraggedLast[1]; + winDraggedLast[0] = glWinX; + winDraggedLast[1] = glWinY; + if( 1 == e.getPointerCount() ) { + final float sdx = shapeEvent.objDrag.x() * scale.x(); // apply scale, since operation + final float sdy = shapeEvent.objDrag.y() * scale.y(); // is from a scaled-model-viewpoint + if( 0 != inResize ) { + final float bw = box.getWidth(); + final float bh = box.getHeight(); + final float sx; + if( 1 == inResize ) { + sx = scale.x() + sdx/bw; // bottom-right + } else { + sx = scale.x() - sdx/bw; // bottom-left + } + final float sy = scale.y() - sdy/bh; + if( resize_sxy_min <= sx && resize_sxy_min <= sy ) { // avoid scale flip + if( DEBUG ) { + System.err.printf("DragZoom: resize %d, win[%4d, %4d], , flip[x %b, y %b], obj[%s], dxy +[%s], sdxy +[%.4f, %.4f], scale [%s] -> [%.4f, %.4f]%n", + inResize, glWinX, glWinY, x_flip, y_flip, objPos, + shapeEvent.objDrag, sdx, sdy, + scale, sx, sy); + } + if( 1 == inResize ) { + move( 0, sdy, 0f); // bottom-right, sticky left- and top-edge + } else { + move( sdx, sdy, 0f); // bottom-left, sticky right- and top-edge + } + setScale(sx, sy, scale.z()); + } + return; // FIXME: pass through event? Issue zoom event? + } else if( inMove ) { + if( DEBUG ) { + System.err.printf("DragMove: win[%4d, %4d] +[%2d, %2d], , flip[x %b, y %b], obj[%s] +[%s], rot %s%n", + glWinX, glWinY, shapeEvent.winDrag[0], shapeEvent.winDrag[1], + x_flip, y_flip, objPos, shapeEvent.objDrag, euler); + } + move( sdx, sdy, 0f); + // FIXME: Pass through event? Issue move event? + } + } + } + break; + } + e.setAttachment(shapeEvent); + + for(int i = 0; !e.isConsumed() && i < mouseListeners.size(); i++ ) { + final MouseGestureListener l = mouseListeners.get(i); + switch( eventType ) { + case MouseEvent.EVENT_MOUSE_CLICKED: + l.mouseClicked(e); + break; + case MouseEvent.EVENT_MOUSE_ENTERED: + l.mouseEntered(e); + break; + case MouseEvent.EVENT_MOUSE_EXITED: + l.mouseExited(e); + break; + case MouseEvent.EVENT_MOUSE_PRESSED: + l.mousePressed(e); + break; + case MouseEvent.EVENT_MOUSE_RELEASED: + l.mouseReleased(e); + break; + case MouseEvent.EVENT_MOUSE_MOVED: + l.mouseMoved(e); + break; + case MouseEvent.EVENT_MOUSE_DRAGGED: + l.mouseDragged(e); + break; + case MouseEvent.EVENT_MOUSE_WHEEL_MOVED: + l.mouseWheelMoved(e); + break; + default: + throw new NativeWindowException("Unexpected mouse event type " + e.getEventType()); + } + } + } + + /** + * @param e original Newt {@link GestureEvent} + * @param glWinX x-position in OpenGL model space + * @param glWinY y-position in OpenGL model space + * @param pmv well formed PMVMatrix for this shape + * @param viewport the viewport + * @param objPos object position of mouse event relative to this shape + */ + /* pp */ final void dispatchGestureEvent(final GestureEvent e, final int glWinX, final int glWinY, final PMVMatrix pmv, final Recti viewport, final Vec3f objPos) { + if( interactive && resizable && e instanceof PinchToZoomGesture.ZoomEvent ) { + final PinchToZoomGesture.ZoomEvent ze = (PinchToZoomGesture.ZoomEvent) e; + final float pixels = ze.getDelta() * ze.getScale(); // + final int winX2 = glWinX + Math.round(pixels); + final Vec3f objPos2 = winToShapeCoord(pmv, viewport, winX2, glWinY, new Vec3f()); + if( null == objPos2 ) { + return; + } + final float dx = objPos2.x(); + final float dy = objPos2.y(); + final float sx = scale.x() + ( dx/box.getWidth() ); // bottom-right + final float sy = scale.y() + ( dy/box.getHeight() ); + if( DEBUG ) { + System.err.printf("DragZoom: resize %b, win %4d/%4d, obj %s, %s + %.3f/%.3f -> %.3f/%.3f%n", + inResize, glWinX, glWinY, objPos, position, dx, dy, sx, sy); + } + if( resize_sxy_min <= sx && resize_sxy_min <= sy ) { // avoid scale flip + if( DEBUG ) { + System.err.printf("PinchZoom: pixels %f, win %4d/%4d, obj %s, %s + %.3f/%.3f -> %.3f/%.3f%n", + pixels, glWinX, glWinY, objPos, position, dx, dy, sx, sy); + } + // move(dx, dy, 0f); + setScale(sx, sy, scale.z()); + } + return; // FIXME: pass through event? Issue zoom event? + } + final Shape.EventInfo shapeEvent = new EventInfo(glWinX, glWinY, this, objPos); + e.setAttachment(shapeEvent); + + for(int i = 0; !e.isConsumed() && i < mouseListeners.size(); i++ ) { + mouseListeners.get(i).gestureDetected(e); + } + } + + // + // + // + + protected abstract void validateImpl(final GLProfile glp, final GL2ES2 gl); + + /** + * Actual draw implementation + * @param gl + * @param renderer + * @param sampleCount + * @param rgba if null, caller is {@link #drawToSelect(GL2ES2, RegionRenderer, int[])}, otherwise regular {@#link #draw(GL2ES2, RegionRenderer, int[])} + */ + protected abstract void drawImpl0(final GL2ES2 gl, final RegionRenderer renderer, final int[] sampleCount, Vec4f rgba); + + protected abstract void clearImpl0(final GL2ES2 gl, final RegionRenderer renderer); + + protected abstract void destroyImpl0(final GL2ES2 gl, final RegionRenderer renderer); + + /** + * Returns true if implementation uses an extra color channel or texture + * which will be modulated with the passed rgba color {@link #drawImpl0(GL2ES2, RegionRenderer, int[], float[])}. + * + * Otherwise the base color will be modulated and passed to {@link #drawImpl0(GL2ES2, RegionRenderer, int[], float[])}. + */ + public abstract boolean hasColorChannel(); + + public static Comparator<Shape> ZAscendingComparator = new Comparator<Shape>() { + @Override + public int compare(final Shape s1, final Shape s2) { + final float s1Z = s1.getBounds().getMinZ()+s1.getPosition().z(); + final float s2Z = s2.getBounds().getMinZ()+s2.getPosition().z(); + if( FloatUtil.isEqual(s1Z, s2Z, FloatUtil.EPSILON) ) { + return 0; + } else if( s1Z < s2Z ){ + return -1; + } else { + return 1; + } + } }; + + // + // + // +} diff --git a/src/graphui/classes/com/jogamp/graph/ui/layout/Alignment.java b/src/graphui/classes/com/jogamp/graph/ui/layout/Alignment.java new file mode 100644 index 000000000..544ae3f26 --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/layout/Alignment.java @@ -0,0 +1,120 @@ +/** + * Copyright 2023 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package com.jogamp.graph.ui.layout; + +import java.util.List; + +/** + * Immutable layout alignment options, including {@link Bit#Fill}. + */ +public final class Alignment { + /** No alignment constant. */ + public static final Alignment None = new Alignment(); + /** {@link Bit#Center} alignment constant. */ + public static final Alignment Center = new Alignment(Alignment.Bit.Center); + /** {@link Bit#Fill} alignment constant. */ + public static final Alignment Fill = new Alignment(Alignment.Bit.Fill.value); + /** {@link Bit#Fill} and {@link Bit#Center} alignment constant. */ + public static final Alignment FillCenter = new Alignment(Alignment.Bit.Fill.value | Alignment.Bit.Center.value); + + public enum Bit { + /** Left alignment. */ + Left ( ( 1 << 0 ) ), + + /** Right alignment. */ + Right ( ( 1 << 1 ) ), + + /** Bottom alignment. */ + Bottom ( ( 1 << 2 ) ), + + /** Top alignment. */ + Top ( ( 1 << 8 ) ), + + /** Center alignment. */ + Center ( ( 1 << 9 ) ), + + /** Scale object to parent size, e.g. fill {@link GridLayout} cell size. */ + Fill ( ( 1 << 15 ) ); + + Bit(final int v) { value = v; } + public final int value; + } + public final int mask; + + public static int getBits(final List<Bit> v) { + int res = 0; + for(final Bit b : v) { + res |= b.value; + } + return res; + } + public Alignment(final List<Bit> v) { + mask = getBits(v); + } + public Alignment(final Bit v) { + mask = v.value; + } + public Alignment(final int v) { + mask = v; + } + public Alignment() { + mask = 0; + } + + public boolean isSet(final Bit bit) { return bit.value == ( mask & bit.value ); } + public boolean isSet(final List<Bit> bits) { final int bits_i = getBits(bits); return bits_i == ( mask & bits_i ); } + public boolean isSet(final int bits) { return bits == ( mask & bits ); } + + @Override + public String toString() { + int count = 0; + final StringBuilder out = new StringBuilder(); + for (final Bit dt : Bit.values()) { + if( isSet(dt) ) { + if( 0 < count ) { out.append(", "); } + out.append(dt.name()); count++; + } + } + if( 0 == count ) { + out.append("None"); + } else if( 1 < count ) { + out.insert(0, "["); + out.append("]"); + } + return out.toString(); + } + + @Override + public boolean equals(final Object other) { + if (this == other) { + return true; + } + return (other instanceof Alignment) && + this.mask == ((Alignment)other).mask; + } +} diff --git a/src/graphui/classes/com/jogamp/graph/ui/layout/BoxLayout.java b/src/graphui/classes/com/jogamp/graph/ui/layout/BoxLayout.java new file mode 100644 index 000000000..acd58d0ab --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/layout/BoxLayout.java @@ -0,0 +1,137 @@ +/** + * Copyright 2023 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package com.jogamp.graph.ui.layout; + +import java.util.List; + +import com.jogamp.graph.ui.Group; +import com.jogamp.graph.ui.Shape; +import com.jogamp.opengl.math.FloatUtil; +import com.jogamp.opengl.math.geom.AABBox; +import com.jogamp.opengl.util.PMVMatrix; + +/** + * GraphUI Stack {@link Group.Layout}. + * <p> + * A stack of {@link Shape}s + * - Size kept unscaled + * - Position depends on {@Link Padding} and {@link Margin} + * - Cell size can be set + * </p> + */ +public class BoxLayout implements Group.Layout { + private final float cellWidth, cellHeight; + private final Margin margin; + private final Padding padding; + + private static final boolean TRACE_LAYOUT = false; + + public BoxLayout(final Padding padding) { + this(0f, 0f, new Margin(), padding); + } + public BoxLayout(final float width, final float height, final Margin margin) { + this(width, height, margin, new Padding()); + } + + /** + * + * @param width + * @param height + * @param margin + * @param padding + */ + public BoxLayout(final float width, final float height, final Margin margin, final Padding padding) { + this.cellWidth = Math.max(0f, width); + this.cellHeight = Math.max(0f, height); + this.margin = margin; + this.padding = padding; + } + + public Padding getPadding() { return padding; } + public Margin getMargin() { return margin; } + + @Override + public void layout(final Group g, final AABBox box, final PMVMatrix pmv) { + final boolean hasCellWidth = !FloatUtil.isZero(cellWidth); + final boolean hasCellHeight = !FloatUtil.isZero(cellHeight); + final List<Shape> shapes = g.getShapes(); + final AABBox sbox = new AABBox(); + for(int i=0; i < shapes.size(); ++i) { + final Shape s = shapes.get(i); + + // measure size + pmv.glPushMatrix(); + s.setTransform(pmv); + s.getBounds().transformMv(pmv, sbox); + pmv.glPopMatrix(); + + // adjust size and position (centered) + final float paddedWidth = sbox.getWidth() + padding.width(); + final float paddedHeight = sbox.getHeight() + padding.height(); + final float cellWidth2 = hasCellWidth ? cellWidth : paddedWidth; + final float cellHeight2 = hasCellHeight ? cellHeight : paddedHeight; + float x = margin.left; + float y = margin.bottom; + if( !margin.isCenteredHoriz() && paddedWidth <= cellWidth2 ) { + x += padding.left; + } + if( !margin.isCenteredVert() && paddedHeight <= cellHeight2 ) { + y += padding.bottom; + } + final float dxh, dyh; + if( margin.isCenteredHoriz() ) { + dxh = 0.5f * ( cellWidth2 - paddedWidth ); // actual horiz-centered + } else { + dxh = 0; + } + if( margin.isCenteredVert() ) { + dyh = 0.5f * ( cellHeight2 - paddedHeight ); // actual vert-centered + } else { + dyh = 0; + } + if( TRACE_LAYOUT ) { + System.err.println("bl["+i+"].0: @ "+s.getPosition()+", sbox "+sbox); + System.err.println("bl["+i+"].m: "+x+" / "+y+" + "+dxh+" / "+dyh+", p "+paddedWidth+" x "+paddedHeight+", sz "+cellWidth2+" x "+cellHeight2+", box "+box.getWidth()+" x "+box.getHeight()); + } + s.move( x + dxh, y + dyh, 0f ); // center the scaled artifact + s.move( sbox.getLow().mul(-1f) ); // remove the bottom-left delta + // resize bounds including padding, excluding margin + box.resize( x + sbox.getWidth() + padding.right, y + sbox.getHeight() + padding.top, 0); + box.resize( x - padding.left, y - padding.bottom, 0); + if( TRACE_LAYOUT ) { + System.err.println("bl["+i+"].x: "+x+" / "+y+" + "+dxh+" / "+dyh+" -> "+s.getPosition()+", p "+paddedWidth+" x "+paddedHeight+", sz "+cellWidth2+" x "+cellHeight2+", box "+box.getWidth()+" x "+box.getHeight()); + } + } + } + + @Override + public String toString() { + return "Box[cell["+cellWidth+" x "+cellHeight+"], "+margin+", "+padding+"]"; + } +} + diff --git a/src/graphui/classes/com/jogamp/graph/ui/layout/Gap.java b/src/graphui/classes/com/jogamp/graph/ui/layout/Gap.java new file mode 100644 index 000000000..4b8caef7a --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/layout/Gap.java @@ -0,0 +1,81 @@ +/** + * Copyright 2023 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package com.jogamp.graph.ui.layout; + +import com.jogamp.opengl.math.FloatUtil; + +/** + * GraphUI CSS property Gap, spaceing between (grid) cells not belonging to the element. + * + * The CSS gap property defines the size of the gap between the rows and columns in a grid layout. + */ +public class Gap { + /** Row gap value, vertical spacing. */ + public final float row; + /** Column gap value, horizontal spacing. */ + public final float column; + + /** + * Ctor w/ zero values + */ + public Gap() { + row = 0f; column = 0f; + } + + /** + * Ctor + * @param row vertical row value + * @param column horizontal column value + */ + public Gap(final float row, final float column) { + this.row = row; this.column = column; + } + + /** + * Ctor + * @param rc vertical row and horizontal column value + */ + public Gap(final float rc) { + this.row = rc; this.column = rc; + } + + /** Return width of horizontal value, i.e. 1 * column. */ + public float width() { return column; } + + /** Return height of vertical value, i.e. 1 * row. */ + public float height() { return row; } + + public boolean zeroSumWidth() { return FloatUtil.isZero( width() ); }; + + public boolean zeroSumHeight() { return FloatUtil.isZero( height() ); }; + + public boolean zeroSumSize() { return zeroSumWidth() && zeroSumHeight(); } + + @Override + public String toString() { return "Gap[r "+row+", c "+column+"]"; } +} diff --git a/src/graphui/classes/com/jogamp/graph/ui/layout/GridLayout.java b/src/graphui/classes/com/jogamp/graph/ui/layout/GridLayout.java new file mode 100644 index 000000000..d4a976012 --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/layout/GridLayout.java @@ -0,0 +1,324 @@ +/** + * Copyright 2023 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package com.jogamp.graph.ui.layout; + +import java.util.List; + +import com.jogamp.graph.ui.Group; +import com.jogamp.graph.ui.Shape; +import com.jogamp.opengl.math.FloatUtil; +import com.jogamp.opengl.math.Vec3f; +import com.jogamp.opengl.math.geom.AABBox; +import com.jogamp.opengl.util.PMVMatrix; + +/** + * GraphUI Grid {@link Group.Layout}. + * <p> + * A grid of {@link Shape}s + * - Optional cell-size with {@link Alignment#Fill} and or {@link Alignment#Center} + * - Without cell-size behaves like a grid bag using individual shape sizes including padding + * - Can be filled in {@link Order#COLUMN} or {@link Order#ROW} major-order. + * - .. + * </p> + */ +public class GridLayout implements Group.Layout { + /** Layout order for {@link Group#getShapes()}} after population. */ + public static enum Order { + /** COLUMN layout order of {@link Group#getShapes()}} is left to right and top to bottom. */ + COLUMN, + /** ROW layout order of {@link Group#getShapes()}} is top to bottom and left to right. */ + ROW + } + private final Order order; + private final int col_limit; + private final int row_limit; + private final float cellWidth, cellHeight; + private final Alignment alignment; + private final Gap gap; + private int row_count, col_count; + + private static final boolean TRACE_LAYOUT = false; + + /** + * Default layout order of {@link Group#getShapes()}} is {@link Order#COLUMN}. + * @param column_limit [1..inf) + * @param cellWidth + * @param cellHeight + * @param alignment TODO + */ + public GridLayout(final int column_limit, final float cellWidth, final float cellHeight, final Alignment alignment) { + this(alignment, Math.max(1, column_limit), -1, cellWidth, cellHeight, new Gap()); + } + + /** + * Default layout order of {@link Group#getShapes()}} is {@link Order#COLUMN}. + * @param column_limit [1..inf) + * @param cellWidth + * @param cellHeight + * @param alignment TODO + * @param gap + */ + public GridLayout(final int column_limit, final float cellWidth, final float cellHeight, final Alignment alignment, final Gap gap) { + this(alignment, Math.max(1, column_limit), -1, cellWidth, cellHeight, gap); + } + + /** + * Default layout order of {@link Group#getShapes()}} is {@link Order#ROW}. + * @param cellWidth + * @param cellHeight + * @param alignment TODO + * @param gap + * @param row_limit [1..inf) + */ + public GridLayout(final float cellWidth, final float cellHeight, final Alignment alignment, final Gap gap, final int row_limit) { + this(alignment, -1, Math.max(1, row_limit), cellWidth, cellHeight, gap); + } + + private GridLayout(final Alignment alignment, final int column_limit, final int row_limit, final float cellWidth, final float cellHeight, final Gap gap) { + this.order = 0 < column_limit ? Order.COLUMN : Order.ROW; + this.col_limit = column_limit; + this.row_limit = row_limit; + this.cellWidth = cellWidth; + this.cellHeight = cellHeight; + this.alignment = alignment; + this.gap = gap; + row_count = 0; + col_count = 0; + } + + public Order getOrder() { return order; } + public int getColumnCount() { return col_count; } + public int getRowCount() { return row_count; } + public Gap getGap() { return gap; } + + @Override + public void layout(final Group g, final AABBox box, final PMVMatrix pmv) { + final Vec3f zeroVec3 = new Vec3f(); + final boolean hasCellWidth = !FloatUtil.isZero(cellWidth); + final boolean hasCellHeight = !FloatUtil.isZero(cellHeight); + final boolean isCenteredHoriz = hasCellWidth && alignment.isSet(Alignment.Bit.Center); + final boolean isCenteredVert = hasCellHeight && alignment.isSet(Alignment.Bit.Center); + final boolean isScaled = alignment.isSet(Alignment.Bit.Fill) && ( hasCellWidth || hasCellHeight ); + final List<Shape> shapes = g.getShapes(); + if( Order.COLUMN == order ) { + row_count = (int) Math.ceil( (double)shapes.size() / (double)col_limit ); + col_count = col_limit; + } else { // Order.ROW_MAJOR == order + row_count = row_limit; + col_count = (int) Math.ceil( (double)shapes.size() / (double)row_limit ); + } + if( TRACE_LAYOUT ) { + System.err.println("gl.00: "+order+", "+col_count+" x "+row_count+", a "+alignment+", shapes "+shapes.size()+", "+gap); + } + int col_i = 0, row_i = 0; + float x=0, y=0; + float totalWidth=-Float.MAX_VALUE, totalHeight=-Float.MAX_VALUE; + final AABBox[] sboxes = new AABBox[shapes.size()]; + final float[] y_pos = new float[col_count * row_count]; // y_bottom = totalHeight - y_pos[..] + + // Pass-1: Determine totalHeight, while collect sbox and y_pos + for(int i=0; i < shapes.size(); ++i) { + final Shape s = shapes.get(i); + // measure size + pmv.glPushMatrix(); + s.setTransform(pmv); + { + final AABBox sbox0 = s.getBounds(); + sboxes[i] = sbox0.transformMv(pmv, new AABBox()); + } + pmv.glPopMatrix(); + final AABBox sbox = sboxes[i]; + + final float sxy; + if( isScaled ) { + // scaling to cell size + final float shapeWidthU = sbox.getWidth(); + final float shapeHeightU = sbox.getHeight(); + final float cellWidthU = hasCellWidth ? cellWidth : shapeWidthU; + final float cellHeightU = hasCellHeight ? cellHeight : shapeHeightU; + final float sx = cellWidthU / shapeWidthU; + final float sy = cellHeightU/ shapeHeightU; + sxy = sx < sy ? sx : sy; + } else { + sxy = 1; + } + final float shapeWidthS = sxy*sbox.getWidth(); + final float shapeHeightS = sxy*sbox.getHeight(); + final float cellWidthS = hasCellWidth ? cellWidth : shapeWidthS; + final float cellHeightS = hasCellHeight ? cellHeight : shapeHeightS; + + // bottom y_pos, top to bottom, to be subtracted from totalHeight + final float y0 = y + cellHeightS; + final float x1 = x + cellWidthS; + totalHeight = Math.max(totalHeight, y0); + totalWidth = Math.max(totalWidth, x1); + y_pos[col_count * row_i + col_i] = y0; + if( TRACE_LAYOUT ) { + System.err.println("gl.00: y("+i+")["+col_i+"]["+row_i+"]: "+y0+", ["+cellWidthS+" x "+cellHeightS+"]"); + } + + // position for next cell + if( i + 1 < shapes.size() ) { + if( Order.COLUMN == order ) { + if( col_i + 1 == col_count ) { + col_i = 0; + row_i++; + x = 0; + y += cellHeightS + gap.height(); + } else { + col_i++; + x += cellWidthS + gap.width(); + } + } else { // Order.ROW_MAJOR == order + if( row_i + 1 == row_count ) { + row_i = 0; + col_i++; + y = 0; + x += cellWidthS + gap.width(); + } else { + row_i++; + y += cellHeightS + gap.height(); + } + } + } + } + if( TRACE_LAYOUT ) { + System.err.println("gl[__].00: Total "+totalWidth+" / "+totalHeight); + } + + // Pass-2: Layout + row_i = 0; col_i = 0; + x = 0; y = 0; + for(int i=0; i < shapes.size(); ++i) { + final Shape s = shapes.get(i); + final AABBox sbox = sboxes[i]; + final float zPos = sbox.getCenter().z(); + final Vec3f diffBL = new Vec3f(); + + { + final AABBox sbox0 = s.getBounds(); + if( !diffBL.set( sbox0.getLow().x(), sbox0.getLow().y(), 0).min( zeroVec3 ).isZero() ) { + // pmv.mulMvMatVec3f(diffBL).scale(-1f, -1f, 0f); + final Vec3f ss = s.getScale(); + diffBL.scale(-1f*ss.x(), -1f*ss.y(), 0f); + } + } + + if( TRACE_LAYOUT ) { + System.err.println("gl("+i+")["+col_i+"]["+row_i+"].0: "+s); + System.err.println("gl("+i+")["+col_i+"]["+row_i+"].0: sbox "+sbox+", diffBL "+diffBL); + } + + // IF isScaled: Uniform scale w/ lowest axis scale and center position on lower-scale axis + final float sxy; + float dxh = 0, dyh = 0; + if( isScaled ) { + // scaling to cell size + final float shapeWidthU = sbox.getWidth(); + final float shapeHeightU = sbox.getHeight(); + final float cellWidth2 = hasCellWidth ? cellWidth : shapeWidthU; + final float cellHeight2 = hasCellHeight ? cellHeight : shapeHeightU; + final float sx = cellWidth2 / shapeWidthU; + final float sy = cellHeight2/ shapeHeightU; + sxy = sx < sy ? sx : sy; + dxh += shapeWidthU * ( sx - sxy ) * 0.5f; // adjustment for scale-axis + dyh += shapeHeightU * ( sy - sxy ) * 0.5f; // ditto + if( TRACE_LAYOUT ) { + System.err.println("gl("+i+")["+col_i+"]["+row_i+"].s: "+sx+" x "+sy+" -> "+sxy+": +"+dxh+" / "+dyh+", U: s "+shapeWidthU+" x "+shapeHeightU+", sz "+cellWidth2+" x "+cellHeight2); + } + } else { + sxy = 1; + } + final float shapeWidthS = sxy*sbox.getWidth(); + final float shapeHeightS = sxy*sbox.getHeight(); + final float cellWidthS = hasCellWidth ? cellWidth : shapeWidthS; + final float cellHeightS = hasCellHeight ? cellHeight : shapeHeightS; + + y = totalHeight - y_pos[col_count * row_i + col_i]; + + if( isCenteredHoriz ) { + dxh += 0.5f * ( cellWidthS - shapeWidthS ); // actual horiz-centered + } + if( isCenteredVert ) { + dyh += 0.5f * ( cellHeightS - shapeHeightS ); // actual vert-centered + } + if( TRACE_LAYOUT ) { + System.err.println("gl("+i+")["+col_i+"]["+row_i+"].m: "+x+" / "+y+" + "+dxh+" / "+dyh+", S: s "+shapeWidthS+" x "+shapeHeightS+", sz "+cellWidthS+" x "+cellHeightS); + } + { + // New shape position, relative to previous position + final float aX = x + dxh; + final float aY = y + dyh; + s.moveTo( aX, aY, 0f ); + s.move( diffBL.scale(sxy) ); // remove the bottom-left delta + + // resize bounds including padding, excluding margin + box.resize( x, y, zPos); + box.resize( aX + cellWidthS, aY + cellHeightS, zPos); + } + s.scale( sxy, sxy, 1f); + + if( TRACE_LAYOUT ) { + System.err.println("gl("+i+")["+col_i+"]["+row_i+"].x: "+x+" / "+y+" + "+dxh+" / "+dyh+" -> "+s.getPosition()+", p3 "+shapeWidthS+" x "+shapeHeightS+", sz3 "+cellWidthS+" x "+cellHeightS+", box "+box.getWidth()+" x "+box.getHeight()); + System.err.println("gl("+i+")["+col_i+"]["+row_i+"].x: "+s); + } + + if( i + 1 < shapes.size() ) { + // position for next cell + if( Order.COLUMN == order ) { + if( col_i + 1 == col_count ) { + col_i = 0; + row_i++; + x = 0; + } else { + col_i++; + x += cellWidthS + gap.width(); + } + } else { // Order.ROW_MAJOR == order + if( row_i + 1 == row_count ) { + row_i = 0; + col_i++; + y = 0; + x += cellWidthS + gap.width(); + } else { + row_i++; + } + } + } + } + if( TRACE_LAYOUT ) { + System.err.println("gl.xx: "+box); + } + } + + @Override + public String toString() { + return "Grid["+col_count+"x"+row_count+", "+order+", cell["+cellWidth+" x "+cellHeight+", a "+alignment+"], "+gap+"]"; + } +} + diff --git a/src/graphui/classes/com/jogamp/graph/ui/layout/Margin.java b/src/graphui/classes/com/jogamp/graph/ui/layout/Margin.java new file mode 100644 index 000000000..fde7217b6 --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/layout/Margin.java @@ -0,0 +1,197 @@ +/** + * Copyright 2023 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package com.jogamp.graph.ui.layout; + +import com.jogamp.opengl.math.FloatUtil; + +/** + * GraphUI CSS property Margin, space between or around elements and not included in the element's size. + * + * The CSS margin properties are used to create space around elements, outside of any defined borders. + * + * {@link Margin#CENTER} is mapped to `zero` while earmarking {@link #isCenteredHoriz()} and {@link #isCenteredVert()}. + * The container must be sized via its layout horizontally and/or vertically matching the centered axis, similar to CSS. + */ +public class Margin { + /** Auto margin value to horizontally and/or vertically center an element within its sized-layout container, value if {@link Float#NaN}. */ + public static final float CENTER = Float.NaN; + + /** Top value */ + public final float top; + /** Right value */ + public final float right; + /** Bottom value */ + public final float bottom; + /** Left value */ + public final float left; + + private final int bits; + static private final int CENTER_HORIZ = 1 << 0; + static private final int CENTER_VERT = 1 << 1; + static private int getBits(final float top, final float right, final float bottom, final float left) { + int b = 0; + if( FloatUtil.isEqual(CENTER, left) && FloatUtil.isEqual(CENTER, right) ) { + b |= CENTER_HORIZ; + } + if( FloatUtil.isEqual(CENTER, top) && FloatUtil.isEqual(CENTER, bottom) ) { + b |= CENTER_VERT; + } + return b; + } + + /** + * Ctor w/ zero values + */ + public Margin() { + top = 0f; right = 0f; bottom = 0f; left = 0f; bits = 0; + } + + /** + * Ctor + * @param top top value + * @param right right value + * @param bottom bottom value + * @param left left value + */ + public Margin(final float top, final float right, final float bottom, final float left) { + this.bits = getBits(top, right, bottom, left); + if( isCenteredVert() ) { + this.top = 0; + this.bottom = 0; + } else { + this.top = top; + this.bottom = bottom; + } + if( isCenteredHoriz() ) { + this.right = 0; + this.left = 0; + } else { + this.right = right; + this.left = left; + } + } + + /** + * Ctor + * @param top top value + * @param rl right and left value, use {@link #CENTER} to horizontally center the element in its container + * @param bottom bottom value + */ + public Margin(final float top, final float rl, final float bottom) { + this.bits = getBits(top, rl, bottom, rl); + if( isCenteredVert() ) { + this.top = 0; + this.bottom = 0; + } else { + this.top = top; + this.bottom = bottom; + } + if( isCenteredHoriz() ) { + this.right = 0; + this.left = 0; + } else { + this.right = rl; + this.left = rl; + } + } + + /** + * Ctor + * @param tb top and bottom value, use {@link #CENTER} to vertically center the element in its container + * @param rl right and left value, use {@link #CENTER} to horizontally center the element in its container + */ + public Margin(final float tb, final float rl) { + this.bits = getBits(tb, rl, tb, rl); + if( isCenteredVert() ) { + this.top = 0; + this.bottom = 0; + } else { + this.top = tb; + this.bottom = tb; + } + if( isCenteredHoriz() ) { + this.right = 0; + this.left = 0; + } else { + this.right = rl; + this.left = rl; + } + } + + /** + * Ctor + * @param trbl top, right, bottom and left value, use {@link #CENTER} to horizontally and vertically center the element in its container. + */ + public Margin(final float trbl) { + this.bits = getBits(trbl, trbl, trbl, trbl); + if( isCenteredVert() ) { + this.top = 0; + this.bottom = 0; + } else { + this.top = trbl; + this.bottom = trbl; + } + if( isCenteredHoriz() ) { + this.right = 0; + this.left = 0; + } else { + this.right = trbl; + this.left = trbl; + } + } + + /** Returns true if {@link #left} and {@link #right} is {@link #CENTER}. */ + public boolean isCenteredHoriz() { + return 0 != ( CENTER_HORIZ & bits ); + } + + /** Returns true if {@link #top} and {@link #bottom} is {@link #CENTER}. */ + public boolean isCenteredVert() { + return 0 != ( CENTER_VERT & bits ); + } + + /** Returns true if {@link #isCenteredHoriz()} and {@link #isCenteredVert()} is true, i.e. for horizontal and vertical center. */ + public boolean isCentered() { + return 0 != ( ( CENTER_VERT | CENTER_HORIZ ) & bits ); + } + + /** Return width of horizontal values top + right. Zero if {@link #isCenteredHoriz()}. */ + public float width() { return left + right; } + + /** Return height of vertical values bottom + top. Zero if {@link #isCenteredVert()}. */ + public float height() { return bottom + top; } + + public boolean zeroSumWidth() { return FloatUtil.isZero( width() ); }; + + public boolean zeroSumHeight() { return FloatUtil.isZero( height() ); }; + + public boolean zeroSumSize() { return zeroSumWidth() && zeroSumHeight(); } + + @Override + public String toString() { return "Margin[t "+top+", r "+right+", b "+bottom+", l "+left+", ctr[h "+isCenteredHoriz()+", v "+isCenteredVert()+"]]"; } +} diff --git a/src/graphui/classes/com/jogamp/graph/ui/layout/Padding.java b/src/graphui/classes/com/jogamp/graph/ui/layout/Padding.java new file mode 100644 index 000000000..1d5eca002 --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/layout/Padding.java @@ -0,0 +1,106 @@ +/** + * Copyright 2023 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package com.jogamp.graph.ui.layout; + +import com.jogamp.opengl.math.FloatUtil; + +/** + * GraphUI CSS property Padding, space belonging to the element and included in the element's size. + * + * The CSS padding properties are used to generate space around an element's content, inside of any defined borders. + */ +public class Padding { + /** Top value */ + public final float top; + /** Right value */ + public final float right; + /** Bottom value */ + public final float bottom; + /** Left value */ + public final float left; + + /** + * Ctor w/ zero values + */ + public Padding() { + top = 0f; right = 0f; bottom = 0f; left = 0f; + } + + /** + * Ctor + * @param top top value + * @param right right value + * @param bottom bottom value + * @param left left value + */ + public Padding(final float top, final float right, final float bottom, final float left) { + this.top = top; this.right = right; this.bottom = bottom; this.left = left; + } + + /** + * Ctor + * @param top top value + * @param rl right and left value + * @param bottom bottom value + */ + public Padding(final float top, final float rl, final float bottom) { + this.top = top; this.right = rl; this.bottom = bottom; this.left = rl; + } + + /** + * Ctor + * @param tb top and bottom value + * @param rl right and left value + */ + public Padding(final float tb, final float rl) { + this.top = tb; this.right = rl; this.bottom = tb; this.left = rl; + } + + /** + * Ctor + * @param trbl top, right, bottom and left value + */ + public Padding(final float trbl) { + this.top = trbl; this.right = trbl; this.bottom = trbl; this.left = trbl; + } + + /** Return width of horizontal values top + right. */ + public float width() { return left + right; } + + /** Return height of vertical values bottom + top. */ + public float height() { return bottom + top; } + + public boolean zeroSumWidth() { return FloatUtil.isZero( width() ); }; + + public boolean zeroSumHeight() { return FloatUtil.isZero( height() ); }; + + public boolean zeroSumSize() { return zeroSumWidth() && zeroSumHeight(); } + + @Override + public String toString() { return "Padding[t "+top+", r "+right+", b "+bottom+", l "+left+"]"; } +} diff --git a/src/graphui/classes/com/jogamp/graph/ui/package.html b/src/graphui/classes/com/jogamp/graph/ui/package.html new file mode 100644 index 000000000..13efd2e9b --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/package.html @@ -0,0 +1,33 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> +<html> +<head> + <title>Public Graph UI Package</title> +</head> + <body> + +<h2>Public <i>Graph</i> UI Package</h2> + +<h3>Disclaimer</h3> + <p> + The API of the namespace <i>com.jogamp.graph.ui.**</i> + is experimental and subject to change until further notice. + </p> + <p> + Part of It's implementation <i>jogamp.graph.ui.**</i> may change at any time + as it is natural with all other API implementations. + </p> + <p> + We are currently refining and completing this new API and it's implementation. + Feel free to comment and help using our public channels. + </p> +<h3>Revision History<br> + </h3> + +<ul> +<li> Early Draft Review, March 10th 2023</li> +</ul> + <br> + <br> + <br> +</body> +</html> diff --git a/src/graphui/classes/com/jogamp/graph/ui/shapes/BaseButton.java b/src/graphui/classes/com/jogamp/graph/ui/shapes/BaseButton.java new file mode 100644 index 000000000..7c3b1119e --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/shapes/BaseButton.java @@ -0,0 +1,171 @@ +/** + * Copyright 2010-2023 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package com.jogamp.graph.ui.shapes; + +import com.jogamp.graph.curve.OutlineShape; +import com.jogamp.graph.ui.GraphShape; +import com.jogamp.opengl.GL2ES2; +import com.jogamp.opengl.GLProfile; + +/** + * An abstract GraphUI base filled button {@link GraphShape}, + * usually used as a backdrop or base shape for more informative button types. + * <p> + * GraphUI is GPU based and resolution independent. + * </p> + * <p> + * This button is rendered with a round oval shape {@link #ROUND_CORNER by default}, + * but can be set to {@link #PERP_CORNER rectangular shape}. + * </p> + */ +public class BaseButton extends GraphShape { + + /** {@link #setCorner(float) Round corner}, value {@value}. This is the default value. */ + public static final float ROUND_CORNER = 1f; + /** {@link #setCorner(float) Perpendicular corner} for a rectangular shape, value {@value}. */ + public static final float PERP_CORNER = 0f; + + protected float width; + protected float height; + protected float corner = ROUND_CORNER; + + public BaseButton(final int renderModes, final float width, final float height) { + super(renderModes); + this.width = width; + this.height = height; + } + + public final float getWidth() { return width; } + + public final float getHeight() { return height; } + + public final float getCorner() { return corner; } + + /** + * Set corner size with range [0.01 .. 1.00] for round corners + * or `zero` for perpendicular corners. + * <p> + * , default is {@link #ROUND_CORNER round corner}, + * alternative a {@link #PERP_CORNER perpendicular corner} for a rectangular shape is available. + * </p> + * @see #ROUND_CORNER + * @see #PERP_CORNER + */ + public BaseButton setCorner(final float corner) { + if( 0.01f <= corner && corner <= 1.0f ) { + this.corner = corner; + } + if( corner > 1.0f ){ + this.corner = 1.0f; + } else if( corner < 0.01f ){ + this.corner = 0.0f; + } else { + this.corner = corner; + } + markShapeDirty(); + return this; + } + + public BaseButton setSize(final float width, final float height) { + this.width = width; + this.height = height; + markShapeDirty(); + return this; + } + + @Override + protected void addShapeToRegion(final GLProfile glp, final GL2ES2 gl) { + final OutlineShape shape = createBaseShape(0f); + resetGLRegion(glp, gl, null, shape); + region.addOutlineShape(shape, null, rgbaColor); + box.resize(shape.getBounds()); + setRotationPivot( box.getCenter() ); + } + + protected OutlineShape createBaseShape(final float zOffset) { + final OutlineShape shape = new OutlineShape(); + if(corner == 0.0f) { + createSharpOutline(shape, zOffset); + } else { + createCurvedOutline(shape, zOffset); + } + shape.setIsQuadraticNurbs(); + shape.setSharpness(oshapeSharpness); + if( DEBUG_DRAW ) { + System.err.println("GraphShape.RoundButton: Shape: "+shape+", "+box); + } + return shape; + } + + protected void createSharpOutline(final OutlineShape shape, final float zOffset) { + final float tw = getWidth(); + final float th = getHeight(); + + final float minX = 0; + final float minY = 0; + final float minZ = zOffset; + + shape.addVertex(minX, minY, minZ, true); + shape.addVertex(minX+tw, minY, minZ, true); + shape.addVertex(minX+tw, minY + th, minZ, true); + shape.addVertex(minX, minY + th, minZ, true); + shape.closeLastOutline(true); + } + + protected void createCurvedOutline(final OutlineShape shape, final float zOffset) { + final float tw = getWidth(); + final float th = getHeight(); + final float dC = 0.5f*corner*Math.min(tw, th); + + final float minX = 0; + final float minY = 0; + final float minZ = zOffset; + + shape.addVertex(minX, minY + dC, minZ, true); + shape.addVertex(minX, minY, minZ, false); + + shape.addVertex(minX + dC, minY, minZ, true); + + shape.addVertex(minX + tw - dC, minY, minZ, true); + shape.addVertex(minX + tw, minY, minZ, false); + shape.addVertex(minX + tw, minY + dC, minZ, true); + shape.addVertex(minX + tw, minY + th- dC, minZ, true); + shape.addVertex(minX + tw, minY + th, minZ, false); + shape.addVertex(minX + tw - dC, minY + th, minZ, true); + shape.addVertex(minX + dC, minY + th, minZ, true); + shape.addVertex(minX, minY + th, minZ, false); + shape.addVertex(minX, minY + th - dC, minZ, true); + + shape.closeLastOutline(true); + } + + @Override + public String getSubString() { + return super.getSubString()+", dim "+getWidth() + " x " + getHeight() + ", corner " + corner; + } +} diff --git a/src/graphui/classes/com/jogamp/graph/ui/shapes/Button.java b/src/graphui/classes/com/jogamp/graph/ui/shapes/Button.java new file mode 100644 index 000000000..5fe99c5c9 --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/shapes/Button.java @@ -0,0 +1,212 @@ +/** + * Copyright 2010-2023 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package com.jogamp.graph.ui.shapes; + +import com.jogamp.opengl.GL2ES2; +import com.jogamp.opengl.GLProfile; +import com.jogamp.graph.curve.OutlineShape; +import com.jogamp.graph.curve.Region; +import com.jogamp.graph.curve.opengl.RegionRenderer; +import com.jogamp.graph.curve.opengl.TextRegionUtil; +import com.jogamp.graph.font.Font; +import com.jogamp.graph.geom.plane.AffineTransform; +import com.jogamp.graph.ui.GraphShape; +import com.jogamp.opengl.math.FloatUtil; +import com.jogamp.opengl.math.Vec2f; +import com.jogamp.opengl.math.Vec3f; +import com.jogamp.opengl.math.Vec4f; +import com.jogamp.opengl.math.geom.AABBox; + +import jogamp.graph.ui.shapes.Label0; + +/** + * A GraphUI text labeled {@link BaseButton} {@link GraphShape} + * <p> + * GraphUI is GPU based and resolution independent. + * </p> + * <p> + * This button is rendered with a round oval shape. + * To render it rectangular, {@link #setCorner(float)} to zero. + * </p> + */ +public class Button extends BaseButton { + /** {@value} */ + public static final float DEFAULT_SPACING_X = 0.12f; + /** {@value} */ + public static final float DEFAULT_SPACING_Y = 0.42f; + + private static final float DEFAULT_LABEL_ZOFFSET = 0.005f; // 0.05f; + private float labelZOffset; + + private final Label0 label; + private float spacingX = DEFAULT_SPACING_X; + private float spacingY = DEFAULT_SPACING_Y; + + private final AffineTransform tempT1 = new AffineTransform(); + private final AffineTransform tempT2 = new AffineTransform(); + private final AffineTransform tempT3 = new AffineTransform(); + + public Button(final int renderModes, final Font labelFont, + final String labelText, final float width, + final float height) { + super(renderModes | Region.COLORCHANNEL_RENDERING_BIT, width, height); + this.labelZOffset = DEFAULT_LABEL_ZOFFSET; + this.label = new Label0(labelFont, labelText, new Vec4f( 1.66f, 1.66f, 1.66f, 1.0f )); // 0.60 * 1.66 ~= 1.0 + } + + public Font getFont() { return label.getFont(); } + public String getLaben() { return label.getText(); } + + @Override + public void draw(final GL2ES2 gl, final RegionRenderer renderer, final int[] sampleCount) { + // No need to setup an poly offset for z-fighting, using one region now + // Setup poly offset for z-fighting + // gl.glEnable(GL.GL_POLYGON_OFFSET_FILL); + // gl.glPolygonOffset(0f, 1f); + super.draw(gl, renderer, sampleCount); + // gl.glDisable(GL.GL_POLYGON_OFFSET_FILL); + } + + @Override + protected void addShapeToRegion(final GLProfile glp, final GL2ES2 gl) { + final OutlineShape shape = createBaseShape( FloatUtil.isZero(labelZOffset) ? 0f : -labelZOffset ); + box.resize(shape.getBounds()); + setRotationPivot( box.getCenter() ); + + // Sum Region buffer size of base-shape + text + final int[/*2*/] vertIndexCount = Region.countOutlineShape(shape, new int[2]); + TextRegionUtil.countStringRegion(label.getFont(), label.getText(), vertIndexCount); + resetGLRegion(glp, gl, null, vertIndexCount[0], vertIndexCount[1]); + + region.addOutlineShape(shape, null, rgbaColor); + + // Precompute text-box size .. guessing pixelSize + final float lw = box.getWidth() * ( 1f - spacingX ) ; + final float lh = box.getHeight() * ( 1f - spacingY ) ; + final AABBox lbox0_em = label.getFont().getGlyphBounds(label.getText(), tempT1, tempT2); + // final AABBox lbox0_em = label.getFont().getGlyphShapeBounds(null, label.getText(), tempT1, tempT2); + final float lsx = lw / lbox0_em.getWidth(); + final float lsy = lh / lbox0_em.getHeight(); + final float lScale = lsx < lsy ? lsx : lsy; + + // Setting left-corner transform using text-box in font em-size [0..1] + final AABBox lbox1_s = new AABBox(lbox0_em).scale2(lScale); + // Center text .. (share same center w/ button) + final Vec3f lctr = lbox1_s.getCenter(); + final Vec3f ctr = box.getCenter(); + final Vec2f ltxy = new Vec2f(ctr.x() - lctr.x(), ctr.y() - lctr.y() ); + + if( DEBUG_DRAW ) { + System.err.println("Button: dim "+width+" x "+height+", spacing "+spacingX+", "+spacingY); + System.err.println("Button: net-text "+lw+" x "+lh); + System.err.println("Button: shape "+box); + System.err.println("Button: text_em "+lbox0_em+" em, "+label.getText()); + System.err.println("Button: lscale "+lsx+" x "+lsy+" -> "+lScale); + System.err.printf ("Button: text_s %s%n", lbox1_s); + System.err.printf ("Button: ltxy %s, %f / %f%n", ltxy, ltxy.x() * lScale, ltxy.y() * lScale); + } + + final AABBox lbox2 = label.addShapeToRegion(lScale, region, ltxy, tempT1, tempT2, tempT3); + if( DEBUG_DRAW ) { + System.err.printf("Button.X: lbox2 %s%n", lbox2); + } + } + + public float getLabelZOffset() { return labelZOffset; } + + public Button setLabelZOffset(final float v) { + labelZOffset = v; + markShapeDirty(); + return this; + } + + public final float getSpacingX() { return spacingX; } + public final float getSpacingY() { return spacingY; } + + /** + * In percent of text label + * @param spacingX spacing in percent on X, default is {@link #DEFAULT_SPACING_X} + * @param spacingY spacing in percent on Y, default is {@link #DEFAULT_SPACING_Y} + */ + public final Button setSpacing(final float spacingX, final float spacingY) { + if ( spacingX < 0.0f ) { + this.spacingX = 0.0f; + } else if ( spacingX > 1.0f ) { + this.spacingX = 1.0f; + } else { + this.spacingX = spacingX; + } + if ( spacingY < 0.0f ) { + this.spacingY = 0.0f; + } else if ( spacingY > 1.0f ) { + this.spacingY = 1.0f; + } else { + this.spacingY = spacingY; + } + markShapeDirty(); + return this; + } + + public final Vec4f getLabelColor() { + return label.getColor(); + } + + public final Button setLabelColor(final float r, final float g, final float b) { + label.setColor(r, g, b, 1.0f); + markShapeDirty(); + return this; + } + + public final Button setFont(final Font labelFont) { + if( !label.getFont().equals(labelFont) ) { + label.setFont(labelFont); + markShapeDirty(); + } + return this; + } + public final Button setLabel(final String labelText) { + if( !label.getText().equals(labelText) ) { + label.setText(labelText); + markShapeDirty(); + } + return this; + } + public final Button setLabel(final Font labelFont, final String labelText) { + if( !label.getText().equals(labelText) || !label.getFont().equals(labelFont) ) { + label.setFont(labelFont); + label.setText(labelText); + markShapeDirty(); + } + return this; + } + + @Override + public String getSubString() { + return super.getSubString()+", "+ label + ", " + "spacing["+spacingX+", "+spacingY+"]"; + } +} diff --git a/src/graphui/classes/com/jogamp/graph/ui/shapes/CrossHair.java b/src/graphui/classes/com/jogamp/graph/ui/shapes/CrossHair.java new file mode 100644 index 000000000..d27fe53e7 --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/shapes/CrossHair.java @@ -0,0 +1,103 @@ +/** + * Copyright 2010-2023 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package com.jogamp.graph.ui.shapes; + +import com.jogamp.graph.curve.OutlineShape; +import com.jogamp.graph.ui.GraphShape; +import com.jogamp.opengl.GL2ES2; +import com.jogamp.opengl.GLProfile; + +/** + * A GraphUI Crosshair {@link GraphShape} + * <p> + * GraphUI is GPU based and resolution independent. + * </p> + */ +public class CrossHair extends GraphShape { + private float width, height, lineWidth; + + public CrossHair(final int renderModes, final float width, final float height, final float linewidth) { + super(renderModes); + this.width = width; + this.height = height; + this.lineWidth = linewidth; + } + + public final float getWidth() { return width; } + public final float getHeight() { return height; } + public final float getLineWidth() { return lineWidth; } + + public void setDimension(final float width, final float height, final float lineWidth) { + this.width = width; + this.height = height; + this.lineWidth = lineWidth; + markShapeDirty(); + } + + @Override + protected void addShapeToRegion(final GLProfile glp, final GL2ES2 gl) { + final OutlineShape shape = new OutlineShape(); + + final float lwh = lineWidth/2f; + + final float tw = getWidth(); + final float th = getHeight(); + final float twh = tw/2f; + final float thh = th/2f; + + final float ctrX = 0f, ctrY = 0f; + final float ctrZ = 0f; + + // middle vertical (CCW!) + shape.moveTo(ctrX-lwh, ctrY-thh, ctrZ); + shape.lineTo(ctrX+lwh, ctrY-thh, ctrZ); + shape.lineTo(ctrX+lwh, ctrY+thh, ctrZ); + shape.lineTo(ctrX-lwh, ctrY+thh, ctrZ); + shape.closePath(); + + // middle horizontal (CCW!) + shape.moveTo(ctrX-twh, ctrY-lwh, ctrZ); + shape.lineTo(ctrX+twh, ctrY-lwh, ctrZ); + shape.lineTo(ctrX+twh, ctrY+lwh, ctrZ); + shape.lineTo(ctrX-twh, ctrY+lwh, ctrZ); + shape.closePath(); + + shape.setIsQuadraticNurbs(); + shape.setSharpness(oshapeSharpness); + + resetGLRegion(glp, gl, null, shape); + region.addOutlineShape(shape, null, rgbaColor); + box.resize(shape.getBounds()); + setRotationPivot( box.getCenter() ); + } + + @Override + public String getSubString() { + return super.getSubString()+", dim "+getWidth() + " x " + getHeight(); + } +} diff --git a/src/graphui/classes/com/jogamp/graph/ui/shapes/GLButton.java b/src/graphui/classes/com/jogamp/graph/ui/shapes/GLButton.java new file mode 100644 index 000000000..57af3587c --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/shapes/GLButton.java @@ -0,0 +1,177 @@ +/** + * Copyright 2014-2023 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package com.jogamp.graph.ui.shapes; + +import com.jogamp.opengl.GL; +import com.jogamp.opengl.GL2ES2; +import com.jogamp.opengl.GLCapabilities; +import com.jogamp.opengl.GLCapabilitiesImmutable; +import com.jogamp.opengl.GLContext; +import com.jogamp.opengl.GLDrawable; +import com.jogamp.opengl.GLDrawableFactory; +import com.jogamp.opengl.GLEventListener; +import com.jogamp.opengl.GLOffscreenAutoDrawable; +import com.jogamp.graph.curve.opengl.RegionRenderer; +import com.jogamp.graph.ui.GraphShape; +import com.jogamp.opengl.FBObject; +import com.jogamp.opengl.util.texture.ImageSequence; +import com.jogamp.opengl.util.texture.Texture; + +/** + * A GraphUI {@link GLEventListener} based {@link TexSeqButton} {@link GraphShape}. + * <p> + * GraphUI is GPU based and resolution independent. + * </p> + * <p> + * The {@link GLEventListener} is rendered via an {@link GLOffscreenAutoDrawable.FBO} into an {@link ImageSequence}. + * </p> + * <p> + * This button is rendered with a round oval shape. + * To render it rectangular, {@link #setCorner(float)} to zero. + * </p> + * <p> + * Default colors (toggle-on is full color): + * - non-toggle: 1 * color + * - pressed: 0.9 * color + * - toggle-off: 0.8 * color + * - toggle-on: 1.0 * color + * </p> + */ +public class GLButton extends TexSeqButton { + private final GLEventListener glel; + private final boolean useAlpha; + private volatile int fboWidth = 200; + private volatile int fboHeight = 200; + private volatile GLOffscreenAutoDrawable.FBO fboGLAD = null; + private boolean animateGLEL = false; + + public GLButton(final int renderModes, final float width, final float height, + final int textureUnit, final GLEventListener glel, final boolean useAlpha) { + super(renderModes, width, height, new ImageSequence(textureUnit, true)); + this.glel = glel; + this.useAlpha = useAlpha; + + setColor(1.0f, 1.0f, 1.0f, 1.0f); + setPressedColorMod(0.9f, 0.9f, 0.9f, 0.7f); + setToggleOffColorMod(0.8f, 0.8f, 0.8f, 1.0f); + setToggleOnColorMod(1.0f, 1.0f, 1.0f, 1.0f); + + // fake surface-size, will be overriden in initial FBO setup @ display + this.fboWidth = 320; + this.fboHeight = Math.round( 640 * height / width ); + } + + public final void setAnimate(final boolean v) { animateGLEL = v; } + public final boolean getAnimate() { return animateGLEL; } + + public final void setFBOSize(final int fboWidth, final int fboHeight) { + this.fboWidth = fboWidth; + this.fboHeight = fboHeight; + } + + public final GLOffscreenAutoDrawable.FBO getFBOAutoDrawable() { return fboGLAD; } + + @Override + protected void destroyImpl(final GL2ES2 gl, final RegionRenderer renderer) { + ((ImageSequence)texSeq).destroy(gl); + fboGLAD.destroy(); + } + + @Override + public void draw(final GL2ES2 gl, final RegionRenderer renderer, final int[] sampleCount) { + final int[/*2*/] surfaceSize = getSurfaceSize(renderer.getMatrix(), renderer.getViewport(), new int[2]); + final boolean got_sz = null != surfaceSize && 0 < surfaceSize[0] && 0 < surfaceSize[1]; + + if( null == fboGLAD ) { + final ImageSequence imgSeq = (ImageSequence)texSeq; + + final GLContext ctx = gl.getContext(); + final GLDrawable drawable = ctx.getGLDrawable(); + final GLCapabilitiesImmutable reqCaps = drawable.getRequestedGLCapabilities(); + final GLCapabilities caps = (GLCapabilities) reqCaps.cloneMutable(); + caps.setFBO(true); + caps.setDoubleBuffered(false); + if( !useAlpha ) { + caps.setAlphaBits(0); + } + final GLDrawableFactory factory = GLDrawableFactory.getFactory(caps.getGLProfile()); + + // System.err.println("XXX FBO initSurfaceSize got_sz "+got_sz+", "+fboWidth+" x "+fboHeight+" -> "+surfaceSize[0]+" x "+surfaceSize[1]); + if( got_sz ) { + // override with real surface-size + fboWidth = surfaceSize[0]; + fboHeight = surfaceSize[1]; + } + fboGLAD = (GLOffscreenAutoDrawable.FBO) factory.createOffscreenAutoDrawable( + drawable.getNativeSurface().getGraphicsConfiguration().getScreen().getDevice(), + caps, null, fboWidth, fboHeight); + fboWidth = 0; + fboHeight = 0; + fboGLAD.setSharedContext(ctx); + fboGLAD.setTextureUnit(imgSeq.getTextureUnit()); + fboGLAD.addGLEventListener(glel); + fboGLAD.display(); // 1st init! + + final FBObject.TextureAttachment texA01 = fboGLAD.getColorbuffer(GL.GL_FRONT).getTextureAttachment(); + final Texture tex = new Texture(texA01.getName(), imgSeq.getTextureTarget(), + fboGLAD.getSurfaceWidth(), fboGLAD.getSurfaceHeight(), fboGLAD.getSurfaceWidth(), fboGLAD.getSurfaceHeight(), + false /* mustFlipVertically */); + imgSeq.addFrame(gl, tex); + markStateDirty(); + } else if( 0 != fboWidth*fboHeight ) { + fboGLAD.setSurfaceSize(fboWidth, fboHeight); + fboWidth = 0; + fboHeight = 0; + markStateDirty(); + } else if( got_sz && ( fboGLAD.getSurfaceWidth() != surfaceSize[0] || fboGLAD.getSurfaceHeight() != surfaceSize[1] ) ) { + // System.err.println("XXX FBO setSurfaceSize "+fboGLAD.getSurfaceWidth()+" x "+fboGLAD.getSurfaceHeight()+" -> "+surfaceSize[0]+" x "+surfaceSize[1]); + final ImageSequence imgSeq = (ImageSequence)texSeq; + + fboGLAD.setSurfaceSize(surfaceSize[0], surfaceSize[1]); + fboGLAD.display(); // re-init! + + imgSeq.destroy(gl); + final FBObject.TextureAttachment texA01 = fboGLAD.getColorbuffer(GL.GL_FRONT).getTextureAttachment(); + final Texture tex = new Texture(texA01.getName(), imgSeq.getTextureTarget(), + fboGLAD.getSurfaceWidth(), fboGLAD.getSurfaceHeight(), fboGLAD.getSurfaceWidth(), fboGLAD.getSurfaceHeight(), + false /* mustFlipVertically */); + imgSeq.addFrame(gl, tex); + fboWidth = 0; + fboHeight = 0; + markStateDirty(); + } else if( animateGLEL ) { + fboGLAD.display(); + } + + super.draw(gl, renderer, sampleCount); + + if( animateGLEL ) { + markStateDirty(); // keep on going + } + } +} diff --git a/src/graphui/classes/com/jogamp/graph/ui/shapes/GlyphShape.java b/src/graphui/classes/com/jogamp/graph/ui/shapes/GlyphShape.java new file mode 100644 index 000000000..c579cb943 --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/shapes/GlyphShape.java @@ -0,0 +1,206 @@ +/** + * Copyright 2010-2023 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package com.jogamp.graph.ui.shapes; + +import java.util.List; + +import com.jogamp.graph.curve.OutlineShape; +import com.jogamp.graph.curve.Region; +import com.jogamp.graph.curve.opengl.GLRegion; +import com.jogamp.graph.font.Font; +import com.jogamp.graph.font.Font.Glyph; +import com.jogamp.graph.geom.plane.AffineTransform; +import com.jogamp.graph.ui.GraphShape; +import com.jogamp.opengl.GL2ES2; +import com.jogamp.opengl.GLProfile; +import com.jogamp.opengl.math.Vec3f; +import com.jogamp.opengl.math.geom.AABBox; +import com.jogamp.opengl.util.texture.TextureSequence; + +/** + * Representing a single {@link Font.Glyph} as a {@link GraphShape} + * + * A GlyphShape is represented in font em-size [0..1] unscaled w/ bottom-left origin at 0/0 + * while preserving an intended position, see {@link #getOrigPos()} and {@link #getOrigPos(float)}. + * + * Scaling, if any, should be applied via {@link #setScale(float, float, float)} etc. + */ +public class GlyphShape extends GraphShape { + private final char symbol; + private final Glyph glyph; + private final int regionVertCount; + private final int regionIdxCount; + private final Vec3f origPos; + + /** + * Creates a new GlyphShape + * @param renderModes Graph's {@link Region} render modes, see {@link GLRegion#create(GLProfile, int, TextureSequence) create(..)}. + * @param symbol the represented character + * @param glyph the {@link Font.Glyph} + * @param x the intended unscaled X position of this Glyph, e.g. if part of a string - otherwise use zero. + * @param y the intended unscaled Y position of this Glyph, e.g. if part of a string - otherwise use zero. + * @see #processString(List, int, Font, String) + */ + public GlyphShape(final int renderModes, final char symbol, final Glyph glyph, final float x, final float y) { + super(renderModes); + this.symbol = symbol; + this.glyph = glyph; + this.origPos = new Vec3f(x, y, 0f); + if( glyph.isWhiteSpace() || null == glyph.getShape() ) { + setEnabled(false); + } + final int[/*2*/] vertIndexCount = Region.countOutlineShape(glyph.getShape(), new int[2]); + regionVertCount = vertIndexCount[0]; + regionIdxCount = vertIndexCount[1]; + } + + /** + * Creates a new GlyphShape + * @param renderModes Graph's {@link Region} render modes, see {@link GLRegion#create(GLProfile, int, TextureSequence) create(..)}. + * @param font the {@link Font} to lookup the symbol's {@link Font.Glyph} + * @param symbol the represented character + * @param x the intended unscaled X position of this Glyph, e.g. if part of a string - otherwise use zero. + * @param y the intended unscaled Y position of this Glyph, e.g. if part of a string - otherwise use zero. + */ + public GlyphShape(final int renderModes, final Font font, final char symbol, final float x, final float y) { + this(renderModes, symbol, font.getGlyph( font.getGlyphID(symbol) ), x, y); + } + + /** Returns the char symbol to be rendered. */ + public char getSymbol() { + return symbol; + } + + /** + * Returns the {@link Font.Glyph} to be rendered. + */ + public Glyph getGlyph() { + return glyph; + } + + /** + * Returns the {@link Font} used to render the text + */ + public Font getFont() { + return glyph.getFont(); + } + + /** + * Returns the unscaled original position of this glyph, e.g. if part of a string, otherwise zero. + * + * Method borrows and returns the internal instance. + * + * @see #processString(List, int, Font, String) + */ + public Vec3f getOrigPos() { return origPos; } + + /** + * Returns the unscaled original position of this glyph, e.g. if part of a string, otherwise zero. + * + * @param s {@link Vec3f} storage to be returned + * @return storage containing the unscaled original position + * @see #processString(List, int, Font, String) + */ + public Vec3f getOrigPos(final Vec3f s) { return s.set(origPos); } + + /** + * Returns a copy of the scaled original position of this glyph, see {@link #getOrigPos(Vec3f)} + * @see #processString(List, int, Font, String) + */ + public Vec3f getOrigPos(final float scale) { return origPos.mul(scale); } + + /** + * Returns the scaled original position of this glyph, see {@link #getOrigPos(float)} + * @param s {@link Vec3f} storage to be returned + * @return storage containing the scaled original position + * @see #processString(List, int, Font, String) + */ + public Vec3f getOrigPos(final Vec3f s, final float scale) { return s.set(origPos).scale(scale); } + + /** Resets this Shape's position to the scaled original position, see {@link #getOrigPos(float)}. */ + public void resetPos(final float scale) { + moveTo(origPos.x() * scale, origPos.y() * scale, 0f); + } + + /** Resets this Shape's position to the scaled original position and {@link #setScale(float, float, float) set scale}, see {@link #resetPos(float)}. */ + public void resetPosAndScale(final float scale) { + moveTo(origPos.x() * scale, origPos.y() * scale, 0f); + setScale(scale, scale, 1f); + } + + /** Returns {@link Font#getLineHeight()}. */ + public float getLineHeight() { + return glyph.getFont().getLineHeight(); + } + + /** + * Process the given text resulting in a list of {@link GlyphShape}s with stored original position {@link #getOrigX()} and {@link #getOrigY()} each at font em-size [0..1]. + * @param res storage for resulting {@link GlyphShape}s. + * @param renderModes Graph's {@link Region} render modes, see {@link GLRegion#create(GLProfile, int, TextureSequence) create(..)}. + * @param font {@link Font} used + * @param text text to be represented + * @return the bounding box of the given string by taking each glyph's font em-sized [0..1] OutlineShape into account. + * @see #getOrigX() + * @see #getOrigY() + */ + public static final AABBox processString(final List<GlyphShape> res, final int renderModes, final Font font, final String text) { + final Font.GlyphVisitor fgv = new Font.GlyphVisitor() { + @Override + public void visit(final char symbol, final Glyph glyph, final AffineTransform t) { + if( !glyph.isWhiteSpace() && null != glyph.getShape() ) { + res.add( new GlyphShape(renderModes, symbol, glyph, t.getTranslateX(), t.getTranslateY()) ); + } + } + }; + return font.processString(fgv, null, text, new AffineTransform(), new AffineTransform()); + } + + @Override + protected void addShapeToRegion(final GLProfile glp, final GL2ES2 gl) { + final OutlineShape shape = glyph.getShape(); + box.reset(); + if( null != shape ) { + final AABBox sbox = shape.getBounds(); + final AffineTransform tmp = new AffineTransform(); + // Enforce bottom-left origin @ 0/0 for good drag-zoom experience, + // but keep the underline (decline) intact! + tmp.setToTranslation(-sbox.getMinX(), -sbox.getMinY() + glyph.getBounds().getMinY()); + shape.setSharpness(oshapeSharpness); + + resetGLRegion(glp, gl, null, regionVertCount, regionIdxCount); + region.addOutlineShape(shape, tmp, rgbaColor); + box.resize(tmp.transform(sbox, new AABBox())); + setRotationPivot( box.getCenter() ); + } + } + + @Override + public String getSubString() { + return super.getSubString()+", origPos " + origPos.x() + " / " + origPos.y() + ", '" + symbol + "'"; + } +} diff --git a/src/graphui/classes/com/jogamp/graph/ui/shapes/ImageButton.java b/src/graphui/classes/com/jogamp/graph/ui/shapes/ImageButton.java new file mode 100644 index 000000000..cd919546d --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/shapes/ImageButton.java @@ -0,0 +1,76 @@ +/** + * Copyright 2014-2023 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package com.jogamp.graph.ui.shapes; + +import com.jogamp.opengl.GL2ES2; +import com.jogamp.graph.curve.opengl.RegionRenderer; +import com.jogamp.graph.ui.GraphShape; +import com.jogamp.opengl.util.texture.ImageSequence; + +/** + * A GraphUI {@link ImageSequence} based {@link TexSeqButton} {@link GraphShape}. + * <p> + * GraphUI is GPU based and resolution independent. + * </p> + * <p> + * This button is rendered with a round oval shape. + * To render it rectangular, {@link #setCorner(float)} to zero. + * </p> + * <p> + * Default colors (toggle-off is full color): + * - non-toggle: 1 * color + * - pressed: 0.9 * color + * - toggle-off: 1.0 * color + * - toggle-on: 0.8 * color + * </p> + */ +public class ImageButton extends TexSeqButton { + + public ImageButton(final int renderModes, final float width, + final float height, final ImageSequence texSeq) { + super(renderModes, width, height, texSeq); + + setColor(1f, 1f, 1f, 1.0f); + setPressedColorMod(0.9f, 0.9f, 0.9f, 0.9f); + setToggleOffColorMod(1f, 1f, 1f, 1f); + setToggleOnColorMod(0.8f, 0.8f, 0.8f, 1f); + } + + public final void setCurrentIdx(final int idx) { + ((ImageSequence)texSeq).setCurrentIdx(idx); + markStateDirty(); + } + + @Override + public void draw(final GL2ES2 gl, final RegionRenderer renderer, final int[] sampleCount) { + super.draw(gl, renderer, sampleCount); + if( !((ImageSequence)texSeq).getManualStepping() ) { + markStateDirty(); // keep on going + } + }; +} diff --git a/src/graphui/classes/com/jogamp/graph/ui/shapes/Label.java b/src/graphui/classes/com/jogamp/graph/ui/shapes/Label.java new file mode 100644 index 000000000..b8edb74e2 --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/shapes/Label.java @@ -0,0 +1,223 @@ +/** + * Copyright 2010-2023 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package com.jogamp.graph.ui.shapes; + +import com.jogamp.opengl.GL2ES2; +import com.jogamp.opengl.GLProfile; +import com.jogamp.opengl.math.geom.AABBox; +import com.jogamp.graph.curve.OutlineShape; +import com.jogamp.graph.curve.opengl.RegionRenderer; +import com.jogamp.graph.curve.opengl.TextRegionUtil; +import com.jogamp.graph.font.Font; +import com.jogamp.graph.font.Font.Glyph; +import com.jogamp.graph.geom.plane.AffineTransform; +import com.jogamp.graph.ui.GraphShape; + +/** + * A GraphUI text label {@link GraphShape} + * <p> + * GraphUI is GPU based and resolution independent. + * </p> + */ +public class Label extends GraphShape { + private Font font; + private float fontScale; + private String text; + + private final AffineTransform tempT1 = new AffineTransform(); + private final AffineTransform tempT2 = new AffineTransform(); + private final AffineTransform tempT3 = new AffineTransform(); + + /** + * Label ctor using a separate {@code fontScale} to scale the em-sized type glyphs + * @param renderModes region renderModes + * @param font the font + * @param fontScale font-scale factor, by which the em-sized type glyphs shall be scaled + * @param text the text to render + */ + public Label(final int renderModes, final Font font, final float fontScale, final String text) { + super(renderModes); + this.font = font; + this.fontScale = fontScale; + this.text = text; + } + + /** + * Label ctor using em-size type glyphs + * @param renderModes region renderModes + * @param font the font + * @param text the text to render + */ + public Label(final int renderModes, final Font font, final String text) { + super(renderModes); + this.font = font; + this.fontScale = 1f; + this.text = text; + } + + /** Return the text to be rendered. */ + public String getText() { + return text; + } + + /** + * Set the text to be rendered. Shape update is pending until next {@link #draw(GL2ES2, RegionRenderer, int[])} or {@link #validate(GL2ES2)}. + * @param text the text to be set. + * @return true if text has been updated, false if unchanged. + */ + public boolean setText(final String text) { + if( !this.text.equals(text) ) { + this.text = text; + markShapeDirty(); + return true; + } else { + return false; + } + } + + /** + * Set the text to be rendered and immediately updates the shape if necessary. + * @param gl {@link GL2ES2} to issue {@link #validate(GL2ES2)} in case text changed to immediately update shape and {@link #getBounds()} + * @param text the text to be set. + * @return true if text has been updated, false if unchanged. + */ + public boolean setText(final GL2ES2 gl, final String text) { + if( setText(text) ) { + validate(gl); + return true; + } else { + return false; + } + } + + /** + * Set the text to be rendered and immediately updates the shape if necessary. + * @param glp {@link GLProfile} to issue {@link #validate(GLProfile)} in case text changed to immediately update shape and {@link #getBounds()} + * @param text the text to be set. + * @return true if text has been updated, false if unchanged. + */ + public boolean setText(final GLProfile glp, final String text) { + if( setText(text) ) { + validate(glp); + return true; + } else { + return false; + } + } + + /** + * Return the {@link Font} used to render the text + */ + public Font getFont() { + return font; + } + + /** + * Set the {@link Font} used to render the text + * @param font the font to be set. + * @return true if font has been updated, false if unchanged. + */ + public boolean setFont(final Font font) { + if( !this.font.equals(font) ) { + this.font = font; + markShapeDirty(); + return true; + } else { + return false; + } + } + + /** + * Gets the font-scale factor, by which the em-sized type glyphs shall be scaled. + */ + public float getFontScale() { + return fontScale; + } + + /** Returns {@link Font#getLineHeight()} * {@link #getFontScale()}. */ + public float getLineHeight() { + return fontScale * font.getLineHeight(); + } + + /** Returns {@link Font#getLineHeight()} * {@link #getFontScale()} * {@link #getScaleY()}. */ + public float getScaledLineHeight() { + return getScale().y() * fontScale * font.getLineHeight(); + } + + /** + * Sets the font-scale factor, by which the em-sized type glyphs shall be scaled. + * <p> + * This will lead to a recreate the shape's region in case fontScale differs. + * </p> + * <p> + * Use {@link #scale(float, float, float)} for non-expensive shape scaling. + * </p> + * @param fontScale font-scale factor, by which the em-sized type glyphs shall be scaled + * @return true if font-scale has been updated, false if unchanged. + */ + public boolean setFontScale(final float fontScale) { + if( this.fontScale != fontScale ) { + this.fontScale = fontScale; + markShapeDirty(); + return true; + } else { + return false; + } + } + + private final Font.GlyphVisitor glyphVisitor = new Font.GlyphVisitor() { + @Override + public void visit(final char symbol, final Glyph glyph, final AffineTransform t) { + if( glyph.isWhiteSpace() ) { + return; + } + final OutlineShape shape = glyph.getShape(); + shape.setSharpness(oshapeSharpness); + region.addOutlineShape(shape, t, rgbaColor); + } + }; + + @Override + protected void addShapeToRegion(final GLProfile glp, final GL2ES2 gl) { + final int[] vertIndCount = TextRegionUtil.countStringRegion(font, text, new int[2]); + resetGLRegion(glp, gl, null, vertIndCount[0], vertIndCount[1]); + + AABBox fbox = font.getGlyphBounds(text, tempT2, tempT3); + tempT1.setToScale(fontScale, fontScale); + tempT1.translate(-fbox.getMinX(), -fbox.getMinY(), tempT2); // enforce bottom-left origin @ 0/0 for good drag-zoom experience + fbox = font.processString(glyphVisitor, tempT1, text, tempT2, tempT3); + setRotationPivot( fbox.getCenter() ); + box.copy(fbox); + } + + @Override + public String getSubString() { + final int m = Math.min(text.length(), 8); + return super.getSubString()+", fscale " + fontScale + ", '" + text.substring(0, m)+"'"; + } +} diff --git a/src/graphui/classes/com/jogamp/graph/ui/shapes/MediaButton.java b/src/graphui/classes/com/jogamp/graph/ui/shapes/MediaButton.java new file mode 100644 index 000000000..927ad8f8a --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/shapes/MediaButton.java @@ -0,0 +1,150 @@ +/** + * Copyright 2014-2023 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package com.jogamp.graph.ui.shapes; + +import com.jogamp.opengl.GL2ES2; +import com.jogamp.common.av.AudioSink; +import com.jogamp.common.util.InterruptSource; +import com.jogamp.graph.curve.opengl.RegionRenderer; +import com.jogamp.graph.ui.GraphShape; +import com.jogamp.opengl.util.av.GLMediaPlayer; +import com.jogamp.opengl.util.av.GLMediaPlayer.GLMediaEventListener; +import com.jogamp.opengl.util.av.GLMediaPlayer.StreamException; +import com.jogamp.opengl.util.texture.TextureSequence.TextureFrame; + +/** + * A GraphUI {@link GLMediaPlayer} based {@link TexSeqButton} {@link GraphShape}. + * <p> + * GraphUI is GPU based and resolution independent. + * </p> + * <p> + * This button is rendered with a round oval shape. + * To render it rectangular, {@link #setCorner(float)} to zero. + * </p> + * <p> + * Default colors (toggle-on is full color): + * - non-toggle: 1 * color + * - pressed: 0.9 * color + * - toggle-off: 0.8 * color + * - toggle-on: 1.0 * color + * </p> + */ +public class MediaButton extends TexSeqButton { + private boolean verbose = false; + + /** + * @param renderModes + * @param width + * @param height + * @param mPlayer + * @param mPlayerListener + */ + public MediaButton(final int renderModes, final float width, + final float height, final GLMediaPlayer mPlayer) { + super(renderModes, width, height, mPlayer); + + setColor(1.0f, 1.0f, 1.0f, 1.0f); + setPressedColorMod(0.9f, 0.9f, 0.9f, 0.7f); + setToggleOffColorMod(0.8f, 0.8f, 0.8f, 1.0f); + setToggleOnColorMod(1.0f, 1.0f, 1.0f, 1.0f); + } + + public void setVerbose(final boolean v) { verbose = v; } + + /** + * Add the default {@link GLMediaEventListener} to {@link #getGLMediaPlayer() this class's GLMediaPlayer}. + */ + public void addDefaultEventListener() { + getGLMediaPlayer().addEventListener(defGLMediaEventListener); + } + + public final GLMediaPlayer getGLMediaPlayer() { return (GLMediaPlayer)texSeq; } + + public final AudioSink getAudioSink() { return getGLMediaPlayer().getAudioSink(); } + + private final GLMediaEventListener defGLMediaEventListener = new GLMediaEventListener() { + @Override + public void newFrameAvailable(final GLMediaPlayer ts, final TextureFrame newFrame, final long when) { + // texButton.markStateDirty(); + } + + @Override + public void attributesChanged(final GLMediaPlayer mp, final GLMediaPlayer.EventMask eventMask, final long when) { + if( verbose ) { + System.err.println("MediaButton AttributesChanges: "+eventMask+", when "+when); + System.err.println("MediaButton State: "+mp); + } + if( eventMask.isSet(GLMediaPlayer.EventMask.Bit.Init) ) { + resetGL = true; + } + if( eventMask.isSet(GLMediaPlayer.EventMask.Bit.Size) ) { + // FIXME: mPlayer.resetGLState(); + } + if( eventMask.isSet(GLMediaPlayer.EventMask.Bit.EOS) ) { + new InterruptSource.Thread() { + @Override + public void run() { + // loop for-ever .. + mp.seek(0); + mp.resume(); + } }.start(); + } else if( eventMask.isSet(GLMediaPlayer.EventMask.Bit.Error) ) { + final StreamException se = mp.getStreamException(); + if( null != se ) { + se.printStackTrace(); + } + } + } }; + + + @Override + protected void destroyImpl(final GL2ES2 gl, final RegionRenderer renderer) { + ((GLMediaPlayer)texSeq).destroy(gl); + } + + volatile boolean resetGL = true; + + @Override + public void draw(final GL2ES2 gl, final RegionRenderer renderer, final int[] sampleCount) { + final GLMediaPlayer mPlayer = (GLMediaPlayer)texSeq; + if( resetGL ) { + resetGL = false; + try { + mPlayer.initGL(gl); + if( null != region ) { + region.markShapeDirty(); // reset texture data + } + } catch (final Exception e) { + e.printStackTrace(); + } + } + super.draw(gl, renderer, sampleCount); + markStateDirty(); // keep on going + }; + +} diff --git a/src/graphui/classes/com/jogamp/graph/ui/shapes/Rectangle.java b/src/graphui/classes/com/jogamp/graph/ui/shapes/Rectangle.java new file mode 100644 index 000000000..2b9698e3a --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/shapes/Rectangle.java @@ -0,0 +1,132 @@ +/** + * Copyright 2010-2023 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package com.jogamp.graph.ui.shapes; + +import com.jogamp.graph.curve.OutlineShape; +import com.jogamp.graph.ui.GraphShape; +import com.jogamp.opengl.GL2ES2; +import com.jogamp.opengl.GLProfile; +import com.jogamp.opengl.math.geom.AABBox; + +/** + * A GraphUI rectangle {@link GraphShape} + * <p> + * GraphUI is GPU based and resolution independent. + * </p> + */ +public class Rectangle extends GraphShape { + private float minX, minY, zPos; + private float width; + private float height; + private float lineWidth; + + public Rectangle(final int renderModes, final float minX, final float minY, final float width, final float height, final float lineWidth, final float zPos) { + super(renderModes); + this.minX = minX; + this.minY = minY; + this.zPos = zPos; + this.width = width; + this.height = height; + this.lineWidth = lineWidth; + } + + public Rectangle(final int renderModes, final AABBox abox, final float lineWidth) { + this( renderModes, abox.getMinX(), abox.getMinY(), abox.getWidth(), abox.getHeight(), lineWidth, abox.getCenter().z()); + } + + public Rectangle(final int renderModes, final float minX, final float minY, final float width, final float height, final float lineWidth) { + this( renderModes, minX, minY, width, height, lineWidth, 0); + } + public Rectangle(final int renderModes, final float width, final float height, final float lineWidth) { + this( renderModes, 0, 0, width, height, lineWidth, 0); + } + + public final float getWidth() { return width; } + public final float getHeight() { return height; } + public final float getLineWidth() { return lineWidth; } + + public void setPosition(final float minX, final float minY, final float zPos) { + this.minX = minX; + this.minY = minY; + this.zPos = zPos; + markShapeDirty(); + } + public void setDimension(final float width, final float height, final float lineWidth) { + this.width = width; + this.height = height; + this.lineWidth = lineWidth; + markShapeDirty(); + } + public void setBounds(final AABBox abox, final float lineWidth) { + setPosition(abox.getMinX(), abox.getMinY(), abox.getCenter().z()); + setDimension(abox.getWidth(), abox.getHeight(), lineWidth); + } + + @Override + protected void addShapeToRegion(final GLProfile glp, final GL2ES2 gl) { + final OutlineShape shape = new OutlineShape(); + final float x1 = minX; + final float y1 = minY; + final float x2 = minX + getWidth(); + final float y2 = minY + getHeight(); + final float z = zPos; + { + // Outer OutlineShape as Winding.CCW. + shape.moveTo(x1, y1, z); + shape.lineTo(x2, y1, z); + shape.lineTo(x2, y2, z); + shape.lineTo(x1, y2, z); + shape.lineTo(x1, y1, z); + shape.closeLastOutline(true); + shape.addEmptyOutline(); + } + { + // Inner OutlineShape as Winding.CW. + // final float dxy0 = getWidth() < getHeight() ? getWidth() : getHeight(); + final float dxy = lineWidth; // dxy0 * getDebugBox(); + shape.moveTo(x1+dxy, y1+dxy, z); + shape.lineTo(x1+dxy, y2-dxy, z); + shape.lineTo(x2-dxy, y2-dxy, z); + shape.lineTo(x2-dxy, y1+dxy, z); + shape.lineTo(x1+dxy, y1+dxy, z); + shape.closeLastOutline(true); + } + shape.setIsQuadraticNurbs(); + shape.setSharpness(oshapeSharpness); + + resetGLRegion(glp, gl, null, shape); + region.addOutlineShape(shape, null, rgbaColor); + box.resize(shape.getBounds()); + setRotationPivot( box.getCenter() ); + } + + @Override + public String getSubString() { + return super.getSubString()+", dim "+getWidth() + " x " + getHeight(); + } +} diff --git a/src/graphui/classes/com/jogamp/graph/ui/shapes/TexSeqButton.java b/src/graphui/classes/com/jogamp/graph/ui/shapes/TexSeqButton.java new file mode 100644 index 000000000..0dbd11adf --- /dev/null +++ b/src/graphui/classes/com/jogamp/graph/ui/shapes/TexSeqButton.java @@ -0,0 +1,66 @@ +/** + * Copyright 2014-2023 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package com.jogamp.graph.ui.shapes; + +import com.jogamp.opengl.GL2ES2; +import com.jogamp.opengl.GLProfile; +import com.jogamp.graph.curve.OutlineShape; +import com.jogamp.graph.curve.Region; +import com.jogamp.graph.ui.GraphShape; +import com.jogamp.opengl.util.texture.TextureSequence; + +/** + * An abstract GraphUI {@link TextureSequence} {@link BaseButton} {@link GraphShape}. + * <p> + * GraphUI is GPU based and resolution independent. + * </p> + * <p> + * This button is rendered with a round oval shape. + * To render it rectangular, {@link #setCorner(float)} to zero. + * </p> + */ +public abstract class TexSeqButton extends BaseButton { + protected final TextureSequence texSeq; + + public TexSeqButton(final int renderModes, final float width, + final float height, final TextureSequence texSeq) { + super(renderModes | Region.COLORTEXTURE_RENDERING_BIT, width, height); + this.texSeq = texSeq; + } + + public final TextureSequence getTextureSequence() { return this.texSeq; } + + @Override + protected void addShapeToRegion(final GLProfile glp, final GL2ES2 gl) { + final OutlineShape shape = createBaseShape(0f); + resetGLRegion(glp, gl, texSeq, shape); + region.addOutlineShape(shape, null, rgbaColor); + box.resize(shape.getBounds()); + setRotationPivot( box.getCenter() ); + } +} |