diff options
Diffstat (limited to 'OpenSim/Region/Physics/ChOdePlugin/ODEPrim.cs')
-rw-r--r-- | OpenSim/Region/Physics/ChOdePlugin/ODEPrim.cs | 230 |
1 files changed, 179 insertions, 51 deletions
diff --git a/OpenSim/Region/Physics/ChOdePlugin/ODEPrim.cs b/OpenSim/Region/Physics/ChOdePlugin/ODEPrim.cs index fbe52c8..7ce01dc 100644 --- a/OpenSim/Region/Physics/ChOdePlugin/ODEPrim.cs +++ b/OpenSim/Region/Physics/ChOdePlugin/ODEPrim.cs | |||
@@ -64,11 +64,16 @@ namespace OpenSim.Region.Physics.OdePlugin | |||
64 | private Vector3 m_taintVelocity; | 64 | private Vector3 m_taintVelocity; |
65 | private Vector3 m_taintTorque; | 65 | private Vector3 m_taintTorque; |
66 | private Quaternion m_taintrot; | 66 | private Quaternion m_taintrot; |
67 | private Vector3 m_angularEnable = Vector3.One; // Current setting | 67 | private Vector3 m_rotateEnable = Vector3.One; // Current setting |
68 | private Vector3 m_taintAngularLock = Vector3.One; // Request from LSL | 68 | private Vector3 m_rotateEnableRequest = Vector3.One; // Request from LSL |
69 | 69 | private bool m_rotateEnableUpdate = false; | |
70 | 70 | private Vector3 m_lockX; | |
71 | private Vector3 m_lockY; | ||
72 | private Vector3 m_lockZ; | ||
71 | private IntPtr Amotor = IntPtr.Zero; | 73 | private IntPtr Amotor = IntPtr.Zero; |
74 | private IntPtr AmotorX = IntPtr.Zero; | ||
75 | private IntPtr AmotorY = IntPtr.Zero; | ||
76 | private IntPtr AmotorZ = IntPtr.Zero; | ||
72 | 77 | ||
73 | private Vector3 m_PIDTarget; | 78 | private Vector3 m_PIDTarget; |
74 | private float m_PIDTau; | 79 | private float m_PIDTau; |
@@ -729,7 +734,12 @@ namespace OpenSim.Region.Physics.OdePlugin | |||
729 | public override float Buoyancy | 734 | public override float Buoyancy |
730 | { | 735 | { |
731 | get { return m_buoyancy; } | 736 | get { return m_buoyancy; } |
732 | set { m_buoyancy = value; } | 737 | // set { m_buoyancy = value; } |
738 | set { | ||
739 | m_buoyancy = value; | ||
740 | |||
741 | Console.WriteLine("m_buoyancy={0}", m_buoyancy); | ||
742 | } | ||
733 | } | 743 | } |
734 | 744 | ||
735 | public override void link(PhysicsActor obj) | 745 | public override void link(PhysicsActor obj) |
@@ -744,14 +754,18 @@ namespace OpenSim.Region.Physics.OdePlugin | |||
744 | 754 | ||
745 | public override void LockAngularMotion(Vector3 axis) | 755 | public override void LockAngularMotion(Vector3 axis) |
746 | { | 756 | { |
747 | // reverse the zero/non zero values for ODE. | 757 | // This is actually ROTATION ENABLE, not a lock. |
758 | // default is <1,1,1> which is all enabled. | ||
759 | // The lock value is updated inside Move(), no point in using the taint system. | ||
760 | // OS 'm_taintAngularLock' etc change to m_rotateEnable. | ||
748 | if (axis.IsFinite()) | 761 | if (axis.IsFinite()) |
749 | { | 762 | { |
750 | axis.X = (axis.X > 0) ? 1f : 0f; | 763 | axis.X = (axis.X > 0) ? 1f : 0f; |
751 | axis.Y = (axis.Y > 0) ? 1f : 0f; | 764 | axis.Y = (axis.Y > 0) ? 1f : 0f; |
752 | axis.Z = (axis.Z > 0) ? 1f : 0f; | 765 | axis.Z = (axis.Z > 0) ? 1f : 0f; |
753 | m_log.DebugFormat("[axislock]: <{0},{1},{2}>", axis.X, axis.Y, axis.Z); | 766 | m_log.DebugFormat("[axislock]: <{0},{1},{2}>", axis.X, axis.Y, axis.Z); |
754 | m_taintAngularLock = axis; | 767 | m_rotateEnableRequest = axis; |
768 | m_rotateEnableUpdate = true; | ||
755 | } | 769 | } |
756 | else | 770 | else |
757 | { | 771 | { |
@@ -1391,10 +1405,10 @@ namespace OpenSim.Region.Physics.OdePlugin | |||
1391 | 1405 | ||
1392 | if (m_taintCollidesWater != m_collidesWater) | 1406 | if (m_taintCollidesWater != m_collidesWater) |
1393 | changefloatonwater(timestep); | 1407 | changefloatonwater(timestep); |
1394 | 1408 | /* obsolete | |
1395 | if (!m_angularEnable.ApproxEquals(m_taintAngularLock,0f)) | 1409 | if (!m_angularLock.ApproxEquals(m_taintAngularLock,0f)) |
1396 | changeAngularLock(timestep); | 1410 | changeAngularLock(timestep); |
1397 | 1411 | */ | |
1398 | } | 1412 | } |
1399 | else | 1413 | else |
1400 | { | 1414 | { |
@@ -1402,15 +1416,16 @@ namespace OpenSim.Region.Physics.OdePlugin | |||
1402 | } | 1416 | } |
1403 | } | 1417 | } |
1404 | 1418 | ||
1405 | 1419 | /* obsolete | |
1406 | private void changeAngularLock(float timestep) | 1420 | private void changeAngularLock(float timestep) |
1407 | { | 1421 | { |
1408 | if (_parent == null) | 1422 | if (_parent == null) |
1409 | { | 1423 | { |
1410 | m_angularEnable = m_taintAngularLock; | 1424 | m_angularLock = m_taintAngularLock; |
1425 | m_angularLockSet = true; | ||
1411 | } | 1426 | } |
1412 | } | 1427 | } |
1413 | 1428 | */ | |
1414 | private void changelink(float timestep) | 1429 | private void changelink(float timestep) |
1415 | { | 1430 | { |
1416 | // If the newly set parent is not null | 1431 | // If the newly set parent is not null |
@@ -2991,7 +3006,7 @@ Console.WriteLine(" JointCreateFixed"); | |||
2991 | if (fence == 1) border_limit = 0.5f; // bounce point | 3006 | if (fence == 1) border_limit = 0.5f; // bounce point |
2992 | 3007 | ||
2993 | frcount++; // used to limit debug comment output | 3008 | frcount++; // used to limit debug comment output |
2994 | if (frcount > 10) | 3009 | if (frcount > 50) |
2995 | frcount = 0; | 3010 | frcount = 0; |
2996 | 3011 | ||
2997 | if(revcount > 0) revcount--; | 3012 | if(revcount > 0) revcount--; |
@@ -3200,8 +3215,91 @@ Console.WriteLine(" JointCreateFixed"); | |||
3200 | } | 3215 | } |
3201 | m_lastposition = l_position; | 3216 | m_lastposition = l_position; |
3202 | 3217 | ||
3203 | /// End UpdatePositionAndVelocity insert | 3218 | /// End UpdatePositionAndVelocity insert |
3204 | 3219 | ||
3220 | |||
3221 | // Rotation lock ===================================== | ||
3222 | if(m_rotateEnableUpdate) | ||
3223 | { | ||
3224 | // Snapshot current angles, set up Amotor(s) | ||
3225 | m_rotateEnableUpdate = false; | ||
3226 | m_rotateEnable = m_rotateEnableRequest; | ||
3227 | Console.WriteLine("RotEnable {0} = {1}",m_primName, m_rotateEnable); | ||
3228 | |||
3229 | if (Amotor != IntPtr.Zero) | ||
3230 | { | ||
3231 | d.JointDestroy(Amotor); | ||
3232 | Amotor = IntPtr.Zero; | ||
3233 | Console.WriteLine("Old Amotor Destroyed"); | ||
3234 | } | ||
3235 | |||
3236 | if (!m_rotateEnable.ApproxEquals(Vector3.One, 0.003f)) | ||
3237 | { // not all are enabled | ||
3238 | d.Quaternion r = d.BodyGetQuaternion(Body); | ||
3239 | Quaternion locrot = new Quaternion(r.X, r.Y, r.Z, r.W); | ||
3240 | // extract the axes vectors | ||
3241 | Vector3 vX = new Vector3(1f,0f,0f); | ||
3242 | Vector3 vY = new Vector3(0f,1f,0f); | ||
3243 | Vector3 vZ = new Vector3(0f,0f,1f); | ||
3244 | vX = vX * locrot; | ||
3245 | vY = vY * locrot; | ||
3246 | vZ = vZ * locrot; | ||
3247 | // snapshot the current angle vectors | ||
3248 | m_lockX = vX; | ||
3249 | m_lockY = vY; | ||
3250 | m_lockZ = vZ; | ||
3251 | // m_lockRot = locrot; | ||
3252 | Amotor = d.JointCreateAMotor(_parent_scene.world, IntPtr.Zero); | ||
3253 | d.JointAttach(Amotor, Body, IntPtr.Zero); | ||
3254 | d.JointSetAMotorMode(Amotor, 0); // User mode?? | ||
3255 | Console.WriteLine("New Amotor Created for {0}", m_primName); | ||
3256 | |||
3257 | float axisnum = 3; // how many to lock | ||
3258 | axisnum = (axisnum - (m_rotateEnable.X + m_rotateEnable.Y + m_rotateEnable.Z)); | ||
3259 | d.JointSetAMotorNumAxes(Amotor,(int)axisnum); | ||
3260 | Console.WriteLine("AxisNum={0}",(int)axisnum); | ||
3261 | |||
3262 | int i = 0; | ||
3263 | |||
3264 | if (m_rotateEnable.X == 0) | ||
3265 | { | ||
3266 | d.JointSetAMotorAxis(Amotor, i, 0, m_lockX.X, m_lockX.Y, m_lockX.Z); | ||
3267 | Console.WriteLine("AxisX {0} set to {1}", i, m_lockX); | ||
3268 | i++; | ||
3269 | } | ||
3270 | |||
3271 | if (m_rotateEnable.Y == 0) | ||
3272 | { | ||
3273 | d.JointSetAMotorAxis(Amotor, i, 0, m_lockY.X, m_lockY.Y, m_lockY.Z); | ||
3274 | Console.WriteLine("AxisY {0} set to {1}", i, m_lockY); | ||
3275 | i++; | ||
3276 | } | ||
3277 | |||
3278 | if (m_rotateEnable.Z == 0) | ||
3279 | { | ||
3280 | d.JointSetAMotorAxis(Amotor, i, 0, m_lockZ.X, m_lockZ.Y, m_lockZ.Z); | ||
3281 | Console.WriteLine("AxisZ {0} set to {1}", i, m_lockZ); | ||
3282 | i++; | ||
3283 | } | ||
3284 | |||
3285 | // These lowstops and high stops are effectively (no wiggle room) | ||
3286 | d.JointSetAMotorParam(Amotor, (int)dParam.LowStop, 0f); | ||
3287 | d.JointSetAMotorParam(Amotor, (int)dParam.LoStop3, 0f); | ||
3288 | d.JointSetAMotorParam(Amotor, (int)dParam.LoStop2, 0f); | ||
3289 | d.JointSetAMotorParam(Amotor, (int)dParam.HiStop, 0f); | ||
3290 | d.JointSetAMotorParam(Amotor, (int)dParam.HiStop3, 0f); | ||
3291 | d.JointSetAMotorParam(Amotor, (int)dParam.HiStop2, 0f); | ||
3292 | d.JointSetAMotorParam(Amotor, (int) dParam.Vel, 0f); | ||
3293 | d.JointSetAMotorParam(Amotor, (int) dParam.Vel3, 0f); | ||
3294 | d.JointSetAMotorParam(Amotor, (int) dParam.Vel2, 0f); | ||
3295 | d.JointSetAMotorParam(Amotor, (int)dParam.StopCFM, 0f); | ||
3296 | d.JointSetAMotorParam(Amotor, (int)dParam.StopCFM3, 0f); | ||
3297 | d.JointSetAMotorParam(Amotor, (int)dParam.StopCFM2, 0f); | ||
3298 | } // else none are locked | ||
3299 | } // end Rotation Update | ||
3300 | |||
3301 | |||
3302 | // VEHICLE processing ========================================== | ||
3205 | if (m_type != Vehicle.TYPE_NONE) | 3303 | if (m_type != Vehicle.TYPE_NONE) |
3206 | { | 3304 | { |
3207 | // get body attitude | 3305 | // get body attitude |
@@ -3420,7 +3518,7 @@ Console.WriteLine(" JointCreateFixed"); | |||
3420 | //if(frcount == 0) Console.WriteLine("V3 = {0}", angObjectVel); | 3518 | //if(frcount == 0) Console.WriteLine("V3 = {0}", angObjectVel); |
3421 | 3519 | ||
3422 | 3520 | ||
3423 | // Rotation Axis Disables: | 3521 | /* // Rotation Axis Disables: |
3424 | if (!m_angularEnable.ApproxEquals(Vector3.One, 0.003f)) | 3522 | if (!m_angularEnable.ApproxEquals(Vector3.One, 0.003f)) |
3425 | { | 3523 | { |
3426 | if (m_angularEnable.X == 0) | 3524 | if (m_angularEnable.X == 0) |
@@ -3430,7 +3528,7 @@ Console.WriteLine(" JointCreateFixed"); | |||
3430 | if (m_angularEnable.Z == 0) | 3528 | if (m_angularEnable.Z == 0) |
3431 | angObjectVel.Z = 0f; | 3529 | angObjectVel.Z = 0f; |
3432 | } | 3530 | } |
3433 | 3531 | */ | |
3434 | angObjectVel = angObjectVel * rotq; // ================ Converts to WORLD rotation | 3532 | angObjectVel = angObjectVel * rotq; // ================ Converts to WORLD rotation |
3435 | 3533 | ||
3436 | // Vertical attractor section | 3534 | // Vertical attractor section |
@@ -3500,35 +3598,58 @@ Console.WriteLine(" JointCreateFixed"); | |||
3500 | } // end VEHICLES | 3598 | } // end VEHICLES |
3501 | else | 3599 | else |
3502 | { | 3600 | { |
3601 | // Dyamics (NON-'VEHICLES') are dealt with here ================================================================ | ||
3602 | |||
3503 | if(!d.BodyIsEnabled (Body)) d.BodyEnable (Body); // KF add 161009 | 3603 | if(!d.BodyIsEnabled (Body)) d.BodyEnable (Body); // KF add 161009 |
3504 | // NON-'VEHICLES' are dealt with here | 3604 | |
3505 | /// Dynamics Angular Lock ======================================================================== | 3605 | /// Dynamics Buoyancy |
3506 | if (d.BodyIsEnabled(Body) && !m_angularEnable.ApproxEquals(Vector3.One, 0.003f)) | ||
3507 | { | ||
3508 | d.Vector3 avel2 = d.BodyGetAngularVel(Body); | ||
3509 | if (m_angularEnable.X == 0) | ||
3510 | avel2.X = 0; | ||
3511 | if (m_angularEnable.Y == 0) | ||
3512 | avel2.Y = 0; | ||
3513 | if (m_angularEnable.Z == 0) | ||
3514 | avel2.Z = 0; | ||
3515 | d.BodySetAngularVel(Body, avel2.X, avel2.Y, avel2.Z); | ||
3516 | } | ||
3517 | |||
3518 | |||
3519 | /// Dynamics Buoyancy =============================================================================== | ||
3520 | //KF: m_buoyancy is set by llSetBuoyancy() and is for non-vehicle. | 3606 | //KF: m_buoyancy is set by llSetBuoyancy() and is for non-vehicle. |
3521 | // m_buoyancy: (unlimited value) <0=Falls fast; 0=1g; 1=0g; >1 = floats up | 3607 | // m_buoyancy: (unlimited value) <0=Falls fast; 0=1g; 1=0g; >1 = floats up |
3522 | // NB Prims in ODE are no subject to global gravity | 3608 | // NB Prims in ODE are no subject to global gravity |
3609 | // This should only affect gravity operations | ||
3610 | |||
3523 | float m_mass = CalculateMass(); | 3611 | float m_mass = CalculateMass(); |
3612 | // calculate z-force due togravity on object. | ||
3524 | fz = _parent_scene.gravityz * (1.0f - m_buoyancy) * m_mass; // force = acceleration * mass | 3613 | fz = _parent_scene.gravityz * (1.0f - m_buoyancy) * m_mass; // force = acceleration * mass |
3525 | 3614 | ||
3526 | if (m_usePID) | 3615 | if ((m_usePID) && (m_PIDTau > 0.0f)) // Dynamics llMoveToTarget. |
3527 | { | 3616 | { |
3528 | //if(frcount == 0) Console.WriteLine("PID " + m_primName); | 3617 | fz = 0; // llMoveToTarget ignores gravity. |
3529 | // KF - this is for object MoveToTarget. | 3618 | // it also ignores mass of object, and any physical resting on it. |
3530 | 3619 | // Vector3 m_PIDTarget is where we are going | |
3531 | //if (!d.BodyIsEnabled(Body)) | 3620 | // float m_PIDTau is time to get there |
3621 | fx = 0; | ||
3622 | fy = 0; | ||
3623 | d.Vector3 pos = d.BodyGetPosition(Body); | ||
3624 | Vector3 error = new Vector3( | ||
3625 | (m_PIDTarget.X - pos.X), | ||
3626 | (m_PIDTarget.Y - pos.Y), | ||
3627 | (m_PIDTarget.Z - pos.Z)); | ||
3628 | if (error.ApproxEquals(Vector3.Zero,0.01f)) | ||
3629 | { // Very close, Jump there and quit move | ||
3630 | d.BodySetPosition(Body, m_PIDTarget.X, m_PIDTarget.Y, m_PIDTarget.Z); | ||
3631 | _target_velocity = Vector3.Zero; | ||
3632 | d.BodySetLinearVel(Body, _target_velocity.X, _target_velocity.Y, _target_velocity.Z); | ||
3633 | } | ||
3634 | else | ||
3635 | { | ||
3636 | float scale = 50.0f * timestep / m_PIDTau; | ||
3637 | if ((error.ApproxEquals(Vector3.Zero,0.5f)) && (_target_velocity != Vector3.Zero)) | ||
3638 | { | ||
3639 | // Nearby, quit update of velocity | ||
3640 | } | ||
3641 | else | ||
3642 | { // Far, calc damped velocity | ||
3643 | _target_velocity = error * scale; | ||
3644 | } | ||
3645 | d.BodySetLinearVel(Body, _target_velocity.X, _target_velocity.Y, _target_velocity.Z); | ||
3646 | } | ||
3647 | } // end PID MoveToTarget | ||
3648 | |||
3649 | /* Original OS implementation: Does not work correctly as another phys object resting on THIS object purturbs its position. | ||
3650 | This is incorrect behavior. llMoveToTarget must move the Body no matter what phys object is resting on it. | ||
3651 | |||
3652 | //if (!d.BodyIsEnabled(Body)) | ||
3532 | //d.BodySetForce(Body, 0f, 0f, 0f); | 3653 | //d.BodySetForce(Body, 0f, 0f, 0f); |
3533 | 3654 | ||
3534 | // no lock; for now it's only called from within Simulate() | 3655 | // no lock; for now it's only called from within Simulate() |
@@ -3559,6 +3680,7 @@ Console.WriteLine(" JointCreateFixed"); | |||
3559 | (m_PIDTarget.Z - pos.Z) * ((PID_G - m_PIDTau) * timestep) | 3680 | (m_PIDTarget.Z - pos.Z) * ((PID_G - m_PIDTau) * timestep) |
3560 | ); | 3681 | ); |
3561 | 3682 | ||
3683 | if(frcount == 0) Console.WriteLine("PID {0} b={1} fz={2} vel={3}", m_primName, m_buoyancy, fz, _target_velocity); | ||
3562 | // if velocity is zero, use position control; otherwise, velocity control | 3684 | // if velocity is zero, use position control; otherwise, velocity control |
3563 | 3685 | ||
3564 | if (_target_velocity.ApproxEquals(Vector3.Zero,0.1f)) | 3686 | if (_target_velocity.ApproxEquals(Vector3.Zero,0.1f)) |
@@ -3591,9 +3713,11 @@ Console.WriteLine(" JointCreateFixed"); | |||
3591 | fz = fz + ((_target_velocity.Z - vel.Z) * (PID_D) * m_mass); | 3713 | fz = fz + ((_target_velocity.Z - vel.Z) * (PID_D) * m_mass); |
3592 | } | 3714 | } |
3593 | } // end if (m_usePID) | 3715 | } // end if (m_usePID) |
3594 | 3716 | End of old PID system */ | |
3717 | |||
3718 | |||
3595 | /// Dynamics Hover =================================================================================== | 3719 | /// Dynamics Hover =================================================================================== |
3596 | // Hover PID Controller needs to be mutually exlusive to MoveTo PID controller | 3720 | // Hover PID Controller can only run if the PIDcontroller is not in use. |
3597 | if (m_useHoverPID && !m_usePID) | 3721 | if (m_useHoverPID && !m_usePID) |
3598 | { | 3722 | { |
3599 | //Console.WriteLine("Hover " + m_primName); | 3723 | //Console.WriteLine("Hover " + m_primName); |
@@ -3709,14 +3833,14 @@ Console.WriteLine(" JointCreateFixed"); | |||
3709 | // d.BodyAddRelTorque(Body, rotforce.X, rotforce.Y, rotforce.Z); | 3833 | // d.BodyAddRelTorque(Body, rotforce.X, rotforce.Y, rotforce.Z); |
3710 | RLAservo = timestep / m_APIDStrength * scaler; | 3834 | RLAservo = timestep / m_APIDStrength * scaler; |
3711 | rotforce = rotforce * RLAservo * diff_angle ; | 3835 | rotforce = rotforce * RLAservo * diff_angle ; |
3712 | 3836 | /* | |
3713 | if (m_angularEnable.X == 0) | 3837 | if (m_angularEnable.X == 0) |
3714 | rotforce.X = 0; | 3838 | rotforce.X = 0; |
3715 | if (m_angularEnable.Y == 0) | 3839 | if (m_angularEnable.Y == 0) |
3716 | rotforce.Y = 0; | 3840 | rotforce.Y = 0; |
3717 | if (m_angularEnable.Z == 0) | 3841 | if (m_angularEnable.Z == 0) |
3718 | rotforce.Z = 0; | 3842 | rotforce.Z = 0; |
3719 | 3843 | */ | |
3720 | d.BodySetAngularVel (Body, rotforce.X, rotforce.Y, rotforce.Z); | 3844 | d.BodySetAngularVel (Body, rotforce.X, rotforce.Y, rotforce.Z); |
3721 | //Console.WriteLine("axis= " + diff_axis + " angle= " + diff_angle + "servo= " + RLAservo); | 3845 | //Console.WriteLine("axis= " + diff_axis + " angle= " + diff_angle + "servo= " + RLAservo); |
3722 | } | 3846 | } |
@@ -3763,11 +3887,12 @@ Console.WriteLine(" JointCreateFixed"); | |||
3763 | fy = nmin; | 3887 | fy = nmin; |
3764 | d.BodyAddForce(Body, fx, fy, fz); | 3888 | d.BodyAddForce(Body, fx, fy, fz); |
3765 | //Console.WriteLine("AddForce " + fx + "," + fy + "," + fz); | 3889 | //Console.WriteLine("AddForce " + fx + "," + fy + "," + fz); |
3766 | } | 3890 | } // end apply forces |
3767 | } | 3891 | } // end Dynamics |
3768 | } | 3892 | |
3769 | else | 3893 | /* obsolete? |
3770 | { // is not physical, or is not a body or is selected | 3894 | else |
3895 | { // is not physical, or is not a body or is selected | ||
3771 | // from old UpdatePositionAndVelocity, ... Not a body.. so Make sure the client isn't interpolating | 3896 | // from old UpdatePositionAndVelocity, ... Not a body.. so Make sure the client isn't interpolating |
3772 | _velocity.X = 0; | 3897 | _velocity.X = 0; |
3773 | _velocity.Y = 0; | 3898 | _velocity.Y = 0; |
@@ -3781,8 +3906,11 @@ Console.WriteLine(" JointCreateFixed"); | |||
3781 | m_rotationalVelocity.Y = 0; | 3906 | m_rotationalVelocity.Y = 0; |
3782 | m_rotationalVelocity.Z = 0; | 3907 | m_rotationalVelocity.Z = 0; |
3783 | _zeroFlag = true; | 3908 | _zeroFlag = true; |
3784 | return; | 3909 | return; |
3785 | } | 3910 | } |
3786 | } // end Move() | 3911 | */ |
3912 | } // end root prims | ||
3913 | |||
3914 | } // end Move() | ||
3787 | } // end class | 3915 | } // end class |
3788 | } | 3916 | } |