├── .gitignore ├── README.md ├── README_CN.md ├── config-overrides.js ├── package.json ├── public ├── favicon.ico ├── index.html ├── manifest.json └── media │ └── icons │ ├── arrow.svg │ ├── arrow_button.svg │ ├── control_forever.svg │ ├── control_repeat.svg │ ├── control_stop.svg │ ├── control_wait.svg │ ├── event_broadcast_blue.svg │ ├── event_broadcast_coral.svg │ ├── event_broadcast_green.svg │ ├── event_broadcast_magenta.svg │ ├── event_broadcast_orange.svg │ ├── event_broadcast_purple.svg │ ├── event_when-broadcast-received_blue.svg │ ├── event_when-broadcast-received_coral.svg │ ├── event_when-broadcast-received_green.svg │ ├── event_when-broadcast-received_magenta.svg │ ├── event_when-broadcast-received_orange.svg │ ├── event_when-broadcast-received_purple.svg │ ├── event_whenflagclicked.svg │ ├── remove.svg │ ├── set-led_blue.svg │ ├── set-led_coral.svg │ ├── set-led_green.svg │ ├── set-led_magenta.svg │ ├── set-led_mystery.svg │ ├── set-led_orange.svg │ ├── set-led_purple.svg │ ├── set-led_white.svg │ ├── set-led_yellow.svg │ ├── wedo_motor-clockwise.svg │ ├── wedo_motor-counterclockwise.svg │ ├── wedo_motor-speed_fast.svg │ ├── wedo_motor-speed_med.svg │ ├── wedo_motor-speed_slow.svg │ ├── wedo_when-distance_close.svg │ ├── wedo_when-tilt-backward.svg │ ├── wedo_when-tilt-forward.svg │ ├── wedo_when-tilt-left.svg │ ├── wedo_when-tilt-right.svg │ └── wedo_when-tilt.svg └── src ├── App.css ├── App.js ├── App.test.js ├── BlockEditor.js ├── CodeBuilder.js ├── arduino.png ├── index.css ├── index.js ├── logo.svg ├── micropy.png ├── python.png ├── s3ext.png └── serviceWorker.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Here is [Scratch3](https://scratch.mit.edu) extension generator based on [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ![image](https://user-images.githubusercontent.com/3390845/53645159-3d5e3300-3c73-11e9-9027-071660c28bfb.png) 4 | 5 | 6 | ## The Scratch3 extension generator 7 | 8 | As scratch3 gets better and better, we find that many users want to implement their own plugins. But writing a plugin for scratch3 is not a simple matter, it requires a solid JavaScript development capability; for professional JavaScript programmers, writing a scratch3 plugin is just tedious and waste of time. The purpose of this webapp is to let you complete your scratch3 plugin framework code in 10 minutes. Hope you guys like it~ 9 | 10 | [中文使用说明](./README_CN.md) 11 | 12 | ## Instructions 13 | 14 | ### Step1 15 | 16 | Open:[https://kittenbot.github.io/scratch3-extension/](https://kittenbot.github.io/scratch3-extension/) 17 | 18 | ### Step2 19 | 20 | Give your extension a name and ID. Note that the extension ID needs to be in ASCII characters and cannot contain spaces or special strings. The plugin ID should be unique in the runtime context of scratch3. 21 | 22 | You can also choose prefered color for your extension. 23 | 24 | ![image](https://user-images.githubusercontent.com/3390845/53679660-5cf46a80-3d0a-11e9-96f1-befbda4b9372.png) 25 | 26 | ### Step3 27 | 28 | Give your extension an icon. The icon is recommended to use less than 200x200 pixels square png or svg images. 29 | 30 | ![image](https://user-images.githubusercontent.com/3390845/53679671-9927cb00-3d0a-11e9-8412-9efc5038dfb5.png) 31 | 32 | ### Step4 33 | 34 | Then we will create a block, click on the `Add Function Block`. Then click on the `Add Text Variable` in the pop-up modal box and name the variable to `WORD`. Note that the variable name needs to be ASCII letters, and cannot contain special characters, and recommend all uppercase. 35 | 36 | Finally we have to change our block ID, the block ID needs to be unique in the current plugin. Here we will name the ID to `sayhello`. 37 | 38 | - Note that the **extension ID** also needs to be an ASCII string and cannot contain a special characters. 39 | 40 | ![image](https://user-images.githubusercontent.com/3390845/53679707-089dba80-3d0b-11e9-9251-2d07f37a5114.png) 41 | 42 | ### Step5 43 | 44 | You can click on the `Generate Preview` in the upper right corner to see the effect of our plugin loading in scratch3. 45 | 46 | ![image](https://user-images.githubusercontent.com/3390845/53679761-e35d7c00-3d0b-11e9-9df5-27a95c9ef18c.png) 47 | 48 | 49 | ### Step6 50 | 51 | Finally click on the `export index.js` in the lower right corner to export the plugin source code. 52 | 53 | For standard scratch3, please load index.js into the extension of `scratch-vm`. 54 | 55 | ------------------- 56 | 57 | The following steps are only valid for Kittenblock. You can download the latest Kittenblock at [https://www.kittenbot.cn/software/] (https://www.kittenbot.cn/software/). 58 | 59 | ### Step7 60 | 61 | Please create a extension folder in the `extensions` directory of the Kittenblock root directory. Here we will name it `sayhello`. Copy the generated `index.js` to this directory. 62 | 63 | ![image](https://user-images.githubusercontent.com/3390845/53679811-bbbae380-3d0c-11e9-9143-b6b262a0b3cf.png) 64 | 65 | ### Step8 66 | 67 | Create a file named `extension.json` under the changed folder, which contains the following contents: 68 | 69 | { 70 | "name": "Say Hello", 71 | "type": "scratch3", 72 | "image": "logo.png" 73 | } 74 | 75 | Then find a picture you like as the main image of the extension, name it `logo.png`, and put it in the folder. Finally, our `sayhello` folder has the following three files. 76 | 77 | ![image](https://user-images.githubusercontent.com/3390845/53679853-7d71f400-3d0d-11e9-872a-b20a57d59115.png) 78 | 79 | ### Step9 80 | 81 | Open Kittenblock and select `Load External Plugin` in the lower left corner then find the plugin we just added. 82 | 83 | ![image](https://user-images.githubusercontent.com/3390845/53679874-c164f900-3d0d-11e9-8391-6786c5200341.png) 84 | 85 | The final effect 86 | 87 | ![image](https://user-images.githubusercontent.com/3390845/53679884-ec4f4d00-3d0d-11e9-95cf-55e9e0db67a4.png) 88 | 89 | ------------------- 90 | 91 | You only need to change the corresponding block execution code in `index.js` to implement the function of a block. 92 | 93 | Please keep tuned to our upcoming updates. -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | 这是[Scratch3](https://scratch.mit.edu) 的插件生成器webapp, 基于[Create React App](https://github.com/facebook/create-react-app)开发,请访问[https://kittenbot.github.io/scratch3-extension/](https://kittenbot.github.io/scratch3-extension/)使用. 2 | 3 | ![image](https://user-images.githubusercontent.com/3390845/53645159-3d5e3300-3c73-11e9-9027-071660c28bfb.png) 4 | 5 | 6 | ## Scratch3插件生成器 7 | 8 | 随着scratch3越来越完善,我们发现很多用户都想自己实现自己的插件。但是编写scratch3的插件并不是一件简单的事情,这需要比较扎实的JavaScript开发能力;而对于专业的JavaScript程序员来说,写scratch3插件又很浪费时间。这个webapp的目的就是可以让你在10分钟内完成自己的scratch3插件框架代码。希望大家喜欢~ 9 | 10 | ## 使用方法 11 | 12 | ### Step1 13 | 14 | 打开网页:[https://kittenbot.github.io/scratch3-extension/](https://kittenbot.github.io/scratch3-extension/) 15 | 16 | ### Step2 17 | 18 | 给你的插件取名字和ID。注意插件ID需要全英文,并且不能包含空格和特殊字符串。插件ID在scratch3的运行环境中全局唯一。 19 | 20 | 之后可以给你的插件选取喜欢的颜色。 21 | 22 | ![image](https://user-images.githubusercontent.com/3390845/53679660-5cf46a80-3d0a-11e9-96f1-befbda4b9372.png) 23 | 24 | ### Step3 25 | 26 | 给插件选择你喜欢的图标,图标建议使用200x200像素以内的正方形png或svg图片。 27 | 28 | ![image](https://user-images.githubusercontent.com/3390845/53679671-9927cb00-3d0a-11e9-8412-9efc5038dfb5.png) 29 | 30 | ### Step4 31 | 32 | 之后我们来新建一个积木块,点击`添加函数方块`. 之后在弹出的模态框中点击`添加文字变量`,并修改变量的名字为`WORD`. 注意变量的名字需要为英文字母,并且不能包含特殊字符串, 并推荐全部大写. 33 | 34 | 最后我们还要更改我们的积木块ID, 积木块ID需要在当前插件中全局唯一. 这里我们将插件ID命名为`sayhello`. 35 | 36 | - 注意**插件ID**同样需要为英文字母,并且不能包含特殊字符串 37 | 38 | ![image](https://user-images.githubusercontent.com/3390845/53679707-089dba80-3d0b-11e9-9251-2d07f37a5114.png) 39 | 40 | ### Step5 41 | 42 | 大家可以点击右上角的`生成预览`查看我们插件在scratch3中加载的效果。 43 | 44 | ![image](https://user-images.githubusercontent.com/3390845/53679761-e35d7c00-3d0b-11e9-9df5-27a95c9ef18c.png) 45 | 46 | 47 | ### Step6 48 | 49 | 最后点击右下角的`export index.js`导出插件源代码. 50 | 51 | 对于标准的scratch3请将index.js加载到scratch-vm的extension中就行了. 52 | 53 | ------------------- 54 | 55 | 以下步骤只对Kittenblock有效, 大家可以前往[https://www.kittenbot.cn/software/](https://www.kittenbot.cn/software/)下载最新的Kittenblock. 56 | 57 | ### Step7 58 | 59 | 请到Kittenblock的安装目录的`extension`目录下新建一个插件的文件夹,这里我们命名为`sayhello`. 并将刚刚生成的`index.js`拷贝到该目录下. 60 | 61 | ![image](https://user-images.githubusercontent.com/3390845/53679811-bbbae380-3d0c-11e9-9143-b6b262a0b3cf.png) 62 | 63 | ### Step8 64 | 65 | 在该文件夹下面建立一个名为`extension.json`的文件,里面放入如下的内容: 66 | 67 | { 68 | "name": "Say Hello", 69 | "type": "scratch3", 70 | "image": "logo.png" 71 | } 72 | 73 | 之后找一张你喜欢的图片作为插件的主图片,命名为`logo.png`,并放入该文件夹下. 最后我们的`sayhello`文件夹下有如下三个文件. 74 | 75 | ![image](https://user-images.githubusercontent.com/3390845/53679853-7d71f400-3d0d-11e9-872a-b20a57d59115.png) 76 | 77 | ### Step9 78 | 79 | 打开Kittenblock,并在左下角选择加载外部插件,可以找到我们刚刚加入的插件。 80 | 81 | ![image](https://user-images.githubusercontent.com/3390845/53679874-c164f900-3d0d-11e9-8391-6786c5200341.png) 82 | 83 | 最终效果 84 | 85 | ![image](https://user-images.githubusercontent.com/3390845/53679884-ec4f4d00-3d0d-11e9-95cf-55e9e0db67a4.png) 86 | 87 | ------------------- 88 | 89 | 大家只需要在`index.js`中更改对应的积木执行代码就能实现具体积木的功能了。 90 | 91 | -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | /* config-overrides.js */ 2 | const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); 3 | 4 | module.exports = function override(config, env) { 5 | if (!config.plugins) { 6 | config.plugins = []; 7 | } 8 | config.plugins.push( 9 | new MonacoWebpackPlugin() 10 | ); 11 | return config; 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "s3ext-scaffold", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://kittenbot.github.io/scratch3-extension", 6 | "dependencies": { 7 | "antd": "^3.13.6", 8 | "lodash.bindall": "^4.4.0", 9 | "react": "^16.8.3", 10 | "react-color": "^2.17.0", 11 | "react-dom": "^16.8.3", 12 | "react-localization": "^1.0.13", 13 | "react-monaco-editor": "^0.25.1", 14 | "react-scripts": "2.1.5", 15 | "scratch-blocks": "0.1.0-prerelease.1549990124" 16 | }, 17 | "scripts": { 18 | "start": "react-app-rewired start", 19 | "build": "react-app-rewired build", 20 | "test": "react-app-rewired test", 21 | "eject": "react-scripts eject", 22 | "deploy": "gh-pages -d build" 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app" 26 | }, 27 | "browserslist": [ 28 | ">0.2%", 29 | "not dead", 30 | "not ie <= 11", 31 | "not op_mini all" 32 | ], 33 | "devDependencies": { 34 | "gh-pages": "^2.0.1", 35 | "monaco-editor-webpack-plugin": "^1.7.0", 36 | "react-app-rewired": "^2.1.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/scratch3-extension/51e0079ac70dc979f03105001e5e04268459b06e/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | Scratch3 Extension Generator 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Scratch3Ext", 3 | "name": "Create Scratch3 extension in an easy way", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/media/icons/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | arrow 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/media/icons/arrow_button.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/media/icons/control_forever.svg: -------------------------------------------------------------------------------- 1 | control_forever -------------------------------------------------------------------------------- /public/media/icons/control_repeat.svg: -------------------------------------------------------------------------------- 1 | control_repeat -------------------------------------------------------------------------------- /public/media/icons/control_stop.svg: -------------------------------------------------------------------------------- 1 | control_stop -------------------------------------------------------------------------------- /public/media/icons/control_wait.svg: -------------------------------------------------------------------------------- 1 | wait -------------------------------------------------------------------------------- /public/media/icons/event_broadcast_blue.svg: -------------------------------------------------------------------------------- 1 | event_broadcast_blue -------------------------------------------------------------------------------- /public/media/icons/event_broadcast_coral.svg: -------------------------------------------------------------------------------- 1 | event_broadcast_coral -------------------------------------------------------------------------------- /public/media/icons/event_broadcast_green.svg: -------------------------------------------------------------------------------- 1 | event_broadcast_green -------------------------------------------------------------------------------- /public/media/icons/event_broadcast_magenta.svg: -------------------------------------------------------------------------------- 1 | event_broadcast_magenta -------------------------------------------------------------------------------- /public/media/icons/event_broadcast_orange.svg: -------------------------------------------------------------------------------- 1 | event_broadcast_orange -------------------------------------------------------------------------------- /public/media/icons/event_broadcast_purple.svg: -------------------------------------------------------------------------------- 1 | send-message-purple -------------------------------------------------------------------------------- /public/media/icons/event_when-broadcast-received_blue.svg: -------------------------------------------------------------------------------- 1 | LetterGet_Blue -------------------------------------------------------------------------------- /public/media/icons/event_when-broadcast-received_coral.svg: -------------------------------------------------------------------------------- 1 | LetterGet_Coral -------------------------------------------------------------------------------- /public/media/icons/event_when-broadcast-received_green.svg: -------------------------------------------------------------------------------- 1 | LetterGet_Green -------------------------------------------------------------------------------- /public/media/icons/event_when-broadcast-received_magenta.svg: -------------------------------------------------------------------------------- 1 | LetterGet_Magenta -------------------------------------------------------------------------------- /public/media/icons/event_when-broadcast-received_orange.svg: -------------------------------------------------------------------------------- 1 | LetterGet_Orange -------------------------------------------------------------------------------- /public/media/icons/event_when-broadcast-received_purple.svg: -------------------------------------------------------------------------------- 1 | LetterGet_Purple -------------------------------------------------------------------------------- /public/media/icons/event_whenflagclicked.svg: -------------------------------------------------------------------------------- 1 | greenflag -------------------------------------------------------------------------------- /public/media/icons/remove.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | delete-argument v2 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/media/icons/set-led_blue.svg: -------------------------------------------------------------------------------- 1 | set-led_blue -------------------------------------------------------------------------------- /public/media/icons/set-led_coral.svg: -------------------------------------------------------------------------------- 1 | set-led_coral -------------------------------------------------------------------------------- /public/media/icons/set-led_green.svg: -------------------------------------------------------------------------------- 1 | set-led_green -------------------------------------------------------------------------------- /public/media/icons/set-led_magenta.svg: -------------------------------------------------------------------------------- 1 | set-led-magenta -------------------------------------------------------------------------------- /public/media/icons/set-led_mystery.svg: -------------------------------------------------------------------------------- 1 | set-led-mystery -------------------------------------------------------------------------------- /public/media/icons/set-led_orange.svg: -------------------------------------------------------------------------------- 1 | set-led-orange -------------------------------------------------------------------------------- /public/media/icons/set-led_purple.svg: -------------------------------------------------------------------------------- 1 | set-led-purple -------------------------------------------------------------------------------- /public/media/icons/set-led_white.svg: -------------------------------------------------------------------------------- 1 | set-led-white -------------------------------------------------------------------------------- /public/media/icons/set-led_yellow.svg: -------------------------------------------------------------------------------- 1 | set-led-yellow -------------------------------------------------------------------------------- /public/media/icons/wedo_motor-clockwise.svg: -------------------------------------------------------------------------------- 1 | wedo_motorclockwise -------------------------------------------------------------------------------- /public/media/icons/wedo_motor-counterclockwise.svg: -------------------------------------------------------------------------------- 1 | wedo_motorclockwise -------------------------------------------------------------------------------- /public/media/icons/wedo_motor-speed_fast.svg: -------------------------------------------------------------------------------- 1 | set-motor-speed_fast -------------------------------------------------------------------------------- /public/media/icons/wedo_motor-speed_med.svg: -------------------------------------------------------------------------------- 1 | set-motor-speed_med -------------------------------------------------------------------------------- /public/media/icons/wedo_motor-speed_slow.svg: -------------------------------------------------------------------------------- 1 | set-motor-speed_slow -------------------------------------------------------------------------------- /public/media/icons/wedo_when-distance_close.svg: -------------------------------------------------------------------------------- 1 | wedo_whendistanceclose -------------------------------------------------------------------------------- /public/media/icons/wedo_when-tilt-backward.svg: -------------------------------------------------------------------------------- 1 | wedo_whentiltbackward -------------------------------------------------------------------------------- /public/media/icons/wedo_when-tilt-forward.svg: -------------------------------------------------------------------------------- 1 | start-when-tilted-forward -------------------------------------------------------------------------------- /public/media/icons/wedo_when-tilt-left.svg: -------------------------------------------------------------------------------- 1 | start-when-tilted-left -------------------------------------------------------------------------------- /public/media/icons/wedo_when-tilt-right.svg: -------------------------------------------------------------------------------- 1 | start-when-tilted-right -------------------------------------------------------------------------------- /public/media/icons/wedo_when-tilt.svg: -------------------------------------------------------------------------------- 1 | start-when-tilted-any -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | @import '~antd/dist/antd.css'; 2 | 3 | .App { 4 | text-align: center; 5 | } 6 | 7 | .App-logo { 8 | animation: App-logo-spin infinite 20s linear; 9 | height: 40vmin; 10 | pointer-events: none; 11 | } 12 | 13 | .App-header { 14 | background-color: #282c34; 15 | min-height: 100vh; 16 | display: flex; 17 | flex-direction: column; 18 | align-items: center; 19 | justify-content: center; 20 | font-size: calc(10px + 2vmin); 21 | color: white; 22 | } 23 | 24 | .App-link { 25 | color: #61dafb; 26 | } 27 | 28 | @keyframes App-logo-spin { 29 | from { 30 | transform: rotate(0deg); 31 | } 32 | to { 33 | transform: rotate(360deg); 34 | } 35 | } 36 | 37 | .radio-img { 38 | width: 40px; 39 | } 40 | 41 | .trigger { 42 | font-size: 18px; 43 | line-height: 64px; 44 | padding: 0 24px; 45 | cursor: pointer; 46 | transition: color .3s; 47 | } 48 | 49 | .trigger:hover { 50 | color: #1890ff; 51 | } 52 | 53 | .logo { 54 | height: 32px; 55 | margin: 16px; 56 | } 57 | 58 | .icon-img { 59 | max-width: 100px; 60 | margin: 10px; 61 | } 62 | 63 | .color-display{ 64 | width: 30px; 65 | height: 30px; 66 | border-radius: 15px; 67 | border: 1px #5B5B5B solid 68 | } 69 | 70 | .color-cover{ 71 | position: fixed; 72 | top: 0px; 73 | right: 0px; 74 | bottom: 0px; 75 | left: 0px; 76 | } 77 | 78 | .config-row { 79 | margin: 10px; 80 | } 81 | 82 | .btn-wrap { 83 | margin-top: 5px; 84 | } 85 | 86 | .btn-wrap > * { 87 | margin-left: 5px; 88 | } 89 | 90 | 91 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import bindAll from 'lodash.bindall'; 2 | import LocalizedStrings from 'react-localization'; 3 | import { Checkbox, Row, Col, Button, Layout, Icon, Menu , Divider, Table, Radio, Popconfirm, Input, Modal, Upload, Tooltip, message } from 'antd'; 4 | import React, { Component } from 'react'; 5 | import Blockly from 'scratch-blocks'; 6 | 7 | import { SketchPicker } from 'react-color'; 8 | import logo from './s3ext.png'; 9 | import './App.css'; 10 | import {BlockScriptEditor, CodePreview, BlockGeneratorEditor} from './BlockEditor'; 11 | import { string } from 'postcss-selector-parser'; 12 | 13 | import micropyImg from './micropy.png'; 14 | import arduinoImg from './arduino.png'; 15 | import pythonImg from './python.png'; 16 | 17 | import {buildJsCode, buildBlockOp, 18 | buildBlockGenCpp, buildBlockGenMpy, 19 | buildEmptyHeadCpp, buildEmptyHeadMpy 20 | } from './CodeBuilder'; 21 | 22 | const { SubMenu } = Menu; 23 | const { Header, Content, Footer, Sider } = Layout; 24 | const RadioGroup = Radio.Group; 25 | 26 | let strings = new LocalizedStrings({ 27 | en:{ 28 | extID: "Extension ID", 29 | extName: "Extension Name", 30 | preview: "Generate Preview", 31 | extdef: "Extension Define", 32 | generator: "Blocks to Code", 33 | maincolor: "Extension Color", 34 | secondcolor: "Parameter Color", 35 | menuIcon: "Menu Icon", 36 | blockIcon: "Block Icon", 37 | addLabel: "Add Label", 38 | addInput: "Add String Parameter", 39 | addInputNum: "Add Number Parameter", 40 | addBool: "Add Boolean Parameter", 41 | addblock: "Add Blocks", 42 | addBlockFun: "Add Functional Block", 43 | addBlockOutput: "Add Output Block", 44 | addBlockBool: "Add Boolean Block", 45 | addBlockHat: "Add Hat Block", 46 | delSure: "delete this block?", 47 | uniqBlockId: "* block ID should be unique", 48 | uniqBlockName: "* block parameter names should be unique", 49 | genHeader: "Edit Header", 50 | promptBlkID: "Please Enter Block ID" 51 | }, 52 | zh: { 53 | extID: "插件ID", 54 | extName: "插件名称", 55 | preview: "生成预览", 56 | extdef: "插件定义", 57 | generator: "图形化转代码", 58 | maincolor: "插件颜色", 59 | secondcolor: "变量颜色", 60 | menuIcon: "菜单栏图标", 61 | blockIcon: "方块图标", 62 | addLabel: "添加文本", 63 | addInput: "添加文本变量", 64 | addInputNum: "添加数字变量", 65 | addBool: "添加布尔变量", 66 | addblock: "添加方块", 67 | addBlockFun: "添加函数方块", 68 | addBlockOutput: "添加输出方块", 69 | addBlockBool: "添加布尔方块", 70 | addBlockHat: "添加帽子方块", 71 | delSure: "删除该方块?", 72 | uniqBlockId: "* 积木ID需要全局唯一", 73 | uniqBlockName: "* 积木参数名字需要唯一", 74 | genHeader: "编辑头文件", 75 | promptBlkID: "请输入方块ID" 76 | } 77 | }); 78 | 79 | const emptyToolBox = ` 80 | 81 | 82 | `; 83 | 84 | const OUTPUT_SHAPE_HEXAGONAL = 1; 85 | const OUTPUT_SHAPE_ROUND = 2; 86 | const OUTPUT_SHAPE_SQUARE = 3; 87 | 88 | 89 | const extOption = [ 90 | { label: Arduino, value: 'arduino' }, 91 | { label: Micro Python, value: 'micropython' } 92 | ]; 93 | 94 | class App extends Component { 95 | constructor (props){ 96 | super(props); 97 | this.state = { 98 | collapsed: true, 99 | extID: 'testExt', 100 | extName: 'Test', 101 | color1Pick: false, 102 | color2Pick: false, 103 | color1: '#0FBD8C', 104 | color2: '#0DA57A', 105 | indexJS: null, 106 | menuIcon: null, 107 | blockIcon: null, 108 | editBlockID: 'newblock', 109 | blocks: [], 110 | menus: [], 111 | addBlockType: '', 112 | showMutation: false, 113 | blockScript: null, 114 | genOption: [], 115 | genHeadScript: null, 116 | blockGenerator: null, 117 | isShowCodePreview: false 118 | } 119 | bindAll(this, [ 120 | "uploadMenuIcon", 121 | "uploadBlockIcon", 122 | "closeMutationModal", 123 | "generatePreview", 124 | "addBlockFun", 125 | "addBlockOutput", 126 | "addBlockBool", 127 | "addBlockHat", 128 | "addLabel", 129 | "addInput", 130 | "addInputNum", 131 | "addBool", 132 | "applyMutation", 133 | "injectDeclareWorkspace", 134 | "makeBlock", 135 | "editBlock", 136 | "deleteBlock", 137 | "saveToJson", 138 | "generateIndexJS", 139 | "loadFromJson", 140 | "exportJs", 141 | "editBlockScript", 142 | "editGeneratorHead", 143 | "editBlockGenerator", 144 | "onExtoptionChange" 145 | ]); 146 | 147 | this.blockColumn = [{ 148 | title: 'Op Code', 149 | dataIndex: 'opcode', 150 | key: 'opcode', 151 | width: '20%', 152 | render: text => {text}, 153 | }, { 154 | title: 'Preview', 155 | dataIndex: 'svg', 156 | key: 'svg', 157 | render: (text, record) => ( 158 | 159 | ) 160 | }, , { 161 | title: 'block op', 162 | key: 'blockop', 163 | render: (text, record) => ( 164 | 165 | this.editBlockScript(record.opcode)} > 166 | 167 | 168 | 169 | 170 | 171 | this.editBlockGenerator(record.opcode)} > 172 | 173 | 174 | 175 | 176 | 177 | ) 178 | }, { 179 | title: 'Action', 180 | key: 'action', 181 | render: (text, record) => ( 182 | 183 | this.editBlock(record.opcode)} >Edit {record.name} 184 | 185 | this.deleteBlock(record.opcode)}> 186 | Delete 187 | 188 | 189 | ), 190 | }]; 191 | } 192 | 193 | componentDidMount (){ 194 | this.previewWorkspace = Blockly.inject('preview', { 195 | media: './media/', 196 | toolbox: emptyToolBox, 197 | zoom: { 198 | startScale: 0.75 199 | } 200 | }); 201 | 202 | Blockly.Procedures.externalProcedureDefCallback = function (mutation, cb) { 203 | console.log("externalProcedureDefCallback"); 204 | } 205 | this.previewWorkspace.getFlyout().setRecyclingEnabled(false); 206 | window.ws = this.previewWorkspace; 207 | } 208 | 209 | onExtoptionChange (opt){ 210 | this.setState({ 211 | genOption: opt 212 | }) 213 | } 214 | 215 | uploadMenuIcon (file){ 216 | let reader = new FileReader(); 217 | const _this = this; 218 | reader.onerror = function () { 219 | console.warn("read image file error") 220 | }; 221 | 222 | reader.onload = function (ev) { 223 | const dataUri = reader.result; 224 | _this.setState({menuIcon: dataUri}); 225 | }; 226 | reader.readAsDataURL(file); 227 | } 228 | 229 | uploadBlockIcon (file){ 230 | let reader = new FileReader(); 231 | const _this = this; 232 | reader.onerror = function () { 233 | console.warn("read image file error") 234 | }; 235 | 236 | reader.onload = function (ev) { 237 | const dataUri = reader.result; 238 | _this.setState({blockIcon: dataUri}); 239 | }; 240 | reader.readAsDataURL(file); 241 | } 242 | 243 | generatePreview (){ 244 | const xmlParts = []; 245 | this.previewWorkspace.clear(); 246 | 247 | const colorXML = `colour="${this.state.color1}" secondaryColour="${this.state.color2}"`; 248 | let menuIconURI = ''; 249 | if (this.state.menuIcon) { 250 | menuIconURI = this.state.menuIcon; 251 | } else if (this.state.blockIcon) { 252 | menuIconURI = this.state.blockIcon; 253 | } 254 | const blockJsons = []; 255 | const menuIconXML = menuIconURI ? 256 | `iconURI="${menuIconURI}"` : ''; 257 | xmlParts.push(``); 258 | xmlParts.push(``); 259 | xmlParts.push.apply(xmlParts, this.state.blocks.map(block => { 260 | const extendedOpcode = `${this.state.extID}_${block.opcode}`; 261 | let argIndex = 0; 262 | const blockJSON = { 263 | type: extendedOpcode, 264 | category: this.state.extName, 265 | colour: this.state.color1, 266 | inputsInline: true, 267 | colourSecondary: this.state.color2, 268 | extensions: ['scratch_extension'] 269 | }; 270 | const iconURI = this.state.blockIcon; 271 | 272 | if (iconURI) { 273 | blockJSON.message0 = '%1 %2'; 274 | const iconJSON = { 275 | type: 'field_image', 276 | src: iconURI, 277 | width: 40, 278 | height: 40 279 | }; 280 | const separatorJSON = { 281 | type: 'field_vertical_separator' 282 | }; 283 | blockJSON.args0 = [ 284 | iconJSON, 285 | separatorJSON 286 | ]; 287 | argIndex+=1; 288 | } 289 | 290 | blockJSON[`message${argIndex}`] = block.msg; 291 | blockJSON[`args${argIndex}`] = block.args.map(arg => arg.json); 292 | 293 | 294 | if (block.type === 'func'){ 295 | blockJSON.outputShape = OUTPUT_SHAPE_SQUARE; 296 | blockJSON.nextStatement = null; 297 | blockJSON.previousStatement = null; 298 | } else if (block.type === 'output'){ 299 | blockJSON.outputShape = OUTPUT_SHAPE_ROUND; 300 | blockJSON.output = "String"; 301 | blockJSON.checkboxInFlyout = true; 302 | } else if (block.type === 'bool'){ 303 | blockJSON.output = "Boolean"; 304 | blockJSON.outputShape = OUTPUT_SHAPE_HEXAGONAL; 305 | } else if (block.type === 'hat'){ 306 | blockJSON.outputShape = OUTPUT_SHAPE_SQUARE; 307 | blockJSON.nextStatement = null; 308 | blockJSON.previousStatement = undefined; // hack to hat module 309 | } 310 | 311 | blockJsons.push(blockJSON); 312 | const inputXML = block.args.map(arg => { 313 | const inputList = []; 314 | const placeholder = arg.placeholder.replace(/[<"&]/, '_'); 315 | const shadowType = arg.shadowType; 316 | const fieldType = arg.fieldType; 317 | const defaultValue = arg.defaultValue || ''; 318 | inputList.push(``); 319 | if (shadowType) { 320 | inputList.push(``); 321 | inputList.push(`${defaultValue}`); 322 | inputList.push(''); 323 | } 324 | inputList.push(''); 325 | 326 | return inputList.join(''); 327 | }); 328 | let blockXML = `${inputXML.join('')}`; 329 | return blockXML; 330 | })); 331 | xmlParts.push(''); 332 | xmlParts.push(``); 333 | Blockly.defineBlocksWithJsonArray(blockJsons); 334 | console.log("extension", xmlParts); 335 | this.previewWorkspace.updateToolbox(xmlParts.join('\n')); 336 | } 337 | 338 | closeMutationModal (){ 339 | this.declareWorkspace.clear(); 340 | this.setState({showMutation: false}) 341 | } 342 | 343 | makeBlock (blockType, mutationText){ 344 | this.mutationRoot = this.declareWorkspace.newBlock('procedures_declaration'); 345 | // this.mutationRoot.setMovable(false); 346 | this.mutationRoot.setDeletable(false); 347 | this.mutationRoot.contextMenu = false; 348 | 349 | // override default custom procedure insert 350 | this.mutationRoot.addStringNumberExternal = function(isNum) { 351 | Blockly.WidgetDiv.hide(true); 352 | if (isNum){ 353 | this.procCode_ = this.procCode_ + ' %n'; 354 | this.displayNames_.push('X'); 355 | } else { 356 | this.procCode_ = this.procCode_ + ' %s'; 357 | this.displayNames_.push('TXT'); 358 | } 359 | this.argumentIds_.push(Blockly.utils.genUid()); 360 | this.argumentDefaults_.push(''); 361 | this.updateDisplay_(); 362 | this.focusLastEditor_(); 363 | }; 364 | 365 | // this.mutationRoot.domToMutation(this.props.mutator); 366 | if (!mutationText){ 367 | mutationText = '' + 368 | '' + 374 | '' + 375 | ''; 376 | } 377 | const dom = Blockly.Xml.textToDom(mutationText).firstChild; 378 | this.mutationRoot.domToMutation(dom); 379 | this.mutationRoot.initSvg(); 380 | this.mutationRoot.render(); 381 | if (blockType === 'bool' || blockType === 'output'){ 382 | this.mutationRoot.setPreviousStatement(false, null); 383 | this.mutationRoot.setNextStatement(false, null); 384 | this.mutationRoot.setInputsInline(true); 385 | if (blockType === 'output'){ 386 | this.mutationRoot.setOutputShape(Blockly.OUTPUT_SHAPE_ROUND); 387 | this.mutationRoot.setOutput(true, 'Boolean'); 388 | } else { 389 | this.mutationRoot.setOutputShape(Blockly.OUTPUT_SHAPE_HEXAGONAL); 390 | this.mutationRoot.setOutput(true, 'Number'); 391 | } 392 | } else if (blockType === 'hat') { 393 | this.mutationRoot.setPreviousStatement(undefined, null); 394 | this.mutationRoot.setNextStatement(true, null); 395 | this.mutationRoot.setInputsInline(true); 396 | } 397 | const {x, y} = this.mutationRoot.getRelativeToSurfaceXY(); 398 | const dy = (360 / 2) - (this.mutationRoot.height / 2) - y; 399 | const dx = (480 / 2) - (this.mutationRoot.width / 2) - x; 400 | this.mutationRoot.moveBy(dx, dy); 401 | window.mu = this.mutationRoot; 402 | } 403 | 404 | injectDeclareWorkspace (ref){ 405 | this.blocks = ref; 406 | const oldDefaultToolbox = Blockly.Blocks.defaultToolbox; 407 | Blockly.Blocks.defaultToolbox = null; 408 | this.declareWorkspace = Blockly.inject('declare', { 409 | media: './media/' 410 | }); 411 | Blockly.Blocks.defaultToolbox = oldDefaultToolbox; 412 | 413 | const _this = this; 414 | this.declareWorkspace.addChangeListener(function(evt) { 415 | // console.log(Object.getPrototypeOf(evt).type, evt); 416 | if (_this.mutationRoot) { 417 | // todo: blockly turn %n to %s in updateDeclarationProcCode_ 418 | _this.mutationRoot.onChangeFn(); 419 | } 420 | }); 421 | this.makeBlock(this.state.addBlockType); 422 | } 423 | 424 | applyMutation (){ 425 | const svg = this.mutationRoot.getSvgRoot(); 426 | const bbox = svg.getBBox(); 427 | svg.removeAttribute('transform'); 428 | let xml = (new XMLSerializer).serializeToString(svg); 429 | xml = ` 430 | ${encodeURIComponent(xml)} 431 | `; 432 | 433 | const mutation = this.mutationRoot.mutationToDom(true) 434 | // console.log(mutation); 435 | const argNames = JSON.parse(mutation.getAttribute('argumentnames')); 436 | const args = []; 437 | 438 | // parse proc code 439 | let argCnt = 0; 440 | const args0 = []; 441 | let proccode = this.mutationRoot.getProcCode(); 442 | proccode = proccode.split(" "); 443 | for (let n=0; n${Blockly.Xml.domToText(mutation)}`; 487 | console.log("mutationText >>", mutationText); 488 | const newBlock = { 489 | opcode: this.state.editBlockID, 490 | svg: xml, 491 | msg, 492 | args, 493 | mutationText: mutationText, 494 | type: this.state.addBlockType 495 | }; 496 | const blocks = [...this.state.blocks].filter(blk => blk.opcode !== this.state.editBlockID); 497 | blocks.push(newBlock); 498 | 499 | this.setState({ 500 | showMutation: false, 501 | blocks: blocks 502 | }); 503 | } 504 | 505 | addLabel (){ 506 | this.mutationRoot.addLabelExternal(); 507 | } 508 | addInput (){ 509 | this.mutationRoot.addStringNumberExternal(); 510 | } 511 | addInputNum (){ 512 | this.mutationRoot.addStringNumberExternal(true); 513 | } 514 | addBool (){ 515 | this.mutationRoot.addBooleanExternal(); 516 | } 517 | addBlockFun (){ 518 | const blkid = prompt(strings.promptBlkID); 519 | if (!blkid || blkid.length == 0) return; 520 | this.setState({ 521 | editBlockID: blkid, 522 | showMutation: true, 523 | addBlockType: 'func', 524 | }); 525 | if (this.declareWorkspace){ 526 | this.declareWorkspace.clear(); 527 | this.makeBlock(); 528 | } 529 | 530 | } 531 | 532 | addBlockOutput (){ 533 | const blkid = prompt(strings.promptBlkID); 534 | if (!blkid || blkid.length == 0) return; 535 | this.setState({ 536 | editBlockID: blkid, 537 | addBlockType: 'output', 538 | showMutation: true 539 | }); 540 | if (this.declareWorkspace){ 541 | this.declareWorkspace.clear(); 542 | this.makeBlock('output'); 543 | } 544 | } 545 | 546 | addBlockBool (){ 547 | const blkid = prompt(strings.promptBlkID); 548 | if (!blkid || blkid.length == 0) return; 549 | this.setState({ 550 | editBlockID: blkid, 551 | addBlockType: 'bool', 552 | showMutation: true 553 | }); 554 | if (this.declareWorkspace){ 555 | this.declareWorkspace.clear(); 556 | this.makeBlock('bool'); 557 | 558 | } 559 | 560 | } 561 | 562 | addBlockHat (){ 563 | const blkid = prompt(strings.promptBlkID); 564 | if (!blkid || blkid.length == 0) return; 565 | this.setState({ 566 | editBlockID: blkid, 567 | addBlockType: 'hat', 568 | showMutation: true 569 | }); 570 | if (this.declareWorkspace){ 571 | this.declareWorkspace.clear(); 572 | this.makeBlock('hat'); 573 | } 574 | 575 | } 576 | 577 | editBlock (opcode){ 578 | const block = this.state.blocks.filter(blk => blk.opcode === opcode); 579 | if (block && block.length == 1){ 580 | this.declareWorkspace.clear(); 581 | this.makeBlock(block[0].type, block[0].mutationText); 582 | this.setState({ 583 | editBlockID: opcode, 584 | showMutation: true, 585 | addBlockType: block[0].type 586 | }); 587 | } 588 | } 589 | 590 | deleteBlock (opcode){ 591 | const blocks = [...this.state.blocks].filter(blk => blk.opcode !== opcode); 592 | this.setState({blocks}); 593 | } 594 | 595 | editBlockScript (opcode){ 596 | const block = this.state.blocks.filter(blk => blk.opcode === opcode); 597 | if (block && block.length == 1){ 598 | const blk = block[0]; 599 | if (!blk.script){ 600 | blk.script = buildBlockOp(blk.opcode, blk.args); 601 | } 602 | this.setState({ 603 | blockScript: { 604 | opcode: blk.opcode, 605 | script: blk.script, 606 | applyScript: (script) => { 607 | blk.script = script 608 | } 609 | } 610 | }); 611 | } 612 | } 613 | 614 | editGeneratorHead (){ 615 | const genHeadScript = { 616 | applyGen: (gen) => { 617 | if (gen.genCpp) this.setState({genCppHead: gen.genCpp}); 618 | if (gen.genMpy) this.setState({genMpyHead: gen.genMpy}); 619 | } 620 | } 621 | for (const code of this.state.genOption){ 622 | if (code === 'arduino'){ 623 | genHeadScript.genCpp = this.state.genCppHead || buildEmptyHeadCpp(this.state.extID); 624 | } else if(code === 'micropython'){ 625 | genHeadScript.genMpy = this.state.genMpyHead || buildEmptyHeadMpy(this.state.extID); 626 | } 627 | } 628 | this.setState({genHeadScript}); 629 | } 630 | 631 | editBlockGenerator (opcode){ 632 | const block = this.state.blocks.filter(blk => blk.opcode === opcode); 633 | if (block && block.length == 1){ 634 | const blk = block[0]; 635 | const blockGenerator = { 636 | opcode: blk.opcode, 637 | applyGen: (gen) => { 638 | if (gen.genCpp) blk.genCpp = gen.genCpp; 639 | if (gen.genMpy) blk.genMpy = gen.genMpy; 640 | } 641 | } 642 | if (!blk.gen){ 643 | for (const code of this.state.genOption){ 644 | if (code === 'arduino'){ 645 | blockGenerator.genCpp = blk.genCpp || buildBlockGenCpp(blk.opcode, blk.args); 646 | } else if(code === 'micropython'){ 647 | blockGenerator.genMpy = blk.genMpy || buildBlockGenMpy(blk.opcode, blk.args); 648 | } 649 | } 650 | } 651 | this.setState({blockGenerator}); 652 | } 653 | } 654 | 655 | saveToJson (){ 656 | const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(this.state, null, 2)); 657 | const downloadAnchorNode = document.createElement('a'); 658 | downloadAnchorNode.setAttribute("href", dataStr); 659 | downloadAnchorNode.setAttribute("download", this.state.extID + ".json"); 660 | document.body.appendChild(downloadAnchorNode); // required for firefox 661 | downloadAnchorNode.click(); 662 | downloadAnchorNode.remove(); 663 | } 664 | 665 | loadFromJson (file){ 666 | if (file){ 667 | let reader = new FileReader(); 668 | const _this = this; 669 | reader.onerror = function () { 670 | console.warn("read image file error") 671 | }; 672 | reader.onload = ev => { 673 | this.setState(Object.assign({}, 674 | JSON.parse(reader.result) 675 | )) 676 | } 677 | reader.readAsText(file); 678 | } 679 | } 680 | 681 | generateIndexJS (){ 682 | const option = { 683 | className: this.state.extID, 684 | extID: this.state.extID, 685 | extName: this.state.extName, 686 | color1: this.state.color1, 687 | color2: this.state.color2, 688 | menuIconURI: this.state.menuIcon ? `"${this.state.menuIcon}"` : 'null', 689 | blockIconURI: this.state.blockIcon ? `"${this.state.blockIcon}"` : 'null', 690 | genCppHead: this.state.genCppHead, 691 | genMpyHead: this.state.genMpyHead, 692 | } 693 | const indexJS = buildJsCode(option, this.state.blocks); 694 | return indexJS; 695 | } 696 | 697 | exportJs (){ 698 | this.setState({ 699 | indexJS: this.generateIndexJS() 700 | }, () => this.setState({isShowCodePreview: true})); 701 | } 702 | 703 | render() { 704 | return ( 705 | 706 | 711 |
712 | 713 |
714 | 715 | 716 | 717 | New Extension 718 | 719 | 720 |
721 | 722 |
723 | this.setState({collapsed: !this.state.collapsed})} 727 | /> 728 |
729 | 733 | 734 | 735 | {strings.extdef} 736 | 737 | 738 |

