Skip to content

Commit

Permalink
Merge pull request #119 from thomthom/master
Browse files Browse the repository at this point in the history
Final fixes and preparations for version 2.1 release.
thomthom committed Feb 25, 2014
2 parents 251efb1 + ad595e1 commit f4ddfe8
Showing 33 changed files with 483 additions and 191 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.sublime-workspace
23 changes: 23 additions & 0 deletions SketchUp STL.sublime-project
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"folders":
[
{
"follow_symlinks": true,
"path": "."
}
],
"settings":
{
"default_encoding": "UTF-8",
"ensure_newline_at_eof_on_save": true,
"rulers":
[
80
],
"show_encoding": true,
"show_line_endings": true,
"tab_size": 2,
"translate_tabs_to_spaces": true,
"trim_trailing_white_space_on_save": true
}
}
23 changes: 12 additions & 11 deletions src/sketchup-stl.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2012 Trimble Navigation Ltd.
# Copyright 2012-2014 Trimble Navigation Ltd.
#
# License: Apache License, Version 2.0
#
@@ -10,42 +10,43 @@

module CommunityExtensions
module STL

PLUGIN_ROOT_PATH = File.dirname(__FILE__)
PLUGIN_PATH = File.join(PLUGIN_ROOT_PATH, 'sketchup-stl')
PLUGIN_STRINGS_PATH = File.join(PLUGIN_PATH, 'strings')

Sketchup::require File.join(PLUGIN_PATH, 'translator')
options = {
:custom_path => PLUGIN_STRINGS_PATH,
:debug => false
}
@translator = Translator.new('STL.strings', options)

# Method for easy access to the translator instance to anything within this
# project.
#
#
# @example
# STL.translate('Hello World')
def self.translate(string)
@translator.get(string)
end

extension = SketchupExtension.new(
STL.translate('STL Import & Export'),
File.join(PLUGIN_PATH, 'loader.rb')
)

extension.description = STL.translate(
'Adds STL file format import and export. ' <<
'This is an open source project sponsored by the SketchUp team. More ' <<
'info and updates at https://github.com/SketchUp/sketchup-stl'
)
extension.version = '2.0.3'
extension.copyright = '2012 Trimble Navigation, released under Apache 2.0'
extension.version = '2.1.0'
extension.copyright = '2012-2014 Trimble Navigation, ' <<
'released under Apache 2.0'
extension.creator = 'J. Foltz, N. Bromham, K. Shroeder, SketchUp Team'

Sketchup.register_extension(extension, true)

end # module STL
end # module CommunityExtensions
2 changes: 1 addition & 1 deletion src/sketchup-stl/SKUI/base.rb
Original file line number Diff line number Diff line change
@@ -99,7 +99,7 @@ def object_id_hex
# Call this method whenever a control property changes, spesifying which
# properties changed. This is sent to the WebDialog for syncing.
#
# @param [Symbol] *properties
# @param [Symbol] properties
#
# @return [Boolean]
# @since 1.0.0
2 changes: 1 addition & 1 deletion src/sketchup-stl/SKUI/bridge.rb
Original file line number Diff line number Diff line change
@@ -34,7 +34,7 @@ def initialize( window, webdialog )
# return_value = window.bridge.call('alert', 'Hello World')
#
# @param [String] function Name of JavaScript function to call.
# @param [Mixed] *args List of arguments for the function call.
# @param [Mixed] args List of arguments for the function call.
#
# @return [Mixed]
# @since 1.0.0
20 changes: 10 additions & 10 deletions src/sketchup-stl/SKUI/checkbox.rb
Original file line number Diff line number Diff line change
@@ -12,12 +12,12 @@ class Checkbox < Control

# @return [Boolean]
# @since 1.0.0
prop_reader_bool( :checked, &TypeCheck::BOOLEAN )
prop_writer( :checked, &TypeCheck::BOOLEAN )

# @since 1.0.0
define_event( :change )
define_event( :click )

# @param [String] label
# @param [Boolean] checked
#
@@ -30,26 +30,26 @@ def initialize( label, checked = false )

# @return [Boolean]
# @since 1.0.0
def check!
checked = true
def check
self.checked = true
end

# @return [Boolean]
# @since 1.0.0
def checked?
checked = window.bridge.get_checkbox_state( ui_id )
self.checked = window.bridge.get_checkbox_state( ui_id )
end

# @return [Boolean]
# @since 1.0.0
def toggle!
checked = !checked?
def toggle
self.checked = !checked?
end

# @return [Boolean]
# @since 1.0.0
def uncheck!
checked = false
def uncheck
self.checked = false
end

end # class
4 changes: 4 additions & 0 deletions src/sketchup-stl/SKUI/control_manager.rb
Original file line number Diff line number Diff line change
@@ -30,6 +30,9 @@ def add_control( control )
# Add to Webdialog
if self.window && self.window.visible?
self.window.bridge.add_control( control )
if control.is_a?(ControlManager)
self.window.bridge.add_container( control )
end
return true
end
false
@@ -40,6 +43,7 @@ def add_control( control )
# @return [Control,Nil]
# @since 1.0.0
def find_control_by_ui_id( ui_id )
return self if self.ui_id == ui_id
for control in @controls
return control if control.ui_id == ui_id
if control.is_a?( ControlManager )
7 changes: 3 additions & 4 deletions src/sketchup-stl/SKUI/core.rb
Original file line number Diff line number Diff line change
@@ -3,9 +3,6 @@

# @since 1.0.0
module SKUI

# @since 1.0.0
VERSION = '0.1.0'.freeze

# @since 1.0.0
PATH = File.dirname( __FILE__ ).freeze
@@ -17,6 +14,8 @@ module SKUI
PLATFORM_IS_OSX = ( Object::RUBY_PLATFORM =~ /darwin/i ) ? true : false
PLATFORM_IS_WINDOWS = !PLATFORM_IS_OSX

