UNPKG

sb-edit

Version:

Import, edit, and export Scratch project files

615 lines (614 loc) 31.8 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 __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; 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 (g && (g = 0, op[0] && (_ = 0)), _) 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 __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.fromSb3JSON = void 0; var JSZip = __importStar(require("jszip")); var sb3 = __importStar(require("./interfaces")); var OpCode_1 = require("../../OpCode"); var Block_1 = require("../../Block"); var Costume_1 = __importDefault(require("../../Costume")); var Project_1 = __importDefault(require("../../Project")); var Sound_1 = __importDefault(require("../../Sound")); var Target_1 = require("../../Target"); var Data_1 = require("../../Data"); var Script_1 = __importDefault(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; var _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; var _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 = {}; // TODO: do we really need to create a new object every time? 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 block_1 = blocks[value]; 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 (block_1.opcode === OpCode_1.OpCode.procedures_prototype) { var mutation = block_1.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", // TODO: remove non-null assertions name: argNames_1.shift(), defaultValue: optionalToNumber(argDefaults_1.shift()) }; case "%b": return { type: "boolean", // TODO: remove non-null assertions 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_1.opcode)) { var defaultInput = Block_1.BlockBase.getDefaultInput(block_1.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) { return "continue"; } 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) { var mutation = block.mutation; result = { PROCCODE: { type: "string", value: mutation.proccode }, INPUTS: { type: "customBlockInputValues", value: JSON.parse(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: "boolean", value: false }; } // Auto-coerce number inputs into numbers // TODO: this may lose data! 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 fieldTypes = sb3.fieldTypeMap[opcode]; if (!fieldTypes) return {}; var result = {}; for (var _i = 0, _a = Object.entries(fields); _i < _a.length; _i++) { var _b = _a[_i], fieldName = _b[0], values = _b[1]; var type = fieldTypes[fieldName]; // TODO: remove this type assertion and actually validate fields 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) { var _a; if (parentId === void 0) { parentId = undefined; } var sb3Block = blocks[blockId]; var block = new Block_1.BlockBase({ opcode: sb3Block.opcode, inputs: getBlockInputs(sb3Block, blockId), id: blockId, parent: parentId, next: (_a = sb3Block.next) !== null && _a !== void 0 ? _a : undefined }); var next = []; if (sb3Block.next !== null) { next = blockWithNext(sb3Block.next, blockId); } return __spreadArray([block], next, true); } return blockWithNext; } function fromSb3JSON(json, options) { return __awaiter(this, void 0, void 0, function () { function getVariables(target) { return Object.entries(target.variables).map(function (_a) { var _b; var id = _a[0], _c = _a[1], name = _c[0], value = _c[1], _d = _c[2], cloud = _d === void 0 ? false : _d; var monitor = (_b = json.monitors) === null || _b === void 0 ? void 0 : _b.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) { return Object.entries(target.lists).map(function (_a) { var _b; var id = _a[0], _c = _a[1], name = _c[0], value = _c[1]; var monitor = (_b = json.monitors) === null || _b === void 0 ? void 0 : _b.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, // set width and height to undefined if they're 0, null, or undefined width: monitor.width || undefined, height: monitor.height || undefined }); }); } 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, _a, _b, _i, _c, target, relevantBlocks, usedVariableIds, _d, relevantBlocks_1, block, id, _e, _f, varList, i, variable; var _g; var _this = this; return __generator(this, function (_h) { switch (_h.label) { case 0: stage = json.targets.find(function (target) { return target.isStage; }); _a = Project_1.default.bind; _g = {}; _b = Target_1.Stage.bind; return [4 /*yield*/, getTargetOptions(stage)]; case 1: _g.stage = new (_b.apply(Target_1.Stage, [void 0, _h.sent()]))(); 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 2: project = new (_a.apply(Project_1.default, [void 0, (_g.sprites = _h.sent(), _g.tempo = stage.tempo, _g.videoOn = stage.videoState === "on", _g.videoAlpha = stage.videoTransparency, _g)]))(); // Run an extra pass on variables (and lists). Only those which are actually // referenced in blocks or monitors should be kept. for (_i = 0, _c = __spreadArray([project.stage], project.sprites, true); _i < _c.length; _i++) { target = _c[_i]; relevantBlocks = void 0; if (target === project.stage) { relevantBlocks = target.blocks.concat(project.sprites.flatMap(function (sprite) { return sprite.blocks; })); } else { relevantBlocks = target.blocks; } usedVariableIds = new Set(); for (_d = 0, relevantBlocks_1 = relevantBlocks; _d < relevantBlocks_1.length; _d++) { block = relevantBlocks_1[_d]; 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 (_e = 0, _f = [target.variables, target.lists]; _e < _f.length; _e++) { varList = _f[_e]; 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, projectFile, 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(); projectFile = inZip.file("project.json"); if (!projectFile) { throw new Error("Missing project.json"); } return [4 /*yield*/, projectFile.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 () { var _b; return __generator(this, function (_c) { // TODO: figure out how to handle missing assets return [2 /*return*/, (_b = inZip.file("".concat(md5, ".").concat(ext))) === null || _b === void 0 ? void 0 : _b.async("arraybuffer")]; }); }); }; return [2 /*return*/, fromSb3JSON(JSON.parse(json), { getAsset: getAsset })]; } }); }); } exports.default = fromSb3;