<commit_before>"use strict";

/***************************************************************************************
 *
 *  이름 붙이기 규칙(2017.1.16)
 *
 *  1. 변수에 해당하는 이름들은 모두 소문자
 *  2. 이어지는 추가 이름은 '_'를 붙여서 연결
 *
 ***************************************************************************************/

/***************************************************************************************
 *  장치와 연관된 변수 및 함수 정의
 ***************************************************************************************/

Entry.byrobot_petrone_v2_controller =
{
    name: 'byrobot_petrone_v2_controller',

    // 초기화
    setZero: function()
    {
        // 초기화

        // 한 번에 명령을 전송하면 hw까지 제대로 전달되지 않는 경우가 있어
        // 명령을 각각 분리하여 전송하게 함(2017.01.03)
        for (var i = 0; i < 1; i++)
        {
            this.transferVibrator(0, 0, 0, 0);
            this.transferbuzzer(0, 0, 0);
            this.transferLightManual(0x31, 0xFF, 0);        // 조종기, flags = 0xFF
            this.transferCommand(0x31, 0x81, 0);            // 조종기, command = 0x81
        }
    },

    // Entry 좌측 하단 하드웨어 모니터 화면에 표시하는 속성
    // listPorts와 ports 두 곳 동시에 동일한 속성을 표시할 수는 없음
    monitorTemplate:
    {
        imgPath: "hw/byrobot_petrone_v2_controller.png",      // 배경 이미지
        width: 500,     // 이미지의 폭
        height: 500,    // 이미지의 높이

        // 모니터 화면 상단에 차례대로 나열하는 값
        listPorts:
        {
            "joystick_left_x"               :{name: Lang.Blocks.byrobot_petrone_v2_controller_joystick_left_x,            type: "input", pos: {x: 0, y: 0}},
            "joystick_left_y"               :{name: Lang.Blocks.byrobot_petrone_v2_controller_joystick_left_y,            type: "input", pos: {x: 0, y: 0}},
            "joystick_left_direction"       :{name: Lang.Blocks.byrobot_petrone_v2_controller_joystick_left_direction,    type: "input", pos: {x: 0, y: 0}},
            "joystick_left_event"           :{name: Lang.Blocks.byrobot_petrone_v2_controller_joystick_left_event,        type: "input", pos: {x: 0, y: 0}},
        //  "joystick_left_command"         :{name: Lang.Blocks.byrobot_petrone_v2_controller_joystick_left_command,      type: "input", pos: {x: 0, y: 0}},
            "joystick_right_x"              :{name: Lang.Blocks.byrobot_petrone_v2_controller_joystick_right_x,           type: "input", pos: {x: 0, y: 0}},
            "joystick_right_y"              :{name: Lang.Blocks.byrobot_petrone_v2_controller_joystick_right_y,           type: "input", pos: {x: 0, y: 0}},
            "joystick_right_direction"      :{name: Lang.Blocks.byrobot_petrone_v2_controller_joystick_right_direction,   type: "input", pos: {x: 0, y: 0}},
            "joystick_right_event"          :{name: Lang.Blocks.byrobot_petrone_v2_controller_joystick_right_event,       type: "input", pos: {x: 0, y: 0}},
        //  "joystick_right_command"        :{name: Lang.Blocks.byrobot_petrone_v2_controller_joystick_right_command,     type: "input", pos: {x: 0, y: 0}},
            "button_button"                 :{name: Lang.Blocks.byrobot_petrone_v2_controller_button_button,              type: "input", pos: {x: 0, y: 0}},
            "button_event"                  :{name: Lang.Blocks.byrobot_petrone_v2_controller_button_event,               type: "input", pos: {x: 0, y: 0}},
            "entryhw_countTransferReserved" :{name: Lang.Blocks.byrobot_petrone_v2_entryhw_count_transfer_reserved,       type: "output", pos: {x: 0, y: 0}},
        },

        // 모니터 화면 지정 위치와 선으로 연결하여 표시하는 값
        ports:
        {

        },

        mode : 'both'   // 표시 모드
    },

    // functions

    // 시간 지연
    checkFinish: function(script, ms)
    {
        if (!script.isStart)
        {
            script.isStart = true;
            script.timeFlag = 1;

            var fps = Entry.FPS || 60;
            var timeValue = (60 / fps) * ms;

            setTimeout(function()
            {
                script.timeFlag = 0;
            }, timeValue);

            return "Start";
        }
        else if (script.timeFlag == 1)
        {
            return "Running";
        }
        else
        {
            delete script.timeFlag;
            delete script.isStart;
            Entry.engine.isContinue = false;
            return "Finish";
        }
    },

    // 데이터 전송
    transferLightManual: function(target, flags, brightness)
    {
        // 범위 조정
        target = Math.max(target, 0);
        target = Math.min(target, 255);
        flags = Math.max(flags, 0);
        flags = Math.min(flags, 255);
        brightness = Math.max(brightness, 0);
        brightness = Math.min(brightness, 255);

        // 전송
        Entry.hw.setDigitalPortValue("target", target);
        Entry.hw.setDigitalPortValue("light_manual_flags", flags);
        Entry.hw.setDigitalPortValue("light_manual_brightness", brightness);

        Entry.hw.update();

        delete Entry.hw.sendQueue["target"];
        delete Entry.hw.sendQueue["light_manual_flags"];
        delete Entry.hw.sendQueue["light_manual_brightness"];
    },

    transferbuzzer: function(mode, value, time)
    {
        // 전송
        Entry.hw.setDigitalPortValue("target", 0x31);
        Entry.hw.setDigitalPortValue("buzzer_mode", mode);
        Entry.hw.setDigitalPortValue("buzzer_value", value);
        Entry.hw.setDigitalPortValue("buzzer_time", time);

        Entry.hw.update();

        delete Entry.hw.sendQueue["target"];
        delete Entry.hw.sendQueue["buzzer_mode"];
        delete Entry.hw.sendQueue["buzzer_value"];
        delete Entry.hw.sendQueue["buzzer_time"];
    },

    transferVibrator: function(mode, timeOn, timeOff, timeRun)
    {
        // 범위 조정
        timeOn = Math.max(timeOn, 1);
        timeOn = Math.min(timeOn, 60000);
        timeOff = Math.max(timeOff, 1);
        timeOff = Math.min(timeOff, 60000);

        // 전송
        Entry.hw.setDigitalPortValue("target", 0x31);
        Entry.hw.setDigitalPortValue("vibrator_mode", mode);
        Entry.hw.setDigitalPortValue("vibrator_on", timeOn);
        Entry.hw.setDigitalPortValue("vibrator_off", timeOff);
        Entry.hw.setDigitalPortValue("vibrator_total", timeRun);

        Entry.hw.update();

        delete Entry.hw.sendQueue["target"];
        delete Entry.hw.sendQueue["vibrator_mode"];
        delete Entry.hw.sendQueue["vibrator_on"];
        delete Entry.hw.sendQueue["vibrator_off"];
        delete Entry.hw.sendQueue["vibrator_total"];
    },

    transferCommand: function(target, command, option)
    {
        // 전송
        Entry.hw.setDigitalPortValue("target", target);
        Entry.hw.setDigitalPortValue("command_command", command);
        Entry.hw.setDigitalPortValue("command_option", option);

        Entry.hw.update();

        delete Entry.hw.sendQueue["target"];
        delete Entry.hw.sendQueue["command_command"];
        delete Entry.hw.sendQueue["command_option"];
    },

    transferUserInterface: function(uicommand, uifunction)
    {
        // 전송
        Entry.hw.setDigitalPortValue("target", 0x31);
        Entry.hw.setDigitalPortValue("userinterface_command", uicommand);
        Entry.hw.setDigitalPortValue("userinterface_function", uifunction);

        Entry.hw.update();

        delete Entry.hw.sendQueue["target"];
        delete Entry.hw.sendQueue["userinterface_command"];
        delete Entry.hw.sendQueue["userinterface_function"];
    },

    // functions for block

    // 데이터 읽기
    getData: function(script, device)
    {
        return Entry.hw.portData[device];
    },

    // LED 수동 설정
    setLightManual: function(script, target, flags, brightness)
    {
        switch( this.checkFinish(script, 40) )
        {
        case "Start":
            {
                this.transferLightManual(target, flags, brightness);
            }
            return script;

        case "Running":
            return script;

        case "Finish":
            return script.callReturn();

        default:
            return script.callReturn();
        }
    },

    // 버저 설정(함수 호출 시 시간은 모두 ms 단위 사용)
    /*
        MuteInstantally     = 1,    // 묵음 즉시 적용
        MuteContinually     = 2,    // 묵음 예약

        ScaleInstantally    = 3,    // 음계 즉시 적용
        ScaleContinually    = 4,    // 음계 예약

        HzInstantally       = 5,    // 주파수 즉시 적용
        HzContinually       = 6,    // 주파수 예약
     */
     // 정지
    setBuzzerStop: function(script)
    {
        switch( this.checkFinish(script, 40) )
        {
        case "Start":
            {
                this.transferbuzzer(0, 0, 0);
            }
            return script;

        case "Running":
            return script;

        case "Finish":
            return script.callReturn();

        default:
            return script.callReturn();
        }
    },

     // 묵음
    setBuzzerMute: function(script, time, flagDelay, flagInstantly)
    {
        time = Math.max(time, 0);
        time = Math.min(time, 60000);

        var timeDelay = 40;
        if( flagDelay )
            timeDelay = time;

        switch( this.checkFinish(script, timeDelay) )
        {
        case "Start":
            {
                var mode = 2;   // 묵음 연속
                if( flagInstantly )
                    mode = 1;   // 묵음 즉시

                this.transferbuzzer(mode, 0xEE, time);
            }
            return script;

        case "Running":
            return script;

        case "Finish":
            return script.callReturn();

        default:
            return script.callReturn();
        }
    },

    setBuzzerScale: function(script, octave, scale, time, flagDelay, flagInstantly)
    {
        time = Math.max(time, 0);
        time = Math.min(time, 60000);

        var timeDelay = 40;
        if( flagDelay )
            timeDelay = time;

        switch( this.checkFinish(script, timeDelay) )
        {
        case "Start":
            {
                var mode = 4;   // Scale 연속
                if( flagInstantly )
                    mode = 3;   // Scale 즉시

                var scalecalc = (octave * 12) + scale;

                this.transferbuzzer(mode, scalecalc, time);
            }
            return script;

        case "Running":
            return script;

        case "Finish":
            return script.callReturn();

        default:
            return script.callReturn();
        }
    },

    setBuzzerHz: function(script, hz, time, flagDelay, flagInstantly)
    {
        time = Math.max(time, 0);
        time = Math.min(time, 60000);

        var timeDelay = 40;
        if( flagDelay )
            timeDelay = time;

        switch( this.checkFinish(script, timeDelay) )
        {
        case "Start":
            {
                var mode = 6;   // Hz 연속
                if( flagInstantly )
                    mode = 5;   // Hz 즉시

                // 범위 조정
                hz = Math.max(hz, 1);
                hz = Math.min(hz, 63999);

                this.transferbuzzer(mode, hz, time);
            }
            return script;

        case "Running":
            return script;

        case "Finish":
            return script.callReturn();

        default:
            return script.callReturn();
        }
    },

    // 진동 제어
    /*
        Stop            = 0,    // 정지
        Instantally     = 1,    // 즉시 적용
        Continually     = 2,    // 예약
     */
    setVibratorStop: function(script)
    {
        switch( this.checkFinish(script, 40) )
        {
        case "Start":
            {
                this.transferVibrator(0, 0, 0, 0);
            }
            return script;

        case "Running":
            return script;

        case "Finish":
            return script.callReturn();

        default:
            return script.callReturn();
        }
    },

    setVibrator: function(script, timeOn, timeOff, timeRun, flagDelay, flagInstantly)
    {
        timeRun = Math.max(timeRun, 0);
        timeRun = Math.min(timeRun, 60000);

        var timeDelay = 40;
        if( flagDelay )
            timeDelay = timeRun;

        switch( this.checkFinish(script, timeDelay) )
        {
        case "Start":
            {
                var mode = 2;   // 예약
                if( flagInstantly )
                    mode = 1;   // 즉시

                this.transferVibrator(mode, timeOn, timeOff, timeRun);
            }
            return script;

        case "Running":
            return script;

        case "Finish":
            return script.callReturn();

        default:
            return script.callReturn();
        }
    },

    sendCommand: function(script, target, command, option)
    {
        switch( this.checkFinish(script, 40) )
        {
        case "Start":
            {
                this.transferCommand(target, command, option);
            }
            return script;

        case "Running":
            return script;

        case "Finish":
            return script.callReturn();

        default:
            return script.callReturn();
        }
    },

    /*
    setUserInterface: function(script, uicommand, uifunction)
    {
        switch( this.checkFinish(script, 40) )
        {
        case "Start":
            {
                this.transferUserInterface(uicommand, uifunction);
            }
            return script;

        case "Running":
            return script;

        case "Finish":
            return script.callReturn();

        default:
            return script.callReturn();
        }
    },
    */
};
<commit_msg>삭제된 기능 정리<commit_after>"use strict";

