Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
7af67a9
project attrs (no sprites yet)
faretek1 Nov 23, 2024
647cee2
sprites with everything but blocks (comments will need to be attached…
faretek1 Nov 23, 2024
860c111
prims are better now
faretek1 Nov 23, 2024
f488191
vlb prims have a vlb value, not a name/id
faretek1 Nov 23, 2024
491297b
renamed some base classes
faretek1 Nov 23, 2024
2d1e0f9
platform dataclass + global var searching
faretek1 Nov 24, 2024
6a8f040
starting to add blocks
faretek1 Nov 24, 2024
1ab0fed
added mutations
faretek1 Nov 24, 2024
0aa48ec
fixed mutation arg defaulting and added local globals (for importing …
faretek1 Nov 24, 2024
cdd5d99
fields
faretek1 Nov 24, 2024
faccac4
inputs
faretek1 Nov 24, 2024
9b19ae7
minor meta edit
faretek1 Nov 24, 2024
0841332
exporting projects (my ide is supper laggy rn bruh)
faretek1 Nov 25, 2024
7b00168
no need for shutil
faretek1 Nov 26, 2024
1fe2602
need to add mutation json
faretek1 Nov 26, 2024
802b2aa
mutation json
faretek1 Nov 26, 2024
ed228a7
mutation json update (not done)
faretek1 Nov 26, 2024
6025639
fixed mutations
faretek1 Nov 26, 2024
1bebf64
functions for systematically making new ids. will need to make specia…
faretek1 Nov 27, 2024
016cfe8
copy -> dcopy, minor change
faretek1 Nov 27, 2024
0f79f34
sprite exportation
faretek1 Nov 28, 2024
c3614ea
Added misc features + todo list
faretek1 Dec 8, 2024
ba029ba
Loading scripts from backpack + other backpack cleanup
faretek1 Dec 8, 2024
88ca79e
clarification
faretek1 Dec 8, 2024
3a91227
twconfig
faretek1 Dec 8, 2024
c9260e2
import twconfig
faretek1 Dec 8, 2024
197c01b
stack types + can next
faretek1 Dec 9, 2024
c9e8d18
added check in classroom.py for nonexistent classes
faretek1 Dec 9, 2024
3a7e606
docstring
faretek1 Dec 10, 2024
6e8d21c
lots of new stuff for blocks
faretek1 Dec 12, 2024
19e910e
duplicating chains attach chain etc
faretek1 Dec 12, 2024
3d3a7be
more block stuffies :D
faretek1 Dec 12, 2024
f6da6f3
more block featuresssssss
faretek1 Dec 13, 2024
4209a8c
bugfix with comments and ids
faretek1 Dec 14, 2024
2a2960d
tw block detection
faretek1 Dec 14, 2024
b553d49
argument types
faretek1 Dec 14, 2024
f0f8a99
with statements with sprites to reduce the add_block calls needed. pr…
faretek1 Dec 15, 2024
83f9346
better singleton implementation
faretek1 Dec 15, 2024
2b01dae
adds reset link send (maybe review this)
faretek1 Dec 20, 2024
6b02a74
Merge branch 'main' into sbeditor
faretek1 Dec 21, 2024
2254b82
remove email thing since i moved it
faretek1 Dec 21, 2024
84ef9d9
Merge remote-tracking branch 'origin/sbeditor' into sbeditor
faretek1 Dec 21, 2024
24d2f0e
Merge branch 'main' into sbeditor
faretek1 Dec 22, 2024
665109b
Merge remote-tracking branch 'origin/sbeditor' into sbeditor
faretek1 Dec 24, 2024
b0e429e
folder support and some new find methods + a few type hints
faretek1 Dec 24, 2024
59a8008
line break
faretek1 Dec 24, 2024
add93f7
Merge branch 'main' into sbeditor
faretek1 Dec 24, 2024
e829df8
rename yesnt
faretek1 Dec 24, 2024
9fe3d68
Merge remote-tracking branch 'origin/sbeditor' into sbeditor
faretek1 Dec 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions scratchattach/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@
from .site.classroom import Classroom, get_classroom
from .site.user import User, get_user
from .site._base import BaseSiteComponent

from . import editor
21 changes: 21 additions & 0 deletions scratchattach/editor/__init__.py
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
198 changes: 198 additions & 0 deletions scratchattach/editor/asset.py
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
117 changes: 117 additions & 0 deletions scratchattach/editor/backpack_json.py
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
Loading