summaryrefslogtreecommitdiffstats
path: root/src/graphui/classes/com
diff options
context:
space:
mode:
Diffstat (limited to 'src/graphui/classes/com')
-rw-r--r--src/graphui/classes/com/jogamp/graph/ui/Container.java116
-rw-r--r--src/graphui/classes/com/jogamp/graph/ui/GraphShape.java271
-rw-r--r--src/graphui/classes/com/jogamp/graph/ui/Group.java342
-rw-r--r--src/graphui/classes/com/jogamp/graph/ui/Scene.java1171
-rw-r--r--src/graphui/classes/com/jogamp/graph/ui/Shape.java1398
-rw-r--r--src/graphui/classes/com/jogamp/graph/ui/layout/Alignment.java120
-rw-r--r--src/graphui/classes/com/jogamp/graph/ui/layout/BoxLayout.java137
-rw-r--r--src/graphui/classes/com/jogamp/graph/ui/layout/Gap.java81
-rw-r--r--src/graphui/classes/com/jogamp/graph/ui/layout/GridLayout.java324
-rw-r--r--src/graphui/classes/com/jogamp/graph/ui/layout/Margin.java197
-rw-r--r--src/graphui/classes/com/jogamp/graph/ui/layout/Padding.java106
-rw-r--r--src/graphui/classes/com/jogamp/graph/ui/package.html33
-rw-r--r--src/graphui/classes/com/jogamp/graph/ui/shapes/BaseButton.java171
-rw-r--r--src/graphui/classes/com/jogamp/graph/ui/shapes/Button.java212
-rw-r--r--src/graphui/classes/com/jogamp/graph/ui/shapes/CrossHair.java103
-rw-r--r--src/graphui/classes/com/jogamp/graph/ui/shapes/GLButton.java177
-rw-r--r--src/graphui/classes/com/jogamp/graph/ui/shapes/GlyphShape.java206
-rw-r--r--src/graphui/classes/com/jogamp/graph/ui/shapes/ImageButton.java76
-rw-r--r--src/graphui/classes/com/jogamp/graph/ui/shapes/Label.java223
-rw-r--r--src/graphui/classes/com/jogamp/graph/ui/shapes/MediaButton.java150
-rw-r--r--src/graphui/classes/com/jogamp/graph/ui/shapes/Rectangle.java132
-rw-r--r--src/graphui/classes/com/jogamp/graph/ui/shapes/TexSeqButton.java66
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() );
+ }
+}