DIYPlay
블로그

Blockly, pixi.js 사용해서 블록코딩 그림판 만들기 - 3

DIYPlayer

·

27일 전

blockly
블록코딩
그림판
pixi.js
블록리

이전 포스팅

블록코딩 콘텐츠 제작을 위한 Blockly 개발 시작하기

Blockly, pixi.js 사용해서 블록코딩 그림판 만들기 - 1

Blockly, pixi.js 사용해서 블록코딩 그림판 만들기 - 2

완성된 프로젝트 바로가기

 블록코딩 그림판 결과물 프로젝트

 

이번 포스팅에서는 Blockly의 워크스페이스에 그림판의 그리기 명령을 위한 커스텀 블록을 추가해 보겠습니다.

커스텀 블록 추가

커스텀 블록을 추가하기 위해서는 3가지 작업을 해야 합니다.

 

1. 커스텀 블록 정의 후 목록에 추가

 커스텀 블록의 이름과 겉모습을 만들어주고 사용하는 변수들의 타입이나 이름을 설정합니다. 

 

2. 블록을 자바스크립트로 어떻게 변환할지 스크립트 작성

 커스텀 블록을 문자열 형태의 자바스크립트 코드를 반환해주도록 함수를 작성합니다. 함수의 인자를 통해 블록의 입출력 값을 가져와서 사용할 수 있습니다.

 

3. 툴 박스 목록에 커스텀 블록 추가

마지막으로 워크 스페이스에서 커스텀 블록을 사용할 수 있도록 툴박스에 커스텀 블록을 추가합니다. 커스텀 블록이 어떤 카테고리에 속하는지 정하고 입력 값에 대해서 기본적으로 연결해서 사용할 그림자 블록과 값을 설정할 수 있습니다. 하나의 블록에 대해서 중복으로 툴 박스에 추가 가능합니다.

 

위 순서에 맞게 각 단계를 코드로 구현해보겠습니다.

1. 커스텀 블록 정의

/blocks/paint.js 경로에 파일을 생성 후 커스텀 블록을 정의해서 추가하는 코드를 작성하겠습니다. 

2번과 3번 단계는 우선순위가 중요하지 않지만 1번은 반드시 먼저 선행 되어야 합니다. 파일을 나눠서 작업을 수행할 때에는 1번 단계의 파일이 먼저 실행 되도록 해야 합니다.

 


const pointBlock = {
  'type': 'point_type',
  'message0': 'X %1 Y %2',
  'args0': [
    {
      "type": "field_number",
      "name": "x",
      "value": 0
    },
    {
      "type": "field_number",
      "name": "y",
      "value": 0
    }
  ],
  'output': 'point_type',
  'colour': 270,
  'tooltip': '',
  'helpUrl': '',
}


const drawLine = {
  'type': 'draw_line',
  'message0': '선 그리기\n 시작점 %1 끝점 %2\n 색상 %3 선 두께 %4',
  'args0': [
    {
      'type': 'input_value',
      'name': 'startPoint',
      "check": "point_type"
    },
    {
      'type': 'input_value',
      'name': 'endPoint',
      "check": "point_type"
    },
    {
      'type': 'input_value',
      'name': 'color',
      'check': 'Colour',
    },
    {
      'type': 'input_value',
      'name': 'weight',
      'check': 'Number',
    }
  ],
  'previousStatement': null,
  'nextStatement': null,
  'colour': 160,
  'tooltip': '',
  'helpUrl': '',
};