{strings.extID}

739 | 740 | 741 | this.setState({extID: e.target.value})} /> 742 | 743 | 744 |

{strings.extName}

745 | 746 | 747 | this.setState({extName: e.target.value})} /> 748 | 749 |
750 | 751 | {strings.maincolor} 752 | 753 |
this.setState({color1Pick: true})} /> 754 | { this.state.color1Pick ?
755 |
this.setState({color1Pick: false})}/> 756 | this.setState({color1: c.hex})} /> 757 |
: null } 758 | 759 | {strings.secondcolor} 760 | 761 |
this.setState({color2Pick: true})} /> 762 | { this.state.color2Pick ?
763 |
this.setState({color2Pick: false})}/> 764 | this.setState({color2: c.hex})} /> 765 |
: null } 766 | 767 | 768 | 769 | 770 | {this.state.menuIcon ? : null} 771 | 778 | 779 | 780 | 781 | 782 | {this.state.blockIcon ? : null} 783 | 790 | 791 | 792 | 793 | 794 | {strings.generator} 795 | 796 | 797 | 798 | 799 | 800 | {strings.addblock} 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 812 |
813 | 814 | 815 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 836 |
837 |
838 | 839 | 840 | 841 | 842 |
843 |

{strings.uniqBlockName}

844 | 845 | 846 |
847 |