# Version and release information.
require File.join( PATH, 'version.rb' )

# Configure Debug mode.
require File.join( PATH, 'debug.rb' )
@@ -50,4 +49,4 @@ def self.reload
$VERBOSE = original_verbose
end

end # module
end # module
32 changes: 31 additions & 1 deletion src/sketchup-stl/SKUI/css/core.css
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@

*
{
cursor: default;
cursor: inherit;
box-sizing: border-box;
}

@@ -27,6 +27,7 @@ html, body
border: 0;
padding: 0;
margin: 0;
cursor: default;
}

html
@@ -48,6 +49,20 @@ body.platform-windows
background: ThreeDFace;
}

/* Windows 8.1 (Or IE11?), or setting text scaling will cause text ir appear
* too big under Windows. Setting explicit size in points appear to make things
* better.
*/
.platform-windows,
.platform-windows input,
.platform-windows select,
.platform-windows textarea,
.platform-windows button,
.platform-windows legend
{
font-size: 9pt;
}


/*******************************************************************************
*
@@ -128,6 +143,21 @@ body.platform-windows
}


/*******************************************************************************
*
* $LABEL
*
******************************************************************************/


.control-label a
{
display: inline-block;
width: 100%;
height: 100%;
}


/*******************************************************************************
*
* $LISTBOX
6 changes: 4 additions & 2 deletions src/sketchup-stl/SKUI/font.rb
Original file line number Diff line number Diff line change
@@ -4,8 +4,10 @@ class Font

attr_accessor( :name, :size, :bold, :italic )

# @param [String] caption
# @param [Control] control Control which focus of forwarded to.
# @param [String] name
# @param [Integer, Nil] size
# @param [Boolean, Nil] bold
# @param [Boolean, Nil] italic
#
# @since 1.0.0
def initialize( name, size = nil, bold = nil, italic = nil )
2 changes: 1 addition & 1 deletion src/sketchup-stl/SKUI/js/bridge.js
Original file line number Diff line number Diff line change
@@ -109,7 +109,7 @@ var Bridge = function() {
message = messages.shift();
if ( message ) {
busy = true;
uri_message = encodeURIComponent( message );
uri_message = encodeURIComponent( message.add_slashes() );
window.location = 'skp:SKUI_Callback@' + uri_message;
return true;
} else {
8 changes: 8 additions & 0 deletions src/sketchup-stl/SKUI/js/string.js
Original file line number Diff line number Diff line change
@@ -5,5 +5,13 @@
******************************************************************************/


String.prototype.add_slashes = function() {
var string = this;
string = string.replace(/\\/g,"\\\\");
string = string.replace(/\'/g,"\\'");
string = string.replace(/\"/g,"\\\"");
return string;
}

//String.prototype.to_css = String.prototype.toString; // Doesn't work.
String.prototype.to_css = function() { return this; }
2 changes: 1 addition & 1 deletion src/sketchup-stl/SKUI/js/ui.js
Original file line number Diff line number Diff line change
@@ -159,7 +159,7 @@ var UI = function() {
var control_class_string = $control.data( 'control_class' );
var control_class = eval( control_class_string );
var control = new control_class( $control );
return control
return control;
},


7 changes: 7 additions & 0 deletions src/sketchup-stl/SKUI/js/ui.label.js
Original file line number Diff line number Diff line change
@@ -26,6 +26,13 @@ Label.add = function( properties ) {
return control;
}

Label.prototype.set_align = function( value ) {
// `value` is a Symbol in Ruby and becomes a string like ":left" in JS.
var css_value = value.substring( 1, value.length );
this.control.css('text-align', css_value);
return value;
};

Label.prototype.set_caption = function( value ) {
$a = this.control.children('a');
$a.text( value );
13 changes: 13 additions & 0 deletions src/sketchup-stl/SKUI/js/ui.window.js
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ function Window( jquery_element ) {
Control.call( this, jquery_element );
this.cancel_button = null;
this.default_button = null;
this.control.data( 'control_class', this.typename );
}

UI.Window = Window;
@@ -37,6 +38,18 @@ Window.init = function( properties ) {
break;
}
});
// Catch when the window received and looses focus.
// (i) These events doesn't trigger correctly when Firebug Lite is active
// because it introduces frames that interfere with the focus
// notifications.
$(window).on( 'focus', function( event ) {
control = UI.get_control( 'body' );
control.callback( 'focus' );
});
$(window).on( 'blur', function( event ) {
control = UI.get_control( 'body' );
control.callback( 'blur' );
});
return;
}

