scratchblocks-plus 1.0.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 +19 -0
- package/README.md +193 -0
- package/browser.es.js +8 -0
- package/browser.js +8 -0
- package/build/scratchblocks-plus.min.es.js +12 -0
- package/build/scratchblocks-plus.min.es.js.map +1 -0
- package/build/scratchblocks-plus.min.js +12 -0
- package/build/scratchblocks-plus.min.js.map +1 -0
- package/build/translations-all-es.js +11 -0
- package/build/translations-all-es.js.map +1 -0
- package/build/translations-all.js +11 -0
- package/build/translations-all.js.map +1 -0
- package/build/translations-es.js +11 -0
- package/build/translations-es.js.map +1 -0
- package/build/translations.js +11 -0
- package/build/translations.js.map +1 -0
- package/index.d.ts +297 -0
- package/index.js +229 -0
- package/locales/ab.json +1630 -0
- package/locales/af.json +1630 -0
- package/locales/all.d.ts +108 -0
- package/locales/all.js +161 -0
- package/locales/am.json +1925 -0
- package/locales/an.json +1630 -0
- package/locales/ar.json +1924 -0
- package/locales/ast.json +1630 -0
- package/locales/az.json +1925 -0
- package/locales/be.json +1630 -0
- package/locales/bg.json +1924 -0
- package/locales/bn.json +1630 -0
- package/locales/ca.json +1930 -0
- package/locales/ckb.json +1630 -0
- package/locales/cs.json +1930 -0
- package/locales/cy.json +1929 -0
- package/locales/da.json +1924 -0
- package/locales/de.json +1929 -0
- package/locales/el.json +1931 -0
- package/locales/eo.json +1630 -0
- package/locales/es-419.json +1924 -0
- package/locales/es.json +1929 -0
- package/locales/et.json +1924 -0
- package/locales/eu.json +1924 -0
- package/locales/fa.json +1929 -0
- package/locales/fi.json +1924 -0
- package/locales/fil.json +1631 -0
- package/locales/forums.js +37 -0
- package/locales/fr.json +1929 -0
- package/locales/fy.json +1630 -0
- package/locales/ga.json +1924 -0
- package/locales/gd.json +1929 -0
- package/locales/gl.json +1924 -0
- package/locales/ha.json +1630 -0
- package/locales/he.json +1929 -0
- package/locales/hi.json +1635 -0
- package/locales/hr.json +1929 -0
- package/locales/ht.json +1630 -0
- package/locales/hu.json +1930 -0
- package/locales/hy.json +1630 -0
- package/locales/id.json +1929 -0
- package/locales/is.json +1924 -0
- package/locales/it.json +1929 -0
- package/locales/ja-Hira.json +1637 -0
- package/locales/ja.json +1931 -0
- package/locales/ka.json +1630 -0
- package/locales/kk.json +1632 -0
- package/locales/km.json +1630 -0
- package/locales/ko.json +1924 -0
- package/locales/ku.json +1632 -0
- package/locales/lt.json +1924 -0
- package/locales/lv.json +1924 -0
- package/locales/mi.json +1924 -0
- package/locales/mn.json +1631 -0
- package/locales/nb.json +1929 -0
- package/locales/nl.json +1929 -0
- package/locales/nn.json +1630 -0
- package/locales/nso.json +1630 -0
- package/locales/oc.json +1630 -0
- package/locales/or.json +1631 -0
- package/locales/pl.json +1929 -0
- package/locales/pt-br.json +1924 -0
- package/locales/pt.json +1929 -0
- package/locales/qu.json +1630 -0
- package/locales/rap.json +1632 -0
- package/locales/ro.json +1929 -0
- package/locales/ru.json +1929 -0
- package/locales/sk.json +1924 -0
- package/locales/sl.json +1929 -0
- package/locales/sr.json +1924 -0
- package/locales/sv.json +1924 -0
- package/locales/sw.json +1630 -0
- package/locales/th.json +1924 -0
- package/locales/tn.json +1630 -0
- package/locales/tr.json +1932 -0
- package/locales/uk.json +1924 -0
- package/locales/uz.json +1631 -0
- package/locales/vi.json +1925 -0
- package/locales/xh.json +1630 -0
- package/locales/zh-cn.json +1930 -0
- package/locales/zh-tw.json +1930 -0
- package/locales/zu.json +1918 -0
- package/package.json +81 -0
- package/scratch2/blocks.js +1000 -0
- package/scratch2/draw.js +452 -0
- package/scratch2/filter.js +78 -0
- package/scratch2/index.js +12 -0
- package/scratch2/style.css.js +148 -0
- package/scratch2/style.js +214 -0
- package/scratch3/blocks.js +1134 -0
- package/scratch3/draw.js +334 -0
- package/scratch3/index.js +12 -0
- package/scratch3/style.css.js +280 -0
- package/scratch3/style.js +877 -0
- package/syntax/blocks.js +921 -0
- package/syntax/commands.js +1755 -0
- package/syntax/dropdowns.js +688 -0
- package/syntax/extensions.js +34 -0
- package/syntax/index.js +17 -0
- package/syntax/model.js +566 -0
- package/syntax/syntax.js +1091 -0
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Label,
|
|
3
|
+
Icon,
|
|
4
|
+
Input,
|
|
5
|
+
Block,
|
|
6
|
+
Comment,
|
|
7
|
+
Glow,
|
|
8
|
+
Script,
|
|
9
|
+
Document,
|
|
10
|
+
extensions,
|
|
11
|
+
movedExtensions,
|
|
12
|
+
aliasExtensions,
|
|
13
|
+
} from "../syntax/index.js"
|
|
14
|
+
|
|
15
|
+
import SVG from "./draw.js"
|
|
16
|
+
|
|
17
|
+
import style from "./style.js"
|
|
18
|
+
const {
|
|
19
|
+
defaultFontFamily,
|
|
20
|
+
makeStyle,
|
|
21
|
+
makeIcons,
|
|
22
|
+
darkRect,
|
|
23
|
+
bevelFilter,
|
|
24
|
+
darkFilter,
|
|
25
|
+
} = style
|
|
26
|
+
|
|
27
|
+
export class LabelView {
|
|
28
|
+
constructor(label) {
|
|
29
|
+
Object.assign(this, label)
|
|
30
|
+
|
|
31
|
+
this.el = null
|
|
32
|
+
this.height = 12
|
|
33
|
+
this.metrics = null
|
|
34
|
+
this.x = 0
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get isLabel() {
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
draw() {
|
|
42
|
+
return this.el
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get width() {
|
|
46
|
+
return this.metrics.width
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
measure() {
|
|
50
|
+
const value = this.value
|
|
51
|
+
const cls = `sb-${this.cls}`
|
|
52
|
+
this.el = SVG.text(0, 10, value, {
|
|
53
|
+
class: `sb-label ${cls}`,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
let cache = LabelView.metricsCache[cls]
|
|
57
|
+
if (!cache) {
|
|
58
|
+
cache = LabelView.metricsCache[cls] = Object.create(null)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (Object.hasOwnProperty.call(cache, value)) {
|
|
62
|
+
this.metrics = cache[value]
|
|
63
|
+
} else {
|
|
64
|
+
const font = /comment-label/.test(this.cls)
|
|
65
|
+
? "bold 12px Helvetica, Arial, DejaVu Sans, sans-serif"
|
|
66
|
+
: /literal/.test(this.cls)
|
|
67
|
+
? `normal 9px ${defaultFontFamily}`
|
|
68
|
+
: `bold 10px ${defaultFontFamily}`
|
|
69
|
+
this.metrics = cache[value] = LabelView.measure(value, font)
|
|
70
|
+
// TODO: word-spacing? (fortunately it seems to have no effect!)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
static measure(value, font) {
|
|
75
|
+
const context = LabelView.measuring
|
|
76
|
+
context.font = font
|
|
77
|
+
const textMetrics = context.measureText(value)
|
|
78
|
+
const width = (textMetrics.width + 0.5) | 0
|
|
79
|
+
return { width: width }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
LabelView.metricsCache = {}
|
|
84
|
+
LabelView.toMeasure = []
|
|
85
|
+
|
|
86
|
+
class IconView {
|
|
87
|
+
constructor(icon) {
|
|
88
|
+
Object.assign(this, icon)
|
|
89
|
+
|
|
90
|
+
const info = IconView.icons[this.name]
|
|
91
|
+
if (!info) {
|
|
92
|
+
throw new Error(`no info for icon: ${this.name}`)
|
|
93
|
+
}
|
|
94
|
+
Object.assign(this, info)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get isIcon() {
|
|
98
|
+
return true
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
draw() {
|
|
102
|
+
return SVG.symbol(`#${this.name}`, {
|
|
103
|
+
width: this.width,
|
|
104
|
+
height: this.height,
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static get icons() {
|
|
109
|
+
return {
|
|
110
|
+
greenFlag: { width: 20, height: 21, dy: -2 },
|
|
111
|
+
stopSign: { width: 20, height: 20 },
|
|
112
|
+
turnLeft: { width: 15, height: 12, dy: +1 },
|
|
113
|
+
turnRight: { width: 15, height: 12, dy: +1 },
|
|
114
|
+
loopArrow: { width: 14, height: 11 },
|
|
115
|
+
addInput: { width: 4, height: 8 },
|
|
116
|
+
delInput: { width: 4, height: 8 },
|
|
117
|
+
list: { width: 12, height: 14 },
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
class MatrixView {
|
|
123
|
+
constructor(matrix) {
|
|
124
|
+
Object.assign(this, matrix)
|
|
125
|
+
this.x = 0
|
|
126
|
+
|
|
127
|
+
if (this.rows && this.rows.length > 0) {
|
|
128
|
+
const numRows = this.rows.length
|
|
129
|
+
const numCols = this.rows[0].length
|
|
130
|
+
|
|
131
|
+
// Calculate cell size based on target height and number of rows
|
|
132
|
+
const cellSpacing = 0.5
|
|
133
|
+
const targetHeight = 10 // Target height for matrix display (scratch2 text dropdown is 14, subtract padding)
|
|
134
|
+
const availableHeight = targetHeight - (numRows - 1) * cellSpacing
|
|
135
|
+
this.cellSize = Math.max(1, Math.floor(availableHeight / numRows))
|
|
136
|
+
|
|
137
|
+
// Calculate actual rendered dimensions
|
|
138
|
+
this.width = numCols * (this.cellSize + cellSpacing) - cellSpacing
|
|
139
|
+
this.height = numRows * (this.cellSize + cellSpacing) - cellSpacing
|
|
140
|
+
} else {
|
|
141
|
+
this.width = 0
|
|
142
|
+
this.height = 0
|
|
143
|
+
this.cellSize = 0
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
get isMatrix() {
|
|
148
|
+
return true
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
measure() {
|
|
152
|
+
// Already measured in constructor
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
draw(parent) {
|
|
156
|
+
if (!this.rows || this.rows.length === 0) {
|
|
157
|
+
return SVG.group([])
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const cellSize = this.cellSize
|
|
161
|
+
const cellSpacing = 0.5
|
|
162
|
+
const totalCellSize = cellSize + cellSpacing
|
|
163
|
+
const elements = []
|
|
164
|
+
|
|
165
|
+
// Draw matrix cells
|
|
166
|
+
for (let rowIdx = 0; rowIdx < this.rows.length; rowIdx++) {
|
|
167
|
+
const row = this.rows[rowIdx]
|
|
168
|
+
for (let colIdx = 0; colIdx < row.length; colIdx++) {
|
|
169
|
+
const cell = row[colIdx]
|
|
170
|
+
const x = colIdx * totalCellSize
|
|
171
|
+
const y = rowIdx * totalCellSize
|
|
172
|
+
|
|
173
|
+
const isFilled = cell === true
|
|
174
|
+
|
|
175
|
+
// Use custom color or category-based styling
|
|
176
|
+
const rect = SVG.el("rect", {
|
|
177
|
+
x: x,
|
|
178
|
+
y: y,
|
|
179
|
+
width: cellSize,
|
|
180
|
+
height: cellSize,
|
|
181
|
+
"stroke-width": 0,
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
if (isFilled) {
|
|
185
|
+
rect.setAttribute("fill", "#FFFFFF")
|
|
186
|
+
} else {
|
|
187
|
+
rect.classList.add(`sb-${parent.info.category}`)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
elements.push(rect)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return SVG.group(elements)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
class InputView {
|
|
199
|
+
constructor(input) {
|
|
200
|
+
Object.assign(this, input)
|
|
201
|
+
if (input.label) {
|
|
202
|
+
this.label = newView(input.label)
|
|
203
|
+
}
|
|
204
|
+
// Create MatrixView if value is a Matrix
|
|
205
|
+
if (input.value && input.value.isMatrix) {
|
|
206
|
+
this.matrixView = new MatrixView(input.value)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this.x = 0
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
measure() {
|
|
213
|
+
if (this.hasLabel) {
|
|
214
|
+
this.label.measure()
|
|
215
|
+
}
|
|
216
|
+
if (this.matrixView) {
|
|
217
|
+
this.matrixView.measure()
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
static get shapes() {
|
|
222
|
+
return {
|
|
223
|
+
string: SVG.rect,
|
|
224
|
+
number: SVG.roundedRect,
|
|
225
|
+
"number-dropdown": SVG.roundedRect,
|
|
226
|
+
color: SVG.rect,
|
|
227
|
+
dropdown: SVG.rect,
|
|
228
|
+
|
|
229
|
+
boolean: SVG.pointedRect,
|
|
230
|
+
stack: SVG.stackRect,
|
|
231
|
+
reporter: SVG.roundedRect,
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
draw(parent) {
|
|
236
|
+
let w
|
|
237
|
+
let label
|
|
238
|
+
let px
|
|
239
|
+
|
|
240
|
+
// Check if this has a matrix view
|
|
241
|
+
const hasMatrix = !!this.matrixView
|
|
242
|
+
|
|
243
|
+
if (hasMatrix) {
|
|
244
|
+
// Use same padding as text dropdowns for consistency
|
|
245
|
+
px = 4
|
|
246
|
+
const matrixWidth = this.matrixView.width
|
|
247
|
+
w = matrixWidth + px + 4 // Left padding + right margin before arrow
|
|
248
|
+
this.height = 14 // Fixed height matching scratch2 text dropdown
|
|
249
|
+
} else if (this.hasLabel) {
|
|
250
|
+
label = this.label.draw()
|
|
251
|
+
w = Math.max(
|
|
252
|
+
14,
|
|
253
|
+
this.label.width +
|
|
254
|
+
(this.shape === "string" || this.shape === "number-dropdown" ? 6 : 9),
|
|
255
|
+
)
|
|
256
|
+
} else {
|
|
257
|
+
w = this.isInset ? 30 : this.isColor ? 13 : null
|
|
258
|
+
}
|
|
259
|
+
if (this.hasArrow) {
|
|
260
|
+
w += 10
|
|
261
|
+
}
|
|
262
|
+
this.width = w
|
|
263
|
+
|
|
264
|
+
const h = (this.height = hasMatrix
|
|
265
|
+
? 14
|
|
266
|
+
: this.isRound || this.isColor
|
|
267
|
+
? 13
|
|
268
|
+
: 14)
|
|
269
|
+
|
|
270
|
+
// For matrix inputs, use rounded rect shape but with dropdown styling
|
|
271
|
+
const shapeForRender = hasMatrix ? "number-dropdown" : this.shape
|
|
272
|
+
let el = InputView.shapes[shapeForRender](w, h)
|
|
273
|
+
|
|
274
|
+
if (this.isColor) {
|
|
275
|
+
SVG.setProps(el, {
|
|
276
|
+
fill: this.value,
|
|
277
|
+
})
|
|
278
|
+
} else if (this.isDarker || hasMatrix) {
|
|
279
|
+
// Apply darkRect styling for dropdown-like appearance
|
|
280
|
+
el = darkRect(w, h, parent.info.category, el)
|
|
281
|
+
if (parent.info.color) {
|
|
282
|
+
SVG.setProps(el, {
|
|
283
|
+
fill: parent.info.color,
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const result = SVG.group([
|
|
289
|
+
SVG.setProps(el, {
|
|
290
|
+
class: `sb-input sb-input-${hasMatrix ? "number-dropdown" : this.shape}`,
|
|
291
|
+
}),
|
|
292
|
+
])
|
|
293
|
+
|
|
294
|
+
// Render matrix content using MatrixView
|
|
295
|
+
if (hasMatrix) {
|
|
296
|
+
const matrixStartX = px
|
|
297
|
+
const matrixStartY = (h - this.matrixView.height) / 2
|
|
298
|
+
|
|
299
|
+
const matrixEl = this.matrixView.draw(parent)
|
|
300
|
+
result.appendChild(SVG.move(matrixStartX, matrixStartY, matrixEl))
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (this.hasLabel) {
|
|
304
|
+
const x = this.isRound ? 5 : 4
|
|
305
|
+
result.appendChild(SVG.move(x, 0, label))
|
|
306
|
+
}
|
|
307
|
+
if (this.hasArrow) {
|
|
308
|
+
const y = this.shape === "dropdown" ? 5 : 4
|
|
309
|
+
result.appendChild(
|
|
310
|
+
SVG.move(
|
|
311
|
+
w - 10,
|
|
312
|
+
y,
|
|
313
|
+
SVG.polygon({
|
|
314
|
+
points: [7, 0, 3.5, 4, 0, 0],
|
|
315
|
+
fill: "#000",
|
|
316
|
+
opacity: "0.6",
|
|
317
|
+
}),
|
|
318
|
+
),
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
return result
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
class BlockView {
|
|
326
|
+
constructor(block) {
|
|
327
|
+
Object.assign(this, block)
|
|
328
|
+
this.children = block.children.map(newView)
|
|
329
|
+
this.comment = this.comment ? newView(this.comment) : null
|
|
330
|
+
|
|
331
|
+
// Store the original block for path reference
|
|
332
|
+
this.block = block
|
|
333
|
+
|
|
334
|
+
if (
|
|
335
|
+
Object.prototype.hasOwnProperty.call(aliasExtensions, this.info.category)
|
|
336
|
+
) {
|
|
337
|
+
// handle aliases first
|
|
338
|
+
this.info.category = aliasExtensions[this.info.category]
|
|
339
|
+
}
|
|
340
|
+
if (
|
|
341
|
+
Object.prototype.hasOwnProperty.call(movedExtensions, this.info.category)
|
|
342
|
+
) {
|
|
343
|
+
this.info.category = movedExtensions[this.info.category]
|
|
344
|
+
} else if (
|
|
345
|
+
Object.prototype.hasOwnProperty.call(extensions, this.info.category)
|
|
346
|
+
) {
|
|
347
|
+
this.info.category = "extension"
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
this.x = 0
|
|
351
|
+
this.width = null
|
|
352
|
+
this.height = null
|
|
353
|
+
this.firstLine = null
|
|
354
|
+
this.innerWidth = null
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
get isBlock() {
|
|
358
|
+
return true
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
measure() {
|
|
362
|
+
for (const child of this.children) {
|
|
363
|
+
if (child.measure) {
|
|
364
|
+
child.measure()
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (this.comment) {
|
|
368
|
+
this.comment.measure()
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
static get shapes() {
|
|
373
|
+
return {
|
|
374
|
+
stack: SVG.stackRect,
|
|
375
|
+
"c-block": SVG.stackRect,
|
|
376
|
+
"if-block": SVG.stackRect,
|
|
377
|
+
celse: SVG.stackRect,
|
|
378
|
+
cend: SVG.stackRect,
|
|
379
|
+
|
|
380
|
+
cap: SVG.capRect,
|
|
381
|
+
reporter: SVG.roundedRect,
|
|
382
|
+
boolean: SVG.pointedRect,
|
|
383
|
+
hat: SVG.hatRect,
|
|
384
|
+
cat: SVG.hatRect,
|
|
385
|
+
"define-hat": SVG.procHatRect,
|
|
386
|
+
ring: SVG.roundedRect,
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
drawSelf(w, h, lines) {
|
|
391
|
+
// mouths
|
|
392
|
+
if (lines.length > 1) {
|
|
393
|
+
return SVG.mouthRect(w, h, this.isFinal, lines, {
|
|
394
|
+
class: `sb-${this.info.category} sb-bevel`,
|
|
395
|
+
})
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// outlines
|
|
399
|
+
if (this.info.shape === "outline") {
|
|
400
|
+
return SVG.setProps(SVG.stackRect(w, h), {
|
|
401
|
+
class: "sb-outline",
|
|
402
|
+
})
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// rings
|
|
406
|
+
if (this.isRing) {
|
|
407
|
+
const child = this.children[0]
|
|
408
|
+
// We use isStack for InputView; isBlock for BlockView; isScript for ScriptView.
|
|
409
|
+
if (child && (child.isStack || child.isBlock || child.isScript)) {
|
|
410
|
+
const shape = child.isScript
|
|
411
|
+
? "stack"
|
|
412
|
+
: child.isStack
|
|
413
|
+
? child.shape
|
|
414
|
+
: child.info.shape
|
|
415
|
+
return SVG.ringRect(w, h, child.y, child.width, child.height, shape, {
|
|
416
|
+
class: `sb-${this.info.category} sb-bevel`,
|
|
417
|
+
})
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const func = BlockView.shapes[this.info.shape]
|
|
422
|
+
if (!func) {
|
|
423
|
+
throw new Error(`no shape func: ${this.info.shape}`)
|
|
424
|
+
}
|
|
425
|
+
return func(w, h, {
|
|
426
|
+
class: `sb-${this.info.category} sb-bevel`,
|
|
427
|
+
})
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
minDistance(child) {
|
|
431
|
+
if (this.isBoolean) {
|
|
432
|
+
return child.isReporter
|
|
433
|
+
? (4 + child.height / 4) | 0
|
|
434
|
+
: child.isLabel
|
|
435
|
+
? (5 + child.height / 2) | 0
|
|
436
|
+
: child.isBoolean || child.shape === "boolean"
|
|
437
|
+
? 5
|
|
438
|
+
: (2 + child.height / 2) | 0
|
|
439
|
+
}
|
|
440
|
+
if (this.isReporter) {
|
|
441
|
+
return (child.isInput && child.isRound) ||
|
|
442
|
+
((child.isReporter || child.isBoolean) && !child.hasScript)
|
|
443
|
+
? 0
|
|
444
|
+
: child.isLabel
|
|
445
|
+
? (2 + child.height / 2) | 0
|
|
446
|
+
: (-2 + child.height / 2) | 0
|
|
447
|
+
}
|
|
448
|
+
return 0
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
static get padding() {
|
|
452
|
+
return {
|
|
453
|
+
hat: [15, 6, 2],
|
|
454
|
+
cat: [15, 6, 2],
|
|
455
|
+
"define-hat": [21, 8, 9],
|
|
456
|
+
reporter: [3, 4, 1],
|
|
457
|
+
boolean: [3, 4, 2],
|
|
458
|
+
cap: [6, 6, 2],
|
|
459
|
+
"c-block": [3, 6, 2],
|
|
460
|
+
"if-block": [3, 6, 2],
|
|
461
|
+
ring: [4, 4, 2],
|
|
462
|
+
null: [4, 6, 2],
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
draw() {
|
|
467
|
+
const isDefine = this.info.shape === "define-hat"
|
|
468
|
+
let children = this.children
|
|
469
|
+
|
|
470
|
+
const padding = BlockView.padding[this.info.shape] || BlockView.padding.null
|
|
471
|
+
let pt = padding[0]
|
|
472
|
+
const px = padding[1]
|
|
473
|
+
const pb = padding[2]
|
|
474
|
+
|
|
475
|
+
let y = 0
|
|
476
|
+
const Line = function (y) {
|
|
477
|
+
this.y = y
|
|
478
|
+
this.width = 0
|
|
479
|
+
this.height = y ? 13 : 16
|
|
480
|
+
this.children = []
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
let innerWidth = 0
|
|
484
|
+
let scriptWidth = 0
|
|
485
|
+
let line = new Line(y)
|
|
486
|
+
const pushLine = isLast => {
|
|
487
|
+
if (lines.length === 0) {
|
|
488
|
+
line.height += pt + pb
|
|
489
|
+
} else {
|
|
490
|
+
line.height += isLast ? 0 : +2
|
|
491
|
+
line.y -= 1
|
|
492
|
+
}
|
|
493
|
+
y += line.height
|
|
494
|
+
lines.push(line)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (this.info.isRTL) {
|
|
498
|
+
let start = 0
|
|
499
|
+
const flip = () => {
|
|
500
|
+
children = children
|
|
501
|
+
.slice(0, start)
|
|
502
|
+
.concat(children.slice(start, i).reverse())
|
|
503
|
+
.concat(children.slice(i))
|
|
504
|
+
}
|
|
505
|
+
let i
|
|
506
|
+
for (i = 0; i < children.length; i++) {
|
|
507
|
+
if (children[i].isScript) {
|
|
508
|
+
flip()
|
|
509
|
+
start = i + 1
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (start < i) {
|
|
513
|
+
flip()
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const lines = []
|
|
518
|
+
for (let i = 0; i < children.length; i++) {
|
|
519
|
+
const child = children[i]
|
|
520
|
+
child.el = child.draw(this)
|
|
521
|
+
|
|
522
|
+
if (child.isScript && this.isCommand) {
|
|
523
|
+
this.hasScript = true
|
|
524
|
+
pushLine()
|
|
525
|
+
child.y = y
|
|
526
|
+
lines.push(child)
|
|
527
|
+
scriptWidth = Math.max(scriptWidth, Math.max(1, child.width))
|
|
528
|
+
child.height = Math.max(12, child.height) + 3
|
|
529
|
+
y += child.height
|
|
530
|
+
line = new Line(y)
|
|
531
|
+
} else if (child.isArrow) {
|
|
532
|
+
line.children.push(child)
|
|
533
|
+
} else {
|
|
534
|
+
const cmw = i > 0 ? 30 : 0 // 27
|
|
535
|
+
const md = this.isCommand ? 0 : this.minDistance(child)
|
|
536
|
+
const mw = this.isCommand
|
|
537
|
+
? child.isBlock || child.isInput
|
|
538
|
+
? cmw
|
|
539
|
+
: 0
|
|
540
|
+
: md
|
|
541
|
+
if (mw && !lines.length && line.width < mw - px) {
|
|
542
|
+
line.width = mw - px
|
|
543
|
+
}
|
|
544
|
+
child.x = line.width
|
|
545
|
+
line.width += child.width
|
|
546
|
+
innerWidth = Math.max(innerWidth, line.width + Math.max(0, md - px))
|
|
547
|
+
line.width += 4
|
|
548
|
+
if (!child.isLabel) {
|
|
549
|
+
line.height = Math.max(line.height, child.height)
|
|
550
|
+
}
|
|
551
|
+
line.children.push(child)
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
pushLine(true)
|
|
555
|
+
|
|
556
|
+
innerWidth = Math.max(
|
|
557
|
+
innerWidth + px * 2,
|
|
558
|
+
this.isHat || this.hasScript
|
|
559
|
+
? 83
|
|
560
|
+
: this.isCommand || this.isOutline || this.isRing
|
|
561
|
+
? 39
|
|
562
|
+
: 20,
|
|
563
|
+
)
|
|
564
|
+
this.height = y
|
|
565
|
+
this.width = scriptWidth
|
|
566
|
+
? Math.max(innerWidth, 15 + scriptWidth)
|
|
567
|
+
: innerWidth
|
|
568
|
+
if (isDefine) {
|
|
569
|
+
const p = Math.min(26, (3.5 + 0.13 * innerWidth) | 0) - 18
|
|
570
|
+
this.height += p
|
|
571
|
+
pt += 2 * p
|
|
572
|
+
}
|
|
573
|
+
this.firstLine = lines[0]
|
|
574
|
+
this.innerWidth = innerWidth
|
|
575
|
+
|
|
576
|
+
const objects = []
|
|
577
|
+
|
|
578
|
+
for (const line of lines) {
|
|
579
|
+
if (line.isScript) {
|
|
580
|
+
objects.push(SVG.move(15, line.y, line.el))
|
|
581
|
+
continue
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const h = line.height
|
|
585
|
+
|
|
586
|
+
for (const child of line.children) {
|
|
587
|
+
if (child.isArrow) {
|
|
588
|
+
objects.push(SVG.move(innerWidth - 15, this.height - 3, child.el))
|
|
589
|
+
continue
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
let y = pt + (h - child.height - pt - pb) / 2 - 1
|
|
593
|
+
if (isDefine && child.isLabel) {
|
|
594
|
+
y += 3
|
|
595
|
+
} else if (child.isIcon) {
|
|
596
|
+
y += child.dy | 0
|
|
597
|
+
}
|
|
598
|
+
if (this.isRing) {
|
|
599
|
+
child.y = (line.y + y) | 0
|
|
600
|
+
if (child.isInset) {
|
|
601
|
+
continue
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
objects.push(SVG.move(px + child.x, (line.y + y) | 0, child.el))
|
|
605
|
+
|
|
606
|
+
if (child.diff === "+") {
|
|
607
|
+
const ellipse = SVG.insEllipse(child.width, child.height)
|
|
608
|
+
objects.push(SVG.move(px + child.x, (line.y + y) | 0, ellipse))
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const el = this.drawSelf(innerWidth, this.height, lines)
|
|
614
|
+
objects.splice(0, 0, el)
|
|
615
|
+
if (this.info.color) {
|
|
616
|
+
SVG.setProps(el, {
|
|
617
|
+
fill: this.info.color,
|
|
618
|
+
})
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const group = SVG.group(objects)
|
|
622
|
+
|
|
623
|
+
// Add data-block-path attribute for highlighting support
|
|
624
|
+
if (this.block && this.block.blockPath) {
|
|
625
|
+
SVG.setProps(group, {
|
|
626
|
+
"data-block-path": this.block.blockPath,
|
|
627
|
+
})
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return group
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
class CommentView {
|
|
635
|
+
constructor(comment) {
|
|
636
|
+
Object.assign(this, comment)
|
|
637
|
+
this.label = newView(comment.label)
|
|
638
|
+
|
|
639
|
+
this.width = null
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
get isComment() {
|
|
643
|
+
return true
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
static get lineLength() {
|
|
647
|
+
return 12
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
get height() {
|
|
651
|
+
return 20
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
measure() {
|
|
655
|
+
this.label.measure()
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
draw() {
|
|
659
|
+
const labelEl = this.label.draw()
|
|
660
|
+
|
|
661
|
+
this.width = this.label.width + 16
|
|
662
|
+
return SVG.group([
|
|
663
|
+
SVG.commentLine(this.hasBlock ? CommentView.lineLength : 0, 6),
|
|
664
|
+
SVG.commentRect(this.width, this.height, {
|
|
665
|
+
class: "sb-comment",
|
|
666
|
+
}),
|
|
667
|
+
SVG.move(8, 4, labelEl),
|
|
668
|
+
])
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
class GlowView {
|
|
673
|
+
constructor(glow) {
|
|
674
|
+
Object.assign(this, glow)
|
|
675
|
+
this.child = newView(glow.child)
|
|
676
|
+
|
|
677
|
+
this.width = null
|
|
678
|
+
this.height = null
|
|
679
|
+
this.y = 0
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
get isGlow() {
|
|
683
|
+
return true
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
measure() {
|
|
687
|
+
this.child.measure()
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
drawSelf() {
|
|
691
|
+
const c = this.child
|
|
692
|
+
let el
|
|
693
|
+
const w = this.width
|
|
694
|
+
const h = this.height - 1
|
|
695
|
+
if (c.isScript) {
|
|
696
|
+
if (!c.isEmpty && c.blocks[0].isHat) {
|
|
697
|
+
el = SVG.hatRect(w, h)
|
|
698
|
+
} else if (c.isFinal) {
|
|
699
|
+
el = SVG.capRect(w, h)
|
|
700
|
+
} else {
|
|
701
|
+
el = SVG.stackRect(w, h)
|
|
702
|
+
}
|
|
703
|
+
} else {
|
|
704
|
+
el = c.drawSelf(w, h, [])
|
|
705
|
+
}
|
|
706
|
+
return SVG.setProps(el, {
|
|
707
|
+
class: "sb-diff sb-diff-ins",
|
|
708
|
+
})
|
|
709
|
+
}
|
|
710
|
+
// TODO how can we always raise Glows above their parents?
|
|
711
|
+
|
|
712
|
+
draw() {
|
|
713
|
+
const c = this.child
|
|
714
|
+
const el = c.isScript ? c.draw(true) : c.draw()
|
|
715
|
+
|
|
716
|
+
this.width = c.width
|
|
717
|
+
this.height = (c.isBlock && c.firstLine.height) || c.height
|
|
718
|
+
|
|
719
|
+
// encircle
|
|
720
|
+
return SVG.group([el, this.drawSelf()])
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
class ScriptView {
|
|
725
|
+
constructor(script) {
|
|
726
|
+
Object.assign(this, script)
|
|
727
|
+
this.blocks = script.blocks.map(newView)
|
|
728
|
+
|
|
729
|
+
this.y = 0
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
get isScript() {
|
|
733
|
+
return true
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
measure() {
|
|
737
|
+
for (const block of this.blocks) {
|
|
738
|
+
block.measure()
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
draw(inside) {
|
|
743
|
+
const children = []
|
|
744
|
+
let y = 0
|
|
745
|
+
this.width = 0
|
|
746
|
+
for (const block of this.blocks) {
|
|
747
|
+
const x = inside ? 0 : 2
|
|
748
|
+
const child = block.draw()
|
|
749
|
+
children.push(SVG.move(x, y, child))
|
|
750
|
+
this.width = Math.max(this.width, block.width)
|
|
751
|
+
|
|
752
|
+
const diff = block.diff
|
|
753
|
+
if (diff === "-") {
|
|
754
|
+
const dw = block.width
|
|
755
|
+
const dh = block.firstLine.height || block.height
|
|
756
|
+
children.push(SVG.move(x, y + dh / 2 + 1, SVG.strikethroughLine(dw)))
|
|
757
|
+
this.width = Math.max(this.width, block.width)
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
y += block.height
|
|
761
|
+
|
|
762
|
+
const comment = block.comment
|
|
763
|
+
if (comment) {
|
|
764
|
+
const line = block.firstLine
|
|
765
|
+
const cx = block.innerWidth + 2 + CommentView.lineLength
|
|
766
|
+
const cy = y - block.height + line.height / 2
|
|
767
|
+
const el = comment.draw()
|
|
768
|
+
children.push(SVG.move(cx, cy - comment.height / 2, el))
|
|
769
|
+
this.width = Math.max(this.width, cx + comment.width)
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
this.height = y
|
|
773
|
+
if (!inside && !this.isFinal) {
|
|
774
|
+
this.height += 3
|
|
775
|
+
}
|
|
776
|
+
const lastBlock = this.blocks[this.blocks.length - 1]
|
|
777
|
+
if (!inside && lastBlock.isGlow) {
|
|
778
|
+
this.height += 2 // TODO unbreak this
|
|
779
|
+
}
|
|
780
|
+
return SVG.group(children)
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
class DocumentView {
|
|
785
|
+
constructor(doc, options) {
|
|
786
|
+
Object.assign(this, doc)
|
|
787
|
+
this.scripts = doc.scripts.map(newView)
|
|
788
|
+
|
|
789
|
+
// Store reference to original document for block lookup
|
|
790
|
+
this.doc = doc
|
|
791
|
+
|
|
792
|
+
this.width = null
|
|
793
|
+
this.height = null
|
|
794
|
+
this.el = null
|
|
795
|
+
this.defs = null
|
|
796
|
+
this.scale = options.scale
|
|
797
|
+
|
|
798
|
+
// Map of blockPath -> { el } for highlighting
|
|
799
|
+
this.elementMap = new Map()
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
measure() {
|
|
803
|
+
this.scripts.forEach(script => script.measure())
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
render(cb) {
|
|
807
|
+
if (typeof cb === "function") {
|
|
808
|
+
throw new Error("render() no longer takes a callback")
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// measure strings
|
|
812
|
+
this.measure()
|
|
813
|
+
|
|
814
|
+
// TODO: separate layout + render steps.
|
|
815
|
+
// render each script
|
|
816
|
+
let width = 0
|
|
817
|
+
let height = 0
|
|
818
|
+
const elements = []
|
|
819
|
+
for (const script of this.scripts) {
|
|
820
|
+
if (height) {
|
|
821
|
+
height += 10
|
|
822
|
+
}
|
|
823
|
+
script.y = height
|
|
824
|
+
elements.push(SVG.move(0, height, script.draw()))
|
|
825
|
+
height += script.height
|
|
826
|
+
width = Math.max(width, script.width + 4)
|
|
827
|
+
}
|
|
828
|
+
this.width = width
|
|
829
|
+
this.height = height
|
|
830
|
+
|
|
831
|
+
// return SVG
|
|
832
|
+
const svg = SVG.newSVG(width, height, this.scale)
|
|
833
|
+
svg.appendChild(
|
|
834
|
+
(this.defs = SVG.withChildren(SVG.el("defs"), [
|
|
835
|
+
bevelFilter("bevelFilter", false),
|
|
836
|
+
bevelFilter("inputBevelFilter", true),
|
|
837
|
+
darkFilter("inputDarkFilter"),
|
|
838
|
+
...makeIcons(),
|
|
839
|
+
])),
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
svg.appendChild(SVG.group(elements))
|
|
843
|
+
this.el = svg
|
|
844
|
+
|
|
845
|
+
// Build element map after rendering
|
|
846
|
+
this._buildElementMap()
|
|
847
|
+
|
|
848
|
+
return svg
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Build the element map by finding all elements with data-block-path
|
|
853
|
+
*/
|
|
854
|
+
_buildElementMap() {
|
|
855
|
+
if (!this.el) return
|
|
856
|
+
|
|
857
|
+
this.elementMap.clear()
|
|
858
|
+
const blocks = this.el.querySelectorAll("[data-block-path]")
|
|
859
|
+
blocks.forEach(el => {
|
|
860
|
+
const path = el.getAttribute("data-block-path")
|
|
861
|
+
if (path) {
|
|
862
|
+
this.elementMap.set(path, { el })
|
|
863
|
+
}
|
|
864
|
+
})
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Get the SVG element for a block by its path
|
|
869
|
+
* @param {string} path - Block path (e.g., "1.2.1")
|
|
870
|
+
* @returns {SVGElement|null}
|
|
871
|
+
*/
|
|
872
|
+
getElementByPath(path) {
|
|
873
|
+
const entry = this.elementMap.get(path)
|
|
874
|
+
return entry ? entry.el : null
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Highlight a block by its path
|
|
879
|
+
* @param {string} path - Block path
|
|
880
|
+
* @param {Object} options - { blink: boolean }
|
|
881
|
+
*/
|
|
882
|
+
highlightBlock(path, options = {}) {
|
|
883
|
+
const el = this.getElementByPath(path)
|
|
884
|
+
if (!el) return false
|
|
885
|
+
|
|
886
|
+
// Add highlight class to the first child (the shape element)
|
|
887
|
+
const shapeEl = el.firstElementChild
|
|
888
|
+
if (shapeEl) {
|
|
889
|
+
// Clear any existing highlight classes first
|
|
890
|
+
shapeEl.classList.remove("sb-highlight", "sb-blink")
|
|
891
|
+
// Force browser reflow to reset animation
|
|
892
|
+
void shapeEl.getBBox()
|
|
893
|
+
|
|
894
|
+
// Now add the new highlight classes
|
|
895
|
+
shapeEl.classList.add("sb-highlight")
|
|
896
|
+
if (options.blink) {
|
|
897
|
+
shapeEl.classList.add("sb-blink")
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
return true
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Clear highlight from a block
|
|
905
|
+
* @param {string} path - Block path, or null to clear all
|
|
906
|
+
*/
|
|
907
|
+
clearHighlight(path = null) {
|
|
908
|
+
if (path) {
|
|
909
|
+
const el = this.getElementByPath(path)
|
|
910
|
+
if (el) {
|
|
911
|
+
const shapeEl = el.firstElementChild
|
|
912
|
+
if (shapeEl) {
|
|
913
|
+
shapeEl.classList.remove("sb-highlight", "sb-blink")
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
} else {
|
|
917
|
+
// Clear all highlights
|
|
918
|
+
const highlighted = this.el.querySelectorAll(".sb-highlight")
|
|
919
|
+
highlighted.forEach(el => {
|
|
920
|
+
el.classList.remove("sb-highlight", "sb-blink")
|
|
921
|
+
})
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/* Export SVG image as XML string */
|
|
926
|
+
exportSVGString() {
|
|
927
|
+
if (this.el == null) {
|
|
928
|
+
throw new Error("call draw() first")
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const style = makeStyle()
|
|
932
|
+
this.defs.appendChild(style)
|
|
933
|
+
const xml = new SVG.XMLSerializer().serializeToString(this.el)
|
|
934
|
+
this.defs.removeChild(style)
|
|
935
|
+
return xml
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/* Export SVG image as data URI */
|
|
939
|
+
exportSVG() {
|
|
940
|
+
const xml = this.exportSVGString()
|
|
941
|
+
return `data:image/svg+xml;utf8,${xml.replace(/[#]/g, encodeURIComponent)}`
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
toCanvas(cb, exportScale) {
|
|
945
|
+
exportScale = exportScale || 1.0
|
|
946
|
+
|
|
947
|
+
const canvas = SVG.makeCanvas()
|
|
948
|
+
canvas.width = Math.max(1, this.width * exportScale * this.scale)
|
|
949
|
+
canvas.height = Math.max(1, this.height * exportScale * this.scale)
|
|
950
|
+
const context = canvas.getContext("2d")
|
|
951
|
+
|
|
952
|
+
const image = new Image()
|
|
953
|
+
image.src = this.exportSVG()
|
|
954
|
+
image.onload = () => {
|
|
955
|
+
context.save()
|
|
956
|
+
context.scale(exportScale, exportScale)
|
|
957
|
+
context.drawImage(image, 0, 0)
|
|
958
|
+
context.restore()
|
|
959
|
+
|
|
960
|
+
cb(canvas)
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
exportPNG(cb, scale) {
|
|
965
|
+
this.toCanvas(canvas => {
|
|
966
|
+
if (URL && URL.createObjectURL && Blob && canvas.toBlob) {
|
|
967
|
+
canvas.toBlob(blob => {
|
|
968
|
+
cb(URL.createObjectURL(blob))
|
|
969
|
+
}, "image/png")
|
|
970
|
+
} else {
|
|
971
|
+
cb(canvas.toDataURL("image/png"))
|
|
972
|
+
}
|
|
973
|
+
}, scale)
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const viewFor = node => {
|
|
978
|
+
switch (node.constructor) {
|
|
979
|
+
case Label:
|
|
980
|
+
return LabelView
|
|
981
|
+
case Icon:
|
|
982
|
+
return IconView
|
|
983
|
+
case Input:
|
|
984
|
+
return InputView
|
|
985
|
+
case Block:
|
|
986
|
+
return BlockView
|
|
987
|
+
case Comment:
|
|
988
|
+
return CommentView
|
|
989
|
+
case Glow:
|
|
990
|
+
return GlowView
|
|
991
|
+
case Script:
|
|
992
|
+
return ScriptView
|
|
993
|
+
case Document:
|
|
994
|
+
return DocumentView
|
|
995
|
+
default:
|
|
996
|
+
throw new Error(`no view for ${node.constructor.name}`)
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
export const newView = (node, options) => new (viewFor(node))(node, options)
|