// 基于:https://github.com/scratch-fuse/serializer/blob/main/src/base.ts
// https://en.scratch-wiki.info/wiki/Scratch_File_Format
export type Sb3BlockID<T extends Sb3Block = Sb3Block> = string & { __brand: T };
export type Sb3BroadcastID = string & { __brand: "Sb3BroadcastID" };
export type Sb3VariableID = string & { __brand: "Sb3VariableID" };
export type Sb3ListID = string & { __brand: "Sb3ListID" };
export interface Sb3Block {
opcode: string
next: Sb3BlockID | null // ID
parent: Sb3BlockID | null // ID
inputs: { [key: string]: Sb3Input }
fields: { [key: string]: Sb3Field }
shadow: boolean
topLevel: boolean
x?: number
y?: number
mutation?: Sb3Mutation
}
export interface Sb3Mutation {
[key: string]: string | Sb3Mutation[]
}
// #region Sb3Input
// shadow block 就是焊死在积木里的输入框或者下拉菜单啥的,此处我简称 shadow。
// 关于 substack,C字积木嘴里的那串东西就是一个 substack,一串积木就是一个stack,一摞积木,这样的感觉。substack 就是一个子串的感觉。
/** 来源于:https://github.com/scratchfoundation/scratch-vm/blob/e5950c3de5adb4859befc6fe1b19a2142be5c192/src/serialization/sb3.js */
export const enum Sb3InputType {
/** 未被遮盖的 shadow,即字面量输入框。
* unobscured shadow */
SAME_BLOCK_SHADOW = 1,
/** 没有 shadow,即没有输入框,底下可能是个布尔型的框,也可能这个输入是C字积木的 substack。
* no shadow */
BLOCK_NO_SHADOW = 2,
/** 底下一个 shadow 输入框,顶上塞进来一个积木把输入框盖住了。
* obscured shadow */
BLOCK_SHADOW = 3,
// Constants referring to 'primitive' blocks that are usually shadows,
// or in the case of variables and lists, appear quite often in projects
/** math_number */
MATH_NUM_PRIMITIVE = 4, // there's no reason these constants can't collide
/** math_positive_number */
POSITIVE_NUM_PRIMITIVE = 5, // with the above, but removing duplication for clarity
/** math_whole_number */
WHOLE_NUM_PRIMITIVE = 6,
/** math_integer */
INTEGER_NUM_PRIMITIVE = 7,
/** math_angle */
ANGLE_NUM_PRIMITIVE = 8,
/** colour_picker */
COLOR_PICKER_PRIMITIVE = 9,
/** text */
TEXT_PRIMITIVE = 10,
/** event_broadcast_menu */
BROADCAST_PRIMITIVE = 11,
/** data_variable */
VAR_PRIMITIVE = 12,
/** data_listcontents */
LIST_PRIMITIVE = 13,
}
/** 数字输入框
* [4, value] */
export type Sb3ShadowInputNumber = [Sb3InputType.MATH_NUM_PRIMITIVE, string];
/** 正数输入框
* [5, value] */
export type Sb3ShadowInputPositiveNumber = [Sb3InputType.POSITIVE_NUM_PRIMITIVE, string];
/** 正整数输入框
* [6, value] */
export type Sb3ShadowInputPositiveInteger = [Sb3InputType.WHOLE_NUM_PRIMITIVE, string];
/** 整数输入框
* [7, value] */
export type Sb3ShadowInputInteger = [Sb3InputType.INTEGER_NUM_PRIMITIVE, string];
/** 角度输入框
* [8, value] */
export type Sb3ShadowInputAngle = [Sb3InputType.ANGLE_NUM_PRIMITIVE, string];
/** 颜色输入框,值为 "#" 开头的十六进制色值字符串
* [9, value] */
export type Sb3ShadowInputColor = [Sb3InputType.COLOR_PICKER_PRIMITIVE, string];
/** 文本输入框
* [10, value] */
export type Sb3ShadowInputSTRING = [Sb3InputType.TEXT_PRIMITIVE, string];
/** 广播输入框
* [11, name, id] */
export type Sb3ShadowInputBroadcast = [Sb3InputType.BROADCAST_PRIMITIVE, Sb3BroadcastID];
/** 变量输入框
* [12, name, id] | [12, name, id, x, y] */
export type Sb3ShadowInputVariable = [Sb3InputType.VAR_PRIMITIVE, Sb3VariableID] | [Sb3InputType.VAR_PRIMITIVE, Sb3VariableID, number, number];
/** 列表输入框
* [13, name, id] | [13, name, id, x, y] */
export type Sb3ShadowInputList = [Sb3InputType.LIST_PRIMITIVE, Sb3ListID] | [Sb3InputType.LIST_PRIMITIVE, Sb3ListID, number, number];
export type Sb3ShadowInput =
Sb3ShadowInputNumber |
Sb3ShadowInputPositiveNumber |
Sb3ShadowInputPositiveInteger |
Sb3ShadowInputInteger |
Sb3ShadowInputAngle |
Sb3ShadowInputColor |
Sb3ShadowInputSTRING |
Sb3ShadowInputBroadcast |
Sb3ShadowInputVariable |
Sb3ShadowInputList ;
/** shadow 输入,字面量或菜单
* [1, shadow | blockID]
* 第二项貌似无论输入什么都会视为 shadow */
export type Sb3InputShadow = [Sb3InputType.SAME_BLOCK_SHADOW, Sb3BlockID | Sb3ShadowInput];
/** 积木输入,底下没有 shadow,可能是布尔输入,也可能是 C 嘴里的子串
* [2, blockID]
* 原则上第二项大概一般是 blockID ,但直接塞个 shadow input 进来也能用 */
export type Sb3InputBlockWithoutShadow = [Sb3InputType.BLOCK_NO_SHADOW, Sb3BlockID | Sb3ShadowInput];
/** 积木输入,底下盖着一个 shadow 输入框
* [3, blockID, shadow] */
export type Sb3InputBlockObscuredShadow =
[Sb3InputType.BLOCK_SHADOW, Sb3BlockID | Sb3ShadowInput, Sb3BlockID | Sb3ShadowInput] |
[Sb3InputType.BLOCK_SHADOW, Sb3BlockID | Sb3ShadowInput] ;
/** sc 把这玩意的行为搞得非常宽松。。
* 有很多人都会手动改 project.json 搞出一堆不规范但能跑的写法,应该把边缘情况都考虑进去 */
export type Sb3Input =
Sb3InputShadow |
Sb3InputBlockWithoutShadow |
Sb3InputBlockObscuredShadow ;
// #endregion
export type Sb3Field = [string, Sb3BroadcastID | Sb3VariableID | Sb3ListID | null] // [value, id]
export type Sb3Workspace = Record<Sb3BlockID, Sb3Block>
export interface Sb3Target {
isStage: boolean
name: string
variables: Record<
Sb3VariableID,
[string, string | number | boolean | (string | number | boolean)[]]
> // id -> [name, value]
lists: Record<Sb3ListID, [string, (string | number | boolean)[]]> // id -> [name, value]
broadcasts: Record<Sb3BroadcastID, string> // id -> name
blocks: Sb3Workspace
comments: Record<string, unknown>
currentCostume: number
costumes: Array<{
assetId: string
name: string
md5ext: string
dataFormat: string
rotationCenterX: number
rotationCenterY: number
}>
sounds: Array<{
assetId: string
name: string
md5ext: string
dataFormat: string
format: string
rate: number
sampleCount: number
}>
volume: number
layerOrder: number
tempo: number
videoTransparency: number
videoState: 'on' | 'off' | 'on-flipped'
textToSpeechLanguage: string | null
}
基于
base.ts,本来只是我自用的,但感觉可能对你们有帮助,所以随手发出来了。用品牌类型跟踪了不同意义的字符串,稍微完善了一下Sb3Input。不保证准确可用,包含一些我个人的需求和习惯,权当个参考吧。