####################################################################################################
#
# Invoking X3D model self-test:
#
#   $ python SliderFloatPrototype.py
#
# Python package x3d.py package is available on PyPI for import.
#   This approach simplifies Python X3D deployment and use.
#   https://pypi.org/project/x3d
#
# Installation:
#       pip install x3d
# or
#       python -m pip install x3d
#
# Developer options for loading x3d package in other Python programs:
#
#    from x3d import *  # preferred approach, terser source that avoids x3d.* class prefixes
#
# or
#    import x3d         # traditional way to subclass x3d package, all classes require x3d.* prefix,
#                       # but python source is very verbose, for example x3d.Material x3d.Shape etc.
#                       # X3dToPython.xslt stylesheet insertPackagePrefix=true supports this option.
#
# Project home page:    # X3D Python Scene Access Interface Library (X3DPSAIL)
#                       # https://www.web3d.org/x3d/stylesheets/python/python.html
# Conversion generator: # https://www.web3d.org/x3d/stylesheets/X3dToPython.xslt
#
####################################################################################################

from x3d import *

newModel=X3D(profile='Immersive',version='3.0',
  head=head(
    children=[
    meta(content='SliderFloatPrototype.x3d',name='title'),
    meta(content='A Slider prototype enabling mouse input where float output values are needed. Size, min/max values and color are defined by the author.',name='description'),
    meta(content='Mike Hunsberger, Jane Wu',name='creator'),
    meta(content='17 October 2001',name='created'),
    meta(content='15 October 2023',name='modified'),
    meta(content='animation slider',name='subject'),
    meta(content='https://www.web3d.org/x3d/content/examples/Savage/Tools/Animation/SliderFloatPrototype.x3d',name='identifier'),
    meta(content='X3D-Edit 4.0, https://www.web3d.org/x3d/tools/X3D-Edit',name='generator'),
    meta(content='../../license.html',name='license')]),
  Scene=Scene(
    children=[
    WorldInfo(title='SliderFloatPrototype.x3d'),
    ProtoDeclare(appinfo='Slider user-interface widget that produces floating-point output values',name='SliderFloat',
      ProtoInterface=ProtoInterface(
        field=[
        field(accessType='initializeOnly',appinfo='Allowed values: vertical, horizontal',name='layoutDirection',type='SFString',value='vertical'),
        field(accessType='initializeOnly',appinfo='default value 1.0',name='height',type='SFFloat',value=1.0),
        field(accessType='initializeOnly',appinfo='default value 0.1',name='radius',type='SFFloat',value=0.1),
        field(accessType='initializeOnly',appinfo='default value 0.02',name='barRadius',type='SFFloat',value=0.02),
        field(accessType='initializeOnly',appinfo='default value .8 .4 .8',name='sliderBarColor',type='SFColor',value=(.8,.4,.8)),
        field(accessType='initializeOnly',appinfo='default value .3 .4 .8',name='sliderBallColor',type='SFColor',value=(.3,.4,.8)),
        field(accessType='initializeOnly',appinfo='default value .2 .3 .9',name='sliderEndColor',type='SFColor',value=(.2,.3,.9)),
        field(accessType='initializeOnly',appinfo='default value 0.0',name='min',type='SFFloat',value=0.0),
        field(accessType='initializeOnly',appinfo='default value 10.0',name='max',type='SFFloat',value=10.0),
        field(accessType='initializeOnly',appinfo='default value 0.0',name='value',type='SFFloat',value=0.0),
        field(accessType='inputOnly',appinfo='set minimum value for slider bar',name='setMin',type='SFFloat'),
        field(accessType='inputOnly',appinfo='set maximum value for slider bar',name='setMax',type='SFFloat'),
        field(accessType='inputOnly',appinfo='set value for slider bar',name='setValue',type='SFFloat'),
        field(accessType='outputOnly',appinfo='output value for slider bar',name='valueChanged',type='SFFloat'),
        field(accessType='initializeOnly',appinfo='enable/disable console output for troubleshooting',name='traceEnabled',type='SFBool',value=False)]),
      ProtoBody=ProtoBody(
        children=[
        Group(
          children=[
          Transform(DEF='LayoutDirectionTransform',
            children=[
            Transform(DEF='SliderBarTransform',
              children=[
              Shape(
                appearance=Appearance(
                  material=Material(DEF='SliderBarMaterial',
                    IS=IS(
                      connect=[
                      connect(nodeField='diffuseColor',protoField='sliderBarColor'),
                      connect(nodeField='emissiveColor',protoField='sliderBarColor')]))),
                geometry=Cylinder(DEF='SliderBar',
                  IS=IS(
                    connect=[
                    connect(nodeField='height',protoField='height'),
                    connect(nodeField='radius',protoField='barRadius')])))]),
            Transform(DEF='SliderBallTransform',
              children=[
              PlaneSensor(DEF='SliderBallPlaneSensor',description='select and drag to change values'),
              Shape(
                appearance=Appearance(
                  material=Material(DEF='SliderBallMaterial',
                    IS=IS(
                      connect=[
                      connect(nodeField='diffuseColor',protoField='sliderBallColor')]))),
                geometry=Sphere(DEF='SliderBall',
                  IS=IS(
                    connect=[
                    connect(nodeField='radius',protoField='radius')])))]),
            Transform(DEF='BottomEndTransform',
              children=[
              TouchSensor(DEF='BottomEndSensor',description='touch bottom end to decrement'),
              Shape(
                appearance=Appearance(
                  material=Material(
                    IS=IS(
                      connect=[
                      connect(nodeField='diffuseColor',protoField='sliderEndColor')]))),
                geometry=Cylinder(height=.05,radius=.1)),
              Transform(translation=(0,-0.1,0),
                children=[
                Shape(DEF='TransparentEndShape',
                  appearance=Appearance(
                    material=Material(transparency=1)),
                  geometry=Box(size=(0.2,0.2,0.01)))])]),
            Transform(DEF='TopEndTransform',
              children=[
              TouchSensor(DEF='TopEndSensor',description='touch top end to increment'),
              Shape(
                appearance=Appearance(
                  material=Material(
                    IS=IS(
                      connect=[
                      connect(nodeField='diffuseColor',protoField='sliderEndColor')]))),
                geometry=Cylinder(height=.05,radius=.1)),
              Transform(translation=(0,0.1,0),
                children=[
                Shape(USE='TransparentEndShape')])]),
            Script(DEF='SliderScript',
              # local variables

              field(accessType='initializeOnly',name='height',type='SFFloat'),
              field(accessType='initializeOnly',name='radius',type='SFFloat'),
              field(accessType='initializeOnly',name='min',type='SFFloat'),
              field(accessType='initializeOnly',name='max',type='SFFloat'),
              field(accessType='initializeOnly',name='value',type='SFFloat'),
              field(accessType='initializeOnly',name='lastBallPosition',type='SFVec3f',value=(0,0,0)),
              field(accessType='initializeOnly',name='beginPosition',type='SFVec3f',value=(0,0,0)),
              field(accessType='initializeOnly',name='endPosition',type='SFVec3f',value=(0,1,0)),
              field(accessType='initializeOnly',name='incrementInterval',type='SFFloat',value=0.1),
              field(accessType='inputOnly',name='setMin',type='SFFloat'),
              field(accessType='inputOnly',name='setMax',type='SFFloat'),
              field(accessType='inputOnly',name='setValue',type='SFFloat'),
              field(accessType='outputOnly',name='valueChanged',type='SFFloat'),
              field(accessType='inputOnly',name='bottomEndTouched',type='SFBool'),
              field(accessType='inputOnly',name='topEndTouched',type='SFBool'),
              field(accessType='inputOnly',name='setBallPosition',type='SFVec3f'),
              field(accessType='outputOnly',name='bottomEndPositionChanged',type='SFVec3f'),
              field(accessType='outputOnly',name='topEndPositionChanged',type='SFVec3f'),
              field(accessType='outputOnly',name='ballPositionChanged',type='SFVec3f'),
              field(accessType='outputOnly',name='minBallPositionChanged',type='SFVec2f'),
              field(accessType='outputOnly',name='maxBallPositionChanged',type='SFVec2f'),
              field(accessType='initializeOnly',name='partialIncrement',type='SFFloat',value=0.0),
              field(accessType='initializeOnly',name='traceEnabled',type='SFBool')],
              IS=IS(
                connect=[
                connect(nodeField='height',protoField='height'),
                connect(nodeField='radius',protoField='radius'),
                connect(nodeField='min',protoField='min'),
                connect(nodeField='max',protoField='max'),
                connect(nodeField='value',protoField='value'),
                connect(nodeField='setMin',protoField='setMin'),
                connect(nodeField='setMax',protoField='setMax'),
                connect(nodeField='setValue',protoField='setValue'),
                connect(nodeField='valueChanged',protoField='valueChanged'),
                connect(nodeField='traceEnabled',protoField='traceEnabled')]),

            sourceCode="""
ecmascript:
function initialize()
{
	tracePrint('initialize() commenced...');

	beginPosition = new SFVec3f(0, (height/2) * (-1) + radius, 0);
	endPosition   = new SFVec3f(0, (height/2) - radius, 0);
	tracePrint('beginPosition=' + beginPosition.toString() + ', endPosition=' + endPosition.toString());
	incrementInterval = (height - (2 * radius)) / (max - min);
	tracePrint('incrementInterval=' + incrementInterval.toString());

	bottomEndPositionChanged = new SFVec3f(0, (height/2) * (-1), 0);
	topEndPositionChanged    = new SFVec3f(0, (height/2), 0);
	tracePrint('bottomEndPositionChanged=' + bottomEndPositionChanged.toString() + ', topEndPositionChanged=' + topEndPositionChanged.toString());

	minBallPositionChanged   = new SFVec2f(0, bottomEndPositionChanged.y + radius);
	maxBallPositionChanged   = new SFVec2f(0, topEndPositionChanged.y - radius);
	tracePrint('minBallPositionChanged=' + minBallPositionChanged.toString() + ', maxBallPositionChanged=' + maxBallPositionChanged.toString());

	ballPositionChanged = new SFVec3f(0, beginPosition.y + (incrementInterval * (value - min)), 0);
	lastBallPosition = ballPositionChanged;

	if (value < min) value = min;
	if (value > max) value = max;

	valueChanged = value;
	tracePrint('value=' + value.toString());
	tracePrint('...initialize() complete');
}

function setMin(inputValue, timeStamp)
{
	min = inputValue;
	if (value < min) value = min;

	incrementInterval = (height - (2 * radius)) / (max - min);
	tracePrint('incrementInterval=' + incrementInterval.toString());

	ballPositionChanged = new SFVec3f(0, beginPosition.y + (incrementInterval * (value - min)), 0);
	lastBallPosition = ballPositionChanged;

	valueChanged = value;
	tracePrint('min=' + min + ', valueChanged=' + valueChanged);
}

function setMax(inputValue, timeStamp)
{
	max = inputValue;
	if (value > max) value = max;

	incrementInterval = (height - (2 * radius)) / (max - min);
	tracePrint('incrementInterval=' + incrementInterval.toString());

	ballPositionChanged = new SFVec3f(0, beginPosition.y + (incrementInterval * (value - min)), 0);
	lastBallPosition = ballPositionChanged;

	valueChanged = value;
	tracePrint('max=' + max + ', valueChanged=' + valueChanged);
}

function setValue(inputValue, timeStamp)
{
        if (inputValue <= min)
	{
		valueChanged = value = min;
		ballPositionChanged = beginPosition;
		lastBallPosition = ballPositionChanged;
	}
	else if (inputValue >= max)
	{
		valueChanged = value = max;
		ballPositionChanged = endPosition;
		lastBallPosition = ballPositionChanged;
	}
	else
	{
		if (inputValue > value) //getting bigger
		{
			ballPositionChanged = new SFVec3f(0, lastBallPosition.y + (incrementInterval * (inputValue - value)), 0);
			lastBallPosition = ballPositionChanged;
		}
		else if (inputValue < value) //getting smaller
		{
			ballPositionChanged = new SFVec3f(0, lastBallPosition.y - (incrementInterval * (value - inputValue)), 0);
			lastBallPosition = ballPositionChanged;
		}
		valueChanged = value = inputValue;
	}
	tracePrint('valueChanged=' + valueChanged);
}

function bottomEndTouched(inputValue, timeStamp)
{
	tracePrint('bottomEndTouched(' + inputValue.toString() + ')');
	if (inputValue == false) return; // ignore deselection

	if (value <= Math.round(value))
		value = Math.round(value);
	else
		value = Math.round(value) + 1;

	partialIncrement = incrementInterval * (valueChanged - (value - 1));

	if (value > min)
	{
		valueChanged = --value;
		if (partialIncrement != incrementInterval)
		{
			ballPositionChanged = new SFVec3f(0, lastBallPosition.y - partialIncrement, 0);
			partialIncrement = incrementInterval;
		}
		else    ballPositionChanged = new SFVec3f(0, lastBallPosition.y - incrementInterval, 0);
		lastBallPosition = ballPositionChanged;
	}
}

function topEndTouched(inputValue, timeStamp)
{
	tracePrint('topEndTouched(' + inputValue.toString() + ')');
	if (inputValue == false) return; // ignore deselection

	if (value < Math.round(value))
		value = Math.round(value) - 1;
	else    value = Math.round(value);

	partialIncrement = incrementInterval * (value + 1 - valueChanged);

	if (value < max)
	{
		valueChanged = ++value;
		if (partialIncrement != incrementInterval)
		{
			ballPositionChanged = new SFVec3f(0, lastBallPosition.y + partialIncrement, 0);
			partialIncrement = incrementInterval;
		}
		else    ballPositionChanged = new SFVec3f(0, lastBallPosition.y + incrementInterval, 0);
		lastBallPosition = ballPositionChanged;
	}
}

function setBallPosition(inputValue, timeStamp)
{
	tracePrint('setBallPosition(' + inputValue.toString() + ')');

	if (inputValue.y > lastBallPosition.y) // moving upwards
	{
		if (value >= max)
		{
			value = max;
			lastBallPosition = endPosition;
		}
		else
		{
			value = (inputValue.y - beginPosition.y) * ((max - min) / (endPosition.y - beginPosition.y));
			lastBallPosition = inputValue;
		}
		valueChanged = value;
		ballPositionChanged = lastBallPosition;
	}
	else if (inputValue.y < lastBallPosition.y) // moving downwards
	{
		if (value <= min)
		{
			value = min;
			lastBallPosition = beginPosition;
		}
		else
		{
			value = (inputValue.y - beginPosition.y) * ((max - min) / (endPosition.y - beginPosition.y));
			lastBallPosition = inputValue;
		}
		valueChanged = value;
		ballPositionChanged = lastBallPosition;
	}
}
function tracePrint (text)
{
	if (traceEnabled) Browser.println ('[SliderFloat SliderScript] ' + text);
}
"""),
            ROUTE(fromField='translation_changed',fromNode='SliderBallPlaneSensor',toField='setBallPosition',toNode='SliderScript'),
            ROUTE(fromField='isActive',fromNode='BottomEndSensor',toField='bottomEndTouched',toNode='SliderScript'),
            ROUTE(fromField='isActive',fromNode='TopEndSensor',toField='topEndTouched',toNode='SliderScript'),
            ROUTE(fromField='minBallPositionChanged',fromNode='SliderScript',toField='set_minPosition',toNode='SliderBallPlaneSensor'),
            ROUTE(fromField='maxBallPositionChanged',fromNode='SliderScript',toField='set_maxPosition',toNode='SliderBallPlaneSensor'),
            ROUTE(fromField='bottomEndPositionChanged',fromNode='SliderScript',toField='set_translation',toNode='BottomEndTransform'),
            ROUTE(fromField='topEndPositionChanged',fromNode='SliderScript',toField='set_translation',toNode='TopEndTransform'),
            ROUTE(fromField='ballPositionChanged',fromNode='SliderScript',toField='set_translation',toNode='SliderBallTransform')]),
          Script(DEF='LayoutDirectionScript',
            field=[
            field(accessType='initializeOnly',name='direction',type='SFString'),
            field(accessType='outputOnly',name='directionRotation',type='SFRotation'),
            field(accessType='initializeOnly',name='traceEnabled',type='SFBool')],
            IS=IS(
              connect=[
              connect(nodeField='direction',protoField='layoutDirection'),
              connect(nodeField='traceEnabled',protoField='traceEnabled')]),

          sourceCode="""
ecmascript:

function initialize ()
{
    if      ((direction=='vertical') || (direction=='Vertical') || (direction=='VERTICAL'))
    {
        directionRotation = new SFRotation(0, 0, 1, 0);
    }
    else if ((direction=='horizontal') || (direction=='Horizontal') || (direction=='HORIZONTAL'))
    {
        directionRotation = new SFRotation(0, 0, 1, -1.57);
    }
    else
    {
        Browser.println ('[SliderFloat LayoutDirectionScript] unrecognized direction: ' + direction + ', using vertical');
        directionRotation = new SFRotation(0, 0, 1, 0);
    }
    tracePrint ('direction=' + direction);
}
function tracePrint (text)
{
	if (traceEnabled) Browser.println ('[SliderFloat LayoutDirectionScript] ' + text);
}
"""),
          ROUTE(fromField='directionRotation',fromNode='LayoutDirectionScript',toField='set_rotation',toNode='LayoutDirectionTransform')])])),
    Comment(' ==================== '),
    Anchor(description='SliderFloatExample',parameter=["target=_blank"],url=["SliderFloatExample.x3d","https://www.web3d.org/x3d/content/examples/Savage/Tools/Animation/SliderFloatExample.x3d","SliderFloatExample.wrl","https://www.web3d.org/x3d/content/examples/Savage/Tools/Animation/SliderFloatExample.wrl"],
      children=[
      Shape(
        appearance=Appearance(
          material=Material(diffuseColor=(0,1,1),emissiveColor=(0,1,1))),
        geometry=Text(string=["SliderFloatPrototype","is a Prototype definition file.","","To see an example scene","select this text and view","SliderFloatExample"],
          fontStyle=FontStyle(justify=["MIDDLE","MIDDLE"],size=0.8)))])])
)