/***************************************************************************************
 *
 *  이름 붙이기 규칙(2017.1.16)
 *
 *  1. 변수에 해당하는 이름들은 모두 소문자
 *  2. 이어지는 추가 이름은 '_'를 붙여서 연결
 *
 ***************************************************************************************/

/***************************************************************************************
 *  장치와 연관된 변수 및 함수 정의
 ***************************************************************************************/

Entry.byrobot_petrone_v2_controller =
{
    name: 'byrobot_petrone_v2_controller',

    // 초기화
    setZero: function()
    {
        // 초기화

        // 한 번에 명령을 전송하면 hw까지 제대로 전달되지 않는 경우가 있어
        // 명령을 각각 분리하여 전송하게 함(2017.01.03)
        for (var i = 0; i < 1; i++)
        {
            this.transferVibrator(0, 0, 0, 0);
            this.transferbuzzer(0, 0, 0);
            this.transferLightManual(0x31, 0xFF, 0);        // 조종기, flags = 0xFF
            this.transferCommand(0x31, 0x81, 0);            // 조종기, command = 0x81
        }
    },

    // Entry 좌측 하단 하드웨어 모니터 화면에 표시하는 속성
    // listPorts와 ports 두 곳 동시에 동일한 속성을 표시할 수는 없음
    monitorTemplate:
    {
        imgPath: "hw/byrobot_petrone_v2_controller.png",      // 배경 이미지
        width: 256,     // 이미지의 폭
        height: 256,    // 이미지의 높이

        // 모니터 화면 상단에 차례대로 나열하는 값
        listPorts:
        {
            "joystick_left_x"               :{name: Lang.Blocks.byrobot_petrone_v2_controller_joystick_left_x,            type: "input", pos: {x: 0, y: 0}},
            "joystick_left_y"               :{name: Lang.Blocks.byrobot_petrone_v2_controller_joystick_left_y,            type: "input", pos: {x: 0, y: 0}},
            "joystick_left_direction"       :{name: Lang.Blocks.byrobot_petrone_v2_controller_joystick_left_direction,    type: "input", pos: {x: 0, y: 0}},
            "joystick_left_event"           :{name: Lang.Blocks.byrobot_petrone_v2_controller_joystick_left_event,        type: "input", pos: {x: 0, y: 0}},
            "joystick_right_x"              :{name: Lang.Blocks.byrobot_petrone_v2_controller_joystick_right_x,           type: "input", pos: {x: 0, y: 0}},
            "joystick_right_y"              :{name: Lang.Blocks.byrobot_petrone_v2_controller_joystick_right_y,           type: "input", pos: {x: 0, y: 0}},
            "joystick_right_direction"      :{name: Lang.Blocks.byrobot_petrone_v2_controller_joystick_right_direction,   type: "input", pos: {x: 0, y: 0}},
            "joystick_right_event"          :{name: Lang.Blocks.byrobot_petrone_v2_controller_joystick_right_event,       type: "input", pos: {x: 0, y: 0}},
            "button_button"                 :{name: Lang.Blocks.byrobot_petrone_v2_controller_button_button,              type: "input", pos: {x: 0, y: 0}},
            "button_event"                  :{name: Lang.Blocks.byrobot_petrone_v2_controller_button_event,               type: "input", pos: {x: 0, y: 0}},
            "entryhw_countTransferReserved" :{name: Lang.Blocks.byrobot_petrone_v2_entryhw_count_transfer_reserved,       type: "output", pos: {x: 0, y: 0}},
        },

        // 모니터 화면 지정 위치와 선으로 연결하여 표시하는 값
        ports:
        {

        },

        mode : 'both'   // 표시 모드
    },

    // functions

    // 시간 지연
    checkFinish: function(script, ms)
    {
        if (!script.isStart)
        {
            script.isStart = true;
            script.timeFlag = 1;

            var fps = Entry.FPS || 60;
            var timeValue = (60 / fps) * ms;

            setTimeout(function()
            {
                script.timeFlag = 0;
            }, timeValue);

            return "Start";
        }
        else if (script.timeFlag == 1)
        {
            return "Running";
        }
        else
        {
            delete script.timeFlag;
            delete script.isStart;
            Entry.engine.isContinue = false;
            return "Finish";
        }
    },

    // 데이터 전송
    transferLightManual: function(target, flags, brightness)
    {
        // 범위 조정
        target = Math.max(target, 0);
        target = Math.min(target, 255);
        flags = Math.max(flags, 0);
        flags = Math.min(flags, 255);
        brightness = Math.max(brightness, 0);
        brightness = Math.min(brightness, 255);

        // 전송
        Entry.hw.setDigitalPortValue("target", target);
        Entry.hw.setDigitalPortValue("light_manual_flags", flags);
        Entry.hw.setDigitalPortValue("light_manual_brightness", brightness);

        Entry.hw.update();

        delete Entry.hw.sendQueue["target"];
        delete Entry.hw.sendQueue["light_manual_flags"];
        delete Entry.hw.sendQueue["light_manual_brightness"];
    },

    transferbuzzer: function(mode, value, time)
    {
        // 전송
        Entry.hw.setDigitalPortValue("target", 0x31);
        Entry.hw.setDigitalPortValue("buzzer_mode", mode);
        Entry.hw.setDigitalPortValue("buzzer_value", value);
        Entry.hw.setDigitalPortValue("buzzer_time", time);

        Entry.hw.update();

        delete Entry.hw.sendQueue["target"];
        delete Entry.hw.sendQueue["buzzer_mode"];
        delete Entry.hw.sendQueue["buzzer_value"];
        delete Entry.hw.sendQueue["buzzer_time"];
    },

    transferVibrator: function(mode, timeOn, timeOff, timeRun)
    {
        // 범위 조정
        timeOn = Math.max(timeOn, 1);
        timeOn = Math.min(timeOn, 60000);
        timeOff = Math.max(timeOff, 1);
        timeOff = Math.min(timeOff, 60000);

        // 전송
        Entry.hw.setDigitalPortValue("target", 0x31);
        Entry.hw.setDigitalPortValue("vibrator_mode", mode);
        Entry.hw.setDigitalPortValue("vibrator_on", timeOn);
        Entry.hw.setDigitalPortValue("vibrator_off", timeOff);
        Entry.hw.setDigitalPortValue("vibrator_total", timeRun);

        Entry.hw.update();

        delete Entry.hw.sendQueue["target"];
        delete Entry.hw.sendQueue["vibrator_mode"];
        delete Entry.hw.sendQueue["vibrator_on"];
        delete Entry.hw.sendQueue["vibrator_off"];
        delete Entry.hw.sendQueue["vibrator_total"];
    },

    transferCommand: function(target, command, option)
    {
        // 전송
        Entry.hw.setDigitalPortValue("target", target);
        Entry.hw.setDigitalPortValue("command_command", command);
        Entry.hw.setDigitalPortValue("command_option", option);

        Entry.hw.update();

        delete Entry.hw.sendQueue["target"];
        delete Entry.hw.sendQueue["command_command"];
        delete Entry.hw.sendQueue["command_option"];
    },

    // functions for block

    // 데이터 읽기
    getData: function(script, device)
    {
        return Entry.hw.portData[device];
    },

    // LED 수동 설정
    setLightManual: function(script, target, flags, brightness)
    {
        switch( this.checkFinish(script, 40) )
        {
        case "Start":
            {
                this.transferLightManual(target, flags, brightness);
            }
            return script;

        case "Running":
            return script;

        case "Finish":
            return script.callReturn();

        default:
            return script.callReturn();
        }
    },

    // 버저 설정(함수 호출 시 시간은 모두 ms 단위 사용)
    /*
        MuteInstantally     = 1,    // 묵음 즉시 적용
        MuteContinually     = 2,    // 묵음 예약

        ScaleInstantally    = 3,    // 음계 즉시 적용
        ScaleContinually    = 4,    // 음계 예약

        HzInstantally       = 5,    // 주파수 즉시 적용
        HzContinually       = 6,    // 주파수 예약
     */
     // 정지
    setBuzzerStop: function(script)
    {
        switch( this.checkFinish(script, 40) )
        {
        case "Start":
            {
                this.transferbuzzer(0, 0, 0);
            }
            return script;

        case "Running":
            return script;

        case "Finish":
            return script.callReturn();

        default:
            return script.callReturn();
        }
    },

     // 묵음
    setBuzzerMute: function(script, time, flagDelay, flagInstantly)
    {
        time = Math.max(time, 0);
        time = Math.min(time, 60000);

        var timeDelay = 40;
        if( flagDelay )
            timeDelay = time;

        switch( this.checkFinish(script, timeDelay) )
        {
        case "Start":
            {
                var mode = 2;   // 묵음 연속
                if( flagInstantly )
                    mode = 1;   // 묵음 즉시

                this.transferbuzzer(mode, 0xEE, time);
            }
            return script;

        case "Running":
            return script;

        case "Finish":
            return script.callReturn();

        default:
            return script.callReturn();
        }
    },

    setBuzzerScale: function(script, octave, scale, time, flagDelay, flagInstantly)
    {
        time = Math.max(time, 0);
        time = Math.min(time, 60000);

        var timeDelay = 40;
        if( flagDelay )
            timeDelay = time;

        switch( this.checkFinish(script, timeDelay) )
        {
        case "Start":
            {
                var mode = 4;   // Scale 연속
                if( flagInstantly )
                    mode = 3;   // Scale 즉시

                var scalecalc = (octave * 12) + scale;

                this.transferbuzzer(mode, scalecalc, time);
            }
            return script;

        case "Running":
            return script;

        case "Finish":
            return script.callReturn();

        default:
            return script.callReturn();
        }
    },

    setBuzzerHz: function(script, hz, time, flagDelay, flagInstantly)
    {
        time = Math.max(time, 0);
        time = Math.min(time, 60000);

        var timeDelay = 40;
        if( flagDelay )
            timeDelay = time;

        switch( this.checkFinish(script, timeDelay) )
        {
        case "Start":
            {
                var mode = 6;   // Hz 연속
                if( flagInstantly )
                    mode = 5;   // Hz 즉시

                // 범위 조정
                hz = Math.max(hz, 1);
                hz = Math.min(hz, 63999);

                this.transferbuzzer(mode, hz, time);
            }
            return script;

        case "Running":
            return script;

        case "Finish":
            return script.callReturn();

        default:
            return script.callReturn();
        }
    },

    // 진동 제어
    /*
        Stop            = 0,    // 정지
        Instantally     = 1,    // 즉시 적용
        Continually     = 2,    // 예약
     */
    setVibratorStop: function(script)
    {
        switch( this.checkFinish(script, 40) )
        {
        case "Start":
            {
                this.transferVibrator(0, 0, 0, 0);
            }
            return script;

        case "Running":
            return script;

        case "Finish":
            return script.callReturn();

        default:
            return script.callReturn();
        }
    },

    setVibrator: function(script, timeOn, timeOff, timeRun, flagDelay, flagInstantly)
    {
        timeRun = Math.max(timeRun, 0);
        timeRun = Math.min(timeRun, 60000);

        var timeDelay = 40;
        if( flagDelay )
            timeDelay = timeRun;

        switch( this.checkFinish(script, timeDelay) )
        {
        case "Start":
            {
                var mode = 2;   // 예약
                if( flagInstantly )
                    mode = 1;   // 즉시

                this.transferVibrator(mode, timeOn, timeOff, timeRun);
            }
            return script;

        case "Running":
            return script;

        case "Finish":
            return script.callReturn();

        default:
            return script.callReturn();
        }
    },

    sendCommand: function(script, target, command, option)
    {
        switch( this.checkFinish(script, 40) )
        {
        case "Start":
            {
                this.transferCommand(target, command, option);
            }
            return script;

        case "Running":
            return script;

        case "Finish":
            return script.callReturn();

        default:
            return script.callReturn();
        }
    },

};
<|endoftext|>