const blocks = Blockly.common.createBlockDefinitionsFromJsonArray(
    [pointBlock, drawLine);

 

 선 그리기 블록과 선의 시작 점과 끝 점을 입력 받을 수 있게 포인트 블록을 정의하고 추가하는 코드입니다. 블록의 정의는 JSON 객체와 Javascript 함수를 통한 두 가지 방법을 제공합니다. 이 프로젝트에서는 JSON 객체를 사용해 정의하였습니다. 
 

type 필드로 블록의 이름을 설정합니다.

message0 필드에서 블록에 표시되는 문구를 설정할 수 있습니다. 이 때 %n 패턴을 통해 args0 에 설정된 입력 값들을 표시할 수 잇습니다.

args0는 배열 형태로 블록에서 사용하는 인자(입력 값)들을 설정합니다. type으로 입력이 필드 형태인지 블록 형태인지 설정하고 name으로 입력된 인자의 이름을 설정합니다. field_ 형태의 입력인 경우 디폴트 값을 설정할 수 있습니다.

previousStatementnextStatement 는 null 로 설정하면 각각 위 아래에 블록을 순차적으로 실행하도록 연결할 수 있게 합니다. 

output은 반환 되는 코드가 값으로 사용되는 경우 값의 타입으로 지정해 줍니다.

 

나머지 필요한 블록도 모두 정의해 주겠습니다.

const pointBlock = {
  'type': 'point_type',
  'message0': 'X %1 Y %2',
  'args0': [
    {
      "type": "field_number",
      "name": "x",
      "value": 0
    },
    {
      "type": "field_number",
      "name": "y",
      "value": 0
    }
  ],
  'output': 'point_type',
  'colour': 270,
  'tooltip': '',
  'helpUrl': '',
}

const drawLine = {
  'type': 'draw_line',
  'message0': '선 그리기\n 시작점 %1 끝점 %2\n 색상 %3 선 두께 %4',
  'args0': [
    {
      'type': 'input_value',
      'name': 'startPoint',
      "check": "point_type"
    },
    {
      'type': 'input_value',
      'name': 'endPoint',
      "check": "point_type"
    },
    {
      'type': 'input_value',
      'name': 'color',
      'check': 'Colour',
    },
    {
      'type': 'input_value',
      'name': 'weight',
      'check': 'Number',
    }
  ],
  'previousStatement': null,
  'nextStatement': null,
  'colour': 160,
  'tooltip': '',
  'helpUrl': '',
};

const drawCircle = {
  'type': 'draw_circle',
  'message0': '원 그리기\n 중심 점 %2 반지름 %3\n채우기 %1 색상 %4 선 두께 %5',
  'args0': [
    {
      "type": "field_checkbox",
      "name": "fill",
      "checked": false
    },
    {
      'type': 'input_value',
      'name': 'point',
      "check": "point_type"
    },
    {
      'type': 'input_value',
      'name': 'radius',
      "check": "Number"
    },
    {
      'type': 'input_value',
      'name': 'color',
      'check': 'Colour',
    },
    {
      'type': 'input_value',
      'name': 'weight',
      'check': 'Number',
    }
  ],
  'previousStatement': null,
  'nextStatement': null,
  'colour': 160,
  'tooltip': '',
  'helpUrl': '',
};

const drawRect = {
  'type': 'draw_rect',
  'message0': '사각형 그리기\n 시작점 %2 넓이 %3 높이 %4\n채우기 %1 색상 %5 선 두께 %6',
  'args0': [
    {
      "type": "field_checkbox",
      "name": "fill",
      "checked": false
    },
    {
      'type': 'input_value',
      'name': 'startPoint',
      "check": "point_type"
    },
    {
      'type': 'input_value',
      'name': 'width',
      "check": "Number"
    },
    {
      'type': 'input_value',
      'name': 'height',
      "check": "Number"
    },
    {
      'type': 'input_value',
      'name': 'color',
      'check': 'Colour',
    },
    {
      'type': 'input_value',
      'name': 'weight',
      'check': 'Number',
    }
  ],
  'previousStatement': null,
  'nextStatement': null,
  'colour': 160,
  'tooltip': '',
  'helpUrl': '',
}

const drawPolygon = {
  'type': 'draw_polygon',
  'message0': '다각형 그리기\n 점 리스트 %2\n 채우기 %1 색상 %3 선두께 %4',
  'args0': [
    {
      "type": "field_checkbox",
      "name": "fill",
      "checked": false
    },
    {
      'type': 'input_value',
      'name': 'points',
      "check": "Array"
    },
    {
      'type': 'input_value',
      'name': 'color',
      'check': 'Colour',
    },
    {
      'type': 'input_value',
      'name': 'weight',
      'check': 'Number',
    }
  ],
  'previousStatement': null,
  'nextStatement': null,
  'colour': 160,
  'tooltip': '',
  'helpUrl': '',
}

const blocks = Blockly.common.createBlockDefinitionsFromJsonArray(
    [pointBlock, drawLine, drawCircle, drawRect, drawPolygon]);

정의된 블록은 Blockly.common.createBlockDefinitionsFromJsonArray API를 통해 생성하여 blocks 변수에 저장 해놓았다가 프로그램 진입점에서 아래 코드를 통해 워크스페이스에 추가하여 줍니다. 

Object.assign(Blockly.JavaScript.forBlock, forBlock);

 

2. 자바스크립트 코드 변환 함수 작성

/generators/javascript.js  경로로 파일을 작성합니다.

const forBlock = Object.create(null);

forBlock['point_type'] = function (
  block, //블록에 설정된 값이나 입력된 블록의 값을 가져올 수 있음
  generator
) {

  //필드 형태의 입력 값을 가져오는 방법
  const x = block.getFieldValue('x');
  const y = block.getFieldValue('y');

  //블록에 대한 함수 호출을 생성합니다.
  const code = '(' + JSON.stringify({ x: x, y: y }) + ')';
  return [code, Blockly.JavaScript.ORDER_NONE];
}

forBlock['draw_line'] = function (
  block, //블록에 설정된 값이나 입력된 블록의 값을 가져올 수 있음
  generator
) {

  //블록 형태의 입력 값을 가져오는 방법
  const startPoint = generator.valueToCode(
    block,
    'startPoint',
    Blockly.JavaScript.ORDER_NONE);

  const endPoint =
    generator.valueToCode(
        block,
        'endPoint',
        Blockly.JavaScript.ORDER_NONE);

  const color = generator.valueToCode(
    block, 'color', Blockly.JavaScript.ORDER_NONE) || undefined;

  const weight = generator.valueToCode(
    block, 'weight', Blockly.JavaScript.ORDER_NONE) || undefined;


  //블록에 대한 함수 호출을 생성합니다.
  const code = `paint.drawLine(${startPoint}, ${endPoint}, ${color}, ${weight});\n`;
  return code;
}

forBlock 이름으로 빈 객체를 만들어 정의된 블록의 이름으로 각각 함수를 정의해 줍니다.

함수의 인자로는 커스텀 블록의 입력 값에 접근할 수 있는 block 과 입력 값이 블록인 경우 입력 블록을 코드로 변환해주는 generator 를 인자로 갖습니다. generator로 값을 코드로 변환하는 경우 block과 입력의 이름과 함께 우선순위를 입력해주어야 합니다. 이번 프로젝트에서는 입력값의 우선순위가 따로 없어서 모두 Blockly.JavaScript.ORDER_NONE 를 사용하였습니다.

 

나머지 블록들도 모두 변환 함수를 작성해주겠습니다.

forBlock['draw_circle'] = function (
  block, 
  generator
) {
  const point = generator.valueToCode(
    block,
    'point',
    Blockly.JavaScript.ORDER_NONE);

  const fill = block.getFieldValue('fill');


  const color = generator.valueToCode(
    block, 'color', Blockly.JavaScript.ORDER_NONE) || undefined;

  const weight = generator.valueToCode(
    block, 'weight', Blockly.JavaScript.ORDER_NONE) || undefined;

  const radius = generator.valueToCode(
    block, 'radius', Blockly.JavaScript.ORDER_NONE) || undefined;

  const func = fill === 'TRUE' ? 'drawCircleFill' : 'drawCircle';


  const code = `paint.${func}(${point}, ${radius}, ${color}, ${weight});\n`;
  return code;
}

forBlock['draw_rect'] = function (
  block,
  generator
) {
  const startPoint = generator.valueToCode(
    block,
    'startPoint',
    Blockly.JavaScript.ORDER_NONE);


  const fill = block.getFieldValue('fill');

  const width = generator.valueToCode(
    block, 'width', Blockly.JavaScript.ORDER_NONE) || undefined;

  const height = generator.valueToCode(
    block, 'height', Blockly.JavaScript.ORDER_NONE) || undefined;

  const color = generator.valueToCode(
    block, 'color', Blockly.JavaScript.ORDER_NONE) || undefined;

  const weight = generator.valueToCode(
    block, 'weight', Blockly.JavaScript.ORDER_NONE) || undefined;

  const func = fill === 'TRUE' ? 'drawRectFill' : 'drawRect';


  const code = `paint.${func}(${startPoint}, ${width}, ${height}, ${color}, ${weight});\n`;
  return code;
}

forBlock['draw_polygon'] = function (
  block, 
  generator
) {
  const pointsStr = generator.valueToCode(
    block,
    'points',
    Blockly.JavaScript.ORDER_NONE);

  if(!pointsStr) return '';

  const fill = block.getFieldValue('fill');

  const color = generator.valueToCode(
    block, 'color', Blockly.JavaScript.ORDER_NONE) || undefined;

  const weight = generator.valueToCode(
    block, 'weight', Blockly.JavaScript.ORDER_NONE) || undefined;

  const func = fill === 'TRUE' ? 'drawPolygonFill' : 'drawPolygon';


  const code = `paint.${func}(${pointsStr}, ${color}, ${weight});\n`;
  return code;
}

 

forBlock 객체에 작성한 변환 함수는 프로그램 진입점에서 아래 코드를 통해 추가해 줍니다.

Object.assign(Blockly.JavaScript.forBlock, forBlock);

 

3. 툴 박스에 추가

/blocks/paintToolbox.js 경로로 파일을 만들고 툴박스에 커스텀블록들을 추가해 보겠습니다.

const paintToolBox = {
  'kind': 'category',
  'name': '그리기',
  'colour': 160,
  'contents': [
  	//...
  ]
}

먼저 그리기 이름의 카테고리를 만들어 줍니다. 

{
  'kind': 'block',
  'type': 'point_type',

},
{
  'kind': 'block',
  'type': 'draw_line',
  'inputs': {
    'startPoint': {
      'shadow': {
        'type': 'point_type',
        'fields': {
          'x': '50',
          'y': '50',
        }
      }
    },
    'endPoint': {
      'shadow': {
        'type': 'point_type',
        'fields': {
          'x': '150',
          'y': '50',
        }
      }
    },
    'color': {
      'shadow': {
        'type': 'colour_picker',
        'fields': {
          'COLOUR': '#00ff00',
        },
      },
    },
    'weight': {
      'shadow': {
        'type': 'math_number',
        'fields': {
          'NUM': '2',
        },
      },
    },
  },
},

카테고리에 해당하는 블록들은 contents 에 추가해 줍니다. point_type 블록과 draw_line 블록을 추가해 주었습니다. point_type 블록은 입력을 필드 형태로만 받고 기본 값은 블록을 정의할 때 설정해주었으므로 간단합니다. 

draw_line  블록은 다른 블록들은 입력 값으로 사용합니다. 만약 연결된 블록이 없는 상태라면 변환 함수에서 값을 불러올 때 에러가 발생할 것입니다. 또한 유저가 블록을 사용할 때 어떤 블록을 연결해야 할 지 몰라서 어려워 할지도 모릅니다. 이를 위해 inputs 에 각 입력 블록들에 대한 그림자 블록을 추가할 수 있습니다. 입력 블록의 이름으로 키를 생성하고 shadow 라고 다시 키를 생성한 후에 각 블록에 맞는 블록의 타입과 기본 값을 설정해 줍니다.

 

필요한 모든 블록들을 모두 추가 주겠습니다.

const paintToolBox = {
  'kind': 'category',
  'name': '그리기',
  'colour': 160,
  'contents': [
    {
      'kind': 'block',
      'type': 'lists_create_with',
      "extraState": {
        "itemCount": 3, // or whatever the count is
      },
      'inputs': {
        "ADD0": {
          'shadow': {
            'type': 'point_type',
            'fields': {
              'x': '300',
              'y': '25',
            }
          }
        },
        "ADD1": {
          'shadow': {
            'type': 'point_type',
            'fields': {
              'x': '330',
              'y': '75',
            }
          }
        },
        "ADD2": {
          'shadow': {
            'type': 'point_type',
            'fields': {
              'x': '270',
              'y': '75',
            }
          }
        },
      }
    },
    {
      'kind': 'block',
      'type': 'point_type',
    },
    {
      'kind': 'block',
      'type': 'draw_line',
      'inputs': {
        'startPoint': {
          'shadow': {
            'type': 'point_type',
            'fields': {
              'x': '50',
              'y': '50',
            }
          }
        },
        'endPoint': {
          'shadow': {
            'type': 'point_type',
            'fields': {
              'x': '150',
              'y': '50',
            }
          }
        },
        'color': {
          'shadow': {
            'type': 'colour_picker',
            'fields': {
              'COLOUR': '#00ff00',
            },
          },
        },
        'weight': {
          'shadow': {
            'type': 'math_number',
            'fields': {
              'NUM': '2',
            },
          },
        },
      },
    },
    {
      'kind': 'block',
      'type': 'draw_circle',
      'inputs': {
        'point': {
          'shadow': {
            'type': 'point_type',
            'fields': {
              'x': '200',
              'y': '150',
            }
          }
        },
        'radius': {
          'shadow': {
            'type': 'math_number',
            'fields': {
              'NUM': '100',
            },
          },
        },
        'color': {
          'shadow': {
            'type': 'colour_picker',
            'fields': {
              'COLOUR': '#ff0000',
            },
          },
        },
        'weight': {
          'shadow': {
            'type': 'math_number',
            'fields': {
              'NUM': '2',
            },
          },
        },
      },
    },
    {
      'kind': 'block',
      'type': 'draw_rect',
      'inputs': {
        'startPoint': {
          'shadow': {
            'type': 'point_type',
            'fields': {
              'x': '100',
              'y': '100',
            }
          }
        },
        'width': {
          'shadow': {
            'type': 'math_number',
            'fields': {
              'NUM': '100',
            },
          },
        },
        'height': {
          'shadow': {
            'type': 'math_number',
            'fields': {
              'NUM': '100',
            },
          },
        },
        'color': {
          'shadow': {
            'type': 'colour_picker',
            'fields': {
              'COLOUR': '#0000ff',
            },
          },
        },
        'weight': {
          'shadow': {
            'type': 'math_number',
            'fields': {
              'NUM': '2',
            },
          },
        },
      },
    },
    {
      'kind': 'block',
      'type': 'draw_polygon',
      'inputs': {
        'points': {
          'shadow': {
            'type': 'lists_create_with',
            "extraState": {
              "itemCount": 3, // or whatever the count is
            },
            'inputs': {
              "ADD0": {
                'shadow': {
                  'type': 'point_type',
                  'fields': {
                    'x': '300',
                    'y': '25',
                  }
                }
              },
              "ADD1": {
                'shadow': {
                  'type': 'point_type',
                  'fields': {
                    'x': '330',
                    'y': '75',
                  }
                }
              },
              "ADD2": {
                'shadow': {
                  'type': 'point_type',
                  'fields': {
                    'x': '270',
                    'y': '75',
                  }
                }
              },
            }
          },
        },
        'color': {
          'shadow': {
            'type': 'colour_picker',
            'fields': {
              'COLOUR': '#880088',
            },
          },
        },
        'weight': {
          'shadow': {
            'type': 'math_number',
            'fields': {
              'NUM': '2',
            },
          },
        },
      },
    }
  ],
};

 

기존 툴박스 목록 생성하는 toolbox.js 에 새로 작성한 그리기 카테고리를 추가해 주겠습니다.

const toolbox = {
  'kind': 'categoryToolbox',
  'contents': [
  
    //...
    
    paintToolBox,
    
    {
      'kind': 'sep',
    },
    {
      'kind': 'category',
      'name': 'Variables',
      'categorystyle': 'variable_category',
      'custom': 'VARIABLE',
    },
    {
      'kind': 'category',
      'name': 'Functions',
      'categorystyle': 'procedure_category',
      'custom': 'PROCEDURE',
    },
  ],
};

 

4. index.js 파일 수정

프로그램의 진입점인 index.js 파일을 수정해서 커스텀 블록을 워크스페이스에 추가하고 그림판의 동작을 제어해 줍니다.

//...

// 추가된 text 추가 블록을 사용할 수 있게 Blockly 정의에 추가
Blockly.common.defineBlocks(blocks);
Object.assign(Blockly.JavaScript.forBlock, forBlock);

//그림판 프로그램 생성
const paintApp = new PaintApp(outputDiv);

const runCode = () => {
  // 워크스페이스를 자바스크립트 코드로 변환
  const code = Blockly.JavaScript.workspaceToCode(ws);

  //코드영역에 자바스크립트 코드 출력
  if (codeDiv) codeDiv.textContent = code;
  
};

//...

 

 

변환 함수에서 자바스크립트 코드를 반환할 때 모든 명령은 paint.함수명 형태로 작성하였습니다. 

‘paint’ 이름의 매개 변수 갖고 code를 내용으로 하는 함수를 생성합니다.

const fn = new Function('paint',code);

생성된 함수를 호출할 때 인자로 paintApp 를 전달합니다.

fn(paintApp);

 

 

runCode  함수에 아래 코드를 추가합니다.

const runCode = () => {
  //...
  
  paintApp.clear();
  const fn = new Function('paint',code);
  fn(paintApp);
  paintApp.run();
  
};

 

 

블록을 배치하고 실행해본 결과 입니다.

 

프로젝트 바로가기

https://diyplay.co.kr/project/view/13

https://diyplay.co.kr/editor/edit/13

 

다음 글

2D 벡터의 내적과 외적 정리

댓글 0
    서비스 이용약관|개인정보 보호정책

    Copyright © DIYPlay All rights reserved.