//////////
// Stackable Graph Cube script
//
// Author: Peter R. Bloomfield (SL: Pedro McMillan)
//
// Place this script inside several independent cubes.
// You can then drag the cubes around, and they will 'snap' to each other neatly.
//
//
// Note regarding object naming:
//  You can name the cubes whatever you like, but make sure all cubes which need
//  stack together have the SAME name.
//
// Note regarding scaling:
//  This is currently setup for a cube of size 0.5x0.5x0.5.
//  If you want to use a different scale, then change the "cubeSize" variable below.
//  Also update the "scanRange" variable to roughly double the width of a cube.
//
//
// Known problems:
//  - can end up with 2 cubes occupying same space
//  - does not always detect when it has been moved
//
// Versions:
//  1.0 - original version with physics... buggy and vertical stacking only
//  1.1 - physics disabled by default, but cubes snap in all dimensions, with correct rotations
//
//
//////////
// GPL:
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//////////
 
// What size are all cubes expected to be?
vector cubeSize = <0.5, 0.5, 0.5>;
// How far should we scan for other cubes?
float scanRange = 1.0;
 
// This will store the position after our last movement
vector lastMove = <0.0, 0.0, 0.0>;
 
 
// Should we use physics?
// (Note: physics makes dragging cube around more fun, but is also prone to bugs)
integer USE_PHYSICS = FALSE;
 
 
default
{
    state_entry()
    {
        // Whether we will use physics or not, disable it until we are moved
        llSetPrimitiveParams([PRIM_PHYSICS, FALSE]);
    }
 
    moving_start()
    {
        // Activate physics for as long as the cube is being moved
        if (USE_PHYSICS) llSetPrimitiveParams([PRIM_PHYSICS, TRUE]);
    }
 
    moving_end()
    {   
        // Store our current position (in case the sensor is delayed)
        lastMove = llGetPos();        
        // Look around for other nearby graph cubes
        llSensor(llGetObjectName(), NULL_KEY, SCRIPTED | PASSIVE, scanRange, PI);
 
        // Deactivate physics so we don't get a feedback loop
        // (aligns self -> physics moves it slightly -> realigns self -> etc..)
        if (USE_PHYSICS) llSetPrimitiveParams([PRIM_PHYSICS, FALSE]);
    }
 
    sensor(integer num_detected)
    {
        // Go through each sensed object to find the nearest one
        integer i = 0;
        float nearestDist = -1.0;
        float curDist = 0.0;
        vector nearestPos = <0.0, 0.0, 0.0>;
        vector curPos = <0.0, 0.0, 0.0>;
        rotation nearestRot = ZERO_ROTATION;
        for (i = 0; i < num_detected; i++) {
            // Is it the nearest so far?
            curPos = llDetectedPos(i);
            curDist = llVecDist(curPos, lastMove);
            if (nearestDist < 0.0 || curDist < nearestDist) {
                nearestDist = curDist;
                nearestPos = curPos;
                nearestRot = llDetectedRot(i);
            }
        }
 
        // Did we end up finding a suitable cube nearby?
        if (nearestDist > 0.0) {
            // Calculate the vector between the sensed object and this one,
            //  and translate it to local space for the sensed object
            //  (this lets us figure out which side of it we're on)
            vector offset = (lastMove - nearestPos) / nearestRot;
            // Figure out which side of it we're on by checking which dimension different is biggest
            float xDiff = llFabs(offset.x);
            float yDiff = llFabs(offset.y);
            float zDiff = llFabs(offset.z);
            vector snapPos = <0.0, 0.0, 0.0>;
            // Determine the position we ought to be in, relative to the local
            //  rotation of the object we are snapping to
            if (xDiff > yDiff) {
                if (xDiff > zDiff) {
                    if (offset.x < 0.0) snapPos.x = -cubeSize.x;
                    else snapPos.x = cubeSize.x;
                } else {
                    if (offset.z < 0.0) snapPos.z = -cubeSize.z;
                    else snapPos.z = cubeSize.z;
                }
            } else {
                if (yDiff > zDiff) {
                    if (offset.y < 0.0) snapPos.y = -cubeSize.y;
                    else snapPos.y = cubeSize.y;
                } else {
                    if (offset.z < 0.0) snapPos.z = -cubeSize.z;
                    else snapPos.z = cubeSize.z;
                }
            }
 
            // Apply our snap offset according to the detected rotation
            snapPos = nearestPos + (snapPos * nearestRot);            
 
            llSetPrimitiveParams([PRIM_POSITION, snapPos, PRIM_ROTATION, nearestRot]);
        }
    }
 
    no_sensor()
    {
        // Nothing detected
    }
}