<commit_before>/*global Tile */
import Geo from './geo';
import {StyleParser} from './styles/style_parser';
import {StyleManager} from './styles/style_manager';
import WorkerBroker from './utils/worker_broker';
import Texture from './gl/texture';

import log from 'loglevel';

export default class Tile {

    /**
        Tile
        @constructor
        Required properties:
        coords: object with {x, y, z} properties identifying tile coordinate location
        worker: web worker to handle tile construction
    */
    constructor({ coords, source, worker, style_zoom }) {
        Object.assign(this, {
            coords: {
                x: null,
                y: null,
                z: null
            },
            debug: {},
            loading: false,
            loaded: false,
            error: null,
            worker: null,
            generation: null,
            visible: false,
            center_dist: 0
        });

        this.worker = worker;
        this.source = source;
        this.style_zoom = style_zoom; // zoom level to be used for styling

        this.coords = coords;
        this.coords = Tile.overZoomedCoordinate(this.coords, this.source.max_zoom);
        this.coord_key = Tile.coordKey(this.coords);
        this.key = Tile.key(this.coords, this.source, this.style_zoom);
        this.min = Geo.metersForTile(this.coords);
        this.max = Geo.metersForTile({x: this.coords.x + 1, y: this.coords.y + 1, z: this.coords.z }),
        this.span = { x: (this.max.x - this.min.x), y: (this.max.y - this.min.y) };
        this.bounds = { sw: { x: this.min.x, y: this.max.y }, ne: { x: this.max.x, y: this.min.y } };

        this.meshes = {}; // renderable VBO meshes keyed by style
        this.textures = []; // textures that the tile owns (labels, etc.)
    }

