Skip to content

Commit b4531a6

Browse files
committed
Bugfix/Feature: Support GIF images, fix drawing imports
Missing `@` for drawing import, added raw base64 prefix detection, dirty check for imported drawing layer redraw.
1 parent 6f4976f commit b4531a6

5 files changed

Lines changed: 77 additions & 44 deletions

File tree

app/assets/javascripts/beak/widgets/config-shims.coffee

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,9 @@ genConfigs = (ractive, viewController, container, compiler) ->
268268
ractive.set('consoleOutput', ractive.get('consoleOutput') + str)
269269

270270
base64ToImageData = (base64) ->
271-
{ array, height, width } = synchroDecoder(base64)
271+
{ array, height, width, didSucceed } = synchroDecoder(base64)
272+
if not didSucceed
273+
throw new Error("Extension exception: Could not decode the image. Only GIF, JPEG, and PNG formats are supported in NetLogo Web.")
272274
new ImageData(array, width, height)
273275

274276
{ asyncDialog: genAsyncDialogConfig(ractive, clearMouse)

app/assets/javascripts/beak/widgets/draw/drawing-layer.coffee

Lines changed: 49 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,16 @@ Possible drawing events:
3636
###
3737

3838
class DrawingLayer extends Layer
39-
# (-> { model: ModelObj, quality: QualityObj, font: FontObj }) -> Unit
39+
# (-> { model: ModelObj, quality: QualityObj, font: FontObj }, (Unit) -> Unit) -> Unit
4040
# see "./layer.coffee" for type info
41-
constructor: (@_getDepInfo) ->
41+
constructor: (@_getDepInfo, @_repaintCallback = ->) ->
4242
super()
4343
@_latestDepInfo = {
4444
model: undefined,
4545
quality: undefined,
4646
font: undefined
4747
}
48+
@_dirty = false
4849
@_canvas = document.createElement('canvas')
4950
@_ctx = @_canvas.getContext('2d')
5051
return
@@ -56,33 +57,38 @@ class DrawingLayer extends Layer
5657
return
5758

5859
repaint: ->
59-
if not mergeInfo(@_latestDepInfo, @_getDepInfo()) then return false
60-
61-
{ model: { model, worldShape }, quality: { quality } } = @_latestDepInfo
62-
{ worldWidth, worldHeight, patchsize } = worldShape
63-
newWidth = worldWidth * patchsize * quality
64-
newHeight = worldHeight * patchsize * quality
65-
if @_canvas.width isnt newWidth or @_canvas.height isnt newHeight
66-
# Save drawing content before resize (setting canvas dimensions always clears the canvas)
67-
prevCanvas = document.createElement('canvas')
68-
prevCanvas.width = @_canvas.width
69-
prevCanvas.height = @_canvas.height
70-
prevCanvas.getContext('2d').drawImage(@_canvas, 0, 0)
71-
@_canvas.width = newWidth
72-
@_canvas.height = newHeight
73-
if prevCanvas.width > 0 and prevCanvas.height > 0
74-
@_ctx.drawImage(prevCanvas, 0, 0, newWidth, newHeight)
75-
for event in model.drawingEvents
76-
switch event.type
77-
when 'clear-drawing' then @_clearDrawing()
78-
when 'line' then @_drawLine(event)
79-
when 'stamp-image'
80-
switch event.agentType
81-
when 'turtle' then @_drawTurtleStamp(event.stamp)
82-
when 'link' then @_drawLinkStamp(event.stamp)
83-
when 'import-drawing' then @_importDrawing(event.imageBase64)
84-
# For those who still remember, `model.drawingEvents` is now reset by the ViewController after
85-
# every layer has finished repainting.
60+
depsChanged = mergeInfo(@_latestDepInfo, @_getDepInfo())
61+
wasDrawnAsync = @_dirty
62+
@_dirty = false
63+
if not depsChanged and not wasDrawnAsync then return false
64+
65+
if depsChanged
66+
{ model: { model, worldShape }, quality: { quality } } = @_latestDepInfo
67+
{ worldWidth, worldHeight, patchsize } = worldShape
68+
newWidth = worldWidth * patchsize * quality
69+
newHeight = worldHeight * patchsize * quality
70+
if @_canvas.width isnt newWidth or @_canvas.height isnt newHeight
71+
# Save drawing content before resize (setting canvas dimensions always clears the canvas)
72+
prevCanvas = document.createElement('canvas')
73+
prevCanvas.width = @_canvas.width
74+
prevCanvas.height = @_canvas.height
75+
prevCanvas.getContext('2d').drawImage(@_canvas, 0, 0)
76+
@_canvas.width = newWidth
77+
@_canvas.height = newHeight
78+
if prevCanvas.width > 0 and prevCanvas.height > 0
79+
@_ctx.drawImage(prevCanvas, 0, 0, newWidth, newHeight)
80+
for event in model.drawingEvents
81+
switch event.type
82+
when 'clear-drawing' then @_clearDrawing()
83+
when 'line' then @_drawLine(event)
84+
when 'stamp-image'
85+
switch event.agentType
86+
when 'turtle' then @_drawTurtleStamp(event.stamp)
87+
when 'link' then @_drawLinkStamp(event.stamp)
88+
when 'import-drawing' then @_importDrawing(event.imageBase64)
89+
# For those who still remember, `model.drawingEvents` is now reset by the ViewController after
90+
# every layer has finished repainting.
91+
8692
true
8793

8894
_clearDrawing: ->
@@ -148,7 +154,17 @@ class DrawingLayer extends Layer
148154
return
149155

150156
_importDrawing: (base64) ->
151-
_clearDrawing()
157+
@_clearDrawing()
158+
src =
159+
if base64.startsWith('data:')
160+
base64
161+
else
162+
# Raw base64 from workspace resources; detect type by magic bytes
163+
mimeType =
164+
if base64.startsWith('R0lG') then 'image/gif'
165+
else if base64.startsWith('/9j/') then 'image/jpeg'
166+
else 'image/png'
167+
"data:#{mimeType};base64,#{base64}"
152168
image = new Image()
153169
image.onload = () =>
154170
canvasRatio = @_canvas.width / @_canvas.height
@@ -163,7 +179,9 @@ class DrawingLayer extends Layer
163179
height = (canvasRatio / imageRatio) * @_canvas.height
164180

165181
@_ctx.drawImage(image, (@_canvas.width - width) / 2, (@_canvas.height - height) / 2, width, height)
166-
image.src = base64
182+
@_dirty = true
183+
@_repaintCallback()
184+
image.src = src
167185
return
168186

169187
# x and y coordinates are given in CSS pixels not accounting for quality.

app/assets/javascripts/beak/widgets/draw/view-controller.coffee

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ import { setImageSmoothing, clearCtx, extractWorldShape } from "./draw-utils.js"
1010
AgentModel = tortoise_require('agentmodel')
1111

1212
# TODO type signature
13-
initLayers = (layerDeps) ->
13+
initLayers = (layerDeps, repaintCallback) ->
1414
# Tis important that we don't access the properties of `layerDeps` except within the client code using `layerDeps`,
1515
# because the identities of the objects will change (see "./layers.coffee"'s comment on layer dependencies' for why).
1616
turtles = new TurtleLayer(-> layerDeps)
1717
patches = new PatchLayer(-> layerDeps)
18-
drawing = new DrawingLayer(-> layerDeps)
18+
drawing = new DrawingLayer((-> layerDeps), repaintCallback)
1919
world = new CompositeLayer([patches, drawing, turtles], -> layerDeps)
2020
spotlight = new SpotlightLayer(-> layerDeps)
2121
highlight = new HighlightLayer(-> layerDeps)
@@ -42,7 +42,7 @@ class ViewController
4242
}
4343
}
4444
@resetModel() # defines `@_model`
45-
@_layers = initLayers(@_layerDeps)
45+
@_layers = initLayers(@_layerDeps, => @repaint())
4646

4747
repaint = => @repaint()
4848
drawingLayer = @_layers.drawing

package-lock.json

Lines changed: 21 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"mocha": {},
77
"dependencies": {
88
"@netlogo/nettango": "0.16.6",
9-
"@netlogo/synchrodecoder": "3.0.0",
9+
"@netlogo/synchrodecoder": "3.1.0",
1010
"codemirror": "5.62.3",
1111
"dompurify": "3.4.5",
1212
"file-saver": "2.0.5",

0 commit comments

Comments
 (0)