Skip to content

Commit 18feec9

Browse files
committed
up
1 parent 06e93a9 commit 18feec9

File tree

5 files changed

+229
-1
lines changed

5 files changed

+229
-1
lines changed

src/GUI/__init__.py

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/fsg/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# fsg(Fake Scratch Git):Scratch伪版本控制系统
2+
3+
## TODO
4+
- [] 实现找出diff

src/fsg/backend.py

Whitespace-only changes.

src/fsg/diff.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from typing import List, Dict, Any, Set, Iterator, Union
2+
from dataclasses import dataclass
3+
from src.utils.objects import Block
4+
5+
@dataclass
6+
class Change: pass
7+
8+
@dataclass
9+
class Add(Change):
10+
block: 'Block'
11+
12+
@dataclass
13+
class Reduce(Change):
14+
block: 'Block'
15+
16+
@dataclass
17+
class Modify(Change):
18+
old_block: 'Block'
19+
new_block: 'Block'
20+
changed_attrs: Dict[str, Any]
21+
22+
@dataclass
23+
class Unchanged(Change):
24+
block: 'Block'
25+
26+
class Block:
27+
def __init__(self, id: str, **kwargs):
28+
self.id = id
29+
self.opcode = kwargs.get('opcode', '')
30+
self.next = kwargs.get('next', None)
31+
self.parent = kwargs.get('parent', None)
32+
self.inputs = kwargs.get('inputs', {})
33+
self.fields = kwargs.get('fields', {})
34+
self.shadow = kwargs.get('shadow', False)
35+
self.topLevel = kwargs.get('topLevel', False)
36+
self.x = kwargs.get('x', None)
37+
self.y = kwargs.get('y', None)
38+
self.isHead = all([self.x, self.y])
39+
40+
def __repr__(self):
41+
return f"Block(id={self.id!r}, opcode={self.opcode!r}, ...)"
42+
43+
# 硬编码需要比较的属性(排除 x, y)
44+
COMPARE_ATTRS = {'opcode', 'next', 'parent', 'inputs', 'fields', 'shadow', 'topLevel', 'isHead'}
45+
46+
def compare_blocks_optimized(
47+
old_blocks: List[Block],
48+
new_blocks: List[Block],
49+
ignore_attrs: Set[str] = {'x', 'y'}
50+
) -> Iterator[Change]:
51+
"""
52+
高效比较两个 Block 列表,返回变更迭代器(内存优化)。
53+
54+
Args:
55+
old_blocks: 旧 Block 列表
56+
new_blocks: 新 Block 列表
57+
ignore_attrs: 要忽略的属性
58+
59+
Yields:
60+
Change: Add/Reduce/Modify/Unchanged 实例
61+
"""
62+
old_dict = {b.id: b for b in old_blocks}
63+
new_dict = {b.id: b for b in new_blocks}
64+
65+
# 先处理新增的 Block
66+
for block_id, new_block in new_dict.items():
67+
if block_id not in old_dict:
68+
yield Add(new_block)
69+
70+
# 再处理删除和修改的 Block
71+
for block_id, old_block in old_dict.items():
72+
if block_id not in new_dict:
73+
yield Reduce(old_block)
74+
else:
75+
new_block = new_dict[block_id]
76+
changed_attrs = {}
77+
78+
# 仅比较指定的属性(提前终止)
79+
for attr in COMPARE_ATTRS - ignore_attrs:
80+
old_val = getattr(old_block, attr, None)
81+
new_val = getattr(new_block, attr, None)
82+
if old_val != new_val:
83+
changed_attrs[attr] = (old_val, new_val)
84+
break # 只要有一个不同就标记为修改
85+
86+
if changed_attrs:
87+
yield Modify(old_block, new_block, changed_attrs)
88+
else:
89+
yield Unchanged(old_block)