    static create(spec) {
        return new Tile(spec);
    }

    static coordKey({x, y, z}) {
        return [x, y, z].join('/');
    }

    static key (coords, source, style_zoom) {
        coords = Tile.overZoomedCoordinate(coords, source.max_zoom);
        return [source.name, style_zoom, coords.x, coords.y, coords.z].join('/');
    }

    static coordinateAtZoom({x, y, z}, zoom) {
        if (z !== zoom) {
            let zscale = Math.pow(2, z - zoom);
            x = Math.floor(x / zscale);
            y = Math.floor(y / zscale);
        }
        return {x, y, z: zoom};
    }

    static isChild(parent, child) {
        if (child.z > parent.z) {
            let {x, y} = Tile.coordinateAtZoom(child, parent.z);
            return (parent.x === x && parent.y === y);
        }
        return false;
    }

    static overZoomedCoordinate({x, y, z}, max_zoom) {
        if (max_zoom !== undefined && z > max_zoom) {
            return Tile.coordinateAtZoom({x, y, z}, max_zoom);
        }
        return {x, y, z};
    }

    // Sort a set of tile instances (which already have a distance from center tile computed)
    static sort(tiles) {
        return tiles.sort((a, b) => {
            let ad = a.center_dist;
            let bd = b.center_dist;
            return (bd > ad ? -1 : (bd === ad ? 0 : 1));
        });
    }

    freeResources() {
        if (this.meshes) {
            for (let m in this.meshes) {
                this.meshes[m].destroy();
            }
        }

        if (this.textures) {
            for (let t of this.textures) {
                let texture = Texture.textures[t];
                if (texture) {
                    texture.destroy();
                }
            }
        }

        this.meshes = {};
        this.textures = [];
    }

    destroy() {
        this.workerMessage('removeTile', this.key);
        this.freeResources();
        this.worker = null;
    }

    buildAsMessage() {
        return {
            key: this.key,
            coord_key: this.coord_key,
            source: this.source.name,
            coords: this.coords,
            min: this.min,
            max: this.max,
            style_zoom: this.style_zoom,
            generation: this.generation,
            debug: this.debug
        };
    }

    workerMessage (...message) {
        return WorkerBroker.postMessage(this.worker, ...message);
    }

    build(generation) {
        this.generation = generation;
        if (!this.loaded) {
            this.loading = true;
        }
        return this.workerMessage('buildTile', { tile: this.buildAsMessage() }).catch(e => { throw e; });
    }

    // Process geometry for tile - called by web worker
    // Returns a set of tile keys that should be sent to the main thread (so that we can minimize data exchange between worker and main thread)
    static buildGeometry (tile, layers, rules, styles) {
        tile.debug.rendering = +new Date();
        tile.debug.features = 0;

        let data = tile.source_data;

        // Treat top-level style rules as 'layers'
        for (let layer_name in layers) {
            let layer = layers[layer_name];
            // Skip layers with no data source defined
            if (!layer.data) {
                log.warn(`Layer ${layer} was defined without a geometry data source and will not be rendered.`);
                continue;
            }

            // Source names don't match
            if (layer.data.source !== tile.source) {
                continue;
            }

            let geom = Tile.getDataForSource(data, layer.data, layer_name);
            if (!geom) {
                continue;
            }

            // Render features in layer
            let num_features = geom.features.length;
            for (let f = num_features-1; f >= 0; f--) {
                let feature = geom.features[f];
                let context = StyleParser.getFeatureParseContext(feature, tile);

                // Get draw groups for this feature
                let layer_rules = rules[layer_name];
                let draw_groups = layer_rules.buildDrawGroups(context, true);
                if (!draw_groups) {
                    continue;
                }

                // Render draw groups
                for (let group_name in draw_groups) {
                    let group = draw_groups[group_name];
                    if (!group.visible) {
                        continue;
                    }

                    // Add to style
                    let style_name = group.style || group_name;
                    let style = styles[style_name];

                    if (!style) {
                        log.warn(`Style '${style_name}' not found for rule in layer '${layer_name}':`, group, feature);
                        continue;
                    }

                    context.properties = group.properties; // add rule-specific properties to context

                    style.addFeature(feature, group, context);

                    context.properties = null; // clear group-specific properties
                }

                tile.debug.features++;
            }
        }
        tile.debug.rendering = +new Date() - tile.debug.rendering;

        // Finalize array buffer for each render style
        let tile_styles = StyleManager.stylesForTile(tile.key);
        tile.mesh_data = {};
        let queue = [];
        for (let style_name of tile_styles) {
            let style = styles[style_name];
            queue.push(style.endData(tile.key).then((style_data) => {
                if (style_data) {
                    tile.mesh_data[style_name] = {
                        vertex_data: style_data.vertex_data,
                        uniforms: style_data.uniforms,
                        textures: style_data.textures
                    };
                }
            }));
        }

        return Promise.all(queue).then(() => {
            // Return keys to be transfered to main thread
            return ['mesh_data'];
        });
    }

    /**
        Retrieves geometry from a tile according to a data source definition
    */
    static getDataForSource (source_data, source_config, default_layer = null) {
        var geom;

        if (source_config != null) {
            // If no layer specified, and a default source layer exists
            if (!source_config.layer && source_data.layers._default) {
                geom = source_data.layers._default;
            }
            // If no layer specified, and a default requested layer exists
            else if (!source_config.layer && default_layer) {
                geom = source_data.layers[default_layer];
            }
            // If a layer is specified by name, use it
            else if (typeof source_config.layer === 'string') {
                geom = source_data.layers[source_config.layer];
            }
            // If multiple layers are specified by name, combine them
            else if (Array.isArray(source_config.layer)) {
                geom = { type: 'FeatureCollection', features: [] };
                source_config.layer.forEach(layer => {
                    if (source_data.layers[layer] && source_data.layers[layer].features) {
                        geom.features.push(...source_data.layers[layer].features);
                    }
                });
            }
            // Assemble a custom layer via a function, which is called with all source layers
            else if (typeof source_config.layer === 'function') {
                geom = source_config.layer(source_data.layers);
            }
        }

        return geom;
    }

