//(c) Copyright 2005, Scott Vorthmann. All rights reserved. package org.vorthmann.zome.app.impl; import java.awt.BasicStroke; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.event.ActionEvent; import java.awt.event.MouseEvent; import java.awt.geom.GeneralPath; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.vorthmann.j3d.MouseTool; import org.vorthmann.j3d.MouseToolDefault; import org.vorthmann.ui.Controller; import org.vorthmann.ui.DefaultController; import org.vorthmann.ui.LeftMouseDragAdapter; import com.vzome.core.algebra.AlgebraicVector; import com.vzome.core.math.RealVector; import com.vzome.core.math.symmetry.Axis; import com.vzome.core.math.symmetry.Direction; import com.vzome.core.math.symmetry.DodecagonalSymmetry; import com.vzome.core.math.symmetry.OctahedralSymmetry; import com.vzome.core.math.symmetry.OrbitSet; import com.vzome.core.math.symmetry.Symmetry; import com.vzome.core.render.Color; import com.vzome.core.render.RenderedModel.OrbitSource; public class OrbitSetController extends DefaultController implements PropertyChangeListener { private final OrbitSource colorSource; private final OrbitSet orbits, allOrbits; private Direction lastOrbit = null; private boolean mOneAtATime = true, showLastOrbit = false; double xMax = 0d, yMax = 0d; private final Map<Direction, OrbitState> orbitDots = new HashMap<>(); private final MouseTool mouseTool = new LeftMouseDragAdapter( new MouseToolDefault() { @Override public void mouseClicked( MouseEvent click ) { Direction pickedDir = pickDirection( click ); if ( pickedDir != null ) { toggleOrbit( pickedDir ); properties() .firePropertyChange( "orbits", true, false ); } } }, /* half-second forgiveness */ 500 ); private static class OrbitState { double dotX, dotY; int dotXint, dotYint; } public OrbitSetController( OrbitSet orbits, OrbitSet allOrbits, OrbitSource colorSource, boolean showLastOrbit ) { this.orbits = orbits; this.allOrbits = allOrbits; this.colorSource = colorSource; this.showLastOrbit = showLastOrbit; this.mOneAtATime = orbits .size() == 1; recalculateDots(); } private synchronized void recalculateDots() { orbits .retainAll( allOrbits ); Symmetry symmetry = allOrbits .getSymmetry(); RealVector test = new RealVector( 0.1d, 0.1d, 1d ); if ( symmetry instanceof OctahedralSymmetry ) test = new RealVector( 2d, 1d, 4d ); else if ( symmetry instanceof DodecagonalSymmetry ) test = new RealVector( 10d, 1d, 1d ); orbitDots .clear(); // lastOrbit = null; // cannot do this, we might have a valid value, for example after loading from XML boolean lastOrbitChanged = false; for ( Direction dir : allOrbits ) { if ( lastOrbit == null ) { // just a way to initialize the lastOrbit lastOrbit = dir; lastOrbitChanged = true; } OrbitState orbit = new OrbitState(); orbitDots .put( dir, orbit ); orbit .dotX = dir .getDotX(); if ( orbit .dotX >= -90d ) { // This orbit supports pre-computed dot locations orbit .dotY = dir .getDotY(); } else { // The old way Axis axis = symmetry .getAxis( test, Collections .singleton( dir ) ); AlgebraicVector v = axis .normal(); double z = v .getComponent( 2 ) .evaluate(); orbit.dotX = v .getComponent( 0 ) .evaluate(); orbit.dotX = orbit.dotX / z; // intersect with z=0 plane orbit.dotY = v .getComponent( 1 ) .evaluate(); orbit.dotY = orbit.dotY / z; // intersect with z=0 plane } // if ( symmetry instanceof IcosahedralSymmetry ) { // switch X and Y (why? don't know, it just works) double temp = orbit.dotX; orbit.dotX = orbit.dotY; orbit.dotY = temp; } if ( orbit.dotY > yMax ) yMax = orbit.dotY; if ( orbit.dotX > xMax ) xMax = orbit.dotX; } if ( ( lastOrbit == null ) || (! allOrbits .contains( lastOrbit ) ) ) { lastOrbitChanged = true; if ( ! orbits .isEmpty() ) lastOrbit = orbits .last(); else if ( ! orbitDots .isEmpty() ) lastOrbit = orbitDots .keySet() .iterator() .next(); else lastOrbit = null; } if ( lastOrbitChanged ) properties() .firePropertyChange( "selectedOrbit", null, lastOrbit == null? null : lastOrbit .getName() ); } @Override public void doAction( String action, ActionEvent e ) throws Exception { if ( action .equals( "refreshDots" ) ) { recalculateDots(); return; } if ( action .equals( "toggleHalf" ) || action .equals( "reset" ) || action .equals( "short" ) || action .equals( "medium" ) || action .equals( "long" ) || action .startsWith( "adjustScale." ) || action .equals( "scaleUp" ) || action .equals( "scaleDown" ) ) { getSubController( "currentLength" ) .doAction( action, e ); return; } if ( action .equals( "setNoDirections" ) ) { orbits .clear(); } else if ( action .equals( "setAllDirections" ) ) { mOneAtATime = false; orbits .addAll( allOrbits ); } else if ( action .equals( "rZomeOrbits" ) ) { mOneAtATime = false; orbits .clear(); for (Direction dir : allOrbits) { if ( dir .isStandard() ) { orbits .add( dir ); } } } else if ( action .equals( "predefinedOrbits" ) ) { mOneAtATime = false; orbits .clear(); for (Direction dir : allOrbits) { if ( ! dir .isAutomatic() ) { orbits .add( dir ); } } } else if ( action .equals( "oneAtATime" ) ) { mOneAtATime = !mOneAtATime; if ( ! mOneAtATime ) return; // no action when releasing the constraint // else, pick one orbits .clear(); if ( lastOrbit != null ) orbits .add( lastOrbit ); } else if ( action .startsWith( "enableDirection." ) ) { String dirName = action .substring( "enableDirection." .length() ); Direction dir = allOrbits .getDirection( dirName ); // TODO: figure out why dir can be null here if ( dir != null && ! orbits .contains( dir ) ) toggleOrbit( dir ); } else if ( action .startsWith( "toggleDirection." ) ) { String dirName = action .substring( "toggleDirection." .length() ); Direction dir = allOrbits .getDirection( dirName ); toggleOrbit( dir ); } else if ( action .startsWith( "setSingleDirection." ) ) { mOneAtATime = true; String dirName = action .substring( "setSingleDirection." .length() ); Direction dir = allOrbits .getDirection( dirName ); toggleOrbit( dir ); } properties() .firePropertyChange( "orbits", true, false ); } @Override public void propertyChange( PropertyChangeEvent evt ) { if ( "length" .equals( evt .getPropertyName() ) && evt .getSource() == getSubController( "currentLength" ) ) properties() .firePropertyChange( evt ); // forward to the NewLengthPanel } void toggleOrbit( Direction dir ) { if ( mOneAtATime ) orbits .clear(); if ( orbits .add( dir ) ) { lastOrbit = dir; properties() .firePropertyChange( "selectedOrbit", null, dir .getName() ); } else if ( orbits .remove( dir ) ) { // leave lastOrbit alone, it can stay "circled", so we always have a length panel... just like "setNoDirections" // if ( lastOrbit == dir ) // { // lastOrbit = null; // if ( ! orbits .isEmpty() ) // { // lastOrbit = (Direction) orbits .last(); // } // properties() .firePropertyChange( "selectedOrbit", null, lastOrbit == null ? null : lastOrbit .getName() ); // } } else throw new IllegalStateException( "could not toggle direction " + dir .getName() ); } @Override public Controller getSubController( String name ) { if ( "currentLength" .equals( name ) ) return super .getSubController( "length." + getProperty( "selectedOrbit" ) ); return super .getSubController( name ); } @Override public String getProperty( String string ) { if ( "oneAtATime" .equals( string ) ) return Boolean .toString( mOneAtATime ); if ( "selectedOrbit" .equals( string ) ) if ( lastOrbit != null ) return lastOrbit .getName(); else return null; if ( "halfSizes" .equals( string ) ) if ( lastOrbit != null && lastOrbit .hasHalfSizes() ) return "true"; else return "false"; if ( "scaleName.superShort" .equals( string ) ) return ( lastOrbit == null )? null : lastOrbit .getScaleName( 0 ); if ( "scaleName.short" .equals( string ) ) return ( lastOrbit == null )? null : lastOrbit .getScaleName( 1 ); if ( "scaleName.medium" .equals( string ) ) return ( lastOrbit == null )? null : lastOrbit .getScaleName( 2 ); if ( "scaleName.long" .equals( string ) ) return ( lastOrbit == null )? null : lastOrbit .getScaleName( 3 ); if ( "color" .equals( string ) ) { Color color = colorSource .getColor( lastOrbit ); if ( color == null ) return null; int rgb = color .getRGB(); return "0x" + Integer .toHexString( rgb ); } if ( "half" .equals( string ) | "unitText" .equals( string ) | "multiplierText" .equals( string ) ) return getSubController( "currentLength" ) .getProperty( string ); return super .getProperty( string ); } @Override public void setProperty( String cmd, Object value ) { if ( "oneAtATime" .equals( cmd ) ) mOneAtATime = "true" .equals( value ); else if ( "multiplier" .equals( cmd ) | "half" .equals( cmd ) ) getSubController( "currentLength" ) .setProperty( cmd, value ); else super .setProperty( cmd, value ); } private static final int RADIUS = 12; private static final int INNER_RADIUS = 5; private static final int OUTER_RADIUS = 19; private static final int DIAM = 2 * RADIUS; private static int TOP = 30; private static int LEFT = TOP; @Override public void repaintGraphics( String panelName, Graphics graphics, Dimension size ) { if ( panelName .startsWith( "oneOrbit." ) ) { Direction dir = allOrbits .getDirection( panelName .substring( "oneOrbit." .length() ) ); Graphics2D g2d = (Graphics2D) graphics; g2d .clearRect( 0, 0, (int) size .getWidth(), (int) size .getHeight() ); Color color = colorSource .getColor( dir ); g2d .setPaint( color == null? java.awt.Color.WHITE : new java.awt.Color( color .getRGB() ) ); g2d .fill( g2d .getClipBounds() ); } else if ( "selectedOrbit" .equals( panelName ) ) { Graphics2D g2d = (Graphics2D) graphics; g2d .clearRect( 0, 0, (int) size .getWidth(), (int) size .getHeight() ); Color color = colorSource .getColor( lastOrbit ); g2d .setPaint( color == null? java.awt.Color.WHITE : new java.awt.Color( color .getRGB() ) ); g2d .fill( g2d .getClipBounds() ); } else if ( "orbits" .equals( panelName ) ) { int fullwidth = (int) size .getWidth(); int fullheight = (int) size .getHeight(); Graphics2D g2d = (Graphics2D) graphics; g2d .clearRect( 0, 0, fullwidth, fullheight ); g2d .setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON ); if ( fullheight > 180 ) fullheight = 180; int width = fullwidth - 2*TOP; double scaleY = width / yMax; // FLIP note: was xMax int height = fullheight - 2*LEFT; double scaleX = height / xMax; int right = LEFT + width; int bottom = TOP + height ; int corner = LEFT; Symmetry symm = allOrbits .getSymmetry(); if ( symm instanceof OctahedralSymmetry || symm instanceof DodecagonalSymmetry ) corner = right; // g2d .setPaint( java.awt.Color.black ); // g2d .fill( g2d .getClipBounds() ); g2d .setStroke( new BasicStroke( 1.5f , BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND ) ); g2d .setPaint( java.awt.Color.black ); GeneralPath path = new GeneralPath(); path .moveTo( corner, TOP ); path .lineTo( LEFT, bottom ); path .lineTo( right, bottom ); path .lineTo( corner, TOP ); path .closePath(); g2d .draw( path ); for (Direction dir : orbitDots .keySet()) { OrbitState orbit = orbitDots .get( dir ); Color color = colorSource .getColor( dir ); int x = LEFT + (int) Math .round( orbit.dotY * scaleY ); // if ( allOrbits .getSymmetry() == OctahedralSymmetry .GOLDEN_INSTANCE ) { // x = right - x + LEFT; // } // now store the int coords for later picking orbit.dotXint = x; orbit.dotYint = bottom - (int) Math .round( orbit.dotX * scaleX ); g2d .setPaint( color == null? java.awt.Color.WHITE : new java.awt.Color( color .getRGB() ) ); g2d .fillOval( orbit.dotXint-RADIUS, orbit.dotYint-RADIUS, DIAM, DIAM ); g2d .setPaint( java.awt.Color.black ); g2d .drawOval( orbit.dotXint-RADIUS, orbit.dotYint-RADIUS, DIAM, DIAM ); if ( orbits .contains( dir ) ) { g2d .setPaint( java.awt.Color.black ); g2d .fillOval( orbit.dotXint-INNER_RADIUS, orbit.dotYint-INNER_RADIUS, INNER_RADIUS*2, INNER_RADIUS*2 ); } if ( showLastOrbit && lastOrbit == dir ) { g2d .setPaint( java.awt.Color.black ); g2d .drawOval( orbit.dotXint-OUTER_RADIUS, orbit.dotYint-OUTER_RADIUS, OUTER_RADIUS*2, OUTER_RADIUS*2 ); } } g2d.dispose(); //clean up } } Direction pickDirection( MouseEvent click ) { double minDist = 999d; Direction pickedDir = null; for (Direction dir : orbitDots .keySet()) { OrbitState orbit = orbitDots .get( dir ); double dist = Math.sqrt( Math.pow( click.getX()-orbit.dotXint, 2 ) + Math.pow( click.getY()-orbit.dotYint, 2 ) ); if ( dist < (double) RADIUS*4 ) { if ( dist < minDist ) { minDist = dist; pickedDir = dir; } } } return pickedDir; } @Override public MouseTool getMouseTool() { return this .mouseTool; } @Override public boolean[] enableContextualCommands( String[] menu, MouseEvent e ) { boolean[] result = new boolean[menu.length]; for ( int i = 0; i < menu.length; i++ ) { String menuItem = menu[i]; switch ( menuItem ) { case "rZomeOrbits": case "predefinedOrbits": case "setAllDirections": case "usedOrbits": case "configureDirections": result[i] = true; break; default: result[i] = false; } } return result; } @Override public String[] getCommandList( String listName ) { if ( listName .equals( "orbits" ) ) { String[] result = new String[ orbits .size() ]; int i = 0; for ( Direction dir : orbits ) { result[ i ] = dir .getName(); i++; } return result; } if ( listName .equals( "allOrbits" ) ) { String[] result = new String[ allOrbits .size() ]; int i = 0; for ( Direction dir : allOrbits ) { result[ i ] = dir .getName(); i++; } return result; } return new String[0]; } }