src/utils/mypath.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
from .config import os,json,USERSET,LOCALDATE,THISPATH,log,Optional,Union,Tuple,repath,init_path
2+
init_path()
3+
4+
import zipfile
5+
import xml.etree.ElementTree as ET
6+
import shutil
7+
8+
from cairosvg import svg2png
9+
from PIL import Image
10+
11+
class PathTool:
12+
def __init__(self,fp:Optional[str|tuple[str, str]]=None,mode='p'):
13+
'''mode = 'p': fp是一个文件路径;
14+
mode = 'n': fp是一个文件名;
15+
mode = 'd': fp是一个目录路径;
16+
mode = 'j': 只合并路径,fp的类型为tuple[str,str]。'''
17+
if fp:
18+
if isinstance(fp,str):
19+
fp=os.path.normpath(fp)
20+
self.NAME:str
21+
match mode:
22+
case 'p':
23+
self.DIR=os.path.dirname(fp)
24+
self.FILE=os.path.basename(fp)
25+
self.NAME,self.SUFFIX=os.path.splitext(self.FILE)
26+
case 'n':
27+
self.FILE=fp
28+
self.NAME,self.SUFFIX=os.path.splitext(fp)
29+
case 'd':
30+
self.DIR=fp
31+
self.NAME=os.path.basename(fp)
32+
elif mode=='j':
33+
self.j=os.path.join(*(os.path.normpath(p) for p in fp))
34+
def rmlog(self,dirpath:str,count:int=0):
35+
# 获取目录中的所有文件
36+
files = os.listdir(dirpath)
37+
if len(files) >= count:
38+
# 过滤出文件,而不是目录
39+
files = [f for f in files if os.path.isfile(os.path.join(dirpath, f))]
40+
if count:
41+
# 获取每个文件的修改时间,并按照修改时间排序
42+
files.sort(key=lambda f: os.path.getmtime(os.path.join(dirpath, f)))
43+
for i in range(count):
44+
os.remove(os.path.join(dirpath,files[i]))
45+
else:
46+
for f in files:
47+
os.remove(os.path.join(dirpath,f))
48+
def join(self,args:Union[str,Tuple[str,...]]=("",)):
49+
if hasattr(PathTool,'j'):
50+
return self.j
51+
elif args and len(args)!= 0:
52+
return os.path.join(*(os.path.normpath(p) for p in args))
53+
54+
LOGPATH=PathTool().join((USERSET['log']['outdir'] if USERSET['log']['outdir'] != "default" else "./../../log",LOCALDATE+".log")) #type: ignore
55+
LOGDIR=os.path.dirname(LOGPATH)
56+
57+
class UnPackingScratch3File:
58+
def __init__(self,fp:str):
59+
"""
60+
解包.sb3文件。
61+
62+
:param fp: .sb3文件位置
63+
"""
64+
log.debug(f"Unpacking {repath(fp)}...")
65+
with zipfile.ZipFile(fp,'r') as self.f: #解压.sb3文件
66+
if os.path.basename(fp)!=fp: #如果是一段路径
67+
self.p=PathTool(fp)
68+
self.cdir=self.p.join((self.p.DIR,self.p.NAME))
69+
else: #如果是一段文件名
70+
self.p=PathTool(fp,mode='n')
71+
self.cdir=self.p.join((THISPATH,self.p.NAME))
72+
self.f.extractall(self.cdir)
73+
74+
#格式化.json文档
75+
self.prj_path=self.p.join((self.cdir,'project.json'))
76+
with open(self.prj_path,'r',encoding='utf-8') as f:
77+
c=json.load(f)
78+
with open(self.prj_path,'w',encoding='utf-8') as f:
79+
json.dump(c,f,ensure_ascii=False,indent=4)
80+
log.success(f"Completed unpacking {repath(fp)} -> {repath(self.cdir)}.")
81+
82+
def convert(self):
83+
self.outdir=self.p.join((self.cdir,'output'))
84+
os.makedirs(self.outdir,exist_ok=True)
85+
for fn in os.listdir(self.cdir): #批量转换
86+
p=PathTool(fn,'n')
87+
if p.SUFFIX=='.svg':
88+
fp=p.join((self.cdir,p.FILE))
89+
#with open(p.join((self.cdir,p.FILE)), 'r',encoding='utf-8') as f:
90+
# svg_size = surface.SVGSurface(f.read(),).width, surface.SVGSurface(p.join((self.cdir,p.FILE))).height
91+
tree = ET.parse(fp)
92+
root = tree.getroot()
93+
svg_size = float(root.attrib['width']), float(root.attrib['height'])
94+
png_path = p.join((self.cdir,"output",p.NAME+".png"))
95+
if svg_size != (0,0):
96+
log.debug(f"The size of {repath(fp)} is {svg_size}.")
97+
svg2png(url=fp,
98+
write_to=png_path,
99+
unsafe=True,
100+
parent_width=svg_size[0],
101+
parent_height=svg_size[1])
102+
else:
103+
log.warning(f"{fn} has no size!")
104+
image = Image.new("RGB",(1, 1),(0,0,0))
105+
image.save(png_path)
106+
#os.remove(p.join((self.cdir,p.FILE)))
107+
#log.success(f"Removed {p.join((self.cdir,p.FILE))}.")
108+
log.success(f"Converted {repath(fp)} -> {repath(png_path)}.")
109+
110+
111+
class PackingScratch3File:
112+
def __init__(self,dp:str):
113+
"""
114+
打包.sb3文件。
115+
116+
:param dp: .sb3文件解包后文件夹位置
117+
"""
118+
log.debug(f"Packing {dp}...")
119+
self.p=PathTool(dp,'d')
120+
fp=['project.json']
121+
with open(self.p.join((self.p.DIR,'project.json')),'r',encoding='utf-8') as f:
122+
c=json.load(f)
123+
for t in c['targets']:
124+
for i in t['costumes']:
125+
if i['md5ext'] in os.listdir(self.p.DIR):
126+
fp.append(i['md5ext'])
127+
for s in t['sounds']:
128+
if s['md5ext'] in os.listdir(self.p.DIR):
129+
fp.append(s['md5ext'])
130+
self.outdir=self.p.join((self.p.DIR,'output'))
131+
os.makedirs(self.outdir,exist_ok=True)
132+
self.outfile=self.p.join((self.outdir,self.p.NAME+'.sb3'))
133+
with zipfile.ZipFile(self.outfile,'w') as f:
134+
for i in fp:
135+
f.write(self.p.join((self.p.DIR,i)),i)
136+
log.success(f"Completed packing {repath(dp)} -> {repath(self.outfile)}.")

0 commit comments

Comments
 (0)