Walk-Ahead Occlusion

Poor turtle's implementation: two steps forward, one step back.

Log in to post a comment.

Canvas.setpenopacity(1);

const DEBUG = false;

const square = (steps) => (step) => Math.floor(4*step/steps) === 4*step/steps ? 90 : 0;
const pentagon = (steps) => (step) => Math.floor(5*step/steps) === 5*step/steps ? 72 : 0;
const star = (steps) => (step) => Math.floor(10*step/steps) === 10*step/steps ? Math.floor(5*step/steps) === 5*step/steps ? -72 : 144 : 0;
const rod = (steps) => (step) => {
    const d = step/steps;
    if (d <= 0.1 || (d > 0.5 && d <= 0.6)) return -0.5;
    return 0;
}
const mysteryObject = (step) => 0.04
    +0.04*Math.sin(0.3*Math.PI*step/1800+0.75*Math.PI)*Math.sin(0.3*Math.PI*step/7200+0.75*Math.PI)
    -0.04*Math.cos(0.15*Math.PI*step/7200+0.5*Math.PI)*Math.cos(0.15*Math.PI*step/7200+0.5*Math.PI);

let originalObjs = [
    {
        x: 15,
        y: -25,
        h: 0,
        dir: "right",
        angleFn: star(3600),
        stepsize: 0.08,
        steps: 3600,
    },
    {
        x: 75,
        y: -0,
        h: 0,
        dir: "right",
        angleFn: pentagon(3600),
        stepsize: 0.04,
        steps: 3600,
    },
    {
        x: 7,
        y: 5,
        h: 90,
        dir: "left",
        angle: 0.1,
        stepsize: 0.05,
        steps: 3600,
    },
    {
        x: 80,
        y: 60,
        h: 25,
        dir: "right",
        angleFn: rod(3600),
        stepsize: 0.08,
        steps: 3600,
    },
    {
        x: -25,
        y: 40,
        h: 180,
        dir: "right",
        angle: 0.1,
        stepsize: 0.05,
        steps: 3600,
    },
    {
        x: -10,
        y: 80,
        h: 4,
        dir: "left",
        angleFn: square(3600),
        stepsize: 0.07,
        steps: 3600,
    },
    {
        x: 42,
        y: 46,
        h: 262,
        dir: "right",
        angleFn: square(3600),
        stepsize: 0.05,
        steps: 3600,
    },
    {
        x: 45,
        y: -85,
        h: 0,
        dir: "right",
        angle: 0.1,
        stepsize: 0.05,
        steps: 3600,
    },
    {
        x: -55,
        y: -25,
        h: 0,
        dir: "right",
        angle: 0.1,
        stepsize: 0.0666,
        steps: 3600,
    },
    {
        x: 3,
        y: -45,
        h: 80,
        dir: "left",
        angleFn: mysteryObject,
        stepsize: 0.01,
        steps: 21240,
    },
    {
        x: 78,
        y: 37,
        h: 72,
        dir: "right",
        angleFn: rod(3600),
        stepsize: 0.09,
        steps: 3600,
    },
    {
        x: 55,
        y: 60,
        h: 60,
        dir: "right",
        angleFn: pentagon(3600),
        stepsize: 0.05,
        steps: 3600,
    },
    {
        x: 7,
        y: 90,
        h: -6,
        dir: "left",
        angleFn: square(3600),
        stepsize: 0.05,
        steps: 3600,
    },
    {
        x: -28,
        y: -65,
        h: 315,
        dir: "right",
        angleFn: square(3600),
        stepsize: 0.06,
        steps: 3600,
    },
    {
        x: -50,
        y: -50,
        h: 180,
        dir: "right",
        angleFn: star(3600),
        stepsize: 0.08,
        steps: 3600,
    },
].map((val, key) => ({ id: key, ...val }));

const objs = [...originalObjs];

for (let i = 0; i < originalObjs.length; i++) {
    const selected = Math.floor(Math.random()*(objs.length - i));
    const elem = objs.splice(selected, 1);
    objs.push(...elem);
}

