//(c) Copyright 2007, Scott Vorthmann. All rights reserved. package org.vorthmann.zome.app.impl; import java.awt.event.ActionEvent; import java.awt.event.MouseWheelEvent; import org.vorthmann.j3d.MouseTool; import org.vorthmann.j3d.MouseToolDefault; import org.vorthmann.ui.Controller; import org.vorthmann.ui.DefaultController; import org.w3c.dom.Element; import com.vzome.core.algebra.AlgebraicField; import com.vzome.core.algebra.AlgebraicNumber; import com.vzome.core.math.DomUtils; import com.vzome.core.math.symmetry.Direction; /** * Because of MOUSE_WHEEL_GAIN issues, this is more a model of the length panel than an actual length scalar value. * */ public class LengthController extends DefaultController { /** * This is a permanent adjustment of the scale slider. When the scale reads 0 for the user, * the actual scale used internally will be SCALE_OFFSET. */ public static final int SCALE_OFFSET = Direction .USER_SCALE; /** * A model for a scale slider. Value range centers on scale 0. * * Actual scale * * @author Scott Vorthmann * */ private class ScaleController extends DefaultController { private static final int MAX_SCALE = 6, MIN_SCALE = -6; @Override public void doAction( String action, ActionEvent e ) throws Exception { if ( "scaleUp" .equals( action ) ) setScale( this .scale + 1 ); else if ( "scaleDown" .equals( action ) ) setScale( this .scale - 1 ); // else if ( "factor" .equals( action ) ) // ; // TODO factor as many of these out of the lengthModel as you can // else if ( "zero" .equals( action ) ) // ; // TODO multiply this value into the lengthModel, and zero this value else super.doAction( action, e ); } private int scale = 0; private final MouseTool tool; public ScaleController() { this .tool = new MouseToolDefault() { int wheelClicks = 0; /** * Simply dividing the roll amt MOUSE_WHEEL_GAIN would be insufficient, because then * wheeling slowing and precisely might never get above 0. I though perhaps a minimum scale change of +/-1 * on any roll might accomplish the right thing, but then it is not possible to wheel * slowly enough. * By keeping an internal state (wheelClicks), and applying MOUSE_WHEEL_GAIN, * we can generate courser grained scale changes without those unnatural effects. */ @Override public void mouseWheelMoved( MouseWheelEvent e ) { int amt = e .getWheelRotation(); int oldScaled = wheelClicks / MOUSE_WHEEL_GAIN; wheelClicks = wheelClicks + amt; int newScaled = wheelClicks / MOUSE_WHEEL_GAIN; if ( oldScaled != newScaled ) // don't want to generate change events when there is no change setScale( scale - newScaled + oldScaled ); // reverse the sense of the wheel, // since mouseWheel clicks are set up for scrollbars } }; } @Override public void setProperty( String property, Object value ) { if ( "scale" .equals( property ) ) { setScale( Integer .parseInt( (String) value ) ); return; } else super.setProperty( property, value ); } @Override public String getProperty( String string ) { if ( "scale" .equals( string ) ) return Integer .toString( scale ); if ( "scaleHtml" .equals( string ) ) { if ( scale == 0 ) return " \u2070"; // space to pad, attempting to prevent width recalc. when sign appears int absScale = Math .abs( scale ); String result = ( absScale == scale )? " " : "\u207B"; // prepend sign exponent or space to fill switch ( absScale ) { case 1: result += "\u00B9"; break; case 2: result += "\u00B2"; break; case 3: result += "\u00B3"; break; case 4: result += "\u2074"; break; case 5: result += "\u2075"; break; case 6: result += "\u2076"; break; case 7: result += "\u2077"; break; case 8: result += "\u2078"; break; case 9: result += "\u2079"; break; default: result += "\u207F"; break; } return result; } return super.getProperty( string ); } public void setScale( int amt ) { int oldScale = scale; scale = amt; // keep the scale between MIN and MAX, inclusive if ( scale > MAX_SCALE ) scale = MAX_SCALE; else if ( scale < MIN_SCALE ) scale = MIN_SCALE; if ( oldScale != scale ) // don't want to generate change events when there is no change LengthController .this .fireLengthChange(); } int getScale() { return scale; } @Override public MouseTool getMouseTool() { return tool; } } private ScaleController currentScale; private NumberController unitController; private static final int MOUSE_WHEEL_GAIN = 4; /** * This is the internal factor applied, determined by the orbit, and fixed. */ private final AlgebraicNumber fixedFactor; /** * This is the user's basis for scale... when the slider is centered on "unit", this is the length value. */ private AlgebraicNumber unitFactor; private final AlgebraicNumber standardUnitFactor; private boolean half = false; private final AlgebraicField field; public LengthController( Direction orbit ) { this( orbit .getSymmetry() .getField(), orbit .getUnitLength() ); } public LengthController( AlgebraicField field ) { this( field, field .one() ); } public LengthController( AlgebraicField field, AlgebraicNumber factor ) { this .field = field; this .standardUnitFactor = field .createPower( 0 ); this .unitFactor = standardUnitFactor; this .fixedFactor = factor; this .currentScale = new ScaleController(); this .currentScale .setNextController( this ); this .unitController = new NumberController( field ); this .unitController .setNextController( this ); } @Override public Controller getSubController( String name ) { switch ( name ) { case "unit": return this .unitController; case "scale": return this .currentScale; default: return super.getSubController( name ); } } public void fireLengthChange() { properties() .firePropertyChange( "length", true, false ); } public void getXml( Element lengthElem ) { // backward-compatible, for now DomUtils .addAttribute( lengthElem, "scale", Integer.toString( currentScale .getScale() + SCALE_OFFSET ) ); DomUtils .addAttribute( lengthElem, "taus", "0" ); DomUtils .addAttribute( lengthElem, "ones", "1" ); DomUtils .addAttribute( lengthElem, "divisor", half? "2" : "1" ); } public void setXml( Element length ) { String attrValue = length .getAttribute( "scale" ); // handling nulls since I used a non-published version to migrate internal models, // and it did not serialize any attributes at all int scale = ( attrValue != null && ! attrValue .isEmpty() )? Integer .parseInt( attrValue ) : 4; this .currentScale .setScale( scale - SCALE_OFFSET ); // vZome files record the actual scale, not user scale // TODO handle other two attribute values! attrValue = length .getAttribute( "divisor" ); half = ( attrValue == null || attrValue .isEmpty() )? false : "2" .equals( attrValue ); } @Override public void doAction( String action, ActionEvent e ) throws Exception { switch ( action ) { case "setCustomUnit": // push the value to the NumberController this .unitController .setValue( this .unitFactor ); return; case "getCustomUnit": // get the value from the NumberController this .unitFactor = this .unitController .getValue(); // now reset everything according to that unitFactor currentScale .setScale( 0 ); fireLengthChange(); return; default: break; } if ( "toggleHalf" .equals( action ) ) { this .half = ! this .half; fireLengthChange(); } else if ( "reset" .equals( action ) || "short" .equals( action ) ) { this .unitFactor = standardUnitFactor; currentScale .setScale( 0 ); } else if ( "supershort" .equals( action ) ) { this .unitFactor = standardUnitFactor; currentScale .setScale( -1 ); } else if ( "medium" .equals( action ) ) { this .unitFactor = standardUnitFactor; currentScale .setScale( 1 ); } else if ( "long" .equals( action ) ) { this .unitFactor = standardUnitFactor; currentScale .setScale( 2 ); } else if ( "scaleUp" .equals( action ) ) currentScale .doAction( action, e ); else if ( "scaleDown" .equals( action ) ) currentScale .doAction( action, e ); else if ( "newZeroScale" .equals( action ) ) { int realScale = currentScale .getScale(); unitFactor = unitFactor .times( field .createPower( realScale ) ); currentScale .setScale( 0 ); } // else if ( action .startsWith( "adjustScale." ) ) // { // int amt = Integer .parseInt( action .substring( "adjustScale." .length() ) ); // this .scale -= amt; // } else super.doAction( action, e ); } @Override public void setProperty( String property, Object value ) { if ( "half" .equals( property ) ) { boolean oldHalf = this .half; this .half = Boolean .parseBoolean( (String) value ); if ( this .half != oldHalf ) fireLengthChange(); } else if ( "scale" .equals( property ) ) { currentScale .setProperty( property, value ); return; } else super.setProperty( property, value ); } @Override public String getProperty( String string ) { if ( "half" .equals( string ) ) return Boolean .toString( this .half ); if ( "scale" .equals( string ) ) return currentScale .getProperty( string ); if ( "unitText" .equals( string ) ) return readable( unitFactor ); if ( "unitIsCustom" .equals( string ) ) return Boolean .toString( ! unitFactor .equals( standardUnitFactor ) ); if ( "lengthText" .equals( string ) ) { int realScale = currentScale .getScale(); AlgebraicNumber result = this .unitFactor; result = result .times( field .createPower( realScale ) ); return readable( result ); } if ( "scaleFactorHtml" .equals( string ) ) { return field .getIrrational( 0 ) + "<font size=+1>" + currentScale .getProperty( "scaleHtml" ) + "</font>"; } return super.getProperty( string ); } private String readable( AlgebraicNumber unitFactor2 ) { StringBuffer buf = new StringBuffer(); unitFactor2 .getNumberExpression( buf, AlgebraicField.DEFAULT_FORMAT ); return buf .toString(); } /** * Get the actual length value to use when rendering. This value will multiply the zone's normal vector, * whatever its length is (not necessarily a unit vector). */ public AlgebraicNumber getValue() { // TODO push part of this into AlgebraicField somehow? AlgebraicNumber result = this .unitFactor .times( this .fixedFactor ); if ( half ) result = result .times( field .createRational( 1, 2 ) ); // TODO support more than one scaling, like rho and sigma for heptagons int realScale = currentScale .getScale() + SCALE_OFFSET; result = result .times( field .createPower( realScale ) ); return result; } @Override public MouseTool getMouseTool() { return currentScale .getMouseTool(); } /** * This is basically an inverse of getValue(), but with scale fixed at zero, * thus forcing unitFactor to float. * * @param length */ public void setActualLength( AlgebraicNumber length ) { half = false; currentScale .setScale( 0 ); length = length .times( field .createPower( -SCALE_OFFSET ) ); unitFactor = length .dividedBy( fixedFactor ); fireLengthChange(); } }