    /**
       Called on main thread when a web worker completes processing
       for a single tile.
    */
    buildMeshes(styles) {
        if (this.error) {
            return;
        }

        // Cleanup existing VBOs
        this.freeResources();

        // Debug
        this.debug.geometries = 0;
        this.debug.buffer_size = 0;

        // Create VBOs
        let mesh_data = this.mesh_data;
        if (mesh_data) {
            for (var s in mesh_data) {
                if (mesh_data[s].vertex_data) {
                    this.debug.buffer_size += mesh_data[s].vertex_data.byteLength;
                    if (!styles[s]) {
                        log.warn(`Could not create mesh because style '${s}' not found, for tile ${this.key}, aborting tile`);
                        this.meshes = {};
                        break;
                    }
                    this.meshes[s] = styles[s].makeMesh(mesh_data[s].vertex_data, mesh_data[s]);
                    this.debug.geometries += this.meshes[s].geometry_count;
                }

                // Assign ownership to textures if needed
                if (mesh_data[s].textures) {
                    this.textures.push(...mesh_data[s].textures);
                }
            }
        }

        this.debug.geom_ratio = (this.debug.geometries / this.debug.features).toFixed(1);
        this.mesh_data = null; // TODO: might want to preserve this for rebuilding geometries when styles/etc. change?
        this.printDebug();
    }

    /**
        Called on main thread when web worker completes processing, but tile has since been discarded
        Frees resources that would have been transferred to the tile object.
        Static method because the tile object no longer exists (the tile data returned by the worker is passed instead).
    */
    static abortBuild (tile) {
        if (tile.mesh_data) {
            for (let s in tile.mesh_data) {
                let textures = tile.mesh_data[s].textures;
                if (textures) {
                    for (let t of textures) {
                        let texture = Texture.textures[t];
                        if (texture) {
                            log.trace(`destroying texture ${t} for tile ${tile.key}`);
                            texture.destroy();
                        }
                    }
                }
            }
        }
    }

    printDebug () {
        log.debug(`Tile: debug for ${this.key}: [  ${JSON.stringify(this.debug)} ]`);
    }

    update(scene) {
        let coords = this.coords;
        if (coords.z !== scene.center_tile.z) {
            coords = Tile.coordinateAtZoom(coords, scene.center_tile.z);
        }
        this.center_dist = Math.abs(scene.center_tile.x - coords.x) + Math.abs(scene.center_tile.y - coords.y);
    }

    // Slice a subset of keys out of a tile
    // Includes a minimum set of pre-defined keys for load state, debug. etc.
    // We use this to send a subset of the tile back to the main thread, to minimize unnecessary data transfer
    // (e.g. very large items like feature geometry are not needed on the main thread)
    static slice (tile, keys) {
        let keep = [
            'key',
            'loading',
            'loaded',
            'generation',
            'error',
            'debug'
        ];
        if (Array.isArray(keys)) {
            keep.push(...keys);
        }

        // Build the tile subset
        var tile_subset = {};
        for (let key of keep) {
            tile_subset[key] = tile[key];
        }

        return tile_subset;
    }

    /**
        Called on worker to cancel loading
        Static method because the worker only has object representations of tile data, there is no
        tile instance created yet.
    */
    static cancel(tile) {
        if (tile && tile.source_data && tile.source_data.request) {
            tile.source_data.request.abort();
        }
    }

    merge(other) {
        for (var key in other) {
            if (key !== 'key') {
                this[key] = other[key];
            }
        }
        return this;
    }

}
<commit_msg>abort tile build when canceling<commit_after>/*global Tile */
import Geo from './geo';
import {StyleParser} from './styles/style_parser';
import {StyleManager} from './styles/style_manager';
import WorkerBroker from './utils/worker_broker';
import Texture from './gl/texture';

import log from 'loglevel';

export default class Tile {

    /**
        Tile
        @constructor
        Required properties:
        coords: object with {x, y, z} properties identifying tile coordinate location
        worker: web worker to handle tile construction
    */
    constructor({ coords, source, worker, style_zoom }) {
        Object.assign(this, {
            coords: {
                x: null,
                y: null,
                z: null
            },
            debug: {},
            loading: false,
            loaded: false,
            error: null,
            worker: null,
            generation: null,
            visible: false,
            center_dist: 0
        });

        this.worker = worker;
        this.source = source;
        this.style_zoom = style_zoom; // zoom level to be used for styling

        this.coords = coords;
        this.coords = Tile.overZoomedCoordinate(this.coords, this.source.max_zoom);
        this.coord_key = Tile.coordKey(this.coords);
        this.key = Tile.key(this.coords, this.source, this.style_zoom);
        this.min = Geo.metersForTile(this.coords);
        this.max = Geo.metersForTile({x: this.coords.x + 1, y: this.coords.y + 1, z: this.coords.z }),
        this.span = { x: (this.max.x - this.min.x), y: (this.max.y - this.min.y) };
        this.bounds = { sw: { x: this.min.x, y: this.max.y }, ne: { x: this.max.x, y: this.min.y } };

        this.meshes = {}; // renderable VBO meshes keyed by style
        this.textures = []; // textures that the tile owns (labels, etc.)
    }

    static create(spec) {
        return new Tile(spec);
    }

    static coordKey({x, y, z}) {
        return [x, y, z].join('/');
    }

    static key (coords, source, style_zoom) {
        coords = Tile.overZoomedCoordinate(coords, source.max_zoom);
        return [source.name, style_zoom, coords.x, coords.y, coords.z].join('/');
    }

    static coordinateAtZoom({x, y, z}, zoom) {
        if (z !== zoom) {
            let zscale = Math.pow(2, z - zoom);
            x = Math.floor(x / zscale);
            y = Math.floor(y / zscale);
        }
        return {x, y, z: zoom};
    }

    static isChild(parent, child) {
        if (child.z > parent.z) {
            let {x, y} = Tile.coordinateAtZoom(child, parent.z);
            return (parent.x === x && parent.y === y);
        }
        return false;
    }

    static overZoomedCoordinate({x, y, z}, max_zoom) {
        if (max_zoom !== undefined && z > max_zoom) {
            return Tile.coordinateAtZoom({x, y, z}, max_zoom);
        }
        return {x, y, z};
    }

    // Sort a set of tile instances (which already have a distance from center tile computed)
    static sort(tiles) {
        return tiles.sort((a, b) => {
            let ad = a.center_dist;
            let bd = b.center_dist;
            return (bd > ad ? -1 : (bd === ad ? 0 : 1));
        });
    }

    freeResources() {
        if (this.meshes) {
            for (let m in this.meshes) {
                this.meshes[m].destroy();
            }
        }

        if (this.textures) {
            for (let t of this.textures) {
                let texture = Texture.textures[t];
                if (texture) {
                    texture.destroy();
                }
            }
        }

        this.meshes = {};
        this.textures = [];
    }

    destroy() {
        this.workerMessage('removeTile', this.key);
        this.freeResources();
        this.worker = null;
    }

    buildAsMessage() {
        return {
            key: this.key,
            coord_key: this.coord_key,
            source: this.source.name,
            coords: this.coords,
            min: this.min,
            max: this.max,
            style_zoom: this.style_zoom,
            generation: this.generation,
            debug: this.debug
        };
    }

    workerMessage (...message) {
        return WorkerBroker.postMessage(this.worker, ...message);
    }

    build(generation) {
        this.generation = generation;
        if (!this.loaded) {
            this.loading = true;
        }
        return this.workerMessage('buildTile', { tile: this.buildAsMessage() }).catch(e => { throw e; });
    }

    // Process geometry for tile - called by web worker
    // Returns a set of tile keys that should be sent to the main thread (so that we can minimize data exchange between worker and main thread)
    static buildGeometry (tile, layers, rules, styles) {
        tile.debug.rendering = +new Date();
        tile.debug.features = 0;

        let data = tile.source_data;

        // Treat top-level style rules as 'layers'
        for (let layer_name in layers) {
            let layer = layers[layer_name];
            // Skip layers with no data source defined
            if (!layer.data) {
                log.warn(`Layer ${layer} was defined without a geometry data source and will not be rendered.`);
                continue;
            }

            // Source names don't match
            if (layer.data.source !== tile.source) {
                continue;
            }

            let geom = Tile.getDataForSource(data, layer.data, layer_name);
            if (!geom) {
                continue;
            }

            // Render features in layer
            let num_features = geom.features.length;
            for (let f = num_features-1; f >= 0; f--) {
                let feature = geom.features[f];
                let context = StyleParser.getFeatureParseContext(feature, tile);

                // Get draw groups for this feature
                let layer_rules = rules[layer_name];
                let draw_groups = layer_rules.buildDrawGroups(context, true);
                if (!draw_groups) {
                    continue;
                }

                // Render draw groups
                for (let group_name in draw_groups) {
                    let group = draw_groups[group_name];
                    if (!group.visible) {
                        continue;
                    }

                    // Add to style
                    let style_name = group.style || group_name;
                    let style = styles[style_name];

                    if (!style) {
                        log.warn(`Style '${style_name}' not found for rule in layer '${layer_name}':`, group, feature);
                        continue;
                    }

                    context.properties = group.properties; // add rule-specific properties to context

                    style.addFeature(feature, group, context);

                    context.properties = null; // clear group-specific properties
                }

                tile.debug.features++;
            }
        }
        tile.debug.rendering = +new Date() - tile.debug.rendering;

        // Finalize array buffer for each render style
        let tile_styles = StyleManager.stylesForTile(tile.key);
        tile.mesh_data = {};
        let queue = [];
        for (let style_name of tile_styles) {
            let style = styles[style_name];
            queue.push(style.endData(tile.key).then((style_data) => {
                if (style_data) {
                    tile.mesh_data[style_name] = {
                        vertex_data: style_data.vertex_data,
                        uniforms: style_data.uniforms,
                        textures: style_data.textures
                    };
                }
            }));
        }

        return Promise.all(queue).then(() => {
            // Return keys to be transfered to main thread
            return ['mesh_data'];
        });
    }

