Skip to content

Commit 701e9d1

Browse files
author
hartsantler
committed
GoogleBlockly: new @block.javascript_callback for StatementBlock, that triggers callback on the fly without having to recompile all the python code to javascript.
1 parent 2f9e29b commit 701e9d1

3 files changed

Lines changed: 132 additions & 43 deletions

File tree

bindings/blockly.py

Lines changed: 100 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@
55
BlocklyBlockGenerators = dict() ## Blocks share a single namespace in Blockly
66
_blockly_selected_block = None ## for triggering on_click_callback
77

8+
with javascript: BlocklyBlockInstances = {} ## block-uid : block instance
9+
_blockly_instances_uid = 0
10+
11+
with javascript:
12+
NEW_LINE = String.fromCharCode(10)
13+
14+
def __blockinstance( result, block_uid ):
15+
with javascript:
16+
BlocklyBlockInstances[ block_uid ].pythonjs_object = result
17+
return result
18+
819
def initialize_blockly( blockly_id='blocklyDiv', toolbox_id='toolbox', on_changed_callback=None ):
920
print 'initialize_blockly'
1021
if len( BlocklyBlockGenerators.keys() ):
@@ -80,13 +91,14 @@ class BlocklyBlock:
8091
. a bare block can have no inputs or output, and no previous or next statement notches.
8192
8293
'''
83-
def __init__(self, name, title=None, color=None, category=None):
84-
self.setup( name, title=title, color=color, category=None )
85-
86-
def setup(self, name, title=None, color=None, category=None):
87-
if name in BlocklyBlockGenerators: raise TypeError
88-
BlocklyBlockGenerators[ name ] = self
89-
self.name = name
94+
def __init__(self, block_name=None, title=None, color=None, category=None):
95+
self.setup( block_name=block_name, title=title, color=color, category=None )
96+
97+
def setup(self, block_name=None, title=None, color=None, category=None):
98+
if block_name is None: block_name = '_generated_block' + str(len(BlocklyBlockGenerators.keys()))
99+
if block_name in BlocklyBlockGenerators: raise TypeError
100+
BlocklyBlockGenerators[ block_name ] = self
101+
self.name = block_name
90102
self.title = title
91103
self.color = color
92104
self.category = category
@@ -98,24 +110,30 @@ def setup(self, name, title=None, color=None, category=None):
98110
self.stack_output = False
99111
self.is_statement = False
100112
self.external_function = None
113+
self.external_javascript_function = None
101114
self.on_click_callback = None
102115

116+
def javascript_callback(self, jsfunc):
117+
print '@callback', jsfunc
118+
self.set_external_function( jsfunc.NAME, javascript=True )
119+
with javascript: arr = jsfunc.args_signature
120+
for i in range(arr.length): self.add_input_value( arr[i] )
121+
return jsfunc
122+
103123
def callback(self, jsfunc): ## decorator
104124
print '@callback', jsfunc
105125
self.set_external_function( jsfunc.NAME )
106126
with javascript: arr = jsfunc.args_signature
107-
print 'arr', arr
108-
for i in range(arr.length):
109-
self.add_input_value( arr[i] )
127+
for i in range(arr.length): self.add_input_value( arr[i] )
110128
return jsfunc
111129

112130
def set_on_click_callback(self, callback):
113131
self.on_click_callback = callback
114132

115-
def set_external_function(self, func_name):
116-
self.external_function = func_name
117-
if not self.title:
118-
self.title = func_name
133+
def set_external_function(self, func_name, javascript=False):
134+
if javascript: self.external_javascript_function = func_name
135+
else: self.external_function = func_name
136+
if not self.title: self.title = func_name
119137

120138
def set_output(self, output):
121139
if self.stack_input: raise TypeError
@@ -147,15 +165,17 @@ def add_input_value(self, name=None, type=None, title=None):
147165
{'name':name, 'type':type, 'title':title}
148166
)
149167

150-
def add_input_statement(self, name=None, title=None):
168+
def add_input_statement(self, name=None, title=None, callback=None):
151169
if name is None: raise TypeError
152170
if title is None: title = name
153171
self.input_statements.append(
154-
{'name':name, 'title':title}
172+
{'name':name, 'title':title, 'callback':callback}
155173
)
156174

157175

158176
def bind_block(self):
177+
global _blockly_instances_uid
178+
159179
block_name = self.name
160180
stack_input = self.stack_input
161181
stack_output = self.stack_output
@@ -168,9 +188,23 @@ def bind_block(self):
168188
color = self.color
169189
input_values = self.input_values.js_object
170190
input_statements = self.input_statements.js_object
191+
external_function = self.external_function
192+
external_javascript_function = self.external_javascript_function
193+
is_statement = self.is_statement
171194

172195
with javascript:
173196
def init():
197+
block_uid = _blockly_instances_uid
198+
_blockly_instances_uid += 1
199+
BlocklyBlockInstances[ block_uid ] = this
200+
this.uid = block_uid ## note that blockly has its own id called: this.id
201+
202+
this.__input_values = input_values
203+
this.__input_statements = input_statements
204+
this.__external_function = external_function
205+
this.__external_javascript_function = external_javascript_function
206+
this.__is_statement = is_statement
207+
174208
if color:
175209
this.setColour( color )
176210

@@ -206,61 +240,92 @@ def init():
206240

207241
def bind_generator(self):
208242
block_name = self.name
209-
external_function = self.external_function
210-
is_statement = self.is_statement
211-
input_values = self.input_values.js_object
212-
input_statements = self.input_statements.js_object
243+
244+
## this is not safe with recursive functions? or this was due to the bad local scope bug below? (see input=null)
245+
#external_function = self.external_function
246+
#external_javascript_function = self.external_javascript_function
247+
#is_statement = self.is_statement
248+
#input_values = self.input_values.js_object
249+
#input_statements = self.input_statements.js_object
213250

214251
with javascript:
215252
def generator(block):
216-
code = ''
253+
input_values = block.__input_values
254+
input_statements = block.__input_statements
255+
external_function = block.__external_function
256+
external_javascript_function = block.__external_javascript_function
257+
is_statement = block.__is_statement
217258

259+
code = ''
260+
input = null ## TODO fix local scope generator in python_to_pythonjs.py - need to traverse whileloops - the bug pops up here because this is recursive?
218261
args = []
219262

220263
i = 0
221264
while i < input_values.length:
222265
input = input_values[i][...]
223-
a = Blockly.Python.valueToCode(block, input['name'], Blockly.Python.ORDER_NONE)
266+
if external_javascript_function:
267+
a = Blockly.JavaScript.valueToCode(block, input['name'], Blockly.JavaScript.ORDER_NONE)
268+
else:
269+
a = Blockly.Python.valueToCode(block, input['name'], Blockly.Python.ORDER_NONE)
224270
if a is not null: ## blockly API not correct? is says this will return null when nothing is connected.
225271
args.push( a )
226272
i += 1
227-
i = 0
228-
while i < input_statements.length:
229-
input = input_statements[i][...]
230-
a = Blockly.Python.statementToCode(block, input['name'])
231-
if a != '': ## blockly API would be better not returning an empty string here, in case we wanted to set an empty string
232-
args.push( a )
233-
i += 1
273+
274+
## input statements are used for dynamic updates
275+
if block.pythonjs_object:
276+
print 'dynamic blockly js-------'
277+
wrapper = block.pythonjs_object[...]
278+
print 'dynamic wrapper:', wrapper
279+
i = 0
280+
while i < input_statements.length:
281+
input = input_statements[i][...]
282+
#if Object.hasOwnProperty(wrapper, input['name']): ## this fails on THREE.Mesh.position, why?
283+
attr = wrapper[ input['name'] ]
284+
if attr:
285+
print 'dynamic wrapper has attr:', input['name']
286+
js = Blockly.JavaScript.statementToCode(block, input['name'])
287+
if input['callback']:
288+
print 'DYNAMIC--calling:', js
289+
input['callback']( attr, eval(js) )
290+
else:
291+
print 'WARN - input is missing callback', input
292+
i += 1
234293

235294
if external_function:
236295
code += external_function + '(' + ','.join(args) + ')'
296+
elif external_javascript_function:
297+
## TODO what about pure javascript functions?
298+
if is_statement and block.parentBlock_: ## TODO request Blockly API change: "parentBlock_" to "parentBlock"
299+
code += external_javascript_function + '( [' + ','.join(args) + '] )' ## calling from js a pyjs function
300+
237301
else: ## this should be a simple series of statements?
238302
for a in args:
239303
code += a + ';'
240304

241305
if is_statement:
242-
return code + ';' ## statements can directly return
306+
return code + NEW_LINE ## statements can directly return
243307
else:
308+
code = '__blockinstance( ' + code + ' ,' + block.uid + ')'
244309
return [ code, Blockly.Python.ORDER_NONE ] ## return Array
245310

246-
print 'bindings block generator:', block_name
247311
Blockly.Python[ block_name ] = generator
312+
Blockly.JavaScript[ block_name ] = generator
248313

249314

250315

251316
class StatementBlock( BlocklyBlock ):
252317
'''
253318
A statement-block has a previous and/or next statement notch: stack_input and/or stack_output
254319
'''
255-
def __init__(self, name, title=None, stack_input=False, stack_output=False, color=170, category=None):
256-
self.setup( name, title=title, color=color, category=category )
320+
def __init__(self, block_name=None, title=None, stack_input=False, stack_output=False, color=170, category=None):
321+
self.setup( block_name=block_name, title=title, color=color, category=category )
257322
self.make_statement( stack_input=stack_input, stack_output=stack_output)
258323

259324

260325
class ValueBlock( BlocklyBlock ):
261-
def __init__(self, name, title=None, output='*', color=100, category=None):
326+
def __init__(self, block_name=None, title=None, output='*', color=100, category=None):
262327
if output is None: raise TypeError
263-
self.setup( name, title=title, color=color, category=category )
328+
self.setup( block_name=block_name, title=title, color=color, category=category )
264329
self.set_output( output )
265330

266331

pythonjs/python_to_pythonjs.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,13 +1092,16 @@ def visit_FunctionDef(self, node):
10921092
writer.write( '%s.args_signature = [%s]' %(node.name, ','.join(['"%s"'%n.id for n in node.args.args])) )
10931093

10941094
if self._with_js and with_js_decorators:
1095-
## these with-js functions are assigned to a some objects prototype,
1096-
## here we assume that they depend on the special "this" variable,
1097-
## therefore this function can not be marked as f.pythonscript_function,
1098-
## because we need get_attribute(f,'__call__') to dynamically bind "this"
10991095
for dec in with_js_decorators:
1100-
assert '.prototype.' in dec
1101-
writer.write( '%s=%s'%(dec,node.name) )
1096+
if '.prototype.' in dec:
1097+
## these with-js functions are assigned to a some objects prototype,
1098+
## here we assume that they depend on the special "this" variable,
1099+
## therefore this function can not be marked as f.pythonscript_function,
1100+
## because we need get_attribute(f,'__call__') to dynamically bind "this"
1101+
writer.write( '%s=%s'%(dec,node.name) )
1102+
else: ## TODO fix with-javascript decorators
1103+
writer.write( '%s = get_attribute(%s,"__call__")( [%s] )' %(node.name, dec, node.name))
1104+
11021105
elif self._with_js: ## this is just an optimization so we can avoid making wrappers at runtime
11031106
writer.write('%s.pythonscript_function=true'%node.name)
11041107
else:

tests/threejs_meets_blockly_and_ace.html

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@
136136
<script src="blockly_compressed.js" type="text/javascript" charset="utf-8"></script>
137137
<script src="blocks_compressed.js" type="text/javascript" charset="utf-8"></script>
138138
<script src="python_compressed.js" type="text/javascript"></script>
139+
<script src="javascript_compressed.js" type="text/javascript"></script>
139140

140141
<script type="text/javascript" src="msg/js/en.js"></script>
141142

@@ -236,7 +237,24 @@
236237

237238
Meshes = []
238239

240+
block = StatementBlock(block_name='x', stack_input=True, category='Transform')
241+
@block.javascript_callback
242+
def vector(x,y,z):
243+
print 'js-callback VECTOR', x,y,z
244+
with javascript:
245+
return [x,y,z]
246+
247+
with javascript: ## called from blockly generator update
248+
def set_vector( vec, arr ):
249+
print 'js-level blockly callback', vec, arr
250+
vec.x = arr[0]
251+
vec.y = arr[1]
252+
vec.z = arr[2]
253+
239254
geoblock1 = ValueBlock('geoblock1', category='Geometry')
255+
geoblock1.add_input_statement('position', callback=set_vector )
256+
geoblock1.add_input_statement('rotation') # ,callback='xxxx')
257+
geoblock1.add_input_statement('scale') # ,callback='xxxx')
240258
@geoblock1.callback
241259
def Circle( radius, segments, start, end, material=None, child=None ):
242260
geo = CircleGeometry( radius, segments, start, end )
@@ -247,6 +265,8 @@
247265
return mesh
248266

249267
geoblock2 = ValueBlock('geoblock2', category='Geometry')
268+
geoblock2.add_input_statement('position', callback=set_vector )
269+
250270
@geoblock2.callback
251271
def Cube( width, height, length, material=None, child=None ):
252272
geo = CubeGeometry( width, height, length )
@@ -289,13 +309,13 @@
289309

290310
tblock1 = ValueBlock('tblock1', category='Transform')
291311
@tblock1.callback
292-
def move(object, x,y,z):
312+
def position(object, x,y,z):
293313
object.position.set(x,y,z)
294314
return object
295315

296316
tblock2 = ValueBlock('tblock2', category='Transform')
297317
@tblock2.callback
298-
def rotate(object, x,y,z):
318+
def rotation(object, x,y,z):
299319
object.rotation.set(x,y,z)
300320
return object
301321

@@ -307,6 +327,7 @@
307327

308328

309329

330+
310331
sblock1 = StatementBlock('sblock1', stack_output=True, category='Scene')
311332
@sblock1.callback
312333
def initialize():

0 commit comments

Comments
 (0)