diff --git a/lib/squib/api/shapes.rb b/lib/squib/api/shapes.rb index 4c2d45b..cbdca9b 100644 --- a/lib/squib/api/shapes.rb +++ b/lib/squib/api/shapes.rb @@ -1,3 +1,6 @@ +require 'squib/args/box' +require 'squib/args/draw' + module Squib class Deck @@ -23,13 +26,12 @@ module Squib # @return [nil] intended to be void # @api public def rect(opts = {}) - opts = needs(opts, [:range, :x, :y, :width, :height, :rect_radius, :x_radius, :y_radius, + opts = needs(opts, [:range, :rect_radius, :x_radius, :y_radius, :fill_color, :stroke_color, :stroke_width, :layout]) + box = Args::Box.new(self).load!(opts, expand_by: size, layout: layout, dpi: dpi) + draw = Args::Draw.new.load!(opts, expand_by: size, layout: layout, dpi: dpi) opts[:range].each do |i| - @cards[i].rect(opts[:x][i], opts[:y][i], opts[:width][i], opts[:height][i], - opts[:x_radius][i], opts[:y_radius][i], - opts[:fill_color][i], opts[:stroke_color][i], - opts[:stroke_width][i]) + @cards[i].rect(box[i], draw[i]) end end diff --git a/lib/squib/args/arg_loader.rb b/lib/squib/args/arg_loader.rb new file mode 100644 index 0000000..9954f5f --- /dev/null +++ b/lib/squib/args/arg_loader.rb @@ -0,0 +1,135 @@ +require 'squib/constants' +require 'squib/conf' + +module Squib + # @api private + module Args + + # Intended to be used a a mix-in, + # For example use see Box as an example + module ArgLoader + + # Main class invoked by the client (i.e. api/ methods) + def load!(args, expand_by: 1, layout: {}, dpi: 300) + set_attributes(args: args) + expand(by: expand_by) + layout_args = prep_layout_args(args[:layout], expand_by: expand_by) + defaultify(layout_args: layout_args || [], layout: layout) + validate + convert_units + self + end + + # Iterate over the args hash and create instance-level attributes for + # each parameter + # Assumes we have a hash of parameters to their default keys in the class + def set_attributes(args: args) + attributes = self.class.parameters.keys + attributes.each do |p| + instance_variable_set "@#{p}", args[p] # often nil, but ok + end + self.class.class_eval { attr_reader *(attributes) } + end + + # Conduct singleton expansion + # If expanding-parameter is not already responding to + # :each then copy it into an array + # + # Assumes we have an self.expanding_parameters + def expand(by: 1) + exp_params = self.class.expanding_parameters + exp_params.each do |p| + attribute = "@#{p}" + arg = instance_variable_get(attribute) + unless arg.respond_to? :each + instance_variable_set attribute, [arg] * by #expand singleton + end + end + end + + # Do singleton expansion on the layout argument as well + # Treated differently since layout is not always specified + def prep_layout_args(layout_args, expand_by: 1) + unless layout_args.respond_to?(:each) + layout_args = [layout_args] * expand_by + end + layout_args || [] + end + + # Go over each argument and fill it in with layout and defaults wherever nil + def defaultify(layout_args: [], layout: {}) + self.class.parameters.each do |param, default| + attribute = "@#{param}" + val = instance_variable_get(attribute) + if val.respond_to? :each + new_val = val.map.with_index do |v, i| + v ||= layout[layout_args[i]][param] unless layout_args[i].nil? + v ||= default + end + instance_variable_set(attribute, new_val) + else # a non-expanded singleton + # TODO handle this case + end + end + end + + # For each parameter/attribute foo we try to invoke a validate_foo + def validate + self.class.parameters.each do |param, default| + method = "validate_#{param}" + if self.respond_to? method + attribute = "@#{param}" + val = instance_variable_get(attribute) + if val.respond_to? :each + new_val = val.map.with_index{ |v, i| send(method, v, i) } + instance_variable_set(attribute, new_val) + else + instance_variable_set(attribute,send(method, val)) + end + end + end + end + + # Access an individual arg for a given card + # @return an OpenStruct that looks just like the mixed-in class + # @api private + def [](i) + card_arg = OpenStruct.new + self.class.expanding_parameters.each do |p| + p_val = instance_variable_get("@#{p}") + card_arg[p] = p_val[i] + end + card_arg + end + + # Convert units + def convert_units(dpi: 300) + self.class.params_with_units.each do |p| + p_str = "@#{p}" + p_val = instance_variable_get(p_str) + if p_val.respond_to? :each + arr = p_val.map { |x| convert_unit(x, dpi) } + instance_variable_set p_str, arr + else + instance_variable_set p_str, convert_unit(p_val, dpi) + end + end + self + end + + def convert_unit(arg, dpi) + case arg.to_s.rstrip + when /in$/ #ends with "in" + arg.rstrip[0..-2].to_f * dpi + when /cm$/ #ends with "cm" + arg.rstrip[0..-2].to_f * dpi * INCHES_IN_CM + else + arg + end + end + module_function :convert_unit + + end + + end +end \ No newline at end of file diff --git a/lib/squib/args/box.rb b/lib/squib/args/box.rb new file mode 100644 index 0000000..8402dbe --- /dev/null +++ b/lib/squib/args/box.rb @@ -0,0 +1,54 @@ +require 'squib/args/arg_loader' + +module Squib + # @api private + module Args + + class Box + include ArgLoader + + def initialize(deck = nil) + @deck = deck + end + + def self.parameters + { x: 0, y: 0, + width: :native, height: :native, + radius: nil, x_radius: 0, y_radius: 0 + } + end + + def self.expanding_parameters + parameters.keys # all of them + end + + def self.params_with_units + parameters.keys # all of them + end + + def validate_width(arg, _i) + return arg if @deck.nil? + return @deck.width if arg == :native + arg + end + + def validate_height(arg, _i) + return arg if @deck.nil? + return @deck.height if arg == :native + arg + end + + def validate_x_radius(arg, i) + return radius[i] unless radius[i].nil? + arg + end + + def validate_y_radius(arg, i) + return radius[i] unless radius[i].nil? + arg + end + + end + + end +end \ No newline at end of file diff --git a/lib/squib/args/draw.rb b/lib/squib/args/draw.rb new file mode 100644 index 0000000..3d48cae --- /dev/null +++ b/lib/squib/args/draw.rb @@ -0,0 +1,25 @@ +require 'squib/args/arg_loader' + +module Squib + # @api private + module Args + + class Draw + include ArgLoader + + def self.parameters + { fill_color: '#0000', stroke_color: :black, stroke_width: 2.0 } + end + + def self.expanding_parameters + parameters.keys # all of them are expandable + end + + def self.params_with_units + [:stroke_width] + end + + end + + end +end \ No newline at end of file diff --git a/lib/squib/deck.rb b/lib/squib/deck.rb index 926d659..76c1bf9 100644 --- a/lib/squib/deck.rb +++ b/lib/squib/deck.rb @@ -35,7 +35,11 @@ module Squib :img_dir, :prefix, :text_hint, :typographer # :nodoc: # @api private - attr_reader :layout, :conf + attr_reader :layout, :conf, :dpi + + # + # deck.size is really just @cards.size + def_delegators :cards, :size # Squib's constructor that sets the immutable properties. # diff --git a/lib/squib/graphics/shapes.rb b/lib/squib/graphics/shapes.rb index 2ab485b..26f4373 100644 --- a/lib/squib/graphics/shapes.rb +++ b/lib/squib/graphics/shapes.rb @@ -4,12 +4,10 @@ module Squib # :nodoc: # @api private - def rect(x, y, width, height, x_radius, y_radius, fill_color, stroke_color, stroke_width) - width = @width if width == :native - height = @height if height == :native + def rect(box, draw) use_cairo do |cc| - cc.rounded_rectangle(x, y, width, height, x_radius, y_radius) - cc.fill_n_stroke(fill_color, stroke_color, stroke_width) + cc.rounded_rectangle(box.x, box.y, box.width, box.height, box.x_radius, box.y_radius) + cc.fill_n_stroke(draw.fill_color, draw.stroke_color, draw.stroke_width) end end diff --git a/spec/args/box_spec.rb b/spec/args/box_spec.rb new file mode 100644 index 0000000..5388185 --- /dev/null +++ b/spec/args/box_spec.rb @@ -0,0 +1,108 @@ +require 'spec_helper' +require 'squib/args/box' + +describe Squib::Args::Box do + subject(:box) { Squib::Args::Box.new } + let(:expected_defaults) { {x: [0], y: [0], width: [:native], height: [:native] } } + + it 'intitially has no params set' do + expect(box).not_to respond_to(:x, :y, :width, :height) + end + + it 'extracts the defaults from Box on an empty hash' do + box.load!({}) + expect(box).to have_attributes(expected_defaults) + end + + it 'extracts what is specified and fills in defaults from Box' do + box.load!(x: 4, width: 40) + expect(box).to have_attributes(x: [4], width: [40], y: [0], height: [:native]) + end + + it 'extracts the defaults from Box on an empty hash' do + box.load!({foo: :bar}) + expect(box).to have_attributes(expected_defaults) + expect(box).not_to respond_to(:foo) + end + + context 'single expansion' do + let(:args) { {x: [1, 2], y: 3} } + before(:each) { box.load!(args, expand_by: 2) } + it 'expands box' do + expect(box).to have_attributes({ + x: [1, 2], + y: [3, 3], + height: [:native, :native], + width: [:native, :native] + }) + end + + it 'gives access to each card too' do + expect(box[0]).to have_attributes({ + x: 1, + y: 3, + height: :native, + width: :native + }) + end + end + + context 'layouts' do + let(:layout) do + { 'attack' => { x: 50 }, + 'defend' => { x: 60 } } + end + + it 'are used when not specified' do + args = { layout: ['attack', 'defend'] } + box.load!(args, expand_by: 2, layout: layout) + expect(box).to have_attributes( + x: [50, 60], # set by layout + y: [0, 0], # Box default + ) + end + + it 'handle single expansion' do + args = { layout: 'attack' } + box.load!(args, expand_by: 2, layout: layout) + expect(box).to have_attributes( + x: [50, 50], # set by layout + y: [0, 0], # Box default + ) + end + end + + context 'unit conversion' do + + it 'converts units on all args' do + args = {x: ['1in', '2in'], y: 300, width: '1in', height: '1in'} + box.load!(args, expand_by: 2) + expect(box).to have_attributes( + x: [300, 600], + y: [300, 300], + width: [300, 300], + height: [300, 300], + ) + end + + end + + context 'validation' do + it 'replaces with deck width and height' do + args = {width: :native, height: :native} + deck = OpenStruct.new(width: 123, height: 456) + box = Squib::Args::Box.new(deck) + box.load!(args, expand_by: 1) + expect(box).to have_attributes(width: [123], height: [456]) + end + + it 'has radius override x_radius and y_radius' do + args = {x_radius: 1, y_radius: 2, radius: 3} + box.load!(args, expand_by: 2) + expect(box).to have_attributes(x_radius: [3, 3], y_radius: [3, 3]) + end + + end + + +end \ No newline at end of file diff --git a/spec/graphics/graphics_shapes_spec.rb b/spec/graphics/graphics_shapes_spec.rb index c0fa4c5..5f8a742 100644 --- a/spec/graphics/graphics_shapes_spec.rb +++ b/spec/graphics/graphics_shapes_spec.rb @@ -32,9 +32,10 @@ describe Squib::Card do expect(cxt).to receive(:restore).once card = Squib::Card.new(deck, 100, 150) - # rect(x, y, width, height, x_radius, y_radius, - # fill_color, stroke_color, stroke_width) - card.rect(37, 38, 50, 100, 10, 15, '#fff', '#f00', 2.0) + # rect(Args::Box, x_radius, y_radius, Args::Draw) + box = OpenStruct.new(x: 37, y: 38, width: 50, height: 100, x_radius: 10, y_radius: 15) + draw = OpenStruct.new(fill_color: '#fff', stroke_color: '#f00', stroke_width: 2.0) + card.rect(box, draw) end end