making a "file import" block

999 views
Skip to first unread message

Jayod

unread,
Aug 10, 2015, 10:39:16 AM8/10/15
to Blockly
I want to make a block that lets the user import a file into their project.

Ideally the block would have a "browse" button/field on it that opens a filesystem window letting the user select which file they want to import.

Has anyone done anything like this, or have any ideas on a good way to start?

thanks,
jayod

Jayod

unread,
Aug 15, 2015, 12:14:09 PM8/15/15
to Blockly
This turned out to be easier than I expected.  I made a FieldFile out of the FieldTextInput code and changed the showEditor_() function.  Instead of popping up the text editor, it triggers a "read file" event.  Put the word "Browse" into the field and suddenly I have a block that when I click on it the user can select a file.  I put a second field on the block that will hold the filename of the loaded file (and I turn the "browse" field invisible).  

Avi

unread,
Jan 16, 2016, 6:05:47 AM1/16/16
to Blockly
Jayod,

This seems like a neat solution. Would you be able to show us your code for FieldFile ?

Thanks
Avi

Jayod

unread,
Mar 21, 2016, 1:20:26 PM3/21/16
to Blockly
short answer, the source is available at https://github.com/einsteinsworkshop/blockscad .  It's actually called FieldButton and I hid it at the end of blockly core's fieldlabel.js file (because I didn't want to have to figure out how to update the build process - lazy!)  Long answer, I'll provide the code here in my next response.

Jayod

unread,
Mar 21, 2016, 1:22:20 PM3/21/16
to Blockly
This is FieldButton, which holds a button you can click to do something.  I based it off of FieldTextInput and FieldLabel.  Instead of showing an editor, it clicks a button on my page.

// lets try adding in a button, which will be a text element that when you click on it, 
// the "editor" is to do the file choose menu.
goog.provide('Blockly.FieldButton');

goog.require('Blockly.Field');
goog.require('Blockly.Msg');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.userAgent');


/**
 * Class for an editable text field.
 * @param {string} text The initial content of the field.
 * @param {Function=} opt_changeHandler An optional function that is called
 *     to validate any constraints on what the user entered.  Takes the new
 *     text as an argument and returns either the accepted text, a replacement
 *     text, or null to abort the change.
 * @extends {Blockly.Field}
 * @constructor
 */
Blockly.FieldButton = function(text, opt_changeHandler) {
  Blockly.FieldButton.superClass_.constructor.call(this, text);
  this.setChangeHandler(opt_changeHandler);
};
goog.inherits(Blockly.FieldButton, Blockly.Field);

/**
 * Mouse cursor style when over the hotspot that initiates the editor.
 */
Blockly.FieldButton.prototype.CURSOR = 'default';
/**
 * Editable fields are saved by the XML renderer, non-editable fields are not.
 */
Blockly.FieldButton.prototype.EDITABLE = true;

/**
 * Close the input widget if this input is being deleted.
 */
Blockly.FieldButton.prototype.dispose = function() {
  Blockly.WidgetDiv.hideIfOwner(this);
  Blockly.FieldButton.superClass_.dispose.call(this);
};

/**
 * Set the text in this field.
 * @param {?string} text New text.
 * @override
 */
Blockly.FieldButton.prototype.setText = function(text) {
  if (text === null) {
    // No change if null.
    return;
  }
  if (this.sourceBlock_ && this.changeHandler_) {
    var validated = this.changeHandler_(text);
    // If the new text is invalid, validation returns null.
    // In this case we still want to display the illegal result.
    if (validated !== null && validated !== undefined) {
      text = validated;
    }
  }
  Blockly.Field.prototype.setText.call(this, text);
};

/**
 * Show the inline free-text editor on top of the text.
 * @param {boolean=} opt_quietInput True if editor should be created without
 *     focus.  Defaults to false.
 * @private
 */
Blockly.FieldButton.prototype.showEditor_ = function(opt_quietInput) {
  // console.log("editor activated");
  Blockscad.currentInterestingBlock = this.sourceBlock_;
  $('#importStl').click();
};

/**
 * Close the editor, save the results, and dispose of the editable
 * text field's elements.
 * @return {!Function} Closure to call on destruction of the WidgetDiv.
 * @private
 */
Blockly.FieldButton.prototype.widgetDispose_ = function() {
  var thisField = this;
  return function() {
    var htmlInput = Blockly.FieldButton.htmlInput_;
    // Save the edit (if it validates).
    var text = htmlInput.value;
    if (thisField.sourceBlock_ && thisField.changeHandler_) {
      var text1 = thisField.changeHandler_(text);
      if (text1 === null) {
        // Invalid edit.
        text = htmlInput.defaultValue;
      } else if (text1 !== undefined) {
        // Change handler has changed the text.
        text = text1;
      }
    }
    thisField.setText(text);
    thisField.sourceBlock_.rendered && thisField.sourceBlock_.render();
    Blockly.unbindEvent_(htmlInput.onKeyDownWrapper_);
    Blockly.unbindEvent_(htmlInput.onKeyUpWrapper_);
    Blockly.unbindEvent_(htmlInput.onKeyPressWrapper_);
    Blockly.unbindEvent_(htmlInput.onWorkspaceChangeWrapper_);
    Blockly.FieldButton.htmlInput_ = null;
    // Delete the width property.
    Blockly.WidgetDiv.DIV.style.width = 'auto';
  };
};

Jayod

unread,
Mar 21, 2016, 1:23:50 PM3/21/16
to Blockly
Here is the definition of the block that uses it, and the generator code below.