const innerLatchCooldown = 15;
const T_ACCURACY = 0.01;
const hitSize = 2; // size of X markers

const round = (coord) => (Math.round(1/T_ACCURACY*(coord))*T_ACCURACY).toFixed(1);
const toT = (xcoord, ycoord) => `${round(xcoord)}:${round(ycoord)}`;
let obj = objs[0];
let objStep = 0;
let T = {};
let currentT = {};
let latches = {};

Turtle.prototype.tryDraw = function (obj, objStep) {
    this.up();
    const _x = this.x();
    const _y = this.y();
    this.executeObjStep(obj, objStep);

    const t = toT(this.x(), this.y());
    
    // Tell the current T that this obj inhabits these coords.
    currentT[t] = [
        ...(T[t] || []), 
        obj.id
    ];
    
    for (const [tLatchId, cooldown] of Object.entries(latches)) {
        // Skip own references.
        if (tLatchId === obj.id) continue;
        // undefined/null signifies that the latch exist but has cooled down, so can be toggled.
        if (cooldown != null) {
            if (cooldown > 0) {
                DEBUG && console.log(`PRE: Obj ${obj.id} Step ${objStep} at ${t}: Cooldown continues for removal of latch ${tLatchId}`)
                latches[tLatchId] = cooldown === 1 ? undefined : cooldown - 1;
            } 
            else if (cooldown < 0) {
                DEBUG && console.log(`PRE: Obj ${obj.id} Step ${objStep} at ${t}: Cooldown continues for addition of latch ${tLatchId}`)
                latches[tLatchId] = cooldown === -1 ? null : cooldown + 1;
            } 
            else if (cooldown === 0) {
                DEBUG && console.log(`PRE: Obj ${obj.id} Step ${objStep} at ${t}: Latch ${tLatchId} is cooled down`)
                latches[tLatchId] = null;
            }
            else {
                DEBUG && console.error(`Whoops!`)
            }
        }
    }

    // Did we hit another object?
    if (T[t]) {
        // For each existing object at the coords...
        for (const tLatchId of T[t] || []) {
            // Skip own references.
            if (tLatchId === obj.id) continue;
            // If the object is already latched, remove the latch.
            const cooldown = latches[tLatchId];
            // undefined/null signifies that the latch exist but has cooled down, so can be toggled.
            if (cooldown === null) {
                DEBUG && console.log(`Obj ${obj.id} Step ${objStep} at ${t}: Removing latch for ${tLatchId}`)
                latches[tLatchId] = innerLatchCooldown;
                if (DEBUG && hitSize) {
                    const aux = new Turtle();
                    aux.jump(_x, _y);
                    aux.down();
                    aux.seth(90+22.5*objStep%3);
                    aux.forward(hitSize);
                    aux.backward(2*hitSize);
                    aux.forward(hitSize);
                    aux.seth(0+22.5*objStep%3);
                    aux.forward(hitSize);
                    aux.backward(2*hitSize);
                }
            }
            // Otherwise, add it.
            else if (typeof cooldown === "undefined") {
                DEBUG && console.log(`Obj ${obj.id} Step ${objStep} at ${t}: Adding latch for ${tLatchId}`)
                latches[tLatchId] = -innerLatchCooldown;
                if (DEBUG && hitSize) {
                    const aux = new Turtle();
                    aux.jump(_x, _y);
                    aux.down();
                    aux.seth(135+22.5*objStep%3);
                    aux.forward(hitSize);
                    aux.backward(2*hitSize);
                    aux.forward(hitSize);
                    aux.seth(45+22.5*objStep%3);
                    aux.forward(hitSize);
                    aux.backward(2*hitSize);
                }
            }
        }
        
        return false;
    }

    let stillLatched = Object.entries(latches)
        .filter(([key, val]) => val !== undefined && !(val > 0))
        .length > 0;
    if (stillLatched) {
        DEBUG && console.log(`Still latched... ${Object.entries(latches).map(e => e.map(s => ''+s).join(':'))}`)
        return false;
    }
    this.executeObjStep(obj, objStep, true);
    this.down();
    this.executeObjStep(obj, objStep);
}