### X3D model conversion complete ###

####################################################################################################
# Self-test diagnostics
####################################################################################################

print('Self-test diagnostics for SliderFloatPrototype.py:')
if        metaDiagnostics(newModel): # built-in utility method in X3D class
    print(metaDiagnostics(newModel)) # display meta info, hint, warning, error, TODO values in this model
# print('check newModel.XML() serialization...')
newModelXML= newModel.XML() # test export method XML() for exceptions during export
newModel.XMLvalidate()
# print(newModelXML) # diagnostic

try:
#   print('check newModel.VRML() serialization...')
    newModelVRML=newModel.VRML() # test export method VRML() for exceptions during export
    # print(prependLineNumbers(newModelVRML)) # debug
    print("Python-to-VRML export of VRML output successful", flush=True)
except Exception as err: # usually BaseException
    # https://stackoverflow.com/questions/18176602/how-to-get-the-name-of-an-exception-that-was-caught-in-python
    print("*** Python-to-VRML export of VRML output failed:", type(err).__name__, err)
    if newModelVRML: # may have failed to generate
        print(prependLineNumbers(newModelVRML, err.lineno))

try:
#   print('check newModel.JSON() serialization...')
    newModelJSON=newModel.JSON() # test export method JSON() for exceptions during export
#   print(prependLineNumbers(newModelJSON)) # debug
    print("Python-to-JSON export of JSON output successful (under development)")
except Exception as err: # usually SyntaxError
    print("*** Python-to-JSON export of JSON output failed:", type(err).__name__, err)
    if newModelJSON: # may have failed to generate
        print(prependLineNumbers(newModelJSON,err.lineno))

print("python SliderFloatPrototype.py load and self-test diagnostics complete.")
