UNPKG

@robotical/scratch-to-python-transpiler

Version:
578 lines (577 loc) 29.9 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __spreadArrays = (this && this.__spreadArrays) || function () { for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; for (var r = Array(s), k = 0, i = 0; i < il; i++) for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) r[k] = a[j]; return r; }; Object.defineProperty(exports, "__esModule", { value: true }); var JSZip = require("jszip"); var sb3 = require("./interfaces"); var OpCode_1 = require("../../OpCode"); var Block_1 = require("../../Block"); var Costume_1 = require("../../Costume"); var Project_1 = require("../../Project"); var Sound_1 = require("../../Sound"); var Target_1 = require("../../Target"); var Data_1 = require("../../Data"); var Script_1 = require("../../Script"); function extractCostumes(target, getAsset) { var _this = this; return Promise.all(target.costumes.map(function (costumeData) { return __awaiter(_this, void 0, void 0, function () { var _a, _b; return __generator(this, function (_c) { switch (_c.label) { case 0: _a = Costume_1.default.bind; _b = { name: costumeData.name }; return [4 /*yield*/, getAsset({ type: "costume", name: costumeData.name, md5: costumeData.assetId, ext: costumeData.dataFormat, spriteName: target.name })]; case 1: return [2 /*return*/, new (_a.apply(Costume_1.default, [void 0, (_b.asset = _c.sent(), _b.md5 = costumeData.assetId, _b.ext = costumeData.dataFormat, // It's possible that the rotation center of the costume is null. // Because computing a rotation center ourselves would be messy and // easily incompatible with Scratch, pass such complexity onto the // Scratch implementation running a project exported from sb-edit. _b.bitmapResolution = costumeData.bitmapResolution || 2, _b.centerX = costumeData.rotationCenterX, _b.centerY = costumeData.rotationCenterY, _b)]))()]; } }); }); })); } function extractSounds(target, getAsset) { return __awaiter(this, void 0, void 0, function () { var _this = this; return __generator(this, function (_a) { return [2 /*return*/, Promise.all(target.sounds.map(function (soundData) { return __awaiter(_this, void 0, void 0, function () { var _a, _b; return __generator(this, function (_c) { switch (_c.label) { case 0: _a = Sound_1.default.bind; _b = { name: soundData.name }; return [4 /*yield*/, getAsset({ type: "sound", name: soundData.name, md5: soundData.assetId, ext: soundData.dataFormat, spriteName: target.name })]; case 1: return [2 /*return*/, new (_a.apply(Sound_1.default, [void 0, (_b.asset = _c.sent(), _b.md5 = soundData.assetId, _b.ext = soundData.dataFormat, _b.sampleCount = soundData.sampleCount, _b.sampleRate = soundData.rate, _b)]))()]; } }); }); }))]; }); }); } function getBlockScript(blocks) { function getBlockInputs(block, blockId) { return __assign(__assign({}, translateInputs(block.inputs)), translateFields(block.fields, block.opcode)); function translateInputs(inputs) { var result = {}; var addInput = function (name, value) { var _a; result = __assign(__assign({}, result), (_a = {}, _a[name] = value, _a)); }; var _loop_1 = function (inputName, input) { var value = input[1]; if (typeof value === "string") { var inputScript = blockWithNext(value, blockId); if (inputScript.length === 1 && blocks[value].shadow) { // Input contains a shadow block. // Conceptually, shadow blocks are weird. // We basically just want to copy the important // information from the shadow block down into // the block containing the shadow. if (blocks[value].opcode === "procedures_prototype") { var mutation = blocks[value].mutation; // Split proccode (such as "letter %n of %s") into ["letter", "%n", "of", "%s"] var parts = mutation.proccode.split(/((^|[^\\])%[nsb])/); parts = parts.map(function (str) { return str.trim(); }); parts = parts.filter(function (str) { return str !== ""; }); var argNames_1 = JSON.parse(mutation.argumentnames); var argDefaults_1 = JSON.parse(mutation.argumentdefaults); var args = parts.map(function (part) { var optionalToNumber = function (value) { if (typeof value !== "string") { return value; } var asNum = Number(value); if (!isNaN(asNum)) { return asNum; } return value; }; switch (part) { case "%s": case "%n": return { type: "numberOrString", name: argNames_1.shift(), defaultValue: optionalToNumber(argDefaults_1.shift()) }; case "%b": return { type: "boolean", name: argNames_1.shift(), defaultValue: argDefaults_1.shift() === "true" }; default: return { type: "label", name: part }; } }); addInput("PROCCODE", { type: "string", value: mutation.proccode }); addInput("ARGUMENTS", { type: "customBlockArguments", value: args }); addInput("WARP", { type: "boolean", value: mutation.warp === "true" }); } else { // In most cases, just copy the shadow block's fields and inputs // into its parent result = __assign(__assign(__assign({}, result), translateInputs(blocks[value].inputs)), translateFields(blocks[value].fields, blocks[value].opcode)); } } else { var isBlocks = void 0; if (Block_1.BlockBase.isKnownBlock(block.opcode)) { var defaultInput = Block_1.BlockBase.getDefaultInput(block.opcode, inputName); if (defaultInput && defaultInput.type === "blocks") { isBlocks = true; } } isBlocks = isBlocks || inputScript.length > 1; if (isBlocks) { addInput(inputName, { type: "blocks", value: inputScript }); } else { addInput(inputName, { type: "block", value: inputScript[0] }); } } } else if (value === null) { addInput(inputName, { type: "string", value: null }); } else { var BIS = sb3.BlockInputStatus; switch (value[0]) { case BIS.MATH_NUM_PRIMITIVE: case BIS.POSITIVE_NUM_PRIMITIVE: case BIS.WHOLE_NUM_PRIMITIVE: case BIS.INTEGER_NUM_PRIMITIVE: { var storedValue = value[1]; var asNum = Number(storedValue); if (!isNaN(asNum)) { storedValue = asNum; } addInput(inputName, { type: "number", value: storedValue }); break; } case BIS.ANGLE_NUM_PRIMITIVE: addInput(inputName, { type: "angle", value: Number(value[1]) }); break; case BIS.COLOR_PICKER_PRIMITIVE: addInput(inputName, { type: "color", value: { r: parseInt(value[1].slice(1, 3), 16), g: parseInt(value[1].slice(3, 5), 16), b: parseInt(value[1].slice(5, 7), 16) } }); break; case BIS.TEXT_PRIMITIVE: addInput(inputName, { type: "string", value: value[1] }); break; case BIS.BROADCAST_PRIMITIVE: addInput(inputName, { type: "broadcast", value: value[1] }); break; case BIS.VAR_PRIMITIVE: // This is a variable input. Convert it to a variable block. addInput(inputName, { type: "block", value: new Block_1.BlockBase({ opcode: OpCode_1.OpCode.data_variable, inputs: { VARIABLE: { type: "variable", value: { id: value[2], name: value[1] } } }, parent: blockId }) }); break; case BIS.LIST_PRIMITIVE: // This is a list input. Convert it to a list contents block. addInput(inputName, { type: "block", value: new Block_1.BlockBase({ opcode: OpCode_1.OpCode.data_listcontents, inputs: { LIST: { type: "list", value: { id: value[2], name: value[1] } } }, parent: blockId }) }); break; } } }; for (var _i = 0, _a = Object.entries(inputs); _i < _a.length; _i++) { var _b = _a[_i], inputName = _b[0], input = _b[1]; _loop_1(inputName, input); } if (block.opcode === OpCode_1.OpCode.procedures_call) { result = { PROCCODE: { type: "string", value: block.mutation.proccode }, INPUTS: { type: "customBlockInputValues", value: JSON.parse(block.mutation.argumentids).map(function (argumentid) { var value = result[argumentid]; if (value === undefined) { // TODO: Find a way to determine type of missing input value // (Caused by things like boolean procedure_call inputs that // were never filled at any time.) return { type: "string", value: null }; } if (typeof value.value === "string") { var asNum = Number(value.value); if (!isNaN(asNum)) { value.value = asNum; } } return value; }) } }; } return result; } function translateFields(fields, opcode) { var result = {}; for (var _i = 0, _a = Object.entries(fields); _i < _a.length; _i++) { var _b = _a[_i], fieldName = _b[0], values = _b[1]; console.log("\nfieldName", fieldName); console.log("opcode", opcode); if (!sb3.fieldTypeMap[opcode]) { result[fieldName] = { type: "string", value: values[0] }; continue; } var type = sb3.fieldTypeMap[opcode][fieldName]; if (fieldName === "VARIABLE" || fieldName === "LIST") { result[fieldName] = { type: type, value: { id: values[1], name: values[0] } }; } else { result[fieldName] = { type: type, value: values[0] }; } } return result; } } function blockWithNext(blockId, parentId) { if (parentId === void 0) { parentId = null; } var sb3Block = blocks[blockId]; var block = new Block_1.BlockBase({ opcode: sb3Block.opcode, inputs: getBlockInputs(sb3Block, blockId), id: blockId, parent: parentId, next: sb3Block.next }); var next = []; if (sb3Block.next !== null) { next = blockWithNext(sb3Block.next, blockId); } return __spreadArrays([block], next); } return blockWithNext; } function fromSb3JSON(json, options) { var _a, _b, _c; return __awaiter(this, void 0, void 0, function () { function getVariables(target) { return Object.entries(target.variables).map(function (_a) { var id = _a[0], _b = _a[1], name = _b[0], value = _b[1], _c = _b[2], cloud = _c === void 0 ? false : _c; var monitor = json.monitors.find(function (monitor) { return monitor.id === id; }); if (!monitor) { // Sometimes .sb3 files are missing monitors. Fill in with reasonable defaults. monitor = { id: id, mode: "default", opcode: "data_variable", params: { VARIABLE: name }, spriteName: target.name, value: value, width: null, height: null, x: 0, y: 0, visible: false, sliderMin: 0, sliderMax: 100, isDiscrete: true }; } return new Data_1.Variable({ name: name, id: id, value: value, cloud: cloud, visible: monitor.visible, mode: monitor.mode, x: monitor.x, y: monitor.y, sliderMin: monitor.sliderMin, sliderMax: monitor.sliderMax, isDiscrete: monitor.isDiscrete }); }); } function getLists(target) { if (!target.lists) return []; return Object.entries(target.lists).map(function (_a) { var id = _a[0], _b = _a[1], name = _b[0], value = _b[1]; var monitor = json.monitors.find(function (monitor) { return monitor.id === id; }); if (!monitor) { // Sometimes .sb3 files are missing monitors. Fill in with reasonable defaults. monitor = { id: id, mode: "list", opcode: "data_listcontents", params: { LIST: name }, spriteName: target.name, value: value, width: null, height: null, x: 0, y: 0, visible: false }; } return new Data_1.List({ name: name, id: id, value: value, visible: monitor.visible, x: monitor.x, y: monitor.y, width: monitor.width === 0 ? null : monitor.width, height: monitor.height === 0 ? null : monitor.height }); }); } function getTargetOptions(target) { return __awaiter(this, void 0, void 0, function () { var _a, costumes, sounds; return __generator(this, function (_b) { switch (_b.label) { case 0: return [4 /*yield*/, Promise.all([ extractCostumes(target, options.getAsset), extractSounds(target, options.getAsset) ])]; case 1: _a = _b.sent(), costumes = _a[0], sounds = _a[1]; return [2 /*return*/, { name: target.name, isStage: target.isStage, costumes: costumes, costumeNumber: target.currentCostume, sounds: sounds, scripts: Object.entries(target.blocks) .filter(function (_a) { var block = _a[1]; return block.topLevel && !block.shadow; }) .map(function (_a) { var id = _a[0], block = _a[1]; return new Script_1.default({ blocks: getBlockScript(target.blocks)(id), x: block.x, y: block.y }); }), variables: getVariables(target), lists: getLists(target), volume: target.volume, layerOrder: target.layerOrder }]; } }); }); } var stage, project, _d, _e, _f, _g, _i, _h, target, relevantBlocks, usedVariableIds, _j, relevantBlocks_1, block, id, _k, _l, varList, i, variable; var _this = this; return __generator(this, function (_m) { switch (_m.label) { case 0: stage = json.targets.find(function (target) { return target.isStage; }); _d = Project_1.default.bind; _e = {}; if (!stage) return [3 /*break*/, 2]; _g = Target_1.Stage.bind; return [4 /*yield*/, getTargetOptions(stage)]; case 1: _f = new (_g.apply(Target_1.Stage, [void 0, _m.sent()]))(); return [3 /*break*/, 3]; case 2: _f = new Target_1.Stage(); _m.label = 3; case 3: _e.stage = _f; return [4 /*yield*/, Promise.all(json.targets .filter(function (target) { return !target.isStage; }) .map(function (spriteData) { return __awaiter(_this, void 0, void 0, function () { var _a, _b; return __generator(this, function (_c) { switch (_c.label) { case 0: _a = Target_1.Sprite.bind; _b = [{}]; return [4 /*yield*/, getTargetOptions(spriteData)]; case 1: return [2 /*return*/, new (_a.apply(Target_1.Sprite, [void 0, __assign.apply(void 0, [__assign.apply(void 0, _b.concat([(_c.sent())])), { x: spriteData.x, y: spriteData.y, size: spriteData.size, direction: spriteData.direction, rotationStyle: { "all around": "normal", "left-right": "leftRight", "don't rotate": "none" }[spriteData.rotationStyle], isDraggable: spriteData.draggable, visible: spriteData.visible }])]))()]; } }); }); }))]; case 4: project = new (_d.apply(Project_1.default, [void 0, (_e.sprites = _m.sent(), _e.tempo = (_a = stage) === null || _a === void 0 ? void 0 : _a.tempo, _e.videoOn = ((_b = stage) === null || _b === void 0 ? void 0 : _b.videoState) === "on", _e.videoAlpha = (_c = stage) === null || _c === void 0 ? void 0 : _c.videoTransparency, _e)]))(); // Run an extra pass on variables (and lists). Only those which are actually // referenced in blocks or monitors should be kept. for (_i = 0, _h = __spreadArrays([project.stage], project.sprites); _i < _h.length; _i++) { target = _h[_i]; relevantBlocks = null; if (target === project.stage) { relevantBlocks = target.blocks.concat(project.sprites.flatMap(function (sprite) { return sprite.blocks; })); } else { relevantBlocks = target.blocks; } usedVariableIds = new Set(); for (_j = 0, relevantBlocks_1 = relevantBlocks; _j < relevantBlocks_1.length; _j++) { block = relevantBlocks_1[_j]; id = null; if (block.inputs.VARIABLE) { id = block.inputs.VARIABLE.value.id; } else if (block.inputs.LIST) { id = block.inputs.LIST.value.id; } else { continue; } usedVariableIds.add(id); } for (_k = 0, _l = [target.variables, target.lists]; _k < _l.length; _k++) { varList = _l[_k]; for (i = 0, variable = void 0; (variable = varList[i]); i++) { if (variable.visible) { continue; } if (usedVariableIds.has(variable.id)) { continue; } varList.splice(i, 1); i--; } } } return [2 /*return*/, project]; } }); }); } exports.fromSb3JSON = fromSb3JSON; function fromSb3(fileData) { return __awaiter(this, void 0, void 0, function () { var inZip, json, getAsset; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, JSZip.loadAsync(fileData)]; case 1: inZip = _a.sent(); return [4 /*yield*/, inZip.file("project.json").async("text")]; case 2: json = _a.sent(); getAsset = function (_a) { var md5 = _a.md5, ext = _a.ext; return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_b) { return [2 /*return*/, inZip.file(md5 + "." + ext).async("arraybuffer")]; }); }); }; return [2 /*return*/, fromSb3JSON(JSON.parse(json), { getAsset: getAsset })]; } }); }); } exports.default = fromSb3;