Turtle.prototype.executeObjStep = function (obj, objStep, reverse = false) {
    const rev = obj.dir === "left" ? "right" : "left";
    !reverse && obj.angle != null && this[obj.dir](obj.angle);
    !reverse && obj.angleFn != null && this[obj.dir](obj.angleFn(objStep));    
    obj.stepsize && this[reverse ? 'backward' : 'forward'](obj.stepsize);
    obj.stepsizeFn && this[reverse ? 'backward' : 'forward'](obj.stepsizeFn(objStep));
    reverse && obj.angle != null && this[rev](obj.angle);
    reverse && obj.angleFn != null && this[rev](obj.angleFn(objStep));    
}

const turtle = new Turtle();

const findLatchesInDirection = (_keys, right = false) => {
    const keys = right ? _keys.sort() : _keys.sort().reverse();
    let met = {};
    for (const key of keys) {
        const ids = T[key];
        for (const id of ids) {
            // Add latch
            if (met[id] == null) {
                met[id] = innerLatchCooldown;
            }
            // Is it, like, really hot?
            else if (met[id] > innerLatchCooldown - 1) {
                // Keep it hot.
                met[id] = innerLatchCooldown;
            }
            else if (met[id] === 0) {
                // Remove latch if cooled down and met again.
                met[id] = undefined;
            }
        }
        // Cool down all the met objects.
        for (const [key, cooldown] in Object.entries(met)) {
            if (cooldown > 1) met[key] = cooldown - 1;
        }
    }
    return Object.entries(met).filter(([key, cooldown]) => cooldown!==undefined).map(([key]) => key);
}

const findLatches = ({ id, x, y }, step) => {
    const Tkeys = Object.keys(T);
    if (Tkeys.length === 0) {
        DEBUG && console.log(`Obj ${id} Step ${step}: Skip latching for first drawn object`)
        return {};
    }
    
    const xkeys = Tkeys.filter(k => {
        return parseFloat(k.split(':')[1]) == y;
    });
    const lkeys = xkeys?.filter(xk => xk.split(':')[0] < x);
    const rkeys = xkeys?.filter(xk => xk.split(':')[0] > x);
    
    let leftLatches = [], rightLatches = [];
    if (lkeys.length > 0) {
        DEBUG && console.log(`Obj ${id} Step ${step}: Looking left from ${x}:${y}...`)
        leftLatches = findLatchesInDirection(lkeys);
    }
    if (rkeys.length > 0) {
        DEBUG && console.log(`Obj ${id} Step ${step}: Looking right from ${x}:${y}...`)
        rightLatches = findLatchesInDirection(rkeys, true);
    }
    
    DEBUG && console.log("Left latches: ", leftLatches)
    DEBUG && console.log("Right latches: ", rightLatches)
    
    let finalLatches = {};
    for (const latch of [...leftLatches, ...rightLatches]) {
         // Both sides latched -> definitely inside this object
        if (leftLatches.includes(latch) && rightLatches.includes(latch)) {
            finalLatches[latch] = null;
        }
        // Otherwise we're not really inside that object...
    }

    return finalLatches;
}

function walk(step) {
    if (objStep === 0) {
        turtle.jump(obj.x, obj.y);
        obj.h!=null && turtle.seth(obj.h);
        obj.hFn!=null && turtle.seth(obj.hFn(objStep));
        latches = findLatches(obj, step);
    }
    if (objStep < obj.steps) {
        turtle.tryDraw(obj, objStep);
        objStep++;
    }
    else {
        objStep = 0;
        objs.shift();
        obj = objs.length ? objs[0] : null;
        let newT = {...T};
        for (const key of Object.keys(currentT)) {
            const nonunique = [...(T[key] || []), ...currentT[key]];
            const unique = {};
            for (const val of nonunique) {
                unique[val] = null;
            }
            Object.assign(T, { [key]: Object.keys(unique)});
        }
        DEBUG && console.log(T)
        currentT = {};
    }
    return !!obj;
}