initial commit

This commit is contained in:
kirill.labutin 2025-09-18 16:11:33 +03:00
commit d7a9b3bbdb
42 changed files with 6786 additions and 0 deletions

18
.gitignore vendored Normal file
View File

@ -0,0 +1,18 @@
.idea
Assembly-CSharp.csproj
Packages
UserSettings
blocks4u-web/node_modules
Library
Logs
ProjectSettings
obj
*.csproj
*.sln
*.user
.collabignore
Assets/Scenes
Assets/TestBeh
Assets/Scenes.meta
Assets/TestBeh.meta
Assets/Blocks4u/htdocs

8
Assets/Blocks4u.meta Normal file
View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 474a6ee9ebe803b4696b44bdbfc2043e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3b68dff098fdeb14f9e2ce28812b58f7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,23 @@
#if UNITY_EDITOR
using Blocks4u.Runtime;
using UnityEditor;
using UnityEngine;
namespace Blocks4u.Editor
{
[CustomEditor(typeof(BlocklyLogicBehaviour), true)]
public class BlocklyLogicEditor : UnityEditor.Editor
{
public override void OnInspectorGUI()
{
if (GUILayout.Button("Open Editor"))
{
var script = MonoScript.FromMonoBehaviour((MonoBehaviour) serializedObject.targetObject);
Application.OpenURL($"{ServerHolder.Server.URL}blocks4u/dist/index.html?script={AssetDatabase.GetAssetPath(script)}&token={ServerHolder.Server.SessionToken}");
}
}
}
}
#endif

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5cec1617d61d458a9ec2919e6b37beeb
timeCreated: 1758136664

View File

@ -0,0 +1,196 @@
#if UNITY_EDITOR
using System;
using System.IO;
using System.Net;
using System.Text;
using Blocks4u.Editor.DTO;
using Unity.Plastic.Newtonsoft.Json;
using Unity.Plastic.Newtonsoft.Json.Linq;
using UnityEngine;
namespace Blocks4u.Editor
{
public class BlocklyServer : IDisposable
{
private string _assetsPath;
public string URL { get; private set; }
private HttpListener _listener;
public string SessionToken { get; private set; }
private bool _running;
public BlocklyServer(string assetsPath, string url, string sessionToken)
{
_assetsPath = assetsPath;
URL = url;
SessionToken = sessionToken;
_listener = new HttpListener();
_listener.Prefixes.Add(url);
_listener.Start();
_running = true;
Listen();
}
private async void Listen()
{
while (_running)
{
ProcessRequest(await _listener.GetContextAsync());
}
}
private void ProcessRequest(HttpListenerContext context)
{
var request = context.Request;
var url = request.Url;
var path = url.AbsolutePath;
// if (path == "/")
// {
// foreach (string kek in context.Request.QueryString)
// {
// Debug.LogError(kek);
// }
//
// HandleRedirect($"/blocks4u/dist/index.html?{context.Request.QueryString}", context);
// return;
// }
if (path != "/rpc")
{
HandleAsset(path, context);
}
else
{
try
{
HandleRpc(context);
}
catch (Exception e)
{
var response = context.Response;
response.StatusCode = 500;
response.OutputStream.Write(Encoding.UTF8.GetBytes(e.Message));
response.Close();
Debug.LogException(e);
}
}
}
private void HandleRpc(HttpListenerContext context)
{
var request = context.Request;
if (request.HttpMethod != "POST")
{
throw new Exception("Invalid HTTP method");
}
var headers = request.Headers;
if (headers["X-Auth"] != SessionToken)
{
throw new Exception("Invalid session token");
}
var response = context.Response;
using var reader = new StreamReader(request.InputStream);
var invocation = JsonConvert.DeserializeObject<MethodInvocation>(reader.ReadToEnd());
var method = typeof(RPC).GetMethod(invocation.method, new[] {typeof(JArray)});
var result = method.Invoke(null, new object[] {invocation.@params});
if (result is not JToken token)
{
throw new Exception("Invalid RPC method signature. Expected public static JToken name(JArray @params)");
}
response.ContentType = "application/json";
response.StatusCode = 200;
var json = new JObject
{
["result"] = (JToken) result
};
response.OutputStream.Write(Encoding.UTF8.GetBytes(json.ToString(Formatting.Indented)));
response.Close();
}
private void HandleAsset(string path, HttpListenerContext context)
{
// if (path.StartsWith("/assets/js/") && !path.EndsWith(".js"))
// {
// path += ".js"; //handle js module import
// }
var response = context.Response;
var file = Path.Combine(_assetsPath, path.Substring(1));
if (path.Contains("..") || !path.StartsWith("/") || !File.Exists(file))
{
response.StatusCode = 404;
response.Close();
return;
}
response.StatusCode = 200;
response.ContentType = GetContentType(Path.GetExtension(file));
var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read);
fileStream.CopyTo(response.OutputStream);
fileStream.Close();
response.Close();
}
private void HandleRedirect(string path, HttpListenerContext context)
{
var response = context.Response;
response.StatusCode = 302;
response.AddHeader("Location", path);
response.Close();
}
private static string GetContentType(string ext)
{
if (string.IsNullOrEmpty(ext))
{
return "application/octet-stream";
}
// Убираем точку если есть
if (ext.StartsWith("."))
{
ext = ext.Substring(1);
}
return ext.ToLower() switch
{
"html" or "htm" => "text/html",
"js" => "application/javascript",
"css" => "text/css",
"svg" => "image/svg+xml",
"png" => "image/png",
"jpeg" or "jpg" => "image/jpeg",
"ico" => "image/x-icon",
"gif" => "image/gif",
"bmp" => "image/bmp",
"webp" => "image/webp",
"txt" => "text/plain",
"json" => "application/json",
"xml" => "application/xml",
"pdf" => "application/pdf",
"zip" => "application/zip",
"mp3" => "audio/mpeg",
"mp4" => "video/mp4",
"woff" => "font/woff",
"woff2" => "font/woff2",
"ttf" => "font/ttf",
"otf" => "font/otf",
_ => "application/octet-stream"
};
}
public void Dispose()
{
_running = false;
_listener?.Stop();
}
}
}
#endif

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fe61cdaaffe742e4b9dee4f3ba2663f9
timeCreated: 1758133729

