# -*- encoding: utf-8; frozen_string_literal: true -*- # #-- # This file is part of HexaPDF. # # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby # Copyright (C) 2014-2021 Thomas Leitner # # HexaPDF is free software: you can redistribute it and/or modify it # under the terms of the GNU Affero General Public License version 3 as # published by the Free Software Foundation with the addition of the # following permission added to Section 15 as permitted in Section 7(a): # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON # INFRINGEMENT OF THIRD PARTY RIGHTS. # # HexaPDF is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with HexaPDF. If not, see . # # The interactive user interfaces in modified source and object code # versions of HexaPDF must display Appropriate Legal Notices, as required # under Section 5 of the GNU Affero General Public License version 3. # # In accordance with Section 7(b) of the GNU Affero General Public # License, a covered work must retain the producer line in every PDF that # is created or manipulated using HexaPDF. # # If the GNU Affero General Public License doesn't fit your need, # commercial licenses are available at . #++ require 'hexapdf/utils/math_helpers' module HexaPDF module Content module GraphicObject # This class describes an elliptical arc in endpoint parameterization. It allows one to # generate an arc from the current point to a given point, similar to Content::Canvas#line_to. # # See: GraphicObject::Arc, ARC - https://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes class EndpointArc EPSILON = 1e-10 include Utils::MathHelpers # Creates and configures a new endpoint arc object. # # See #configure for the allowed keyword arguments. def self.configure(**kwargs) new.configure(**kwargs) end # x-coordinate of endpoint attr_reader :x # y-coordinate of endpoint attr_reader :y # Length of semi-major axis attr_reader :a # Length of semi-minor axis attr_reader :b # Inclination in degrees of semi-major axis in respect to x-axis attr_reader :inclination # Large arc choice - if +true+ use the large arc (i.e. the one spanning more than 180 # degrees), else the small arc attr_reader :large_arc # Direction of arc - if +true+ in clockwise direction, else in counterclockwise direction attr_reader :clockwise # Creates an endpoint arc with default values x=0, y=0, a=0, b=0, inclination=0, # large_arc=true, clockwise=false (a line to the origin). def initialize @x = @y = 0 @a = @b = 0 @inclination = 0 @large_arc = true @clockwise = false end # Configures the endpoint arc with # # * endpoint (+x+, +y+), # * semi-major axis +a+, # * semi-minor axis +b+, # * an inclination in respect to the x-axis of +inclination+ degrees, # * the given large_arc flag and # * the given clockwise flag. # # The +large_arc+ option determines whether the large arc, i.e. the one spanning more than # 180 degrees, is used (+true+) or the small arc (+false+). # # The +clockwise+ option determines if the arc is drawn in the counterclockwise direction # (+false+) or in the clockwise direction (+true+). # # Any arguments not specified are not modified and retain their old value, see #initialize # for the inital values. # # Returns self. def configure(x: nil, y: nil, a: nil, b: nil, inclination: nil, large_arc: nil, clockwise: nil) @x = x if x @y = y if y @a = a.abs if a @b = b.abs if b @inclination = inclination % 360 if inclination @large_arc = large_arc unless large_arc.nil? @clockwise = clockwise unless clockwise.nil? self end # Draws the arc on the given Canvas. def draw(canvas) x1, y1 = *canvas.current_point # ARC F.6.2 - nothing to do if endpoint is equal to current point return if float_equal(x1, @x) && float_equal(y1, @y) if @a == 0 || @b == 0 # ARC F.6.2, F.6.6 - just use a line if it is not really an arc canvas.line_to(@x, @y) else values = compute_arc_values(x1, y1) arc = canvas.graphic_object(:arc, **values) arc.draw(canvas, move_to_start: false) end end private # Compute the center parameterization from the endpoint parameterization. # # The argument (x1, y1) is the starting point. # # See: ARC F.6.5, F.6.6 def compute_arc_values(x1, y1) x2 = @x y2 = @y rx = @a ry = @b theta = deg_to_rad(@inclination) cos_theta = Math.cos(theta) sin_theta = Math.sin(theta) # F.6.5.1 x1p = (x1 - x2) / 2.0 * cos_theta + (y1 - y2) / 2.0 * sin_theta y1p = (x1 - x2) / 2.0 * -sin_theta + (y1 - y2) / 2.0 * cos_theta x1ps = x1p**2 y1ps = y1p**2 rxs = rx**2 rys = ry**2 # F.6.6.2 l = x1ps / rxs + y1ps / rys if l > 1 rx *= Math.sqrt(l) ry *= Math.sqrt(l) rxs = rx**2 rys = ry**2 end # F.6.5.2 sqrt = (rxs * rys - rxs * y1ps - rys * x1ps) / (rxs * y1ps + rys * x1ps) sqrt = 0 if sqrt.abs < EPSILON sqrt = Math.sqrt(sqrt) sqrt *= -1 unless @large_arc == @clockwise cxp = sqrt * rx * y1p / ry cyp = - sqrt * ry * x1p / rx # F.6.5.3 cx = cos_theta * cxp - sin_theta * cyp + (x1 + x2) / 2.0 cy = sin_theta * cxp + cos_theta * cyp + (y1 + y2) / 2.0 # F.6.5.5 start_angle = compute_angle_to_x_axis((x1p - cxp) / rx, (y1p - cyp) / ry) # F.6.5.6 (modified bc we just need the end angle) end_angle = compute_angle_to_x_axis((-x1p - cxp) / rx, (-y1p - cyp) / ry) {cx: cx, cy: cy, a: rx, b: ry, start_angle: start_angle, end_angle: end_angle, inclination: @inclination, clockwise: @clockwise} end # Compares two float numbers if they are within a certain delta. def float_equal(a, b) (a - b).abs < EPSILON end # Computes the angle in degrees between the x-axis and the vector. def compute_angle_to_x_axis(vx, vy) (vy < 0 ? -1 : 1) * rad_to_deg(Math.acos(vx / Math.sqrt(vx**2 + vy**2))) end end end end end