-
Notifications
You must be signed in to change notification settings - Fork 49
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #295 from FAReTek1/sbeditor
sbeditor (Rival to project_json_capabilites) @ scratchattach.editor
- Loading branch information
Showing
34 changed files
with
7,019 additions
and
129 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
""" | ||
scratchattach.editor (sbeditor v2) - a library for all things sb3 | ||
""" | ||
|
||
from .asset import Asset, Costume, Sound | ||
from .project import Project | ||
from .extension import Extensions, Extension | ||
from .mutation import Mutation, Argument, parse_proc_code | ||
from .meta import Meta, set_meta_platform | ||
from .sprite import Sprite | ||
from .block import Block | ||
from .prim import Prim, PrimTypes | ||
from .backpack_json import load_script as load_script_from_backpack | ||
from .twconfig import TWConfig, is_valid_twconfig | ||
from .inputs import Input, ShadowStatuses | ||
from .field import Field | ||
from .vlb import Variable, List, Broadcast | ||
from .comment import Comment | ||
from .monitor import Monitor | ||
|
||
from .build_defaulting import add_chain, add_comment, add_block |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
from __future__ import annotations | ||
|
||
from dataclasses import dataclass, field | ||
from hashlib import md5 | ||
import requests | ||
|
||
from . import base, commons, sprite, build_defaulting | ||
|
||
|
||
@dataclass(init=True, repr=True) | ||
class AssetFile: | ||
filename: str | ||
_data: bytes = field(repr=False, default=None) | ||
_md5: str = field(repr=False, default=None) | ||
|
||
@property | ||
def data(self): | ||
if self._data is None: | ||
# Download and cache | ||
rq = requests.get(f"https://assets.scratch.mit.edu/internalapi/asset/{self.filename}/get/") | ||
if rq.status_code != 200: | ||
raise ValueError(f"Can't download asset {self.filename}\nIs not uploaded to scratch! Response: {rq.text}") | ||
|
||
self._data = rq.content | ||
|
||
return self._data | ||
|
||
@property | ||
def md5(self): | ||
if self._md5 is None: | ||
self._md5 = md5(self.data).hexdigest() | ||
|
||
return self._md5 | ||
|
||
|
||
class Asset(base.SpriteSubComponent): | ||
def __init__(self, | ||
name: str = "costume1", | ||
file_name: str = "b7853f557e4426412e64bb3da6531a99.svg", | ||
_sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT): | ||
""" | ||
Represents a generic asset. Can be a sound or an image. | ||
https://en.scratch-wiki.info/wiki/Scratch_File_Format#Assets | ||
""" | ||
try: | ||
asset_id, data_format = file_name.split('.') | ||
except ValueError: | ||
raise ValueError(f"Invalid file name: {file_name}, # of '.' in {file_name} ({file_name.count('.')}) != 2; " | ||
f"(too many/few values to unpack)") | ||
self.name = name | ||
|
||
self.id = asset_id | ||
self.data_format = data_format | ||
|
||
super().__init__(_sprite) | ||
|
||
def __repr__(self): | ||
return f"Asset<{self.name!r}>" | ||
|
||
@property | ||
def folder(self): | ||
return commons.get_folder_name(self.name) | ||
|
||
@property | ||
def name_nfldr(self): | ||
return commons.get_name_nofldr(self.name) | ||
|
||
@property | ||
def file_name(self): | ||
return f"{self.id}.{self.data_format}" | ||
|
||
@property | ||
def md5ext(self): | ||
return self.file_name | ||
|
||
@property | ||
def parent(self): | ||
if self.project is None: | ||
return self.sprite | ||
else: | ||
return self.project | ||
|
||
@property | ||
def asset_file(self) -> AssetFile: | ||
for asset_file in self.parent.asset_data: | ||
if asset_file.filename == self.file_name: | ||
return asset_file | ||
|
||
# No pre-existing asset file object; create one and add it to the project | ||
asset_file = AssetFile(self.file_name) | ||
self.project.asset_data.append(asset_file) | ||
return asset_file | ||
|
||
@staticmethod | ||
def from_json(data: dict): | ||
_name = data.get("name") | ||
_file_name = data.get("md5ext") | ||
if _file_name is None: | ||
if "dataFormat" in data and "assetId" in data: | ||
_id = data["assetId"] | ||
_data_format = data["dataFormat"] | ||
_file_name = f"{_id}.{_data_format}" | ||
|
||
return Asset(_name, _file_name) | ||
|
||
def to_json(self) -> dict: | ||
return { | ||
"name": self.name, | ||
|
||
"assetId": self.id, | ||
"md5ext": self.file_name, | ||
"dataFormat": self.data_format, | ||
} | ||
|
||
""" | ||
@staticmethod | ||
def from_file(fp: str, name: str = None): | ||
image_types = ("png", "jpg", "jpeg", "svg") | ||
sound_types = ("wav", "mp3") | ||
# Should save data as well so it can be uploaded to scratch if required (add to project asset data) | ||
... | ||
""" | ||
|
||
|
||
class Costume(Asset): | ||
def __init__(self, | ||
name: str = "Cat", | ||
file_name: str = "b7853f557e4426412e64bb3da6531a99.svg", | ||
|
||
bitmap_resolution=None, | ||
rotation_center_x: int | float = 48, | ||
rotation_center_y: int | float = 50, | ||
_sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT): | ||
""" | ||
A costume. An asset with additional properties | ||
https://en.scratch-wiki.info/wiki/Scratch_File_Format#Costumes | ||
""" | ||
super().__init__(name, file_name, _sprite) | ||
|
||
self.bitmap_resolution = bitmap_resolution | ||
self.rotation_center_x = rotation_center_x | ||
self.rotation_center_y = rotation_center_y | ||
|
||
@staticmethod | ||
def from_json(data): | ||
_asset_load = Asset.from_json(data) | ||
|
||
bitmap_resolution = data.get("bitmapResolution") | ||
|
||
rotation_center_x = data["rotationCenterX"] | ||
rotation_center_y = data["rotationCenterY"] | ||
return Costume(_asset_load.name, _asset_load.file_name, | ||
|
||
bitmap_resolution, rotation_center_x, rotation_center_y) | ||
|
||
def to_json(self) -> dict: | ||
_json = super().to_json() | ||
_json.update({ | ||
"bitmapResolution": self.bitmap_resolution, | ||
"rotationCenterX": self.rotation_center_x, | ||
"rotationCenterY": self.rotation_center_y | ||
}) | ||
return _json | ||
|
||
|
||
class Sound(Asset): | ||
def __init__(self, | ||
name: str = "pop", | ||
file_name: str = "83a9787d4cb6f3b7632b4ddfebf74367.wav", | ||
|
||
rate: int = None, | ||
sample_count: int = None, | ||
_sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT): | ||
""" | ||
A sound. An asset with additional properties | ||
https://en.scratch-wiki.info/wiki/Scratch_File_Format#Sounds | ||
""" | ||
super().__init__(name, file_name, _sprite) | ||
|
||
self.rate = rate | ||
self.sample_count = sample_count | ||
|
||
@staticmethod | ||
def from_json(data): | ||
_asset_load = Asset.from_json(data) | ||
|
||
rate = data.get("rate") | ||
sample_count = data.get("sampleCount") | ||
return Sound(_asset_load.name, _asset_load.file_name, rate, sample_count) | ||
|
||
def to_json(self) -> dict: | ||
_json = super().to_json() | ||
commons.noneless_update(_json, { | ||
"rate": self.rate, | ||
"sampleCount": self.sample_count | ||
}) | ||
return _json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
""" | ||
Module to deal with the backpack's weird JSON format, by overriding with new load methods | ||
""" | ||
from __future__ import annotations | ||
|
||
from . import block, prim, field, inputs, mutation, sprite | ||
|
||
|
||
def parse_prim_fields(_fields: dict[str]) -> tuple[str | None, str | None, str | None]: | ||
""" | ||
Function for reading the fields in a backpack **primitive** | ||
""" | ||
for key, value in _fields.items(): | ||
key: str | ||
value: dict[str, str] | ||
prim_value, prim_name, prim_id = (None,) * 3 | ||
if key == "NUM": | ||
prim_value = value.get("value") | ||
else: | ||
prim_name = value.get("value") | ||
prim_id = value.get("id") | ||
|
||
# There really should only be 1 item, and this function can only return for that item | ||
return prim_value, prim_name, prim_id | ||
return (None,) * 3 | ||
|
||
|
||
class BpField(field.Field): | ||
""" | ||
A normal field but with a different load method | ||
""" | ||
|
||
@staticmethod | ||
def from_json(data: dict[str, str]) -> field.Field: | ||
# We can very simply convert it to the regular format | ||
data = [data.get("value"), data.get("id")] | ||
return field.Field.from_json(data) | ||
|
||
|
||
class BpInput(inputs.Input): | ||
""" | ||
A normal input but with a different load method | ||
""" | ||
|
||
@staticmethod | ||
def from_json(data: dict[str, str]) -> inputs.Input: | ||
# The actual data is stored in a separate prim block | ||
_id = data.get("shadow") | ||
_obscurer_id = data.get("block") | ||
|
||
if _obscurer_id == _id: | ||
# If both the shadow and obscurer are the same, then there is no actual obscurer | ||
_obscurer_id = None | ||
# We cannot work out the shadow status yet since that is located in the primitive | ||
return inputs.Input(None, _id=_id, _obscurer_id=_obscurer_id) | ||
|
||
|
||
class BpBlock(block.Block): | ||
""" | ||
A normal block but with a different load method | ||
""" | ||
|
||
@staticmethod | ||
def from_json(data: dict) -> prim.Prim | block.Block: | ||
""" | ||
Load a block in the **backpack** JSON format | ||
:param data: A dictionary (not list) | ||
:return: A new block/prim object | ||
""" | ||
_opcode = data["opcode"] | ||
|
||
_x, _y = data.get("x"), data.get("y") | ||
if prim.is_prim_opcode(_opcode): | ||
# This is actually a prim | ||
prim_value, prim_name, prim_id = parse_prim_fields(data["fields"]) | ||
return prim.Prim(prim.PrimTypes.find(_opcode, "opcode"), | ||
prim_value, prim_name, prim_id) | ||
|
||
_next_id = data.get("next") | ||
_parent_id = data.get("parent") | ||
|
||
_shadow = data.get("shadow", False) | ||
_top_level = data.get("topLevel", _parent_id is None) | ||
|
||
_inputs = {} | ||
for _input_code, _input_data in data.get("inputs", {}).items(): | ||
_inputs[_input_code] = BpInput.from_json(_input_data) | ||
|
||
_fields = {} | ||
for _field_code, _field_data in data.get("fields", {}).items(): | ||
_fields[_field_code] = BpField.from_json(_field_data) | ||
|
||
if "mutation" in data: | ||
_mutation = mutation.Mutation.from_json(data["mutation"]) | ||
else: | ||
_mutation = None | ||
|
||
return block.Block(_opcode, _shadow, _top_level, _mutation, _fields, _inputs, _x, _y, _next_id=_next_id, | ||
_parent_id=_parent_id) | ||
|
||
|
||
def load_script(_script_data: list[dict]) -> sprite.Sprite: | ||
""" | ||
Loads a script into a sprite from the backpack JSON format | ||
:param _script_data: Backpack script JSON data | ||
:return: a blockchain object containing the script | ||
""" | ||
# Using a sprite since it simplifies things, e.g. local global loading | ||
_blockchain = sprite.Sprite() | ||
|
||
for _block_data in _script_data: | ||
_block = BpBlock.from_json(_block_data) | ||
_block.sprite = _blockchain | ||
_blockchain.blocks[_block_data["id"]] = _block | ||
|
||
_blockchain.link_subcomponents() | ||
return _blockchain |
Oops, something went wrong.