diff --git a/src/conversion/conversion-layer.mjs b/src/conversion/conversion-layer.mjs index 6f3a88ffe19..1ffaf556479 100644 --- a/src/conversion/conversion-layer.mjs +++ b/src/conversion/conversion-layer.mjs @@ -1,417 +1,415 @@ -export default class ConversionLayer { - static patchApi = { - // Motion blocks: - move: { - opcode: "motion_movesteps", - parameters: ["STEPS"], - }, - goToXY: { - opcode: "motion_gotoxy", - parameters: ["X", "Y"], - }, - goTo: { - opcode: "motion_goto", - parameters: ["TO"], - }, - turnRight: { - opcode: "motion_turnright", - parameters: ["DEGREES"], - }, - turnLeft: { - opcode: "motion_turnleft", - parameters: ["DEGREES"], - }, - pointInDirection: { - opcode: "motion_pointindirection", - parameters: ["DIRECTION"], - }, - pointTowards: { - opcode: "motion_pointtowards", - parameters: ["TOWARDS"], - }, - glide: { - opcode: "motion_glidesecstoxy", - parameters: ["SECS", "X", "Y"], - }, - glideTo: { - opcode: "motion_glideto", - parameters: ["SECS", "TO"], - }, - ifOnEdgeBounce: { - opcode: "motion_ifonedgebounce", - parameters: [], - }, - setRotationStyle: { - opcode: "motion_setrotationstyle", - parameters: ["STYLE"], - }, - changeX: { - opcode: "motion_changexby", - parameters: ["DX"], - }, - setX: { - opcode: "motion_setx", - parameters: ["X"], - }, - changeY: { - opcode: "motion_changeyby", - parameters: ["DY"], - }, - setY: { - opcode: "motion_sety", - parameters: ["Y"], - }, - getX: { - opcode: "motion_xposition", - parameters: [], - }, - getY: { - opcode: "motion_yposition", - parameters: [], - }, - getDirection: { - opcode: "motion_direction", - parameters: [], - }, - goToMenu: { - opcode: "motion_goto_menu", - parameters: ["TO"], - returnParametersInstead: ["TO"], - }, - glideToMenu: { - opcode: "motion_glideto_menu", - parameters: ["TO"], - returnParametersInstead: ["TO"], - }, - pointTowardsMenu: { - opcode: "motion_pointtowards_menu", - parameters: ["TOWARDS"], - returnParametersInstead: ["TOWARDS"], - }, +export default { + // Motion blocks: + move: { + opcode: "motion_movesteps", + parameters: ["STEPS"], + }, + goToXY: { + opcode: "motion_gotoxy", + parameters: ["X", "Y"], + }, + goTo: { + opcode: "motion_goto", + parameters: ["TO"], + }, + turnRight: { + opcode: "motion_turnright", + parameters: ["DEGREES"], + }, + turnLeft: { + opcode: "motion_turnleft", + parameters: ["DEGREES"], + }, + pointInDirection: { + opcode: "motion_pointindirection", + parameters: ["DIRECTION"], + }, + pointTowards: { + opcode: "motion_pointtowards", + parameters: ["TOWARDS"], + }, + glide: { + opcode: "motion_glidesecstoxy", + parameters: ["SECS", "X", "Y"], + }, + glideTo: { + opcode: "motion_glideto", + parameters: ["SECS", "TO"], + }, + ifOnEdgeBounce: { + opcode: "motion_ifonedgebounce", + parameters: [], + }, + setRotationStyle: { + opcode: "motion_setrotationstyle", + parameters: ["STYLE"], + }, + changeX: { + opcode: "motion_changexby", + parameters: ["DX"], + }, + setX: { + opcode: "motion_setx", + parameters: ["X"], + }, + changeY: { + opcode: "motion_changeyby", + parameters: ["DY"], + }, + setY: { + opcode: "motion_sety", + parameters: ["Y"], + }, + getX: { + opcode: "motion_xposition", + parameters: [], + }, + getY: { + opcode: "motion_yposition", + parameters: [], + }, + getDirection: { + opcode: "motion_direction", + parameters: [], + }, + goToMenu: { + opcode: "motion_goto_menu", + parameters: ["TO"], + returnParametersInstead: ["TO"], + }, + glideToMenu: { + opcode: "motion_glideto_menu", + parameters: ["TO"], + returnParametersInstead: ["TO"], + }, + pointTowardsMenu: { + opcode: "motion_pointtowards_menu", + parameters: ["TOWARDS"], + returnParametersInstead: ["TOWARDS"], + }, - // Looks blocks: - say: { - opcode: "looks_say", - parameters: ["MESSAGE"], - }, - sayFor: { - opcode: "looks_sayforsecs", - parameters: ["MESSAGE", "SECS"], - }, - think: { - opcode: "looks_think", - parameters: ["MESSAGE"], - }, - thinkFor: { - opcode: "looks_thinkforsecs", - parameters: ["MESSAGE", "SECS"], - }, - show: { - opcode: "looks_show", - parameters: [], - }, - hide: { - opcode: "looks_hide", - parameters: [], - }, - setCostumeTo: { - opcode: "looks_switchcostumeto", - parameters: ["COSTUME"], - }, - setBackdropTo: { - opcode: "looks_switchbackdropto", - parameters: ["BACKDROP"], - }, - setBackdropToAndWait: { - opcode: "looks_switchbackdroptoandwait", - parameters: ["BACKDROP"], - }, - nextCostume: { - opcode: "looks_nextcostume", - parameters: [], - }, - nextBackdrop: { - opcode: "looks_nextbackdrop", - parameters: [], - }, - changeGraphicEffectBy: { - opcode: "looks_changeeffectby", - parameters: ["EFFECT", "CHANGE"], - }, - setGraphicEffectTo: { - opcode: "looks_seteffectto", - parameters: ["EFFECT", "VALUE"], - }, - clearGraphicEffects: { - opcode: "looks_cleargraphiceffects", - parameters: [], - }, - changeSizeBy: { - opcode: "looks_changesizeby", - parameters: ["CHANGE"], - }, - setSizeTo: { - opcode: "looks_setsizeto", - parameters: ["SIZE"], - }, - setLayerTo: { - opcode: "looks_gotofrontback", - parameters: ["FRONT_BACK"], - }, - changeLayerBy: { - opcode: "looks_goforwardbackwardlayers", - parameters: ["FORWARD_BACKWARD", "NUM"], - }, - getSize: { - opcode: "looks_size", - parameters: [], - }, - getCostume: { - opcode: "looks_costumenumbername", - parameters: [], - }, - getBackdrop: { - opcode: "looks_backdropnumbername", - parameters: [], - }, - costume: { - opcode: "looks_costume", - parameters: ["COSTUME"], - returnParametersInstead: ["COSTUME"], - }, - backdrops: { - opcode: "looks_backdrops", - parameters: ["BACKDROP"], - returnParametersInstead: ["BACKDROP"], - }, + // Looks blocks: + say: { + opcode: "looks_say", + parameters: ["MESSAGE"], + }, + sayFor: { + opcode: "looks_sayforsecs", + parameters: ["MESSAGE", "SECS"], + }, + think: { + opcode: "looks_think", + parameters: ["MESSAGE"], + }, + thinkFor: { + opcode: "looks_thinkforsecs", + parameters: ["MESSAGE", "SECS"], + }, + show: { + opcode: "looks_show", + parameters: [], + }, + hide: { + opcode: "looks_hide", + parameters: [], + }, + setCostumeTo: { + opcode: "looks_switchcostumeto", + parameters: ["COSTUME"], + }, + setBackdropTo: { + opcode: "looks_switchbackdropto", + parameters: ["BACKDROP"], + }, + setBackdropToAndWait: { + opcode: "looks_switchbackdroptoandwait", + parameters: ["BACKDROP"], + }, + nextCostume: { + opcode: "looks_nextcostume", + parameters: [], + }, + nextBackdrop: { + opcode: "looks_nextbackdrop", + parameters: [], + }, + changeGraphicEffectBy: { + opcode: "looks_changeeffectby", + parameters: ["EFFECT", "CHANGE"], + }, + setGraphicEffectTo: { + opcode: "looks_seteffectto", + parameters: ["EFFECT", "VALUE"], + }, + clearGraphicEffects: { + opcode: "looks_cleargraphiceffects", + parameters: [], + }, + changeSizeBy: { + opcode: "looks_changesizeby", + parameters: ["CHANGE"], + }, + setSizeTo: { + opcode: "looks_setsizeto", + parameters: ["SIZE"], + }, + setLayerTo: { + opcode: "looks_gotofrontback", + parameters: ["FRONT_BACK"], + }, + changeLayerBy: { + opcode: "looks_goforwardbackwardlayers", + parameters: ["FORWARD_BACKWARD", "NUM"], + }, + getSize: { + opcode: "looks_size", + parameters: [], + }, + getCostume: { + opcode: "looks_costumenumbername", + parameters: [], + }, + getBackdrop: { + opcode: "looks_backdropnumbername", + parameters: [], + }, + costume: { + opcode: "looks_costume", + parameters: ["COSTUME"], + returnParametersInstead: ["COSTUME"], + }, + backdrops: { + opcode: "looks_backdrops", + parameters: ["BACKDROP"], + returnParametersInstead: ["BACKDROP"], + }, - // Sound blocks: - playSound: { - opcode: "sound_play", - parameters: ["SOUND_MENU"], - }, - playSoundUntilDone: { - opcode: "sound_playuntildone", - parameters: ["SOUND_MENU"], - }, - stopAllSounds: { - opcode: "sound_stopallsounds", - parameters: [], - }, - setSoundEffectTo: { - opcode: "sound_seteffectto", - parameters: ["EFFECT", "VALUE"], - }, - changeSoundEffectBy: { - opcode: "sound_changeeffectby", - parameters: ["EFFECT", "VALUE"], - }, - clearSoundEffects: { - opcode: "sound_cleareffects", - parameters: [], - }, - setVolumeTo: { - opcode: "sound_setvolumeto", - parameters: ["VOLUME"], - }, - changeVolumeBy: { - opcode: "sound_changevolumeby", - parameters: ["VOLUME"], - }, - getVolume: { - opcode: "sound_volume", - parameters: [], - }, - soundsMenu: { - opcode: "sound_sounds_menu", - parameters: ["SOUND_MENU"], - returnParametersInstead: ["SOUND_MENU"], - }, + // Sound blocks: + playSound: { + opcode: "sound_play", + parameters: ["SOUND_MENU"], + }, + playSoundUntilDone: { + opcode: "sound_playuntildone", + parameters: ["SOUND_MENU"], + }, + stopAllSounds: { + opcode: "sound_stopallsounds", + parameters: [], + }, + setSoundEffectTo: { + opcode: "sound_seteffectto", + parameters: ["EFFECT", "VALUE"], + }, + changeSoundEffectBy: { + opcode: "sound_changeeffectby", + parameters: ["EFFECT", "VALUE"], + }, + clearSoundEffects: { + opcode: "sound_cleareffects", + parameters: [], + }, + setVolumeTo: { + opcode: "sound_setvolumeto", + parameters: ["VOLUME"], + }, + changeVolumeBy: { + opcode: "sound_changevolumeby", + parameters: ["VOLUME"], + }, + getVolume: { + opcode: "sound_volume", + parameters: [], + }, + soundsMenu: { + opcode: "sound_sounds_menu", + parameters: ["SOUND_MENU"], + returnParametersInstead: ["SOUND_MENU"], + }, - // Broadcast blocks: - // The way these work in Scratch is that you have to create each broadcast, then it becomes an option in the dropdown - // on the blocks. However, in Patch, it will just accept any string on both the send and recieve. For this reason, - // broadcasts are removed from each Scratch target in the conversion from Scratch to Patch. - broadcast: { - opcode: "event_broadcast", - parameters: ["BROADCAST_INPUT"], - }, - broadcastAndWait: { - opcode: "event_broadcastandwait", - parameters: ["BROADCAST_INPUT"], - }, + // Broadcast blocks: + // The way these work in Scratch is that you have to create each broadcast, then it becomes an option in the dropdown + // on the blocks. However, in Patch, it will just accept any string on both the send and recieve. For this reason, + // broadcasts are removed from each Scratch target in the conversion from Scratch to Patch. + broadcast: { + opcode: "event_broadcast", + parameters: ["BROADCAST_INPUT"], + }, + broadcastAndWait: { + opcode: "event_broadcastandwait", + parameters: ["BROADCAST_INPUT"], + }, - // Sensing blocks: - isTouching: { - opcode: "sensing_touchingobject", - parameters: ["TOUCHINGOBJECTMENU"], - }, - isTouchingColor: { - opcode: "sensing_touchingcolor", - parameters: ["COLOR"], - }, - isColorTouchingColor: { - opcode: "sensing_coloristouchingcolor", - parameters: ["COLOR", "COLOR2"], - }, - distanceTo: { - opcode: "sensing_distanceto", - parameters: ["DISTANCETOMENU"], - }, - getTimer: { - opcode: "sensing_timer", - parameters: [], - }, - resetTimer: { - opcode: "sensing_resettimer", - parameters: [], - }, - getAttributeOf: { - opcode: "sensing_of", - parameters: ["OBJECT", "PROPERTY"], - }, - getMouseX: { - opcode: "sensing_mousex", - parameters: [], - }, - getMouseY: { - opcode: "sensing_mousey", - parameters: [], - }, - isMouseDown: { - opcode: "sensing_mousedown", - parameters: [], - }, - // setDragMode: { - // opcode: "sensing_setdragmode", - // parameters: ["degrees"], - // }, - isKeyPressed: { - opcode: "sensing_keypressed", - parameters: ["KEY_OPTION"], - }, - current: { - opcode: "sensing_current", - parameters: ["CURRENTMENU"], - }, - daysSince2000: { - opcode: "sensing_dayssince2000", - parameters: [], - }, - getLoudness: { - opcode: "sensing_loudness", - parameters: [], - }, - getUsername: { - opcode: "sensing_username", - parameters: [], - }, - ask: { - opcode: "sensing_askandwait", - parameters: ["QUESTION"], - }, - // getAnswer: { - // opcode: "sensing_answer" - // }, - getAnswer: { - opcode: "sensing_answer", - parameters: [], - returnInstead: ["_patchAnswer"], - }, - touchingObjectMenu: { - opcode: "sensing_touchingobjectmenu", - parameters: ["TOUCHINGOBJECTMENU"], - returnParametersInstead: ["TOUCHINGOBJECTMENU"] - }, - distanceToMenu: { - opcode: "sensing_distancetomenu", - parameters: ["DISTANCETOMENU"], - returnParametersInstead: ["DISTANCETOMENU"] - }, - keyOptions: { - opcode: "sensing_keyoptions", - parameters: ["KEY_OPTION"], - returnParametersInstead: ["KEY_OPTION"] - }, - getAttributeOfObjectMenu: { - opcode: "sensing_of_object_menu", - parameters: ["OBJECT"], - returnParametersInstead: ["OBJECT"] - }, + // Sensing blocks: + isTouching: { + opcode: "sensing_touchingobject", + parameters: ["TOUCHINGOBJECTMENU"], + }, + isTouchingColor: { + opcode: "sensing_touchingcolor", + parameters: ["COLOR"], + }, + isColorTouchingColor: { + opcode: "sensing_coloristouchingcolor", + parameters: ["COLOR", "COLOR2"], + }, + distanceTo: { + opcode: "sensing_distanceto", + parameters: ["DISTANCETOMENU"], + }, + getTimer: { + opcode: "sensing_timer", + parameters: [], + }, + resetTimer: { + opcode: "sensing_resettimer", + parameters: [], + }, + getAttributeOf: { + opcode: "sensing_of", + parameters: ["OBJECT", "PROPERTY"], + }, + getMouseX: { + opcode: "sensing_mousex", + parameters: [], + }, + getMouseY: { + opcode: "sensing_mousey", + parameters: [], + }, + isMouseDown: { + opcode: "sensing_mousedown", + parameters: [], + }, + // setDragMode: { + // opcode: "sensing_setdragmode", + // parameters: ["degrees"], + // }, + isKeyPressed: { + opcode: "sensing_keypressed", + parameters: ["KEY_OPTION"], + }, + current: { + opcode: "sensing_current", + parameters: ["CURRENTMENU"], + }, + daysSince2000: { + opcode: "sensing_dayssince2000", + parameters: [], + }, + getLoudness: { + opcode: "sensing_loudness", + parameters: [], + }, + getUsername: { + opcode: "sensing_username", + parameters: [], + }, + ask: { + opcode: "sensing_askandwait", + parameters: ["QUESTION"], + }, + // getAnswer: { + // opcode: "sensing_answer" + // }, + getAnswer: { + opcode: "sensing_answer", + parameters: [], + returnInstead: ["_patchAnswer"], + }, + touchingObjectMenu: { + opcode: "sensing_touchingobjectmenu", + parameters: ["TOUCHINGOBJECTMENU"], + returnParametersInstead: ["TOUCHINGOBJECTMENU"] + }, + distanceToMenu: { + opcode: "sensing_distancetomenu", + parameters: ["DISTANCETOMENU"], + returnParametersInstead: ["DISTANCETOMENU"] + }, + keyOptions: { + opcode: "sensing_keyoptions", + parameters: ["KEY_OPTION"], + returnParametersInstead: ["KEY_OPTION"] + }, + getAttributeOfObjectMenu: { + opcode: "sensing_of_object_menu", + parameters: ["OBJECT"], + returnParametersInstead: ["OBJECT"] + }, - wait: { - opcode: "control_wait", - parameters: ["DURATION"], - }, - // waitUntil: { - // opcode: "control_wait_until", - // parameters: ["condition"], - // }, - stop: { - opcode: "control_stop", - parameters: ["STOP_OPTION"], - }, - createClone: { - opcode: "control_create_clone_of", - parameters: ["CLONE_OPTION"], - }, - deleteClone: { - opcode: "control_delete_this_clone", - parameters: [], - }, - createCloneMenu: { - opcode: "control_create_clone_of_menu", - parameters: ["CLONE_OPTION"], - returnParametersInstead: ["CLONE_OPTION"], - }, + wait: { + opcode: "control_wait", + parameters: ["DURATION"], + }, + // waitUntil: { + // opcode: "control_wait_until", + // parameters: ["condition"], + // }, + stop: { + opcode: "control_stop", + parameters: ["STOP_OPTION"], + }, + createClone: { + opcode: "control_create_clone_of", + parameters: ["CLONE_OPTION"], + }, + deleteClone: { + opcode: "control_delete_this_clone", + parameters: [], + }, + createCloneMenu: { + opcode: "control_create_clone_of_menu", + parameters: ["CLONE_OPTION"], + returnParametersInstead: ["CLONE_OPTION"], + }, - erasePen: { - opcode: "pen_clear", - parameters: [], - }, - stampPen: { - opcode: "pen_stamp", - parameters: [], - }, - penDown: { - opcode: "pen_penDown", - parameters: [], - }, - penUp: { - opcode: "pen_penUp", - parameters: [], - }, - setPenColor: { - opcode: "pen_setPenColorToColor", - parameters: ["COLOR"], - }, - changePenEffect: { - opcode: "pen_changePenColorParamBy", - parameters: ["COLOR_PARAM", "VALUE"], - }, - setPenEffect: { - opcode: "pen_setPenColorParamTo", - parameters: ["COLOR_PARAM", "VALUE"], - }, - changePenSize: { - opcode: "pen_changePenSizeBy", - parameters: ["SIZE"], - }, - setPenSize: { - opcode: "pen_setPenSizeTo", - parameters: ["SIZE"], - }, - penEffectMenu: { - opcode: "pen_menu_colorParam", - // The opcode should be camelcase; this isn't a mistake (unless it isn't camelcase, in which case it shouls - // be made camelcase). - parameters: ["colorParam"], - returnParametersInstead: ["colorParam"], - }, + erasePen: { + opcode: "pen_clear", + parameters: [], + }, + stampPen: { + opcode: "pen_stamp", + parameters: [], + }, + penDown: { + opcode: "pen_penDown", + parameters: [], + }, + penUp: { + opcode: "pen_penUp", + parameters: [], + }, + setPenColor: { + opcode: "pen_setPenColorToColor", + parameters: ["COLOR"], + }, + changePenEffect: { + opcode: "pen_changePenColorParamBy", + parameters: ["COLOR_PARAM", "VALUE"], + }, + setPenEffect: { + opcode: "pen_setPenColorParamTo", + parameters: ["COLOR_PARAM", "VALUE"], + }, + changePenSize: { + opcode: "pen_changePenSizeBy", + parameters: ["SIZE"], + }, + setPenSize: { + opcode: "pen_setPenSizeTo", + parameters: ["SIZE"], + }, + penEffectMenu: { + opcode: "pen_menu_colorParam", + // The opcode should be camelcase; this isn't a mistake (unless it isn't camelcase, in which case it shouls + // be made camelcase). + parameters: ["colorParam"], + returnParametersInstead: ["colorParam"], + }, - endThread: { - opcode: "core_endthread", - parameters: [], - }, - }; -} \ No newline at end of file + endThread: { + opcode: "core_endthread", + parameters: [], + }, +}; diff --git a/src/conversion/scratch-conversion-control.mjs b/src/conversion/scratch-conversion-control.mjs index d76691b19af..2bea5a70880 100644 --- a/src/conversion/scratch-conversion-control.mjs +++ b/src/conversion/scratch-conversion-control.mjs @@ -1,102 +1,84 @@ -import PatchTargetThread from "./patch-target-thread.mjs"; -import ScratchBlock from "./scratch-block.mjs"; - -import { indentLines, processInputs } from "./scratch-conversion-helper.mjs"; - -export default class ScratchConversionControl { - /** +/** * - * @param {Object.} blocks + * @param {*} target * @param {string} blockId - * @param {Object. 0) { + if (blocks[hatId].opcode === "event_whenkeypressed") { + // eslint-disable-next-line prefer-destructuring + thread.triggerEventOption = blocks[hatId].fields[hatFieldsKeys[0]][0].toUpperCase(); + } else { + // eslint-disable-next-line prefer-destructuring + thread.triggerEventOption = blocks[hatId].fields[hatFieldsKeys[0]][0]; + } + } + + // Convert code + let currentBlockId = nextId; + while (currentBlockId) { + const currentBlock = blocks[currentBlockId]; + // Store a copy of the opcode so we don't have to keep doing currentBlock.opcode + const { opcode } = currentBlock; + + // TODO: figure out nested blocks + + const patchApiKeys = Object.keys(patchApi); + + // Convert the block + // Duplicates shouldn't exist in the translation API, but if they do the first entry will be used + let patchKey = null; + for (let i = 0; i < patchApiKeys.length; i++) { + const key = patchApiKeys[i]; + + if (patchApi[key].opcode === opcode) { + patchKey = key; + break; + } + } + + if (!patchKey) { + if (opcode.substring(0, 8) === "control_") { + const conversionResult = convertControlBlock(target, currentBlockId, processInputs, indentLines, convertBlocksPart); + thread.script += `${conversionResult}\n`; + } else if (opcode.substring(0, 9) === "operator_") { + const conversionResult = convertOperatorBlock(target, currentBlockId, processInputs); + thread.script += `${conversionResult}\n`; + } else if (opcode.substring(0, 5) === "data_") { + const conversionResult = convertDataBlock(target, currentBlockId, processInputs); + thread.script += `${conversionResult}\n`; + } else { + // Couldn't find the opcode in the map. + console.error("Error translating from scratch to patch. Unable to find the key for the opcode %s.", opcode); + } + } else { + // const inputsKeys = Object.keys(currentBlock.inputs); + const detectedArgs = processInputs(target, currentBlockId, true, false); + + let patchCode = ""; + + const conversionLayerResult = patchApi[patchKey]; + if (conversionLayerResult.hasOwnProperty("returnInstead")) { + let patchArgs = ""; + for (let i = 0; i < conversionLayerResult.returnInstead.length; i++) { + const val = conversionLayerResult.returnInstead[i]; + + // Add options to change this based on language later. + if (patchArgs !== "") { + patchArgs += ", "; + } + + patchArgs += val; + } + + patchCode = `${patchArgs}\n`; + } else if (conversionLayerResult.hasOwnProperty("returnParametersInstead")) { + let patchArgs = ""; + for (let i = 0; i < conversionLayerResult.returnParametersInstead.length; i++) { + const parameter = conversionLayerResult.returnParametersInstead[i];// .toUpperCase(); + + // Add options to change this based on language later. + if (patchArgs !== "") { + patchArgs += ", "; + } + + if (detectedArgs[parameter]) { + patchArgs += detectedArgs[parameter]; + } else { + console.warn("Couldn't find parameter with opcode %s.", parameter); + patchArgs += "\"# Error: couldn't find the parameter to go here.\""; + } + } + + patchCode = `${patchArgs}\n`; + } else { + let patchArgs = ""; + for (let i = 0; i < conversionLayerResult.parameters.length; i++) { + const parameter = conversionLayerResult.parameters[i];// .toUpperCase(); + + // Add options to change this based on language later. + if (patchArgs !== "") { + patchArgs += ", "; + } + + if (detectedArgs[parameter]) { + patchArgs += detectedArgs[parameter]; + } else { + console.warn("Couldn't find parameter with opcode %s.", parameter); + patchArgs += "\"# Error: couldn't find the parameter to go here.\""; + } + } + + // Handle a special case: Patch implements the Ask block differently + // TODO: should this be a global variable? + if (currentBlock.opcode === "sensing_askandwait") { + patchKey = `_patchAnswer = ${patchKey}`; + } + + // Join all the bits and pieces together. Add options to change this based on language later. + patchCode = `${patchKey}(${patchArgs})\n` + } + + thread.script += patchCode; + } + + // Next block + currentBlockId = currentBlock.next; + } + + return thread; +} \ No newline at end of file diff --git a/src/conversion/scratch-conversion-operator.mjs b/src/conversion/scratch-conversion-operator.mjs index 64df8af9295..02ee2d7c1e0 100644 --- a/src/conversion/scratch-conversion-operator.mjs +++ b/src/conversion/scratch-conversion-operator.mjs @@ -1,165 +1,155 @@ -import { processInputs } from "./scratch-conversion-helper.mjs" -import ScratchBlock from "./scratch-block.mjs"; - -export default class ScratchConversionOperator { - /** +/** * - * @param {Object.} blocks + * @param {*} target * @param {string} blockId - * @param {Object. ${OPERAND2}`; - break; - } - case "operator_and": { - const { OPERAND1, OPERAND2 } = processInputs(blocks, currentBlockId, currentBlock, patchApi, patchApiKeys, convertBlocksPart, true, true); - - script += `${OPERAND1} and ${OPERAND2}`; - break; - } - case "operator_or": { - const { OPERAND1, OPERAND2 } = processInputs(blocks, currentBlockId, currentBlock, patchApi, patchApiKeys, convertBlocksPart, true, true); - - script += `${OPERAND1} or ${OPERAND2}`; - break; - } - case "operator_not": { - const { OPERAND } = processInputs(blocks, currentBlockId, currentBlock, patchApi, patchApiKeys, convertBlocksPart, true, true); - - script += `not ${OPERAND}`; - break; - } - case "operator_random": { - const { FROM, TO } = processInputs(blocks, currentBlockId, currentBlock, patchApi, patchApiKeys, convertBlocksPart, true, true); - - script += `patch_random(${FROM}, ${TO})`; - break; - } - case "operator_join": { - const { STRING1, STRING2 } = processInputs(blocks, currentBlockId, currentBlock, patchApi, patchApiKeys, convertBlocksPart, true, false); - - // TODO: is there a more pythonic way to implement this? - script += `${STRING1} + ${STRING2}`; - break; - } - case "operator_letter_of": { - const { STRING } = processInputs(blocks, currentBlockId, currentBlock, patchApi, patchApiKeys, convertBlocksPart, true, false); - const { LETTER } = processInputs(blocks, currentBlockId, currentBlock, patchApi, patchApiKeys, convertBlocksPart, true, true); - - script += `${STRING}[${LETTER - 1}]`; - break; - } - case "operator_length": { - const { STRING } = processInputs(blocks, currentBlockId, currentBlock, patchApi, patchApiKeys, convertBlocksPart, true, false); - - script += `len(${STRING})`; - break; - } - case "operator_contains": { - const { STRING1, STRING2 } = processInputs(blocks, currentBlockId, currentBlock, patchApi, patchApiKeys, convertBlocksPart, true, false); - - script += `${STRING2} in ${STRING1}`; - break; - } - case "operator_mod": { - const { NUM1, NUM2 } = processInputs(blocks, currentBlockId, currentBlock, patchApi, patchApiKeys, convertBlocksPart, true); - - script += `${NUM1} % ${NUM2}`; - break; - } - case "operator_round": { - const { NUM } = processInputs(blocks, currentBlockId, currentBlock, patchApi, patchApiKeys, convertBlocksPart, true); - - script += `round(${NUM})`; - break; - } - case "operator_mathop": { - const { OPERATOR } = processInputs(blocks, currentBlockId, currentBlock, patchApi, patchApiKeys, convertBlocksPart, true); - const { NUM } = processInputs(blocks, currentBlockId, currentBlock, patchApi, patchApiKeys, convertBlocksPart, true, true); - - // Remove the quotation marks that processInputs adds - const formattedOperator = OPERATOR.substring(1, OPERATOR.length - 1); - - const mathOpsDict = { - "abs": `abs(${ NUM })`, - "ceiling": `math.ceil(${ NUM })`, - "sqrt": `math.sqrt(${ NUM })`, - "floor": `math.floor(${ NUM })`, - /* Trig in scratch uses degrees. To keep this consistent, we must convert the inputs of - trig (but not inverse trig) */ - "sin": `math.sin(math.radians(${ NUM }))`, - "cos": `math.cos(math.radians(${ NUM }))`, - "tan": `math.tan(math.radians(${ NUM }))`, - "asin": `math.degrees(math.asin(${ NUM }))`, - "acos": `math.degrees(math.acos(${ NUM }))`, - "atan": `math.degrees(math.atan(${ NUM }))`, - /* in Python, math.log defaults to base e, not base 10 */ - "ln": `math.log(${ NUM })`, - "log": `math.log(${ NUM }, 10)`, - "e ^": `pow(math.e, ${ NUM })`, /* `math.exp(${ NUM })`, */ - "10 ^": `pow(10, ${ NUM })` - }; - - script += mathOpsDict[formattedOperator]; - break; - } - default: { - break; - } - } - - return script; +export default function convertOperatorBlock(target, currentBlockId, processInputs) { + const currentBlock = target.blocks[currentBlockId]; + const { opcode } = currentBlock; + + let script = ""; + + switch (opcode) { + case "operator_add": { + const { NUM1, NUM2 } = processInputs(target, currentBlockId, true); + + script += `${NUM1} + ${NUM2}`; + break; + } + case "operator_subtract": { + const { NUM1, NUM2 } = processInputs(target, currentBlockId, true); + + script += `${NUM1} - ${NUM2}`; + break; + } + case "operator_multiply": { + const { NUM1, NUM2 } = processInputs(target, currentBlockId, true); + + script += `${NUM1} * ${NUM2}`; + break; + } + case "operator_divide": { + const { NUM1, NUM2 } = processInputs(target, currentBlockId, true); + + script += `${NUM1} / ${NUM2}`; + break; + } + case "operator_lt": { + const { OPERAND1, OPERAND2 } = processInputs(target, currentBlockId, true, true); + + script += `${OPERAND1} < ${OPERAND2}`; + break; + } + case "operator_equals": { + const { OPERAND1, OPERAND2 } = processInputs(target, currentBlockId, true, true); + + script += `${OPERAND1} == ${OPERAND2}`; + break; + } + case "operator_gt": { + const { OPERAND1, OPERAND2 } = processInputs(target, currentBlockId, true, true); + + script += `${OPERAND1} > ${OPERAND2}`; + break; + } + case "operator_and": { + const { OPERAND1, OPERAND2 } = processInputs(target, currentBlockId, true, true); + + script += `${OPERAND1} and ${OPERAND2}`; + break; + } + case "operator_or": { + const { OPERAND1, OPERAND2 } = processInputs(target, currentBlockId, true, true); + + script += `${OPERAND1} or ${OPERAND2}`; + break; + } + case "operator_not": { + const { OPERAND } = processInputs(target, currentBlockId, true, true); + + script += `not ${OPERAND}`; + break; + } + case "operator_random": { + const { FROM, TO } = processInputs(target, currentBlockId, true, true); + + script += `patch_random(${FROM}, ${TO})`; + break; + } + case "operator_join": { + const { STRING1, STRING2 } = processInputs(target, currentBlockId, true, false); + + // TODO: is there a more pythonic way to implement this? + script += `${STRING1} + ${STRING2}`; + break; + } + case "operator_letter_of": { + const { STRING } = processInputs(target, currentBlockId, true, false); + const { LETTER } = processInputs(target, currentBlockId, true, true); + + script += `${STRING}[${LETTER - 1}]`; + break; + } + case "operator_length": { + const { STRING } = processInputs(target, currentBlockId, true, false); + + script += `len(${STRING})`; + break; + } + case "operator_contains": { + const { STRING1, STRING2 } = processInputs(target, currentBlockId, true, false); + + script += `${STRING2} in ${STRING1}`; + break; + } + case "operator_mod": { + const { NUM1, NUM2 } = processInputs(target, currentBlockId, true); + + script += `${NUM1} % ${NUM2}`; + break; + } + case "operator_round": { + const { NUM } = processInputs(target, currentBlockId, true); + + script += `round(${NUM})`; + break; + } + case "operator_mathop": { + const { OPERATOR } = processInputs(target, currentBlockId, true); + const { NUM } = processInputs(target, currentBlockId, true, true); + + // Remove the quotation marks that processInputs adds + const formattedOperator = OPERATOR.substring(1, OPERATOR.length - 1); + + const mathOpsDict = { + "abs": `abs(${NUM})`, + "ceiling": `math.ceil(${NUM})`, + "sqrt": `math.sqrt(${NUM})`, + "floor": `math.floor(${NUM})`, + /* Trig in scratch uses degrees. To keep this consistent, we must convert the inputs of + trig (but not inverse trig) */ + "sin": `math.sin(math.radians(${NUM}))`, + "cos": `math.cos(math.radians(${NUM}))`, + "tan": `math.tan(math.radians(${NUM}))`, + "asin": `math.degrees(math.asin(${NUM}))`, + "acos": `math.degrees(math.acos(${NUM}))`, + "atan": `math.degrees(math.atan(${NUM}))`, + /* in Python, math.log defaults to base e, not base 10 */ + "ln": `math.log(${NUM})`, + "log": `math.log(${NUM}, 10)`, + "e ^": `pow(math.e, ${NUM})`, /* `math.exp(${ NUM })`, */ + "10 ^": `pow(10, ${NUM})` + }; + + script += mathOpsDict[formattedOperator]; + break; + } + default: { + break; + } } -} \ No newline at end of file + + return script; +} diff --git a/src/conversion/scratch-conversion.mjs b/src/conversion/scratch-conversion.mjs index d6fd438c635..3c9f82e0cf8 100644 --- a/src/conversion/scratch-conversion.mjs +++ b/src/conversion/scratch-conversion.mjs @@ -1,25 +1,16 @@ import JSZip from "jszip"; -import ConversionLayer from "./conversion-layer.mjs"; import Scratch3EventBlocks from "../blocks/scratch3_event.mjs"; -import PatchTargetThread from "./patch-target-thread.mjs"; - -import ScratchConversionControl from "./scratch-conversion-control.mjs"; -import ScratchConversionOperator from "./scratch-conversion-operator.mjs"; - -import { processInputs } from "./scratch-conversion-helper.mjs"; import Scratch3ControlBlocks from "../blocks/scratch3_control.mjs"; +import { convertBlocksPart } from "./scratch-conversion-helper.mjs"; + export default class ScratchConverter { data = null; scratchJson = null; - scratchControlConverter = new ScratchConversionControl(); - - scratchOperatorConverter = new ScratchConversionOperator(); - /** * * @param {ArrayBuffer} scratchData An ArrayBuffer representation of the .sb3 file to convert @@ -80,6 +71,8 @@ export default class ScratchConverter { const jsonDataString = await zip.files["project.json"].async("text").then((text) => text); const vmState = JSON.parse(jsonDataString); + const globalVariables = []; + // This function will convert each target's blocks and local variables into Patch code. // Then, it will remove the blocks from the JSON (not strictly necessary) and handle backgrounds and other // things that Patch and Scratch store differently. Also, everything will be moved to being a child of a json @@ -88,7 +81,7 @@ export default class ScratchConverter { // Step 1: blocks + variables to code; then add code for (let i = 0; i < vmState.targets.length; i++) { - vmState.targets[i].threads = this.convertTargetBlocks(vmState.targets[i].blocks, vmState.targets[i].variables); + vmState.targets[i].threads = this.convertTargetBlocks(vmState.targets[i]); } // Step 2: remove blocks (this isn't strictly necessary) and variables + broadcasts (this is necessary) @@ -96,7 +89,19 @@ export default class ScratchConverter { // remover however. for (let i = 0; i < vmState.targets.length; i++) { vmState.targets[i].blocks = {}; + const variableKeys = Object.keys(vmState.targets[i].variables); + variableKeys.forEach(key => { + const variable = vmState.targets[i].variables[key]; + console.log(variable); + if (vmState.targets[i].isStage) { + // In Scratch, global variables are actually stored as sprite variables on the stage. + globalVariables.push({name: variable[0], value: variable[1]}); + } else { + globalVariables.push({name: `${vmState.targets[i].name}_${variable[0]}`, value: variable[1]}); + } + }); vmState.targets[i].variables = {}; + vmState.targets[i].lists = {}; vmState.targets[i].broadcasts = {}; } @@ -108,178 +113,14 @@ export default class ScratchConverter { // Step 4: make everything a child of "vmstate" and add global variables // TODO: global variables - const baseJson = { vmstate: vmState, globalVariables: [] }; + console.log(globalVariables); + const baseJson = { vmstate: vmState, globalVariables: globalVariables }; // Step 4: convert this back to a blob, make everything a child of "vmstate", and return it. const newJsonBlob = new Blob([JSON.stringify(baseJson)], { type: "application/json" }); return newJsonBlob; } - convertBlocksPart(blocks, hatId, nextId, patchApi, patchApiKeys) { - const thread = new PatchTargetThread(); - - thread.triggerEventId = blocks[hatId].opcode; - console.log("blocks[hatId].opcode", blocks[hatId].opcode); - // TODO: triggerEventOption - const hatFieldsKeys = Object.keys(blocks[hatId].fields); - if (hatFieldsKeys && hatFieldsKeys.length > 0) { - if (blocks[hatId].opcode === "event_whenkeypressed") { - // eslint-disable-next-line prefer-destructuring - thread.triggerEventOption = blocks[hatId].fields[hatFieldsKeys[0]][0].toUpperCase(); - } else { - // eslint-disable-next-line prefer-destructuring - thread.triggerEventOption = blocks[hatId].fields[hatFieldsKeys[0]][0]; - } - } - - // Convert code - let currentBlockId = nextId; - while (currentBlockId) { - const currentBlock = blocks[currentBlockId]; - // Store a copy of the opcode so we don't have to keep doing currentBlock.opcode - const { opcode } = currentBlock; - - // TODO: figure out nested blocks - - // Convert the block - // Duplicates shouldn't exist in the translation API, but if they do the first entry will be used - let patchKey = null; - for (let i = 0; i < patchApiKeys.length; i++) { - const key = patchApiKeys[i]; - - if (patchApi[key].opcode === opcode) { - patchKey = key; - break; - } - } - - if (!patchKey) { - if (opcode.substring(0, 8) === "control_") { - const conversionResult = this.scratchControlConverter.convertControlBlock(blocks, currentBlockId, patchApi, patchApiKeys, this.convertBlocksPart, this); - thread.script += `${conversionResult}\n`; - } else if (opcode.substring(0, 9) === "operator_") { - const conversionResult = this.scratchOperatorConverter.convertOperatorBlock(blocks, currentBlockId, patchApi, patchApiKeys, this.convertBlocksPart, this); - thread.script += `${conversionResult}\n`; - } else { - // Couldn't find the opcode in the map. - console.error("Error translating from scratch to patch. Unable to find the key for the opcode %s.", opcode); - } - } else { - // const inputsKeys = Object.keys(currentBlock.inputs); - const detectedArgs = processInputs(blocks, currentBlockId, currentBlock, patchApi, patchApiKeys, this.convertBlocksPart.bind(this), true, false); - - /* for (let i = 0; i < inputsKeys.length; i++) { - const inputsKey = inputsKeys[i]; - - // Add options to change this based on language later. - if (patchArgs !== "") { - patchArgs += ", "; - } - - // TODO: validate this more - let newArg = ""; - - const argType = getArgType(currentBlock.inputs[inputsKey]) - - switch (argType) { - case 0: { - newArg = `${currentBlock.inputs[inputsKey][1][1]}`; - break; - } - case 1: { - newArg = `"${currentBlock.inputs[inputsKey][1][1]}"`; - break; - } - case 2: { - // Nested block - const subThread = this.convertBlocksPart(blocks, currentBlockId, currentBlock.inputs[inputsKey][1], patchApi, patchApiKeys); - // remove the newline - newArg = subThread.script.substring(0, subThread.script.length - 1); - break; - } - default: { - console.error("Unknown argType."); - break; - } - } - - patchArgs += newArg; - } */ - - let patchCode = ""; - - const conversionLayerResult = patchApi[patchKey]; - if (conversionLayerResult.hasOwnProperty("returnInstead")) { - let patchArgs = ""; - for (let i = 0; i < conversionLayerResult.returnInstead.length; i++) { - const val = conversionLayerResult.returnInstead[i]; - - // Add options to change this based on language later. - if (patchArgs !== "") { - patchArgs += ", "; - } - - patchArgs += val; - } - - patchCode = `${patchArgs}\n`; - } else if (conversionLayerResult.hasOwnProperty("returnParametersInstead")) { - let patchArgs = ""; - for (let i = 0; i < conversionLayerResult.returnParametersInstead.length; i++) { - const parameter = conversionLayerResult.returnParametersInstead[i];// .toUpperCase(); - - // Add options to change this based on language later. - if (patchArgs !== "") { - patchArgs += ", "; - } - - if (detectedArgs[parameter]) { - patchArgs += detectedArgs[parameter]; - } else { - console.warn("Couldn't find parameter with opcode %s.", parameter); - patchArgs += "\"# Error: couldn't find the parameter to go here.\""; - } - } - - patchCode = `${patchArgs}\n`; - } else { - let patchArgs = ""; - for (let i = 0; i < conversionLayerResult.parameters.length; i++) { - const parameter = conversionLayerResult.parameters[i];// .toUpperCase(); - - // Add options to change this based on language later. - if (patchArgs !== "") { - patchArgs += ", "; - } - - if (detectedArgs[parameter]) { - patchArgs += detectedArgs[parameter]; - } else { - console.warn("Couldn't find parameter with opcode %s.", parameter); - patchArgs += "\"# Error: couldn't find the parameter to go here.\""; - } - } - - // Handle a special case: Patch implements the Ask block differently - // TODO: should this be a global variable? - if (currentBlock.opcode === "sensing_askandwait") { - patchKey = `_patchAnswer = ${patchKey}`; - } - - // Join all the bits and pieces together. Add options to change this based on language later. - patchCode = `${patchKey}(${patchArgs})\n` - } - - thread.script += patchCode; - } - - // Next block - currentBlockId = currentBlock.next; - } - - return thread; - } - /** * Converts an object representation of a Scratch target's blocks into an object * representation of the corresponding Patch threads and thread code. @@ -288,10 +129,12 @@ export default class ScratchConverter { * @param {Object.} variables * @returns {PatchTargetThread[]} An array of object representations of the patch threads */ - convertTargetBlocks(blocks, variables) { + convertTargetBlocks(target) { // TODO: convert variables // https://en.scratch-wiki.info/wiki/Scratch_File_Format#Blocks + const {blocks} = target; + const blocksKeys = Object.keys(blocks); const returnVal = []; @@ -310,11 +153,8 @@ export default class ScratchConverter { } }); - const { patchApi } = ConversionLayer; - const patchApiKeys = Object.keys(patchApi); - hatLocations.forEach(hatId => { - const returnValPart = this.convertBlocksPart(blocks, hatId, blocks[hatId].next, patchApi, patchApiKeys); + const returnValPart = convertBlocksPart(target, hatId, blocks[hatId].next); if (returnValPart.script.includes("math.")) { returnValPart.script = `import math\n\n${ returnValPart.script }`;