2 changes: 1 addition & 1 deletion src/sketchup-stl/SKUI/js/utilities.js
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ Function.prototype.get_typename = function() {
if (Function.prototype.name === undefined) {
var funcNameRegex = /function\s([^(]{1,})\(/;
var results = funcNameRegex.exec( this.toString() );
return (results && results.length > 1) ? results[1].trim() : "";
return (results && results.length > 1) ? $.trim(results[1]) : "";
} else {
return this.name;
}
18 changes: 17 additions & 1 deletion src/sketchup-stl/SKUI/js/webdialog.js
Original file line number Diff line number Diff line change
@@ -33,9 +33,25 @@ var WebDialog = function() {

userAgent : function() {
return navigator.userAgent;
},


add_scripts : function(scripts) {
var $head = $('head');
for (var i = 0; i < scripts.length; ++i)
{
var script = scripts[i];
// Must use a closure to ensure `script` is returned properly.
(function(script) {
jQuery.getScript(script, function() {
Sketchup.callback('SKUI::Window.on_script_loaded', script);
});
})(scripts[i]);
}
return null;
}


};

}(); // WebDialog
}(); // WebDialog
5 changes: 5 additions & 0 deletions src/sketchup-stl/SKUI/label.rb
Original file line number Diff line number Diff line change
@@ -6,6 +6,10 @@ module SKUI
# @since 1.0.0
class Label < Control

# @return [Control]
# @since 1.0.0
prop( :align, &TypeCheck::TEXTALIGN )

# @return [String]
# @since 1.0.0
prop( :caption, &TypeCheck::STRING )
@@ -28,6 +32,7 @@ class Label < Control
def initialize( caption, control = nil )
super()

@properties[ :align ] = :left
@properties[ :caption ] = caption
@properties[ :control ] = control

90 changes: 74 additions & 16 deletions src/sketchup-stl/SKUI/listbox.rb
Original file line number Diff line number Diff line change
@@ -9,20 +9,19 @@ class Listbox < Control
# @return [Array<String>]
# @since 1.0.0
prop_reader( :items )

# @return [Boolean]
# @since 1.0.0
prop_bool( :multiple, &TypeCheck::BOOLEAN )
prop_reader_bool( :multiple, &TypeCheck::BOOLEAN )

# @return [Integer]
# @since 1.0.0
prop( :size, &TypeCheck::INTEGER )

# @since 1.0.0
define_event( :change )

# @param [Array<String>] list
# @param [Proc] on_click
#
# @since 1.0.0
def initialize( list = [] )
@@ -32,8 +31,9 @@ def initialize( list = [] )
# (?) Check for String content? Convert to strings? Accept #to_a objects?
super()
# (?) Should the :items list be a Hash instead? To allow key/value pairs.
@properties[ :items ] = list
@properties[ :multiple ] = false
@properties[ :items ] = list.dup
@properties[ :multiple ] = false # Select multiple.
@properties[ :size ] = 1 # Makes no sense!
end

# @overload add_item(string, ...)
@@ -108,6 +108,21 @@ def move_selected_down
true
end

# @param [Boolean] value
#
# @return [Boolean]
# @since 1.0.0
def multiple=( value )
value = TypeCheck::BOOLEAN.call( value )
if value && self.size < 2
raise( ArgumentError,
'Can only select multiple when size is greater than 1.' )
end
@properties[ :multiple ] = value
update_properties( :multiple )
value
end

# @overload remove_item(string)
# @param [String] string
#
@@ -122,8 +137,10 @@ def remove_item( arg )
unless index
raise( ArgumentError, 'Invalid item.' )
end
else
elsif arg.is_a?( Integer )
index = arg
else
raise( ArgumentError, 'Invalid argument.' )
end
if index < 0 || index >= @properties[ :items ].length
raise( ArgumentError, 'Index out of range.' )
@@ -133,26 +150,67 @@ def remove_item( arg )
nil
end

# @param [Integer] value
#
# @return [Integer]
# @since 1.0.0
def size=( value )
value = TypeCheck::INTEGER.call( value )
if value < 2
@properties[ :size ] = value
@properties[ :multiple ] = false
update_properties( :size, :multiple )
else
@properties[ :size ] = value
update_properties( :size )
end
value
end

# @return [String]
# @since 1.0.0
def value
data = window.bridge.get_value( "##{ui_id} select" )
@properties[ :value ] = data
data
end

# @param [String] string

# @overload value=(string)
# @param [String] string
# @return [String]
#
# @return [String]
# @overload value=(string,...)
# @param [String] string
# @return [Array<String>]
#
# @overload value=(strings)
# @param [Array<String>] strings
# @return [Array<String>]
# @since 1.0.0
def value=( string )
unless @properties[ :items ].include?( string )
raise( ArgumentError, "'#{string}' not a valid value in list." )
def value=( *args )
if args.size == 1 && args[0].is_a?( Array )
#return self.value=( *args[0] )
return send( :value=, *args[0] )
end
@properties[ :value ] = string

unless args.all? { |item| item.is_a?( String ) }
raise( ArgumentError, 'Arguments must be strings.' )
end

if !self.multiple? && args.size > 1
raise( ArgumentError, 'Not configured to select multiple items.' )
end

items = @properties[ :items ]
unless (items | args).length == items.length
not_in_list = (args - items).join(', ')
raise( ArgumentError, "'#{not_in_list}' not valid values in list." )
end

@properties[ :value ] = args.dup
update_properties( :value )
string
args
end

end # class
end # module
end # module
13 changes: 5 additions & 8 deletions src/sketchup-stl/SKUI/properties.rb
Original file line number Diff line number Diff line change
@@ -2,15 +2,12 @@ module SKUI
# Mix-in module for the Control class. Simplifies the definition of properties
# with getter and setter methods that access the +@properties+ stack.
#
# @todo Make YARD document there properties:
# http://yardoc.org/guides/extending-yard/writing-handlers.html#Creating_a_Simple_DSL_Handler
#
# @since 1.0.0
module Properties

private

# @param [Symbol] *symbols
# @param [Symbol] symbols
#
# @return [Nil]
# @since 1.0.0
@@ -21,7 +18,7 @@ def prop( *symbols, &block )
end
alias :prop_accessor :prop

# @param [Symbol] *symbols
# @param [Symbol] symbols
#
# @return [Nil]
# @since 1.0.0
@@ -31,7 +28,7 @@ def prop_bool( *symbols )
nil
end

# @param [Symbol] *symbols
# @param [Symbol] symbols
#
# @return [Nil]
# @since 1.0.0
@@ -50,7 +47,7 @@ def prop_reader( *symbols )
nil
end

# @param [Symbol] *symbols
# @param [Symbol] symbols
#
# @return [Nil]
# @since 1.0.0
@@ -70,7 +67,7 @@ def prop_reader_bool( *symbols )
# block is given one argument, the new value of the property. Use this to
# add argument validation to the property.
#
# @param [Symbol] *symbols
# @param [Symbol] symbols
#
# @return [Nil]
# @since 1.0.0
10 changes: 9 additions & 1 deletion src/sketchup-stl/SKUI/typecheck.rb
Original file line number Diff line number Diff line change
@@ -69,11 +69,19 @@ module TypeCheck
# @since 1.0.0
INTEGER = Proc.new { |value|
unless value.respond_to?( :to_i )
raise( ArgumentError, 'Not an valid Integer value.' )
raise( ArgumentError, 'Not a valid Integer value.' )
end
value.to_i
}

# @since 1.0.0
TEXTALIGN = Proc.new { |value|
unless [:left, :center, :right].include?( value )
raise( ArgumentError, 'Not a valid alignment value.' )
end
value
}

# @since 1.0.0
STRING = Proc.new { |value|
unless value.respond_to?( :to_s )
6 changes: 6 additions & 0 deletions src/sketchup-stl/SKUI/version.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module SKUI

# @since 1.0.0
VERSION = '0.1.0'.freeze

end
87 changes: 84 additions & 3 deletions src/sketchup-stl/SKUI/window.rb
Original file line number Diff line number Diff line change
@@ -28,6 +28,15 @@ class Window < Base
# @since 1.0.0
define_event( :ready )

# @since 1.0.0
define_event( :close )

# @since 1.0.0
define_event( :focus, :blur )

# @since 1.0.0
define_event( :scripts_loaded )

# @since 1.0.0
THEME_DEFAULT = nil
THEME_GRAPHITE = File.join( PATH_CSS, 'theme_graphite.css' ).freeze
@@ -69,6 +78,9 @@ def initialize( options = {} )

@properties[:theme] = @options[:theme]

@scripts = []
@loaded_scripts = []

# Create a dummy WebDialog here in order for the Bridge to respond in a
# more sensible manner other than being `nil`. The WebDialog is recreated
# right before the window is displayed due to a SketchUp bug.
@@ -77,12 +89,25 @@ def initialize( options = {} )
@bridge = Bridge.new( self, @webdialog )
end

# Adds the given JavaScript. This allow custom solutions outside of SKUI's
# Ruby class wrappers.
#
# @return [Nil]
# @since 1.0.0
def add_script(script_file)
unless File.exist?(script_file)
raise ArgumentError, "File not found: #{script_file}"
end
@scripts << script_file
nil
end

# Returns an array with the width and height of the client area.
#
# @return [Array(Integer,Integer)]
# @since 1.0.0
def client_size
@bridge.call( 'Webdialog.get_client_size' )
@bridge.call( 'WebDialog.get_client_size' )
end

# Adjusts the window so the client area fits the given +width+ and +height+.
@@ -99,7 +124,7 @@ def client_size=( value )
end
# (!) Cache size difference.
@webdialog.set_size( width, height )
client_width, client_height = get_client_size()
client_width, client_height = client_size()
adjust_width = width - client_width
adjust_height = height - client_height
unless adjust_width == 0 && adjust_height == 0
@@ -187,6 +212,16 @@ def title
@options[:title].dup
end

# @return [Nil]
# @since 1.0.0
def toggle
if visible?
close()
else
show()
end
end

# @return [Boolean]
# @since 1.0.0
def visible?
@@ -276,6 +311,8 @@ def callback_handler( webdialog, params )
event_open_url( arguments[0] )
when 'SKUI::Window.on_ready'
event_window_ready( webdialog )
when 'SKUI::Window.on_script_loaded'
event_script_loaded( arguments[0] )
end
ensure
# Inform the Webdialog the message was received so it can process any
@@ -293,6 +330,10 @@ def callback_handler( webdialog, params )
def event_window_ready( webdialog )
Debug.puts( '>> Dialog Ready' )
@bridge.call( 'Bridge.set_window_id', ui_id )
unless @scripts.empty?
@loaded_scripts.clear
@bridge.call( 'WebDialog.add_scripts', @scripts )
end
update_properties( *@properties.keys )
@bridge.add_container( self )
trigger_event( :ready )
@@ -327,6 +368,20 @@ def event_open_url( url )
nil
end


# @param [String] script
#
# @return [Nil]
# @since 1.0.0
def event_script_loaded( script )
#Debug.puts( "SKUI::Window.event_script_loaded(#{script})" )
@loaded_scripts << script
if @loaded_scripts.sort == @scripts.sort
trigger_event( :scripts_loaded )
end
nil
end

# @○param [Hash] options Same as #initialize
#
# @return [UI::WebDialog]
@@ -355,6 +410,28 @@ def init_webdialog( options )
# Ensure the size for fixed windows is set - otherwise SketchUp will use
# the last saved properties.
unless options[:resizable]
# OSX has a bug where it ignores the resize flag and let the user resize
# the window. Setting the min and max values for width and height works
# around this issue.
#
# To make things worse, OSX sets the client size with the min/max
# methods - causing the window to grow if you set the min size to the
# desired target size. To account for this we set the min sizes to be
# a little less that the desired width. The size should be larger than
# the titlebar height.
#
# All this has to be done before we set the size in order to restore the
# desired size because the min/max methods will transpose the external
# size to content size.
#
# The result is that the height is adjustable a little bit, but at least
# it's restrained to be close to the desired size. Lesser evil until
# this is fixed in SketchUp.
webdialog.min_width = options[:width]
webdialog.max_width = options[:width]
webdialog.min_height = options[:height] - 30
webdialog.max_height = options[:height]

webdialog.set_size( options[:width], options[:height] )
end
# Limit the size of the window. The limits can be either an Integer for
@@ -385,6 +462,10 @@ def init_webdialog( options )
# then the WebDialog instance will not GC. Call a wrapper that
# prevents this.
add_callback( webdialog, 'SKUI_Callback', :callback_handler )
# Hook up events to capture when the window closes.
webdialog.set_on_close {
trigger_event( :close )
}
# (i) There appear to be differences between OS when the HTML content
# is prepared. OSX loads HTML on #set_file? Inspect this.
html_file = File.join( PATH_HTML, 'window.html' )
@@ -393,4 +474,4 @@ def init_webdialog( options )
end

end # class
end # module
end # module
3 changes: 2 additions & 1 deletion src/sketchup-stl/css/base.css
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@ body {
*/
html, input, textarea, select, button {
font: caption; /* Use native UI fonts */
font-size: 9pt;
}


@@ -36,4 +37,4 @@ html {
button {
width: 80px;
height: 25px;
}
}
4 changes: 2 additions & 2 deletions src/sketchup-stl/exporter.rb
Original file line number Diff line number Diff line change
@@ -44,7 +44,7 @@ def self.select_export_file
end

def self.export(path, options = OPTIONS)
file = File.new(path , 'w')
file = File.new(path , 'w')
if options['stl_format'] == STL_BINARY
file.binmode
@write_face = method(:write_face_binary)
@@ -66,7 +66,7 @@ def self.find_faces(file, entities, scale, tform)
face_count = 0
entities.each do |entity|
if entity.is_a?(Sketchup::Face)
write_face(file, entity, scale, tform)
write_face(file, entity, scale, tform)
face_count += 1
elsif entity.is_a?(Sketchup::Group) ||
entity.is_a?(Sketchup::ComponentInstance)
10 changes: 5 additions & 5 deletions src/sketchup-stl/html/importer.html
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Import STL Options</title>

<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta http-equiv="MSThemeCompatible" content="Yes">
@@ -18,7 +18,7 @@

<fieldset id="geometry">
<legend class="ui_string">Geometry</legend>

<label id="merge_coplanar" class="checkbox">
<input type="checkbox" id="chkMergeCoplanar" />
<span class="ui_string">Merge coplanar faces</span>
@@ -27,7 +27,7 @@

<fieldset id="scale">
<legend class="ui_string">Scale</legend>

<label id="units" class="droplist">
<span class="ui_string">Units:</span>
<select id="lstUnits">
@@ -38,12 +38,12 @@
<option value="0" class="ui_string">Inches</option>
</select>
</label>

<label id="preserve_origin" class="checkbox">
<input type="checkbox" id="chkPreserveOrigin" />
<span class="ui_string">Preserve drawing origin</span>
</label>

</fieldset>

<div id="footer">
112 changes: 72 additions & 40 deletions src/sketchup-stl/importer.rb
Original file line number Diff line number Diff line change
@@ -22,6 +22,26 @@ class Importer < Sketchup::Importer
IMPORT_FILE_NOT_FOUND = ImportFileNotFound
IMPORT_SKETCHUP_VERSION_NOT_SUPPORTED = 5

# Ruby #pack / #unpack directives:
# http://www.ruby-doc.org/core-2.0.0/String.html#method-i-unpack
UINT16 = 'v'.freeze
UINT32 = 'V'.freeze
REAL32 = 'e'.freeze

UINT16_BYTE_SIZE = 2 # 16 bits
UINT32_BYTE_SIZE = 4 # 32 bits
REAL32_BYTE_SIZE = 4 # 32 bits

BINARY_HEADER_SIZE = 80 # UINT8[80]
BINARY_POINT3D_SIZE = REAL32_BYTE_SIZE * 3
BINARY_VECTOR3D_SIZE = REAL32_BYTE_SIZE * 3

BINARY_POINT3D = (REAL32 * 3).freeze
BINARY_VECTOR3D = (REAL32 * 3).freeze

MESH_NO_SOFTEN_OR_SMOOTH = 0


def initialize
@stl_units = UNIT_MILLIMETERS
@stl_merge = false
@@ -165,48 +185,60 @@ def do_msg(msg)
private :do_msg

def stl_binary_import(filename, try = 1)
stl_conv = get_unit_ratio(@stl_units)
f = File.new(filename, 'rb')
# Header
header = ''
80.times {
c = f.read(1).unpack('c')[0]
if c <= 32 or c > 126 or c.nil?
c = ?.
end
header << c
}
int_size = [42].pack('i').size
float_size = [42.0].pack('f').size
len = f.read(int_size).unpack('i')[0]

pts = []
while !f.eof
normal = f.read(3 * float_size).unpack('fff')
v1 = f.read(3 * float_size).unpack('fff')
v1.map!{|e| e * stl_conv}
v2 = f.read(3 * float_size).unpack('fff')
v2.map!{|e| e * stl_conv}
v3 = f.read(3 * float_size).unpack('fff')
v3.map!{|e| e * stl_conv}
# UINT16 Attribute byte count? (STL format spec)
abc = f.read(2)
pts << [v1, v2, v3]
end # while
f.close

n_triangles = pts.length
mesh = Geom::PolygonMesh.new(3 * n_triangles, n_triangles)
pts.each { |poly| mesh.add_polygon(poly) }

# add faces
unit_ratio_scale = get_unit_ratio(@stl_units)
number_of_triangles = 0
points = []

# Ensure to open the file in binary mode with no encoding.
filemode = 'rb'
if RUBY_VERSION.to_f > 1.8
filemode << ':ASCII-8BIT'
end
File.open(filename, 'rb') { |file|
# Skip the header block because we don't need it. There doesn't appear
# to be anyone implementing any data into this.
file.seek(BINARY_HEADER_SIZE, IO::SEEK_SET)

# Read how many triangles there are should be.
number_of_triangles = file.read(UINT32_BYTE_SIZE).unpack(UINT32)[0]

# Read geometry data.
number_of_triangles.times { |i|
normal = file.read(BINARY_VECTOR3D_SIZE).unpack(BINARY_VECTOR3D)

vertex1 = file.read(BINARY_POINT3D_SIZE).unpack(BINARY_POINT3D)
vertex1.map!{ |value| value * unit_ratio_scale }

vertex2 = file.read(BINARY_POINT3D_SIZE).unpack(BINARY_POINT3D)
vertex2.map!{ |value| value * unit_ratio_scale }

vertex3 = file.read(BINARY_POINT3D_SIZE).unpack(BINARY_POINT3D)
vertex3.map!{ |value| value * unit_ratio_scale }

# Read attribute data.
attributes_byte_size = file.read(UINT16_BYTE_SIZE).unpack(UINT16)[0]
# NOTE: This value appear to be junk value in some files. Files can
# have non-zero attribute-byte-size values, yet there is no extra
# data following this data chunk. Therefore this value is ignored.
#file.seek(attributes_byte_size, IO::SEEK_CUR)

points << [vertex1, vertex2, vertex3]
}
} # File.open

# Generate a PolygonMesh from the parsed STL data.
number_of_points = 3 * number_of_triangles
mesh = Geom::PolygonMesh.new(number_of_points, number_of_triangles)
points.each { |triangle| mesh.add_polygon(triangle) }

# Create SketchUp entities from the PolygonMesh.
entities = Sketchup.active_model.entities
if entities.length > 0
grp = entities.add_group
entities = grp.entities
group = entities.add_group
entities = group.entities
end
st = entities.fill_from_mesh(mesh, false, 0)
return entities
entities.fill_from_mesh(mesh, false, MESH_NO_SOFTEN_OR_SMOOTH)
entities
end
private :stl_binary_import

@@ -266,7 +298,7 @@ def stl_dialog
# Since WebDialogs under OSX isn't truly modal there is a chance the user
# can click the Options button while the window is already open. We then
# just bring it to the front.
#
#
# The reference is being released when the window is closed so it's
# easier to develop - make updates. Otherwise the WebDialog object would
# have been cached. And it also should ensure it's garbage collected.
28 changes: 14 additions & 14 deletions src/sketchup-stl/js/base.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/* UI namespace */
var UI = function() {

var KEYCODE_ENTER = 13;
var KEYCODE_ESC = 27;

return {

init : function() {
UI.activate_key_shortcuts();
UI.sync_checkbox_values();
@@ -18,7 +18,7 @@ var UI = function() {
// Ready Event
window.location = 'skp:Window_Ready@' + ui_strings_params;
},

// Ensure links are opened in the default browser.
activate_key_shortcuts : function() {
$(document).on('keyup', function(event) {
@@ -32,7 +32,7 @@ var UI = function() {
}
});
},

// Ensure links are opened in the default browser.
disable_select : function() {
$(document).on('mousedown selectstart', function(event) {
@@ -42,14 +42,14 @@ var UI = function() {
// right click on text.
$(':not(input, textarea, select, option)').css('user-select', 'none');
},

// Ensure links are opened in the default browser.
disable_context_menu : function() {
$(document).on('contextmenu', function(event) {
return $(event.target).is('input[type=text], textarea');
});
},

/* Update the value attribute of checkbox elements as it's state changes.
* This way the value can be pulled from WebDialog.get_element_value from
* Ruby side.
@@ -66,7 +66,7 @@ var UI = function() {
$checkbox.val( $checkbox.prop('checked') );
});
},

// Set the value of the given element.
update_value : function(element_id, value) {
$element = $('#' + element_id);
@@ -75,15 +75,15 @@ var UI = function() {
$element.prop( 'checked', value );
}
},

// Set the text of the given elements. Argument is a hash with jQuery
// selectors and replacement text.
update_text : function(json) {
for (selector in json) {
$(selector).text( json[selector] );
}
},

// Set the text of the given elements. Argument is a hash with jQuery
// selectors and replacement text.
update_strings : function(strings) {
@@ -93,17 +93,17 @@ var UI = function() {
$(this).text( strings[index] );
});
}

};

// Private Functions

function collect_ui_strings() {
return $('.ui_string').map(function() {
return $.trim( $(this).text() );
}).get();
}

}(); // UI

$(document).ready( UI.init );
$(document).ready( UI.init );
10 changes: 5 additions & 5 deletions src/sketchup-stl/js/importer.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/* Importer namespace */
var Importer = function() {
return {

init : function() {
Importer.setup_events();
},

// Ensure links are opened in the default browser.
setup_events : function() {
// Import
@@ -17,9 +17,9 @@ var Importer = function() {
window.location = 'skp:Event_Cancel';
});
}

};

}(); // Importer

$(document).ready( Importer.init );
$(document).ready( Importer.init );
6 changes: 3 additions & 3 deletions src/sketchup-stl/loader.rb
Original file line number Diff line number Diff line change
@@ -6,16 +6,16 @@

module CommunityExtensions
module STL

IS_OSX = ( Object::RUBY_PLATFORM =~ /darwin/i ? true : false )

# Matches Sketchup.active_model.options['UnitsOptions']['LengthUnit']
UNIT_METERS = 4
UNIT_CENTIMETERS = 3
UNIT_MILLIMETERS = 2
UNIT_FEET = 1
UNIT_INCHES = 0

Sketchup::require File.join(PLUGIN_PATH, 'utils')
Sketchup::require File.join(PLUGIN_PATH, 'importer')
Sketchup::require File.join(PLUGIN_PATH, 'exporter')
82 changes: 41 additions & 41 deletions src/sketchup-stl/translator.rb
Original file line number Diff line number Diff line change
@@ -6,18 +6,18 @@

module CommunityExtensions
module STL

# Class that handles string localizations.
#
#
# It is written to be able compatible with LanguageHandler which ships with
# SketchUp (Current version 8M4). Existing .string files can be used and
# methods are aliases so the class can be used as a drop in replacement.
#
#
# Strings must be placed in similar folder systems to SketchUp and the
# naming of the folders much match what Sketchup.get_locale reports.
#
# Strings should be saved in UTF-8 encoded files, with or without BOM.
#
#
# Enhanced features include:
# * One-line comments can start anywhere.
# * Multi-line comments can start and stop anywhere outside a string.
@@ -28,17 +28,17 @@ module STL
# producing junk data.
# * Can be easily extended to include advanced features such as
# escape-characters if needed.
#
#
# See Tests folder for sample .strings files.
#
#
# Locales can be tested by starting SketchUp with the following arguments:
# sketchup.exe /lang de
#
#
# Note that there must exist a folder with that locale in the SketchUp
# Resources folder.
# https://github.com/SketchUp/sketchup-stl/issues/45#issuecomment-10819945
class Translator

STATE_SEARCH = 0 # Looking for " or /
STATE_IN_KEY = 1 # Looking for "
STATE_EXPECT_EQUAL = 2 # Looking for =
@@ -50,7 +50,7 @@ class Translator
STATE_EXPECT_COMMENT_END = 8 # Found * - Expecting / next
STATE_IN_COMMENT_SINGLE = 9 # Found // - Looking for end of line
STATE_EXPECT_UTF8_BOM = 10 # Looking for UTF-8 BOM

TOKEN_WHITESPACE = /\s/
TOKEN_CONCAT = 43 # +
TOKEN_QUOTE = 34 # "
@@ -60,12 +60,12 @@ class Translator
TOKEN_COMMENT_START = 47 # /
TOKEN_COMMENT_MULTI = 42 # *
TOKEN_COMMENT_SINGLE = 47 # /

class ParseError < StandardError; end

# A second optional Hash argument can be used to specify behaviour that
# differ from LanguageHandler.
#
#
# Option Keys:
# * :custom_path - String pointing to a custom path where the localized
# strings are. If omitted the Translator will look in
@@ -86,7 +86,7 @@ def initialize(filename, options = nil)
end
@strings = parse(filename, @path)
end

# If the requested string is not in the localization dictionary then the
# original string is returned.
#
@@ -116,7 +116,7 @@ def self.GetResourceSubPath(custom_path = nil)
components = full_file_path.split(File::SEPARATOR)
components[-2, 2].join(File::SEPARATOR)
end

# Prints out the dictionary for visual inspection.
#
# @return [String]
@@ -130,17 +130,17 @@ def print_dictionary
end
puts output
end

# @return [String]
def inspect
object_hex_id = "0x%x" % (self.object_id << 1)
size = @strings.size
locale = Sketchup.get_locale
"<#{self.class}::#{object_hex_id} - Strings:#{size} (#{locale})>"
end

private

# Parses the given file and returns a Hash lookup.
#
# @param [String] filename
@@ -157,7 +157,7 @@ def parse(filename, custom_path = nil)
else
full_file_path = Sketchup.get_resource_path(filename)
end

# Define returned dictionary. Make a hash that will return the key given
# if the key doesn't exist. That way, when a translation is missing for
# a string it will be returned un-translated.
@@ -168,17 +168,17 @@ def parse(filename, custom_path = nil)
puts "Warning! Could not load dictionary: #{full_file_path}"
return strings
end

# Read and process the content.
state = STATE_SEARCH
key_buffer = ''
value_buffer = ''
state_cache = nil # Used when comments are exited.

# File position statistics.
last_line_break = nil
line_pos = 0

if Sketchup.version.split('.')[0].to_i < 14
read_flags = 'r'
else
@@ -199,9 +199,9 @@ def parse(filename, custom_path = nil)
line_pos += 1
last_line_break = nil
end

log_state(state, byte)

# Check for UTF-8 BOM at the beginning of the file. (0xEF,0xBB,0xBF)
# This is done here before the rest of the parsing as these are
# special bytes that doesn't appear visible in editors.
@@ -222,12 +222,12 @@ def parse(filename, custom_path = nil)
raise ParseError, parse_error(file, state, byte, line_pos)
end
end

# Process the current byte.
# Note that White-space and EOL matches are done with regex and
# therefore last in evaluation.
case state

# Neutral state looking for the beginning of a key or comment.
when STATE_SEARCH
if byte == TOKEN_QUOTE
@@ -240,7 +240,7 @@ def parse(filename, custom_path = nil)
else
raise ParseError, parse_error(file, state, byte, line_pos)
end

# Parser is inside a key string looking for end-quote.
# All characters that are not the end-quote is considered part of
# the string and is added to the buffer.
@@ -250,7 +250,7 @@ def parse(filename, custom_path = nil)
else
key_buffer << byte
end

# After a key the parser expects to find an equal token or a concat
# token that will allow a string to be split up. Comments are
# allowed.
@@ -269,7 +269,7 @@ def parse(filename, custom_path = nil)
else
raise ParseError, parse_error(file, state, byte, line_pos)
end

# After a key and equal-token is found the parser expects to find
# a value string. Comments are allowed.
when STATE_EXPECT_VALUE
@@ -283,7 +283,7 @@ def parse(filename, custom_path = nil)
else
raise ParseError, parse_error(file, state, byte, line_pos)
end

# Parser is inside a value string looking for end-quote.
# All characters that are not the end-quote is considered part of
# the string and is added to the buffer.
@@ -294,14 +294,14 @@ def parse(filename, custom_path = nil)
else
value_buffer << byte
end

# After a key and value pair has been found the parser expects to
# find and end token or end of line. The end token is only required
# if multiple statements are placed on the same line.
#
# A concat token will kick the parser back into looking for a value
# string.
#
#
# Comments are allowed.
when STATE_EXPECT_END
if byte == TOKEN_END || byte.chr =~ TOKEN_EOL
@@ -318,7 +318,7 @@ def parse(filename, custom_path = nil)
else
raise ParseError, parse_error(file, state, byte, line_pos)
end

# The beginning of a comment is found. The next token is expected to
# be a token for either singe-line or multi-line comment.
when STATE_EXPECT_COMMENT
@@ -329,15 +329,15 @@ def parse(filename, custom_path = nil)
else
raise ParseError, parse_error(file, state, byte, line_pos)
end

# The parser is processing a multi-line comment. When it encounter a
# multi-line token will look for an comment end-token next. All
# other data is ignored.
when STATE_IN_COMMENT_MULTI
if byte == TOKEN_COMMENT_MULTI # Multiline Comment
state = STATE_EXPECT_COMMENT_END
end

# The parser is processing a multi-line comment and the last token
# was an indication for end of comment. If this token is not an
# end-token it will resume to processing the comment.
@@ -347,7 +347,7 @@ def parse(filename, custom_path = nil)
elsif byte != TOKEN_COMMENT_MULTI
state = STATE_IN_COMMENT_MULTI
end

# The parser is processing a single-line comment. The comment ends
# at the first end-of-line.
when STATE_IN_COMMENT_SINGLE
@@ -361,7 +361,7 @@ def parse(filename, custom_path = nil)
return strings
end
alias :ParseLangFile :parse

# Converts a state code into a readable string. For debugging and error
# messages.
#
@@ -383,7 +383,7 @@ def state_to_string(state)
STATE_EXPECT_UTF8_BOM => 'STATE_EXPECT_UTF8_BOM'
}[state]
end

# Prints out the current state of the parser if debugging is enabled.
# Slows down the process a lot when enabled - but gives detailed insight
# to what the parser is doing.
@@ -416,10 +416,10 @@ def log_state(state, byte)
puts "#{state_to_string(state)}\n #{token} (#{byte})"
nil
end

# Generates a formatted string with debug info - used when a ParseError
# is raised.
#
#
# @param [File] file The File object being parsed.
# @param [Integer] state The state of the parser.
# @param [Integer] byte The current byte being read.
@@ -433,6 +433,6 @@ def parse_error(file, state, byte, line_pos)
end

end # class Translator

end # module STL
end # module CommunityExtensions
end # module CommunityExtensions
14 changes: 7 additions & 7 deletions src/sketchup-stl/utils.rb
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ module Utils
# Cleans up the geometry in the given +entities+ collection.
#
# @param [Sketchup::Entities] entities
#
#
# @return [Nil]
def cleanup_geometry(entities)
stack = entities.select { |e| e.is_a?(Sketchup::Edge) }
@@ -25,20 +25,20 @@ def cleanup_geometry(entities)
# In CleanUp the faces are checked to not be duplicate of each other -
# overlapping. But can we assume the STL importer doesn't create
# such messy geometry?
#
#
# There is also a routine in CleanUp omitted here that checks if the
# faces to be merged are degenerate - all edges are parallel.
#
#
# These check have been omitted to save processing time - as they might
# not appear in a STL import? The checks were required in CleanUp due
# to the large amount of degenerate geometry it was fed.
#
#
#
#
# Erasing the shared edges is tricky. Often things get messed up if we
# try to erase them all at once. When colouring the result of
# shared_edges it appear that edges between non-planar faces are
# returned. Not sure why this is.
#
#
# What does seem to work best is to first erase the edge we got from the
# stack and then check the shared set of edges afterwards and erase them
# after we've verified they are not part of any faces any more.
@@ -71,7 +71,7 @@ def cleanup_geometry(entities)
# Should there be a number of cases where healing is required and this
# method doesn't cut it, then it can be replaced with the alternative
# version. But until we have a set of real world cases I want to use this
# one.
# one.
#
# -ThomThom
#
22 changes: 11 additions & 11 deletions src/sketchup-stl/webdialog_extensions.rb
Original file line number Diff line number Diff line change
@@ -6,15 +6,15 @@ module STL

# Helper module to ease some of the communication with WebDialogs.
# Extend the WebDialog instance or include it in a subclass.
#
#
# Instance Extend:
#
#
# window = UI::WebDialog.new(window_options)
# window.extend( WebDialogExtensions )
# # ...
#
#
# Sub-class include:
#
#
# class CustomWindow << UI::WebDialog
# include WebDialogExtensions
# # ...
@@ -23,9 +23,9 @@ module WebDialogExtensions

# Wrapper that makes calling JavaScript functions cleaner and easier. A very
# simplified version of the wrapper used in TT::GUI::Window.
#
#
# `function` is a string with the JavaScript function name.
#
#
# The remaining arguments are optional and will be passed to the function.
def call_function(function, *args)
# Just a simple conversion, which ensures strings are escaped.
@@ -41,26 +41,26 @@ def call_function(function, *args)
end

# (i) Assumes the WebDialog HTML includes `base.js`.
#
#
# Updates the form value of the given element. Use the id attribute of the
# form element - without the `#` prefix.
def update_value(element_id, value)
call_function('UI.update_value', element_id, value)
end

# (i) Assumes the WebDialog HTML includes `base.js`.
#
#
# Updates the text of the given jQuery selector matches.
def update_text(hash)
call_function('UI.update_text', hash)
end

# Returns a JavaScript JSON object for the given Ruby Hash.
def hash_to_json(hash)
data = hash.map { |key, value| "#{key.inspect}: #{value.inspect}" }
"{#{data.join(',')}}"
end

def parse_params(params)
params.split('|||')
end

0 comments on commit f4ddfe8

Please sign in to comment.