Current File : //usr/share/texlive/texmf-dist/tex/generic/pgf/graphdrawing/lua/pgf/gd/model/Path_arced.lua |
-- Copyright 2014 by Till Tantau
--
-- This file may be distributed an/or modified
--
-- 1. under the LaTeX Project Public License and/or
-- 2. under the GNU Public License
--
-- See the file doc/generic/pgf/licenses/LICENSE for more information
-- @release $Header$
local Path = require 'pgf.gd.model.Path'
-- Imports
local Coordinate = require "pgf.gd.model.Coordinate"
local Transform = require "pgf.gd.lib.Transform"
-- Locals
local rigid = Path.rigid
local tan = math.tan
local sin = math.sin
local cos = math.cos
local sqrt = math.sqrt
local atan2 = math.atan2
local abs = math.abs
local to_rad = math.pi/180
local to_deg = 180/math.pi
local pi_half = math.pi/2
local function sin_quarter(x)
x = x % 360
if x == 0 then
return 0
elseif x == 90 then
return 1
elseif x == 180 then
return 0
else
return -1
end
end
local function cos_quarter(x)
x = x % 360
if x == 0 then
return 1
elseif x == 90 then
return 0
elseif x == 180 then
return -1
else
return 0
end
end
local function atan2deg(y,x)
-- Works like atan2, but returns the angle in degrees and, returns
-- exactly a multiple of 90 if x or y are zero
if x == 0 then
if y < 0 then
return -90
else
return 90
end
elseif y == 0 then
if x < 0 then
return 180
else
return 0
end
else
return atan2(y,x) * to_deg
end
end
local function subarc (path, startx, starty, start_angle, delta, radius, trans, center_x, center_y)
local end_angle = start_angle + delta
local factor = tan (delta*to_rad/4) * 1.333333333333333333333 * radius
local s1, c1, s190, c190, s2, c2, s290, c290
if start_angle % 90 == 0 then
s1, c1, s190, c190 = sin_quarter(start_angle), cos_quarter(start_angle), sin_quarter(start_angle+90), cos_quarter(start_angle+90)
else
local a1 = start_angle*to_rad
s1, c1, s190, c190 = sin(a1), cos(a1), sin(a1+pi_half), cos(a1+pi_half)
end
if end_angle % 90 == 0 then
s2, c2, s290, c290 = sin_quarter(end_angle), cos_quarter(end_angle), sin_quarter(end_angle-90), cos_quarter(end_angle-90)
else
local a2 = end_angle * to_rad
s2, c2, s290, c290 = sin(a2), cos(a2), sin(a2-pi_half), cos(a2-pi_half)
end
local lastx, lasty = center_x + c2*radius, center_y + s2*radius
path[#path + 1] = "curveto"
path[#path + 1] = Coordinate.new (startx + c190*factor, starty + s190*factor)
path[#path + 1] = Coordinate.new (lastx + c290*factor, lasty + s290*factor)
path[#path + 1] = Coordinate.new (lastx, lasty)
if trans then
path[#path-2]:apply(trans)
path[#path-1]:apply(trans)
path[#path ]:apply(trans)
end
return lastx, lasty, end_angle
end
local function arc (path, start, start_angle, end_angle, radius, trans, centerx, centery)
-- @param path is the path object
-- @param start is the start coordinate
-- @param start_angle is given in degrees
-- @param end_angle is given in degrees
-- @param radius is the radius
-- @param trans is an optional transformation matrix that gets applied to all computed points
-- @param centerx optionally: x-part of the center of the circle
-- @param centery optionally: y-part of the center of the circle
local startx, starty = start.x, start.y
-- Compute center:
centerx = centerx or startx - cos(start_angle*to_rad)*radius
centery = centery or starty - sin(start_angle*to_rad)*radius
if start_angle < end_angle then
-- First, ensure that the angles are in a reasonable range:
start_angle = start_angle % 360
end_angle = end_angle % 360
if end_angle <= start_angle then
-- In case the modulo has inadvertently moved the end angle
-- before the start angle:
end_angle = end_angle + 360
end
-- Ok, now create a series of arcs that are at most quarter-cycles:
while start_angle < end_angle do
if start_angle + 179 < end_angle then
-- Add a quarter cycle:
startx, starty, start_angle = subarc(path, startx, starty, start_angle, 90, radius, trans, centerx, centery)
elseif start_angle + 90 < end_angle then
-- Add 60 degrees to ensure that there are no small segments
-- at the end
startx, starty, start_angle = subarc(path, startx, starty, start_angle, (end_angle-start_angle)/2, radius, trans, centerx, centery)
else
subarc(path, startx, starty, start_angle, end_angle - start_angle, radius, trans, centerx, centery)
break
end
end
elseif start_angle > end_angle then
-- First, ensure that the angles are in a reasonable range:
start_angle = start_angle % 360
end_angle = end_angle % 360
if end_angle >= start_angle then
-- In case the modulo has inadvertedly moved the end angle
-- before the start angle:
end_angle = end_angle - 360
end
-- Ok, now create a series of arcs that are at most quarter-cycles:
while start_angle > end_angle do
if start_angle - 179 > end_angle then
-- Add a quarter cycle:
startx, starty, start_angle = subarc(path, startx, starty, start_angle, -90, radius, trans, centerx, centery)
elseif start_angle - 90 > end_angle then
-- Add 60 degrees to ensure that there are no small segments
-- at the end
startx, starty, start_angle = subarc(path, startx, starty, start_angle, (end_angle-start_angle)/2, radius, trans, centerx, centery)
else
subarc(path, startx, starty, start_angle, end_angle - start_angle, radius, trans, centerx, centery)
break
end
end
-- else, do nothing
end
end
-- Doc see Path.lua
function Path:appendArc(start_angle,end_angle,radius, trans)
local start = rigid(self[#self])
assert(type(start) == "table", "trying to append an arc to a path that does not end with a coordinate")
if trans then
start = start:clone()
start:apply(Transform.invert(trans))
end
arc (self, start, start_angle, end_angle, radius, trans)
end
-- Doc see Path.lua
function Path:appendArcTo (target, radius_or_center, clockwise, trans)
local start = rigid(self[#self])
assert(type(start) == "table", "trying to append an arc to a path that does not end with a coordinate")
local trans_target = target
local centerx, centery, radius
if type(radius_or_center) == "number" then
radius = radius_or_center
else
centerx, centery = radius_or_center.x, radius_or_center.y
end
if trans then
start = start:clone()
trans_target = target:clone()
local itrans = Transform.invert(trans)
start:apply(itrans)
trans_target:apply(itrans)
if centerx then
local t = radius_or_center:clone()
t:apply(itrans)
centerx, centery = t.x, t.y
end
end
if not centerx then
-- Compute center
local dx, dy = target.x - start.x, target.y - start.y
if abs(dx) == abs(dy) and abs(dx) == radius then
if (dx < 0 and dy < 0) or (dx > 0 and dy > 0) then
centerx = start.x
centery = trans_target.y
else
centerx = trans_target.x
centery = start.y
end
else
local l_sq = dx*dx + dy*dy
if l_sq >= radius*radius*4*0.999999 then
centerx = (start.x+trans_target.x) / 2
centery = (start.y+trans_target.y) / 2
assert(l_sq <= radius*radius*4/0.999999, "radius too small for arc")
else
-- Normalize
local l = sqrt(l_sq)
local nx = dx / l
local ny = dy / l
local e = sqrt(radius*radius - 0.25*l_sq)
centerx = start.x + 0.5*dx - ny*e
centery = start.y + 0.5*dy + nx*e
end
end
end
local start_dx, start_dy, target_dx, target_dy =
start.x - centerx, start.y - centery,
trans_target.x - centerx, trans_target.y - centery
if not radius then
-- Center is given, compute radius:
radius_sq = start_dx^2 + start_dy^2
-- Ensure that the circle is, indeed, centered:
assert (abs(target_dx^2 + target_dy^2 - radius_sq)/radius_sq < 1e-5, "attempting to add an arc with incorrect center")
radius = sqrt(radius_sq)
end
-- Compute start and end angle:
local start_angle = atan2deg(start_dy, start_dx)
local end_angle = atan2deg(target_dy, target_dx)
if clockwise then
if end_angle > start_angle then
end_angle = end_angle - 360
end
else
if end_angle < start_angle then
end_angle = end_angle + 360
end
end
arc (self, start, start_angle, end_angle, radius, trans, centerx, centery)
-- Patch last point to avoid rounding problems:
self[#self] = target
end
-- Done
return true