diff --git a/shoes-core/lib/shoes/arc.rb b/shoes-core/lib/shoes/arc.rb index bb76a08d1..e61fd9d90 100644 --- a/shoes-core/lib/shoes/arc.rb +++ b/shoes-core/lib/shoes/arc.rb @@ -11,6 +11,9 @@ class Shoes # the arc itself! # @note The angle passed in is measured in Radians starting at 90 degrees or the 3 o'clock position. class Arc < Common::ArtElement + include Math + # angle is the gradient angle used across all art elements + # angle1/2 are the angles of the arc itself! style_with :angle1, :angle2, :art_styles, :center, :common_styles, :dimensions, :radius, :wedge STYLES = { wedge: false, fill: Shoes::COLORS[:black] }.freeze @@ -55,5 +58,247 @@ def center_point=(point) self.top = point.y - (height * 0.5).to_i end end + + def radius_x + @radius_x ||= width.to_f / 2 + end + + def radius_y + @radius_y ||= height.to_f / 2 + end + + def middle_x + @middle_x ||= left + radius_x + end + + def middle_y + @middle_y ||= top + radius_y + end + + def oval_axis_fraction(axis, input, difference = 0) + (((input - send("middle_#{axis}"))**2).to_f / ((send("radius_#{axis}") - difference)**2)) + end + + def inner_oval_axis_fraction(axis, input) + oval_axis_fraction(axis, input, inner_oval_difference) + end + + def inside_oval?(x, y) + # The following equation (https://math.stackexchange.com/questions/76457/check-if-a-point-is-within-an-ellipse) + # + # (x - h)**2 (y - k)**2 + # ------- + ------- <= 1 + # Rx ** 2 Ry ** 2 + # + # The above equation determines if a pair of coordinates are located in an oval. + # Rx = Radius X; Ry = Radius Y; h,k is the origin; x,y are the input coordinate + (oval_axis_fraction(:x, x) + oval_axis_fraction(:y, y)) <= 1 + end + + def inside_inner_oval?(x, y) + # For use when style[:fill] is nil, meaning it is just the edge of the arc + # This uses shared code with #inside_oval? to check if a point is also inside a second smaller oval. + # The smaller oval signifies the no_fill area of the arc + x_side = inner_oval_axis_fraction(:x, x) + y_side = inner_oval_axis_fraction(:y, y) + x_side + y_side <= 1 + end + + def normalize_angle(input_angle) + # This fixes angles > 6.283185 and < 0 + input_angle % (Math::PI * 2) + end + + def adjust_angle(input_angle) + # Must pad angle, since angles in standard formulas start at different location than in shoes + adjusted_angle = input_angle + 1.5708 + + # Must make angle 0..6.28318 + normalize_angle(adjusted_angle) + end + + def y_adjust_negative?(input_angle) + in_first_quater = (0..Shoes::HALF_PI).cover?(input_angle) + in_last_quarter = ((Shoes::HALF_PI * 3)..Shoes::TWO_PI).cover?(input_angle) + in_first_quater || in_last_quarter + end + + def x_adjust_positive?(input_angle) + (0..Math::PI).cover?(input_angle) + end + + def y_result_adjustment(input_angle, y_result) + # This checks which way from center the angle is + if y_adjust_negative?(input_angle) + middle_y - y_result + else + middle_y + y_result + end + end + + def x_result_adjustment(input_angle, x_result) + # This checks which way from center the angle is + if x_adjust_positive?(input_angle) + middle_x + x_result + else + middle_x - x_result + end + end + + def generate_coordinates(input_angle, x_result, y_result) + # This turns results on distance from center into actual coordinates + x_result = x_result_adjustment(input_angle, x_result).round(3) + y_result = y_result_adjustment(input_angle, y_result).round(3) + + { + x_value: x_result, + y_value: y_result + } + end + + def tangent_squared(input_angle) + tan(input_angle)**2 + end + + def angle_base_coords(given_angle) + # First the angle must be rotated to work with shoes, also be normalized into the 0..2PI range + modded_angle = adjust_angle(given_angle) + + # https://math.stackexchange.com/questions/22064/calculating-a-point-that-lies-on-an-ellipse-given-an-angle + # The above link was used in creating the below formula to find the point values from the center + + top_of_equation = (radius_x * radius_y) + + x_result = top_of_equation / (((radius_y**2) + ((radius_x**2) / tangent_squared(modded_angle)))**0.5) + y_result = top_of_equation / (((radius_x**2) + ((radius_y**2) * tangent_squared(modded_angle)))**0.5) + + # With distance from center for x and y found, we turn that into coordinates: + generate_coordinates(modded_angle, x_result, y_result) + end + + # The following angle coordinate methods are to simply clean up code + def angle1_coordinates + @angle1_coordinates ||= angle_base_coords(angle1) + end + + def angle2_coordinates + @angle2_coordinates ||= angle_base_coords(angle2) + end + + def angle1_x + angle1_coordinates[:x_value] + end + + def angle1_y + angle1_coordinates[:y_value] + end + + def angle2_x + angle2_coordinates[:x_value] + end + + def angle2_y + angle2_coordinates[:y_value] + end + + def slope_of_angles + # slope = (y2 - y1) / (x2 - x1) + (angle2_y - angle1_y) / (angle2_x - angle1_x) + end + + def b_value_for_line + # SINCE y = mx + b + # THEN b = y - mx + mx_value = (angle1_x * slope_of_angles) + + # If value of mx is Infinity, the line is horizontal, just return angle1_y + if mx_value == Float::INFINITY + angle1_y + else + angle1_y - mx_value + end + end + + def vertical_check(x_input) + # Since line is vertical, we can just compare x values and map to "above" or "below" + if angle1_x == x_input + :on + elsif angle1_x < x_input + :above + else + :below + end + end + + def normal_above_below_check(mx_value, y_input) + # SINCE y = mx + b + # We solve for where the y value is for the line between the arc's angles given the slope * input x + y_value_arc_line = mx_value + b_value_for_line + + if y_value_arc_line == y_input + # If input y is same as y on the arc line ...input is on the line + :on + elsif y_value_arc_line > y_input + # If input y is more than y on the arc line, point is above the line + :above + else + # If input y is less than y on the arc line, point is below the line + :below + end + end + + def above_below_on(x_input, y_input) + # SINCE y = mx + b + # We get mx_value of the line between arc points at the input of x + mx_value = (x_input * slope_of_angles) + + if mx_value.abs > 1_000_000.00 + # If line is straight up and down, compare with x value to an x coordinate + vertical_check(x_input) + else + # If standard slope...find what the y value between arc points would be given the input x + normal_above_below_check(mx_value, y_input) + end + end + + def angle1_x_smaller_check(x, y) + # if angle 1 is smaller, point should be above + above_below_on(x, y) == :above && angle1_x < angle2_x + end + + def angle2_x_smaller_check(x, y) + # if angle 2 is smaller, point should be below + above_below_on(x, y) == :below && angle1_x > angle2_x + end + + def on_shaded_part?(x, y) + # This checks if point is on the correct side of the oval, based on the lines between the arc points + angle1_x_smaller_check(x, y) || angle2_x_smaller_check(x, y) + end + + def standard_arc_bounds_check?(x, y) + # Check if it is in the oval, and then if on correct side of the line between points + inside_oval?(x, y) && on_shaded_part?(x, y) + end + + def inner_oval_difference + # this calculates how much smaller the inner oval must be to test a no_fill oval + @inner_oval_difference ||= calculate_inner_oval_difference + end + + def calculate_inner_oval_difference + [style[:strokewidth].to_f, 3.0].max + end + + def in_bounds?(x, y) + bounds_check = standard_arc_bounds_check?(x, y) + + if bounds_check && !style[:fill] + # If no fill, then it is just the edge and needs a further check + bounds_check = !inside_inner_oval?(x, y) + end + + bounds_check + end end end diff --git a/shoes-core/lib/shoes/oval.rb b/shoes-core/lib/shoes/oval.rb index f83a5721d..c3bab9aec 100644 --- a/shoes-core/lib/shoes/oval.rb +++ b/shoes-core/lib/shoes/oval.rb @@ -14,7 +14,7 @@ def create_dimensions(left, top, width, height) @dimensions = AbsoluteDimensions.new left, top, width, height, @style end - # Check out http://math.stackexchange.com/questions/60070/checking-whether-a-point-lies-on-a-wide-line-segment + # Check out https://math.stackexchange.com/questions/76457/check-if-a-point-is-within-an-ellipse # for explanations how the algorithm works def in_bounds?(x, y) radius_x = width.to_f / 2 diff --git a/shoes-core/spec/shoes/arc_spec.rb b/shoes-core/spec/shoes/arc_spec.rb index df7446ae1..0e9b674d6 100644 --- a/shoes-core/spec/shoes/arc_spec.rb +++ b/shoes-core/spec/shoes/arc_spec.rb @@ -45,6 +45,7 @@ subject(:arc) { Shoes::Arc.new(app, parent, left, top, width, height, start_angle, end_angle, wedge: true) } its(:wedge) { should eq(true) } + its(:wedge?) { should eq(true) } end describe "center_point" do @@ -235,4 +236,577 @@ expect { dsl.arc 10, 20, 30, 40, Shoes::PI, Shoes::TWO_PI, 666, left: -1 }.to raise_error(ArgumentError) end end + + describe 'in_bounds methods' do + subject(:arc) { Shoes::Arc.new(app, parent, left, top, width, height, 0, Shoes::HALF_PI) } + + describe '#radius_x' do + it 'must return half the width' do + expect(arc.radius_x).to eq(width.to_f / 2) + end + end + + describe '#radius_y' do + it 'must return half the height' do + expect(arc.radius_y).to eq(height.to_f / 2) + end + end + + describe '#middle_x' do + it 'must return the left plus radius_x' do + expect(arc.middle_x).to eq(113.0) + end + end + + describe '#middle_y' do + it 'must return the top plus radius_y' do + expect(arc.middle_y).to eq(194.0) + end + end + + describe '#inside_oval?' do + context 'when point is inside' do + it 'must return true' do + expect(arc.inside_oval?(arc.middle_x, arc.middle_y)).to eq(true) + end + end + + context 'when point is outside' do + it 'must return false' do + expect(arc.inside_oval?(arc.top, arc.left)).to eq(false) + end + end + end + + describe '#adjust_angle' do + it 'must rotate the angle 1.5708 radians (90 degrees)' do + expect(arc.adjust_angle(1.5708)).to eq(3.1416) + end + end + + describe '#y_adjust_negative?' do + context 'when angle is between 0 and 1.5708' do + it 'must return true' do + expect(arc.y_adjust_negative?(1)).to eq(true) + end + end + + context 'when angle is between 1.5708 and 4.71239' do + it 'must return false' do + expect(arc.y_adjust_negative?(2)).to eq(false) + end + end + + context 'when angle is between 4.71239 and 6.28319' do + it 'must return true' do + expect(arc.y_adjust_negative?(5)).to eq(true) + end + end + end + + describe '#x_adjust_positive?' do + context 'when angle is between 0 and 3.14159' do + it 'must return true' do + expect(arc.y_adjust_negative?(1)).to eq(true) + end + end + + context 'when angle is above 3.14159' do + it 'must return false' do + expect(arc.y_adjust_negative?(2)).to eq(false) + end + end + end + + describe '#y_result_adjustment' do + context 'when #y_adjust_negative? is false' do + before(:each) { allow(arc).to receive(:y_adjust_negative?) { false } } + + it 'should add y_result to middle_y' do + expect(arc.y_result_adjustment(1, 1)).to eq(195.0) + end + end + + context 'when #y_adjust_negative? is true' do + before(:each) { allow(arc).to receive(:y_adjust_negative?) { true } } + + it 'should subtract y_result from middle_y' do + expect(arc.y_result_adjustment(1, 1)).to eq(193.0) + end + end + end + + describe '#x_result_adjustment' do + context 'when #x_adjust_positive? is false' do + before(:each) { allow(arc).to receive(:x_adjust_positive?) { false } } + + it 'should subtract y_result from middle_y' do + expect(arc.x_result_adjustment(1, 1)).to eq(112.0) + end + end + + context 'when #x_adjust_positive? is true' do + before(:each) { allow(arc).to receive(:x_adjust_positive?) { true } } + + it 'should add y_result from middle_y' do + expect(arc.x_result_adjustment(1, 1)).to eq(114.0) + end + end + end + + describe '#generate_coordinates' do + it 'must output a hash of the adjusted x and y coordinates' do + allow(arc).to receive(:x_result_adjustment) { 5.0035 } + allow(arc).to receive(:y_result_adjustment) { 7.0034 } + expect(arc.generate_coordinates(0, 50, 50)).to eq(x_value: 5.004, y_value: 7.003) + end + end + + describe '#angle_base_coords' do + it 'must find the coordinates for a given angle' do + expect(arc.angle_base_coords(1.5708)).to eq(x_value: 112.999, y_value: 344.0) + end + end + + describe '#angle1_coordinates' do + it 'must call angle_base_coords' do + allow(arc).to receive(:angle_base_coords) { |e| e } + expect(arc.angle1_coordinates).to eq(arc.angle1) + end + end + + describe '#angle2_coordinates' do + it 'must call angle_base_coords' do + allow(arc).to receive(:angle_base_coords) { |e| e } + expect(arc.angle2_coordinates).to eq(arc.angle2) + end + end + + describe '#angle1_x' do + it 'must grab :x_value from @angle1_coordinates' do + allow(arc).to receive(:angle1_coordinates) { {x_value: 55 } } + expect(arc.angle1_x).to eq(55) + end + end + + describe '#angle1_y' do + it 'must grab :y_value from @angle1_coordinates' do + allow(arc).to receive(:angle1_coordinates) { {y_value: 55 } } + expect(arc.angle1_y).to eq(55) + end + end + + describe '#angle2_x' do + it 'must grab :x_value from @angle2_coordinates' do + allow(arc).to receive(:angle2_coordinates) { {x_value: 55 } } + expect(arc.angle2_x).to eq(55) + end + end + + describe '#angle2_y' do + it 'must grab :y_value from @angle2_coordinates' do + allow(arc).to receive(:angle2_coordinates) { {y_value: 55 } } + expect(arc.angle2_y).to eq(55) + end + end + + describe '#slope_of_angles' do + it 'must calculate the slope value from the angles' do + expect(arc.slope_of_angles).to eq(-1.4999850001499984) + end + end + + describe '#b_value_for_line' do + context 'when #mx_value is not infinity' do + it 'must calculate the b value of the line equation from its angles' do + expect(arc.b_value_for_line).to eq(513.4968050319496) + end + end + + context 'when #mx_value is infinity' do + it 'must return angle1_y' do + allow(arc).to receive(:angle1_x) { Float::INFINITY } + allow(arc).to receive(:slope_of_angles) { Float::INFINITY } + allow(arc).to receive(:angle1_y) { 55 } + expect(arc.b_value_for_line).to eq(55) + end + end + end + + describe '#vertical_check' do + context 'if angle_x is same as input' do + it 'must return :on' do + allow(arc).to receive(:angle1_x) { 55 } + expect(arc.vertical_check(55)).to eq(:on) + end + end + + context 'if angle_x is less than input' do + it 'must return :above' do + allow(arc).to receive(:angle1_x) { 1 } + expect(arc.vertical_check(55)).to eq(:above) + end + end + + context 'if angle_x is more than input' do + it 'must return :below' do + allow(arc).to receive(:angle1_x) { 100 } + expect(arc.vertical_check(55)).to eq(:below) + end + end + end + + describe '#normal_above_below_check' do + before(:each) { allow(arc).to receive(:b_value_for_line) { 0 } } + + context 'if right_side_of_equation is same as y_input' do + it 'must return :on' do + expect(arc.normal_above_below_check(10, 10)).to eq(:on) + end + end + + context 'if right_side_of_equation is less than y_input' do + it 'must return :below' do + expect(arc.normal_above_below_check(8, 10)).to eq(:below) + end + end + + context 'if right_side_of_equation is more than y_input' do + it 'must return :above' do + expect(arc.normal_above_below_check(12, 10)).to eq(:above) + end + end + end + + describe '#above_below_on' do + before(:each) { allow(arc).to receive(:vertical_check) { |x| x } } + before(:each) { allow(arc).to receive(:normal_above_below_check) { |x, y| [x, y] } } + + context 'when mx_value over a million' do + context 'when #slope_of_angles is positive' do + before(:each) { allow(arc).to receive(:slope_of_angles) { 1 } } + + it 'must call and return #vertical_check' do + expect(arc.above_below_on(6_000_000, 100)).to eq(6_000_000) + end + end + + context 'when #slope_of_angles is negative' do + before(:each) { allow(arc).to receive(:slope_of_angles) { -1 } } + + it 'must call and return #vertical_check' do + expect(arc.above_below_on(-6_000_000, 100)).to eq(-6_000_000) + end + end + end + + context 'when mx_value under a million' do + it 'must call #normal_above_below_check' do + allow(arc).to receive(:slope_of_angles) { 1 } + expect(arc.above_below_on(60, 100)).to eq([60, 100]) + end + end + end + + describe '#standard_arc_bounds_check?' do + context 'when #inside_oval? is false' do + it 'must return false' do + allow(arc).to receive(:inside_oval?) { false } + expect(arc.standard_arc_bounds_check?(1, 1)).to eq(false) + end + end + + context 'when #inside_oval? is true' do + before(:each) { allow(arc).to receive(:inside_oval?) { true } } + context 'when #on_shaded_part? is false' do + it 'must return false' do + allow(arc).to receive(:on_shaded_part?) { false } + expect(arc.standard_arc_bounds_check?(1, 1)).to eq(false) + end + end + + context 'when #on_shaded_part? is true' do + it 'must return true' do + allow(arc).to receive(:on_shaded_part?) { true } + expect(arc.standard_arc_bounds_check?(1, 1)).to eq(true) + end + end + end + end + + describe '#on_shaded_part?' do + context 'when #angle2_x_smaller_check is true' do + it 'must return true' do + allow(arc).to receive(:angle1_x_smaller_check) { false } + allow(arc).to receive(:angle2_x_smaller_check) { true } + expect(arc.on_shaded_part?(1, 1)).to eq(true) + end + end + + context 'when #angle2_x_smaller_check is true' do + it 'must return true' do + allow(arc).to receive(:angle1_x_smaller_check) { true } + allow(arc).to receive(:angle2_x_smaller_check) { false } + expect(arc.on_shaded_part?(1, 1)).to eq(true) + end + end + + context 'when both #angle2_x_smaller_check and #angle1_x_smaller_check are false' do + it 'must return false' do + allow(arc).to receive(:angle1_x_smaller_check) { false } + allow(arc).to receive(:angle2_x_smaller_check) { false } + expect(arc.on_shaded_part?(1, 1)).to eq(false) + end + end + end + + describe '#angle1_x_smaller_check' do + context 'when #above_below_on is :below' do + it 'must return false' do + allow(arc).to receive(:above_below_on) { :below } + allow(arc).to receive(:angle1_x) { 1 } + allow(arc).to receive(:angle2_x) { 2 } + expect(arc.angle1_x_smaller_check(1, 1)).to eq(false) + end + end + + context 'when #angle1_x is greater than #angle2_x' do + it 'must return false' do + allow(arc).to receive(:above_below_on) { :above } + allow(arc).to receive(:angle1_x) { 2 } + allow(arc).to receive(:angle2_x) { 1 } + expect(arc.angle1_x_smaller_check(1, 1)).to eq(false) + end + end + + context 'when #above_below_on is :above and #angle1_x is less than #angle2_x' do + it 'must return false' do + allow(arc).to receive(:above_below_on) { :above } + allow(arc).to receive(:angle1_x) { 1 } + allow(arc).to receive(:angle2_x) { 2 } + expect(arc.angle1_x_smaller_check(1, 1)).to eq(true) + end + end + end + + describe '#angle2_x_smaller_check' do + context 'when #above_below_on is :above' do + it 'must return false' do + allow(arc).to receive(:above_below_on) { :above } + allow(arc).to receive(:angle1_x) { 2 } + allow(arc).to receive(:angle2_x) { 1 } + expect(arc.angle2_x_smaller_check(1, 1)).to eq(false) + end + end + + context 'when #angle1_x is less than #angle2_x' do + it 'must return false' do + allow(arc).to receive(:above_below_on) { :below } + allow(arc).to receive(:angle1_x) { 1 } + allow(arc).to receive(:angle2_x) { 2 } + expect(arc.angle2_x_smaller_check(1, 1)).to eq(false) + end + end + + context 'when #above_below_on is :below and #angle1_x is greater than #angle2_x' do + it 'must return false' do + allow(arc).to receive(:above_below_on) { :below } + allow(arc).to receive(:angle1_x) { 2 } + allow(arc).to receive(:angle2_x) { 1 } + expect(arc.angle2_x_smaller_check(1, 1)).to eq(true) + end + end + end + + describe '#inner_oval_difference' do + it 'must call #calculate_inner_oval_difference only once' do + allow(arc).to receive(:calculate_inner_oval_difference) { 1 } + expect(arc).to receive(:calculate_inner_oval_difference).once + arc.inner_oval_difference + arc.inner_oval_difference + end + end + + describe '#in_bounds?' do + context 'without stubbing methods' do + context 'with fill' do + context 'with standard angles' do + let(:test_arc) { Shoes::Arc.new(app, parent, 30, 30, 360, 360, 0.0, 4.71238898038469) } + context 'when point is inside the arc' do + it 'must return true' do + expect(test_arc.in_bounds?(245.0, 158.0)).to eq(true) + end + end + + context 'when point is outside the arc' do + it 'must return false' do + expect(test_arc.in_bounds?(215.0, 31.0)).to eq(false) + end + end + end + + context 'with negative angles' do + let(:test_arc) { Shoes::Arc.new(app, parent, 30, 30, 360, 360, -7.330382858376184, -5.759586531581287) } + context 'when point is inside the arc' do + it 'must return true' do + expect(test_arc.in_bounds?(350.0, 170.0)).to eq(true) + end + end + + context 'when point is outside the arc' do + it 'must return false' do + expect(test_arc.in_bounds?(385.0, 305.0)).to eq(false) + end + end + end + + context 'with an angle over 2 PI' do + let(:test_arc) { Shoes::Arc.new(app, parent, 210, 210, 200, 200, 6.8067840827778845, 5.235987755982989) } + context 'when point is inside the arc' do + it 'must return true' do + expect(test_arc.in_bounds?(295.0, 239.0)).to eq(true) + end + end + + context 'when point is outside the arc' do + it 'must return false' do + expect(test_arc.in_bounds?(385.0, 305.0)).to eq(false) + end + end + end + end + + context 'with fill' do + context 'with standard angles' do + let(:test_arc) { Shoes::Arc.new(app, parent, 30, 30, 360, 360, 0.0, 4.71238898038469) } + before(:each) { allow(test_arc).to receive(:style) { {fill: nil} } } + context 'when point is inside the arc' do + it 'must return true' do + expect(test_arc.in_bounds?(210.5, 30.5)).to eq(true) + end + end + + context 'when point is outside the arc' do + it 'must return false' do + expect(test_arc.in_bounds?(245.0, 158.0)).to eq(false) + end + end + end + + context 'with negative angles' do + let(:test_arc) { Shoes::Arc.new(app, parent, 30, 30, 360, 360, -7.330382858376184, -5.759586531581287) } + before(:each) { allow(test_arc).to receive(:style) { {fill: nil} } } + context 'when point is inside the arc' do + it 'must return true' do + expect(test_arc.in_bounds?(301, 55)).to eq(true) + end + end + + context 'when point is outside the arc' do + it 'must return false' do + expect(test_arc.in_bounds?(350.0, 170.0)).to eq(false) + end + end + end + + context 'with an angle over 2 PI' do + let(:test_arc) { Shoes::Arc.new(app, parent, 210, 210, 200, 200, 6.8067840827778845, 5.235987755982989) } + before(:each) { allow(test_arc).to receive(:style) { {fill: nil} } } + context 'when point is inside the arc' do + it 'must return true' do + expect(test_arc.in_bounds?(210.0, 310.0)).to eq(true) + end + end + + context 'when point is outside the arc' do + it 'must return false' do + expect(test_arc.in_bounds?(295.0, 239.0)).to eq(false) + end + end + end + end + end + + context 'with stubging methods' do + context 'when #standard_arc_bounds_check? is false' do + it 'must return false' do + allow(arc).to receive(:standard_arc_bounds_check?) { false } + expect(arc.in_bounds?(1, 1)).to eq(false) + end + end + + context 'when #standard_arc_bounds_check? is true' do + before(:each) { allow(arc).to receive(:standard_arc_bounds_check?) { true } } + context 'when #style[:fill] is present' do + it 'must return false' do + allow(arc).to receive(:style) { {fill: 1} } + expect(arc.in_bounds?(1, 1)).to eq(true) + end + end + + context 'when #style[:fill] is nil' do + before(:each) { allow(arc).to receive(:style) { {} } } + context 'when #inside_inner_oval? is false' do + it 'must return true' do + allow(arc).to receive(:inside_inner_oval?) { false } + expect(arc.in_bounds?(1, 1)).to eq(true) + end + end + + context 'when #inside_inner_oval? is true' do + it 'must return false' do + allow(arc).to receive(:inside_inner_oval?) { true } + expect(arc.in_bounds?(1, 1)).to eq(false) + end + end + end + end + end + end + + describe '#calculate_inner_oval_difference' do + context 'when style[:strokewidth] is under 3.0' do + it 'must return 3' do + arc.instance_variable_set(:@style, strokewidth: 2) + expect(arc.calculate_inner_oval_difference).to eq(3.0) + end + end + + context 'when style[:strokewidth] is more than 3.0' do + it 'must return style[:strokewidth]' do + arc.instance_variable_set(:@style, strokewidth: 5) + expect(arc.calculate_inner_oval_difference).to eq(5.0) + end + end + end + + describe '#inner_oval_axis_fraction' do + it 'must call #oval_axis_fraction with the input inner_oval_difference' do + allow(arc).to receive(:inner_oval_difference) { :test_input } + expect(arc).to receive(:oval_axis_fraction).with(:a, :b, :test_input) + arc.inner_oval_axis_fraction(:a, :b) + end + end + + describe '#inside_inner_oval?' do + before(:each) { allow(arc).to receive(:inner_oval_axis_fraction) { |_, a| a } } + context 'when sum is less than one' do + it 'must return true' do + expect(arc.inside_inner_oval?(0.25, 0.25)).to eq(true) + end + end + + context 'when sum is greater than one' do + it 'must return false' do + expect(arc.inside_inner_oval?(1.25, 0.25)).to eq(false) + end + end + + context 'when sum is one' do + it 'must return true' do + expect(arc.inside_inner_oval?(0.75, 0.25)).to eq(true) + end + end + end + end end