// I want a block for stl import (file). 
Blockly.Blocks['stl_import'] = {
  init: function() {
    this.category = 'PRIMITIVE_CSG'
    this.appendDummyInput()
        .appendField("STL Import");
    this.appendDummyInput('')
        .setAlign(Blockly.ALIGN_RIGHT)
        .appendField(new Blockly.FieldLabel(""),'STL_FILENAME');
    this.appendDummyInput('')
        .setAlign(Blockly.ALIGN_RIGHT)
        .appendField(new Blockly.FieldButton("Browse"),'STL_BUTTON');
    this.appendDummyInput('C')
        .appendField(new Blockly.FieldLabel(""),'STL_CONTENTS')
        .setVisible(false);
    this.setInputsInline(true);
    this.setPreviousStatement(true);
    this.setColourHex(Blockscad.Toolbox.HEX_3D_PRIMITIVE);
    this.setTooltip('');
    this.setWarningText('STL files are not saved with your blocks.');
    this.setHelpUrl('http://www.example.com/');
  },
  onchange: function() {
    if (!this.workspace) {
      // Block has been deleted.
      return;
    }    
    // if one of the value fields is missing, I want to pop up a warning.
    var fn = this.getField('STL_FILENAME').getText();
    var contents = this.getField('STL_CONTENTS').getText();
    if (fn.length > 0) {
      this.getField('STL_BUTTON').setVisible(false);
      this.setCommentText(fn + '\ncenter: (' + Blockscad.csg_center[contents] + ')');
    }
    this.getField('STL_CONTENTS').setVisible(false);
    // this.render();
  }
};


this is the generator code

Jayod

unread,
Mar 21, 2016, 1:29:34 PM3/21/16
to Blockly
Okay, _this_ is the generator code:

Blockly.OpenSCAD['stl_import'] = function(block) {
  var text_filename = block.getFieldValue('STL_FILENAME');
  var code = 'import("' + text_filename + '");\n';
  return code;
};

And this is the code I use in my main app when the button is clicked (which can happen through a UI menu or the file import block) - we use FileSaver.js to handle the file reading:

$('#file-menu').on('change', '#importStl', function(e) { Blockscad.readStlFile(e);});


Blockscad.readStlFile = function(evt) {

  // this can be called from an existing importSTL block.  If so, 
  // don't create a new block - instead, update the fields on the existing block.

  //Retrieve the first (and only!) File from the FileList object
  var f = evt.target.files[0]; 

  if (f) {
    var contents = {};
    var stuff = {};
    var r = new FileReader();

    // all the file processing has to go inside the onload function. -JY
    r.onload = function(e) { 

      var contents = e.target.result;  
      var result = importSTL(contents);
      // console.log("result is:",result);
      var src = result[0];
      var center = result[1];
      if (!center) center = 'blah';
      // console.log(center);
      var proj_name = f.name.substr(0,f.name.lastIndexOf('(')) || f.name;
      proj_name = proj_name.substr(0,f.name.lastIndexOf('.')) || proj_name;

      // trim any whitespace from the beginning or end of the project name
      proj_name = proj_name.replace(/^\s+|\s+$/g,'');
      var proj_name_use = proj_name;
      var add = 1;
      var found_file = 0;
      while (Blockscad.csg_commands[proj_name_use] && !found_file) {
        if (src != Blockscad.csg_commands[proj_name_use]) {
          proj_name_use = proj_name + '_' + add;
          add++;
        }
        else found_file = 1;
      }
      //console.log("stl file parsed is",src);
      // save these CSG commands so I never have to run this conversion again.
      Blockscad.csg_commands[proj_name_use] = src;
      if (!found_file)
        Blockscad.csg_filename[proj_name_use] = f.name + ':::';
      else Blockscad.csg_filename[proj_name_use] += f.name + ':::';

      Blockscad.csg_center[proj_name_use] = center;
      // I've got a file here.  What should I do with it?
      var bt_input;
      if (Blockscad.currentInterestingBlock) {
        // console.log('the current block is:', Blockscad.currentInterestingBlock);
        var fn_input = Blockscad.currentInterestingBlock.getField('STL_FILENAME');
        bt_input = Blockscad.currentInterestingBlock.getField('STL_BUTTON');
        var ct_input = Blockscad.currentInterestingBlock.getField('STL_CONTENTS');
        fn_input.setText(f.name);
        fn_input.setVisible(true);
        bt_input.setVisible(false);
        ct_input.setText(proj_name_use);
        Blockscad.currentInterestingBlock.setCommentText(f.name + '\ncenter:(' + center + ')');

        Blockscad.currentInterestingBlock = null;

      }
      else {
        // lets make some xml and load a block into the workspace.
        // console.log("making block from xml");
        var xml = '<xml xmlns="http://blockscad.einsteinsworkshop.com"><block type="stl_import" id="1" x="10" y="10"><field name="STL_FILENAME">' +
        f.name + '</field>' + '<field name="STL_BUTTON">Browse</field>' + 
        '<field name="STL_CONTENTS">'+ proj_name_use + '</field></block></xml>';
        //console.log("xml is:",xml);
        var stuff = Blockly.Xml.textToDom(xml);
        var newblock = Blockly.Xml.domToBlock(Blockscad.workspace, stuff.firstChild);
        bt_input = newblock.getField('STL_BUTTON');
        bt_input.setVisible(false);
        newblock.setCommentText(f.name + '\ncenter:(' + center + ')');
        newblock.render();
      }

    };
    r.readAsBinaryString(f);
    // in order that we can read this filename again, I'll clear out the current filename
    $("#importStl")[0].value = '';

    // switch us back to the blocks tab in case we were on the code tab.
    $('#displayBlocks').click();
    // enable the render button.
    // $('#renderButton').prop('disabled', false);       

  } else { 
    alert("Failed to load file");
  }
};
Reply all
Reply to author
Forward
0 new messages