    /**
        Retrieves geometry from a tile according to a data source definition
    */
    static getDataForSource (source_data, source_config, default_layer = null) {
        var geom;

        if (source_config != null) {
            // If no layer specified, and a default source layer exists
            if (!source_config.layer && source_data.layers._default) {
                geom = source_data.layers._default;
            }
            // If no layer specified, and a default requested layer exists
            else if (!source_config.layer && default_layer) {
                geom = source_data.layers[default_layer];
            }
            // If a layer is specified by name, use it
            else if (typeof source_config.layer === 'string') {
                geom = source_data.layers[source_config.layer];
            }
            // If multiple layers are specified by name, combine them
            else if (Array.isArray(source_config.layer)) {
                geom = { type: 'FeatureCollection', features: [] };
                source_config.layer.forEach(layer => {
                    if (source_data.layers[layer] && source_data.layers[layer].features) {
                        geom.features.push(...source_data.layers[layer].features);
                    }
                });
            }
            // Assemble a custom layer via a function, which is called with all source layers
            else if (typeof source_config.layer === 'function') {
                geom = source_config.layer(source_data.layers);
            }
        }

        return geom;
    }

    /**
       Called on main thread when a web worker completes processing
       for a single tile.
    */
    buildMeshes(styles) {
        if (this.error) {
            return;
        }

        // Cleanup existing VBOs
        this.freeResources();

        // Debug
        this.debug.geometries = 0;
        this.debug.buffer_size = 0;

        // Create VBOs
        let mesh_data = this.mesh_data;
        if (mesh_data) {
            for (var s in mesh_data) {
                if (mesh_data[s].vertex_data) {
                    this.debug.buffer_size += mesh_data[s].vertex_data.byteLength;
                    if (!styles[s]) {
                        log.warn(`Could not create mesh because style '${s}' not found, for tile ${this.key}, aborting tile`);
                        this.meshes = {};
                        break;
                    }
                    this.meshes[s] = styles[s].makeMesh(mesh_data[s].vertex_data, mesh_data[s]);
                    this.debug.geometries += this.meshes[s].geometry_count;
                }

                // Assign ownership to textures if needed
                if (mesh_data[s].textures) {
                    this.textures.push(...mesh_data[s].textures);
                }
            }
        }

        this.debug.geom_ratio = (this.debug.geometries / this.debug.features).toFixed(1);
        this.mesh_data = null; // TODO: might want to preserve this for rebuilding geometries when styles/etc. change?
        this.printDebug();
    }

    /**
        Called on main thread when web worker completes processing, but tile has since been discarded
        Frees resources that would have been transferred to the tile object.
        Static method because the tile object no longer exists (the tile data returned by the worker is passed instead).
    */
    static abortBuild (tile) {
        if (tile.mesh_data) {
            for (let s in tile.mesh_data) {
                let textures = tile.mesh_data[s].textures;
                if (textures) {
                    for (let t of textures) {
                        let texture = Texture.textures[t];
                        if (texture) {
                            log.trace(`destroying texture ${t} for tile ${tile.key}`);
                            texture.destroy();
                        }
                    }
                }
            }
        }
    }

    printDebug () {
        log.debug(`Tile: debug for ${this.key}: [  ${JSON.stringify(this.debug)} ]`);
    }

    update(scene) {
        let coords = this.coords;
        if (coords.z !== scene.center_tile.z) {
            coords = Tile.coordinateAtZoom(coords, scene.center_tile.z);
        }
        this.center_dist = Math.abs(scene.center_tile.x - coords.x) + Math.abs(scene.center_tile.y - coords.y);
    }

    // Slice a subset of keys out of a tile
    // Includes a minimum set of pre-defined keys for load state, debug. etc.
    // We use this to send a subset of the tile back to the main thread, to minimize unnecessary data transfer
    // (e.g. very large items like feature geometry are not needed on the main thread)
    static slice (tile, keys) {
        let keep = [
            'key',
            'loading',
            'loaded',
            'generation',
            'error',
            'debug'
        ];
        if (Array.isArray(keys)) {
            keep.push(...keys);
        }

        // Build the tile subset
        var tile_subset = {};
        for (let key of keep) {
            tile_subset[key] = tile[key];
        }

        return tile_subset;
    }

    /**
        Called on worker to cancel loading
        Static method because the worker only has object representations of tile data, there is no
        tile instance created yet.
    */
    static cancel(tile) {
        if (tile) {
            if (tile.source_data && tile.source_data.request) {
                tile.source_data.request.abort();
            }
            Tile.abortBuild(tile);
        }
    }

    merge(other) {
        for (var key in other) {
            if (key !== 'key') {
                this[key] = other[key];
            }
        }
        return this;
    }

}
<|endoftext|>


<commit_before>/*jshint esnext: true*/