View File

@ -0,0 +1,14 @@
{
"name": "Blocks4u.Editor",
"rootNamespace": "",
"references": ["GUID:bec37852146fc47489663b3fa2776725"],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 41c61e5900ec7ab49bf8b9de7417b6eb
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 022d1ac797574516a091a98f6bac61e6
timeCreated: 1758139215

View File

@ -0,0 +1,11 @@
using Unity.Plastic.Newtonsoft.Json.Linq;
namespace Blocks4u.Editor.DTO
{
[System.Serializable]
public class MethodInvocation
{
public string method;
public JArray @params;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 170added63cf4b818b60ddef5c8da834
timeCreated: 1758139227

View File

@ -0,0 +1,67 @@
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using Unity.Plastic.Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;
using Debug = UnityEngine.Debug;
namespace Blocks4u.Editor
{
public static class RPC
{
public static JToken Test(JArray @params)
{
var jObject = new JObject();
jObject.Add("KEK", new JValue("KEK"));
return jObject;
}
public static JToken GetCurrentScene(JArray @params)
{
return new JValue(SceneManager.GetActiveScene().name);
}
private static List<Transform> GetChildren(Transform parent)
{
return parent.Cast<Transform>().ToList();
}
private static JObject BuildHierarchy(Transform parent)
{
var tree = new JObject();
foreach (var child in GetChildren(parent))
{
tree[child.name] = BuildHierarchy(child);
}
return tree;
}
public static JToken GetSceneHierarchy(JArray @params)
{
var scene = SceneManager.GetSceneByName(@params[0].ToString());
var hierarchy = new JObject();
foreach (var root in scene.GetRootGameObjects())
{
hierarchy[root.name] = BuildHierarchy(root.transform);
}
return hierarchy;
}
public static JToken TestSession(JArray @params)
{
return new JValue(true);
}
}
}
#endif

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 28f17b6292ab461ab0a40f2d1a386190
timeCreated: 1758138472

View File

@ -0,0 +1,72 @@
#if UNITY_EDITOR
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using UnityEditor;
using Debug = UnityEngine.Debug;
namespace Blocks4u.Editor
{
[InitializeOnLoad]
public class ServerHolder
{
public static BlocklyServer Server { get; private set; }
private static string ConnectionURL => $"http://127.0.0.1:38080/";
static ServerHolder()
{
try
{
var path = DetectAssetsDirectory();
AppDomain.CurrentDomain.DomainUnload += OnDomainUnload;
//TODO token генерить при каждом запуске униту
//TODO вынести параметры порта в отдельный EditorPrefs
//TODO чекать что порт уже не занят!
Debug.LogError("Starting server");
Server = new BlocklyServer(path, ConnectionURL, "UUID1234567");
}
catch (Exception e)
{
Debug.LogException(e);
}
}
private static string DetectAssetsDirectory()
{
// Получаем stack trace и находим вызывающий метод
var stackTrace = new StackTrace(true);
var frames = stackTrace.GetFrames();
if (frames == null)
{
throw new Exception("Failed to get assets directory");
}
foreach (var frame in frames)
{
var fileName = frame.GetFileName();
if (string.IsNullOrEmpty(fileName) || !fileName.EndsWith(nameof(ServerHolder) + ".cs"))
{
continue;
}
return Path.Combine(Path.GetDirectoryName(Path.GetDirectoryName(fileName)) ?? string.Empty, "htdocs");
}
throw new Exception("Failed to get assets directory");
}
private static void OnDomainUnload(object sender, EventArgs e)
{
AppDomain.CurrentDomain.DomainUnload -= OnDomainUnload;
Debug.LogError("Unloading server");
Server?.Dispose();
}
}
}
#endif

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 476d2f54f7cd2c3488d1ae10fc3dd446
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: cfb28e1d07638394a8c541d3915ac6ca
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,9 @@
using UnityEngine;
namespace Blocks4u.Runtime
{
public abstract class BlocklyLogicBehaviour : MonoBehaviour, IBlocklyLogic
{
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 22d41610e5fc4575ad8e923c92830e96
timeCreated: 1758136812

View File

@ -0,0 +1,14 @@
{
"name": "Blocks4u.Runtime",
"rootNamespace": "",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: bec37852146fc47489663b3fa2776725
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,59 @@
using System;
using UnityEditor;
using UnityEngine;
namespace Blocks4u.Runtime
{
[InitializeOnLoad]
public static class CastUtility
{
//TODO cast anything to anything
public static T Cast<T>(object anything)
{
var targetType = typeof(T);
if (anything is T target)
{
return target;
}
if (targetType == typeof(string))
{
return (T) (object) (anything.ToString());
}
if (targetType == typeof(float))
{
var t = (float) (int) anything;
return (T) (object) t;
}
if (IsNumericType(targetType) && IsNumericType(anything.GetType()))
{
// ReSharper disable once PossibleInvalidCastException
return (T) anything;
}
return (T) anything;
throw new InvalidCastException($"Cannot cast object of type {anything.GetType()} to type {typeof(T)}");
}
public static bool IsNumericType(Type type)
{
return type.IsAssignableFrom(typeof(IConvertible)) && type.IsAssignableFrom(typeof(IComparable));
}
static CastUtility()
{
object kek = 123;
var jabba = Cast<float>(kek);
Debug.LogError(jabba);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 49d2b81f0e1549c7956b28616757c333
timeCreated: 1758143623

View File

@ -0,0 +1,7 @@
namespace Blocks4u.Runtime
{
public interface IBlocklyLogic
{
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 52945f1a76f14f24bc4f13fd7a1bb805
timeCreated: 1758136629

View File

@ -0,0 +1,30 @@
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Blocks4u.Runtime
{
public static class SceneUtilities
{
public static GameObject FindGameObjectByPath(string path)
{
foreach (var root in SceneManager.GetActiveScene().GetRootGameObjects())
{
if (path == root.name)
{
return root;
}
if (!path.StartsWith(root.name + "/"))
{
continue;
}
foreach (Transform found in root.transform.Find(path.Substring(root.name.Length + 1)))
{
return found.gameObject;
}
}
return null;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f8d2aa74ade44295afc55fde9c3695db
timeCreated: 1758143123

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 82bac68f45db6b343b008d260f3f7a79
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

22
blocks4u-web/.npmignore Normal file
View File

@ -0,0 +1,22 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Dependency directories
node_modules/
# Generated files
dist/
build/
.DS_Store
# Optional npm cache directory
.npm
# IDEs and editors
.idea
*.sublime-workspace
.vscode/*

50
blocks4u-web/README.md Normal file
View File

@ -0,0 +1,50 @@
# Blockly Sample App
## Purpose
This app illustrates how to use Blockly together with common programming tools like node/npm, webpack, typescript, eslint, and others. You can use it as the starting point for your own application and modify it as much as you'd like. It contains basic infrastructure for running, building, testing, etc. that you can use even if you don't understand how to configure the related tool yet. When your needs outgrow the functionality provided here, you can replace the provided configuration or tool with your own.
## Quick Start
1. [Install](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) npm if you haven't before.
2. Run [`npx @blockly/create-package app <application-name>`](https://www.npmjs.com/package/@blockly/create-package) to clone this application to your own machine.
3. Run `npm install` to install the required dependencies.
4. Run `npm run start` to run the development server and see the app in action.
5. If you make any changes to the source code, just refresh the browser while the server is running to see them.
## Tooling
The application uses many of the same tools that the Blockly team uses to develop Blockly itself. Following is a brief overview, and you can read more about them on our [developer site](https://developers.google.com/blockly/guides/contribute/get-started/development_tools).
- Structure: The application is built as an npm package. You can use npm to manage the dependencies of the application.
- Modules: ES6 modules to handle imports to/exports from other files.
- Building/bundling: Webpack to build the source code and bundle it into one file for serving.
- Development server: webpack-dev-server to run locally while in development.
- Testing: Mocha to run unit tests.
- Linting: Eslint to lint the code and ensure it conforms with a standard style.
- UI Framework: Does not use a framework. For more complex applications, you may wish to integrate a UI framework like React or Angular.
You can disable, reconfigure, or replace any of these tools at any time, but they are preconfigured to get you started developing your Blockly application quickly.
## Structure
- `package.json` contains basic information about the app. This is where the scripts to run, build, etc. are listed.
- `package-lock.json` is used by npm to manage dependencies
- `webpack.config.js` is the configuration for webpack. This handles bundling the application and running our development server.
- `src/` contains the rest of the source code.
- `dist/` contains the packaged output (that you could host on a server, for example). This is ignored by git and will only appear after you run `npm run build` or `npm run start`.
### Source Code
- `index.html` contains the skeleton HTML for the page. This file is modified during the build to import the bundled source code output by webpack.
- `index.js` is the entry point of the app. It configures Blockly and sets up the page to show the blocks, the generated code, and the output of running the code in JavaScript.
- `serialization.js` has code to save and load the workspace using the browser's local storage. This is how your workspace is saved even after refreshing or leaving the page. You could replace this with code that saves the user's data to a cloud database instead.
- `toolbox.js` contains the toolbox definition for the app. The current toolbox contains nearly every block that Blockly provides out of the box. You probably want to replace this definition with your own toolbox that uses your custom blocks and only includes the default blocks that are relevant to your application.
- `blocks/text.js` has code for a custom text block, just as an example of creating your own blocks. You probably want to delete this block, and add your own blocks in this directory.
- `generators/javascript.js` contains the JavaScript generator for the custom text block. You'll need to include block generators for any custom blocks you create, in whatever programming language(s) your application will use.
## Serving
To run your app locally, run `npm run start` to run the development server. This mode generates source maps and ingests the source maps created by Blockly, so that you can debug using unminified code.
To deploy your app so that others can use it, run `npm run build` to run a production build. This will bundle your code and minify it to reduce its size. You can then host the contents of the `dist` directory on a web server of your choosing. If you're just getting started, try using [GitHub Pages](https://pages.github.com/).

51
blocks4u-web/RPC.js Normal file
View File

@ -0,0 +1,51 @@
export const RPC = {
__session: "none",
__invoke: async function (method, params) {
const __this = this;
const resp = await fetch(
"/rpc",
{
method: "POST",
mode: "cors",
cache: "no-cache",
credentials: "include",
headers: {
"Content-Type": "application/json",
"X-Auth": __this.__session
},
redirect: "follow",
referrerPolicy: "no-referrer",
body: JSON.stringify({
jsonrpc: '2.0',
method: method,
params: params,
id: __this.__id++
})
}
);
const success = resp.status === 200;
const result = (success ? (await resp.json()).result : {
"error": true,
"code": resp.status,
"status": resp.statusText,
"body": await resp.text()
});
return {
"result": result,
"success": success
};
},
__id: 1,
TestSession: async function () {
return (await this.__invoke("TestSession", [])).success;
},
GetCurrentScene: async function () {
return (await this.__invoke("GetCurrentScene", [])).result;
},
GetSceneHierarchy: async function (sceneName) {
return (await this.__invoke("GetSceneHierarchy", [sceneName])).result;
},
}

View File

@ -0,0 +1,31 @@
<html>
<body>
<div id="blocklyDiv" style="height: 480px; width: 600px;"></div>
<!--<script src="./node_modules/blockly/blockly.js"></script>-->
<!--<script src="https://unpkg.com/blockly/blockly.min.js"></script>-->
<!--<link rel="stylesheet" href="https://unpkg.com/blockly/blockly.css">-->
<script >
import * as Blockly from "blockly";
const workspace = Blockly.inject('blocklyDiv', { /* config */ });
// const urlParams = new URLSearchParams(window.location.search);
// RPC.__session = urlParams.get('token');
//
// if (await RPC.TestSession()) {
// let scene = await RPC.GetCurrentScene();
// let hierarchy = await RPC.GetSceneHierarchy(scene);
// console.log(hierarchy)
// } else {
// alert("auth fail");
// window.close();
// }
</script>
</body>
</html>

5096
blocks4u-web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
blocks4u-web/package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "blocks4u",
"version": "1.0.0",
"description": "A sample app using Blockly",
"main": "index.js",
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --mode production",
"start": "webpack serve --open --mode development"
},
"keywords": [
"blockly"
],
"author": "",
"license": "Apache-2.0",
"devDependencies": {
"css-loader": "^6.7.1",
"html-webpack-plugin": "^5.5.0",
"source-map-loader": "^4.0.1",
"style-loader": "^3.3.1",
"webpack": "^5.76.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.11.1"
},
"dependencies": {
"blockly": "^11.0.0"
}
}

View File

@ -0,0 +1,35 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import * as Blockly from 'blockly/core';
// Create a custom block called 'add_text' that adds
// text to the output div on the sample app.
// This is just an example and you should replace this with your
// own custom blocks.
const addText = {
type: 'add_text',
message0: 'Add text %1',
args0: [
{
type: 'input_value',
name: 'TEXT',
check: 'String',
},
],
previousStatement: null,
nextStatement: null,
colour: 160,
tooltip: '',
helpUrl: '',
};
// Create the block definitions for the JSON-only blocks.
// This does not register their definitions with Blockly.
// This file has no side effects!
export const blocks = Blockly.common.createBlockDefinitionsFromJsonArray([
addText,
]);

View File

@ -0,0 +1,30 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {Order} from 'blockly/javascript';
// Export all the code generators for our custom blocks,
// but don't register them with Blockly yet.
// This file has no side effects!
export const forBlock = Object.create(null);
forBlock['add_text'] = function (block, generator) {
const text = generator.valueToCode(block, 'TEXT', Order.NONE) || "''";
const addText = generator.provideFunction_(
'addText',
`function ${generator.FUNCTION_NAME_PLACEHOLDER_}(text) {
// Add text to the output area.
const outputDiv = document.getElementById('output');
const textEl = document.createElement('p');
textEl.innerText = text;
outputDiv.appendChild(textEl);
}`,
);
// Generate the function call for this block.
const code = `${addText}(${text});\n`;
return code;
};

View File

@ -0,0 +1,40 @@
body {
margin: 0;
max-width: 100vw;
}
pre,
code {
overflow: auto;
}
#pageContainer {
display: flex;
width: 100%;
max-width: 100vw;
height: 100vh;
}
#blocklyDiv {
flex-basis: 100%;
height: 100%;
min-width: 600px;
}
#outputPane {
display: flex;
flex-direction: column;
width: 400px;
flex: 0 0 400px;
overflow: auto;
margin: 1rem;
}
#generatedCode {
height: 50%;
background-color: rgb(247, 240, 228);
}
#output {
height: 50%;
}

View File

@ -0,0 +1,16 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Blockly Sample App</title>
</head>
<body>
<div id="pageContainer">
<div id="outputPane">
<pre id="generatedCode"><code></code></pre>
<div id="output"></div>
</div>
<div id="blocklyDiv"></div>
</div>
</body>
</html>

62
blocks4u-web/src/index.js Normal file
View File

@ -0,0 +1,62 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import * as Blockly from 'blockly';
import {blocks} from './blocks/text';
import {forBlock} from './generators/javascript';
import {javascriptGenerator} from 'blockly/javascript';
import {save, load} from './serialization';
import {toolbox} from './toolbox';
import './index.css';
// Register the blocks and generator with Blockly
Blockly.common.defineBlocks(blocks);
Object.assign(javascriptGenerator.forBlock, forBlock);
// Set up UI elements and inject Blockly
const codeDiv = document.getElementById('generatedCode').firstChild;
const outputDiv = document.getElementById('output');
const blocklyDiv = document.getElementById('blocklyDiv');
const ws = Blockly.inject(blocklyDiv, {toolbox});
// This function resets the code and output divs, shows the
// generated code from the workspace, and evals the code.
// In a real application, you probably shouldn't use `eval`.
const runCode = () => {
const code = javascriptGenerator.workspaceToCode(ws);
codeDiv.innerText = code;
outputDiv.innerHTML = '';
eval(code);
};
// Load the initial state from storage and run the code.
load(ws);
runCode();
// Every time the workspace changes state, save the changes to storage.
ws.addChangeListener((e) => {
// UI events are things like scrolling, zooming, etc.
// No need to save after one of these.
if (e.isUiEvent) return;
save(ws);
});
// Whenever the workspace changes meaningfully, run the code again.
ws.addChangeListener((e) => {
// Don't run the code when the workspace finishes loading; we're
// already running it once when the application starts.
// Don't run the code during drags; we might have invalid state.
if (
e.isUiEvent ||
e.type == Blockly.Events.FINISHED_LOADING ||
ws.isDragging()
) {
return;
}
runCode();
});

View File

@ -0,0 +1,32 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import * as Blockly from 'blockly/core';
const storageKey = 'mainWorkspace';
/**
* Saves the state of the workspace to browser's local storage.
* @param {Blockly.Workspace} workspace Blockly workspace to save.
*/
export const save = function (workspace) {
const data = Blockly.serialization.workspaces.save(workspace);
window.localStorage?.setItem(storageKey, JSON.stringify(data));
};
/**
* Loads saved state from local storage into the given workspace.
* @param {Blockly.Workspace} workspace Blockly workspace to load into.
*/
export const load = function (workspace) {
const data = window.localStorage?.getItem(storageKey);
if (!data) return;
// Don't emit events during loading.
Blockly.Events.disable();
Blockly.serialization.workspaces.load(JSON.parse(data), workspace, false);
Blockly.Events.enable();
};

629
blocks4u-web/src/toolbox.js Normal file
View File

@ -0,0 +1,629 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/*
This toolbox contains nearly every single built-in block that Blockly offers,
in addition to the custom block 'add_text' this sample app adds.
You probably don't need every single block, and should consider either rewriting
your toolbox from scratch, or carefully choosing whether you need each block
listed here.
*/
export const toolbox = {
kind: 'categoryToolbox',
contents: [
{
kind: 'category',
name: 'Logic',
categorystyle: 'logic_category',
contents: [
{
kind: 'block',
type: 'controls_if',
},
{
kind: 'block',
type: 'logic_compare',
},
{
kind: 'block',
type: 'logic_operation',
},
{
kind: 'block',
type: 'logic_negate',
},
{
kind: 'block',
type: 'logic_boolean',
},
{
kind: 'block',
type: 'logic_null',
},
{
kind: 'block',
type: 'logic_ternary',
},
],
},
{
kind: 'category',
name: 'Loops',
categorystyle: 'loop_category',
contents: [
{
kind: 'block',
type: 'controls_repeat_ext',
inputs: {
TIMES: {
shadow: {
type: 'math_number',
fields: {
NUM: 10,
},
},
},
},
},
{
kind: 'block',
type: 'controls_whileUntil',
},
{
kind: 'block',
type: 'controls_for',
inputs: {
FROM: {
shadow: {
type: 'math_number',
fields: {
NUM: 1,
},
},
},
TO: {
shadow: {
type: 'math_number',
fields: {
NUM: 10,
},
},
},
BY: {
shadow: {
type: 'math_number',
fields: {
NUM: 1,
},
},
},
},
},
{
kind: 'block',
type: 'controls_forEach',
},
{
kind: 'block',
type: 'controls_flow_statements',
},
],
},
{
kind: 'category',
name: 'Math',
categorystyle: 'math_category',
contents: [
{
kind: 'block',
type: 'math_number',
fields: {
NUM: 123,
},
},
{
kind: 'block',
type: 'math_arithmetic',
inputs: {
A: {
shadow: {
type: 'math_number',
fields: {
NUM: 1,
},
},
},
B: {
shadow: {
type: 'math_number',
fields: {
NUM: 1,
},
},
},
},
},
{
kind: 'block',
type: 'math_single',
inputs: {
NUM: {
shadow: {
type: 'math_number',
fields: {
NUM: 9,
},
},
},
},
},
{
kind: 'block',
type: 'math_trig',
inputs: {
NUM: {
shadow: {
type: 'math_number',
fields: {
NUM: 45,
},
},
},
},
},
{
kind: 'block',
type: 'math_constant',
},
{
kind: 'block',
type: 'math_number_property',
inputs: {
NUMBER_TO_CHECK: {
shadow: {
type: 'math_number',
fields: {
NUM: 0,
},
},
},
},
},
{
kind: 'block',
type: 'math_round',
fields: {
OP: 'ROUND',
},
inputs: {
NUM: {
shadow: {
type: 'math_number',
fields: {
NUM: 3.1,
},
},
},
},
},
{
kind: 'block',
type: 'math_on_list',
fields: {
OP: 'SUM',
},
},
{
kind: 'block',
type: 'math_modulo',
inputs: {
DIVIDEND: {
shadow: {
type: 'math_number',
fields: {
NUM: 64,
},
},
},
DIVISOR: {
shadow: {
type: 'math_number',
fields: {
NUM: 10,
},
},
},
},
},
{
kind: 'block',
type: 'math_constrain',
inputs: {
VALUE: {
shadow: {
type: 'math_number',
fields: {
NUM: 50,
},
},
},
LOW: {
shadow: {
type: 'math_number',
fields: {
NUM: 1,
},
},
},
HIGH: {
shadow: {
type: 'math_number',
fields: {
NUM: 100,
},
},
},
},
},
{
kind: 'block',
type: 'math_random_int',
inputs: {
FROM: {
shadow: {
type: 'math_number',
fields: {
NUM: 1,
},
},
},
TO: {
shadow: {
type: 'math_number',
fields: {
NUM: 100,
},
},
},
},
},
{
kind: 'block',
type: 'math_random_float',
},
{
kind: 'block',
type: 'math_atan2',
inputs: {
X: {
shadow: {
type: 'math_number',
fields: {
NUM: 1,
},
},
},
Y: {
shadow: {
type: 'math_number',
fields: {
NUM: 1,
},
},
},
},
},
],
},
{
kind: 'category',
name: 'Text',
categorystyle: 'text_category',
contents: [
{
kind: 'block',
type: 'text',
},
{
kind: 'block',
type: 'text_join',
},
{
kind: 'block',
type: 'text_append',
inputs: {
TEXT: {
shadow: {
type: 'text',
fields: {
TEXT: '',
},
},
},
},
},
{
kind: 'block',
type: 'text_length',
inputs: {
VALUE: {
shadow: {
type: 'text',
fields: {
TEXT: 'abc',
},
},
},
},
},
{
kind: 'block',
type: 'text_isEmpty',
inputs: {
VALUE: {
shadow: {
type: 'text',
fields: {
TEXT: '',
},
},
},
},
},
{
kind: 'block',
type: 'text_indexOf',
inputs: {
VALUE: {
block: {
type: 'variables_get',
},
},
FIND: {
shadow: {
type: 'text',
fields: {
TEXT: 'abc',
},
},
},
},
},
{
kind: 'block',
type: 'text_charAt',
inputs: {
VALUE: {
block: {
type: 'variables_get',
},
},
},
},
{
kind: 'block',
type: 'text_getSubstring',
inputs: {
STRING: {
block: {
type: 'variables_get',
},
},
},
},
{
kind: 'block',
type: 'text_changeCase',
inputs: {
TEXT: {
shadow: {
type: 'text',
fields: {
TEXT: 'abc',
},
},
},
},
},
{
kind: 'block',
type: 'text_trim',
inputs: {
TEXT: {
shadow: {
type: 'text',
fields: {
TEXT: 'abc',
},
},
},
},
},
{
kind: 'block',
type: 'text_count',
inputs: {
SUB: {
shadow: {
type: 'text',
},
},
TEXT: {
shadow: {
type: 'text',
},
},
},
},
{
kind: 'block',
type: 'text_replace',
inputs: {
FROM: {
shadow: {
type: 'text',
},
},
TO: {
shadow: {
type: 'text',
},
},
TEXT: {
shadow: {
type: 'text',
},
},
},
},
{
kind: 'block',
type: 'text_reverse',
inputs: {
TEXT: {
shadow: {
type: 'text',
},
},
},
},
{
kind: 'block',
type: 'add_text',
inputs: {
TEXT: {
shadow: {
type: 'text',
fields: {
TEXT: 'abc',
},
},
},
},
},
],
},
{
kind: 'category',
name: 'Lists',
categorystyle: 'list_category',
contents: [
{
kind: 'block',
type: 'lists_create_with',
},
{
kind: 'block',
type: 'lists_create_with',
},
{
kind: 'block',
type: 'lists_repeat',
inputs: {
NUM: {
shadow: {
type: 'math_number',
fields: {
NUM: 5,
},
},
},
},
},
{
kind: 'block',
type: 'lists_length',
},
{
kind: 'block',
type: 'lists_isEmpty',
},
{
kind: 'block',
type: 'lists_indexOf',
inputs: {
VALUE: {
block: {
type: 'variables_get',
},
},
},
},
{
kind: 'block',
type: 'lists_getIndex',
inputs: {
VALUE: {
block: {
type: 'variables_get',
},
},
},
},
{
kind: 'block',
type: 'lists_setIndex',
inputs: {
LIST: {
block: {
type: 'variables_get',
},
},
},
},
{
kind: 'block',
type: 'lists_getSublist',
inputs: {
LIST: {
block: {
type: 'variables_get',
},
},
},
},
{
kind: 'block',
type: 'lists_split',
inputs: {
DELIM: {
shadow: {
type: 'text',
fields: {
TEXT: ',',
},
},
},
},
},
{
kind: 'block',
type: 'lists_sort',
},
{
kind: 'block',
type: 'lists_reverse',
},
],
},
{
kind: 'sep',
},
{
kind: 'category',
name: 'Variables',
categorystyle: 'variable_category',
custom: 'VARIABLE',
},
{
kind: 'category',
name: 'Functions',
categorystyle: 'procedure_category',
custom: 'PROCEDURE',
},
],
};

View File

@ -0,0 +1,59 @@
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
// Base config that applies to either development or production mode.
const config = {
entry: './src/index.js',
output: {
// Compile the source files into a bundle.
filename: 'bundle.js',
path: path.resolve(__dirname, '../Assets/Blocks4u/htdocs'),
clean: true,
},
// Enable webpack-dev-server to get hot refresh of the app.
devServer: {
static: './build',
},
module: {
rules: [
{
// Load CSS files. They can be imported into JS files.
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
// Generate the HTML index page based on our template.
// This will output the same index page with the bundle we
// created above added in a script tag.
new HtmlWebpackPlugin({
template: 'src/index.html',
}),
],
};
module.exports = (env, argv) => {
if (argv.mode === 'development') {
// Set the output path to the `build` directory
// so we don't clobber production builds.
config.output.path = path.resolve(__dirname, 'build');
// Generate source maps for our code for easier debugging.
// Not suitable for production builds. If you want source maps in
// production, choose a different one from https://webpack.js.org/configuration/devtool
config.devtool = 'eval-cheap-module-source-map';
// Include the source maps for Blockly for easier debugging Blockly code.
config.module.rules.push({
test: /(blockly\/.*\.js)$/,
use: [require.resolve('source-map-loader')],
enforce: 'pre',
});
// Ignore spurious warnings from source-map-loader
// It can't find source maps for some Closure modules and that is expected
config.ignoreWarnings = [/Failed to parse source map/];
}
return config;
};