@nitro-bolt/scratchblocks 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +18 -0
- package/README.md +205 -0
- package/browser.es.js +8 -0
- package/browser.js +8 -0
- package/build/scratchblocks.min.es.js +11 -0
- package/build/scratchblocks.min.es.js.map +1 -0
- package/build/scratchblocks.min.js +11 -0
- package/build/scratchblocks.min.js.map +1 -0
- package/build/translations-all-es.js +10 -0
- package/build/translations-all.js +10 -0
- package/build/translations-es.js +10 -0
- package/build/translations.js +10 -0
- package/index.js +163 -0
- package/locales/ab.json +261 -0
- package/locales/af.json +261 -0
- package/locales/all.js +161 -0
- package/locales/am.json +262 -0
- package/locales/an.json +261 -0
- package/locales/ar.json +261 -0
- package/locales/ast.json +261 -0
- package/locales/az.json +262 -0
- package/locales/be.json +261 -0
- package/locales/bg.json +261 -0
- package/locales/bn.json +261 -0
- package/locales/ca.json +267 -0
- package/locales/ckb.json +261 -0
- package/locales/cs.json +267 -0
- package/locales/cy.json +266 -0
- package/locales/da.json +261 -0
- package/locales/de.json +266 -0
- package/locales/el.json +268 -0
- package/locales/eo.json +261 -0
- package/locales/es-419.json +261 -0
- package/locales/es.json +266 -0
- package/locales/et.json +261 -0
- package/locales/eu.json +261 -0
- package/locales/fa.json +266 -0
- package/locales/fi.json +261 -0
- package/locales/fil.json +262 -0
- package/locales/forums.js +37 -0
- package/locales/fr.json +266 -0
- package/locales/fy.json +261 -0
- package/locales/ga.json +261 -0
- package/locales/gd.json +266 -0
- package/locales/gl.json +261 -0
- package/locales/ha.json +261 -0
- package/locales/he.json +266 -0
- package/locales/hi.json +266 -0
- package/locales/hr.json +266 -0
- package/locales/ht.json +261 -0
- package/locales/hu.json +267 -0
- package/locales/hy.json +261 -0
- package/locales/id.json +266 -0
- package/locales/is.json +261 -0
- package/locales/it.json +266 -0
- package/locales/ja-Hira.json +268 -0
- package/locales/ja.json +268 -0
- package/locales/ka.json +261 -0
- package/locales/kk.json +263 -0
- package/locales/km.json +261 -0
- package/locales/ko.json +261 -0
- package/locales/ku.json +263 -0
- package/locales/lt.json +261 -0
- package/locales/lv.json +261 -0
- package/locales/mi.json +261 -0
- package/locales/mn.json +262 -0
- package/locales/nb.json +266 -0
- package/locales/nl.json +266 -0
- package/locales/nn.json +261 -0
- package/locales/nso.json +261 -0
- package/locales/oc.json +261 -0
- package/locales/or.json +262 -0
- package/locales/pl.json +266 -0
- package/locales/pt-br.json +261 -0
- package/locales/pt.json +266 -0
- package/locales/qu.json +261 -0
- package/locales/rap.json +263 -0
- package/locales/ro.json +266 -0
- package/locales/ru.json +266 -0
- package/locales/sk.json +261 -0
- package/locales/sl.json +266 -0
- package/locales/sr.json +261 -0
- package/locales/sv.json +261 -0
- package/locales/sw.json +261 -0
- package/locales/th.json +261 -0
- package/locales/tn.json +261 -0
- package/locales/tr.json +269 -0
- package/locales/uk.json +261 -0
- package/locales/uz.json +262 -0
- package/locales/vi.json +262 -0
- package/locales/xh.json +261 -0
- package/locales/zh-cn.json +266 -0
- package/locales/zh-tw.json +266 -0
- package/locales/zu.json +261 -0
- package/package.json +77 -0
- package/scratch3/blocks.js +959 -0
- package/scratch3/draw.js +404 -0
- package/scratch3/index.js +12 -0
- package/scratch3/style.css.js +226 -0
- package/scratch3/style.js +807 -0
- package/syntax/blocks.js +467 -0
- package/syntax/commands.js +1692 -0
- package/syntax/extensions.js +33 -0
- package/syntax/index.js +17 -0
- package/syntax/model.js +427 -0
- package/syntax/syntax.js +941 -0
|
@@ -0,0 +1,959 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Label,
|
|
3
|
+
Icon,
|
|
4
|
+
Input,
|
|
5
|
+
Newline,
|
|
6
|
+
Block,
|
|
7
|
+
Comment,
|
|
8
|
+
Glow,
|
|
9
|
+
Script,
|
|
10
|
+
Document,
|
|
11
|
+
extensions,
|
|
12
|
+
aliasExtensions,
|
|
13
|
+
} from "../syntax/index.js"
|
|
14
|
+
|
|
15
|
+
import SVG from "./draw.js"
|
|
16
|
+
import style from "./style.js"
|
|
17
|
+
const {
|
|
18
|
+
defaultFont,
|
|
19
|
+
commentFont,
|
|
20
|
+
makeStyle,
|
|
21
|
+
makeOriginalIcons,
|
|
22
|
+
makeHighContrastIcons,
|
|
23
|
+
iconName,
|
|
24
|
+
} = style
|
|
25
|
+
|
|
26
|
+
export class LabelView {
|
|
27
|
+
constructor(label) {
|
|
28
|
+
Object.assign(this, label)
|
|
29
|
+
|
|
30
|
+
this.el = null
|
|
31
|
+
this.height = 12
|
|
32
|
+
this.metrics = null
|
|
33
|
+
this.x = 0
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get isLabel() {
|
|
37
|
+
return true
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
draw(_iconStyle) {
|
|
41
|
+
return this.el
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get width() {
|
|
45
|
+
return this.metrics.width
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
measure() {
|
|
49
|
+
const value = this.value
|
|
50
|
+
const cls = `sb3-${this.cls}`
|
|
51
|
+
this.el = SVG.text(0, 13, value, {
|
|
52
|
+
class: `sb3-label ${cls}`,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
let cache = LabelView.metricsCache[cls]
|
|
56
|
+
if (!cache) {
|
|
57
|
+
cache = LabelView.metricsCache[cls] = Object.create(null)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (Object.hasOwnProperty.call(cache, value)) {
|
|
61
|
+
this.metrics = cache[value]
|
|
62
|
+
} else {
|
|
63
|
+
const font = /comment-label/.test(this.cls) ? commentFont : defaultFont
|
|
64
|
+
this.metrics = cache[value] = LabelView.measure(value, font)
|
|
65
|
+
// TODO: word-spacing? (fortunately it seems to have no effect!)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static measure(value, font) {
|
|
70
|
+
const context = LabelView.measuring
|
|
71
|
+
context.font = font
|
|
72
|
+
const textMetrics = context.measureText(value)
|
|
73
|
+
const width = (textMetrics.width + 0.5) | 0
|
|
74
|
+
return { width: width }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
LabelView.metricsCache = {}
|
|
79
|
+
LabelView.toMeasure = []
|
|
80
|
+
|
|
81
|
+
export class IconView {
|
|
82
|
+
constructor(icon) {
|
|
83
|
+
Object.assign(this, icon)
|
|
84
|
+
|
|
85
|
+
const info = IconView.icons[this.name]
|
|
86
|
+
if (!info) {
|
|
87
|
+
throw new Error(`no info for icon: ${this.name}`)
|
|
88
|
+
}
|
|
89
|
+
Object.assign(this, info)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get isIcon() {
|
|
93
|
+
return true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
draw(iconStyle) {
|
|
97
|
+
const symbol = SVG.symbol(`#sb3-${iconName(this.name, iconStyle)}`, {
|
|
98
|
+
width: this.width,
|
|
99
|
+
height: this.height,
|
|
100
|
+
})
|
|
101
|
+
if (this.name === "addInput" || this.name === "delInput") {
|
|
102
|
+
const scaled = SVG.group([symbol])
|
|
103
|
+
SVG.setProps(scaled, {
|
|
104
|
+
transform: "scale(1.6)",
|
|
105
|
+
})
|
|
106
|
+
return SVG.group([scaled])
|
|
107
|
+
}
|
|
108
|
+
return symbol
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
static get icons() {
|
|
112
|
+
return {
|
|
113
|
+
greenFlag: { width: 20, height: 21, dy: -2 },
|
|
114
|
+
stopSign: { width: 20, height: 20 },
|
|
115
|
+
turnLeft: { width: 24, height: 24 },
|
|
116
|
+
turnRight: { width: 24, height: 24 },
|
|
117
|
+
loopArrow: { width: 24, height: 24 },
|
|
118
|
+
addInput: { width: 16, height: 21 },
|
|
119
|
+
delInput: { width: 16, height: 21 },
|
|
120
|
+
list: { width: 15, height: 18 },
|
|
121
|
+
musicBlock: { width: 40, height: 40 },
|
|
122
|
+
penBlock: { width: 40, height: 40 },
|
|
123
|
+
videoBlock: { width: 40, height: 40, dy: 10 },
|
|
124
|
+
ttsBlock: { width: 40, height: 40 },
|
|
125
|
+
translateBlock: { width: 40, height: 40 },
|
|
126
|
+
wedoBlock: { width: 40, height: 40 },
|
|
127
|
+
ev3Block: { width: 40, height: 40 },
|
|
128
|
+
microbitBlock: { width: 40, height: 40 },
|
|
129
|
+
makeymakeyBlock: { width: 40, height: 40 },
|
|
130
|
+
gdxforBlock: { width: 40, height: 40 },
|
|
131
|
+
boostBlock: { width: 40, height: 40 },
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export class LineView {
|
|
137
|
+
constructor() {
|
|
138
|
+
this.width = 1
|
|
139
|
+
this.height = 40
|
|
140
|
+
this.x = 0
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
get isLine() {
|
|
144
|
+
return true
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
measure() {}
|
|
148
|
+
|
|
149
|
+
draw(_iconStyle, parent) {
|
|
150
|
+
const category = parent.info.category
|
|
151
|
+
return SVG.el("line", {
|
|
152
|
+
class: `sb3-${category}`,
|
|
153
|
+
"stroke-linecap": "round",
|
|
154
|
+
x1: 0,
|
|
155
|
+
y1: 0,
|
|
156
|
+
x2: 0,
|
|
157
|
+
y2: 40,
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export class InputView {
|
|
163
|
+
constructor(input) {
|
|
164
|
+
Object.assign(this, input)
|
|
165
|
+
if (input.label) {
|
|
166
|
+
this.label = newView(input.label)
|
|
167
|
+
}
|
|
168
|
+
this.isBoolean = this.shape === "boolean"
|
|
169
|
+
this.isObject = this.shape === "object"
|
|
170
|
+
this.isArray = this.shape === "array"
|
|
171
|
+
this.isDropdown = this.shape === "dropdown"
|
|
172
|
+
this.isRound = !(this.isBoolean || this.isDropdown)
|
|
173
|
+
|
|
174
|
+
this.x = 0
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
get isInput() {
|
|
178
|
+
return true
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
measure() {
|
|
182
|
+
if (this.hasLabel) {
|
|
183
|
+
this.label.measure()
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
static get shapes() {
|
|
188
|
+
return {
|
|
189
|
+
string: SVG.pillRect,
|
|
190
|
+
number: SVG.pillRect,
|
|
191
|
+
"number-dropdown": SVG.pillRect,
|
|
192
|
+
color: SVG.pillRect,
|
|
193
|
+
dropdown: SVG.roundRect,
|
|
194
|
+
|
|
195
|
+
boolean: SVG.pointedRect,
|
|
196
|
+
object: SVG.objectRect,
|
|
197
|
+
array: SVG.roundRect,
|
|
198
|
+
stack: SVG.stackRect,
|
|
199
|
+
reporter: SVG.pillRect,
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
draw(iconStyle, parent) {
|
|
204
|
+
let w
|
|
205
|
+
let label
|
|
206
|
+
if (this.isBoolean) {
|
|
207
|
+
w = 48
|
|
208
|
+
} else if (this.isColor) {
|
|
209
|
+
w = 40
|
|
210
|
+
} else if (this.hasLabel) {
|
|
211
|
+
label = this.label.draw(iconStyle)
|
|
212
|
+
// Minimum padding of 11
|
|
213
|
+
// Minimum width of 40, at which point we center the label
|
|
214
|
+
const px = this.label.width >= 18 ? 11 : (40 - this.label.width) / 2
|
|
215
|
+
w = this.label.width + 2 * px
|
|
216
|
+
label = SVG.move(px, 9, label)
|
|
217
|
+
} else {
|
|
218
|
+
w = this.isObject || this.isArray ? 48 : this.isInset ? 30 : null
|
|
219
|
+
}
|
|
220
|
+
if (this.hasArrow) {
|
|
221
|
+
w += 20
|
|
222
|
+
}
|
|
223
|
+
this.width = w
|
|
224
|
+
|
|
225
|
+
const h = (this.height = 32)
|
|
226
|
+
|
|
227
|
+
const el = InputView.shapes[this.shape](w, h)
|
|
228
|
+
SVG.setProps(el, {
|
|
229
|
+
class: `${
|
|
230
|
+
this.isColor ? "" : `sb3-${parent.info.category}`
|
|
231
|
+
} sb3-input sb3-input-${this.shape}`,
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
if (this.isColor) {
|
|
235
|
+
SVG.setProps(el, {
|
|
236
|
+
fill: this.value,
|
|
237
|
+
})
|
|
238
|
+
} else if (this.shape === "dropdown") {
|
|
239
|
+
// custom colors
|
|
240
|
+
if (parent.info.color) {
|
|
241
|
+
SVG.setProps(el, {
|
|
242
|
+
fill: parent.info.color,
|
|
243
|
+
stroke: "rgba(0, 0, 0, 0.2)",
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
} else if (this.shape === "number-dropdown") {
|
|
247
|
+
el.classList.add(`sb3-${parent.info.category}-alt`)
|
|
248
|
+
|
|
249
|
+
// custom colors
|
|
250
|
+
if (parent.info.color) {
|
|
251
|
+
SVG.setProps(el, {
|
|
252
|
+
fill: "rgba(0, 0, 0, 0.1)",
|
|
253
|
+
stroke: "rgba(0, 0, 0, 0.15)", // combines with fill...
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
} else if (
|
|
257
|
+
this.shape === "boolean" ||
|
|
258
|
+
this.shape === "object" ||
|
|
259
|
+
this.shape === "array"
|
|
260
|
+
) {
|
|
261
|
+
el.classList.remove(`sb3-${parent.info.category}`)
|
|
262
|
+
el.classList.add(`sb3-${parent.info.category}-dark`)
|
|
263
|
+
|
|
264
|
+
// custom colors
|
|
265
|
+
if (parent.info.color) {
|
|
266
|
+
SVG.setProps(el, {
|
|
267
|
+
fill: "rgba(0, 0, 0, 0.15)",
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const result = SVG.group([el])
|
|
273
|
+
if (this.hasLabel) {
|
|
274
|
+
result.appendChild(label)
|
|
275
|
+
}
|
|
276
|
+
if (this.hasArrow) {
|
|
277
|
+
result.appendChild(
|
|
278
|
+
SVG.move(
|
|
279
|
+
w - 24,
|
|
280
|
+
13,
|
|
281
|
+
SVG.symbol(
|
|
282
|
+
iconStyle === "high-contrast"
|
|
283
|
+
? "#sb3-dropdownArrow-high-contrast"
|
|
284
|
+
: "#sb3-dropdownArrow",
|
|
285
|
+
{},
|
|
286
|
+
),
|
|
287
|
+
),
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
return result
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export class NewlineView {
|
|
295
|
+
get isNewline() {
|
|
296
|
+
return true
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
measure() {}
|
|
300
|
+
|
|
301
|
+
draw() {
|
|
302
|
+
this.width = 0
|
|
303
|
+
this.height = 0
|
|
304
|
+
return SVG.group([])
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
class BlockView {
|
|
309
|
+
constructor(block) {
|
|
310
|
+
Object.assign(this, block)
|
|
311
|
+
this.children = block.children.map(newView)
|
|
312
|
+
this.comment = this.comment ? newView(this.comment) : null
|
|
313
|
+
this.isRound = this.info.shape === "reporter"
|
|
314
|
+
|
|
315
|
+
// Avoid accidental mutation
|
|
316
|
+
this.info = { ...block.info }
|
|
317
|
+
if (
|
|
318
|
+
Object.prototype.hasOwnProperty.call(aliasExtensions, this.info.category)
|
|
319
|
+
) {
|
|
320
|
+
this.info.category = aliasExtensions[this.info.category]
|
|
321
|
+
}
|
|
322
|
+
if (Object.prototype.hasOwnProperty.call(extensions, this.info.category)) {
|
|
323
|
+
this.children.unshift(new LineView())
|
|
324
|
+
this.children.unshift(
|
|
325
|
+
new IconView({ name: this.info.category + "Block" }),
|
|
326
|
+
)
|
|
327
|
+
this.info.category = "extension"
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
this.x = 0
|
|
331
|
+
this.width = null
|
|
332
|
+
this.height = null
|
|
333
|
+
this.firstLine = null
|
|
334
|
+
this.innerWidth = null
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
get isBlock() {
|
|
338
|
+
return true
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
measure() {
|
|
342
|
+
for (const child of this.children) {
|
|
343
|
+
if (child.measure) {
|
|
344
|
+
child.measure()
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (this.comment) {
|
|
348
|
+
this.comment.measure()
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
static get shapes() {
|
|
353
|
+
return {
|
|
354
|
+
stack: SVG.stackRect,
|
|
355
|
+
"c-block": SVG.stackRect,
|
|
356
|
+
"if-block": SVG.stackRect,
|
|
357
|
+
celse: SVG.stackRect,
|
|
358
|
+
cend: SVG.stackRect,
|
|
359
|
+
|
|
360
|
+
cap: SVG.capRect,
|
|
361
|
+
reporter: SVG.pillRect,
|
|
362
|
+
object: SVG.objectRect,
|
|
363
|
+
array: SVG.roundRect,
|
|
364
|
+
boolean: SVG.pointedRect,
|
|
365
|
+
hat: SVG.hatRect,
|
|
366
|
+
cat: SVG.catHat,
|
|
367
|
+
"define-hat": SVG.procHatRect,
|
|
368
|
+
ring: SVG.pillRect,
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
drawSelf(iconStyle, w, h, lines) {
|
|
373
|
+
// mouths
|
|
374
|
+
if (lines.some(line => line.isScript)) {
|
|
375
|
+
return SVG.mouthRect(w, h, this.isFinal, lines, {
|
|
376
|
+
class: `sb3-${this.info.category}`,
|
|
377
|
+
})
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// outlines
|
|
381
|
+
if (this.info.shape === "outline") {
|
|
382
|
+
return SVG.setProps(SVG.stackRect(w, h), {
|
|
383
|
+
class: `sb3-${this.info.category} sb3-${this.info.category}-alt`,
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// rings
|
|
388
|
+
if (this.isRing) {
|
|
389
|
+
const child = this.children[0]
|
|
390
|
+
if (child && (child.isInput || child.isBlock || child.isScript)) {
|
|
391
|
+
return SVG.roundRect(w, h, {
|
|
392
|
+
class: `sb3-${this.info.category}`,
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const func = BlockView.shapes[this.info.shape]
|
|
398
|
+
if (!func) {
|
|
399
|
+
throw new Error(`no shape func: ${this.info.shape}`)
|
|
400
|
+
}
|
|
401
|
+
return func(w, h, {
|
|
402
|
+
class: `sb3-${this.info.category}`,
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
static get padding() {
|
|
407
|
+
return {
|
|
408
|
+
hat: [24, 8],
|
|
409
|
+
cat: [24, 8],
|
|
410
|
+
"define-hat": [20, 16],
|
|
411
|
+
null: [4, 4],
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
horizontalPadding(child) {
|
|
416
|
+
if (this.isObject) {
|
|
417
|
+
return child.isLabel || child.isIcon ? 16 : 8
|
|
418
|
+
}
|
|
419
|
+
if (this.isRound) {
|
|
420
|
+
if (child.isIcon) {
|
|
421
|
+
return 16
|
|
422
|
+
} else if (child.isLabel) {
|
|
423
|
+
return 12 // text in circle: 3 units
|
|
424
|
+
} else if (child.isDropdown) {
|
|
425
|
+
return 12 // square in circle: 3 units
|
|
426
|
+
} else if (child.isBoolean) {
|
|
427
|
+
return 12 // hexagon in circle: 3 units
|
|
428
|
+
} else if (child.isRound) {
|
|
429
|
+
return 4 // circle in circle: 1 unit
|
|
430
|
+
}
|
|
431
|
+
} else if (this.isBoolean) {
|
|
432
|
+
if (child.isIcon) {
|
|
433
|
+
return 24 // icon in hexagon: ???
|
|
434
|
+
} else if (child.isLabel) {
|
|
435
|
+
return 20 // text in hexagon: 5 units
|
|
436
|
+
} else if (child.isDropdown) {
|
|
437
|
+
return 20 // square in hexagon: 5 units
|
|
438
|
+
} else if (child.isRound && child.isBlock) {
|
|
439
|
+
return 24 // circle in hexagon: 5 + 1 units
|
|
440
|
+
} else if (child.isRound) {
|
|
441
|
+
return 20 // circle in hexagon: 5 units
|
|
442
|
+
} else if (child.isBoolean) {
|
|
443
|
+
return 8 // hexagon in hexagon: 2 units
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return 8 // default: 2 units
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
marginBetween(a, b) {
|
|
450
|
+
// Consecutive labels should be rendered as a single text element.
|
|
451
|
+
// For now, approximate the size of one space
|
|
452
|
+
if (a.isLabel && b.isLabel) {
|
|
453
|
+
return 5
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return 8 // default: 2 units
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
draw(iconStyle) {
|
|
460
|
+
const isDefine = this.info.shape === "define-hat"
|
|
461
|
+
let children = this.children
|
|
462
|
+
const isCommand = this.isCommand
|
|
463
|
+
|
|
464
|
+
const padding = BlockView.padding[this.info.shape] || BlockView.padding.null
|
|
465
|
+
const visualOutputPadding =
|
|
466
|
+
!isCommand && children.some(child => child.isNewline) ? 4 : 0
|
|
467
|
+
const pt = padding[0] + visualOutputPadding,
|
|
468
|
+
pb = padding[1] + visualOutputPadding
|
|
469
|
+
|
|
470
|
+
let y = this.info.shape === "cat" ? 16 : 0
|
|
471
|
+
const Line = function (y) {
|
|
472
|
+
this.y = y
|
|
473
|
+
this.width = 0
|
|
474
|
+
this.height = isCommand ? 40 : 32
|
|
475
|
+
this.children = []
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
let innerWidth = 0
|
|
479
|
+
let scriptWidth = 0
|
|
480
|
+
let line = new Line(y)
|
|
481
|
+
const pushLine = isLast => {
|
|
482
|
+
if (lines.length === 0) {
|
|
483
|
+
line.height += pt + pb
|
|
484
|
+
} else if (line.isVisualRow && isLast) {
|
|
485
|
+
line.height += pb + 4
|
|
486
|
+
} else if (!line.isVisualRow) {
|
|
487
|
+
line.height -= 11
|
|
488
|
+
line.y -= 2
|
|
489
|
+
}
|
|
490
|
+
y += line.height
|
|
491
|
+
lines.push(line)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (this.info.isRTL) {
|
|
495
|
+
let start = 0
|
|
496
|
+
const flip = () => {
|
|
497
|
+
children = children
|
|
498
|
+
.slice(0, start)
|
|
499
|
+
.concat(children.slice(start, i).reverse())
|
|
500
|
+
.concat(children.slice(i))
|
|
501
|
+
}
|
|
502
|
+
let i
|
|
503
|
+
for (i = 0; i < children.length; i++) {
|
|
504
|
+
if (children[i].isScript) {
|
|
505
|
+
flip()
|
|
506
|
+
start = i + 1
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (start < i) {
|
|
510
|
+
flip()
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const lines = []
|
|
515
|
+
let previousChild
|
|
516
|
+
let lastChild
|
|
517
|
+
for (let i = 0; i < children.length; i++) {
|
|
518
|
+
const child = children[i]
|
|
519
|
+
child.el = child.draw(iconStyle, this)
|
|
520
|
+
|
|
521
|
+
if (child.isNewline) {
|
|
522
|
+
pushLine()
|
|
523
|
+
line = new Line(y)
|
|
524
|
+
line.isVisualRow = true
|
|
525
|
+
previousChild = null
|
|
526
|
+
} else if (child.isScript && this.hasScript) {
|
|
527
|
+
this.hasScript = true
|
|
528
|
+
pushLine()
|
|
529
|
+
child.y = y - 1
|
|
530
|
+
lines.push(child)
|
|
531
|
+
scriptWidth = Math.max(scriptWidth, Math.max(1, child.width))
|
|
532
|
+
child.height = Math.max(29, child.height + 3) - 2
|
|
533
|
+
y += child.height
|
|
534
|
+
line = new Line(y)
|
|
535
|
+
previousChild = null
|
|
536
|
+
} else if (child.isArrow) {
|
|
537
|
+
line.children.push(child)
|
|
538
|
+
previousChild = child
|
|
539
|
+
} else {
|
|
540
|
+
// Remember the last child on the first line
|
|
541
|
+
if (!lines.length) {
|
|
542
|
+
lastChild = child
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Leave space between inputs
|
|
546
|
+
if (previousChild) {
|
|
547
|
+
line.width += this.marginBetween(previousChild, child)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Align first input with right of notch
|
|
551
|
+
if (children[0] != null) {
|
|
552
|
+
const cmw = 48 - this.horizontalPadding(children[0])
|
|
553
|
+
if (
|
|
554
|
+
(this.isCommand || this.isOutline) &&
|
|
555
|
+
!child.isLabel &&
|
|
556
|
+
!child.isIcon &&
|
|
557
|
+
line.width < cmw
|
|
558
|
+
) {
|
|
559
|
+
line.width = cmw
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Align extension category icons below notch
|
|
564
|
+
if (child.isIcon && i === 0 && this.isCommand) {
|
|
565
|
+
line.height = Math.max(line.height, child.height + 8)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
child.x = line.width
|
|
569
|
+
line.width += child.width
|
|
570
|
+
innerWidth = Math.max(innerWidth, line.width)
|
|
571
|
+
if (!child.isLabel) {
|
|
572
|
+
line.height = Math.max(line.height, child.height)
|
|
573
|
+
}
|
|
574
|
+
line.children.push(child)
|
|
575
|
+
previousChild = child
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
pushLine(true)
|
|
579
|
+
|
|
580
|
+
let padLeft = children.length ? this.horizontalPadding(children[0]) : 0
|
|
581
|
+
const padRight = children.length ? this.horizontalPadding(lastChild) : 0
|
|
582
|
+
innerWidth += padLeft + padRight
|
|
583
|
+
|
|
584
|
+
// Commands have a minimum width.
|
|
585
|
+
// Outline min-width is deliberately higher (because Scratch 3 looks silly).
|
|
586
|
+
const originalInnerWidth = innerWidth
|
|
587
|
+
innerWidth = Math.max(
|
|
588
|
+
this.hasScript
|
|
589
|
+
? 160
|
|
590
|
+
: this.isHat
|
|
591
|
+
? 100 // Correct for Scratch 3.0.
|
|
592
|
+
: this.isCommand || this.isOutline
|
|
593
|
+
? 64
|
|
594
|
+
: this.isReporter
|
|
595
|
+
? 48
|
|
596
|
+
: 0,
|
|
597
|
+
innerWidth,
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
// Center the label text inside small reporters.
|
|
601
|
+
if (this.isReporter) {
|
|
602
|
+
padLeft += (innerWidth - originalInnerWidth) / 2
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
this.height = y
|
|
606
|
+
|
|
607
|
+
this.width = scriptWidth
|
|
608
|
+
? Math.max(innerWidth, 15 + scriptWidth)
|
|
609
|
+
: innerWidth
|
|
610
|
+
this.firstLine = lines[0]
|
|
611
|
+
this.innerWidth = innerWidth
|
|
612
|
+
|
|
613
|
+
const objects = []
|
|
614
|
+
|
|
615
|
+
for (let i = 0; i < lines.length; i++) {
|
|
616
|
+
const line = lines[i]
|
|
617
|
+
if (line.isScript) {
|
|
618
|
+
objects.push(SVG.move(16, line.y, line.el))
|
|
619
|
+
continue
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const h = line.height
|
|
623
|
+
|
|
624
|
+
for (let j = 0; j < line.children.length; j++) {
|
|
625
|
+
const child = line.children[j]
|
|
626
|
+
if (child.isArrow) {
|
|
627
|
+
objects.push(SVG.move(innerWidth - 32, this.height - 28, child.el))
|
|
628
|
+
continue
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
let y = pt + (h - child.height - pt - pb) / 2
|
|
632
|
+
if (child.isLabel && i === 0) {
|
|
633
|
+
// We only do this for the first line so that the `else` label is
|
|
634
|
+
// correctly aligned
|
|
635
|
+
y -= 1
|
|
636
|
+
} else if (isDefine && child.isLabel) {
|
|
637
|
+
y += 3
|
|
638
|
+
} else if (child.isIcon) {
|
|
639
|
+
y += child.dy | 0
|
|
640
|
+
if (this.isCommand && i === 0 && j === 0) {
|
|
641
|
+
y += 4
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
let x = padLeft + child.x
|
|
646
|
+
if (child.dx) {
|
|
647
|
+
x += child.dx
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
objects.push(SVG.move(x, (line.y + y) | 0, child.el))
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const el = this.drawSelf(iconStyle, innerWidth, this.height, lines)
|
|
655
|
+
objects.splice(0, 0, el)
|
|
656
|
+
if (this.info.color) {
|
|
657
|
+
SVG.setProps(el, {
|
|
658
|
+
fill: this.info.color,
|
|
659
|
+
stroke: "rgba(0, 0, 0, 0.2)",
|
|
660
|
+
})
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return SVG.group(objects)
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export class CommentView {
|
|
668
|
+
constructor(comment) {
|
|
669
|
+
Object.assign(this, comment)
|
|
670
|
+
this.label = newView(comment.label)
|
|
671
|
+
|
|
672
|
+
this.width = null
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
get isComment() {
|
|
676
|
+
return true
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
static get lineLength() {
|
|
680
|
+
return 12
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
get height() {
|
|
684
|
+
return 20
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
measure() {
|
|
688
|
+
this.label.measure()
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
draw(iconStyle) {
|
|
692
|
+
const labelEl = this.label.draw(iconStyle)
|
|
693
|
+
|
|
694
|
+
this.width = this.label.width + 16
|
|
695
|
+
return SVG.group([
|
|
696
|
+
SVG.commentLine(this.hasBlock ? CommentView.lineLength : 0, 6),
|
|
697
|
+
SVG.commentRect(this.width, this.height, {
|
|
698
|
+
class: "sb3-comment",
|
|
699
|
+
}),
|
|
700
|
+
SVG.move(8, 4, labelEl),
|
|
701
|
+
])
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
class GlowView {
|
|
706
|
+
constructor(glow) {
|
|
707
|
+
Object.assign(this, glow)
|
|
708
|
+
this.child = newView(glow.child)
|
|
709
|
+
|
|
710
|
+
this.width = null
|
|
711
|
+
this.height = null
|
|
712
|
+
this.y = 0
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
get isGlow() {
|
|
716
|
+
return true
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
measure() {
|
|
720
|
+
this.child.measure()
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
drawSelf(iconStyle) {
|
|
724
|
+
const c = this.child
|
|
725
|
+
let el
|
|
726
|
+
const w = this.width
|
|
727
|
+
const h = this.height - 1
|
|
728
|
+
if (c.isScript) {
|
|
729
|
+
if (!c.isEmpty && c.blocks[0].isHat) {
|
|
730
|
+
el = SVG.hatRect(w, h)
|
|
731
|
+
} else if (c.isFinal) {
|
|
732
|
+
el = SVG.capRect(w, h)
|
|
733
|
+
} else {
|
|
734
|
+
el = SVG.stackRect(w, h)
|
|
735
|
+
}
|
|
736
|
+
} else {
|
|
737
|
+
el = c.drawSelf(iconStyle, w, h, [])
|
|
738
|
+
}
|
|
739
|
+
return SVG.setProps(el, {
|
|
740
|
+
class: "sb3-diff sb3-diff-ins",
|
|
741
|
+
})
|
|
742
|
+
}
|
|
743
|
+
// TODO how can we always raise Glows above their parents?
|
|
744
|
+
|
|
745
|
+
draw(iconStyle) {
|
|
746
|
+
const c = this.child
|
|
747
|
+
const el = c.isScript ? c.draw(iconStyle, true) : c.draw(iconStyle)
|
|
748
|
+
|
|
749
|
+
this.width = c.width
|
|
750
|
+
this.height = (c.isBlock && c.firstLine.height) || c.height
|
|
751
|
+
|
|
752
|
+
// encircle
|
|
753
|
+
return SVG.group([el, this.drawSelf(iconStyle)])
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
class ScriptView {
|
|
758
|
+
constructor(script) {
|
|
759
|
+
Object.assign(this, script)
|
|
760
|
+
this.blocks = script.blocks.map(newView)
|
|
761
|
+
|
|
762
|
+
this.y = 0
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
get isScript() {
|
|
766
|
+
return true
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
measure() {
|
|
770
|
+
for (const block of this.blocks) {
|
|
771
|
+
block.measure()
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
draw(iconStyle, inside) {
|
|
776
|
+
const children = []
|
|
777
|
+
let y = 1
|
|
778
|
+
this.width = 0
|
|
779
|
+
for (const block of this.blocks) {
|
|
780
|
+
const x = inside ? 0 : 2
|
|
781
|
+
const child = block.draw(iconStyle)
|
|
782
|
+
children.push(SVG.move(x, y, child))
|
|
783
|
+
this.width = Math.max(this.width, block.width)
|
|
784
|
+
|
|
785
|
+
const diff = block.diff
|
|
786
|
+
if (diff === "-") {
|
|
787
|
+
const dw = block.width
|
|
788
|
+
const dh = block.firstLine.height || block.height
|
|
789
|
+
children.push(SVG.move(x, y + dh / 2 + 1, SVG.strikethroughLine(dw)))
|
|
790
|
+
this.width = Math.max(this.width, block.width)
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
y += block.height
|
|
794
|
+
|
|
795
|
+
const comment = block.comment
|
|
796
|
+
if (comment) {
|
|
797
|
+
const line = block.firstLine
|
|
798
|
+
const cx = block.innerWidth + 2 + CommentView.lineLength
|
|
799
|
+
const cy = y - block.height + line.height / 2
|
|
800
|
+
const el = comment.draw(iconStyle)
|
|
801
|
+
children.push(SVG.move(cx, cy - comment.height / 2, el))
|
|
802
|
+
this.width = Math.max(this.width, cx + comment.width)
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
const lastBlock = this.blocks[this.blocks.length - 1]
|
|
806
|
+
this.height = y + 1
|
|
807
|
+
if (!inside && !this.isFinal) {
|
|
808
|
+
this.height += lastBlock.hasPuzzle ? 8 : 0
|
|
809
|
+
}
|
|
810
|
+
if (!inside && lastBlock.isGlow) {
|
|
811
|
+
this.height += 7 // TODO unbreak this
|
|
812
|
+
}
|
|
813
|
+
return SVG.group(children)
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
class DocumentView {
|
|
818
|
+
constructor(doc, options) {
|
|
819
|
+
Object.assign(this, doc)
|
|
820
|
+
this.scripts = doc.scripts.map(newView)
|
|
821
|
+
|
|
822
|
+
this.width = null
|
|
823
|
+
this.height = null
|
|
824
|
+
this.el = null
|
|
825
|
+
this.defs = null
|
|
826
|
+
this.scale = options.scale
|
|
827
|
+
this.iconStyle = options.style.replace("scratch3-", "")
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
measure() {
|
|
831
|
+
this.scripts.forEach(script => {
|
|
832
|
+
script.measure()
|
|
833
|
+
})
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
render(cb) {
|
|
837
|
+
if (typeof cb === "function") {
|
|
838
|
+
throw new Error("render() no longer takes a callback")
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// measure strings
|
|
842
|
+
this.measure()
|
|
843
|
+
|
|
844
|
+
// TODO: separate layout + render steps.
|
|
845
|
+
// render each script
|
|
846
|
+
let width = 0
|
|
847
|
+
let height = 0
|
|
848
|
+
const elements = []
|
|
849
|
+
for (let i = 0; i < this.scripts.length; i++) {
|
|
850
|
+
const script = this.scripts[i]
|
|
851
|
+
if (height) {
|
|
852
|
+
height += 10
|
|
853
|
+
}
|
|
854
|
+
script.y = height
|
|
855
|
+
elements.push(SVG.move(0, height, script.draw(this.iconStyle)))
|
|
856
|
+
height += script.height
|
|
857
|
+
if (i !== this.scripts.length - 1) {
|
|
858
|
+
height += 36
|
|
859
|
+
}
|
|
860
|
+
width = Math.max(width, script.width + 4)
|
|
861
|
+
}
|
|
862
|
+
this.width = width
|
|
863
|
+
this.height = height
|
|
864
|
+
|
|
865
|
+
// return SVG
|
|
866
|
+
const svg = SVG.newSVG(width, height, this.scale)
|
|
867
|
+
const icons =
|
|
868
|
+
this.iconStyle === "high-contrast"
|
|
869
|
+
? makeHighContrastIcons()
|
|
870
|
+
: makeOriginalIcons()
|
|
871
|
+
svg.appendChild((this.defs = SVG.withChildren(SVG.el("defs"), icons)))
|
|
872
|
+
|
|
873
|
+
svg.appendChild(
|
|
874
|
+
SVG.setProps(SVG.group(elements), {
|
|
875
|
+
style: `transform: scale(${this.scale})`,
|
|
876
|
+
}),
|
|
877
|
+
)
|
|
878
|
+
this.el = svg
|
|
879
|
+
return svg
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/* Export SVG image as XML string */
|
|
883
|
+
exportSVGString() {
|
|
884
|
+
if (this.el == null) {
|
|
885
|
+
throw new Error("call draw() first")
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const style = makeStyle()
|
|
889
|
+
this.defs.appendChild(style)
|
|
890
|
+
const xml = new SVG.XMLSerializer().serializeToString(this.el)
|
|
891
|
+
this.defs.removeChild(style)
|
|
892
|
+
return xml
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/* Export SVG image as data URI */
|
|
896
|
+
exportSVG() {
|
|
897
|
+
const xml = this.exportSVGString()
|
|
898
|
+
return `data:image/svg+xml;utf8,${xml.replace(/[#]/g, encodeURIComponent)}`
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
toCanvas(cb, exportScale) {
|
|
902
|
+
exportScale = exportScale || 1.0
|
|
903
|
+
|
|
904
|
+
const canvas = SVG.makeCanvas()
|
|
905
|
+
canvas.width = Math.max(1, this.width * exportScale * this.scale)
|
|
906
|
+
canvas.height = Math.max(1, this.height * exportScale * this.scale)
|
|
907
|
+
const context = canvas.getContext("2d")
|
|
908
|
+
|
|
909
|
+
const image = new Image()
|
|
910
|
+
image.src = this.exportSVG()
|
|
911
|
+
image.onload = () => {
|
|
912
|
+
context.save()
|
|
913
|
+
context.scale(exportScale, exportScale)
|
|
914
|
+
context.drawImage(image, 0, 0)
|
|
915
|
+
context.restore()
|
|
916
|
+
|
|
917
|
+
cb(canvas)
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
exportPNG(cb, scale) {
|
|
922
|
+
this.toCanvas(canvas => {
|
|
923
|
+
if (URL && URL.createObjectURL && Blob && canvas.toBlob) {
|
|
924
|
+
canvas.toBlob(blob => {
|
|
925
|
+
cb(URL.createObjectURL(blob))
|
|
926
|
+
}, "image/png")
|
|
927
|
+
} else {
|
|
928
|
+
cb(canvas.toDataURL("image/png"))
|
|
929
|
+
}
|
|
930
|
+
}, scale)
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const viewFor = node => {
|
|
935
|
+
switch (node.constructor) {
|
|
936
|
+
case Label:
|
|
937
|
+
return LabelView
|
|
938
|
+
case Icon:
|
|
939
|
+
return IconView
|
|
940
|
+
case Input:
|
|
941
|
+
return InputView
|
|
942
|
+
case Newline:
|
|
943
|
+
return NewlineView
|
|
944
|
+
case Block:
|
|
945
|
+
return BlockView
|
|
946
|
+
case Comment:
|
|
947
|
+
return CommentView
|
|
948
|
+
case Glow:
|
|
949
|
+
return GlowView
|
|
950
|
+
case Script:
|
|
951
|
+
return ScriptView
|
|
952
|
+
case Document:
|
|
953
|
+
return DocumentView
|
|
954
|
+
default:
|
|
955
|
+
throw new Error(`no view for ${node.constructor.name}`)
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
export const newView = (node, options) => new (viewFor(node))(node, options)
|