Compare commits
17 Commits
2.10.0-rc2
..
2.10.0
| Author | SHA1 | Date | |
|---|---|---|---|
| e6dd630caf | |||
| cad14b318a | |||
| 4df1cc17bf | |||
| 884bb331b3 | |||
| ff4a83af47 | |||
| f6f323b307 | |||
| 330280d611 | |||
| 809432cbbd | |||
| d64fee1d15 | |||
| 3d01e15710 | |||
| c2d7a5aca8 | |||
| 2e8a040210 | |||
| 50886147ae | |||
| c0f2885de6 | |||
| e80574ecd7 | |||
| 7cdcf9e86f | |||
| 304720fc8e |
@@ -13,6 +13,9 @@ settings.json
|
||||
speckle_connector/vue_ui
|
||||
speckle_connector/html
|
||||
|
||||
# speckle-sharp-ci-tools
|
||||
/speckle-sharp-ci-tools
|
||||
|
||||
# _sqlite3
|
||||
/_sqlite3/.vs
|
||||
/_sqlite3/Release (2.7)
|
||||
|
||||
@@ -20,6 +20,7 @@ AllCops:
|
||||
- 'ui/**/*'
|
||||
- 'speckle_connector/src/ext/**/*.rb'
|
||||
- 'vendor/bundle/**/*'
|
||||
- 'tests/**/*.rb'
|
||||
SketchUp:
|
||||
SourcePath: .
|
||||
TargetSketchUpVersion: 2017
|
||||
|
||||
@@ -34,7 +34,11 @@ end
|
||||
ruby_critic_paths = FileList[
|
||||
'speckle_connector/**/*.rb',
|
||||
'speckle_connector.rb',
|
||||
'tests/**/*.rb'] - FileList['_tools/**/*.rb']
|
||||
'tests/**/*.rb'] -
|
||||
FileList[
|
||||
'_tools/**/*.rb',
|
||||
'speckle_connector/src/ext/**/*.rb',
|
||||
]
|
||||
|
||||
# for local
|
||||
RubyCritic::RakeTask.new('rubycritic') do |task|
|
||||
|
||||
+128
-28
@@ -1,6 +1,52 @@
|
||||
#include "Database.h"
|
||||
#include "RubyUtils/RubyUtils.h"
|
||||
#include "ruby.h"
|
||||
#include "sqlite3.h"
|
||||
|
||||
VALUE
|
||||
rbsqlite3_new(VALUE klass, VALUE pathValue)
|
||||
{
|
||||
// Arguments array
|
||||
VALUE argv[1];
|
||||
|
||||
// Convert pathValue to actual path string
|
||||
const char* path;
|
||||
path = StringValuePtr(pathValue);
|
||||
|
||||
SQLite::Database* db = new SQLite::Database(path);
|
||||
|
||||
VALUE obj = Data_Wrap_Struct(klass, NULL, NULL, db);
|
||||
rb_iv_set(obj, "@path", rb_str_new2(path));
|
||||
return obj;
|
||||
}
|
||||
|
||||
VALUE
|
||||
rbsqlite3_table_exist(VALUE klass, VALUE tableNameValue) {
|
||||
// Convert pathValue to actual path string
|
||||
const char* tableName;
|
||||
tableName = StringValuePtr(tableNameValue);
|
||||
|
||||
SQLite::Database* database;
|
||||
Data_Get_Struct(klass, SQLite::Database, database);
|
||||
|
||||
bool val = database->tableExists(tableName);
|
||||
return val ? Qtrue : Qfalse;
|
||||
}
|
||||
|
||||
VALUE
|
||||
rbsqlite3_exec(VALUE klass, VALUE execValue) {
|
||||
VALUE rows = rb_ary_new();
|
||||
|
||||
// Convert pathValue to actual path string
|
||||
const char* query;
|
||||
query = StringValuePtr(execValue);
|
||||
|
||||
SQLite::Database* database;
|
||||
Data_Get_Struct(klass, SQLite::Database, database);
|
||||
|
||||
VALUE val = database->exec(query);
|
||||
return val;
|
||||
}
|
||||
|
||||
VALUE ruby_platform() {
|
||||
return GetRubyInterface(RUBY_PLATFORM);
|
||||
@@ -9,36 +55,91 @@ VALUE ruby_platform() {
|
||||
// Load this module from Ruby using:
|
||||
// require 'Sqlite3'
|
||||
extern "C" {
|
||||
// Proof of concept to test it on Sketchup. Call;
|
||||
// SpeckleConnector::Sqlite.greetings!
|
||||
static VALUE hi_from_c_sqlite3() {
|
||||
char message[] = "hi from c sqlite3!";
|
||||
VALUE str_val = rb_str_new2(message);
|
||||
return str_val;
|
||||
static int hash_callback_function(VALUE callback_ary, int count, char** data, char** columns)
|
||||
{
|
||||
VALUE new_hash = rb_hash_new();
|
||||
int i;
|
||||
|
||||
for (i = 0; i < count; i++) {
|
||||
if (data[i] == NULL) {
|
||||
rb_hash_aset(new_hash, rb_str_new_cstr(columns[i]), Qnil);
|
||||
}
|
||||
else {
|
||||
rb_hash_aset(new_hash, rb_str_new_cstr(columns[i]), rb_str_new_cstr(data[i]));
|
||||
}
|
||||
}
|
||||
|
||||
rb_ary_push(callback_ary, new_hash);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int regular_callback_function(VALUE callback_ary, int count, char** data, char** columns)
|
||||
{
|
||||
VALUE new_ary = rb_ary_new();
|
||||
int i;
|
||||
|
||||
for (i = 0; i < count; i++) {
|
||||
if (data[i] == NULL) {
|
||||
rb_ary_push(new_ary, Qnil);
|
||||
}
|
||||
else {
|
||||
rb_ary_push(new_ary, rb_str_new_cstr(data[i]));
|
||||
}
|
||||
}
|
||||
|
||||
rb_ary_push(callback_ary, new_ary);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Is invoked by calling db.execute_batch2(sql, &block)
|
||||
*
|
||||
* Executes all statements in a given string separated by semicolons.
|
||||
* If a query is made, all values returned are strings
|
||||
* (except for 'NULL' values which return nil),
|
||||
* so the user may parse values with a block.
|
||||
* If no query is made, an empty array will be returned.
|
||||
*/
|
||||
static VALUE rbsqlite3_exec_batch(VALUE self, VALUE sql, VALUE results_as_hash)
|
||||
{
|
||||
SQLite::Database* db;
|
||||
int status;
|
||||
VALUE callback_ary = rb_ary_new();
|
||||
char* errMsg;
|
||||
VALUE errexp;
|
||||
|
||||
Data_Get_Struct(self, SQLite::Database, db);
|
||||
if (!db->getHandle()) \
|
||||
rb_raise(rb_path2class("SQLite3::Exception"), "cannot use a closed database");
|
||||
|
||||
status = sqlite3_exec(db->getHandle(), StringValuePtr(sql), (sqlite3_callback)regular_callback_function, (void*)callback_ary, &errMsg);
|
||||
|
||||
if (status != SQLITE_OK)
|
||||
{
|
||||
errexp = rb_exc_new2(rb_eRuntimeError, errMsg);
|
||||
sqlite3_free(errMsg);
|
||||
rb_exc_raise(errexp);
|
||||
}
|
||||
|
||||
return callback_ary;
|
||||
}
|
||||
|
||||
static void rbsqlite3_free(void* ptr) {
|
||||
delete (SQLite::Database*)ptr;
|
||||
}
|
||||
|
||||
static VALUE rbsqlite3_new(VALUE klass, VALUE pathValue) {
|
||||
VALUE argv[1];
|
||||
const char* path = "C:/Users/sotas/AppData/Roaming/Speckle/Accounts.db";
|
||||
SQLite::Database* database = new SQLite::Database(path);
|
||||
VALUE obj = Data_Wrap_Struct(klass, 0, rbsqlite3_free, database);
|
||||
argv[0] = pathValue;
|
||||
|
||||
rb_obj_call_init(obj, 1, argv);
|
||||
rb_iv_set(obj, "@path", pathValue);
|
||||
return obj;
|
||||
}
|
||||
|
||||
static VALUE rbsqlite3_new2(VALUE klass) {
|
||||
const char* path = "C:/Users/sotas/AppData/Roaming/Speckle/Accounts.db";
|
||||
SQLite::Database* database = new SQLite::Database(path);
|
||||
VALUE obj = Data_Wrap_Struct(klass, 0, rbsqlite3_free, database);
|
||||
rb_obj_call_init(obj, 0, 0);
|
||||
return obj;
|
||||
/* call-seq: db.close
|
||||
*
|
||||
* Closes this database.
|
||||
*/
|
||||
static VALUE rbsqlite3_close(VALUE self)
|
||||
{
|
||||
SQLite::Database* database;
|
||||
Data_Get_Struct(self, SQLite::Database, database);
|
||||
sqlite3_close(database->getHandle());
|
||||
rb_iv_set(self, "-aggregators", Qnil);
|
||||
return self;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,12 +153,11 @@ void Init_sqlite3()
|
||||
VALUE speckle_connector_sqlite3_database = rb_define_class_under(speckle_connector_sqlite3, "Database", rb_cObject);
|
||||
|
||||
rb_define_singleton_method(speckle_connector_sqlite3, "ruby_platform", (ruby_method)ruby_platform, 0);
|
||||
rb_define_singleton_method(speckle_connector_sqlite3, "greetings!", (ruby_method)hi_from_c_sqlite3, 0);
|
||||
|
||||
rb_define_singleton_method(speckle_connector_sqlite3_database, "new", (ruby_method)rbsqlite3_new, 1);
|
||||
rb_define_singleton_method(speckle_connector_sqlite3_database, "new2", (ruby_method)rbsqlite3_new2, 0);
|
||||
|
||||
rb_define_method(speckle_connector_sqlite3_database, "close", (ruby_method)rbsqlite3_free, 0);
|
||||
rb_define_method(speckle_connector_sqlite3_database, "close", (ruby_method)rbsqlite3_close, 0);
|
||||
rb_define_method(speckle_connector_sqlite3_database, "exec", (ruby_method)rbsqlite3_exec_batch, 1);
|
||||
rb_define_method(speckle_connector_sqlite3_database, "table_exist?", (ruby_method)rbsqlite3_table_exist, 1);
|
||||
}
|
||||
|
||||
void Init_sqlite3_20()
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'JSON'
|
||||
require_relative '../ext/sqlite3'
|
||||
require_relative '../constants/path_constants'
|
||||
|
||||
module SpeckleConnector
|
||||
# Accounts to communicate with models on user's account.
|
||||
module Accounts
|
||||
def self.load_accounts
|
||||
require_relative '../ext/sqlite3'
|
||||
|
||||
db_path = SPECKLE_ACCOUNTS_DB_PATH
|
||||
unless File.exist?(db_path)
|
||||
raise(
|
||||
@@ -18,8 +17,8 @@ module SpeckleConnector
|
||||
)
|
||||
end
|
||||
|
||||
db = SQLite3::Database.new(db_path)
|
||||
rows = db.execute('SELECT * FROM objects')
|
||||
db = Sqlite3::Database.new(db_path)
|
||||
rows = db.exec('SELECT * FROM objects')
|
||||
db.close
|
||||
rows.map { |row| JSON.parse(row[1]) }
|
||||
end
|
||||
|
||||
@@ -4,7 +4,7 @@ require_relative 'action'
|
||||
require_relative '../accounts/accounts'
|
||||
require_relative '../actions/save_stream'
|
||||
require_relative '../actions/queue_send'
|
||||
require_relative '../convertors/converter_sketchup'
|
||||
require_relative '../convertors/converter'
|
||||
|
||||
module SpeckleConnector
|
||||
module Actions
|
||||
@@ -25,7 +25,8 @@ module SpeckleConnector
|
||||
puts 'No local account found. Please refer to speckle.guide for more information.'
|
||||
return state
|
||||
end
|
||||
path = Sketchup.active_model.path
|
||||
sketchup_model = state.sketchup_state.sketchup_model
|
||||
path = sketchup_model.path
|
||||
if @stream_name.nil?
|
||||
@stream_name = path ? File.basename(path, '.*') : 'Untitled SketchUp Model'
|
||||
end
|
||||
@@ -34,22 +35,20 @@ module SpeckleConnector
|
||||
request = Sketchup::Http::Request.new("#{acct['serverInfo']['url']}/graphql", Sketchup::Http::POST)
|
||||
request.headers = { 'Authorization' => "Bearer #{acct['token']}", 'Content-Type' => 'application/json' }
|
||||
request.body = { query: query, variables: vars }.to_json
|
||||
to_convert = if Sketchup.active_model.selection.count > 0
|
||||
Sketchup.active_model.selection
|
||||
to_convert = if sketchup_model.selection.count > 0
|
||||
sketchup_model.selection
|
||||
else
|
||||
Sketchup.active_model.entities
|
||||
sketchup_model.entities
|
||||
end
|
||||
state = evaluate_request(request, state, to_convert)
|
||||
state = evaluate_request(sketchup_model, request, state, to_convert)
|
||||
Actions::LoadSavedStreams.update_state(state, {})
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
|
||||
private
|
||||
|
||||
def evaluate_request(request, state, to_convert)
|
||||
su_unit = Sketchup.active_model.options['UnitsOptions']['LengthUnit']
|
||||
unit = Converters::SKETCHUP_UNITS[su_unit]
|
||||
converter = Converters::ConverterSketchup.new(unit)
|
||||
def evaluate_request(sketchup_model, request, state, to_convert)
|
||||
converter = Converters::Converter.new(sketchup_model)
|
||||
|
||||
request.start do |_req, res|
|
||||
res_data = JSON.parse(res.body)['data']
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
require_relative 'action'
|
||||
require_relative '../states/state'
|
||||
require_relative '../states/speckle_state'
|
||||
require_relative '../states/sketchup_state'
|
||||
require_relative '../accounts/accounts'
|
||||
|
||||
module SpeckleConnector
|
||||
@@ -12,11 +13,11 @@ module SpeckleConnector
|
||||
# @param state [States::State] the current state of the {App::SpeckleConnectorApp}
|
||||
# @return [States::State] the new updated state object
|
||||
def self.update_state(state)
|
||||
# FIXME: below is how supposed to be
|
||||
# accounts = Accounts.load_accounts.to_json
|
||||
accounts = {}
|
||||
accounts = SpeckleConnector::Accounts.load_accounts.to_json
|
||||
speckle_state = States::SpeckleState.new(accounts, {}, {})
|
||||
States::State.new(state.user_state, speckle_state, false)
|
||||
# This should be the only point that `Sketchup_active_model` passed to application state.
|
||||
sketchup_state = States::SketchupState.new(Sketchup.active_model)
|
||||
States::State.new(state.user_state, speckle_state, sketchup_state, false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,7 +9,7 @@ module SpeckleConnector
|
||||
# @param state [States::State] the current state of the {App::SpeckleConnectorApp}
|
||||
# @return [States::State] the new updated state object
|
||||
def self.update_state(state, _data)
|
||||
(saved_streams = Sketchup.active_model.attribute_dictionary('speckle', true)['streams']) or []
|
||||
(saved_streams = state.sketchup_state.sketchup_model.attribute_dictionary('speckle', true)['streams']) or []
|
||||
state.with_add_queue('setSavedStreams', saved_streams, [])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,6 +4,7 @@ require_relative 'action'
|
||||
require_relative '../accounts/accounts'
|
||||
require_relative '../actions/create_stream'
|
||||
require_relative '../actions/queue_send'
|
||||
require_relative '../convertors/to_speckle'
|
||||
|
||||
module SpeckleConnector
|
||||
module Actions
|
||||
@@ -18,13 +19,13 @@ module SpeckleConnector
|
||||
puts 'No local account found. Please refer to speckle.guide for more information.'
|
||||
return state
|
||||
end
|
||||
su_model = Sketchup.active_model
|
||||
to_convert = su_model.selection.count > 0 ? su_model.selection : su_model.entities
|
||||
first_saved_stream = first_saved_stream(su_model)
|
||||
sketchup_model = state.sketchup_state.sketchup_model
|
||||
to_convert = sketchup_model.selection.count > 0 ? sketchup_model.selection : sketchup_model.entities
|
||||
first_saved_stream = first_saved_stream(sketchup_model)
|
||||
action = if first_saved_stream.nil?
|
||||
Actions::CreateStream.new
|
||||
else
|
||||
Actions::QueueSend.new(first_saved_stream, convert_to_speckle(to_convert))
|
||||
Actions::QueueSend.new(first_saved_stream, convert_to_speckle(sketchup_model, to_convert))
|
||||
end
|
||||
|
||||
action.update_state(state)
|
||||
@@ -35,11 +36,9 @@ module SpeckleConnector
|
||||
saved_streams.nil? || saved_streams.empty? ? nil : saved_streams[0]
|
||||
end
|
||||
|
||||
def self.convert_to_speckle(to_convert)
|
||||
su_unit = Sketchup.active_model.options['UnitsOptions']['LengthUnit']
|
||||
unit = Converters::SKETCHUP_UNITS[su_unit]
|
||||
converter = Converters::ConverterSketchup.new(unit)
|
||||
to_convert.map { |entity| converter.convert_to_speckle(entity) }
|
||||
def self.convert_to_speckle(sketchup_model, to_convert)
|
||||
converter = Converters::ToSpeckle.new(sketchup_model)
|
||||
to_convert.map { |entity| converter.convert(entity) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
require_relative 'action'
|
||||
require_relative '../convertors/units'
|
||||
require_relative '../convertors/converter_sketchup'
|
||||
require_relative '../convertors/to_native'
|
||||
|
||||
module SpeckleConnector
|
||||
module Actions
|
||||
@@ -17,10 +17,12 @@ module SpeckleConnector
|
||||
# @param state [States::State] the current state of the {App::SpeckleConnectorApp}
|
||||
# @return [States::State] the new updated state object
|
||||
def update_state(state)
|
||||
su_unit = Sketchup.active_model.options['UnitsOptions']['LengthUnit']
|
||||
unit = Converters::SKETCHUP_UNITS[su_unit]
|
||||
converter = Converters::ConverterSketchup.new(unit)
|
||||
converter = Converters::ToNative.new(state.sketchup_state.sketchup_model)
|
||||
# Have side effects on the sketchup model. It effects directly on the entities by adding new objects.
|
||||
start_time = Time.now.to_f
|
||||
converter.traverse_commit_object(@base)
|
||||
elapsed_time = (Time.now.to_f - start_time).round(3)
|
||||
puts "==== Converting to Native executed in #{elapsed_time} sec ===="
|
||||
state.with_add_queue('finishedReceiveInSketchup', @stream_id, [])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,7 +10,7 @@ module SpeckleConnector
|
||||
class ReloadAccounts < Action
|
||||
# @param state [States::State] the current state of the {App::SpeckleConnectorApp}
|
||||
# @return [States::State] the new updated state object
|
||||
def self.update_state(state)
|
||||
def self.update_state(state, _data)
|
||||
puts 'Reload of Speckle accounts requested by plugin'
|
||||
accounts_data = Accounts.load_accounts.to_json
|
||||
state.with_add_queue('loadAccounts', accounts_data, [])
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
require_relative 'action'
|
||||
require_relative '../accounts/accounts'
|
||||
require_relative '../convertors/units'
|
||||
require_relative '../convertors/converter_sketchup'
|
||||
require_relative '../convertors/converter'
|
||||
|
||||
module SpeckleConnector
|
||||
module Actions
|
||||
@@ -18,7 +18,7 @@ module SpeckleConnector
|
||||
# @param state [States::State] the current state of the {App::SpeckleConnectorApp}
|
||||
# @return [States::State] the new updated state object
|
||||
def update_state(state)
|
||||
speckle_dict = Sketchup.active_model.attribute_dictionary('speckle', true)
|
||||
speckle_dict = state.sketchup_state.sketchup_model.attribute_dictionary('speckle', true)
|
||||
saved = speckle_dict['streams'] || []
|
||||
saved -= [@stream_id]
|
||||
speckle_dict['streams'] = saved
|
||||
|
||||
@@ -16,7 +16,7 @@ module SpeckleConnector
|
||||
# @param state [States::State] the current state of the {App::SpeckleConnectorApp}
|
||||
# @return [States::State] the new updated state object
|
||||
def update_state(state)
|
||||
speckle_dict = Sketchup.active_model.attribute_dictionary('speckle', true)
|
||||
speckle_dict = state.sketchup_state.sketchup_model.attribute_dictionary('speckle', true)
|
||||
saved = speckle_dict['streams'] || []
|
||||
saved = saved.empty? ? [@stream_id] : saved.unshift(@stream_id)
|
||||
speckle_dict['streams'] = saved
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
require_relative 'action'
|
||||
require_relative '../convertors/units'
|
||||
require_relative '../convertors/converter_sketchup'
|
||||
require_relative '../convertors/to_speckle'
|
||||
|
||||
module SpeckleConnector
|
||||
module Actions
|
||||
@@ -16,10 +16,9 @@ module SpeckleConnector
|
||||
# @param state [States::State] the current state of the {App::SpeckleConnectorApp}
|
||||
# @return [States::State] the new updated state object
|
||||
def update_state(state)
|
||||
su_unit = Sketchup.active_model.options['UnitsOptions']['LengthUnit']
|
||||
unit = Converters::SKETCHUP_UNITS[su_unit]
|
||||
converter = Converters::ConverterSketchup.new(unit)
|
||||
converted = Sketchup.active_model.selection.map { |entity| converter.convert_to_speckle(entity) }
|
||||
sketchup_model = state.sketchup_state.sketchup_model
|
||||
converter = Converters::ToSpeckle.new(sketchup_model)
|
||||
converted = converter.convert_selection
|
||||
|
||||
puts("converted #{converted.count} objects for stream #{@stream_id}")
|
||||
state.with_add_queue('convertedFromSketchup', @stream_id, [converted.to_json])
|
||||
|
||||
@@ -13,5 +13,6 @@ module SpeckleConnector
|
||||
else
|
||||
raise "Unsupported OS: #{host_os.inspect}"
|
||||
end
|
||||
RUBY_VERSION_NUMBER = RUBY_VERSION.split('.')[0..1].join.to_i
|
||||
end
|
||||
# rubocop:enable Style/Documentation
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
# rubocop:disable Metrics/PerceivedComplexity
|
||||
|
||||
module SpeckleConnector
|
||||
module Converters
|
||||
# CleanUp is a plugin developed by [Thomas Thomassen](https://github.com/thomthom).
|
||||
module CleanUp
|
||||
# Removes coplanar entities from the given entities.
|
||||
# @param entities [Sketchup::Entities] entities to remove edges between that make entities coplanar.
|
||||
# @note Merging coplanar faces idea originated from [CleanUp](https://github.com/thomthom/cleanup) plugin
|
||||
# which is developed by [Thomas Thomassen](https://github.com/thomthom).
|
||||
def merge_coplanar_faces(entities)
|
||||
edges = []
|
||||
faces = entities.collect { |entity| entity if entity.is_a? Sketchup::Face }.compact
|
||||
faces.each { |face| face.edges.each { |edge| edges << edge } }
|
||||
edges.compact!
|
||||
edges.each { |edge| remove_edge_have_coplanar_faces(edge, false) }
|
||||
end
|
||||
|
||||
# Detect edges to remove by checking following controls respectively;
|
||||
# - Upcoming Sketchup entity is Sketchup::Edge or not.
|
||||
# - Whether edge has 2 face or not.
|
||||
# - Whether faces are duplicated or not.
|
||||
# - Whether edges safe to merge or not.
|
||||
# - Whether faces have same material or not.
|
||||
# - Whether UV texture map is aligned between faces or not.
|
||||
# - Finally, if faces are coplanar by correcting these checks, then removes edge from Sketchup.active_model.
|
||||
# @param edge [Sketchup::Edge] edge to check.
|
||||
# @param ignore_materials [Boolean] whether ignore materials or not.
|
||||
# Returns true if the given edge separating two coplanar faces.
|
||||
# Return false otherwise.
|
||||
def remove_edge_have_coplanar_faces(edge, ignore_materials)
|
||||
return false unless edge.valid? && edge.is_a?(Sketchup::Edge)
|
||||
return false unless edge.faces.size == 2
|
||||
|
||||
face_1, face_2 = edge.faces
|
||||
|
||||
return false if face_duplicate?(face_1, face_2)
|
||||
# Check for troublesome faces which might lead to missing geometry if merged.
|
||||
return false unless edge_safe_to_merge?(edge)
|
||||
|
||||
# Check materials match.
|
||||
unless ignore_materials
|
||||
return false unless (face_1.material == face_2.material) && (face_1.back_material == face_2.back_material)
|
||||
# Verify UV mapping match.
|
||||
if (!face_1.material.nil? || face_1.material.texture.nil?) && !continuous_uv?(face_1, face_2, edge)
|
||||
return false
|
||||
end
|
||||
end
|
||||
# Check faces are coplanar or not.
|
||||
return false unless faces_coplanar?(face_1, face_2)
|
||||
|
||||
edge.erase!
|
||||
true
|
||||
end
|
||||
|
||||
# Determines if two faces are overlapped.
|
||||
def face_duplicate?(face_1, face_2, overlapping: false)
|
||||
return false if face_1 == face_2
|
||||
|
||||
v_1 = face_1.outer_loop.vertices
|
||||
v_2 = face_2.outer_loop.vertices
|
||||
return true if (v_1 - v_2).empty? && (v_2 - v_1).empty?
|
||||
|
||||
if overlapping && (v_2 - v_1).empty?
|
||||
edges = (face_2.outer_loop.edges - face_1.outer_loop.edges)
|
||||
unless edges.empty?
|
||||
point = edges[0].start.position.offset(edges[0].line[1], 0.01)
|
||||
return true if face_1.classify_point(point) <= 4
|
||||
end
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
# Checks the given edge for potential problems if the connected faces would
|
||||
# be merged.
|
||||
def edge_safe_to_merge?(edge)
|
||||
edge.faces.all? { |face| face_safe_to_merge?(face) }
|
||||
end
|
||||
|
||||
# Returns true if the two faces connected by the edge has continuous UV mapping.
|
||||
# UV's are normalized to 0.0..1.0 before comparison.
|
||||
def continuous_uv?(face_1, face_2, edge)
|
||||
tw = Sketchup.create_texture_writer
|
||||
uvh_1 = face_1.get_UVHelper(true, true, tw)
|
||||
uvh_2 = face_2.get_UVHelper(true, true, tw)
|
||||
p_1 = edge.start.position
|
||||
p_2 = edge.end.position
|
||||
uv_equal?(uvh_1.get_front_UVQ(p_1), uvh_2.get_front_UVQ(p_1)) &&
|
||||
uv_equal?(uvh_1.get_front_UVQ(p_2), uvh_2.get_front_UVQ(p_2)) &&
|
||||
uv_equal?(uvh_1.get_back_UVQ(p_1), uvh_2.get_back_UVQ(p_1)) &&
|
||||
uv_equal?(uvh_1.get_back_UVQ(p_2), uvh_2.get_back_UVQ(p_2))
|
||||
end
|
||||
|
||||
# Normalize UV's to 0.0..1.0 and compare them.
|
||||
def uv_equal?(uvq_1, uvq_2)
|
||||
uv_1 = uvq_1.to_a.map { |n| n % 1 }
|
||||
uv_2 = uvq_2.to_a.map { |n| n % 1 }
|
||||
uv_1 == uv_2
|
||||
end
|
||||
|
||||
# Validates that the given face can be merged with other faces without causing
|
||||
# problems.
|
||||
def face_safe_to_merge?(face)
|
||||
stack = face.outer_loop.edges
|
||||
edge = stack.shift
|
||||
direction = edge.line[1]
|
||||
until stack.empty?
|
||||
edge = stack.shift
|
||||
return true unless edge.line[1].parallel?(direction)
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
# Determines if two faces are coplanar.
|
||||
def faces_coplanar?(face_1, face_2)
|
||||
vertices = face_1.vertices + face_2.vertices
|
||||
plane = Geom.fit_plane_to_points(vertices)
|
||||
vertices.all? { |v| v.position.on_plane?(plane) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:enable Metrics/CyclomaticComplexity
|
||||
# rubocop:enable Metrics/PerceivedComplexity
|
||||
@@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module SpeckleConnector
|
||||
module Converters
|
||||
# Helper class to convert geometries between server and Sketchup.
|
||||
class Converter
|
||||
attr_accessor :units, :definitions, :registry, :entity_observer, :sketchup_model
|
||||
|
||||
def initialize(sketchup_model)
|
||||
@sketchup_model = sketchup_model
|
||||
su_unit = @sketchup_model.options['UnitsOptions']['LengthUnit']
|
||||
@units = Converters::SKETCHUP_UNITS[su_unit]
|
||||
@definitions = {}
|
||||
# @registry = Sketchup.active_model.attribute_dictionary("speckle_id_registry", true)
|
||||
# @entity_observer = SpeckleEntityObserver.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,33 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'sketchup'
|
||||
require_relative 'to_speckle'
|
||||
require_relative 'to_native'
|
||||
|
||||
module SpeckleConnector
|
||||
module Converters
|
||||
# Helper class to convert geometries between server and Sketchup.
|
||||
class ConverterSketchup
|
||||
include ToNative
|
||||
include ToSpeckle
|
||||
|
||||
attr_accessor :units, :component_defs, :registry, :entity_observer
|
||||
|
||||
def initialize(units = 'm')
|
||||
@units = units
|
||||
@component_defs = {}
|
||||
# @registry = Sketchup.active_model.attribute_dictionary("speckle_id_registry", true)
|
||||
# @entity_observer = SpeckleEntityObserver.new
|
||||
end
|
||||
|
||||
def convert_to_speckle(obj)
|
||||
return edge_to_speckle(obj) if obj.is_a?(Sketchup::Edge)
|
||||
return face_to_speckle(obj) if obj.is_a?(Sketchup::Face)
|
||||
return component_instance_to_speckle(obj, is_group: true) if obj.is_a?(Sketchup::Group)
|
||||
return component_definition_to_speckle(obj) if obj.is_a?(Sketchup::ComponentDefinition)
|
||||
|
||||
component_instance_to_speckle(obj) if obj.is_a?(Sketchup::ComponentInstance)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,22 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'sketchup'
|
||||
require_relative 'converter'
|
||||
require_relative '../speckle_objects/other/transform'
|
||||
require_relative '../speckle_objects/other/render_material'
|
||||
require_relative '../speckle_objects/geometry/point'
|
||||
require_relative '../speckle_objects/geometry/line'
|
||||
require_relative '../speckle_objects/geometry/mesh'
|
||||
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
# rubocop:disable Metrics/PerceivedComplexity
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
# rubocop:disable Metrics/ModuleLength
|
||||
# rubocop:disable SketchupSuggestions/AddGroup
|
||||
|
||||
# To Native conversions for the ConverterSketchup
|
||||
module SpeckleConnector
|
||||
module Converters
|
||||
# Converts speckle geometries to native SketchUp entities.
|
||||
module ToNative
|
||||
# Converts sketchup entities to speckle objects.
|
||||
class ToNative < Converter
|
||||
# Module aliases
|
||||
GEOMETRY = SpeckleObjects::Geometry
|
||||
OTHER = SpeckleObjects::Other
|
||||
|
||||
# Class aliases
|
||||
POINT = GEOMETRY::Point
|
||||
LINE = GEOMETRY::Line
|
||||
MESH = GEOMETRY::Mesh
|
||||
BLOCK_DEFINITION = OTHER::BlockDefinition
|
||||
BLOCK_INSTANCE = OTHER::BlockInstance
|
||||
|
||||
BASE_OBJECT_PROPS = %w[applicationId id speckle_type totalChildrenCount].freeze
|
||||
CONVERTABLE_SPECKLE_TYPES = %w[
|
||||
Objects.Geometry.Line
|
||||
Objects.Geometry.Polyline
|
||||
Objects.Geometry.Mesh
|
||||
Objects.Geometry.Brep
|
||||
Objects.Other.BlockInstance
|
||||
Objects.Other.BlockDefinition
|
||||
Objects.Other.RenderMaterial
|
||||
].freeze
|
||||
|
||||
def can_convert_to_native(obj)
|
||||
return false unless obj.is_a?(Hash) && obj.key?('speckle_type')
|
||||
|
||||
CONVERTABLE_SPECKLE_TYPES.include?(obj['speckle_type'])
|
||||
end
|
||||
|
||||
def ignored_speckle_type?(obj)
|
||||
['Objects.BuiltElements.Revit.Parameter'].include?(obj['speckle_type'])
|
||||
end
|
||||
|
||||
# Traversal method to create Sketchup objects from upcoming base object.
|
||||
# @param obj [Hash, Array] object might be source base object or it's sub objects, because this method is a
|
||||
# self-caller method means that call itself according to conditions inside of it.
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
# rubocop:disable Metrics/PerceivedComplexity
|
||||
def traverse_commit_object(obj)
|
||||
if can_convert_to_native(obj)
|
||||
convert_to_native(obj, Sketchup.active_model.entities)
|
||||
convert_to_native(obj)
|
||||
elsif obj.is_a?(Hash) && obj.key?('speckle_type')
|
||||
return if ignored_speckle_type?(obj)
|
||||
|
||||
@@ -34,359 +68,51 @@ module SpeckleConnector
|
||||
obj.each { |value| traverse_commit_object(value) }
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/CyclomaticComplexity
|
||||
# rubocop:enable Metrics/PerceivedComplexity
|
||||
|
||||
def can_convert_to_native(obj)
|
||||
return false unless obj.is_a?(Hash) && obj.key?('speckle_type')
|
||||
|
||||
[
|
||||
'Objects.Geometry.Line',
|
||||
'Objects.Geometry.Polyline',
|
||||
'Objects.Geometry.Mesh',
|
||||
'Objects.Geometry.Brep',
|
||||
'Objects.Other.BlockInstance',
|
||||
'Objects.Other.BlockDefinition',
|
||||
'Objects.Other.RenderMaterial'
|
||||
].include?(obj['speckle_type'])
|
||||
end
|
||||
|
||||
def ignored_speckle_type?(obj)
|
||||
['Objects.BuiltElements.Revit.Parameter'].include?(obj['speckle_type'])
|
||||
end
|
||||
|
||||
def convert_to_native(obj, entities = Sketchup.active_model.entities)
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
def convert_to_native(obj, entities = sketchup_model.entities)
|
||||
return display_value_to_native_component(obj, entities) unless obj['displayValue'].nil?
|
||||
|
||||
convert = method(:convert_to_native)
|
||||
case obj['speckle_type']
|
||||
when 'Objects.Geometry.Line', 'Objects.Geometry.Polyline' then edge_to_native(obj, entities)
|
||||
when 'Objects.Other.BlockInstance' then component_instance_to_native(obj, entities)
|
||||
when 'Objects.Other.BlockDefinition' then component_definition_to_native(obj, entities)
|
||||
when 'Objects.Geometry.Mesh' then mesh_to_native(obj, entities)
|
||||
when 'Objects.Geometry.Brep' then mesh_to_native(obj['displayValue'], entities)
|
||||
when 'Objects.Geometry.Line', 'Objects.Geometry.Polyline' then LINE.to_native(obj, entities)
|
||||
when 'Objects.Other.BlockInstance' then BLOCK_INSTANCE.to_native(sketchup_model, obj, entities, &convert)
|
||||
when 'Objects.Other.BlockDefinition' then BLOCK_DEFINITION.to_native(sketchup_model, obj, entities, &convert)
|
||||
when 'Objects.Geometry.Mesh' then MESH.to_native(sketchup_model, obj, entities)
|
||||
when 'Objects.Geometry.Brep' then MESH.to_native(sketchup_model, obj['displayValue'], entities)
|
||||
end
|
||||
rescue StandardError => e
|
||||
puts("Failed to convert #{obj['speckle_type']} (id: #{obj['id']})")
|
||||
puts(e)
|
||||
nil
|
||||
end
|
||||
|
||||
def length_to_native(length, units = @units)
|
||||
length.__send__(Converters::SKETCHUP_UNIT_STRINGS[units])
|
||||
end
|
||||
|
||||
def edge_to_native(line, entities)
|
||||
if line.key?('value')
|
||||
values = line['value']
|
||||
points = values.each_slice(3).to_a.map { |pt| point_to_native(pt[0], pt[1], pt[2], line['units']) }
|
||||
points.push(points[0]) if line['closed']
|
||||
entities.add_edges(*points)
|
||||
else
|
||||
start_pt = point_to_native(line['start']['x'], line['start']['y'], line['start']['z'], line['units'])
|
||||
end_pt = point_to_native(line['end']['x'], line['end']['y'], line['end']['z'], line['units'])
|
||||
entities.add_edges(start_pt, end_pt)
|
||||
end
|
||||
end
|
||||
|
||||
def edge_to_native_component(line, entities)
|
||||
line_id = line['applicationId'].to_s.empty? ? line['id'] : line['applicationId']
|
||||
definition = component_definition_to_native([line], "def::#{line_id}")
|
||||
find_and_erase_existing_instance(definition, line_id)
|
||||
instance = entities.add_instance(definition, Geom::Transformation.new)
|
||||
instance.name = line_id
|
||||
instance
|
||||
end
|
||||
|
||||
def face_to_native
|
||||
nil
|
||||
end
|
||||
|
||||
def point_to_native(x, y, z, units)
|
||||
Geom::Point3d.new(length_to_native(x, units), length_to_native(y, units), length_to_native(z, units))
|
||||
end
|
||||
|
||||
def point_to_native_array(x, y, z, units)
|
||||
[length_to_native(x, units), length_to_native(y, units), length_to_native(z, units)]
|
||||
end
|
||||
|
||||
# converts a mesh to a native mesh and adds the faces to the given entities collection
|
||||
def mesh_to_native(mesh, entities)
|
||||
_speckle_mesh_to_native_mesh(mesh, entities)
|
||||
end
|
||||
|
||||
def _speckle_mesh_to_native_mesh(mesh, entities)
|
||||
native_mesh = Geom::PolygonMesh.new(mesh['vertices'].count / 3)
|
||||
points = []
|
||||
mesh['vertices'].each_slice(3) do |pt|
|
||||
points.push(point_to_native(pt[0], pt[1], pt[2], mesh['units']))
|
||||
end
|
||||
faces = mesh['faces']
|
||||
while faces.count > 0
|
||||
num_pts = faces.shift
|
||||
# 0 -> 3, 1 -> 4 to preserve backwards compatibility
|
||||
num_pts += 3 if num_pts < 3
|
||||
indices = faces.shift(num_pts)
|
||||
native_mesh.add_polygon(indices.map { |index| points[index] })
|
||||
end
|
||||
entities.add_faces_from_mesh(native_mesh, 4, material_to_native(mesh['renderMaterial']))
|
||||
merge_coplanar_faces(entities)
|
||||
native_mesh
|
||||
end
|
||||
|
||||
# Removes coplanar entities from the given entities.
|
||||
# @param entities [Sketchup::Entities] entities to remove edges between that make entities coplanar.
|
||||
# @note Merging coplanar faces idea originated from [CleanUp](https://github.com/thomthom/cleanup) plugin
|
||||
# which is developed by [Thomas Thomassen](https://github.com/thomthom).
|
||||
def merge_coplanar_faces(entities)
|
||||
edges = []
|
||||
faces = entities.collect { |entity| entity if entity.is_a? Sketchup::Face }.compact
|
||||
faces.each { |face| face.edges.each { |edge| edges << edge } }
|
||||
edges.compact!
|
||||
edges.each { |edge| remove_edge_have_coplanar_faces(edge, false) }
|
||||
end
|
||||
|
||||
# Detect edges to remove by checking following controls respectively;
|
||||
# - Upcoming Sketchup entity is Sketchup::Edge or not.
|
||||
# - Whether edge has 2 face or not.
|
||||
# - Whether faces are duplicated or not.
|
||||
# - Whether edges safe to merge or not.
|
||||
# - Whether faces have same material or not.
|
||||
# - Whether UV texture map is aligned between faces or not.
|
||||
# - Finally, if faces are coplanar by correcting these checks, then removes edge from Sketchup.active_model.
|
||||
# @param edge [Sketchup::Edge] edge to check.
|
||||
# @param ignore_materials [Boolean] whether ignore materials or not.
|
||||
# Returns true if the given edge separating two coplanar faces.
|
||||
# Return false otherwise.
|
||||
def remove_edge_have_coplanar_faces(edge, ignore_materials)
|
||||
return false unless edge.valid? && edge.is_a?(Sketchup::Edge)
|
||||
return false unless edge.faces.size == 2
|
||||
|
||||
face_1, face_2 = edge.faces
|
||||
|
||||
return false if face_duplicate?(face_1, face_2)
|
||||
# Check for troublesome faces which might lead to missing geometry if merged.
|
||||
return false unless edge_safe_to_merge?(edge)
|
||||
|
||||
# Check materials match.
|
||||
unless ignore_materials
|
||||
return false unless (face_1.material == face_2.material) && (face_1.back_material == face_2.back_material)
|
||||
# Verify UV mapping match.
|
||||
if (!face_1.material.nil? || face_1.material.texture.nil?) && !continuous_uv?(face_1, face_2, edge)
|
||||
return false
|
||||
end
|
||||
end
|
||||
# Check faces are coplanar or not.
|
||||
return false unless faces_coplanar?(face_1, face_2)
|
||||
|
||||
edge.erase!
|
||||
true
|
||||
end
|
||||
|
||||
# Determines if two faces are overlapped.
|
||||
def face_duplicate?(face_1, face_2, overlapping: false)
|
||||
return false if face_1 == face_2
|
||||
|
||||
v_1 = face_1.outer_loop.vertices
|
||||
v_2 = face_2.outer_loop.vertices
|
||||
return true if (v_1 - v_2).empty? && (v_2 - v_1).empty?
|
||||
|
||||
if overlapping && (v_2 - v_1).empty?
|
||||
edges = (face_2.outer_loop.edges - face_1.outer_loop.edges)
|
||||
unless edges.empty?
|
||||
point = edges[0].start.position.offset(edges[0].line[1], 0.01)
|
||||
return true if face_1.classify_point(point) <= 4
|
||||
end
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
# Checks the given edge for potential problems if the connected faces would
|
||||
# be merged.
|
||||
def edge_safe_to_merge?(edge)
|
||||
edge.faces.all? { |face| face_safe_to_merge?(face) }
|
||||
end
|
||||
|
||||
# Returns true if the two faces connected by the edge has continuous UV mapping.
|
||||
# UV's are normalized to 0.0..1.0 before comparison.
|
||||
def continuous_uv?(face_1, face_2, edge)
|
||||
tw = Sketchup.create_texture_writer
|
||||
uvh_1 = face_1.get_UVHelper(true, true, tw)
|
||||
uvh_2 = face_2.get_UVHelper(true, true, tw)
|
||||
p_1 = edge.start.position
|
||||
p_2 = edge.end.position
|
||||
uv_equal?(uvh_1.get_front_UVQ(p_1), uvh_2.get_front_UVQ(p_1)) &&
|
||||
uv_equal?(uvh_1.get_front_UVQ(p_2), uvh_2.get_front_UVQ(p_2)) &&
|
||||
uv_equal?(uvh_1.get_back_UVQ(p_1), uvh_2.get_back_UVQ(p_1)) &&
|
||||
uv_equal?(uvh_1.get_back_UVQ(p_2), uvh_2.get_back_UVQ(p_2))
|
||||
end
|
||||
|
||||
# Normalize UV's to 0.0..1.0 and compare them.
|
||||
def uv_equal?(uvq_1, uvq_2)
|
||||
uv_1 = uvq_1.to_a.map { |n| n % 1 }
|
||||
uv_2 = uvq_2.to_a.map { |n| n % 1 }
|
||||
uv_1 == uv_2
|
||||
end
|
||||
|
||||
# Validates that the given face can be merged with other faces without causing
|
||||
# problems.
|
||||
def face_safe_to_merge?(face)
|
||||
stack = face.outer_loop.edges
|
||||
edge = stack.shift
|
||||
direction = edge.line[1]
|
||||
until stack.empty?
|
||||
edge = stack.shift
|
||||
return true unless edge.line[1].parallel?(direction)
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
# Determines if two faces are coplanar.
|
||||
def faces_coplanar?(face_1, face_2)
|
||||
vertices = face_1.vertices + face_2.vertices
|
||||
plane = Geom.fit_plane_to_points(vertices)
|
||||
vertices.all? { |v| v.position.on_plane?(plane) }
|
||||
end
|
||||
|
||||
def _hidden_edges_mesh_to_native_mesh(mesh, entities)
|
||||
native_mesh = Geom::PolygonMesh.new(mesh['vertices'].count / 3)
|
||||
points = []
|
||||
mesh['vertices'].each_slice(3) do |pt|
|
||||
points.push(point_to_native(pt[0], pt[1], pt[2], mesh['units']))
|
||||
end
|
||||
edge_flags = mesh['faceEdgeFlags']
|
||||
faces = mesh['faces']
|
||||
loops = []
|
||||
flags = []
|
||||
while faces.count > 0
|
||||
num_pts = faces.shift
|
||||
# 0 -> 3, 1 -> 4 to preserve backwards compatibility
|
||||
num_pts += 3 if num_pts < 3
|
||||
indices = faces.shift(num_pts)
|
||||
current_edge_flags = edge_flags.shift(num_pts)
|
||||
outer_loop = indices.map { |index| points[index] }
|
||||
if current_edge_flags.include?(true)
|
||||
loops << outer_loop
|
||||
flags << current_edge_flags
|
||||
else
|
||||
native_mesh.add_polygon(outer_loop)
|
||||
end
|
||||
end
|
||||
entities.add_faces_from_mesh(native_mesh, 0, material_to_native(mesh['renderMaterial']))
|
||||
|
||||
loops.each do |l|
|
||||
loop_flags = flags.shift
|
||||
face = entities.add_face(l)
|
||||
face.edges.each_with_index { |edge, index| edge.soft = edge.smooth = loop_flags[index] }
|
||||
end
|
||||
|
||||
native_mesh
|
||||
end
|
||||
# rubocop:enable Metrics/CyclomaticComplexity
|
||||
|
||||
# creates a component definition and instance from a speckle object with a display value
|
||||
def display_value_to_native_component(obj, entities)
|
||||
obj_id = obj['applicationId'].to_s.empty? ? obj['id'] : obj['applicationId']
|
||||
definition = component_definition_to_native(obj['displayValue'], "def::#{obj_id}")
|
||||
definition = BLOCK_DEFINITION.to_native(
|
||||
sketchup_model,
|
||||
obj['displayValue'],
|
||||
"def::#{obj_id}",
|
||||
&method(:convert_to_native)
|
||||
)
|
||||
|
||||
find_and_erase_existing_instance(definition, obj_id)
|
||||
transform = obj['transform'].nil? ? Geom::Transformation.new : transform_to_native(obj['transform'])
|
||||
t_arr = obj['transform']
|
||||
transform = t_arr.nil? ? Geom::Transformation.new : OTHER::Transform.to_native(t_arr, units)
|
||||
instance = entities.add_instance(definition, transform)
|
||||
instance.name = obj_id
|
||||
instance
|
||||
end
|
||||
|
||||
# finds or creates a component definition from the geometry and the given name
|
||||
def component_definition_to_native(geometry, name, application_id = '')
|
||||
definition = Sketchup.active_model.definitions[name]
|
||||
return definition if definition && (definition.name == name || definition.guid == application_id)
|
||||
|
||||
definition&.entities&.clear!
|
||||
definition ||= Sketchup.active_model.definitions.add(name)
|
||||
geometry.each { |obj| convert_to_native(obj, definition.entities) }
|
||||
puts("definition finished: #{name} (#{application_id})")
|
||||
# puts(" entity count: #{definition.entities.count}")
|
||||
definition
|
||||
end
|
||||
|
||||
# takes a component definition and finds and erases the first instance with the matching name
|
||||
# (and optionally the applicationId)
|
||||
def find_and_erase_existing_instance(definition, name, app_id = '')
|
||||
definition.instances.find { |ins| ins.name == name || ins.guid == app_id }&.erase!
|
||||
end
|
||||
|
||||
# creates a component instance from a block
|
||||
def component_instance_to_native(block, entities)
|
||||
# is_group = block.key?("is_sketchup_group") && block["is_sketchup_group"]
|
||||
# something about this conversion is freaking out if nested block geo is a group
|
||||
# so this is set to false always until I can figure this out
|
||||
is_group = false
|
||||
|
||||
definition = component_definition_to_native(
|
||||
block['blockDefinition']['geometry'],
|
||||
block['blockDefinition']['name'],
|
||||
block['blockDefinition']['applicationId']
|
||||
)
|
||||
name = block['name'].nil? || block['name'].empty? ? block['id'] : block['name']
|
||||
transform = transform_to_native(
|
||||
block['transform'].is_a?(Hash) ? block['transform']['value'] : block['transform'],
|
||||
block['units']
|
||||
)
|
||||
instance =
|
||||
if is_group
|
||||
entities.add_group(definition.entities.to_a)
|
||||
else
|
||||
entities.add_instance(definition, transform)
|
||||
end
|
||||
# erase existing instances after creation and before rename because you can't have definitions without instances
|
||||
find_and_erase_existing_instance(definition, name, block['applicationId'])
|
||||
puts("Failed to create instance for speckle block instance #{block['id']}") if instance.nil?
|
||||
instance.transformation = transform if is_group
|
||||
instance.material = material_to_native(block['renderMaterial'])
|
||||
instance.name = name
|
||||
instance
|
||||
end
|
||||
|
||||
def transform_to_native(t_arr, units = @units)
|
||||
Geom::Transformation.new(
|
||||
[
|
||||
t_arr[0],
|
||||
t_arr[4],
|
||||
t_arr[8],
|
||||
t_arr[12],
|
||||
t_arr[1],
|
||||
t_arr[5],
|
||||
t_arr[9],
|
||||
t_arr[13],
|
||||
t_arr[2],
|
||||
t_arr[6],
|
||||
t_arr[10],
|
||||
t_arr[14],
|
||||
length_to_native(t_arr[3], units),
|
||||
length_to_native(t_arr[7], units),
|
||||
length_to_native(t_arr[11], units),
|
||||
t_arr[15]
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
def material_to_native(render_mat)
|
||||
return if render_mat.nil?
|
||||
|
||||
# return material with same name if it exists
|
||||
name = render_mat['name'] || render_mat['id']
|
||||
material = Sketchup.active_model.materials[name]
|
||||
return material if material
|
||||
|
||||
# create a new sketchup material
|
||||
material = Sketchup.active_model.materials.add(name)
|
||||
material.alpha = render_mat['opacity']
|
||||
argb = render_mat['diffuse']
|
||||
material.color = Sketchup::Color.new((argb >> 16) & 255, (argb >> 8) & 255, argb & 255, (argb >> 24) & 255)
|
||||
material
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
# rubocop:enable Metrics/PerceivedComplexity
|
||||
# rubocop:enable Metrics/CyclomaticComplexity
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
# rubocop:enable Metrics/ModuleLength
|
||||
# rubocop:enable SketchupSuggestions/AddGroup
|
||||
|
||||
@@ -1,310 +1,29 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'sketchup'
|
||||
require_relative 'units'
|
||||
require_relative 'converter'
|
||||
require_relative '../speckle_objects/geometry/line'
|
||||
require_relative '../speckle_objects/geometry/mesh'
|
||||
require_relative '../speckle_objects/other/block_instance'
|
||||
require_relative '../speckle_objects/other/block_definition'
|
||||
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
# rubocop:disable Metrics/PerceivedComplexity
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
# rubocop:disable Metrics/ModuleLength
|
||||
|
||||
# To Speckle conversions for the ConverterSketchup
|
||||
module SpeckleConnector
|
||||
# Convertors to convert geometry one end to another between Speckle server and Sketchup.
|
||||
module Converters
|
||||
# Convert geometries to speckle geometry.
|
||||
module ToSpeckle
|
||||
def length_to_speckle(length)
|
||||
length.__send__("to_#{SKETCHUP_UNIT_STRINGS[@units]}")
|
||||
# Converts sketchup entities to speckle objects.
|
||||
class ToSpeckle < Converter
|
||||
def convert_selection
|
||||
sketchup_model.selection.map { |entity| convert(entity) }
|
||||
end
|
||||
|
||||
# convert an edge to a speckle line
|
||||
def edge_to_speckle(edge)
|
||||
{
|
||||
speckle_type: 'Objects.Geometry.Line',
|
||||
applicationId: edge.persistent_id.to_s,
|
||||
units: @units,
|
||||
start: vertex_to_speckle(edge.start),
|
||||
end: vertex_to_speckle(edge.end),
|
||||
domain: speckle_interval(0, Float(edge.length)),
|
||||
bbox: bounds_to_speckle(edge.bounds)
|
||||
}
|
||||
end
|
||||
|
||||
# Convert a component definition to a speckle block definition
|
||||
def component_definition_to_speckle(definition)
|
||||
guid = definition.guid
|
||||
return @component_defs[guid] if @component_defs.key?(guid)
|
||||
|
||||
speckle_def = {
|
||||
speckle_type: 'Objects.Other.BlockDefinition',
|
||||
applicationId: guid,
|
||||
units: @units,
|
||||
name: definition.name,
|
||||
# i think the base point is always the origin?
|
||||
basePoint: speckle_point,
|
||||
'@geometry' => if definition.entities[0].is_a?(Sketchup::Edge) | definition.entities[0].is_a?(Sketchup::Face)
|
||||
group_mesh_to_speckle(definition)
|
||||
else
|
||||
definition.entities.map { |entity| convert_to_speckle(entity) }
|
||||
end
|
||||
}
|
||||
@component_defs[guid] = speckle_def
|
||||
end
|
||||
|
||||
# convert a component instance to a speckle block instance
|
||||
def component_instance_to_speckle(instance, is_group: false)
|
||||
transform = instance.transformation
|
||||
{
|
||||
speckle_type: 'Objects.Other.BlockInstance',
|
||||
applicationId: instance.guid,
|
||||
is_sketchup_group: is_group,
|
||||
units: @units,
|
||||
bbox: bounds_to_speckle(instance.bounds),
|
||||
name: instance.name == '' ? nil : instance.name,
|
||||
renderMaterial: instance.material.nil? ? nil : material_to_speckle(instance.material),
|
||||
transform: transform_to_speckle(transform),
|
||||
'@blockDefinition' => component_definition_to_speckle(instance.definition)
|
||||
}
|
||||
end
|
||||
|
||||
def group_mesh_to_speckle(component_def)
|
||||
mat_groups = {}
|
||||
nested_blocks = []
|
||||
lines = []
|
||||
|
||||
component_def.entities.each do |entity|
|
||||
nested_blocks.push(component_instance_to_speckle(entity)) if entity.is_a?(Sketchup::ComponentInstance)
|
||||
next unless entity.is_a?(Sketchup::Face)
|
||||
|
||||
face = entity
|
||||
# convert material
|
||||
mat_id = face.material.nil? ? 'none' : face.material.entityID
|
||||
mat_groups[mat_id] = initialise_group_mesh(face, component_def.bounds) unless mat_groups.key?(mat_id)
|
||||
|
||||
if face.loops.size > 1
|
||||
mesh = face.mesh
|
||||
mat_groups[mat_id]['@(31250)vertices'].push(*mesh_points_to_array(mesh))
|
||||
mat_groups[mat_id]['@(62500)faces'].push(*mesh_faces_to_array(mesh, mat_groups[mat_id][:pt_count] - 1))
|
||||
mat_groups[mat_id]['@(31250)faceEdgeFlags'].push(*mesh_edge_flags_to_array(mesh))
|
||||
else
|
||||
mat_groups[mat_id]['@(31250)vertices'].push(*face_vertices_to_array(face))
|
||||
mat_groups[mat_id]['@(62500)faces'].push(*face_indices_to_array(face, mat_groups[mat_id][:pt_count]))
|
||||
mat_groups[mat_id]['@(31250)faceEdgeFlags'].push(*face_edge_flags_to_array(face))
|
||||
end
|
||||
mat_groups[mat_id][:pt_count] += face.vertices.count
|
||||
def convert(obj)
|
||||
return SpeckleObjects::Geometry::Line.from_edge(obj, @units).to_h if obj.is_a?(Sketchup::Edge)
|
||||
return SpeckleObjects::Geometry::Mesh.from_face(obj, @units) if obj.is_a?(Sketchup::Face)
|
||||
return SpeckleObjects::Other::BlockInstance.from_group(obj, @units, @definitions) if obj.is_a?(Sketchup::Group)
|
||||
if obj.is_a?(Sketchup::ComponentInstance)
|
||||
return SpeckleObjects::Other::BlockInstance.from_component_instance(obj, @units, @definitions)
|
||||
end
|
||||
|
||||
mat_groups.values.map { |group| group.delete(:pt_count) }
|
||||
mat_groups.values + lines + nested_blocks
|
||||
end
|
||||
|
||||
def transform_to_speckle(transform)
|
||||
t_arr = transform.to_a
|
||||
{
|
||||
speckle_type: 'Objects.Other.Transform',
|
||||
units: @units,
|
||||
value: [
|
||||
t_arr[0], t_arr[4], t_arr[8], length_to_speckle(t_arr[12]),
|
||||
t_arr[1], t_arr[5], t_arr[9], length_to_speckle(t_arr[13]),
|
||||
t_arr[2], t_arr[6], t_arr[10], length_to_speckle(t_arr[14]),
|
||||
t_arr[3], t_arr[7], t_arr[11], t_arr[15]
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def initialise_group_mesh(face, bounds)
|
||||
{
|
||||
speckle_type: 'Objects.Geometry.Mesh',
|
||||
units: @units,
|
||||
bbox: bounds_to_speckle(bounds),
|
||||
'@(31250)vertices' => [],
|
||||
'@(62500)faces' => [],
|
||||
'@(31250)faceEdgeFlags' => [],
|
||||
'@(31250)textureCoordinates' => [],
|
||||
pt_count: 0,
|
||||
renderMaterial: face.material.nil? ? nil : material_to_speckle(face.material)
|
||||
}
|
||||
end
|
||||
|
||||
# get an array of face indices from a sketchup polygon mesh
|
||||
def mesh_faces_to_array(mesh, offset = 0)
|
||||
faces = []
|
||||
mesh.polygons.each do |poly|
|
||||
faces.push(
|
||||
poly.count, *poly.map { |index| index.abs + offset }
|
||||
)
|
||||
end
|
||||
faces
|
||||
end
|
||||
|
||||
# get an array of face indices from a sketchup polygon mesh INCLUDING negative indices for hidden meshes
|
||||
def mesh_faces_with_edges_to_array(mesh, offset)
|
||||
faces = []
|
||||
mesh.polygons.each do |poly|
|
||||
faces.push(
|
||||
poly.count, *poly.map { |index| index > 0 ? index + offset : index - offset }
|
||||
)
|
||||
end
|
||||
faces
|
||||
end
|
||||
|
||||
# get a flat array of vertices from a sketchup polygon mesh
|
||||
def mesh_points_to_array(mesh)
|
||||
pts_array = []
|
||||
mesh.points.each do |pt|
|
||||
pts_array.push(
|
||||
length_to_speckle(pt[0]),
|
||||
length_to_speckle(pt[1]),
|
||||
length_to_speckle(pt[2])
|
||||
)
|
||||
end
|
||||
pts_array
|
||||
end
|
||||
|
||||
def mesh_edge_flags_to_array(mesh)
|
||||
edge_flags = []
|
||||
mesh.polygons.each do |poly|
|
||||
edge_flags.push(
|
||||
*poly.map(&:negative?)
|
||||
)
|
||||
end
|
||||
edge_flags
|
||||
end
|
||||
|
||||
# get a flat array of face indices from a sketchup face
|
||||
def face_indices_to_array(face, offset)
|
||||
face_array = [face.vertices.count]
|
||||
face_array.push(*face.vertices.count.times.map { |index| index + offset })
|
||||
face_array
|
||||
end
|
||||
|
||||
# get a flat array of face indices from a sketchup face
|
||||
def face_indices_with_edges_to_array(face, offset = 1)
|
||||
soft_edges = face.outer_loop.edges.map(&:soft?)
|
||||
face_array = [face.vertices.count]
|
||||
face_array.push(*face.vertices.count.times.map do |index|
|
||||
soft_edges[index] ? -(index + offset) : index + offset
|
||||
end)
|
||||
face_array
|
||||
end
|
||||
|
||||
def face_edge_flags_to_array(face)
|
||||
face.outer_loop.edges.map(&:soft?)
|
||||
end
|
||||
|
||||
# get a flat array of vertices from a list of sketchup vertices
|
||||
def face_vertices_to_array(face)
|
||||
pts_array = []
|
||||
face.vertices.each do |v|
|
||||
pt = v.position
|
||||
pts_array.push(length_to_speckle(pt[0]), length_to_speckle(pt[1]), length_to_speckle(pt[2]))
|
||||
end
|
||||
pts_array
|
||||
end
|
||||
|
||||
def uvs_to_array(mesh)
|
||||
uvs_array = []
|
||||
mesh.uvs(true).each do |pt|
|
||||
uvs_array.push(
|
||||
length_to_speckle(pt[0] / pt[2]),
|
||||
length_to_speckle(pt[1] / pt[2])
|
||||
)
|
||||
end
|
||||
uvs_array
|
||||
end
|
||||
|
||||
def face_to_speckle(face)
|
||||
mesh = face.loops.count > 1 ? face.mesh : nil
|
||||
{
|
||||
speckle_type: 'Objects.Geometry.Mesh',
|
||||
units: @units,
|
||||
renderMaterial: face.material.nil? ? nil : material_to_speckle(face.material),
|
||||
bbox: bounds_to_speckle(face.bounds),
|
||||
'@(31250)vertices' => mesh.nil? ? face_vertices_to_array(face) : mesh_points_to_array(mesh),
|
||||
'@(62500)faces' => mesh.nil? ? face_indices_to_array(face, 0) : mesh_faces_to_array(mesh, -1),
|
||||
'@(31250)faceEdgeFlags' => mesh.nil? ? face_edge_flags_to_array(face) : mesh_edge_flags_to_array(mesh)
|
||||
}
|
||||
end
|
||||
|
||||
def vertex_to_speckle(vertex)
|
||||
point = vertex.position
|
||||
{
|
||||
speckle_type: 'Objects.Geometry.Point',
|
||||
units: @units,
|
||||
x: length_to_speckle(point[0]),
|
||||
y: length_to_speckle(point[1]),
|
||||
z: length_to_speckle(point[2])
|
||||
}
|
||||
end
|
||||
|
||||
def material_to_speckle(material)
|
||||
rgba = material.color.to_a
|
||||
{
|
||||
speckle_type: 'Objects.Other.RenderMaterial',
|
||||
name: material.name,
|
||||
diffuse: [rgba[3] << 24 | rgba[0] << 16 | rgba[1] << 8 | rgba[2]].pack('l').unpack1('l'),
|
||||
opacity: material.alpha,
|
||||
emissive: -16_777_216,
|
||||
metalness: 0,
|
||||
roughness: 1
|
||||
}
|
||||
end
|
||||
|
||||
def bounds_to_speckle(bounds)
|
||||
min_pt = bounds.min
|
||||
{
|
||||
speckle_type: 'Objects.Geometry.Box',
|
||||
units: @units,
|
||||
area: 0,
|
||||
volume: 0,
|
||||
xSize: speckle_interval(min_pt[0], bounds.width),
|
||||
ySize: speckle_interval(min_pt[1], bounds.height),
|
||||
zSize: speckle_interval(min_pt[2], bounds.depth),
|
||||
basePlane: speckle_plane
|
||||
}
|
||||
end
|
||||
|
||||
def speckle_interval(start_val, end_val)
|
||||
{
|
||||
speckle_type: 'Objects.Primitive.Interval',
|
||||
units: @units,
|
||||
start: start_val.is_a?(Length) ? length_to_speckle(start_val) : start_val,
|
||||
end: end_val.is_a?(Length) ? length_to_speckle(end_val) : end_val
|
||||
}
|
||||
end
|
||||
|
||||
def speckle_point(x = 0.0, y = 0.0, z = 0.0, vector: false)
|
||||
{
|
||||
speckle_type: vector ? 'Objects.Geometry.Vector' : 'Objects.Geometry.Point',
|
||||
units: @units,
|
||||
x: x.is_a?(Length) ? length_to_speckle(x) : x,
|
||||
y: y.is_a?(Length) ? length_to_speckle(y) : y,
|
||||
z: z.is_a?(Length) ? length_to_speckle(z) : z
|
||||
}
|
||||
end
|
||||
|
||||
def speckle_vector(x = 0.0, y = 0.0, z = 0.0)
|
||||
speckle_point(x, y, z, vector: true)
|
||||
end
|
||||
|
||||
def speckle_plane(xdir: [1, 0, 0], ydir: [0, 1, 0], normal: [0, 0, 1], origin: [0, 0, 0])
|
||||
{
|
||||
speckle_type: 'Objects.Geometry.Plane',
|
||||
units: @units,
|
||||
xdir: speckle_vector(*xdir),
|
||||
ydir: speckle_vector(*ydir),
|
||||
normal: speckle_vector(*normal),
|
||||
origin: speckle_point(*origin)
|
||||
}
|
||||
SpeckleObjects::Other::BlockDefinition.from_definition(obj, @units, @definitions)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
# rubocop:enable Metrics/PerceivedComplexity
|
||||
# rubocop:enable Metrics/CyclomaticComplexity
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
# rubocop:enable Metrics/ModuleLength
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
# support multiple ruby version (fat binaries under windows)
|
||||
begin
|
||||
RUBY_VERSION =~ /(\d+\.\d+)/
|
||||
require "sqlite3/#{$1}/sqlite3_native"
|
||||
rescue LoadError
|
||||
require 'sqlite3/sqlite3_native'
|
||||
end
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'sqlite3/database'
|
||||
require_relative 'sqlite3/version'
|
||||
require_relative '../constants/platform_constants'
|
||||
|
||||
module SQLite3
|
||||
# Was sqlite3 compiled with thread safety on?
|
||||
def self.threadsafe?; threadsafe > 0; end
|
||||
module SpeckleConnector
|
||||
extension = if OPERATING_SYSTEM == OS_WIN
|
||||
'so'
|
||||
else
|
||||
'bundle'
|
||||
end
|
||||
sqlite3_file = "sqlite3_#{RUBY_VERSION_NUMBER}.#{extension}"
|
||||
require_relative(File.join('sqlite3', sqlite3_file))
|
||||
end
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
module SQLite3 ; module Constants
|
||||
|
||||
module TextRep
|
||||
UTF8 = 1
|
||||
UTF16LE = 2
|
||||
UTF16BE = 3
|
||||
UTF16 = 4
|
||||
ANY = 5
|
||||
DETERMINISTIC = 0x800
|
||||
end
|
||||
|
||||
module ColumnType
|
||||
INTEGER = 1
|
||||
FLOAT = 2
|
||||
TEXT = 3
|
||||
BLOB = 4
|
||||
NULL = 5
|
||||
end
|
||||
|
||||
module ErrorCode
|
||||
OK = 0 # Successful result
|
||||
ERROR = 1 # SQL error or missing database
|
||||
INTERNAL = 2 # An internal logic error in SQLite
|
||||
PERM = 3 # Access permission denied
|
||||
ABORT = 4 # Callback routine requested an abort
|
||||
BUSY = 5 # The database file is locked
|
||||
LOCKED = 6 # A table in the database is locked
|
||||
NOMEM = 7 # A malloc() failed
|
||||
READONLY = 8 # Attempt to write a readonly database
|
||||
INTERRUPT = 9 # Operation terminated by sqlite_interrupt()
|
||||
IOERR = 10 # Some kind of disk I/O error occurred
|
||||
CORRUPT = 11 # The database disk image is malformed
|
||||
NOTFOUND = 12 # (Internal Only) Table or record not found
|
||||
FULL = 13 # Insertion failed because database is full
|
||||
CANTOPEN = 14 # Unable to open the database file
|
||||
PROTOCOL = 15 # Database lock protocol error
|
||||
EMPTY = 16 # (Internal Only) Database table is empty
|
||||
SCHEMA = 17 # The database schema changed
|
||||
TOOBIG = 18 # Too much data for one row of a table
|
||||
CONSTRAINT = 19 # Abort due to constraint violation
|
||||
MISMATCH = 20 # Data type mismatch
|
||||
MISUSE = 21 # Library used incorrectly
|
||||
NOLFS = 22 # Uses OS features not supported on host
|
||||
AUTH = 23 # Authorization denied
|
||||
|
||||
ROW = 100 # sqlite_step() has another row ready
|
||||
DONE = 101 # sqlite_step() has finished executing
|
||||
end
|
||||
|
||||
end ; end
|
||||
@@ -1,741 +0,0 @@
|
||||
require_relative 'constants'
|
||||
require_relative 'errors'
|
||||
require_relative 'pragmas'
|
||||
require_relative 'statement'
|
||||
require_relative 'translator'
|
||||
require_relative 'value'
|
||||
|
||||
module SQLite3
|
||||
|
||||
# The Database class encapsulates a single connection to a SQLite3 database.
|
||||
# Its usage is very straightforward:
|
||||
#
|
||||
# require 'sqlite3'
|
||||
#
|
||||
# SQLite3::Database.new( "data.db" ) do |db|
|
||||
# db.execute( "select * from table" ) do |row|
|
||||
# p row
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# It wraps the lower-level methods provides by the selected driver, and
|
||||
# includes the Pragmas module for access to various pragma convenience
|
||||
# methods.
|
||||
#
|
||||
# The Database class provides type translation services as well, by which
|
||||
# the SQLite3 data types (which are all represented as strings) may be
|
||||
# converted into their corresponding types (as defined in the schemas
|
||||
# for their tables). This translation only occurs when querying data from
|
||||
# the database--insertions and updates are all still typeless.
|
||||
#
|
||||
# Furthermore, the Database class has been designed to work well with the
|
||||
# ArrayFields module from Ara Howard. If you require the ArrayFields
|
||||
# module before performing a query, and if you have not enabled results as
|
||||
# hashes, then the results will all be indexible by field name.
|
||||
class Database
|
||||
attr_reader :collations
|
||||
|
||||
include Pragmas
|
||||
|
||||
class << self
|
||||
|
||||
alias :open :new
|
||||
|
||||
# Quotes the given string, making it safe to use in an SQL statement.
|
||||
# It replaces all instances of the single-quote character with two
|
||||
# single-quote characters. The modified string is returned.
|
||||
def quote( string )
|
||||
string.gsub( /'/, "''" )
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# A boolean that indicates whether rows in result sets should be returned
|
||||
# as hashes or not. By default, rows are returned as arrays.
|
||||
attr_accessor :results_as_hash
|
||||
|
||||
# call-seq: SQLite3::Database.new(file, options = {})
|
||||
#
|
||||
# Create a new Database object that opens the given file. If utf16
|
||||
# is +true+, the filename is interpreted as a UTF-16 encoded string.
|
||||
#
|
||||
# By default, the new database will return result rows as arrays
|
||||
# (#results_as_hash) and has type translation disabled (#type_translation=).
|
||||
|
||||
def initialize file, options = {}, zvfs = nil
|
||||
mode = Constants::Open::READWRITE | Constants::Open::CREATE
|
||||
|
||||
file = file.to_path if file.respond_to? :to_path
|
||||
if file.encoding == ::Encoding::UTF_16LE || file.encoding == ::Encoding::UTF_16BE || options[:utf16]
|
||||
open16 file
|
||||
else
|
||||
# The three primary flag values for sqlite3_open_v2 are:
|
||||
# SQLITE_OPEN_READONLY
|
||||
# SQLITE_OPEN_READWRITE
|
||||
# SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE -- always used for sqlite3_open and sqlite3_open16
|
||||
mode = Constants::Open::READONLY if options[:readonly]
|
||||
|
||||
if options[:readwrite]
|
||||
raise "conflicting options: readonly and readwrite" if options[:readonly]
|
||||
mode = Constants::Open::READWRITE
|
||||
end
|
||||
|
||||
if options[:flags]
|
||||
if options[:readonly] || options[:readwrite]
|
||||
raise "conflicting options: flags with readonly and/or readwrite"
|
||||
end
|
||||
mode = options[:flags]
|
||||
end
|
||||
|
||||
open_v2 file.encode("utf-8"), mode, zvfs
|
||||
|
||||
if options[:strict]
|
||||
disable_quirk_mode
|
||||
end
|
||||
end
|
||||
|
||||
@tracefunc = nil
|
||||
@authorizer = nil
|
||||
@encoding = nil
|
||||
@busy_handler = nil
|
||||
@collations = {}
|
||||
@functions = {}
|
||||
@results_as_hash = options[:results_as_hash]
|
||||
@type_translation = options[:type_translation]
|
||||
@type_translator = make_type_translator @type_translation
|
||||
@readonly = mode & Constants::Open::READONLY != 0
|
||||
|
||||
if block_given?
|
||||
begin
|
||||
yield self
|
||||
ensure
|
||||
close
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def type_translation= value # :nodoc:
|
||||
warn(<<-eowarn) if $VERBOSE
|
||||
#{caller[0]} is calling SQLite3::Database#type_translation=
|
||||
SQLite3::Database#type_translation= is deprecated and will be removed
|
||||
in version 2.0.0.
|
||||
eowarn
|
||||
@type_translator = make_type_translator value
|
||||
@type_translation = value
|
||||
end
|
||||
attr_reader :type_translation # :nodoc:
|
||||
|
||||
# Return the type translator employed by this database instance. Each
|
||||
# database instance has its own type translator; this allows for different
|
||||
# type handlers to be installed in each instance without affecting other
|
||||
# instances. Furthermore, the translators are instantiated lazily, so that
|
||||
# if a database does not use type translation, it will not be burdened by
|
||||
# the overhead of a useless type translator. (See the Translator class.)
|
||||
def translator
|
||||
@translator ||= Translator.new
|
||||
end
|
||||
|
||||
# Installs (or removes) a block that will be invoked for every access
|
||||
# to the database. If the block returns 0 (or +nil+), the statement
|
||||
# is allowed to proceed. Returning 1 causes an authorization error to
|
||||
# occur, and returning 2 causes the access to be silently denied.
|
||||
def authorizer( &block )
|
||||
self.authorizer = block
|
||||
end
|
||||
|
||||
# Returns a Statement object representing the given SQL. This does not
|
||||
# execute the statement; it merely prepares the statement for execution.
|
||||
#
|
||||
# The Statement can then be executed using Statement#execute.
|
||||
#
|
||||
def prepare sql
|
||||
stmt = SQLite3::Statement.new( self, sql )
|
||||
return stmt unless block_given?
|
||||
|
||||
begin
|
||||
yield stmt
|
||||
ensure
|
||||
stmt.close unless stmt.closed?
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the filename for the database named +db_name+. +db_name+ defaults
|
||||
# to "main". Main return `nil` or an empty string if the database is
|
||||
# temporary or in-memory.
|
||||
def filename db_name = 'main'
|
||||
db_filename db_name
|
||||
end
|
||||
|
||||
# Executes the given SQL statement. If additional parameters are given,
|
||||
# they are treated as bind variables, and are bound to the placeholders in
|
||||
# the query.
|
||||
#
|
||||
# Note that if any of the values passed to this are hashes, then the
|
||||
# key/value pairs are each bound separately, with the key being used as
|
||||
# the name of the placeholder to bind the value to.
|
||||
#
|
||||
# The block is optional. If given, it will be invoked for each row returned
|
||||
# by the query. Otherwise, any results are accumulated into an array and
|
||||
# returned wholesale.
|
||||
#
|
||||
# See also #execute2, #query, and #execute_batch for additional ways of
|
||||
# executing statements.
|
||||
def execute sql, bind_vars = [], *args, &block
|
||||
if bind_vars.nil? || !args.empty?
|
||||
if args.empty?
|
||||
bind_vars = []
|
||||
else
|
||||
bind_vars = [bind_vars] + args
|
||||
end
|
||||
|
||||
warn(<<-eowarn) if $VERBOSE
|
||||
#{caller[0]} is calling SQLite3::Database#execute with nil or multiple bind params
|
||||
without using an array. Please switch to passing bind parameters as an array.
|
||||
Support for bind parameters as *args will be removed in 2.0.0.
|
||||
eowarn
|
||||
end
|
||||
|
||||
prepare( sql ) do |stmt|
|
||||
stmt.bind_params(bind_vars)
|
||||
stmt = ResultSet.new self, stmt
|
||||
|
||||
if block_given?
|
||||
stmt.each do |row|
|
||||
yield row
|
||||
end
|
||||
else
|
||||
stmt.to_a
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Executes the given SQL statement, exactly as with #execute. However, the
|
||||
# first row returned (either via the block, or in the returned array) is
|
||||
# always the names of the columns. Subsequent rows correspond to the data
|
||||
# from the result set.
|
||||
#
|
||||
# Thus, even if the query itself returns no rows, this method will always
|
||||
# return at least one row--the names of the columns.
|
||||
#
|
||||
# See also #execute, #query, and #execute_batch for additional ways of
|
||||
# executing statements.
|
||||
def execute2( sql, *bind_vars )
|
||||
prepare( sql ) do |stmt|
|
||||
result = stmt.execute( *bind_vars )
|
||||
if block_given?
|
||||
yield stmt.columns
|
||||
result.each { |row| yield row }
|
||||
else
|
||||
return result.inject( [ stmt.columns ] ) { |arr,row|
|
||||
arr << row; arr }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Executes all SQL statements in the given string. By contrast, the other
|
||||
# means of executing queries will only execute the first statement in the
|
||||
# string, ignoring all subsequent statements. This will execute each one
|
||||
# in turn. The same bind parameters, if given, will be applied to each
|
||||
# statement.
|
||||
#
|
||||
# This always returns +nil+, making it unsuitable for queries that return
|
||||
# rows.
|
||||
#
|
||||
# See also #execute_batch2 for additional ways of
|
||||
# executing statements.
|
||||
def execute_batch( sql, bind_vars = [], *args )
|
||||
# FIXME: remove this stuff later
|
||||
unless [Array, Hash].include?(bind_vars.class)
|
||||
bind_vars = [bind_vars]
|
||||
warn(<<-eowarn) if $VERBOSE
|
||||
#{caller[0]} is calling SQLite3::Database#execute_batch with bind parameters
|
||||
that are not a list of a hash. Please switch to passing bind parameters as an
|
||||
array or hash. Support for this behavior will be removed in version 2.0.0.
|
||||
eowarn
|
||||
end
|
||||
|
||||
# FIXME: remove this stuff later
|
||||
if bind_vars.nil? || !args.empty?
|
||||
if args.empty?
|
||||
bind_vars = []
|
||||
else
|
||||
bind_vars = [nil] + args
|
||||
end
|
||||
|
||||
warn(<<-eowarn) if $VERBOSE
|
||||
#{caller[0]} is calling SQLite3::Database#execute_batch with nil or multiple bind params
|
||||
without using an array. Please switch to passing bind parameters as an array.
|
||||
Support for this behavior will be removed in version 2.0.0.
|
||||
eowarn
|
||||
end
|
||||
|
||||
sql = sql.strip
|
||||
until sql.empty? do
|
||||
prepare( sql ) do |stmt|
|
||||
unless stmt.closed?
|
||||
# FIXME: this should probably use sqlite3's api for batch execution
|
||||
# This implementation requires stepping over the results.
|
||||
if bind_vars.length == stmt.bind_parameter_count
|
||||
stmt.bind_params(bind_vars)
|
||||
end
|
||||
stmt.step
|
||||
end
|
||||
sql = stmt.remainder.strip
|
||||
end
|
||||
end
|
||||
# FIXME: we should not return `nil` as a success return value
|
||||
nil
|
||||
end
|
||||
|
||||
# Executes all SQL statements in the given string. By contrast, the other
|
||||
# means of executing queries will only execute the first statement in the
|
||||
# string, ignoring all subsequent statements. This will execute each one
|
||||
# in turn. Bind parameters cannot be passed to #execute_batch2.
|
||||
#
|
||||
# If a query is made, all values will be returned as strings.
|
||||
# If no query is made, an empty array will be returned.
|
||||
#
|
||||
# Because all values except for 'NULL' are returned as strings,
|
||||
# a block can be passed to parse the values accordingly.
|
||||
#
|
||||
# See also #execute_batch for additional ways of
|
||||
# executing statements.
|
||||
def execute_batch2(sql, &block)
|
||||
if block_given?
|
||||
result = exec_batch(sql, @results_as_hash)
|
||||
result.map do |val|
|
||||
yield val
|
||||
end
|
||||
else
|
||||
exec_batch(sql, @results_as_hash)
|
||||
end
|
||||
end
|
||||
|
||||
# This is a convenience method for creating a statement, binding
|
||||
# parameters to it, and calling execute:
|
||||
#
|
||||
# result = db.query( "select * from foo where a=?", [5])
|
||||
# # is the same as
|
||||
# result = db.prepare( "select * from foo where a=?" ).execute( 5 )
|
||||
#
|
||||
# You must be sure to call +close+ on the ResultSet instance that is
|
||||
# returned, or you could have problems with locks on the table. If called
|
||||
# with a block, +close+ will be invoked implicitly when the block
|
||||
# terminates.
|
||||
def query( sql, bind_vars = [], *args )
|
||||
|
||||
if bind_vars.nil? || !args.empty?
|
||||
if args.empty?
|
||||
bind_vars = []
|
||||
else
|
||||
bind_vars = [bind_vars] + args
|
||||
end
|
||||
|
||||
warn(<<-eowarn) if $VERBOSE
|
||||
#{caller[0]} is calling SQLite3::Database#query with nil or multiple bind params
|
||||
without using an array. Please switch to passing bind parameters as an array.
|
||||
Support for this will be removed in version 2.0.0.
|
||||
eowarn
|
||||
end
|
||||
|
||||
result = prepare( sql ).execute( bind_vars )
|
||||
if block_given?
|
||||
begin
|
||||
yield result
|
||||
ensure
|
||||
result.close
|
||||
end
|
||||
else
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
# A convenience method for obtaining the first row of a result set, and
|
||||
# discarding all others. It is otherwise identical to #execute.
|
||||
#
|
||||
# See also #get_first_value.
|
||||
def get_first_row( sql, *bind_vars )
|
||||
execute( sql, *bind_vars ).first
|
||||
end
|
||||
|
||||
# A convenience method for obtaining the first value of the first row of a
|
||||
# result set, and discarding all other values and rows. It is otherwise
|
||||
# identical to #execute.
|
||||
#
|
||||
# See also #get_first_row.
|
||||
def get_first_value( sql, *bind_vars )
|
||||
query( sql, bind_vars ) do |rs|
|
||||
if (row = rs.next)
|
||||
return @results_as_hash ? row[rs.columns[0]] : row[0]
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
alias :busy_timeout :busy_timeout=
|
||||
|
||||
# Creates a new function for use in SQL statements. It will be added as
|
||||
# +name+, with the given +arity+. (For variable arity functions, use
|
||||
# -1 for the arity.)
|
||||
#
|
||||
# The block should accept at least one parameter--the FunctionProxy
|
||||
# instance that wraps this function invocation--and any other
|
||||
# arguments it needs (up to its arity).
|
||||
#
|
||||
# The block does not return a value directly. Instead, it will invoke
|
||||
# the FunctionProxy#result= method on the +func+ parameter and
|
||||
# indicate the return value that way.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# db.create_function( "maim", 1 ) do |func, value|
|
||||
# if value.nil?
|
||||
# func.result = nil
|
||||
# else
|
||||
# func.result = value.split(//).sort.join
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# puts db.get_first_value( "select maim(name) from table" )
|
||||
def create_function name, arity, text_rep=Constants::TextRep::UTF8, &block
|
||||
define_function_with_flags(name, text_rep) do |*args|
|
||||
fp = FunctionProxy.new
|
||||
block.call(fp, *args)
|
||||
fp.result
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
# Creates a new aggregate function for use in SQL statements. Aggregate
|
||||
# functions are functions that apply over every row in the result set,
|
||||
# instead of over just a single row. (A very common aggregate function
|
||||
# is the "count" function, for determining the number of rows that match
|
||||
# a query.)
|
||||
#
|
||||
# The new function will be added as +name+, with the given +arity+. (For
|
||||
# variable arity functions, use -1 for the arity.)
|
||||
#
|
||||
# The +step+ parameter must be a proc object that accepts as its first
|
||||
# parameter a FunctionProxy instance (representing the function
|
||||
# invocation), with any subsequent parameters (up to the function's arity).
|
||||
# The +step+ callback will be invoked once for each row of the result set.
|
||||
#
|
||||
# The +finalize+ parameter must be a +proc+ object that accepts only a
|
||||
# single parameter, the FunctionProxy instance representing the current
|
||||
# function invocation. It should invoke FunctionProxy#result= to
|
||||
# store the result of the function.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# db.create_aggregate( "lengths", 1 ) do
|
||||
# step do |func, value|
|
||||
# func[ :total ] ||= 0
|
||||
# func[ :total ] += ( value ? value.length : 0 )
|
||||
# end
|
||||
#
|
||||
# finalize do |func|
|
||||
# func.result = func[ :total ] || 0
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# puts db.get_first_value( "select lengths(name) from table" )
|
||||
#
|
||||
# See also #create_aggregate_handler for a more object-oriented approach to
|
||||
# aggregate functions.
|
||||
def create_aggregate( name, arity, step=nil, finalize=nil,
|
||||
text_rep=Constants::TextRep::ANY, &block )
|
||||
|
||||
proxy = Class.new do
|
||||
def self.step( &block )
|
||||
define_method(:step_with_ctx, &block)
|
||||
end
|
||||
|
||||
def self.finalize( &block )
|
||||
define_method(:finalize_with_ctx, &block)
|
||||
end
|
||||
end
|
||||
|
||||
if block_given?
|
||||
proxy.instance_eval(&block)
|
||||
else
|
||||
proxy.class_eval do
|
||||
define_method(:step_with_ctx, step)
|
||||
define_method(:finalize_with_ctx, finalize)
|
||||
end
|
||||
end
|
||||
|
||||
proxy.class_eval do
|
||||
# class instance variables
|
||||
@name = name
|
||||
@arity = arity
|
||||
|
||||
def self.name
|
||||
@name
|
||||
end
|
||||
|
||||
def self.arity
|
||||
@arity
|
||||
end
|
||||
|
||||
def initialize
|
||||
@ctx = FunctionProxy.new
|
||||
end
|
||||
|
||||
def step( *args )
|
||||
step_with_ctx(@ctx, *args)
|
||||
end
|
||||
|
||||
def finalize
|
||||
finalize_with_ctx(@ctx)
|
||||
@ctx.result
|
||||
end
|
||||
end
|
||||
define_aggregator2(proxy, name)
|
||||
end
|
||||
|
||||
# This is another approach to creating an aggregate function (see
|
||||
# #create_aggregate). Instead of explicitly specifying the name,
|
||||
# callbacks, arity, and type, you specify a factory object
|
||||
# (the "handler") that knows how to obtain all of that information. The
|
||||
# handler should respond to the following messages:
|
||||
#
|
||||
# +arity+:: corresponds to the +arity+ parameter of #create_aggregate. This
|
||||
# message is optional, and if the handler does not respond to it,
|
||||
# the function will have an arity of -1.
|
||||
# +name+:: this is the name of the function. The handler _must_ implement
|
||||
# this message.
|
||||
# +new+:: this must be implemented by the handler. It should return a new
|
||||
# instance of the object that will handle a specific invocation of
|
||||
# the function.
|
||||
#
|
||||
# The handler instance (the object returned by the +new+ message, described
|
||||
# above), must respond to the following messages:
|
||||
#
|
||||
# +step+:: this is the method that will be called for each step of the
|
||||
# aggregate function's evaluation. It should implement the same
|
||||
# signature as the +step+ callback for #create_aggregate.
|
||||
# +finalize+:: this is the method that will be called to finalize the
|
||||
# aggregate function's evaluation. It should implement the
|
||||
# same signature as the +finalize+ callback for
|
||||
# #create_aggregate.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# class LengthsAggregateHandler
|
||||
# def self.arity; 1; end
|
||||
# def self.name; 'lengths'; end
|
||||
#
|
||||
# def initialize
|
||||
# @total = 0
|
||||
# end
|
||||
#
|
||||
# def step( ctx, name )
|
||||
# @total += ( name ? name.length : 0 )
|
||||
# end
|
||||
#
|
||||
# def finalize( ctx )
|
||||
# ctx.result = @total
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# db.create_aggregate_handler( LengthsAggregateHandler )
|
||||
# puts db.get_first_value( "select lengths(name) from A" )
|
||||
def create_aggregate_handler( handler )
|
||||
# This is a compatibility shim so the (basically pointless) FunctionProxy
|
||||
# "ctx" object is passed as first argument to both step() and finalize().
|
||||
# Now its up to the library user whether he prefers to store his
|
||||
# temporaries as instance variables or fields in the FunctionProxy.
|
||||
# The library user still must set the result value with
|
||||
# FunctionProxy.result= as there is no backwards compatible way to
|
||||
# change this.
|
||||
proxy = Class.new(handler) do
|
||||
def initialize
|
||||
super
|
||||
@fp = FunctionProxy.new
|
||||
end
|
||||
|
||||
def step( *args )
|
||||
super(@fp, *args)
|
||||
end
|
||||
|
||||
def finalize
|
||||
super(@fp)
|
||||
@fp.result
|
||||
end
|
||||
end
|
||||
define_aggregator2(proxy, proxy.name)
|
||||
self
|
||||
end
|
||||
|
||||
# Define an aggregate function named +name+ using a object template
|
||||
# object +aggregator+. +aggregator+ must respond to +step+ and +finalize+.
|
||||
# +step+ will be called with row information and +finalize+ must return the
|
||||
# return value for the aggregator function.
|
||||
#
|
||||
# _API Change:_ +aggregator+ must also implement +clone+. The provided
|
||||
# +aggregator+ object will serve as template that is cloned to provide the
|
||||
# individual instances of the aggregate function. Regular ruby objects
|
||||
# already provide a suitable +clone+.
|
||||
# The functions arity is the arity of the +step+ method.
|
||||
def define_aggregator( name, aggregator )
|
||||
# Previously, this has been implemented in C. Now this is just yet
|
||||
# another compatibility shim
|
||||
proxy = Class.new do
|
||||
@template = aggregator
|
||||
@name = name
|
||||
|
||||
def self.template
|
||||
@template
|
||||
end
|
||||
|
||||
def self.name
|
||||
@name
|
||||
end
|
||||
|
||||
def self.arity
|
||||
# this is what sqlite3_obj_method_arity did before
|
||||
@template.method(:step).arity
|
||||
end
|
||||
|
||||
def initialize
|
||||
@klass = self.class.template.clone
|
||||
end
|
||||
|
||||
def step(*args)
|
||||
@klass.step(*args)
|
||||
end
|
||||
|
||||
def finalize
|
||||
@klass.finalize
|
||||
end
|
||||
end
|
||||
define_aggregator2(proxy, name)
|
||||
self
|
||||
end
|
||||
|
||||
# Begins a new transaction. Note that nested transactions are not allowed
|
||||
# by SQLite, so attempting to nest a transaction will result in a runtime
|
||||
# exception.
|
||||
#
|
||||
# The +mode+ parameter may be either <tt>:deferred</tt> (the default),
|
||||
# <tt>:immediate</tt>, or <tt>:exclusive</tt>.
|
||||
#
|
||||
# If a block is given, the database instance is yielded to it, and the
|
||||
# transaction is committed when the block terminates. If the block
|
||||
# raises an exception, a rollback will be performed instead. Note that if
|
||||
# a block is given, #commit and #rollback should never be called
|
||||
# explicitly or you'll get an error when the block terminates.
|
||||
#
|
||||
# If a block is not given, it is the caller's responsibility to end the
|
||||
# transaction explicitly, either by calling #commit, or by calling
|
||||
# #rollback.
|
||||
def transaction( mode = :deferred )
|
||||
execute "begin #{mode.to_s} transaction"
|
||||
|
||||
if block_given?
|
||||
abort = false
|
||||
begin
|
||||
yield self
|
||||
rescue
|
||||
abort = true
|
||||
raise
|
||||
ensure
|
||||
abort and rollback or commit
|
||||
end
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
# Commits the current transaction. If there is no current transaction,
|
||||
# this will cause an error to be raised. This returns +true+, in order
|
||||
# to allow it to be used in idioms like
|
||||
# <tt>abort? and rollback or commit</tt>.
|
||||
def commit
|
||||
execute "commit transaction"
|
||||
true
|
||||
end
|
||||
|
||||
# Rolls the current transaction back. If there is no current transaction,
|
||||
# this will cause an error to be raised. This returns +true+, in order
|
||||
# to allow it to be used in idioms like
|
||||
# <tt>abort? and rollback or commit</tt>.
|
||||
def rollback
|
||||
execute "rollback transaction"
|
||||
true
|
||||
end
|
||||
|
||||
# Returns +true+ if the database has been open in readonly mode
|
||||
# A helper to check before performing any operation
|
||||
def readonly?
|
||||
@readonly
|
||||
end
|
||||
|
||||
# A helper class for dealing with custom functions (see #create_function,
|
||||
# #create_aggregate, and #create_aggregate_handler). It encapsulates the
|
||||
# opaque function object that represents the current invocation. It also
|
||||
# provides more convenient access to the API functions that operate on
|
||||
# the function object.
|
||||
#
|
||||
# This class will almost _always_ be instantiated indirectly, by working
|
||||
# with the create methods mentioned above.
|
||||
class FunctionProxy
|
||||
attr_accessor :result
|
||||
|
||||
# Create a new FunctionProxy that encapsulates the given +func+ object.
|
||||
# If context is non-nil, the functions context will be set to that. If
|
||||
# it is non-nil, it must quack like a Hash. If it is nil, then none of
|
||||
# the context functions will be available.
|
||||
def initialize
|
||||
@result = nil
|
||||
@context = {}
|
||||
end
|
||||
|
||||
# Set the result of the function to the given error message.
|
||||
# The function will then return that error.
|
||||
def set_error( error )
|
||||
@driver.result_error( @func, error.to_s, -1 )
|
||||
end
|
||||
|
||||
# (Only available to aggregate functions.) Returns the number of rows
|
||||
# that the aggregate has processed so far. This will include the current
|
||||
# row, and so will always return at least 1.
|
||||
def count
|
||||
@driver.aggregate_count( @func )
|
||||
end
|
||||
|
||||
# Returns the value with the given key from the context. This is only
|
||||
# available to aggregate functions.
|
||||
def []( key )
|
||||
@context[ key ]
|
||||
end
|
||||
|
||||
# Sets the value with the given key in the context. This is only
|
||||
# available to aggregate functions.
|
||||
def []=( key, value )
|
||||
@context[ key ] = value
|
||||
end
|
||||
end
|
||||
|
||||
# Translates a +row+ of data from the database with the given +types+
|
||||
def translate_from_db types, row
|
||||
@type_translator.call types, row
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
NULL_TRANSLATOR = lambda { |_, row| row }
|
||||
|
||||
def make_type_translator should_translate
|
||||
if should_translate
|
||||
lambda { |types, row|
|
||||
types.zip(row).map do |type, value|
|
||||
translator.translate( type, value )
|
||||
end
|
||||
}
|
||||
else
|
||||
NULL_TRANSLATOR
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,35 +0,0 @@
|
||||
require_relative 'constants'
|
||||
|
||||
module SQLite3
|
||||
class Exception < ::StandardError
|
||||
# A convenience for accessing the error code for this exception.
|
||||
attr_reader :code
|
||||
end
|
||||
|
||||
class SQLException < Exception; end
|
||||
class InternalException < Exception; end
|
||||
class PermissionException < Exception; end
|
||||
class AbortException < Exception; end
|
||||
class BusyException < Exception; end
|
||||
class LockedException < Exception; end
|
||||
class MemoryException < Exception; end
|
||||
class ReadOnlyException < Exception; end
|
||||
class InterruptException < Exception; end
|
||||
class IOException < Exception; end
|
||||
class CorruptException < Exception; end
|
||||
class NotFoundException < Exception; end
|
||||
class FullException < Exception; end
|
||||
class CantOpenException < Exception; end
|
||||
class ProtocolException < Exception; end
|
||||
class EmptyException < Exception; end
|
||||
class SchemaChangedException < Exception; end
|
||||
class TooBigException < Exception; end
|
||||
class ConstraintException < Exception; end
|
||||
class MismatchException < Exception; end
|
||||
class MisuseException < Exception; end
|
||||
class UnsupportedException < Exception; end
|
||||
class AuthorizationException < Exception; end
|
||||
class FormatException < Exception; end
|
||||
class RangeException < Exception; end
|
||||
class NotADatabaseException < Exception; end
|
||||
end
|
||||
@@ -1,595 +0,0 @@
|
||||
require_relative 'errors'
|
||||
|
||||
module SQLite3
|
||||
|
||||
# This module is intended for inclusion solely by the Database class. It
|
||||
# defines convenience methods for the various pragmas supported by SQLite3.
|
||||
#
|
||||
# For a detailed description of these pragmas, see the SQLite3 documentation
|
||||
# at http://sqlite.org/pragma.html.
|
||||
module Pragmas
|
||||
|
||||
# Returns +true+ or +false+ depending on the value of the named pragma.
|
||||
def get_boolean_pragma( name )
|
||||
get_first_value( "PRAGMA #{name}" ) != "0"
|
||||
end
|
||||
|
||||
# Sets the given pragma to the given boolean value. The value itself
|
||||
# may be +true+ or +false+, or any other commonly used string or
|
||||
# integer that represents truth.
|
||||
def set_boolean_pragma( name, mode )
|
||||
case mode
|
||||
when String
|
||||
case mode.downcase
|
||||
when "on", "yes", "true", "y", "t"; mode = "'ON'"
|
||||
when "off", "no", "false", "n", "f"; mode = "'OFF'"
|
||||
else
|
||||
raise Exception,
|
||||
"unrecognized pragma parameter #{mode.inspect}"
|
||||
end
|
||||
when true, 1
|
||||
mode = "ON"
|
||||
when false, 0, nil
|
||||
mode = "OFF"
|
||||
else
|
||||
raise Exception,
|
||||
"unrecognized pragma parameter #{mode.inspect}"
|
||||
end
|
||||
|
||||
execute( "PRAGMA #{name}=#{mode}" )
|
||||
end
|
||||
|
||||
# Requests the given pragma (and parameters), and if the block is given,
|
||||
# each row of the result set will be yielded to it. Otherwise, the results
|
||||
# are returned as an array.
|
||||
def get_query_pragma( name, *params, &block ) # :yields: row
|
||||
if params.empty?
|
||||
execute( "PRAGMA #{name}", &block )
|
||||
else
|
||||
args = "'" + params.join("','") + "'"
|
||||
execute( "PRAGMA #{name}( #{args} )", &block )
|
||||
end
|
||||
end
|
||||
|
||||
# Return the value of the given pragma.
|
||||
def get_enum_pragma( name )
|
||||
get_first_value( "PRAGMA #{name}" )
|
||||
end
|
||||
|
||||
# Set the value of the given pragma to +mode+. The +mode+ parameter must
|
||||
# conform to one of the values in the given +enum+ array. Each entry in
|
||||
# the array is another array comprised of elements in the enumeration that
|
||||
# have duplicate values. See #synchronous, #default_synchronous,
|
||||
# #temp_store, and #default_temp_store for usage examples.
|
||||
def set_enum_pragma( name, mode, enums )
|
||||
match = enums.find { |p| p.find { |i| i.to_s.downcase == mode.to_s.downcase } }
|
||||
raise Exception,
|
||||
"unrecognized #{name} #{mode.inspect}" unless match
|
||||
execute( "PRAGMA #{name}='#{match.first.upcase}'" )
|
||||
end
|
||||
|
||||
# Returns the value of the given pragma as an integer.
|
||||
def get_int_pragma( name )
|
||||
get_first_value( "PRAGMA #{name}" ).to_i
|
||||
end
|
||||
|
||||
# Set the value of the given pragma to the integer value of the +value+
|
||||
# parameter.
|
||||
def set_int_pragma( name, value )
|
||||
execute( "PRAGMA #{name}=#{value.to_i}" )
|
||||
end
|
||||
|
||||
# The enumeration of valid synchronous modes.
|
||||
SYNCHRONOUS_MODES = [ [ 'full', 2 ], [ 'normal', 1 ], [ 'off', 0 ] ]
|
||||
|
||||
# The enumeration of valid temp store modes.
|
||||
TEMP_STORE_MODES = [ [ 'default', 0 ], [ 'file', 1 ], [ 'memory', 2 ] ]
|
||||
|
||||
# The enumeration of valid auto vacuum modes.
|
||||
AUTO_VACUUM_MODES = [ [ 'none', 0 ], [ 'full', 1 ], [ 'incremental', 2 ] ]
|
||||
|
||||
# The list of valid journaling modes.
|
||||
JOURNAL_MODES = [ [ 'delete' ], [ 'truncate' ], [ 'persist' ], [ 'memory' ],
|
||||
[ 'wal' ], [ 'off' ] ]
|
||||
|
||||
# The list of valid locking modes.
|
||||
LOCKING_MODES = [ [ 'normal' ], [ 'exclusive' ] ]
|
||||
|
||||
# The list of valid encodings.
|
||||
ENCODINGS = [ [ 'utf-8' ], [ 'utf-16' ], [ 'utf-16le' ], [ 'utf-16be ' ] ]
|
||||
|
||||
# The list of valid WAL checkpoints.
|
||||
WAL_CHECKPOINTS = [ [ 'passive' ], [ 'full' ], [ 'restart' ], [ 'truncate' ] ]
|
||||
|
||||
def application_id
|
||||
get_int_pragma "application_id"
|
||||
end
|
||||
|
||||
def application_id=( integer )
|
||||
set_int_pragma "application_id", integer
|
||||
end
|
||||
|
||||
def auto_vacuum
|
||||
get_enum_pragma "auto_vacuum"
|
||||
end
|
||||
|
||||
def auto_vacuum=( mode )
|
||||
set_enum_pragma "auto_vacuum", mode, AUTO_VACUUM_MODES
|
||||
end
|
||||
|
||||
def automatic_index
|
||||
get_boolean_pragma "automatic_index"
|
||||
end
|
||||
|
||||
def automatic_index=( mode )
|
||||
set_boolean_pragma "automatic_index", mode
|
||||
end
|
||||
|
||||
def busy_timeout
|
||||
get_int_pragma "busy_timeout"
|
||||
end
|
||||
|
||||
def busy_timeout=( milliseconds )
|
||||
set_int_pragma "busy_timeout", milliseconds
|
||||
end
|
||||
|
||||
def cache_size
|
||||
get_int_pragma "cache_size"
|
||||
end
|
||||
|
||||
def cache_size=( size )
|
||||
set_int_pragma "cache_size", size
|
||||
end
|
||||
|
||||
def cache_spill
|
||||
get_boolean_pragma "cache_spill"
|
||||
end
|
||||
|
||||
def cache_spill=( mode )
|
||||
set_boolean_pragma "cache_spill", mode
|
||||
end
|
||||
|
||||
def case_sensitive_like=( mode )
|
||||
set_boolean_pragma "case_sensitive_like", mode
|
||||
end
|
||||
|
||||
def cell_size_check
|
||||
get_boolean_pragma "cell_size_check"
|
||||
end
|
||||
|
||||
def cell_size_check=( mode )
|
||||
set_boolean_pragma "cell_size_check", mode
|
||||
end
|
||||
|
||||
def checkpoint_fullfsync
|
||||
get_boolean_pragma "checkpoint_fullfsync"
|
||||
end
|
||||
|
||||
def checkpoint_fullfsync=( mode )
|
||||
set_boolean_pragma "checkpoint_fullfsync", mode
|
||||
end
|
||||
|
||||
def collation_list( &block ) # :yields: row
|
||||
get_query_pragma "collation_list", &block
|
||||
end
|
||||
|
||||
def compile_options( &block ) # :yields: row
|
||||
get_query_pragma "compile_options", &block
|
||||
end
|
||||
|
||||
def count_changes
|
||||
get_boolean_pragma "count_changes"
|
||||
end
|
||||
|
||||
def count_changes=( mode )
|
||||
set_boolean_pragma "count_changes", mode
|
||||
end
|
||||
|
||||
def data_version
|
||||
get_int_pragma "data_version"
|
||||
end
|
||||
|
||||
def database_list( &block ) # :yields: row
|
||||
get_query_pragma "database_list", &block
|
||||
end
|
||||
|
||||
def default_cache_size
|
||||
get_int_pragma "default_cache_size"
|
||||
end
|
||||
|
||||
def default_cache_size=( size )
|
||||
set_int_pragma "default_cache_size", size
|
||||
end
|
||||
|
||||
def default_synchronous
|
||||
get_enum_pragma "default_synchronous"
|
||||
end
|
||||
|
||||
def default_synchronous=( mode )
|
||||
set_enum_pragma "default_synchronous", mode, SYNCHRONOUS_MODES
|
||||
end
|
||||
|
||||
def default_temp_store
|
||||
get_enum_pragma "default_temp_store"
|
||||
end
|
||||
|
||||
def default_temp_store=( mode )
|
||||
set_enum_pragma "default_temp_store", mode, TEMP_STORE_MODES
|
||||
end
|
||||
|
||||
def defer_foreign_keys
|
||||
get_boolean_pragma "defer_foreign_keys"
|
||||
end
|
||||
|
||||
def defer_foreign_keys=( mode )
|
||||
set_boolean_pragma "defer_foreign_keys", mode
|
||||
end
|
||||
|
||||
def encoding
|
||||
get_enum_pragma "encoding"
|
||||
end
|
||||
|
||||
def encoding=( mode )
|
||||
set_enum_pragma "encoding", mode, ENCODINGS
|
||||
end
|
||||
|
||||
def foreign_key_check( *table, &block ) # :yields: row
|
||||
get_query_pragma "foreign_key_check", *table, &block
|
||||
end
|
||||
|
||||
def foreign_key_list( table, &block ) # :yields: row
|
||||
get_query_pragma "foreign_key_list", table, &block
|
||||
end
|
||||
|
||||
def foreign_keys
|
||||
get_boolean_pragma "foreign_keys"
|
||||
end
|
||||
|
||||
def foreign_keys=( mode )
|
||||
set_boolean_pragma "foreign_keys", mode
|
||||
end
|
||||
|
||||
def freelist_count
|
||||
get_int_pragma "freelist_count"
|
||||
end
|
||||
|
||||
def full_column_names
|
||||
get_boolean_pragma "full_column_names"
|
||||
end
|
||||
|
||||
def full_column_names=( mode )
|
||||
set_boolean_pragma "full_column_names", mode
|
||||
end
|
||||
|
||||
def fullfsync
|
||||
get_boolean_pragma "fullfsync"
|
||||
end
|
||||
|
||||
def fullfsync=( mode )
|
||||
set_boolean_pragma "fullfsync", mode
|
||||
end
|
||||
|
||||
def ignore_check_constraints=( mode )
|
||||
set_boolean_pragma "ignore_check_constraints", mode
|
||||
end
|
||||
|
||||
def incremental_vacuum( pages, &block ) # :yields: row
|
||||
get_query_pragma "incremental_vacuum", pages, &block
|
||||
end
|
||||
|
||||
def index_info( index, &block ) # :yields: row
|
||||
get_query_pragma "index_info", index, &block
|
||||
end
|
||||
|
||||
def index_list( table, &block ) # :yields: row
|
||||
get_query_pragma "index_list", table, &block
|
||||
end
|
||||
|
||||
def index_xinfo( index, &block ) # :yields: row
|
||||
get_query_pragma "index_xinfo", index, &block
|
||||
end
|
||||
|
||||
def integrity_check( *num_errors, &block ) # :yields: row
|
||||
get_query_pragma "integrity_check", *num_errors, &block
|
||||
end
|
||||
|
||||
def journal_mode
|
||||
get_enum_pragma "journal_mode"
|
||||
end
|
||||
|
||||
def journal_mode=( mode )
|
||||
set_enum_pragma "journal_mode", mode, JOURNAL_MODES
|
||||
end
|
||||
|
||||
def journal_size_limit
|
||||
get_int_pragma "journal_size_limit"
|
||||
end
|
||||
|
||||
def journal_size_limit=( size )
|
||||
set_int_pragma "journal_size_limit", size
|
||||
end
|
||||
|
||||
def legacy_file_format
|
||||
get_boolean_pragma "legacy_file_format"
|
||||
end
|
||||
|
||||
def legacy_file_format=( mode )
|
||||
set_boolean_pragma "legacy_file_format", mode
|
||||
end
|
||||
|
||||
def locking_mode
|
||||
get_enum_pragma "locking_mode"
|
||||
end
|
||||
|
||||
def locking_mode=( mode )
|
||||
set_enum_pragma "locking_mode", mode, LOCKING_MODES
|
||||
end
|
||||
|
||||
def max_page_count
|
||||
get_int_pragma "max_page_count"
|
||||
end
|
||||
|
||||
def max_page_count=( size )
|
||||
set_int_pragma "max_page_count", size
|
||||
end
|
||||
|
||||
def mmap_size
|
||||
get_int_pragma "mmap_size"
|
||||
end
|
||||
|
||||
def mmap_size=( size )
|
||||
set_int_pragma "mmap_size", size
|
||||
end
|
||||
|
||||
def page_count
|
||||
get_int_pragma "page_count"
|
||||
end
|
||||
|
||||
def page_size
|
||||
get_int_pragma "page_size"
|
||||
end
|
||||
|
||||
def page_size=( size )
|
||||
set_int_pragma "page_size", size
|
||||
end
|
||||
|
||||
def parser_trace=( mode )
|
||||
set_boolean_pragma "parser_trace", mode
|
||||
end
|
||||
|
||||
def query_only
|
||||
get_boolean_pragma "query_only"
|
||||
end
|
||||
|
||||
def query_only=( mode )
|
||||
set_boolean_pragma "query_only", mode
|
||||
end
|
||||
|
||||
def quick_check( *num_errors, &block ) # :yields: row
|
||||
get_query_pragma "quick_check", *num_errors, &block
|
||||
end
|
||||
|
||||
def read_uncommitted
|
||||
get_boolean_pragma "read_uncommitted"
|
||||
end
|
||||
|
||||
def read_uncommitted=( mode )
|
||||
set_boolean_pragma "read_uncommitted", mode
|
||||
end
|
||||
|
||||
def recursive_triggers
|
||||
get_boolean_pragma "recursive_triggers"
|
||||
end
|
||||
|
||||
def recursive_triggers=( mode )
|
||||
set_boolean_pragma "recursive_triggers", mode
|
||||
end
|
||||
|
||||
def reverse_unordered_selects
|
||||
get_boolean_pragma "reverse_unordered_selects"
|
||||
end
|
||||
|
||||
def reverse_unordered_selects=( mode )
|
||||
set_boolean_pragma "reverse_unordered_selects", mode
|
||||
end
|
||||
|
||||
def schema_cookie
|
||||
get_int_pragma "schema_cookie"
|
||||
end
|
||||
|
||||
def schema_cookie=( cookie )
|
||||
set_int_pragma "schema_cookie", cookie
|
||||
end
|
||||
|
||||
def schema_version
|
||||
get_int_pragma "schema_version"
|
||||
end
|
||||
|
||||
def schema_version=( version )
|
||||
set_int_pragma "schema_version", version
|
||||
end
|
||||
|
||||
def secure_delete
|
||||
get_boolean_pragma "secure_delete"
|
||||
end
|
||||
|
||||
def secure_delete=( mode )
|
||||
set_boolean_pragma "secure_delete", mode
|
||||
end
|
||||
|
||||
def short_column_names
|
||||
get_boolean_pragma "short_column_names"
|
||||
end
|
||||
|
||||
def short_column_names=( mode )
|
||||
set_boolean_pragma "short_column_names", mode
|
||||
end
|
||||
|
||||
def shrink_memory
|
||||
execute( "PRAGMA shrink_memory" )
|
||||
end
|
||||
|
||||
def soft_heap_limit
|
||||
get_int_pragma "soft_heap_limit"
|
||||
end
|
||||
|
||||
def soft_heap_limit=( mode )
|
||||
set_int_pragma "soft_heap_limit", mode
|
||||
end
|
||||
|
||||
def stats( &block ) # :yields: row
|
||||
get_query_pragma "stats", &block
|
||||
end
|
||||
|
||||
def synchronous
|
||||
get_enum_pragma "synchronous"
|
||||
end
|
||||
|
||||
def synchronous=( mode )
|
||||
set_enum_pragma "synchronous", mode, SYNCHRONOUS_MODES
|
||||
end
|
||||
|
||||
def temp_store
|
||||
get_enum_pragma "temp_store"
|
||||
end
|
||||
|
||||
def temp_store=( mode )
|
||||
set_enum_pragma "temp_store", mode, TEMP_STORE_MODES
|
||||
end
|
||||
|
||||
def threads
|
||||
get_int_pragma "threads"
|
||||
end
|
||||
|
||||
def threads=( count )
|
||||
set_int_pragma "threads", count
|
||||
end
|
||||
|
||||
def user_cookie
|
||||
get_int_pragma "user_cookie"
|
||||
end
|
||||
|
||||
def user_cookie=( cookie )
|
||||
set_int_pragma "user_cookie", cookie
|
||||
end
|
||||
|
||||
def user_version
|
||||
get_int_pragma "user_version"
|
||||
end
|
||||
|
||||
def user_version=( version )
|
||||
set_int_pragma "user_version", version
|
||||
end
|
||||
|
||||
def vdbe_addoptrace=( mode )
|
||||
set_boolean_pragma "vdbe_addoptrace", mode
|
||||
end
|
||||
|
||||
def vdbe_debug=( mode )
|
||||
set_boolean_pragma "vdbe_debug", mode
|
||||
end
|
||||
|
||||
def vdbe_listing=( mode )
|
||||
set_boolean_pragma "vdbe_listing", mode
|
||||
end
|
||||
|
||||
def vdbe_trace
|
||||
get_boolean_pragma "vdbe_trace"
|
||||
end
|
||||
|
||||
def vdbe_trace=( mode )
|
||||
set_boolean_pragma "vdbe_trace", mode
|
||||
end
|
||||
|
||||
def wal_autocheckpoint
|
||||
get_int_pragma "wal_autocheckpoint"
|
||||
end
|
||||
|
||||
def wal_autocheckpoint=( mode )
|
||||
set_int_pragma "wal_autocheckpoint", mode
|
||||
end
|
||||
|
||||
def wal_checkpoint
|
||||
get_enum_pragma "wal_checkpoint"
|
||||
end
|
||||
|
||||
def wal_checkpoint=( mode )
|
||||
set_enum_pragma "wal_checkpoint", mode, WAL_CHECKPOINTS
|
||||
end
|
||||
|
||||
def writable_schema=( mode )
|
||||
set_boolean_pragma "writable_schema", mode
|
||||
end
|
||||
|
||||
###
|
||||
# Returns information about +table+. Yields each row of table information
|
||||
# if a block is provided.
|
||||
def table_info table
|
||||
stmt = prepare "PRAGMA table_info(#{table})"
|
||||
columns = stmt.columns
|
||||
|
||||
needs_tweak_default =
|
||||
version_compare(SQLite3.libversion.to_s, "3.3.7") > 0
|
||||
|
||||
result = [] unless block_given?
|
||||
stmt.each do |row|
|
||||
new_row = Hash[columns.zip(row)]
|
||||
|
||||
# FIXME: This should be removed but is required for older versions
|
||||
# of rails
|
||||
if(Object.const_defined?(:ActiveRecord))
|
||||
new_row['notnull'] = new_row['notnull'].to_s
|
||||
end
|
||||
|
||||
tweak_default(new_row) if needs_tweak_default
|
||||
|
||||
# Ensure the type value is downcased. On Mac and Windows
|
||||
# platforms this value is now being returned as all upper
|
||||
# case.
|
||||
if new_row['type']
|
||||
new_row['type'] = new_row['type'].downcase
|
||||
end
|
||||
|
||||
if block_given?
|
||||
yield new_row
|
||||
else
|
||||
result << new_row
|
||||
end
|
||||
end
|
||||
stmt.close
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Compares two version strings
|
||||
def version_compare(v1, v2)
|
||||
v1 = v1.split(".").map { |i| i.to_i }
|
||||
v2 = v2.split(".").map { |i| i.to_i }
|
||||
parts = [v1.length, v2.length].max
|
||||
v1.push 0 while v1.length < parts
|
||||
v2.push 0 while v2.length < parts
|
||||
v1.zip(v2).each do |a,b|
|
||||
return -1 if a < b
|
||||
return 1 if a > b
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
# Since SQLite 3.3.8, the table_info pragma has returned the default
|
||||
# value of the row as a quoted SQL value. This method essentially
|
||||
# unquotes those values.
|
||||
def tweak_default(hash)
|
||||
case hash["dflt_value"]
|
||||
when /^null$/i
|
||||
hash["dflt_value"] = nil
|
||||
when /^'(.*)'$/m
|
||||
hash["dflt_value"] = $1.gsub(/''/, "'")
|
||||
when /^"(.*)"$/m
|
||||
hash["dflt_value"] = $1.gsub(/""/, '"')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,187 +0,0 @@
|
||||
require_relative 'constants'
|
||||
require_relative 'errors'
|
||||
|
||||
module SQLite3
|
||||
|
||||
# The ResultSet object encapsulates the enumerability of a query's output.
|
||||
# It is a simple cursor over the data that the query returns. It will
|
||||
# very rarely (if ever) be instantiated directly. Instead, clients should
|
||||
# obtain a ResultSet instance via Statement#execute.
|
||||
class ResultSet
|
||||
include Enumerable
|
||||
|
||||
class ArrayWithTypes < Array # :nodoc:
|
||||
attr_accessor :types
|
||||
end
|
||||
|
||||
class ArrayWithTypesAndFields < Array # :nodoc:
|
||||
attr_writer :types
|
||||
attr_writer :fields
|
||||
|
||||
def types
|
||||
warn(<<-eowarn) if $VERBOSE
|
||||
#{caller[0]} is calling #{self.class}#types. This method will be removed in
|
||||
sqlite3 version 2.0.0, please call the `types` method on the SQLite3::ResultSet
|
||||
object that created this object
|
||||
eowarn
|
||||
@types
|
||||
end
|
||||
|
||||
def fields
|
||||
warn(<<-eowarn) if $VERBOSE
|
||||
#{caller[0]} is calling #{self.class}#fields. This method will be removed in
|
||||
sqlite3 version 2.0.0, please call the `columns` method on the SQLite3::ResultSet
|
||||
object that created this object
|
||||
eowarn
|
||||
@fields
|
||||
end
|
||||
end
|
||||
|
||||
# The class of which we return an object in case we want a Hash as
|
||||
# result.
|
||||
class HashWithTypesAndFields < Hash # :nodoc:
|
||||
attr_writer :types
|
||||
attr_writer :fields
|
||||
|
||||
def types
|
||||
warn(<<-eowarn) if $VERBOSE
|
||||
#{caller[0]} is calling #{self.class}#types. This method will be removed in
|
||||
sqlite3 version 2.0.0, please call the `types` method on the SQLite3::ResultSet
|
||||
object that created this object
|
||||
eowarn
|
||||
@types
|
||||
end
|
||||
|
||||
def fields
|
||||
warn(<<-eowarn) if $VERBOSE
|
||||
#{caller[0]} is calling #{self.class}#fields. This method will be removed in
|
||||
sqlite3 version 2.0.0, please call the `columns` method on the SQLite3::ResultSet
|
||||
object that created this object
|
||||
eowarn
|
||||
@fields
|
||||
end
|
||||
|
||||
def [] key
|
||||
key = fields[key] if key.is_a? Numeric
|
||||
super key
|
||||
end
|
||||
end
|
||||
|
||||
# Create a new ResultSet attached to the given database, using the
|
||||
# given sql text.
|
||||
def initialize db, stmt
|
||||
@db = db
|
||||
@stmt = stmt
|
||||
end
|
||||
|
||||
# Reset the cursor, so that a result set which has reached end-of-file
|
||||
# can be rewound and reiterated.
|
||||
def reset( *bind_params )
|
||||
@stmt.reset!
|
||||
@stmt.bind_params( *bind_params )
|
||||
@eof = false
|
||||
end
|
||||
|
||||
# Query whether the cursor has reached the end of the result set or not.
|
||||
def eof?
|
||||
@stmt.done?
|
||||
end
|
||||
|
||||
# Obtain the next row from the cursor. If there are no more rows to be
|
||||
# had, this will return +nil+. If type translation is active on the
|
||||
# corresponding database, the values in the row will be translated
|
||||
# according to their types.
|
||||
#
|
||||
# The returned value will be an array, unless Database#results_as_hash has
|
||||
# been set to +true+, in which case the returned value will be a hash.
|
||||
#
|
||||
# For arrays, the column names are accessible via the +fields+ property,
|
||||
# and the column types are accessible via the +types+ property.
|
||||
#
|
||||
# For hashes, the column names are the keys of the hash, and the column
|
||||
# types are accessible via the +types+ property.
|
||||
def next
|
||||
if @db.results_as_hash
|
||||
return next_hash
|
||||
end
|
||||
|
||||
row = @stmt.step
|
||||
return nil if @stmt.done?
|
||||
|
||||
row = @db.translate_from_db @stmt.types, row
|
||||
|
||||
if row.respond_to?(:fields)
|
||||
# FIXME: this can only happen if the translator returns something
|
||||
# that responds to `fields`. Since we're removing the translator
|
||||
# in 2.0, we can remove this branch in 2.0.
|
||||
row = ArrayWithTypes.new(row)
|
||||
else
|
||||
# FIXME: the `fields` and `types` methods are deprecated on this
|
||||
# object for version 2.0, so we can safely remove this branch
|
||||
# as well.
|
||||
row = ArrayWithTypesAndFields.new(row)
|
||||
end
|
||||
|
||||
row.fields = @stmt.columns
|
||||
row.types = @stmt.types
|
||||
row
|
||||
end
|
||||
|
||||
# Required by the Enumerable mixin. Provides an internal iterator over the
|
||||
# rows of the result set.
|
||||
def each
|
||||
while node = self.next
|
||||
yield node
|
||||
end
|
||||
end
|
||||
|
||||
# Provides an internal iterator over the rows of the result set where
|
||||
# each row is yielded as a hash.
|
||||
def each_hash
|
||||
while node = next_hash
|
||||
yield node
|
||||
end
|
||||
end
|
||||
|
||||
# Closes the statement that spawned this result set.
|
||||
# <em>Use with caution!</em> Closing a result set will automatically
|
||||
# close any other result sets that were spawned from the same statement.
|
||||
def close
|
||||
@stmt.close
|
||||
end
|
||||
|
||||
# Queries whether the underlying statement has been closed or not.
|
||||
def closed?
|
||||
@stmt.closed?
|
||||
end
|
||||
|
||||
# Returns the types of the columns returned by this result set.
|
||||
def types
|
||||
@stmt.types
|
||||
end
|
||||
|
||||
# Returns the names of the columns returned by this result set.
|
||||
def columns
|
||||
@stmt.columns
|
||||
end
|
||||
|
||||
# Return the next row as a hash
|
||||
def next_hash
|
||||
row = @stmt.step
|
||||
return nil if @stmt.done?
|
||||
|
||||
# FIXME: type translation is deprecated, so this can be removed
|
||||
# in 2.0
|
||||
row = @db.translate_from_db @stmt.types, row
|
||||
|
||||
# FIXME: this can be switched to a regular hash in 2.0
|
||||
row = HashWithTypesAndFields[*@stmt.columns.zip(row).flatten]
|
||||
|
||||
# FIXME: these methods are deprecated for version 2.0, so we can remove
|
||||
# this code in 2.0
|
||||
row.fields = @stmt.columns
|
||||
row.types = @stmt.types
|
||||
row
|
||||
end
|
||||
end
|
||||
end
|
||||
Binary file not shown.
@@ -1,145 +0,0 @@
|
||||
require_relative 'errors'
|
||||
require_relative 'resultset'
|
||||
|
||||
class String
|
||||
def to_blob
|
||||
SQLite3::Blob.new( self )
|
||||
end
|
||||
end
|
||||
|
||||
module SQLite3
|
||||
# A statement represents a prepared-but-unexecuted SQL query. It will rarely
|
||||
# (if ever) be instantiated directly by a client, and is most often obtained
|
||||
# via the Database#prepare method.
|
||||
class Statement
|
||||
include Enumerable
|
||||
|
||||
# This is any text that followed the first valid SQL statement in the text
|
||||
# with which the statement was initialized. If there was no trailing text,
|
||||
# this will be the empty string.
|
||||
attr_reader :remainder
|
||||
|
||||
# Binds the given variables to the corresponding placeholders in the SQL
|
||||
# text.
|
||||
#
|
||||
# See Database#execute for a description of the valid placeholder
|
||||
# syntaxes.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# stmt = db.prepare( "select * from table where a=? and b=?" )
|
||||
# stmt.bind_params( 15, "hello" )
|
||||
#
|
||||
# See also #execute, #bind_param, Statement#bind_param, and
|
||||
# Statement#bind_params.
|
||||
def bind_params( *bind_vars )
|
||||
index = 1
|
||||
bind_vars.flatten.each do |var|
|
||||
if Hash === var
|
||||
var.each { |key, val| bind_param key, val }
|
||||
else
|
||||
bind_param index, var
|
||||
index += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Execute the statement. This creates a new ResultSet object for the
|
||||
# statement's virtual machine. If a block was given, the new ResultSet will
|
||||
# be yielded to it; otherwise, the ResultSet will be returned.
|
||||
#
|
||||
# Any parameters will be bound to the statement using #bind_params.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# stmt = db.prepare( "select * from table" )
|
||||
# stmt.execute do |result|
|
||||
# ...
|
||||
# end
|
||||
#
|
||||
# See also #bind_params, #execute!.
|
||||
def execute( *bind_vars )
|
||||
reset! if active? || done?
|
||||
|
||||
bind_params(*bind_vars) unless bind_vars.empty?
|
||||
@results = ResultSet.new(@connection, self)
|
||||
|
||||
step if 0 == column_count
|
||||
|
||||
yield @results if block_given?
|
||||
@results
|
||||
end
|
||||
|
||||
# Execute the statement. If no block was given, this returns an array of
|
||||
# rows returned by executing the statement. Otherwise, each row will be
|
||||
# yielded to the block.
|
||||
#
|
||||
# Any parameters will be bound to the statement using #bind_params.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# stmt = db.prepare( "select * from table" )
|
||||
# stmt.execute! do |row|
|
||||
# ...
|
||||
# end
|
||||
#
|
||||
# See also #bind_params, #execute.
|
||||
def execute!( *bind_vars, &block )
|
||||
execute(*bind_vars)
|
||||
block_given? ? each(&block) : to_a
|
||||
end
|
||||
|
||||
# Returns true if the statement is currently active, meaning it has an
|
||||
# open result set.
|
||||
def active?
|
||||
!done?
|
||||
end
|
||||
|
||||
# Return an array of the column names for this statement. Note that this
|
||||
# may execute the statement in order to obtain the metadata; this makes it
|
||||
# a (potentially) expensive operation.
|
||||
def columns
|
||||
get_metadata unless @columns
|
||||
return @columns
|
||||
end
|
||||
|
||||
def each
|
||||
loop do
|
||||
val = step
|
||||
break self if done?
|
||||
yield val
|
||||
end
|
||||
end
|
||||
|
||||
# Return an array of the data types for each column in this statement. Note
|
||||
# that this may execute the statement in order to obtain the metadata; this
|
||||
# makes it a (potentially) expensive operation.
|
||||
def types
|
||||
must_be_open!
|
||||
get_metadata unless @types
|
||||
@types
|
||||
end
|
||||
|
||||
# Performs a sanity check to ensure that the statement is not
|
||||
# closed. If it is, an exception is raised.
|
||||
def must_be_open! # :nodoc:
|
||||
if closed?
|
||||
raise SQLite3::Exception, "cannot use a closed statement"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
# A convenience method for obtaining the metadata about the query. Note
|
||||
# that this will actually execute the SQL, which means it can be a
|
||||
# (potentially) expensive operation.
|
||||
def get_metadata
|
||||
@columns = Array.new(column_count) do |column|
|
||||
column_name column
|
||||
end
|
||||
@types = Array.new(column_count) do |column|
|
||||
val = column_decltype(column)
|
||||
val.nil? ? nil : val.downcase
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,118 +0,0 @@
|
||||
require 'time'
|
||||
require 'date'
|
||||
|
||||
module SQLite3
|
||||
|
||||
# The Translator class encapsulates the logic and callbacks necessary for
|
||||
# converting string data to a value of some specified type. Every Database
|
||||
# instance may have a Translator instance, in order to assist in type
|
||||
# translation (Database#type_translation).
|
||||
#
|
||||
# Further, applications may define their own custom type translation logic
|
||||
# by registering translator blocks with the corresponding database's
|
||||
# translator instance (Database#translator).
|
||||
class Translator
|
||||
|
||||
# Create a new Translator instance. It will be preinitialized with default
|
||||
# translators for most SQL data types.
|
||||
def initialize
|
||||
@translators = Hash.new( proc { |type,value| value } )
|
||||
@type_name_cache = {}
|
||||
register_default_translators
|
||||
end
|
||||
|
||||
# Add a new translator block, which will be invoked to process type
|
||||
# translations to the given type. The type should be an SQL datatype, and
|
||||
# may include parentheses (i.e., "VARCHAR(30)"). However, any parenthetical
|
||||
# information is stripped off and discarded, so type translation decisions
|
||||
# are made solely on the "base" type name.
|
||||
#
|
||||
# The translator block itself should accept two parameters, "type" and
|
||||
# "value". In this case, the "type" is the full type name (including
|
||||
# parentheses), so the block itself may include logic for changing how a
|
||||
# type is translated based on the additional data. The "value" parameter
|
||||
# is the (string) data to convert.
|
||||
#
|
||||
# The block should return the translated value.
|
||||
def add_translator( type, &block ) # :yields: type, value
|
||||
warn(<<-eowarn) if $VERBOSE
|
||||
#{caller[0]} is calling `add_translator`.
|
||||
Built in translators are deprecated and will be removed in version 2.0.0
|
||||
eowarn
|
||||
@translators[ type_name( type ) ] = block
|
||||
end
|
||||
|
||||
# Translate the given string value to a value of the given type. In the
|
||||
# absence of an installed translator block for the given type, the value
|
||||
# itself is always returned. Further, +nil+ values are never translated,
|
||||
# and are always passed straight through regardless of the type parameter.
|
||||
def translate( type, value )
|
||||
unless value.nil?
|
||||
# FIXME: this is a hack to support Sequel
|
||||
if type && %w{ datetime timestamp }.include?(type.downcase)
|
||||
@translators[ type_name( type ) ].call( type, value.to_s )
|
||||
else
|
||||
@translators[ type_name( type ) ].call( type, value )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# A convenience method for working with type names. This returns the "base"
|
||||
# type name, without any parenthetical data.
|
||||
def type_name( type )
|
||||
@type_name_cache[type] ||= begin
|
||||
type = "" if type.nil?
|
||||
type = $1 if type =~ /^(.*?)\(/
|
||||
type.upcase
|
||||
end
|
||||
end
|
||||
private :type_name
|
||||
|
||||
# Register the default translators for the current Translator instance.
|
||||
# This includes translators for most major SQL data types.
|
||||
def register_default_translators
|
||||
[ "time",
|
||||
"timestamp" ].each { |type| add_translator( type ) { |t, v| Time.parse( v ) } }
|
||||
|
||||
add_translator( "date" ) { |t,v| Date.parse(v) }
|
||||
add_translator( "datetime" ) { |t,v| DateTime.parse(v) }
|
||||
|
||||
[ "decimal",
|
||||
"float",
|
||||
"numeric",
|
||||
"double",
|
||||
"real",
|
||||
"dec",
|
||||
"fixed" ].each { |type| add_translator( type ) { |t,v| v.to_f } }
|
||||
|
||||
[ "integer",
|
||||
"smallint",
|
||||
"mediumint",
|
||||
"int",
|
||||
"bigint" ].each { |type| add_translator( type ) { |t,v| v.to_i } }
|
||||
|
||||
[ "bit",
|
||||
"bool",
|
||||
"boolean" ].each do |type|
|
||||
add_translator( type ) do |t,v|
|
||||
!( v.strip.gsub(/00+/,"0") == "0" ||
|
||||
v.downcase == "false" ||
|
||||
v.downcase == "f" ||
|
||||
v.downcase == "no" ||
|
||||
v.downcase == "n" )
|
||||
end
|
||||
end
|
||||
|
||||
add_translator( "tinyint" ) do |type, value|
|
||||
if type =~ /\(\s*1\s*\)/
|
||||
value.to_i == 1
|
||||
else
|
||||
value.to_i
|
||||
end
|
||||
end
|
||||
end
|
||||
private :register_default_translators
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,57 +0,0 @@
|
||||
require_relative 'constants'
|
||||
|
||||
module SQLite3
|
||||
|
||||
class Value
|
||||
attr_reader :handle
|
||||
|
||||
def initialize( db, handle )
|
||||
@driver = db.driver
|
||||
@handle = handle
|
||||
end
|
||||
|
||||
def null?
|
||||
type == :null
|
||||
end
|
||||
|
||||
def to_blob
|
||||
@driver.value_blob( @handle )
|
||||
end
|
||||
|
||||
def length( utf16=false )
|
||||
if utf16
|
||||
@driver.value_bytes16( @handle )
|
||||
else
|
||||
@driver.value_bytes( @handle )
|
||||
end
|
||||
end
|
||||
|
||||
def to_f
|
||||
@driver.value_double( @handle )
|
||||
end
|
||||
|
||||
def to_i
|
||||
@driver.value_int( @handle )
|
||||
end
|
||||
|
||||
def to_int64
|
||||
@driver.value_int64( @handle )
|
||||
end
|
||||
|
||||
def to_s( utf16=false )
|
||||
@driver.value_text( @handle, utf16 )
|
||||
end
|
||||
|
||||
def type
|
||||
case @driver.value_type( @handle )
|
||||
when Constants::ColumnType::INTEGER then :int
|
||||
when Constants::ColumnType::FLOAT then :float
|
||||
when Constants::ColumnType::TEXT then :text
|
||||
when Constants::ColumnType::BLOB then :blob
|
||||
when Constants::ColumnType::NULL then :null
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,23 +0,0 @@
|
||||
module SQLite3
|
||||
|
||||
VERSION = "1.5.3"
|
||||
|
||||
module VersionProxy
|
||||
MAJOR = 1
|
||||
MINOR = 5
|
||||
TINY = 3
|
||||
BUILD = nil
|
||||
|
||||
STRING = [ MAJOR, MINOR, TINY, BUILD ].compact.join( "." )
|
||||
|
||||
VERSION = ::SQLite3::VERSION
|
||||
end
|
||||
|
||||
def self.const_missing(name)
|
||||
return super unless name == :Version
|
||||
warn(<<-eowarn) if $VERBOSE
|
||||
#{caller[0]}: SQLite::Version will be removed in sqlite3-ruby version 2.0.0
|
||||
eowarn
|
||||
VersionProxy
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module SpeckleConnector
|
||||
module SpeckleObjects
|
||||
# @return [Array] available speckle object types.
|
||||
AVAILABLE_SPECKLE_OBJECTS = %w[
|
||||
Objects.Geometry.Point
|
||||
Objects.Geometry.Vector
|
||||
Objects.Geometry.Line
|
||||
Objects.Geometry.Polyline
|
||||
Objects.Geometry.Mesh
|
||||
Objects.Geometry.Brep
|
||||
Objects.Geometry.Box
|
||||
Objects.Geometry.Plane
|
||||
Objects.Other.BlockInstance
|
||||
Objects.Other.BlockDefinition
|
||||
Objects.Other.RenderMaterial
|
||||
Objects.Other.Transform
|
||||
Objects.Primitive.Interval
|
||||
].freeze
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,47 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../typescript/typescript_object'
|
||||
require_relative '../primitive/interval'
|
||||
require_relative '../geometry/plane'
|
||||
|
||||
module SpeckleConnector
|
||||
module SpeckleObjects
|
||||
module Geometry
|
||||
# BoundingBox object definition for Speckle.
|
||||
class BoundingBox < Typescript::TypescriptObject
|
||||
SPECKLE_TYPE = 'Objects.Geometry.Box'
|
||||
ATTRIBUTES = {
|
||||
speckle_type: String,
|
||||
units: String,
|
||||
area: Numeric,
|
||||
volume: Numeric,
|
||||
xSize: Primitive::Interval,
|
||||
ySize: Primitive::Interval,
|
||||
zSize: Primitive::Interval,
|
||||
basePlane: Geometry::Plane
|
||||
}.freeze
|
||||
|
||||
# @param bounds [Geom::BoundingBox] bounding box object of Sketchup.
|
||||
def self.from_bounds(bounds, units)
|
||||
min_pt = bounds.min
|
||||
BoundingBox.new(
|
||||
speckle_type: SPECKLE_TYPE,
|
||||
units: units,
|
||||
area: 0,
|
||||
volume: 0,
|
||||
xSize: Primitive::Interval.from_lengths(min_pt[0], bounds.width, units),
|
||||
ySize: Primitive::Interval.from_lengths(min_pt[1], bounds.height, units),
|
||||
zSize: Primitive::Interval.from_lengths(min_pt[2], bounds.depth, units),
|
||||
basePlane: Plane.origin(units)
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def attribute_types
|
||||
ATTRIBUTES
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module SpeckleConnector
|
||||
module SpeckleObjects
|
||||
# Geometric objects to convert speckle.
|
||||
module Geometry
|
||||
def self.length_to_speckle(length, units)
|
||||
length.__send__("to_#{SpeckleConnector::Converters::SKETCHUP_UNIT_STRINGS[units]}")
|
||||
end
|
||||
|
||||
def self.length_to_native(length, units)
|
||||
length.__send__(SpeckleConnector::Converters::SKETCHUP_UNIT_STRINGS[units])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,61 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'length'
|
||||
require_relative 'point'
|
||||
require_relative 'bounding_box'
|
||||
require_relative '../primitive/interval'
|
||||
require_relative '../../typescript/typescript_object'
|
||||
|
||||
module SpeckleConnector
|
||||
module SpeckleObjects
|
||||
module Geometry
|
||||
# Line object definition for Speckle.
|
||||
class Line < Typescript::TypescriptObject
|
||||
SPECKLE_TYPE = 'Objects.Geometry.Line'
|
||||
ATTRIBUTES = {
|
||||
speckle_type: String,
|
||||
applicationId: String,
|
||||
units: String,
|
||||
start: Geometry::Point,
|
||||
end: Geometry::Point,
|
||||
domain: Primitive::Interval,
|
||||
bbox: Geometry::BoundingBox
|
||||
}.freeze
|
||||
|
||||
# @param edge [Sketchup::Edge] edge to convert line.
|
||||
def self.from_edge(edge, units)
|
||||
start_vertex = edge.start.position
|
||||
end_vertex = edge.end.position
|
||||
Line.new(
|
||||
speckle_type: SPECKLE_TYPE,
|
||||
units: units,
|
||||
applicationId: edge.persistent_id.to_s,
|
||||
start: Geometry::Point.from_vertex(start_vertex, units),
|
||||
end: Geometry::Point.from_vertex(end_vertex, units),
|
||||
domain: Primitive::Interval.from_numeric(0, Float(edge.length), units),
|
||||
bbox: Geometry::BoundingBox.from_bounds(edge.bounds, units)
|
||||
)
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
def self.to_native(line, entities)
|
||||
if line.key?('value')
|
||||
values = line['value']
|
||||
points = values.each_slice(3).to_a.map { |pt| Point.to_native(pt[0], pt[1], pt[2], line['units']) }
|
||||
points.push(points[0]) if line['closed']
|
||||
entities.add_edges(*points)
|
||||
else
|
||||
start_pt = Point.to_native(line['start']['x'], line['start']['y'], line['start']['z'], line['units'])
|
||||
end_pt = Point.to_native(line['end']['x'], line['end']['y'], line['end']['z'], line['units'])
|
||||
entities.add_edges(start_pt, end_pt)
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
|
||||
def attribute_types
|
||||
ATTRIBUTES
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,124 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../geometry/bounding_box'
|
||||
require_relative '../other/render_material'
|
||||
require_relative '../../typescript/typescript_object'
|
||||
require_relative '../../convertors/clean_up'
|
||||
|
||||
module SpeckleConnector
|
||||
module SpeckleObjects
|
||||
# Geometry objects in the Speckleverse.
|
||||
module Geometry
|
||||
include Converters::CleanUp
|
||||
|
||||
# Mesh object definition for Speckle.
|
||||
class Mesh < Typescript::TypescriptObject
|
||||
SPECKLE_TYPE = 'Objects.Geometry.Mesh'
|
||||
ATTRIBUTES = {
|
||||
speckle_type: String,
|
||||
units: String,
|
||||
renderMaterial: [NilClass, Other::RenderMaterial],
|
||||
bbox: Geometry::BoundingBox,
|
||||
'@(31250)vertices': Array,
|
||||
'@(62500)faces': Array,
|
||||
'@(31250)faceEdgeFlags': Array
|
||||
}.freeze
|
||||
|
||||
def self.to_native(sketchup_model, mesh, entities)
|
||||
native_mesh = Geom::PolygonMesh.new(mesh['vertices'].count / 3)
|
||||
points = []
|
||||
mesh['vertices'].each_slice(3) do |pt|
|
||||
points.push(Point.to_native(pt[0], pt[1], pt[2], mesh['units']))
|
||||
end
|
||||
faces = mesh['faces']
|
||||
while faces.count > 0
|
||||
num_pts = faces.shift
|
||||
# 0 -> 3, 1 -> 4 to preserve backwards compatibility
|
||||
num_pts += 3 if num_pts < 3
|
||||
indices = faces.shift(num_pts)
|
||||
native_mesh.add_polygon(indices.map { |index| points[index] })
|
||||
end
|
||||
material = Other::RenderMaterial.to_native(sketchup_model, mesh['renderMaterial'])
|
||||
entities.add_faces_from_mesh(native_mesh, 4, material)
|
||||
merge_coplanar_faces(entities)
|
||||
native_mesh
|
||||
end
|
||||
|
||||
# @param face [Sketchup::Face] face to convert mesh
|
||||
def self.from_face(face, units)
|
||||
mesh = face.loops.count > 1 ? face.mesh : nil
|
||||
Mesh.new(
|
||||
speckle_type: SPECKLE_TYPE,
|
||||
units: units,
|
||||
renderMaterial: face.material.nil? ? nil : Other::RenderMaterial.from_material(face.material),
|
||||
bbox: Geometry::BoundingBox.from_bounds(face.bounds, units),
|
||||
'@(31250)vertices': mesh.nil? ? face_vertices_to_array(face, units) : mesh_points_to_array(mesh, units),
|
||||
'@(62500)faces': mesh.nil? ? face_indices_to_array(face, 0) : mesh_faces_to_array(mesh, -1),
|
||||
'@(31250)faceEdgeFlags': mesh.nil? ? face_edge_flags_to_array(face) : mesh_edge_flags_to_array(mesh)
|
||||
)
|
||||
end
|
||||
|
||||
# get a flat array of vertices from a list of sketchup vertices
|
||||
def self.face_vertices_to_array(face, units)
|
||||
pts_array = []
|
||||
face.vertices.each do |v|
|
||||
pt = v.position
|
||||
pts_array.push(Geometry.length_to_speckle(pt[0], units),
|
||||
Geometry.length_to_speckle(pt[1], units),
|
||||
Geometry.length_to_speckle(pt[2], units))
|
||||
end
|
||||
pts_array
|
||||
end
|
||||
|
||||
# get a flat array of vertices from a sketchup polygon mesh
|
||||
def self.mesh_points_to_array(mesh, units)
|
||||
pts_array = []
|
||||
mesh.points.each do |pt|
|
||||
pts_array.push(
|
||||
Geometry.length_to_speckle(pt[0], units),
|
||||
Geometry.length_to_speckle(pt[1], units),
|
||||
Geometry.length_to_speckle(pt[2], units)
|
||||
)
|
||||
end
|
||||
pts_array
|
||||
end
|
||||
|
||||
# get a flat array of face indices from a sketchup face
|
||||
def self.face_indices_to_array(face, offset)
|
||||
face_array = [face.vertices.count]
|
||||
face_array.push(*face.vertices.count.times.map { |index| index + offset })
|
||||
face_array
|
||||
end
|
||||
|
||||
# get an array of face indices from a sketchup polygon mesh
|
||||
def self.mesh_faces_to_array(mesh, offset = 0)
|
||||
faces = []
|
||||
mesh.polygons.each do |poly|
|
||||
faces.push(
|
||||
poly.count, *poly.map { |index| index.abs + offset }
|
||||
)
|
||||
end
|
||||
faces
|
||||
end
|
||||
|
||||
def self.face_edge_flags_to_array(face)
|
||||
face.outer_loop.edges.map(&:soft?)
|
||||
end
|
||||
|
||||
def self.mesh_edge_flags_to_array(mesh)
|
||||
edge_flags = []
|
||||
mesh.polygons.each do |poly|
|
||||
edge_flags.push(
|
||||
*poly.map(&:negative?)
|
||||
)
|
||||
end
|
||||
edge_flags
|
||||
end
|
||||
|
||||
def attribute_types
|
||||
ATTRIBUTES
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,54 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'point'
|
||||
require_relative 'vector'
|
||||
require_relative '../../typescript/typescript_object'
|
||||
|
||||
module SpeckleConnector
|
||||
module SpeckleObjects
|
||||
module Geometry
|
||||
# Plane object definition for Speckle.
|
||||
class Plane < Typescript::TypescriptObject
|
||||
SPECKLE_TYPE = 'Objects.Geometry.Plane'
|
||||
ATTRIBUTES = {
|
||||
speckle_type: String,
|
||||
units: String,
|
||||
xdir: Geometry::Vector,
|
||||
ydir: Geometry::Vector,
|
||||
normal: Geometry::Vector,
|
||||
origin: Geometry::Point
|
||||
}.freeze
|
||||
|
||||
# @param x_dir [Geometry::Vector] vector on the x direction
|
||||
# @param y_dir [Geometry::Vector] vector on the y direction
|
||||
# @param normal [Geometry::Vector] normal vector
|
||||
# @param origin [Geometry::Point] origin point
|
||||
# @param units [String] units of the Sketchup model
|
||||
def self.from_vectors(x_dir, y_dir, normal, origin, units)
|
||||
Plane.new(
|
||||
speckle_type: SPECKLE_TYPE,
|
||||
units: units,
|
||||
xdir: x_dir,
|
||||
ydir: y_dir,
|
||||
normal: normal,
|
||||
origin: origin
|
||||
)
|
||||
end
|
||||
|
||||
def self.origin(units)
|
||||
from_vectors(
|
||||
Vector.from_coordinates(1, 0, 0, units),
|
||||
Vector.from_coordinates(0, 1, 0, units),
|
||||
Vector.from_coordinates(0, 0, 1, units),
|
||||
Point.from_coordinates(0, 0, 0, units),
|
||||
units
|
||||
)
|
||||
end
|
||||
|
||||
def attribute_types
|
||||
ATTRIBUTES
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,54 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'length'
|
||||
require_relative '../../typescript/typescript_object'
|
||||
|
||||
module SpeckleConnector
|
||||
module SpeckleObjects
|
||||
module Geometry
|
||||
# Point object definition for Speckle.
|
||||
class Point < Typescript::TypescriptObject
|
||||
SPECKLE_TYPE = 'Objects.Geometry.Point'
|
||||
ATTRIBUTES = {
|
||||
speckle_type: String,
|
||||
units: String,
|
||||
x: Numeric,
|
||||
y: Numeric,
|
||||
z: Numeric
|
||||
}.freeze
|
||||
|
||||
def self.from_coordinates(x, y, z, units)
|
||||
Point.new(
|
||||
speckle_type: SPECKLE_TYPE,
|
||||
units: units,
|
||||
x: x,
|
||||
y: y,
|
||||
z: z
|
||||
)
|
||||
end
|
||||
|
||||
def self.from_vertex(vertex, units)
|
||||
Point.new(
|
||||
speckle_type: SPECKLE_TYPE,
|
||||
units: units,
|
||||
x: Geometry.length_to_speckle(vertex[0], units),
|
||||
y: Geometry.length_to_speckle(vertex[1], units),
|
||||
z: Geometry.length_to_speckle(vertex[2], units)
|
||||
)
|
||||
end
|
||||
|
||||
def self.to_native(x, y, z, units)
|
||||
Geom::Point3d.new(
|
||||
Geometry.length_to_native(x, units),
|
||||
Geometry.length_to_native(y, units),
|
||||
Geometry.length_to_native(z, units)
|
||||
)
|
||||
end
|
||||
|
||||
def attribute_types
|
||||
ATTRIBUTES
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../typescript/typescript_object'
|
||||
|
||||
module SpeckleConnector
|
||||
module SpeckleObjects
|
||||
module Geometry
|
||||
# Vector object definition for Speckle.
|
||||
class Vector < Typescript::TypescriptObject
|
||||
SPECKLE_TYPE = 'Objects.Geometry.Vector'
|
||||
ATTRIBUTES = {
|
||||
speckle_type: String,
|
||||
units: String,
|
||||
x: Numeric,
|
||||
y: Numeric,
|
||||
z: Numeric
|
||||
}.freeze
|
||||
|
||||
def self.from_coordinates(x, y, z, units)
|
||||
Vector.new(
|
||||
speckle_type: SPECKLE_TYPE,
|
||||
units: units,
|
||||
x: x,
|
||||
y: y,
|
||||
z: z
|
||||
)
|
||||
end
|
||||
|
||||
def attribute_types
|
||||
ATTRIBUTES
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,127 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'render_material'
|
||||
require_relative 'transform'
|
||||
require_relative '../geometry/point'
|
||||
require_relative '../geometry/mesh'
|
||||
require_relative '../geometry/bounding_box'
|
||||
require_relative '../../typescript/typescript_object'
|
||||
|
||||
module SpeckleConnector
|
||||
module SpeckleObjects
|
||||
module Other
|
||||
# BlockDefinition object definition for Speckle.
|
||||
class BlockDefinition < Typescript::TypescriptObject
|
||||
SPECKLE_TYPE = 'Objects.Other.BlockDefinition'
|
||||
ATTRIBUTE_TYPES = {
|
||||
speckle_type: String,
|
||||
units: String,
|
||||
applicationId: String,
|
||||
name: String,
|
||||
basePoint: Geometry::Point,
|
||||
'@geometry': Array
|
||||
}.freeze
|
||||
|
||||
# @param definition [Sketchup::ComponentDefinition] component definition might be belong to group or component
|
||||
# instance
|
||||
# @param units [String] units of the Sketchup model
|
||||
# @param definitions [Hash{String=>BlockDefinition}] all converted {BlockDefinition}s on the converter.
|
||||
def self.from_definition(definition, units, definitions)
|
||||
guid = definition.guid
|
||||
return definitions[guid] if definitions.key?(guid)
|
||||
|
||||
# FIXME: This part should be refactored.
|
||||
geometry = group_mesh_to_speckle(definition, units, definitions)
|
||||
|
||||
# FIXME: Previously as below, need to understand why
|
||||
# geometry = if definition.entities[0].is_a?(Sketchup::Edge) | definition.entities[0].is_a?(Sketchup::Face)
|
||||
# group_mesh_to_speckle(definition)
|
||||
# else
|
||||
# definition.entities.map { |entity| convert(entity) }
|
||||
# end
|
||||
|
||||
BlockDefinition.new(
|
||||
speckle_type: SPECKLE_TYPE,
|
||||
units: units,
|
||||
applicationId: guid,
|
||||
name: definition.name,
|
||||
basePoint: Geometry::Point.from_coordinates(0, 0, 0, units),
|
||||
'@geometry': geometry
|
||||
)
|
||||
end
|
||||
|
||||
# finds or creates a component definition from the geometry and the given name
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
def self.to_native(sketchup_model, geometry, name, application_id = '', &convert)
|
||||
definition = sketchup_model.definitions[name]
|
||||
return definition if definition && (definition.name == name || definition.guid == application_id)
|
||||
|
||||
definition&.entities&.clear!
|
||||
definition ||= sketchup_model.definitions.add(name)
|
||||
geometry.each { |obj| convert.call(obj, definition.entities) }
|
||||
puts("definition finished: #{name} (#{application_id})")
|
||||
# puts(" entity count: #{definition.entities.count}")
|
||||
definition
|
||||
end
|
||||
# rubocop:enable Metrics/CyclomaticComplexity
|
||||
|
||||
def self.group_mesh_to_speckle(definition, units, definitions)
|
||||
# {material_id => Mesh}
|
||||
mat_groups = {}
|
||||
nested_blocks = []
|
||||
lines = []
|
||||
|
||||
definition.entities.each do |entity|
|
||||
if entity.is_a?(Sketchup::ComponentInstance)
|
||||
nested_blocks.push(BlockInstance.from_component_instance(entity, units, definitions))
|
||||
end
|
||||
next unless entity.is_a?(Sketchup::Face)
|
||||
|
||||
group_meshes_by_material(definition, entity, mat_groups, units)
|
||||
end
|
||||
|
||||
mat_groups.values.map { |group| group.delete(:pt_count) }
|
||||
mat_groups.values + lines + nested_blocks
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
def self.group_meshes_by_material(definition, face, mat_groups, units)
|
||||
# convert material
|
||||
mat_id = face.material.nil? ? 'none' : face.material.entityID
|
||||
mat_groups[mat_id] = initialise_group_mesh(face, definition.bounds, units) unless mat_groups.key?(mat_id)
|
||||
mat_group = mat_groups[mat_id]
|
||||
if face.loops.size > 1
|
||||
mesh = face.mesh
|
||||
mat_group['@(31250)vertices'].push(*Geometry::Mesh.mesh_points_to_array(mesh, units))
|
||||
mat_group['@(62500)faces'].push(*Geometry::Mesh.mesh_faces_to_array(mesh, mat_group[:pt_count] - 1))
|
||||
mat_group['@(31250)faceEdgeFlags'].push(*Geometry::Mesh.mesh_edge_flags_to_array(mesh))
|
||||
else
|
||||
mat_group['@(31250)vertices'].push(*Geometry::Mesh.face_vertices_to_array(face, units))
|
||||
mat_group['@(62500)faces'].push(*Geometry::Mesh.face_indices_to_array(face, mat_group[:pt_count]))
|
||||
mat_group['@(31250)faceEdgeFlags'].push(*Geometry::Mesh.face_edge_flags_to_array(face))
|
||||
end
|
||||
mat_group[:pt_count] += face.vertices.count
|
||||
end
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
|
||||
def self.initialise_group_mesh(face, bounds, units)
|
||||
{
|
||||
speckle_type: 'Objects.Geometry.Mesh',
|
||||
units: @units,
|
||||
bbox: Geometry::BoundingBox.from_bounds(bounds, units),
|
||||
'@(31250)vertices' => [],
|
||||
'@(62500)faces' => [],
|
||||
'@(31250)faceEdgeFlags' => [],
|
||||
'@(31250)textureCoordinates' => [],
|
||||
pt_count: 0,
|
||||
renderMaterial: face.material.nil? ? nil : RenderMaterial.from_material(face.material)
|
||||
}
|
||||
end
|
||||
|
||||
def attribute_types
|
||||
ATTRIBUTE_TYPES
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,105 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'render_material'
|
||||
require_relative 'transform'
|
||||
require_relative 'block_definition'
|
||||
require_relative '../geometry/bounding_box'
|
||||
require_relative '../../typescript/typescript_object'
|
||||
|
||||
module SpeckleConnector
|
||||
module SpeckleObjects
|
||||
module Other
|
||||
# BlockInstance object definition for Speckle.
|
||||
class BlockInstance < Typescript::TypescriptObject
|
||||
SPECKLE_TYPE = 'Objects.Other.BlockInstance'
|
||||
ATTRIBUTE_TYPES = {
|
||||
speckle_type: String,
|
||||
units: String,
|
||||
applicationId: String,
|
||||
is_sketchup_group: [TrueClass, FalseClass],
|
||||
bbox: Geometry::BoundingBox,
|
||||
name: [String, NilClass],
|
||||
renderMaterial: [Other::RenderMaterial, NilClass],
|
||||
transform: Other::Transform,
|
||||
'@blockDefinition': Other::BlockDefinition
|
||||
}.freeze
|
||||
|
||||
# @param group [Sketchup::Group] group to convert Speckle BlockInstance
|
||||
def self.from_group(group, units, component_defs)
|
||||
BlockInstance.new(
|
||||
speckle_type: SPECKLE_TYPE,
|
||||
units: units,
|
||||
applicationId: group.guid,
|
||||
is_sketchup_group: true,
|
||||
bbox: Geometry::BoundingBox.from_bounds(group.bounds, units),
|
||||
name: group.name == '' ? nil : group.name,
|
||||
renderMaterial: group.material.nil? ? nil : RenderMaterial.from_material(group.material),
|
||||
transform: Other::Transform.from_transformation(group.transformation, units),
|
||||
'@blockDefinition': BlockDefinition.from_definition(group.definition, units, component_defs)
|
||||
)
|
||||
end
|
||||
|
||||
# @param component_instance [Sketchup::ComponentInstance] component instance to convert Speckle BlockInstance
|
||||
def self.from_component_instance(component_instance, units, component_defs)
|
||||
BlockInstance.new(
|
||||
speckle_type: SPECKLE_TYPE,
|
||||
units: units,
|
||||
applicationId: component_instance.guid,
|
||||
is_sketchup_group: false,
|
||||
bbox: Geometry::BoundingBox.from_bounds(component_instance.bounds, units),
|
||||
name: component_instance.name == '' ? nil : component_instance.name,
|
||||
renderMaterial: component_instance.material.nil? ? nil : RenderMaterial.from_material(group.material),
|
||||
transform: Other::Transform.from_transformation(component_instance.transformation, units),
|
||||
'@blockDefinition': BlockDefinition.from_definition(component_instance.definition, units, component_defs)
|
||||
)
|
||||
end
|
||||
|
||||
# Creates a component instance from a block
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
def self.to_native(sketchup_model, block, entities, &convert)
|
||||
# is_group = block.key?("is_sketchup_group") && block["is_sketchup_group"]
|
||||
# something about this conversion is freaking out if nested block geo is a group
|
||||
# so this is set to false always until I can figure this out
|
||||
is_group = false
|
||||
definition = BLOCK_DEFINITION.to_native(
|
||||
sketchup_model, block['blockDefinition']['geometry'], block['blockDefinition']['name'],
|
||||
block['blockDefinition']['applicationId'], convert
|
||||
)
|
||||
|
||||
name = block['name'].nil? || block['name'].empty? ? block['id'] : block['name']
|
||||
t_arr = block['transform'].is_a?(Hash) ? block['transform']['value'] : block['transform']
|
||||
transform = Other::Transform.to_native(t_arr, block['units'])
|
||||
instance =
|
||||
if is_group
|
||||
# rubocop:disable SketchupSuggestions/AddGroup
|
||||
entities.add_group(definition.entities.to_a)
|
||||
# rubocop:enable SketchupSuggestions/AddGroup
|
||||
else
|
||||
entities.add_instance(definition, transform)
|
||||
end
|
||||
# erase existing instances after creation and before rename because you can't have definitions
|
||||
# without instances
|
||||
find_and_erase_existing_instance(definition, name, block['applicationId'])
|
||||
puts("Failed to create instance for speckle block instance #{block['id']}") if instance.nil?
|
||||
instance.transformation = transform if is_group
|
||||
instance.material = Other::RenderMaterial.to_native(sketchup_model, block['renderMaterial'])
|
||||
instance.name = name
|
||||
instance
|
||||
end
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
|
||||
# takes a component definition and finds and erases the first instance with the matching name
|
||||
# (and optionally the applicationId)
|
||||
def self.find_and_erase_existing_instance(definition, name, app_id = '')
|
||||
definition.instances.find { |ins| ins.name == name || ins.guid == app_id }&.erase!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def attribute_types
|
||||
ATTRIBUTE_TYPES
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../typescript/typescript_object'
|
||||
|
||||
module SpeckleConnector
|
||||
module SpeckleObjects
|
||||
module Other
|
||||
# RenderMaterial object definition for Speckle.
|
||||
class RenderMaterial < Typescript::TypescriptObject
|
||||
SPECKLE_TYPE = 'Objects.Other.RenderMaterial'
|
||||
ATTRIBUTE_TYPES = {
|
||||
speckle_type: String,
|
||||
name: String,
|
||||
diffuse: Numeric,
|
||||
opacity: Numeric,
|
||||
emissive: Numeric,
|
||||
metalness: Numeric,
|
||||
roughness: Numeric
|
||||
}.freeze
|
||||
|
||||
def self.from_material(material)
|
||||
rgba = material.color.to_a
|
||||
RenderMaterial.new(
|
||||
speckle_type: SPECKLE_TYPE,
|
||||
name: material.name,
|
||||
diffuse: [rgba[3] << 24 | rgba[0] << 16 | rgba[1] << 8 | rgba[2]].pack('l').unpack1('l'),
|
||||
opacity: material.alpha,
|
||||
emissive: -16_777_216,
|
||||
metalness: 0,
|
||||
roughness: 1
|
||||
)
|
||||
end
|
||||
|
||||
def self.to_native(sketchup_model, render_material)
|
||||
return if render_material.nil?
|
||||
|
||||
# return material with same name if it exists
|
||||
name = render_material['name'] || render_material['id']
|
||||
material = sketchup_model.materials[name]
|
||||
return material if material
|
||||
|
||||
# create a new sketchup material
|
||||
material = sketchup_model.materials.add(name)
|
||||
material.alpha = render_material['opacity']
|
||||
argb = render_material['diffuse']
|
||||
material.color = Sketchup::Color.new((argb >> 16) & 255, (argb >> 8) & 255, argb & 255, (argb >> 24) & 255)
|
||||
material
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def attribute_types
|
||||
ATTRIBUTE_TYPES
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,53 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../typescript/typescript_object'
|
||||
|
||||
module SpeckleConnector
|
||||
module SpeckleObjects
|
||||
module Other
|
||||
# Transform object definition for Speckle.
|
||||
class Transform < Typescript::TypescriptObject
|
||||
SPECKLE_TYPE = 'Objects.Other.Transform'
|
||||
ATTRIBUTE_TYPES = {
|
||||
speckle_type: String,
|
||||
units: String,
|
||||
value: Array
|
||||
}.freeze
|
||||
|
||||
def self.from_transformation(transformation, units)
|
||||
t_arr = transformation.to_a
|
||||
Transform.new(
|
||||
speckle_type: SPECKLE_TYPE,
|
||||
units: units,
|
||||
value: [
|
||||
t_arr[0], t_arr[4], t_arr[8], Geometry.length_to_speckle(t_arr[12], units),
|
||||
t_arr[1], t_arr[5], t_arr[9], Geometry.length_to_speckle(t_arr[13], units),
|
||||
t_arr[2], t_arr[6], t_arr[10], Geometry.length_to_speckle(t_arr[14], units),
|
||||
t_arr[3], t_arr[7], t_arr[11], t_arr[15]
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
def self.to_native(t_arr, units)
|
||||
Geom::Transformation.new(
|
||||
[
|
||||
t_arr[0], t_arr[4], t_arr[8], t_arr[12],
|
||||
t_arr[1], t_arr[5], t_arr[9], t_arr[13],
|
||||
t_arr[2], t_arr[6], t_arr[10], t_arr[14],
|
||||
Geometry.length_to_native(t_arr[3], units),
|
||||
Geometry.length_to_native(t_arr[7], units),
|
||||
Geometry.length_to_native(t_arr[11], units),
|
||||
t_arr[15]
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def attribute_types
|
||||
ATTRIBUTE_TYPES
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../typescript/typescript_object'
|
||||
|
||||
module SpeckleConnector
|
||||
module SpeckleObjects
|
||||
module Primitive
|
||||
# Interval object definition for Speckle.
|
||||
class Interval < Typescript::TypescriptObject
|
||||
SPECKLE_TYPE = 'Objects.Primitive.Interval'
|
||||
ATTRIBUTES = {
|
||||
speckle_type: String,
|
||||
units: String,
|
||||
start: Numeric,
|
||||
end: Numeric
|
||||
}.freeze
|
||||
|
||||
def self.from_numeric(start_value, end_value, units)
|
||||
Interval.new(
|
||||
speckle_type: SPECKLE_TYPE,
|
||||
units: units,
|
||||
start: start_value,
|
||||
end: end_value
|
||||
)
|
||||
end
|
||||
|
||||
def self.from_lengths(length_1, length_2, units)
|
||||
start_value = Geometry.length_to_speckle(length_1, units)
|
||||
end_value = Geometry.length_to_speckle(length_2, units)
|
||||
from_numeric(start_value, end_value, units)
|
||||
end
|
||||
|
||||
def attribute_types
|
||||
ATTRIBUTES
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../immutable/immutable'
|
||||
|
||||
module SpeckleConnector
|
||||
module States
|
||||
# Sketchup model state holds information about sketchup related objects like model, layers, materials etc.
|
||||
class SketchupState
|
||||
include Immutable::ImmutableUtils
|
||||
|
||||
# @return [Sketchup::Model] active model on the sketchup
|
||||
attr_reader :sketchup_model
|
||||
|
||||
# @param sketchup_model [Sketchup::Model] active model on the sketchup
|
||||
def initialize(sketchup_model)
|
||||
@sketchup_model = sketchup_model
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -8,15 +8,19 @@ module SpeckleConnector
|
||||
class State < InitialState
|
||||
include Immutable::ImmutableUtils
|
||||
|
||||
# @return [States::SketchupState] the state of the Sketchup Application
|
||||
attr_reader :sketchup_state
|
||||
|
||||
# @return [States::SpeckleState] the states of the Speckle
|
||||
attr_reader :speckle_state
|
||||
|
||||
# @return [States::UserState] the user specific part of the states
|
||||
attr_reader :user_state
|
||||
|
||||
def initialize(user_state, speckle_state, is_connected)
|
||||
def initialize(user_state, speckle_state, sketchup_state, is_connected)
|
||||
@speckle_state = speckle_state
|
||||
@is_connected = is_connected
|
||||
@sketchup_state = sketchup_state
|
||||
super(user_state)
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'json'
|
||||
|
||||
module SpeckleConnector
|
||||
module Typescript
|
||||
# Object to help convert object attributes to JSON and by checking types.
|
||||
class TypescriptObject
|
||||
# @param attributes [Hash{Symbol=>Object}] attributes are given as key value pairs
|
||||
def initialize(**attributes)
|
||||
@attributes = attributes
|
||||
check_attributes
|
||||
end
|
||||
|
||||
# @return [String] the JSON representation of the object
|
||||
def to_json(*options)
|
||||
@attributes.to_json(*options)
|
||||
end
|
||||
|
||||
def to_h(*options)
|
||||
JSON.parse(to_json(*options), { symbolize_names: true })
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
def check_attributes
|
||||
attribute_types.each do |key, class_or_classes|
|
||||
value = @attributes[key]
|
||||
case class_or_classes
|
||||
when Array
|
||||
is_class_correct = class_or_classes.any? do |klass|
|
||||
raise "#{klass} is not a class" unless klass.is_a? Class
|
||||
|
||||
value.is_a? klass
|
||||
end
|
||||
raise "attribute #{key} is of class #{value.class} and not #{class_or_classes}" unless is_class_correct
|
||||
when Class
|
||||
raise ArgumentError, "#{class_or_classes} should be class" unless class_or_classes.is_a? Class
|
||||
|
||||
unless value.is_a? class_or_classes
|
||||
raise ArgumentError,
|
||||
"attribute #{key} is of class #{value.class} and not #{class_or_classes}"
|
||||
end
|
||||
else
|
||||
raise ArgumentError, "#{class_or_classes} should be class or array of classes"
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/CyclomaticComplexity
|
||||
|
||||
def attribute_types
|
||||
raise NotImplementedError, 'Implement in child class'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -74,7 +74,7 @@ module SpeckleConnector
|
||||
def init_dialog
|
||||
dialog = UI::HtmlDialog.new(@dialog_specs)
|
||||
File.exist?(@htm_file) ? dialog.set_file(@htm_file) : dialog.set_url('http://localhost:8081')
|
||||
# ui.set_url('http://localhost:8081') # uncomment this line if you want to use your local built version of ui
|
||||
# dialog.set_url('http://localhost:8081') # uncomment this line if you want to use your local version of ui
|
||||
add_exec_callback(dialog)
|
||||
dialog
|
||||
end
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'json'
|
||||
require_relative '../../../test_helper'
|
||||
require_relative '../../../../speckle_connector/src/speckle_objects/geometry/plane'
|
||||
|
||||
module SpeckleConnector
|
||||
module SpeckleObjects
|
||||
module Geometry
|
||||
class PlaneTest < Minitest::Test
|
||||
def setup
|
||||
# Do nothing
|
||||
end
|
||||
|
||||
def teardown
|
||||
# Do nothing
|
||||
end
|
||||
|
||||
def test_plane_to_json
|
||||
plane = Plane.origin('m')
|
||||
serialized_plane = {
|
||||
speckle_type: 'Objects.Geometry.Plane',
|
||||
units: 'm',
|
||||
xdir: {
|
||||
speckle_type: 'Objects.Geometry.Vector',
|
||||
units: 'm',
|
||||
x: 1.0,
|
||||
y: 0.0,
|
||||
z: 0.0
|
||||
},
|
||||
ydir: {
|
||||
speckle_type: 'Objects.Geometry.Vector',
|
||||
units: 'm',
|
||||
x: 0.0,
|
||||
y: 1.0,
|
||||
z: 0.0
|
||||
},
|
||||
normal: {
|
||||
speckle_type: 'Objects.Geometry.Vector',
|
||||
units: 'm',
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
z: 1.0
|
||||
},
|
||||
origin: {
|
||||
speckle_type: 'Objects.Geometry.Point',
|
||||
units: 'm',
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
z: 0.0
|
||||
}
|
||||
}
|
||||
|
||||
serialized = JSON.generate(plane)
|
||||
hash = JSON.parse(serialized, { symbolize_names: true })
|
||||
|
||||
assert_equal(serialized_plane, hash)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'json'
|
||||
require_relative '../../../test_helper'
|
||||
require_relative '../../../../speckle_connector/src/speckle_objects/geometry/point'
|
||||
|
||||
module SpeckleConnector
|
||||
module SpeckleObjects
|
||||
module Geometry
|
||||
class PointTest < Minitest::Test
|
||||
def setup
|
||||
# Do nothing
|
||||
end
|
||||
|
||||
def teardown
|
||||
# Do nothing
|
||||
end
|
||||
|
||||
def test_point_to_json
|
||||
point = Point.from_coordinates(1.0, 1.0, 1.0, 'm')
|
||||
serialized_point = {
|
||||
speckle_type: 'Objects.Geometry.Point',
|
||||
units: 'm',
|
||||
x: 1.0,
|
||||
y: 1.0,
|
||||
z: 1.0
|
||||
}
|
||||
serialized = JSON.generate(point)
|
||||
hash = JSON.parse(serialized, { symbolize_names: true })
|
||||
|
||||
assert_equal(serialized_point, hash)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'json'
|
||||
require_relative '../../../test_helper'
|
||||
require_relative '../../../../speckle_connector/src/speckle_objects/geometry/vector'
|
||||
|
||||
module SpeckleConnector
|
||||
module SpeckleObjects
|
||||
module Geometry
|
||||
class VectorTest < Minitest::Test
|
||||
def setup
|
||||
# Do nothing
|
||||
end
|
||||
|
||||
def teardown
|
||||
# Do nothing
|
||||
end
|
||||
|
||||
def test_vector_to_json
|
||||
point = Vector.from_coordinates(1.0, 1.0, 1.0, 'm')
|
||||
serialized_point = {
|
||||
speckle_type: 'Objects.Geometry.Vector',
|
||||
units: 'm',
|
||||
x: 1.0,
|
||||
y: 1.0,
|
||||
z: 1.0
|
||||
}
|
||||
serialized = JSON.generate(point)
|
||||
hash = JSON.parse(serialized, { symbolize_names: true })
|
||||
|
||||
assert_equal(serialized_point, hash)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Require this file in all tests
|
||||
require 'simplecov'
|
||||
SimpleCov.start
|
||||
|
||||
require 'minitest/autorun'
|
||||
Reference in New Issue
Block a user