(function () {

    const ID_MIN = 1000000000000000;
    const ID_MAX = 9007199254740992;

    var ElementSet = function ElementSet( elements ) {
        this._set = elements instanceof Array ? new Set(elements) : new Set();
        this.size = 0;
    };
    ElementSet.prototype.add = function add( value ) {
        this._set.add( value );
        this.size = this._set.size;
        return this;
    };
    ElementSet.prototype.clear = function clear() {
        this._set.clear();
        this.size = 0;
    };
    ElementSet.prototype.delete = function ( value ) {
        var result = this._set.delete( value );
        this.size = this._set.size;
        return result;
    };
    ElementSet.prototype.entries = function entries() {
        return this._set.entries();
    };
    ElementSet.prototype.forEach = function forEach( callbackFn, thisArg ) {
        this._set.forEach( callbackFn, thisArg );
    };
    ElementSet.prototype.has = function has( value ) {
        return this._set.has( value );
    };
    ElementSet.prototype.keys = function keys() {
        return this._set.keys();
    };
    ElementSet.prototype.values = function values() {
        return this._set.values();
    };
    ElementSet.prototype['@@iterator'] = ElementSet.prototype.values;
    ElementSet.prototype.$verts = function $verts() {
        var results = new ElementSet();
        for ( var element of this ) {
            var verts = element.$verts();
            for ( var vert of verts ) {
                results.add( vert );
            }
        }
        return results;
    };
    ElementSet.prototype.$edges = function $edges() {
        var results = new ElementSet();
        for ( var element of this ) {
            var edges = element.$$edges();
            for ( var edge of edges ) {
                results.add( edge );
            }
        }
        return results;
    };
    ElementSet.prototype.$faces = function $faces() {
        var results = new ElementSet();
        for ( var element of this ) {
            var faces = element.$faces();
            for ( var face of faces ) {
                results.add( face );
            }
        }
        return results;
    };

    var Vert = function Vert() {
        this.id = undefined;
        this.annotation = {};
        this.edges = new ElementSet();  // Incident edges.
    };
    Vert.prototype.$verts = function $verts() {
        var results = new ElementSet();
        for ( var edge of this.edges ) {
            results.add( edge.vert1 );
            results.add( edge.vert2 );
        }
        results.delete( this );
        return results;
    };
    Vert.prototype.$edges = function $edges() {
        return this.edges;
    };
    Vert.prototype.$faces = function $faces() {
        var results = new ElementSet();
        for ( var edge of this.edges ) {
            for ( var face of edge.faces ) {
                results.add(face);
            }
        }
        return results;
    };

    var Edge = function Edge() {
        this.id = undefined;
        this.annotation = {};
        this.vert1 = undefined;         // Adjacent vertex 1.
        this.vert2 = undefined;         // Adjacent vertex 2.
        this.faces = new ElementSet();  // Incident faces.
    };
    Edge.prototype.$verts = function $verts() {
        return new ElementSet([this.vert1, this.vert2]);
    };
    Edge.prototype.$edges = function $edges() {
        var results = new ElementSet();
        for ( var edge of this.vert1.edges ) {
            results.add( edge );
        }
        for ( var edge of this.vert2.edges ) {
            results.add( edge );
        }
        results.delete( this );
        return results;
    };
    Edge.prototype.$faces = function $faces() {
        return this.faces;
    };

    var Face = function Face() {
        this.id = undefined;
        this.annotation = {};
        this.edges = new ElementSet();  // Adjacent edges.
    };
    Face.prototype.$verts = function $verts() {
        var results = new ElementSet();
        for ( var edge of this.edges ) {
            results.add( edge.vert1 );
            results.add( edge.vert2 );
        }
        return results;
    };
    Face.prototype.$edges = function $edges() {
        return this.edges;
    };
    Face.prototype.$faces = function $faces() {
        var results = new ElementSet();
        for ( var edge of this.edges ) {
            for ( var face of edge.faces ) {
                results.add( face );
            }
        }
        results.delete( this );
        return results;
    };

    var Mesh = function Mesh() {
        this.verts = new Map(); // Table of all vertices.
        this.edges = new Map(); // Table of all edges.
        this.faces = new Map(); // Table of all faces.
    };
    Mesh.prototype.addVert = function addVert( vert ) {
        if ( vert instanceof Vert ) {
            var id = vert.id ? vert.id : Math.floor( Math.random() * ( ID_MAX - ID_MIN + 1 ) );
            vert.id = id;
            this.verts.set( id, vert );
            return vert;
        } else {
            throw new TypeError('Not a vert!');
        }
    };
    Mesh.prototype.addEdge = function addEdge( edge ) {
        if ( edge instanceof Edge ) {
            var id = edge.id ? edge.id : Math.floor( Math.random() * ( ID_MAX - ID_MIN + 1 ) );
            edge.id = id;
            this.edges.set( id, edge );
            return edge;
        } else {
            throw new TypeError('Not an edge!');
        }
    };
    Mesh.prototype.addFace = function addFace( face ) {
        if ( face instanceof Face ) {
            var id = face.id ? face.id : Math.floor( Math.random() * ( ID_MAX - ID_MIN + 1 ) );
            face.id = id;
            this.faces.set( id, face );
            return face;
        } else {
            throw new TypeError('Not a face!');
        }
    };
    Mesh.prototype.deleteVert = function addVert( vert ) {
        if ( vert instanceof Vert ) {
            return this.verts.delete( vert.id );
        } else {
            return this.verts.delete( vert );
        }
    };
    Mesh.prototype.deleteEdge = function addEdge( edge ) {
        if ( edge instanceof Edge ) {
            return this.edges.delete( edge.id );
        } else {
            return this.edges.delete( edge );
        }
    };
    Mesh.prototype.deleteFace = function addFace( face ) {
        if ( face instanceof Face ) {
            return this.faces.delete( face.id );
        } else {
            return this.faces.delete( face );
        }
    };

    var SimpleMesh = function SimpleMesh() {
        Mesh.call(this);
    };
    SimpleMesh.prototype = Object.create( SimpleMesh.prototype );
    SimpleMesh.prototype.addVert = function addVert( x, y, z ) {
        var vert = new Vert();
        vert.annotation.x = x ? x : 0;
        vert.annotation.y = y ? y : 0;
        vert.annotation.z = z ? z : 0;
        return Mesh.prototype.addVert.call( this, vert );
    };
    SimpleMesh.prototype.addEdge = function addEdge( vert1, vert2 ) {
        if ( vert1 === vert2 ) {
            throw new TypeError('Degenerate edge!');
        }
        if ( !this.verts.has( vert1.id ) || !this.verts.has( vert2.id ) ) {
            throw new ReferenceError('Vert not in mesh!');
        }
        var edge = new Edge();
        edge.vert1 = vert1;
        edge.vert2 = vert2;
        vert1.edges.add( edge );
        vert2.edges.add( edge );
        return Mesh.prototype.addEdge.call( this, edge );
    };
    SimpleMesh.prototype.addFace = function addFace( edges ) {
        if ( edges.length < 3 || edges.size < 3 ) {
            throw new TypeError('Degenerate face!');
        }
        var face = new Face();
        for ( var edge of edges ) {
            if ( !this.edges.has( edge.id ) ) {
                throw new ReferenceError('Edge not in mesh!');
            }
            edge.faces.add( face );
            face.edges.add( edge );
        }
        //TODO: Check if edges form a loop before trying to add to mesh.
        return Mesh.prototype.addFace.call( this, face );
    };

    module.exports = {
        ElementSet:     ElementSet,
        Vert:           Vert,
        Edge:           Edge,
        Face:           Face,
        Mesh:           Mesh,
        SimpleMesh:     SimpleMesh
    };

}).call(this);
<commit_msg>Changed `SimpleMesh` api, added validation for edge loops, and face addition via verts.<commit_after>/*jshint esnext: true*/

