Compare commits

...

9 Commits

Author SHA1 Message Date
oguzhankoral e6dd630caf Feat (sqlite3): compile sqlite3 from c as sketchup compatible
Feat (sqlite3): compile sqlite3 from c as sketchup compatible
2022-11-24 03:14:34 +03:00
oguzhankoral cad14b318a Use native sqlite3 module for queries 2022-11-24 03:04:34 +03:00
oguzhankoral 4df1cc17bf Add switch for operating system 2022-11-24 03:04:09 +03:00
oguzhankoral 884bb331b3 Add compiled sqlite3_27.so file for windows 2022-11-24 03:03:29 +03:00
oguzhankoral ff4a83af47 Add ruby version number constant 2022-11-24 03:02:58 +03:00
oguzhankoral f6f323b307 Remove externally compiled files, was workaround 2022-11-24 03:02:24 +03:00
oguzhankoral 330280d611 Implement conversion methods from C/C++ to Ruby for our connector 2022-11-24 03:01:30 +03:00
oguzhankoral 809432cbbd Feat (Converters): refactor and document converters
ToSpeckle and ToNative modules are refactored with classes that parallels to objects in the Speckle.

 Length
 Point
 Vector
 Plane
 BoundingBox
 Plane
 Mesh
 Transform
 RenderMaterial
 BlockDefinition
 BlockInstance
 Interval
2022-11-23 16:37:12 +03:00
oguzhankoral d64fee1d15 Refactor ToNative methods 2022-11-23 16:31:19 +03:00
28 changed files with 480 additions and 2339 deletions
+5 -1
View File
@@ -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
View File
@@ -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()
+3 -9
View File
@@ -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,13 +17,8 @@ module SpeckleConnector
)
end
db = SQLite3::Database.new(db_path)
# FIXME: It's workaround, throws error when queried from database
begin
rows = db.execute('SELECT * FROM objects')
rescue StandardError
rows = db.execute('SELECT * FROM objects')
end
db = Sqlite3::Database.new(db_path)
rows = db.exec('SELECT * FROM objects')
db.close
rows.map { |row| JSON.parse(row[1]) }
end
@@ -18,7 +18,11 @@ module SpeckleConnector
# @return [States::State] the new updated state object
def update_state(state)
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
@@ -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
+63 -336
View File
@@ -1,21 +1,56 @@
# frozen_string_literal: true
require_relative 'converter'
# rubocop:disable Metrics/MethodLength
# rubocop:disable Metrics/PerceivedComplexity
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/ClassLength
# rubocop:disable SketchupSuggestions/AddGroup
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'
module SpeckleConnector
module Converters
# 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)
@@ -33,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/ClassLength
# rubocop:enable SketchupSuggestions/AddGroup
+10 -12
View File
@@ -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
@@ -7,6 +7,10 @@ module SpeckleConnector
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
@@ -37,6 +37,21 @@ module SpeckleConnector
)
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
@@ -3,10 +3,14 @@
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'
@@ -20,6 +24,26 @@ module SpeckleConnector
'@(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
@@ -37,6 +37,14 @@ module SpeckleConnector
)
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
@@ -50,6 +50,21 @@ module SpeckleConnector
)
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 = {}
@@ -54,6 +54,46 @@ module SpeckleConnector
)
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
@@ -31,6 +31,22 @@ module SpeckleConnector
)
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
@@ -28,6 +28,20 @@ module SpeckleConnector
)
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
@@ -17,7 +17,7 @@ module SpeckleConnector
end
def test_point_to_json
point = Point.new(1.0, 1.0, 1.0, 'm')
point = Point.from_coordinates(1.0, 1.0, 1.0, 'm')
serialized_point = {
speckle_type: 'Objects.Geometry.Point',
units: 'm',
@@ -17,7 +17,7 @@ module SpeckleConnector
end
def test_vector_to_json
point = Vector.new(1.0, 1.0, 1.0, 'm')
point = Vector.from_coordinates(1.0, 1.0, 1.0, 'm')
serialized_point = {
speckle_type: 'Objects.Geometry.Vector',
units: 'm',