Block ID

848 | 849 | 850 | this.setState({editBlockID: e.target.value})} /> 851 | 852 | 853 |

{strings.uniqBlockId}

854 | 855 | 856 | 857 | {this.state.genHeadScript ? this.setState({genHeadScript: null})} 861 | /> : null} 862 | {this.state.blockScript ? this.setState({blockScript: null})} 865 | /> : null} 866 | {this.state.blockGenerator ? this.setState({blockGenerator: null})} 870 | /> : null} 871 | {this.state.isShowCodePreview ? this.setState({isShowCodePreview: false})} 874 | /> : null} 875 | 876 | ); 877 | } 878 | } 879 | 880 | export default App; 881 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/BlockEditor.js: -------------------------------------------------------------------------------- 1 | import bindAll from 'lodash.bindall'; 2 | import LocalizedStrings from 'react-localization'; 3 | import { Modal, Tabs } from 'antd'; 4 | import React, { Component } from 'react'; 5 | import MonacoEditor from 'react-monaco-editor'; 6 | 7 | const TabPane = Tabs.TabPane; 8 | 9 | class BlockScriptEditor extends Component { 10 | constructor (props){ 11 | super(props); 12 | bindAll(this, [ 13 | 'onChange', 14 | 'onApply' 15 | ]) 16 | this.state = { 17 | script: this.props.blockScript.script 18 | }; 19 | } 20 | 21 | onChange(newValue, e) { 22 | this.setState({ 23 | script: newValue 24 | }) 25 | } 26 | onApply (){ 27 | this.props.blockScript.applyScript(this.state.script); 28 | this.props.onClose(); 29 | } 30 | render (){ 31 | const { 32 | onClose, 33 | blockScript, 34 | } = this.props; 35 | 36 | return ( 43 | 51 | ) 52 | } 53 | } 54 | 55 | class BlockGeneratorEditor extends Component { 56 | constructor (props){ 57 | super(props); 58 | bindAll(this, [ 59 | 'onChange', 60 | 'onApply', 61 | 'switchCode' 62 | ]) 63 | this.state = { 64 | genCpp: this.props.gen.genCpp, 65 | genMpy: this.props.gen.genMpy, 66 | }; 67 | } 68 | 69 | onChange(newValue, e) { 70 | this.setState({ 71 | script: newValue 72 | }) 73 | } 74 | onApply (){ 75 | this.props.gen.applyGen({ 76 | genCpp: this.state.genCpp, 77 | genMpy: this.state.genMpy 78 | }); 79 | this.props.onClose(); 80 | } 81 | switchCode (c){ 82 | 83 | } 84 | render (){ 85 | const { 86 | onClose, 87 | genOption, 88 | gen, 89 | } = this.props; 90 | 91 | return ( 98 | 99 | {genOption.indexOf('arduino') > -1 ? 100 | this.setState({genCpp: code})} 107 | /> :null} 108 | {genOption.indexOf('micropython') > -1 ? 109 | this.setState({genMpy: code})} 116 | /> :null} 117 | 118 | ) 119 | } 120 | 121 | } 122 | 123 | 124 | class CodePreview extends Component { 125 | constructor (props){ 126 | super(props); 127 | bindAll(this, [ 128 | 'onChange', 129 | 'downloadIndexjs' 130 | ]) 131 | this.state = { 132 | script: this.props.code 133 | }; 134 | } 135 | 136 | downloadIndexjs (){ 137 | const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(this.state.script); 138 | const downloadAnchorNode = document.createElement('a'); 139 | downloadAnchorNode.setAttribute("href", dataStr); 140 | downloadAnchorNode.setAttribute("download", "index.js"); 141 | document.body.appendChild(downloadAnchorNode); // required for firefox 142 | downloadAnchorNode.click(); 143 | downloadAnchorNode.remove(); 144 | } 145 | onChange(newValue, e) { 146 | this.setState({ 147 | script: newValue 148 | }) 149 | } 150 | render (){ 151 | const { 152 | code, 153 | onClose, 154 | } = this.props; 155 | 156 | return ( 163 | 171 | ) 172 | } 173 | } 174 | 175 | 176 | 177 | export { 178 | BlockScriptEditor, 179 | BlockGeneratorEditor, 180 | CodePreview 181 | } -------------------------------------------------------------------------------- /src/CodeBuilder.js: -------------------------------------------------------------------------------- 1 | 2 | const BlockTypeMap = { 3 | func: "COMMAND", 4 | output: "REPORTER", 5 | bool: "BOOLEAN", 6 | hat: "HAT" 7 | } 8 | 9 | const buildEmptyHeadCpp = function (extID){ 10 | return `cppComm(gen){ 11 | gen.includes_['${extID}'] = '#include "YourHeader.h"'; 12 | gen.definitions_['${extID}'] = 'YourClass object;'; 13 | };` 14 | } 15 | 16 | const buildEmptyHeadMpy = function (extID){ 17 | return `mpyComm(gen){ 18 | gen.includes_['${extID}'] = 'import YourClass'; 19 | };` 20 | } 21 | 22 | const buildBlockGenCpp = function (opcode, args){ 23 | const code = `${opcode}Cpp (gen, block){\n cppComm(gen);\n return gen.template2code(block, '${opcode}')\n}\n` 24 | return code; 25 | } 26 | 27 | const buildBlockGenMpy = function (opcode, args){ 28 | const code = `${opcode}Cpp (gen, block){\n mpyComm(gen);\n return gen.template2code(block, '${opcode}')\n}\n` 29 | return code; 30 | } 31 | 32 | const buildBlockOp = function(opcode, args){ 33 | const argDefine = args.reduce((sc, arg) => { 34 | return sc += ` const ${arg.placeholder} = args.${arg.placeholder};\n` 35 | }, "") 36 | const code = `${opcode} (args, util){\n${argDefine}\n return this.write(\`M0 \\n\`);\n}\n` 37 | return code; 38 | } 39 | 40 | const buildJsCode = function(opt, blocks){ 41 | const blockFunctions = []; 42 | const blocksInfo = []; 43 | 44 | for (const block of blocks){ 45 | let txt = block.msg; 46 | let argIndex = 1; 47 | const blockCode = { 48 | opcode: `'${block.opcode}'`, 49 | blockType: `BlockType.${BlockTypeMap[block.type]}` 50 | }; 51 | if (block.type === 'hat'){ 52 | blockCode.isEdgeActivated = false 53 | } 54 | if (block.args.length){ 55 | blockCode.arguments = {}; 56 | for (let n=0;n { 125 | if (parser){ 126 | this.reporter = { 127 | parser, 128 | resolve 129 | } 130 | } 131 | this.session.write(data); 132 | }) 133 | } 134 | } 135 | 136 | onmessage (data){ 137 | const dataStr = this.decoder.decode(data); 138 | this.lineBuffer += dataStr; 139 | if (this.lineBuffer.indexOf('\\n') !== -1){ 140 | const lines = this.lineBuffer.split('\\n'); 141 | this.lineBuffer = lines.pop(); 142 | for (const l of lines){ 143 | if (this.reporter){ 144 | const {parser, resolve} = this.reporter; 145 | resolve(parser(l)); 146 | }; 147 | } 148 | } 149 | } 150 | 151 | scan (){ 152 | this.comm.getDeviceList().then(result => { 153 | this.runtime.emit(this.runtime.constructor.PERIPHERAL_LIST_UPDATE, result); 154 | }); 155 | } 156 | 157 | getInfo (){ 158 | return { 159 | id: '${opt.extID}', 160 | name: '${opt.extName}', 161 | color1: '${opt.color1}', 162 | color2: '${opt.color2}', 163 | menuIconURI: menuIconURI, 164 | blockIconURI: blockIconURI, 165 | blocks: ${blkInfoCode} 166 | } 167 | } 168 | 169 | ${blockFunctions.join('\n')} 170 | } 171 | 172 | module.exports = ${opt.className}; 173 | `; 174 | 175 | return indexJS; 176 | } 177 | 178 | 179 | 180 | export { 181 | buildJsCode, 182 | buildBlockOp, 183 | buildBlockGenCpp, 184 | buildBlockGenMpy, 185 | buildEmptyHeadCpp, 186 | buildEmptyHeadMpy 187 | }; -------------------------------------------------------------------------------- /src/arduino.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/scratch3-extension/51e0079ac70dc979f03105001e5e04268459b06e/src/arduino.png -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: http://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/micropy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/scratch3-extension/51e0079ac70dc979f03105001e5e04268459b06e/src/micropy.png -------------------------------------------------------------------------------- /src/python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/scratch3-extension/51e0079ac70dc979f03105001e5e04268459b06e/src/python.png -------------------------------------------------------------------------------- /src/s3ext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/scratch3-extension/51e0079ac70dc979f03105001e5e04268459b06e/src/s3ext.png -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | --------------------------------------------------------------------------------