(function () {

    // Bounds for random element id generation.

    const ID_MIN = 1000000000000000;
    const ID_MAX = 9007199254740992;

    // Element collection object.
    // An ES6 Set-like object that collects unique references to elements.
    // And is extended to query adjacency/incidence information of collected elements.

    var ElementSet = function ElementSet( elements ) {
        this._set = elements !== undefined ? new Set(elements) : new Set();
        this.size = 0;
    };
    ElementSet.prototype.add = function add( value ) {
        this._set.add( value );
        this.size = this._set.size;
        return this;
    };
    ElementSet.prototype.clear = function clear() {
        this._set.clear();
        this.size = 0;
    };
    ElementSet.prototype.delete = function ( value ) {
        var result = this._set.delete( value );
        this.size = this._set.size;
        return result;
    };
    ElementSet.prototype.entries = function entries() {
        return this._set.entries();
    };
    ElementSet.prototype.forEach = function forEach( callbackFn, thisArg ) {
        this._set.forEach( callbackFn, thisArg );
    };
    ElementSet.prototype.has = function has( value ) {
        return this._set.has( value );
    };
    ElementSet.prototype.keys = function keys() {
        return this._set.keys();
    };
    ElementSet.prototype.values = function values() {
        return this._set.values();
    };
    ElementSet.prototype['@@iterator'] = ElementSet.prototype.values; // Required to be iterable.
    ElementSet.prototype.$verts = function $verts() {
        var results = new ElementSet();
        for ( var element of this ) {
            var verts = element.$verts();
            for ( var vert of verts ) {
                results.add( vert );
            }
        }
        return results;
    };
    ElementSet.prototype.$edges = function $edges() {
        var results = new ElementSet();
        for ( var element of this ) {
            var edges = element.$$edges();
            for ( var edge of edges ) {
                results.add( edge );
            }
        }
        return results;
    };
    ElementSet.prototype.$faces = function $faces() {
        var results = new ElementSet();
        for ( var element of this ) {
            var faces = element.$faces();
            for ( var face of faces ) {
                results.add( face );
            }
        }
        return results;
    };

    // AIF mesh elements.

    var Vert = function Vert() {
        this.id = undefined;
        this.annotation = {};
        this.edges = new ElementSet();  // Incident edges.
    };
    Vert.prototype.$verts = function $verts() {
        var results = new ElementSet();
        for ( var edge of this.edges ) {
            results.add( edge.vert1 );
            results.add( edge.vert2 );
        }
        results.delete( this );
        return results;
    };
    Vert.prototype.$edges = function $edges() {
        return this.edges;
    };
    Vert.prototype.$faces = function $faces() {
        var results = new ElementSet();
        for ( var edge of this.edges ) {
            for ( var face of edge.faces ) {
                results.add(face);
            }
        }
        return results;
    };

    var Edge = function Edge() {
        this.id = undefined;
        this.annotation = {};
        this.vert1 = undefined;         // Adjacent vertex 1.
        this.vert2 = undefined;         // Adjacent vertex 2.
        this.faces = new ElementSet();  // Incident faces.
    };
    Edge.prototype.$verts = function $verts() {
        return new ElementSet([this.vert1, this.vert2]);
    };
    Edge.prototype.$edges = function $edges() {
        var results = new ElementSet();
        var edge;
        for ( edge of this.vert1.edges ) {
            results.add( edge );
        }
        for ( edge of this.vert2.edges ) {
            results.add( edge );
        }
        results.delete( this );
        return results;
    };
    Edge.prototype.$faces = function $faces() {
        return this.faces;
    };

    var Face = function Face() {
        this.id = undefined;
        this.annotation = {};
        this.edges = new ElementSet();  // Adjacent edges.
    };
    Face.prototype.$verts = function $verts() {
        var results = new ElementSet();
        for ( var edge of this.edges ) {
            results.add( edge.vert1 );
            results.add( edge.vert2 );
        }
        return results;
    };
    Face.prototype.$edges = function $edges() {
        return this.edges;
    };
    Face.prototype.$faces = function $faces() {
        var results = new ElementSet();
        for ( var edge of this.edges ) {
            for ( var face of edge.faces ) {
                results.add( face );
            }
        }
        results.delete( this );
        return results;
    };

    // Basic AIF mesh.
    // Fully non-manifold, no validation when adding elements.

    var Mesh = function Mesh() {
        this.verts = new Map(); // Table of all vertices.
        this.edges = new Map(); // Table of all edges.
        this.faces = new Map(); // Table of all faces.
    };

    Mesh.prototype.addVert = function addVert( vert ) {
        if ( vert instanceof Vert ) {
            var id = vert.id ? vert.id : Math.floor( Math.random() * ( ID_MAX - ID_MIN + 1 ) );
            vert.id = id;
            this.verts.set( id, vert );
            return vert;
        } else {
            throw new TypeError('Not a vert!');
        }
    };
    Mesh.prototype.addEdge = function addEdge( edge ) {
        if ( edge instanceof Edge ) {
            var id = edge.id ? edge.id : Math.floor( Math.random() * ( ID_MAX - ID_MIN + 1 ) );
            edge.id = id;
            this.edges.set( id, edge );
            return edge;
        } else {
            throw new TypeError('Not an edge!');
        }
    };
    Mesh.prototype.addFace = function addFace( face ) {
        if ( face instanceof Face ) {
            var id = face.id ? face.id : Math.floor( Math.random() * ( ID_MAX - ID_MIN + 1 ) );
            face.id = id;
            this.faces.set( id, face );
            return face;
        } else {
            throw new TypeError('Not a face!');
        }
    };
    Mesh.prototype.deleteVert = function addVert( vert ) {
        if ( vert instanceof Vert ) {
            return this.verts.delete( vert.id );
        } else {
            return this.verts.delete( vert );
        }
    };
    Mesh.prototype.deleteEdge = function addEdge( edge ) {
        if ( edge instanceof Edge ) {
            return this.edges.delete( edge.id );
        } else {
            return this.edges.delete( edge );
        }
    };
    Mesh.prototype.deleteFace = function addFace( face ) {
        if ( face instanceof Face ) {
            return this.faces.delete( face.id );
        } else {
            return this.faces.delete( face );
        }
    };

    // Simple mesh extension.
    // Implements element addition via coordinates or adjacent facets.
    // Still non-manifold, but validates edges and faces against degeneration.

    var SimpleMesh = function SimpleMesh() {
        Mesh.call(this);
    };

    SimpleMesh.getCommonEdgeOfVertPair = function getCommonEdgeOfVertPair( vert1, vert2 ) {

        var edges1 = vert1.edges;
        var edges2 = vert2.edges;
        var edgesUnion = new Set(edges1);
        var edgesCommon;
        var edge;

        for ( edge of edges2 ) {
            edgesUnion.add(edge);
        }

        if ( edgesUnion.size == edges1.size + edges2.size ) {
            return new ElementSet();
        } else {
            for ( edge of edges2 ) {
                edgesUnion.delete( edge );
            }
            edgesCommon = new ElementSet( edges1 );
            for ( edge of edgesUnion ) {
                edgesCommon.delete( edge );
            }
            return edgesCommon;
        }

    };
    SimpleMesh.validateEdgeLoop = function validate( edges ) {

        var list = [ ...edges ];

        if ( list.length < 3 ) {
            return false;
        }

        var start;
        var chain;

        var v11 = list[0].vert1;
        var v12 = list[0].vert2;
        var v21 = list[1].vert1;
        var v22 = list[1].vert2;
        if ( v11 === v21 ) {
            start = v12;
            chain = v22;
        } else if ( v11 === v22 ) {
            start = v12;
            chain = v21;
        } else if ( v12 === v21 ) {
            start = v11;
            chain = v22;
        } else if ( v12 === v22 ) {
            start = v11;
            chain = v21;
        } else {
            return false;
        }

        for ( var i = 2; i < list.length; i++ ) {
            v21 = list[i].vert1;
            v22 = list[i].vert2;
            if ( chain === v21 ) {
                chain = v22;
            } else if ( chain === v22 ) {
                chain = v21;
            } else {
            return false;
            }
        }

        if ( chain !== start ) {
            return false;
        }

        return true;

    };

    SimpleMesh.prototype = Object.create( SimpleMesh.prototype );
    SimpleMesh.prototype.addVertByCoords = function addVertByCoords( x, y, z ) {
        var vert = new Vert();
        vert.annotation.x = x ? x : 0;
        vert.annotation.y = y ? y : 0;
        vert.annotation.z = z ? z : 0;
        return Mesh.prototype.addVert.call( this, vert );
    };
    SimpleMesh.prototype.addEdgeByVertPair = function addEdgeByVertPair( vert1, vert2, validate ) {
        if ( ( validate || validate === undefined ) && vert1 === vert2 ) {
            throw new TypeError('Degenerate edge!');
        }
        if ( !this.verts.has( vert1.id ) || !this.verts.has( vert2.id ) ) {
            throw new ReferenceError('Vert not in mesh!');
        }
        if ( ( validate || validate === undefined )  && SimpleMesh.getCommonEdgeOfVertPair.size ) {
            throw new ReferenceError('Vert pair already has edge in common!');
        }
        var edge = new Edge();
        edge.vert1 = vert1;
        edge.vert2 = vert2;
        vert1.edges.add( edge );
        vert2.edges.add( edge );
        return Mesh.prototype.addEdge.call( this, edge );
    };
    SimpleMesh.prototype.addFaceByEdgeLoop = function addFaceByEdgeLoop( edges, validate ) {
        if ( ( validate || validate === undefined )  && !SimpleMesh.validateEdgeLoop( edges ) ) {
            throw new TypeError('Degenerate edge-loop!');
        }
        var face = new Face();
        for ( var edge of edges ) {
            if ( !this.edges.has( edge.id ) ) {
                throw new ReferenceError('Edge not in mesh!');
            }
            edge.faces.add( face );
            face.edges.add( edge );
        }
        return Mesh.prototype.addFace.call( this, face );
    };
    SimpleMesh.prototype.addFaceByVertLoop = function addFaceByVertLoop( verts ) {
        if ( verts.length < 3 || verts.size < 3 ) {
            throw new TypeError('Degenerate vert-loop!');
        }
        var list = [ ...verts ];
        var edges = [];
        var previous = list[0];
        var current;
        var commonEdge;
        for ( var i = 1; i < list.length; i++ ) {
            current = list[i];
            commonEdge = SimpleMesh.getCommonEdgeOfVertPair( previous, current );
            if ( commonEdge.size < 1 ) {
                throw new TypeError('Degenerate vert-loop!');
            }
            edges.push( [ ...commonEdge ][0] );
            previous = current;
        }
        commonEdge = SimpleMesh.getCommonEdgeOfVertPair( list[list.length-1], list[0] );
        if ( commonEdge.size < 1 ) {
            throw new TypeError('Degenerate vert-loop!');
        }
        edges.push( [ ...commonEdge ][0] );
        return this.addFaceByEdgeLoop( edges, false );
    };

    // CommonJS export.

    module.exports = {
        ElementSet:     ElementSet,
        Vert:           Vert,
        Edge:           Edge,
        Face:           Face,
        Mesh:           Mesh,
        SimpleMesh:     SimpleMesh
    };

}).call(this);
<|endoftext|>