Advertisement
creamygoat

VegeSVGPlot

May 8th, 2012
991
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 105.08 KB | None | 0 0
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. #-------------------------------------------------------------------------------
  4. # vegesvgplot.py
  5. # http://pastebin.com/6Aek3Exm
  6. #-------------------------------------------------------------------------------
  7.  
  8.  
  9. u'''VegeSVGPlot, a Python SVG plotting and maths module for vegetables
  10.  
  11. Description:
  12.  The VegeSVGPlot module is a collection of simple mathematics, geometry
  13.  and SVG output functions. The module makes it easy to write simple Python
  14.  scripts to produce 2D graphical output such as function plots.
  15.  
  16. Author:
  17.  Daniel Neville (Blancmange), creamygoat@gmail.com
  18.  
  19. Copyright:
  20.  None
  21.  
  22. Licence:
  23.  Public domain
  24.  
  25.  
  26. INDEX
  27.  
  28.  
  29. Imports
  30.  
  31. Shape constants:
  32.  
  33.  Pt_Break
  34.  Pt_Anchor
  35.  Pt_Control
  36.  
  37.  PtCmdWithCoordsSet
  38.  PtCmdSet
  39.  
  40. Legendre–Gauss quadrature tables:
  41.  
  42.  LGQWeights25
  43.  LGQAbscissae25
  44.  
  45. Exceptions:
  46.  
  47.  Error
  48.  
  49. tIndentTracker:
  50.  
  51.  __init__([IndentUnitStr], [StartingLevel], [LineTerminator])
  52.  __call__(*StringArgs)
  53.  Clone()
  54.  StepIn([LevelIncrement])
  55.  StepOut([LevelDecrement])
  56.  LineTerminator
  57.  IndentUnitStr
  58.  Level
  59.  LIStr
  60.  
  61. tAffineMtx:
  62.  
  63.  __init__(Origin, BasisVectors)
  64.  __repr__()
  65.  MultV(Vector)
  66.  MultVectors(Vectors)
  67.  MultAM(AM)
  68.  B
  69.  T
  70.  
  71. Affine matrix creation functions:
  72.  
  73.  AffineMtxTS(Translation, Scale)
  74.  AffineMtxTRS2D(Translation, Angle, Scale)
  75.  Affine2DMatrices(Origin, X)
  76.  
  77. Utility functions:
  78.  
  79.  ValidatedRange(ARange, Length)
  80.  MergedDictionary(Original, Patch)
  81.  Save(Data, FileName)
  82.  
  83. Array access functions:
  84.  
  85.  ArrayDimensions(MDArray)
  86.  NewArray(Dimensions, Value)
  87.  CopyArray(Source, [CopyDepth])
  88.  At(MDArray, Indices)
  89.  SetAt(MDArray, Indices, Value)
  90.  EnumerateArray(MDArray, [Depth])
  91.  
  92. Basic vector functions:
  93.  
  94.  VZeros(n)
  95.  VOnes(n)
  96.  VStdBasis(Dimensions, Index, [Scale])
  97.  VDim(A, n)
  98.  VAug(A, x)
  99.  VMajorAxis(A)
  100.  VNeg(A)
  101.  VSum(A, B...)
  102.  VDiff(A, B)
  103.  VSchur(A, B...)
  104.  VDot(A, B)
  105.  VLengthSquared(A)
  106.  VLength(A)
  107.  VManhattan(A)
  108.  VScaled(A, scale)
  109.  VNormalised(A)
  110.  VPerp(A)
  111.  VRevPerp(A)
  112.  VCrossProduct(A, B)
  113.  VCrossProduct4D(A, B, C)
  114.  VScalarTripleProduct(A, B, C)
  115.  VVectorTripleProduct(A, B, C)
  116.  VProjectionOnto(Axis, Vector)
  117.  VDiagonalMAV(Diagonals)
  118.  VTransposedMAV(BasisVectors)
  119.  VRectToPol(A)
  120.  VPolToRect(DistanceAngle)
  121.  VLerp(A, B, Progress)
  122.  
  123. Mathematical functions:
  124.  
  125.  BinomialCoefficient(n, k)
  126.  BinomialRow(n)
  127.  NextBinomialRow(Row)
  128.  ApproxSaggita(ArcLength, ChordLength)
  129.  LGQIntegral25(Interval, Fn, InitalParams, FinalParams)
  130.  IntegralFunctionOfPLF(Vertices, C=0.0)
  131.  EvaluatePQF(PQF, x)
  132.  EvaluateInvPQF(PQF, y)
  133.  
  134. Linear intersection functions:
  135.  
  136.  LineParameter(Point, Line)
  137.  XInterceptOfLine(Line)
  138.  UnitXToLineIntersection(Line2)
  139.  LineToLineIntersectionPoint(Line1, Line2)
  140.  LineToLineIntersection(Line1, Line2)
  141.  
  142. Bézier functions:
  143.  
  144.  CubicBezierArcHandleLength(AngleSweep)
  145.  BezierPoint(Curve, t)
  146.  BezierPAT(Curve, t)
  147.  SplitBezier(Curve, t)
  148.  BezierDerivative(Curve)
  149.  ManhattanBezierDeviance(Curve)
  150.  BezierLength(Curve, [Interval])
  151.  
  152. Bézier intersection functions:
  153.  
  154.  UnitXToBezierIntersections(Curve, [Tolerance])
  155.  LineToBezierIntersections(Line, Curve, [Tolerance])
  156.  
  157. Shape functions:
  158.  
  159.  ShapeDim(Shape, n)
  160.  ShapeFromVertices(Vertices, Order)
  161.  ShapePoints(Shape, [Range])
  162.  ShapeSubpathRanges(Shape)
  163.  ShapeCurveRanges(Shape, [Range])
  164.  ShapeLength(Shape)
  165.  LineToShapeIntersections(Line, Shape, [Tolerance])
  166.  TransformedShape(AM, Shape)
  167.  PiecewiseArc(Centre, Radius, AngleRange, NumPieces)
  168.  
  169. Output formatting functions:
  170.  
  171.  MaxDP(x, n)
  172.  GFListStr(L)
  173.  GFTupleStr(T)
  174.  HTMLEscaped(Text)
  175.  AttrMarkup(Attributes, PrependSpace)
  176.  ProgressColourStr(Progress, [Opacity])
  177.  
  178. SVG functions:
  179.  
  180.  SVGStart(IT, Title, [SVGAttributes])
  181.  SVGEnd(IT)
  182.  SVGPathDataSegments(ShapePoints)
  183.  SVGPath(IT, ShapePoints, [Attributes])
  184.  SVGText(IT, Position, Text, [Attributes])
  185.  SVGGroup(IT, [Attributes])
  186.  SVGGroupEnd(IT)
  187.  SVGGrid(IT, Grid)
  188.  
  189. '''
  190.  
  191.  
  192. #-------------------------------------------------------------------------------
  193. # Imports
  194. #-------------------------------------------------------------------------------
  195.  
  196.  
  197. import math
  198.  
  199. from math import (
  200.   pi, sqrt, hypot, sin, cos, tan, asin, acos, atan, atan2, radians, degrees,
  201.   floor, ceil
  202. )
  203.  
  204.  
  205. #-------------------------------------------------------------------------------
  206. # Shape constants
  207. #
  208. # A “Shape” is a list of (PointType, Point) pairs intended to represent paths
  209. # in the matter of Postscript or SVG but in a way which allows a path segment
  210. # to be very easily reversed.
  211. #
  212. #-------------------------------------------------------------------------------
  213.  
  214.  
  215. Pt_Break = 0
  216. Pt_Anchor = 1
  217. Pt_Control = 2
  218.  
  219. PtCmdWithCoordsSet = set([Pt_Anchor, Pt_Control])
  220. PtCmdSet = set([Pt_Break, Pt_Anchor, Pt_Control])
  221.  
  222.  
  223. #-------------------------------------------------------------------------------
  224. # Legendre–Gauss quadrature tables
  225. #
  226. # The following tables were adapted from Mike "Pomax" Kamermans’s tables,
  227. # which were computed to 295 decimal places and given for orders up to 64.
  228. #
  229. #-------------------------------------------------------------------------------
  230.  
  231.  
  232. # Legendre–Gauss quadrature weights for
  233. # the 25th order Legendre polynomial
  234.  
  235. LGQWeights25 = [
  236.   0.1222424429903100416889595189458515058351, # −12
  237.   0.1222424429903100416889595189458515058351, # +12
  238.   0.1194557635357847722281781265129010473902, # −11
  239.   0.1194557635357847722281781265129010473902, # +11
  240.   0.1148582591457116483393255458695558086409, # −10
  241.   0.1148582591457116483393255458695558086409, # +10
  242.   0.1085196244742636531160939570501166193401, #  −9
  243.   0.1085196244742636531160939570501166193401, #  +9
  244.   0.1005359490670506442022068903926858269885, #  −8
  245.   0.1005359490670506442022068903926858269885, #  +8
  246.   0.0910282619829636498114972207028916533810, #  −7
  247.   0.0910282619829636498114972207028916533810, #  +7
  248.   0.0801407003350010180132349596691113022902, #  −6
  249.   0.0801407003350010180132349596691113022902, #  +6
  250.   0.0680383338123569172071871856567079685547, #  −5
  251.   0.0680383338123569172071871856567079685547, #  +5
  252.   0.0549046959758351919259368915404733241601, #  −4
  253.   0.0549046959758351919259368915404733241601, #  +4
  254.   0.0409391567013063126556234877116459536608, #  −3
  255.   0.0409391567013063126556234877116459536608, #  +3
  256.   0.0263549866150321372619018152952991449360, #  −2
  257.   0.0263549866150321372619018152952991449360, #  +2
  258.   0.0113937985010262879479029641132347736033, #  −1
  259.   0.0113937985010262879479029641132347736033, #  +1
  260.   0.1231760537267154512039028730790501424382  #   0
  261. ]
  262.  
  263. # Legendre–Gauss quadrature abscissae (x-values)
  264. # for the 25th order Legendre polynomial
  265.  
  266. LGQAbscissae25 = [
  267.  -0.1228646926107103963873598188080368055322, # −12
  268.   0.1228646926107103963873598188080368055322, # +12
  269.  -0.2438668837209884320451903627974515864056, # −11
  270.   0.2438668837209884320451903627974515864056, # +11
  271.  -0.3611723058093878377358217301276406674221, # −10
  272.   0.3611723058093878377358217301276406674221, # +10
  273.  -0.4730027314457149605221821150091920413318, #  −9
  274.   0.4730027314457149605221821150091920413318, #  +9
  275.  -0.5776629302412229677236898416126540673957, #  −8
  276.   0.5776629302412229677236898416126540673957, #  +8
  277.  -0.6735663684734683644851206332476221758834, #  −7
  278.   0.6735663684734683644851206332476221758834, #  +7
  279.  -0.7592592630373576305772828652043609763875, #  −6
  280.   0.7592592630373576305772828652043609763875, #  +6
  281.  -0.8334426287608340014210211086935695694610, #  −5
  282.   0.8334426287608340014210211086935695694610, #  +5
  283.  -0.8949919978782753688510420067828049541746, #  −4
  284.   0.8949919978782753688510420067828049541746, #  +4
  285.  -0.9429745712289743394140111696584705319052, #  −3
  286.   0.9429745712289743394140111696584705319052, #  +3
  287.  -0.9766639214595175114983153864795940677454, #  −2
  288.   0.9766639214595175114983153864795940677454, #  +2
  289.  -0.9955569697904980979087849468939016172576, #  −1
  290.   0.9955569697904980979087849468939016172576, #  +1
  291.   0                                           #   0
  292. ]
  293.  
  294.  
  295. #-------------------------------------------------------------------------------
  296. # Exceptions
  297. #-------------------------------------------------------------------------------
  298.  
  299.  
  300. class Error (Exception):
  301.   pass;
  302.  
  303.  
  304. #-------------------------------------------------------------------------------
  305. # tIndentTracker
  306. #-------------------------------------------------------------------------------
  307.  
  308.  
  309. class tIndentTracker (object):
  310.  
  311.   u'''Keeper of current indentation levels for text output
  312.  
  313.  Functions which output strings of nicely indented markup can be made
  314.  especially easy to use when an object like this keeps track of the
  315.  current indenting level and the caller’s preference for the character
  316.  or string used for indenting.
  317.  
  318.  The default method returns indented and line-terminated text given one
  319.  or more string arguments, one argument per line of text. (Use the “*”
  320.  list or tuple unpacking operator to work with lists or tuples.)
  321.  
  322.  For convenience when writing custom markup, the LIStr field may
  323.  be used to begin each line of output.
  324.  
  325.  Functions which produce markup which imply a change in indentation
  326.  level should update both the Level and LIStr fields.
  327.  
  328.  Methods:
  329.  
  330.    __init__([IndentUnitStr], [StartingLevel], [LineTerminator])
  331.    __call__(*StringArgs)
  332.    Clone()
  333.    StepIn([LevelIncrement])
  334.    StepOut([LevelDecrement])
  335.  
  336.  Fields:
  337.  
  338.    IndentUnitStr
  339.    LineTerminator
  340.    Level
  341.    LIStr
  342.  
  343.  '''
  344.  
  345.   #-----------------------------------------------------------------------------
  346.  
  347.   def __init__(self, IndentUnitStr='  ', StartingLevel=0, LineTerminator='\n'):
  348.  
  349.     u'''Construct a keeper for the current indentation level for text output.
  350.  
  351.    All three arguments are optional.
  352.  
  353.    IndentUnitStr is the string used for each level of indentation.
  354.    This string is typically two spaces (to emulate the em-square of
  355.    typography), four spaces or a tab control character.
  356.  
  357.    StartingLevel, which defaults to zero, sets the current indentation
  358.    level.
  359.  
  360.    LineTerminator is the character or string appended to each string
  361.    argument supplied to the default method, a function which returns
  362.    indented and line-terminated text.
  363.  
  364.    '''
  365.  
  366.     self.IndentUnitStr = IndentUnitStr
  367.     self.LineTerminator = LineTerminator
  368.     self.Level = StartingLevel
  369.     self.LIStr = IndentUnitStr * StartingLevel
  370.  
  371.   #-----------------------------------------------------------------------------
  372.  
  373.   def __call__(self, *StringArgs):
  374.  
  375.     u'''Return a block of text with lines indented and terminated.
  376.  
  377.    Each argument is taken to be a line of text which is to be indented
  378.    at the current level and terminated with LineTerminator. If a list
  379.    or tuple of strings is to be processed, use the list or tuple unpacking
  380.    operator “*”.
  381.  
  382.    '''
  383.  
  384.     IndentedLines = []
  385.     for Line in StringArgs:
  386.       ILine = Line.rstrip()
  387.       if len(Line) > 0:
  388.         ILine = self.LIStr + ILine
  389.       IndentedLines.append(ILine)
  390.  
  391.     Result = self.LineTerminator.join(IndentedLines) + self.LineTerminator
  392.  
  393.     return Result
  394.  
  395.   #-----------------------------------------------------------------------------
  396.  
  397.   def Clone(self):
  398.  
  399.     u'''Create a clone of this tIndentTracker instance.
  400.  
  401.    A clone is handy for temporarily going into deeper nesting levels when
  402.    the supplied tIndentTracker instance must be treated as immutable.
  403.  
  404.    '''
  405.  
  406.     Result = tIndentTracker()
  407.  
  408.     # Return an exact copy, even if it is internally inconsistent.
  409.     # The user probably knows what they’re doing.
  410.  
  411.     Result.IndentUnitStr = self.IndentUnitStr
  412.     Result.LineTerminator = self.LineTerminator
  413.     Result.Level = self.StartingLevel
  414.     Result.LIStr = self.LIStr
  415.  
  416.     return Result
  417.  
  418.   #-----------------------------------------------------------------------------
  419.  
  420.   def StepIn(self, LevelIncrement=1):
  421.  
  422.     u'''Increase the indentation level.
  423.  
  424.    The fields Level and the convenience string LIStr are updated.
  425.  
  426.    '''
  427.  
  428.     self.Level += LevelIncrement
  429.     self.LIStr = self.IndentUnitStr * max(0, self.Level)
  430.  
  431.   #-----------------------------------------------------------------------------
  432.  
  433.   def StepOut(self, LevelDecrement=1):
  434.  
  435.     u'''Decrease the indentation level.
  436.  
  437.    The fields Level and the convenience string LIStr are updated.
  438.  
  439.    '''
  440.  
  441.     self.Level -= LevelDecrement
  442.     self.LIStr = self.IndentUnitStr * max(0, self.Level)
  443.  
  444.   #-----------------------------------------------------------------------------
  445.  
  446. #-------------------------------------------------------------------------------
  447. # tAffineMtx
  448. #-------------------------------------------------------------------------------
  449.  
  450.  
  451. class tAffineMtx (object):
  452.  
  453.   u'''Affine matrix for transforming non-homogeneous vectors
  454.  
  455.  An affine matrix is the usual square transformation matrix augmented
  456.  with a translation vector. Thus uniform and non-uniform scaling,
  457.  shearing, rotation and translation transformations can be combined
  458.  in any order. Using the convention that vectors are written as columns,
  459.  an affine matrix is like a homogeneous matrix without the bottom row and
  460.  is used to transform non-homogeneous vectors or other affine matrices.
  461.  
  462.  Methods:
  463.  
  464.    __init__(Origin, BasisVectors)
  465.    __repr__()
  466.    MultV(Vector)
  467.    MultVectors(Vectors)
  468.    MultAM(AM)
  469.  
  470.  Fields:
  471.  
  472.    B - Basis vectors (rotation, scaling, skewing)
  473.    T - Translation (Local origin)
  474.  
  475.  '''
  476.  
  477.   #-----------------------------------------------------------------------------
  478.  
  479.   def __init__(self, Origin, BasisVectors):
  480.  
  481.     u'''Construct an affine matrix.
  482.  
  483.    The origin is loaded into the translation part and is measured in terms
  484.    of the reference frame outside the local frame constructed by the basis
  485.    vectors and the origin.
  486.  
  487.    '''
  488.  
  489.     self.B = tuple(tuple(Bi) for Bi in BasisVectors)
  490.     self.T = tuple(Origin)
  491.  
  492.   #-----------------------------------------------------------------------------
  493.  
  494.   def __repr__(self):
  495.  
  496.     u'''Return a Python code string representation of this affine matrix.'''
  497.  
  498.     Name = self.__class__.__name__
  499.     return Name + '(' + repr(self.T) + ', ' + repr(self.B) + ')'
  500.  
  501.   #-----------------------------------------------------------------------------
  502.  
  503.   def MultV(self, Vector):
  504.  
  505.     u'''Return a vector transformed by this matrix.
  506.  
  507.    The vector is permitted to have a number of dimensions that differ to
  508.    the number of basis vectors represented in the affine matrix. Excess
  509.    basis vectors or excess vector components are ignored.
  510.  
  511.    If the vector is None or has a length of zero, the vector is returned
  512.    untransformed. This behaviour is convenient for processing Shapes, which
  513.    may include items like (Pt_Break, None).
  514.  
  515.    '''
  516.  
  517.     if (Vector is not None) and (len(Vector) > 0):
  518.  
  519.       Result = self.T
  520.  
  521.       for i, Basis in enumerate(self.B):
  522.         if i < len(Vector):
  523.           Result = VSum(Result, VScaled(Basis, Vector[i]))
  524.  
  525.     else:
  526.  
  527.       Result = Vector
  528.  
  529.     return Result
  530.  
  531.   #-----------------------------------------------------------------------------
  532.  
  533.   def MultVectors(self, Vectors):
  534.  
  535.     u'''Return a tuple of vectors transformed by this affine matrix.
  536.  
  537.    The vectors are each permitted to have a number of dimensions
  538.    different to the number of basis vectors represented in the
  539.    affine matrix. Excess basis vectors or excess vector components
  540.    are ignored.
  541.  
  542.    Any vector that is None or has a length of zero is appears in
  543.    the result untransformed.
  544.  
  545.    '''
  546.  
  547.     Result = []
  548.  
  549.     for V in Vectors:
  550.  
  551.       if (V is not None) and (len(V) > 0):
  552.  
  553.         VPrime = self.T
  554.  
  555.         for i, Basis in enumerate(self.B):
  556.           if i < len(V):
  557.             VPrime = VSum(VPrime, VScaled(Basis, V[i]))
  558.  
  559.       else:
  560.  
  561.         VPrime = V
  562.  
  563.       Result.append(VPrime)
  564.  
  565.     Result = tuple(Result)
  566.  
  567.     return Result
  568.  
  569.   #-----------------------------------------------------------------------------
  570.  
  571.   def MultAM(self, AM):
  572.  
  573.     u'''Return the product of this and a second affine matrix.
  574.  
  575.    When the resulting affine matrix is applied to vertices, the effect is as
  576.    if the second matrix was applied first and the first matrix applied last.
  577.    From the point of view of nested transformed reference frames in the manner
  578.    of SVG and OpenGL, the transformations are nested in left-to-right order.
  579.  
  580.    Considering the vectors to be transformed as column vectors, the nesting of
  581.    reference frames is performed by postmultiplying the current affine matrix
  582.    with successive (affine) transformation matrices. The composite matrix is
  583.    then premultiplied to each vector to be transformed.
  584.  
  585.    '''
  586.  
  587.     d = len(self.T)
  588.     dr = range(d)
  589.  
  590.     B1t = VTransposedMAV(self.B)
  591.     T1 = self.T
  592.     B2 = AM.B
  593.     T2 = AM.T
  594.  
  595.     BasisVectors = tuple(tuple(VDot(B1t[j], B2[i]) for j in dr) for i in dr)
  596.     Traslation = tuple(VDot(B1t[j], T2) + T1[j] for j in DimRange)
  597.  
  598.     return tAffineMtx(Translation, BasisVectors)
  599.  
  600.   #-----------------------------------------------------------------------------
  601.  
  602.  
  603. #-------------------------------------------------------------------------------
  604. # Affine matrix creation functions
  605. #-------------------------------------------------------------------------------
  606.  
  607.  
  608. def AffineMtxTS(Translation, Scale):
  609.  
  610.   u'''Return an affine matrix given a translation and a scale.
  611.  
  612.  When the tAffineMtx returned is applied to vertices, the translation
  613.  is performed last. From the point of view of transformed reference
  614.  frames in the manner of SVG and OpenGL, the translation is performed
  615.  first.
  616.  
  617.  The dimension of the resulting affine matrix is made to match the
  618.  dimension of Translation, which defines a local origin relative to
  619.  the origin of the ambient frame.
  620.  
  621.  If Scale is or contains only a single number, the scaling is uniform.
  622.  If Scale is a list or tuple, say, the scaling can be different for
  623.  each axis. Negative scales are permissible. Excess scale components
  624.  are ignored and unspecified scale components are assumed to be 1.0.
  625.  
  626.  '''
  627.  
  628.   #-----------------------------------------------------------------------------
  629.  
  630.   d = len(Translation)
  631.   dr = range(d)
  632.  
  633.   if hasattr(Scale, '__iter__'):
  634.     S = tuple(Scale)[:d]
  635.     S += (1.0,) * max(0, d - len(S))
  636.   else:
  637.     S = (Scale,) * d
  638.  
  639.   BasisVectors = VDiagonalMAV(S)
  640.  
  641.   return tAffineMtx(Translation, BasisVectors)
  642.  
  643.  
  644. #-------------------------------------------------------------------------------
  645.  
  646.  
  647. def AffineMtxTRS2D(Translation, Angle, Scale):
  648.  
  649.   u'''Return an affine matrix given a translation, rotation angle and a scale.
  650.  
  651.  Translation must be a vector of at least two dimensions, More dimensions
  652.  are permitted but the rotation is performed only in the plane of the first
  653.  two.
  654.  
  655.  Angle is measured in radians from the +x axis to the +y axis of the ambient
  656.  frame.
  657.  
  658.  The transformation is applied to vectors in the order scaling, rotation
  659.  and translation. From the point of view of nested reference frames in
  660.  the manner of OpenGL and SCG, the order of nested transformations is
  661.  translation, rotation and scaling.
  662.  
  663.  '''
  664.  
  665.   d = len(Translation)
  666.   AM = AffineMtxTS(Translation, Scale)
  667.  
  668.   c = cos(Angle)
  669.   s = sin(Angle)
  670.  
  671.   xs = AM.B[0][0]
  672.   ys = AM.B[1][1]
  673.   B = ((xs * c, xs * s), (ys * -s, ys * c))
  674.  
  675.   return tAffineMtx(AM.T, B)
  676.  
  677.  
  678. #-------------------------------------------------------------------------------
  679.  
  680.  
  681. def Affine2DMatrices(Origin, X):
  682.  
  683.   u'''Create matrices for mapping a line in 2D space to (0,0)–(1,0) and back.
  684.  
  685.  The line’s endpoints lie at Origin and Origin + X. This function gives the
  686.  scale, rotation and translation necessary to map the line onto the x-axis
  687.  between 0 and 1, thus aiding quick intersection and rejection tests.
  688.  
  689.  The result is in the form (M, InvM) where:
  690.  
  691.    M is the affine matrix to transform points so Line maps to (0,0)–(1,0) and
  692.    InvM restores points from the UnitX frame back to the original frame.
  693.  
  694.  If the vector X is too small, an overflow or a divide-by-zero exception
  695.  may be raised.
  696.  
  697.  '''
  698.  
  699.   Y = VPerp(X)
  700.  
  701.   # The scale to use in forming the inverse matrix’s basis vectors is
  702.   # is the inverse square of the length of the X basis vector, not just
  703.   # the inverse length. This is because the lengths of the basis vectors
  704.   # of one matrix must be the reciprocals of those of the other.
  705.   # ‖(‖X‖⁻²X)‖ × ‖X‖ = 1.
  706.  
  707.   s2 = 1.0 / VLengthSquared(X)
  708.  
  709.   MX = (s2 * X[0], s2 * Y[0])
  710.   MY = (s2 * X[1], s2 * Y[1])
  711.   MT = (
  712.     -(MX[0] * Origin[0] + MY[0] * Origin[1]),
  713.     -(MX[1] * Origin[0] + MY[1] * Origin[1]),
  714.   )
  715.   M = tAffineMtx(MT, (MX, MY))
  716.   InvM = tAffineMtx(Origin, (X, Y))
  717.  
  718.   return (M, InvM)
  719.  
  720.  
  721. #-------------------------------------------------------------------------------
  722. # Utility functions
  723. #-------------------------------------------------------------------------------
  724.  
  725.  
  726. def ValidatedRange(ARange, Length):
  727.  
  728.   u'''Return a validated range for a given array length.
  729.  
  730.  The validated range may have a span of zero and it may lie at the end of
  731.  a zero-based array (at an index of Length). It will, however, be sure to
  732.  not be reversed or include intervals beyond either end of the array.
  733.  
  734.  if ARange is None, the range (0, Length) is returned. This behaviour
  735.  makes it convenient to use this function on optional range arguments.
  736.  
  737.  '''
  738.  
  739.   if ARange is None:
  740.  
  741.     Start, End = (0, Length)
  742.  
  743.   else:
  744.  
  745.     Start, End = ARange
  746.  
  747.     Start = min(Length, Start) if Start >= 0 else max(0, Length + Start)
  748.     End = min(Length, End) if End >= 0 else Length + End
  749.     End = max(Start, End)
  750.  
  751.   return (Start, End)
  752.  
  753.  
  754. #-------------------------------------------------------------------------------
  755.  
  756.  
  757. def MergedDictionary(Original, Patch):
  758.  
  759.   u'''Return the union of two Python dictionaries.
  760.  
  761.  In the case of a key appearing in both dictionaries, Patch takes
  762.  precedence over Original.
  763.  
  764.  For the convenience of easily providing defaults or immutable
  765.  attributes to HTML or SVG tags, either argument may be None.
  766.  In any case, the result is a (shallow) copy.
  767.  
  768.  '''
  769.  
  770.   if Original is not None:
  771.     if Patch is not None:
  772.       return dict(list(Original.items()) + list(Patch.items()))
  773.     else:
  774.       return dict(Original)
  775.   else:
  776.     if Patch is not None:
  777.       return dict(Patch)
  778.     else:
  779.       return dict()
  780.  
  781.  
  782. #-------------------------------------------------------------------------------
  783.  
  784.  
  785. def Save(Data, FileName):
  786.  
  787.   u'''Save data to a new file.
  788.  
  789.  Data is usually a string. If a file with the given name already exists,
  790.  it is overwritten.
  791.  
  792.  If the string is a Unicode string, it should be encoded:
  793.  
  794.    Save(Data.encode('utf_8'), FileName)
  795.  
  796.  Encoding will fail if Data is a non-Unicode string which contains a
  797.  character outside of the 7-bit ASCII range. This can easily occur
  798.  when an ordinary byte string contains a Latin-1 character such as
  799.  the times symbol (“×”, code 215). Take care to prefix non-ASCII
  800.  string constants with “u”.
  801.  
  802.  '''
  803.  
  804.   f = open(FileName, 'wb')
  805.  
  806.   try:
  807.     f.write(Data)
  808.   finally:
  809.     f.close()
  810.  
  811.  
  812. #-------------------------------------------------------------------------------
  813. # Array access functions
  814. #-------------------------------------------------------------------------------
  815.  
  816.  
  817. def ArrayDimensions(MDArray):
  818.  
  819.   u'''Measure the size of the array in each dimension.
  820.  
  821.  If the given array is three dimensional and indexed with subscripts
  822.  [0…5][0…7][0…2], the result will be (6, 8, 3).
  823.  
  824.  The array, which in Python is really an array of arrays if it is
  825.  multidimensional, is assumed to be linear, rectangular, cuboid or
  826.  similarly regular for higher dimensions.
  827.  
  828.  An empty array has a dimension of zero.
  829.  
  830.  '''
  831.  
  832.   Result = []
  833.   Axis = MDArray
  834.   while hasattr(Axis, '__iter__'):
  835.     Result.append(len(Axis))
  836.     if len(Axis) < 1:
  837.       break
  838.     Axis = Axis[0]
  839.   Result = tuple(Result)
  840.   return Result
  841.  
  842.  
  843. #-------------------------------------------------------------------------------
  844.  
  845.  
  846. def NewArray(Dimensions, Value):
  847.  
  848.   u'''Return a new array of the given dimensions, filled with a given value.
  849.  
  850.  Dimensions may either be a tuple or a list. When creating one-dimensional
  851.  arrays, remember that the tuple or list constructor is needed around a
  852.  bare integer and that the syntax for a one-element tuple has a comma to
  853.  distinguish a tuple from an parenthesised number.
  854.  
  855.    NewArray((5,), 42) = [42, 42, 42, 42, 42]
  856.  
  857.  '''
  858.  
  859.   n = len(Dimensions)
  860.   if n < 2:
  861.     Result = [Value] * Dimensions[0]
  862.   else:
  863.     Result = []
  864.     for i in range(Dimensions[0]):
  865.       Result.append(NewArray(Dimensions[1:], Value))
  866.   return Result
  867.  
  868.  
  869. #-------------------------------------------------------------------------------
  870.  
  871.  
  872. def CopyArray(Source, CopyDepth=None):
  873.  
  874.   u'''Create an arbitrarily deep copy of an array.
  875.  
  876.  If CopyDepth is 0, the result is another reference to the same array,
  877.  not a copy. If CopyDepth is 1, a shallow copy is made. Higher values of
  878.  CopyDepth result in deeper copies. This allows one to create separately
  879.  mutable multidimensional arrays to refer to objects shared between them.
  880.  
  881.  Negative values for CopyDepth work in the manner of negative slice
  882.  indices in python: A value of -1, for example, causes the entries of
  883.  the final dimension to be shared.
  884.  
  885.  If CopyDepth is None (the default), the result will be a completely
  886.  distinct copy.
  887.  
  888.  The behaviour of this function with CopyDepth set to None is subtly
  889.  different to that for negative values of CopyDepth. A value of -1
  890.  or lower causes the ArrayDimensions() function to be invoked. The
  891.  measurement then makes the assumption of the array being regular
  892.  important.
  893.  
  894.  This function cannot be used with arrays which contain circular
  895.  references.
  896.  
  897.  '''
  898.  
  899.   #-----------------------------------------------------------------------------
  900.  
  901.   def CopyArray_Recursive(Source, d):
  902.     if d == 0 or not hasattr(Source, '__iter__'):
  903.       Result = Source
  904.     elif d == 1:
  905.       Result = [x for x in Source]
  906.     else:
  907.       Result = [CopyArray_Recursive(x, d - 1) for x in Source]
  908.     return Result
  909.  
  910.   #-----------------------------------------------------------------------------
  911.  
  912.   if CopyDepth is None:
  913.     d = -1
  914.   else:
  915.     d = CopyDepth
  916.     if d < 0:
  917.       d = max(0, len(ArrayDimensions(Source)) + d)
  918.  
  919.   return CopyArray_Recursive(Source, d)
  920.  
  921.  
  922. #-------------------------------------------------------------------------------
  923.  
  924.  
  925. def At(MDArray, Indices):
  926.  
  927.   u'''Return an array element of the given indices.
  928.  
  929.  An array element that would be accessed as A[3][1][0], say, can
  930.  be accessed as At(A, (3, 1, 0)) or as At(A, X) where X = (3, 1, 0).
  931.  This is handy for writing code that is agnostic in regard to the
  932.  number of dimensions being used.
  933.  
  934.  Remember that Indices must be a tuple, a list or something similar
  935.  and that the Python syntax for a single-element tuple is “(x,)”.
  936.  
  937.  '''
  938.  
  939.   Result = MDArray
  940.   for i in Indices:
  941.     Result = Result[i]
  942.   return Result
  943.  
  944.  
  945. #-------------------------------------------------------------------------------
  946.  
  947.  
  948. def SetAt(MDArray, Indices, Value):
  949.  
  950.   u'''Set an array element at the given indices.
  951.  
  952.  An array element that would be set with the statement A[1][5][2] = x,
  953.  say, can be set with SetAt(A, Pos, x) where Pos is (1, 5, 2).
  954.  
  955.  Remember that Indices must be a tuple, a list or something similar
  956.  and that the Python syntax for a single-element tuple is “(x,)”.
  957.  
  958.  '''
  959.  
  960.   Target = MDArray
  961.   for i in Indices[:-1]:
  962.     Target = Target[i]
  963.   Target[Indices[-1]] = Value
  964.  
  965.  
  966. #-------------------------------------------------------------------------------
  967.  
  968.  
  969. def EnumerateArray(MDArray, Depth=None):
  970.  
  971.   u'''Enumerate the indices and the elements of a multidimensional array.
  972.  
  973.  The result is a generator which can be used in the following manner:
  974.  
  975.    for Pos, Element in EnumerateArray(MDArray):
  976.      ...
  977.  
  978.  Pos is a tuple of the indices required to access the array using At
  979.  (reading) or SetAt (writing). Element is already set to At(MDArray, Pos).
  980.  
  981.  Depth, an optional argument in the constructor, allows the traversal
  982.  to be restricted to the major dimensions. In the manner of Python
  983.  array indices, Depth may be given a negative value.
  984.  
  985.  A value of -1 for Depth causes all but the last dimension to be
  986.  traversed. This can be handy for efficiently updating the contents
  987.  of a multidimensional array. If Depth is zero, the enumeration yields
  988.  nothing.
  989.  
  990.  '''
  991.  
  992.   Dim = ArrayDimensions(MDArray)
  993.  
  994.   if Depth is None:
  995.     d = len(Dim)
  996.   else:
  997.     d = Depth
  998.     if d < 0:
  999.       d += len(Dim)
  1000.     Dim = Dim[:d]
  1001.  
  1002.   Pos = [0] * len(Dim)
  1003.   i = len(Pos) - 1
  1004.  
  1005.   while i >= 0:
  1006.     yield (tuple(Pos), At(MDArray, Pos))
  1007.     while i >= 0:
  1008.       x = (Pos[i] + 1)
  1009.       if x < Dim[i]:
  1010.         Pos[i] = x
  1011.         i = len(Pos) - 1
  1012.         break
  1013.       else:
  1014.         Pos[i] = 0
  1015.         i -= 1
  1016.  
  1017.  
  1018. #-------------------------------------------------------------------------------
  1019. # Basic vector functions
  1020. #
  1021. # For serious work with linear algebra, the numpy module is useful, though
  1022. # it’s handy to have basic vector functions which work with the tuples or
  1023. # lists commonly used as vectors.
  1024. #
  1025. # Vectors are assumed to be column vectors represented by zero-based tuples
  1026. # such that
  1027. #
  1028. #       ⎡x⎤
  1029. #   A = ⎢y⎥ implies A[0] = x, A[1] = y and A[2] = z.
  1030. #       ⎣z⎦
  1031. #
  1032. # In program code, A = (x, y, z). Though lists are acceptable as arguments,
  1033. # tuples are preferred because they consume less memory, are handled much
  1034. # more quickly than lists and most importantly, are free from the risk of
  1035. # aliasing bugs. Some aliasing bugs, such as which result from using a list
  1036. # as a default argument in a function can be surprising.
  1037. #
  1038. # The vector functions here assume the vector operands have the correct or
  1039. # consistent number of dimensions.
  1040. #
  1041. #-------------------------------------------------------------------------------
  1042.  
  1043.  
  1044. def VZeros(n):
  1045.  
  1046.   u'''Return an n-dimensional vector of zeros.'''
  1047.  
  1048.   return (0.0,) * n
  1049.  
  1050.  
  1051. #-------------------------------------------------------------------------------
  1052.  
  1053.  
  1054. def VOnes(n):
  1055.  
  1056.   u'''Return an n-dimensional vector of ones.'''
  1057.  
  1058.   return (1.0,) * n
  1059.  
  1060.  
  1061. #-------------------------------------------------------------------------------
  1062.  
  1063.  
  1064. def VStdBasis(Dimensions, Index, Scale=1.0):
  1065.  
  1066.   u'''Return a standard basis vector, optionally scaled.
  1067.  
  1068.  The result is a vector with Dimension elements, all zero except
  1069.  for a 1 at the given (zero-based) index. If the argument Scale
  1070.  is specified, Scale is used instead of the 1.
  1071.  
  1072.  VStdBasis(4, 2, 5) returns
  1073.  
  1074.    ⎡0⎤
  1075.    ⎢0⎥
  1076.    ⎢5⎥
  1077.    ⎣0⎦.
  1078.  
  1079.  Note that it’s common to use a one-based subscript to identify the
  1080.  basis vectors: The basis vectors e₁, e₂, e₃ correspond to x, y, z.
  1081.  This function uses zero-based indices to be compatible with the
  1082.  zero-based indices of lists and tuples in Python.
  1083.  
  1084.  '''
  1085.  
  1086.   return((0.0,) * Index) + (Scale,) + ((0.0,) * (Dimensions - 1 - Index))
  1087.  
  1088.  
  1089. #-------------------------------------------------------------------------------
  1090.  
  1091.  
  1092. def VDim(A, n):
  1093.  
  1094.   u'''Procrastinate a vector to the required number of dimensions.'''
  1095.  
  1096.   d = len(A)
  1097.   if n > d:
  1098.     # To the rack!
  1099.     Result = tuple(A) + (0.0,) * (n - d)
  1100.   elif n < d:
  1101.     # To the guillotine!
  1102.     Result = tuple(A)[:n]
  1103.   else:
  1104.     # No help is needed from Procrastus.
  1105.     Result = tuple(A)
  1106.   return Result
  1107.  
  1108.  
  1109. #-------------------------------------------------------------------------------
  1110.  
  1111.  
  1112. def VAug(A, *x):
  1113.  
  1114.   u'''Return vector augmented with one or more extra components.
  1115.  
  1116.  The extra components are supplied with one or more arguments after the
  1117.  given vector. If the extra componenets are to come from a vector, use
  1118.  the tuple unpacking operator “*”.
  1119.  
  1120.  Though Python’s array handling functions make this a trivial operation,
  1121.  it’s nice to avoid the use of the "+" operator in a way that might imply
  1122.  arithmetic addition to a bleary-eyed programmer.
  1123.  
  1124.  '''
  1125.  
  1126.   return tuple(A) + x
  1127.  
  1128.  
  1129. #-------------------------------------------------------------------------------
  1130.  
  1131.  
  1132. def VMajorAxis(A):
  1133.  
  1134.   u'''Return the index of the largest component of a vector.
  1135.  
  1136.  This function is handy for choosing numerically robust Cartesian-to-Euler
  1137.  conversion functions.
  1138.  
  1139.  '''
  1140.  
  1141.   MajorAbsValue = 0
  1142.   MajorIx = 0
  1143.  
  1144.   Ix = 0
  1145.  
  1146.   while Ix < len(A):
  1147.     AbsValue = abs(A[Ix])
  1148.     if AbsValue > MajorAbsValue:
  1149.       MajorAbsValue = AbsValue
  1150.       MajorIx = Ix
  1151.     Ix += 1
  1152.  
  1153.   return MajorIx
  1154.  
  1155.  
  1156. #-------------------------------------------------------------------------------
  1157.  
  1158.  
  1159. def VNeg(A):
  1160.  
  1161.   u'''Return −A, the negative of a vector.
  1162.  
  1163.  A negated vector has the same size as the original, but points in the
  1164.  opposite direction.
  1165.  
  1166.  '''
  1167.  
  1168.   return tuple(-x for x in A)
  1169.  
  1170.  
  1171. #-------------------------------------------------------------------------------
  1172.  
  1173.  
  1174. def VSum(*VectorArgs):
  1175.  
  1176.   u'''Return the sum of vectors.
  1177.  
  1178.  The result of the sum of vectors can be described as the result of
  1179.  moving in the direction and by the distance implied by each vector
  1180.  in turn. Thus if A is 4m due east and B is 3m due north, the sum of
  1181.  A and B is 4m east and 3m north (5m at a nautical bearing of 53.13°).
  1182.  
  1183.  Vector addition is both commutative and associative.
  1184.  
  1185.  In the manner of the built-in function max(), this function can take
  1186.  either two or more vector arguments or a single argument which contains
  1187.  a list or tuple of vectors.
  1188.  
  1189.  '''
  1190.  
  1191.   if len(VectorArgs) == 1:
  1192.     Vectors = VectorArgs[0]
  1193.   else:
  1194.     Vectors = VectorArgs
  1195.   Result = tuple(Vectors[0])
  1196.   for i in range(1, len(Vectors)):
  1197.     Result = tuple(a + b for a, b in zip(Result, Vectors[i]))
  1198.   return Result
  1199.  
  1200.  
  1201. #-------------------------------------------------------------------------------
  1202.  
  1203.  
  1204. def VDiff(A, B):
  1205.  
  1206.   u'''Return A − B, the difference between two vectors.
  1207.  
  1208.  Just as in ordinary arithmetic, A − B = A + −B. If A and B are position
  1209.  vectors, the result is the position of A relative to B. Likewise, if A
  1210.  and B are velocity vectors, the result is the velocity of A relative
  1211.  to B.
  1212.  
  1213.  '''
  1214.  
  1215.   return tuple(x - y for x, y in zip(A, B))
  1216.  
  1217.  
  1218. #-------------------------------------------------------------------------------
  1219.  
  1220.  
  1221. def VSchur(*VectorArgs):
  1222.  
  1223.   u'''Return the Schur product of vectors.
  1224.  
  1225.  The Schur or Hadamard product of two vectors is the element-wise
  1226.  product of the vectors.
  1227.  
  1228.    ⎡4⎤ ⎡ 3⎤   ⎡ 12⎤
  1229.    ⎢7⎥○⎢−2⎥ = ⎢−14⎥
  1230.    ⎣3⎦ ⎣ 3⎦   ⎣ 9 ⎦
  1231.  
  1232.  In the manner of the built-in function max(), this function can take
  1233.  either two or more vector arguments or a single argument which contains
  1234.  a list or tuple of vectors.
  1235.  
  1236.  '''
  1237.  
  1238.   if len(VectorArgs) == 1:
  1239.     Vectors = VectorArgs[0]
  1240.   else:
  1241.     Vectors = VectorArgs
  1242.   Result = tuple(Vectors[0])
  1243.   for i in range(1, len(Vectors)):
  1244.     Result = tuple(a * b for a, b in zip(Result, Vectors[i]))
  1245.   return Result
  1246.  
  1247.  
  1248. #-------------------------------------------------------------------------------
  1249.  
  1250.  
  1251. def VDot(A, B):
  1252.  
  1253.   u'''Return A·B, the dot product of two vectors.
  1254.  
  1255.  The dot product of two vectors is a scalar (a number from the set of reals)
  1256.  that is a product of each of lengths of the two vectors and of the cosine
  1257.  of the angle between them. If one vector is a unit vector (a vector whose
  1258.  length is exactly 1), the dot product of that and the other vector will
  1259.  indicate the length of the projection along the unit vector cast by the
  1260.  other vector.
  1261.  
  1262.  The dot product of two unit vectors gives the cosine of the angle between
  1263.  them. Thus:
  1264.  
  1265.    +1 ⇒ Parallel
  1266.     0 ⇒ Perpendicular
  1267.    −1 ⇒ Antiparallel
  1268.  
  1269.  Similarly, the sign of the dot product of any two vectors indicates
  1270.  whether they are at an acute, right or obtuse angle.
  1271.  
  1272.  The dot product of two (Cartesian) vectors is the sum of the elements
  1273.  of the Schur product of those vectors.
  1274.  
  1275.  '''
  1276.  
  1277.   return sum(x * y for x, y in zip(A, B))
  1278.  
  1279.  
  1280. #-------------------------------------------------------------------------------
  1281.  
  1282.  
  1283. def VLengthSquared(A):
  1284.  
  1285.   u'''Return ‖A‖², the square of the length of a vector.
  1286.  
  1287.  The square of the length of a vector A is A·A and thus requires no
  1288.  transcendental functions to compute whereas computing the length of
  1289.  a vector requires a call to the square root function.
  1290.  
  1291.  It is usually more efficient to test the square of a vector’s length
  1292.  against the square of a threshold than to test that vector’s length
  1293.  against the threshold.
  1294.  
  1295.  '''
  1296.  
  1297.   return sum(x * x for x in A)
  1298.  
  1299.  
  1300. #-------------------------------------------------------------------------------
  1301.  
  1302.  
  1303. def VLength(A):
  1304.  
  1305.   u'''Return ‖A‖, the length of a vector.
  1306.  
  1307.  The length of a vector is the square root of the sum of the squares of its
  1308.  Cartesian components in the manner of Pythagoras. Very often, ‖A‖² is
  1309.  needed. To save calling the (transcendental) square root function and
  1310.  squaring the result, use the VLengthSquared() function wherever possible.
  1311.  
  1312.  '''
  1313.  
  1314.   return sqrt(VLengthSquared(A))
  1315.  
  1316.  
  1317. #-------------------------------------------------------------------------------
  1318.  
  1319.  
  1320. def VManhattan(A):
  1321.  
  1322.   u'''Return the Manhattan (or taxicab) length of a vector.
  1323.  
  1324.  The Manhattan length is the sum of the absolute value of the vector’s
  1325.  components. It represents the distance from the origin one must travel
  1326.  to reach a given location when only north-south and east-west travel
  1327.  is permitted.
  1328.  
  1329.  This function is often used to find a rough approximation to the length
  1330.  of a vector.
  1331.  
  1332.  '''
  1333.  
  1334.   return sum(abs(x) for x in A)
  1335.  
  1336.  
  1337. #-------------------------------------------------------------------------------
  1338.  
  1339.  
  1340. def VScaled(A, Scale):
  1341.  
  1342.   u'''Return the scalar multiple of a vector.
  1343.  
  1344.  A scalar multiple of a vector is one that is parallel to the original
  1345.  vector (antiparallel in the case of a negative scale) but whose length
  1346.  is the original vector’s length multiplied by the absolute value of scale.
  1347.  
  1348.  Often a vector of a desired length is constructed from a unit vector
  1349.  and a scalar. A vehicle with a position P and an orientation given by
  1350.  a unit heading vector H made to travel a distance d in a straight line
  1351.  will arrive at P + d(H) where d(H) is H scaled by d.
  1352.  
  1353.  '''
  1354.  
  1355.   return tuple(x * Scale for x in A)
  1356.  
  1357.  
  1358. #-------------------------------------------------------------------------------
  1359.  
  1360.  
  1361. def VNormalised(A):
  1362.  
  1363.   u'''Return ‖A‖⁻¹(A), a unit vector parallel to the given vector.
  1364.  
  1365.  Unit vectors are vectors with a length (or "norm") of one. They are often
  1366.  used as basis vectors for defining coordinate systems or for indicating
  1367.  direction without magnitude.
  1368.  
  1369.  No exception is raised if the vector is null or an overflow results.
  1370.  Instead, a sensible-looking, made-up unit vector is returned.
  1371.  
  1372.  '''
  1373.  
  1374.   try:
  1375.     Result = VScaled(A, 1.0 / VLength(A))
  1376.   except (ZeroDivisionError, OverflowError):
  1377.     Result = VStdBasis(len(A), VMajorAxis(A))
  1378.   return Result
  1379.  
  1380.  
  1381. #-------------------------------------------------------------------------------
  1382.  
  1383.  
  1384. def VPerp(A):
  1385.  
  1386.   u'''Return the original 2D vector rotated by positive 90 degrees.
  1387.  
  1388.  Rotation is always positive when a point lying on the positive
  1389.  half of the first axis moves in the direction of the positive
  1390.  half of the second axis. On a graph where x is right and y is
  1391.  up, the perpendicular vector returned is anticlockwise from the
  1392.  original vector.
  1393.  
  1394.  The operator is written as superscript “up tack” to the right of
  1395.  the vector. In a manner similar to that of the 3D cross product
  1396.  operator, a mnemonic for the calculation is a construct resembling
  1397.  the determinant of a matrix:
  1398.  
  1399.    Perp(A) = ⎢ Ax  Ay  ⎥
  1400.              ⎢ <x> <y> ⎥
  1401.  
  1402.  '''
  1403.  
  1404.   return (-A[1], A[0])
  1405.  
  1406.  
  1407. #-------------------------------------------------------------------------------
  1408.  
  1409.  
  1410. def VRevPerp(A):
  1411.  
  1412.   u'''Return the original 2D vector rotated by negative 90 degrees.
  1413.  
  1414.  See VPerp().
  1415.  
  1416.  '''
  1417.  
  1418.   return (A[1], -A[0])
  1419.  
  1420.  
  1421. #-------------------------------------------------------------------------------
  1422.  
  1423.  
  1424. def VCrossProduct(A, B):
  1425.  
  1426.   u'''Return A × B, the vector cross product of two 3D vectors.
  1427.  
  1428.  The returned vector is perpendicular to both A and B, in the direction
  1429.  given by the left hand rule or the right hand rule, depending on the
  1430.  hand of the coordinate system used. The length of the resulting vector
  1431.  is proportional to the length of A, the length of B and the sine of the
  1432.  angle between A and B.
  1433.  
  1434.  The length of the cross product is the area of a parallelogram spanned
  1435.  by A and B. The area of a triangle spanned by A and B is half this value,
  1436.  a useful thing to know when it comes to calculating the area of a polygon.
  1437.  
  1438.  A mnemonic for calculating the cross product is a construction resembling
  1439.  the determinant of a matrix:
  1440.  
  1441.            ⎢ Ax  Ay  Az  ⎥
  1442.    A × B = ⎢ Bx  By  Bz  ⎥
  1443.            ⎢ <x> <y> <z> ⎥
  1444.  
  1445.   Some linear algebra textbooks have the standard basis vectors along the
  1446.   top row rather than the bottom row. Although that form of the determinant
  1447.   mnemonic yields the correct result in 3D or higher odd-numbered dimensions,
  1448.   the form shown above works just as well when it is applied to 2D or higher
  1449.   even-numbered dimensions.
  1450.  
  1451.   Remember that the cross product operator is neither commutative nor
  1452.   associative. In fact, A × B = −(B × A).
  1453.  
  1454.  '''
  1455.  
  1456.   return (
  1457.     (A[1] * B[2]) - (A[2] * B[1]),
  1458.     (A[2] * B[0]) - (A[0] * B[2]),
  1459.     (A[0] * B[1]) - (A[1] * B[0])
  1460.   )
  1461.  
  1462.  
  1463. #-------------------------------------------------------------------------------
  1464.  
  1465.  
  1466. def VCrossProduct4D(A, B, C):
  1467.  
  1468.   u'''Return the four-dimensional cross product of three 4D vectors.
  1469.  
  1470.  Explaining this function would require four dimensional hands or the help
  1471.  of one of the Centaurs in Greg Egan’s book Diaspora but the result can be
  1472.  calculated from this determinant-shaped mnemonic:
  1473.  
  1474.    ⎢ Ax  Ay  Az  Aw  ⎥
  1475.    ⎢ Bx  By  Bz  Bw  ⎥
  1476.    ⎢ Cx  Cy  Cz  Cw  ⎥
  1477.    ⎢ <x> <y> <z> <w> ⎥
  1478.  
  1479.  '''
  1480.  
  1481.   x = -(
  1482.     A[1] * (B[2] * C[3] - B[3] * C[2]) -
  1483.     A[2] * (B[1] * C[3] - B[3] * C[1]) +
  1484.     A[3] * (B[1] * C[2] - B[2] * C[1])
  1485.   )
  1486.  
  1487.   y = (
  1488.     A[0] * (B[2] * C[3] - B[3] * C[2]) -
  1489.     A[2] * (B[0] * C[3] - B[3] * C[0]) +
  1490.     A[3] * (B[0] * C[2] - B[2] * C[0])
  1491.   )
  1492.  
  1493.   z = -(
  1494.     A[0] * (B[1] * C[3] - B[3] * C[1]) -
  1495.     A[1] * (B[0] * C[3] - B[3] * C[0]) +
  1496.     A[3] * (B[0] * C[1] - B[1] * C[0])
  1497.   )
  1498.  
  1499.   w = (
  1500.     A[0] * (B[1] * C[2] - B[2] * C[1]) -
  1501.     A[1] * (B[0] * C[2] - B[2] * C[0]) +
  1502.     A[2] * (B[0] * C[1] - B[1] * C[0])
  1503.   )
  1504.  
  1505.   return (x, y, z, w)
  1506.  
  1507.  
  1508. #-------------------------------------------------------------------------------
  1509.  
  1510.  
  1511. def VScalarTripleProduct(A, B, C):
  1512.  
  1513.   u'''Return A·B×C, the scalar triple product of three 3D vectors.
  1514.  
  1515.  The result is a scalar whose absolute value happens to be the volume of a
  1516.  parallelepiped spanned by the three vectors. The volume of a tethrahedron
  1517.  spanned by the same vectors is 1/6 of this value, a useful thing to know
  1518.  when it comes to finding the volume of a polyhedron.
  1519.  
  1520.  The effect of reordering the arguments is only a possible change of sign
  1521.  of the result. If A, B and C follow the right-hand rule in a right-handed
  1522.  coordinate system, the result will be positive.
  1523.  
  1524.  If the result is zero, two or all three of the vectors are co-planar.
  1525.  
  1526.  u'''
  1527.  
  1528.   return VDot(A, VCrossProduct(B, C))
  1529.  
  1530.  
  1531. #-------------------------------------------------------------------------------
  1532.  
  1533.  
  1534. def VVectorTripleProduct(A, B, C):
  1535.  
  1536.   u'''Return A×(B×C), the vector triple product of three vectors.
  1537.  
  1538.  This function is included to keep VScalarTripleProduct from getting
  1539.  lonely. Apparently it has some meaning in physics.
  1540.  
  1541.  There are two forms of the vector triple product:
  1542.  
  1543.    A×(B×C) = B(A·C) − C(A·B)
  1544.    (A×B)×C = B(A·C) − A(C·B)
  1545.  
  1546.  This function returns the first one, Lagrange’s form.
  1547.  
  1548.  u'''
  1549.  
  1550.   return VDiff(
  1551.     VScaled(B, VDot(A, C)),
  1552.     VScaled(C, VDot(A, B))
  1553.   )
  1554.  
  1555.  
  1556. #-------------------------------------------------------------------------------
  1557.  
  1558.  
  1559. def VProjectionOnto(Axis, Vector):
  1560.  
  1561.   u'''Return the component of Vector that lies along Axis.
  1562.  
  1563.  Axis need not be normalised.
  1564.  
  1565.             A·V
  1566.  Proj (V) = ―――(A)
  1567.      A      A·A
  1568.  
  1569.  Note that no square root calculation is necessary.
  1570.  
  1571.  The component of Vector that is perpendicular to Axis can be
  1572.  found by subtracting the result of this function from Vector.
  1573.  
  1574.  If Axis is null or an overflow occurs, no exception is raised.
  1575.  Instead, a null vector is returned.
  1576.  
  1577.  '''
  1578.  
  1579.   try:
  1580.     Result = VScaled(
  1581.       Axis,
  1582.       VDot(Vector, Axis) / (1.0 * VLengthSquared(Axis))
  1583.     )
  1584.   except (ZeroDivisionError, OverflowError):
  1585.     Result = VZeros(len(Vector))
  1586.   return Result
  1587.  
  1588.  
  1589. #-------------------------------------------------------------------------------
  1590.  
  1591.  
  1592. def VDiagonalMAV(Diagonals):
  1593.  
  1594.     u'''Create a matrix-as-vectors with the entries for the main diagonal set.
  1595.  
  1596.                              ⎡1 0 0⎤   ⎛⎡1⎤ ⎡0⎤ ⎡0⎤⎞
  1597.    VDiagonalMAV((1, 3, 7)) = ⎢0 3 0⎥ = ⎜⎢0⎥,⎢3⎥,⎢0⎥⎟
  1598.                              ⎣0 0 7⎦   ⎝⎣0⎦ ⎣0⎦ ⎣7⎦⎠
  1599.  
  1600.    Because a diagonal matrix is its own transpose, the result can be thought
  1601.    of as either an array of column vectors or an array of row vectors. Here,
  1602.    the convention is that vectors are considered column vectors and matrices
  1603.    are arrays of column vectors.
  1604.  
  1605.    '''
  1606.  
  1607.     return tuple(
  1608.       VStdBasis(len(Diagonals), i, x) for i, x in enumerate(Diagonals)
  1609.     )
  1610.  
  1611.  
  1612. #-------------------------------------------------------------------------------
  1613.  
  1614.  
  1615. def VTransposedMAV(MtxAsVectors):
  1616.  
  1617.   u'''Return the transpose of a square matrix defined as a bunch of vectors.
  1618.  
  1619.  A matrix entry at i,j corresponds to the entries j,i in the transpose.
  1620.  
  1621.                  ⎛⎡a⎤ ⎡d⎤ ⎡g⎤⎞   ⎛⎡a⎤ ⎡b⎤ ⎡c⎤⎞
  1622.    VTransposedMAV⎜⎢b⎥,⎢e⎥,⎢h⎥⎟ = ⎜⎢d⎥,⎢e⎥,⎢f⎥⎟
  1623.                  ⎝⎣c⎦ ⎣f⎦ ⎣i⎦⎠   ⎝⎣g⎦ ⎣h⎦ ⎣i⎦⎠
  1624.  
  1625.  Note that matrix subscripts like i,j in maths refers to row i, column j
  1626.  (even where column vectors are used) while indexing elements of a matrix
  1627.  made of column vectors in Python requires subscripts of the form [j][i]
  1628.  (though i and j would both start at zero instead of one).
  1629.  
  1630.  '''
  1631.  
  1632.   DimRange = range(len(MtxAsVectors))
  1633.   return tuple(tuple(MtxAsVectors[j][i] for j in DimRange) for i in DimRange)
  1634.  
  1635.  
  1636. #-------------------------------------------------------------------------------
  1637.  
  1638.  
  1639. def VRectToPol(A):
  1640.  
  1641.   u'''Return the polar (distance, angle) form of a Cartesian (x, y) vector.
  1642.  
  1643.  The angle is given in radians y-from-x and ranges from −pi to +pi.
  1644.  If the given vector is null (0, 0), the result is also (0, 0).
  1645.  
  1646.  '''
  1647.  
  1648.   Distance = VLength(A)
  1649.   Angle = atan2(A[1], A[0])
  1650.   return (Distance, Angle)
  1651.  
  1652.  
  1653. #-------------------------------------------------------------------------------
  1654.  
  1655.  
  1656. def VPolToRect(DistanceAngle):
  1657.  
  1658.   u'''Return the Cartesian (x, y) form of a polar (distance, angle) vector.
  1659.  
  1660.  The angle is taken to be in radians y-from-x.
  1661.  
  1662.  '''
  1663.  
  1664.   c = cos(DistanceAngle[1])
  1665.   s = sin(DistanceAngle[1])
  1666.   return (DistanceAngle[0] * c, DistanceAngle[0] * s)
  1667.  
  1668.  
  1669. #-------------------------------------------------------------------------------
  1670.  
  1671.  
  1672. def VLerp(A, B, Progress):
  1673.  
  1674.   u'''Linearly interpolate between two vectors.
  1675.  
  1676.  If Progress is 0.0, A is returned. If Progress is 1.0, B is returned.
  1677.  A value of Progress between 0.0 and 1.0 yields a result that is a linear
  1678.  interpolation of A and B.
  1679.  
  1680.  This function can be used to blend colours, though if non-linear RGB
  1681.  values (such as stored in computer frame buffers) are used, it is
  1682.  important to convert them to linear RGB values in order to perform
  1683.  interpolation (blending) or indeed any meaningful colour arithmetic.
  1684.  
  1685.  Progress is not restricted to the range 0.0 to 1.0. The result of using
  1686.  a value for Progress that is less than zero or greater than one is a
  1687.  linear extrapolation.
  1688.  
  1689.  '''
  1690.  
  1691.   return VSum(
  1692.     VScaled(A, 1.0 - Progress),
  1693.     VScaled(B, Progress)
  1694.   )
  1695.  
  1696.  
  1697. #-------------------------------------------------------------------------------
  1698. # Mathematical functions
  1699. #-------------------------------------------------------------------------------
  1700.  
  1701.  
  1702. def BinomialCoefficient(n, k):
  1703.  
  1704.   u'''Return n-choose-k.
  1705.  
  1706.          k
  1707.      0 1 2 3 4⋯
  1708.     ┌─────────
  1709.    0│1
  1710.    1│1 1
  1711.  n 2│1 2 1
  1712.    3│1 3 3 1
  1713.    4│1 4 6 4 1
  1714.       ⋮
  1715.  
  1716.  The binomial coefficients are the coefficients that result from
  1717.  the expansion of (x + 1)ⁿ, e.g., 1x⁴ + 4x³ + 6x² + 4x + 1 for n = 4.
  1718.  
  1719.  '''
  1720.  
  1721.   if k >= 0 and k <= n:
  1722.  
  1723.     # The basic algorithm is now sufficiently guarded. All
  1724.     # the other “if” statements are merely for optimisation.
  1725.  
  1726.     if k == 0 or k == n:
  1727.  
  1728.       Result = 1
  1729.  
  1730.     else:
  1731.  
  1732.       if n < 4:
  1733.  
  1734.         Result = n
  1735.  
  1736.       else:
  1737.  
  1738.         if k <= (n // 2):
  1739.           j = k
  1740.         else:
  1741.           j = n - k
  1742.  
  1743.         # The basic algorithm begins here (but using j instead of k).
  1744.  
  1745.         Numerator = 1
  1746.         Divisor = 1
  1747.         i = 1
  1748.  
  1749.         while i <= j:
  1750.           Numerator *= (n - j + i)
  1751.           Divisor *= i
  1752.           i += 1
  1753.  
  1754.         Result = Numerator / Divisor
  1755.  
  1756.   else:
  1757.  
  1758.     Result = 0
  1759.  
  1760.   return Result
  1761.  
  1762.  
  1763. #-------------------------------------------------------------------------------
  1764.  
  1765.  
  1766. def BinomialRow(n):
  1767.  
  1768.   u'''Return an entire row of binomial coefficients.'''
  1769.  
  1770.   if n < 1:
  1771.  
  1772.     if n == 0:
  1773.       Result = (1,)
  1774.     else:
  1775.       Result = ()
  1776.  
  1777.   else:
  1778.  
  1779.     R = [1] + n * [0]
  1780.     i = 1
  1781.  
  1782.     while i <= n:
  1783.       j = i - 1
  1784.       while j >= 1:
  1785.         R[j] = R[j] + R[j - 1]
  1786.         j -= 1
  1787.       R[i] = 1
  1788.       i += 1
  1789.  
  1790.     Result = tuple(R)
  1791.  
  1792.   return Result
  1793.  
  1794.  
  1795. #-------------------------------------------------------------------------------
  1796.  
  1797.  
  1798. def NextBinomialRow(Row):
  1799.  
  1800.   u'''Return the successive row of binomial coefficients.
  1801.  
  1802.  The given row must be a tuple or list of valid binomial coefficients.
  1803.  The first valid row, then, is a list containing 1 (or 1.0).
  1804.  
  1805.  '''
  1806.  
  1807.   w = len(Row)
  1808.  
  1809.   if w > 0:
  1810.  
  1811.     R = [1]
  1812.     k = 1
  1813.  
  1814.     while k < len(Row):
  1815.       R.append(Row[k - 1] + Row[k])
  1816.       k += 1
  1817.     R.append(Row[0])
  1818.  
  1819.     Result = tuple(R)
  1820.  
  1821.   else:
  1822.  
  1823.     Result = (1,)
  1824.  
  1825.   return Result
  1826.  
  1827.  
  1828.  
  1829. #-------------------------------------------------------------------------------
  1830.  
  1831.  
  1832. def ApproxSaggita(ArcLength, ChordLength):
  1833.  
  1834.   u'''Approximate the saggita of a circular arc, given arc and chord lengths.
  1835.  
  1836.  The saggita is the distance between the midpoint of the arc and
  1837.  the midpoint of the chord, the line connecting the arc’s endpoints.
  1838.  If the arc is imagined as a bow and the chord the string, the saggita
  1839.  is the arrow. (“Saggita” means arrow.)
  1840.  
  1841.  '''
  1842.  
  1843.   return sqrt(0.1875 * (ArcLength * ArcLength - ChordLength * ChordLength))
  1844.  
  1845.  
  1846. #-------------------------------------------------------------------------------
  1847.  
  1848.  
  1849. def LGQIntegral25(Interval, Fn, InitalParams, FinalParams):
  1850.  
  1851.   u'''Perform 25-point Legendre–Gauss quadrature integration on a function.
  1852.  
  1853.  Interval is the range of integration in the form (x-start, x-finish).
  1854.  Fn is the function to be integrated, called with varying values of x,
  1855.  as determined by a table of Legendre–Gauss quadrature abscissae.
  1856.  InitialParams, x and FinalParams form the arguments for the function Fn.
  1857.  
  1858.  '''
  1859.  
  1860.   # The Gauss quadrature rule only integrates ∫f(x).dx over [−1,+1].
  1861.   # To integrate over [a,b], we must use ½(b−a)∫f(½(b−a)x + ½(b+a)).dx.
  1862.   # Thus to integrate over [0,t], we use ½t∫f(½tx + ½t).dx.
  1863.  
  1864.   a, b = Interval
  1865.   Scale = 0.5 * (b - a)
  1866.   Offset = 0.5 * (b + a)
  1867.  
  1868.   # Assemble the parameters. Just so the parameters don’t have to be
  1869.   # repeatedly concatenated, a single entry in the composite argument
  1870.   # list is marked for repeated overwriting with the value of x.
  1871.  
  1872.   IP = list(InitalParams)
  1873.   FP = list(FinalParams)
  1874.   Params = IP + [None] + FP
  1875.   XParamIx = len(IP)
  1876.  
  1877.   WFSum = 0.0
  1878.   i = 25
  1879.  
  1880.   while i > 0:
  1881.     i -= 1
  1882.     x = Scale * LGQAbscissae25[i] + Offset;
  1883.     Params[XParamIx] = x
  1884.     WFSum += LGQWeights25[i] * Fn(*Params)
  1885.  
  1886.   return Scale * WFSum
  1887.  
  1888. #-------------------------------------------------------------------------------
  1889.  
  1890.  
  1891. def IntegralFunctionOfPLF(Vertices, C=0.0):
  1892.  
  1893.   u'''Return QPF data for the integral of a piecewise linear function.
  1894.  
  1895.  The piecewise linear function is specified with a list of (x,y) vertices,
  1896.  the x components of which must be monotonically increasing. The result is
  1897.  a more complicated structure representing a piecewise quadratic function
  1898.  that is the integral of the given function.
  1899.  
  1900.  The linear function is assumed to be zero outside of the domain implied
  1901.  by the list of vertices. Thus C is both the integration constant and the
  1902.  value of the integral for the minimum value of x in the domain.
  1903.  
  1904.  The result is a list of ((x₀, S₀), (x₁, S₁), (a, b, c)) tuples where
  1905.  (x₀, S₀) is the start point of a segment, (x₁, S₁) is the segment’s
  1906.  end point and a, b and c are the coefficients of the polynomial
  1907.  S(x) = ax² + bx + c for that segment.
  1908.  
  1909.  '''
  1910.  
  1911.   Result = []
  1912.   S0 = C
  1913.   i = 0
  1914.  
  1915.   while i + 1 < len(Vertices):
  1916.  
  1917.     x0, y0 = Vertices[i]
  1918.     x1, y1 = Vertices[i + 1]
  1919.     dx = x1 - x0
  1920.     dy = y1 - y0
  1921.  
  1922.     if dx > 0.0:
  1923.  
  1924.       g = float(dy) / float(dx)
  1925.  
  1926.       # Find a, b and c in the expression ax² + bx + c.
  1927.       a = 0.5 * g
  1928.       b = y0 - g * x0
  1929.       c0 = x0 * (x0 * a + b)
  1930.       c = S0 - c0
  1931.  
  1932.       S1 = x1 * (x1 * a + b) + c
  1933.       Result.append(((x0, S0), (x1, S1), (a, b, c)))
  1934.       S0 = S1
  1935.  
  1936.     i += 1
  1937.  
  1938.   return Result
  1939.  
  1940.  
  1941. #-------------------------------------------------------------------------------
  1942.  
  1943.  
  1944. def EvaluatePQF(PQF, x):
  1945.  
  1946.   u'''Evaluate a piecewise quadratic function at x.
  1947.  
  1948.  The function is specified with a list of ((x₀, y₀), (x₁, y₁), (a, b, c))
  1949.  tuples where (x₀, y₀) is the start point of a segment, (x₁, y₁) is the
  1950.  segment’s end point and a, b and c are the coefficients of the polynomial
  1951.  y = ax² + bx + c for that segment.
  1952.  
  1953.  The segments must be continuous and monotonically increasing in x.
  1954.  
  1955.  '''
  1956.  
  1957.   FirstPoint = PQF[0][0]
  1958.  
  1959.   if x <= FirstPoint[0]:
  1960.  
  1961.     Result = FirstPoint[1]
  1962.  
  1963.   else:
  1964.  
  1965.     FinalPoint = PQF[-1][1]
  1966.  
  1967.     if x >= FinalPoint[0]:
  1968.  
  1969.       Result = FinalPoint[1]
  1970.  
  1971.     else:
  1972.  
  1973.       # Start with initial inclusive index bounds.
  1974.       LowIx = 0
  1975.       HighIx = len(PQF) - 1
  1976.  
  1977.       while HighIx > LowIx:
  1978.         MidIx = LowIx + ((HighIx - LowIx) // 2)
  1979.         x1 = PQF[MidIx][1][0]
  1980.         if x1 <= x:
  1981.           LowIx = MidIx + 1
  1982.         else:
  1983.           HighIx = MidIx
  1984.       # Whatever happens, LowIx = HighIx
  1985.  
  1986.       # The quadratic function is given as the coefficients in ax² + bx + c.
  1987.       QF = PQF[LowIx]
  1988.       a, b, c = QF[2]
  1989.       Result = x * (x * a + b) + c
  1990.  
  1991.   return Result
  1992.  
  1993.  
  1994. #-------------------------------------------------------------------------------
  1995.  
  1996.  
  1997. def EvaluateInvPQF(PQF, y):
  1998.  
  1999.   u'''Evaluate at y, the inverse of a piecewise quadratic function.
  2000.  
  2001.  The (non-inverted) function is specified with a list of ((x₀, y₀),
  2002.  (x₁, y₁), (a, b, c)) tuples where (x₀, y₀) is the start point of a
  2003.  segment, (x₁, y₁) is the segment’s end point and a, b and c are the
  2004.  coefficients of the polynomial y = ax² + bx + c for that segment.
  2005.  
  2006.  The segments must be continuous and monotonically increasing in both
  2007.  x and y.
  2008.  
  2009.  '''
  2010.  
  2011.   FirstPoint = PQF[0][0]
  2012.  
  2013.   if y <= FirstPoint[1]:
  2014.  
  2015.     Result = FirstPoint[0]
  2016.  
  2017.   else:
  2018.  
  2019.     FinalPoint = PQF[-1][1]
  2020.  
  2021.     if y >= FinalPoint[1]:
  2022.  
  2023.       Result = FinalPoint[0]
  2024.  
  2025.     else:
  2026.  
  2027.       # Start with initial inclusive index bounds.
  2028.       LowIx = 0
  2029.       HighIx = len(PQF) - 1
  2030.  
  2031.       while HighIx > LowIx:
  2032.         MidIx = LowIx + ((HighIx - LowIx) // 2)
  2033.         y1 = PQF[MidIx][1][1]
  2034.         if y > y1:
  2035.           LowIx = MidIx + 1
  2036.         else:
  2037.           HighIx = MidIx
  2038.       # Whatever happens, LowIx = HighIx
  2039.  
  2040.       QF = PQF[LowIx]
  2041.  
  2042.       # The formula for roots of quadratics is x = (−b ± √(b² - 4ac))/(2a)
  2043.       # so x = (−b ± √(b² - 4a(c - y)))/(2a).
  2044.  
  2045.       a, b, c = QF[2]
  2046.  
  2047.       if abs(a) < 1e-6:
  2048.         # The segment is close to linear.
  2049.         Result = (y - c) / b
  2050.       else:
  2051.         # The segment is parabolic.
  2052.         det = max(0.0, b * b - 4 * a * (c - y))
  2053.         Result = (-b + sqrt(det)) / (2 * a)
  2054.  
  2055.   return Result
  2056.  
  2057.  
  2058. #-------------------------------------------------------------------------------
  2059. # Linear intersection functions
  2060. #-------------------------------------------------------------------------------
  2061.  
  2062.  
  2063. def LineParameter(Point, Line):
  2064.  
  2065.   u'''Return a point’s corresponding parameter of a linear parametric function.
  2066.  
  2067.  The line is specified with two points, the first of which maps to
  2068.  parameter t = 0 and the second of which maps to t = 1. The point’s
  2069.  projection onto the line rather than the point itself is measured.
  2070.  
  2071.  The line is considered to be infinite in extent and the result may
  2072.  have a value less thank zero or greater than one.
  2073.  
  2074.  If the length of the line is zero or is very small, an exception is
  2075.  likely to be raised.
  2076.  
  2077.  '''
  2078.  
  2079.   K = VDiff(Point, Line[0])
  2080.   L = VDiff(Line[1], Line[0])
  2081.   t = VDot(K, L) / VLengthSquared(L)
  2082.  
  2083.   return t
  2084.  
  2085.  
  2086. #-------------------------------------------------------------------------------
  2087.  
  2088.  
  2089. def XInterceptOfLine(Line):
  2090.  
  2091.   u'''Return where an infinitely long line crosses the x axis.
  2092.  
  2093.  The line is described with two points. If no intersection with
  2094.  the x axis is found (because the line is horizontal, say), None
  2095.  is returned.
  2096.  
  2097.  If an intersection is found, The result is the value of x at the
  2098.  intersection rather than a coordinate pair.
  2099.  
  2100.  '''
  2101.  
  2102.   P1, P2 = Line
  2103.  
  2104.   x1, y1 = P1
  2105.   x2, y2 = P2
  2106.  
  2107.   try:
  2108.     Result = (x1 * y2 - y1 * x2) / float(y2 - y1)
  2109.   except (ZeroDivisionError, OverflowError):
  2110.     Result = None
  2111.  
  2112.   return Result
  2113.  
  2114.  
  2115. #-------------------------------------------------------------------------------
  2116.  
  2117.  
  2118. def UnitXToLineIntersection(Line2):
  2119.  
  2120.   u'''Return a [0..1] x-intercept as a pair of line parameters.
  2121.  
  2122.  The first line segment lies on the x-axis from x = 0 to x = 1. The
  2123.  second line, given by the argument Line2 is defined with two endpoints.
  2124.  The result is of the form (t1, t2) where t1 and t2 are parameters
  2125.  between 0 and 1 for UnitX and Line2 respectively. If the intersection
  2126.  point lies outside either of the line segments or the lines are parallel
  2127.  or degenerate, None is returned.
  2128.  
  2129.  '''
  2130.  
  2131.   Result = None
  2132.  
  2133.   x1, y1 = Line2[0]
  2134.   x2, y2 = Line2[1]
  2135.  
  2136.   if y1 <= 0.0 <= y2 or y2 <= 0.0 <= y1:
  2137.  
  2138.     try:
  2139.  
  2140.       t2 = max(0.0, min(1.0, y1 / (y1 - y2)))
  2141.       t1 = (1.0 - t2) * x1 + t2 * x2
  2142.  
  2143.       if 0.0 <= t1 <= 1.0:
  2144.         Result = (t1, t2)
  2145.  
  2146.     except (ZeroDivisionError, OverflowError):
  2147.  
  2148.       MinX, MaxX = (x1, x2) if x1 <= x2 else (x2, y1)
  2149.  
  2150.       if MinX <= 1.0 and MaxX >= 0.0:
  2151.  
  2152.         t1 = 0.5 * (max(MinX, 0.0) + min(MaxX, 1.0))
  2153.  
  2154.         try:
  2155.           t2 = (t1 - x1) / (x2 - x1)
  2156.         except (ZeroDivisionError, OverflowError):
  2157.           t2 = 0.5
  2158.  
  2159.         Result = (t1, t2)
  2160.  
  2161.   return Result
  2162.  
  2163.  
  2164. #-------------------------------------------------------------------------------
  2165.  
  2166.  
  2167. def LineToLineIntersectionPoint(Line1, Line2):
  2168.  
  2169.   u'''Return the intersection of two infinitely long lines.
  2170.  
  2171.  Each line is described with two points. If no intersection is found
  2172.  (because the lines are parallel or close to parallel, say), None is
  2173.  returned.
  2174.  
  2175.  '''
  2176.  
  2177.   P1, P2 = Line1
  2178.   P3, P4 = Line2
  2179.  
  2180.   x1, y1 = P1
  2181.   x2, y2 = P2
  2182.   x3, y3 = P3
  2183.   x4, y4 = P4
  2184.  
  2185.   #            ⎛                                      ⎞
  2186.   #            ⎜ ⎢⎢x1 y1⎥ ⎢x1 1⎥⎥    ⎢⎢x1 y1⎥ ⎢y1 1⎥⎥ ⎟
  2187.   #            ⎜ ⎢⎢x2 y2⎥ ⎢x2 1⎥⎥    ⎢⎢x2 y2⎥ ⎢y2 1⎥⎥ ⎟
  2188.   #            ⎜ ⎢              ⎥    ⎢              ⎥ ⎟
  2189.   #            ⎜ ⎢⎢x3 y3⎥ ⎢x3 1⎥⎥    ⎢⎢x3 y3⎥ ⎢y3 1⎥⎥ ⎟
  2190.   #            ⎜ ⎢⎢x4 y4⎥ ⎢x4 1⎥⎥    ⎢⎢x4 y4⎥ ⎢y4 1⎥⎥ ⎟
  2191.   # (x, y)  =  ⎜――――――――――――――――――, ――――――――――――――――――⎟
  2192.   #            ⎜ ⎢⎢x1 1⎥  ⎢y1 1⎥⎥    ⎢⎢x1 1⎥  ⎢y1 1⎥⎥ ⎟
  2193.   #            ⎜ ⎢⎢x2 1⎥  ⎢y2 1⎥⎥    ⎢⎢x2 1⎥  ⎢y2 1⎥⎥ ⎟
  2194.   #            ⎜ ⎢              ⎥    ⎢              ⎥ ⎟
  2195.   #            ⎜ ⎢⎢x3 1⎥  ⎢y3 1⎥⎥    ⎢⎢x3 1⎥  ⎢y3 1⎥⎥ ⎟
  2196.   #            ⎜ ⎢⎢x4 1⎥  ⎢y4 1⎥⎥    ⎢⎢x4 1⎥  ⎢y4 1⎥⎥ ⎟
  2197.   #            ⎝                                      ⎠
  2198.  
  2199.   c = x1 - x2
  2200.   d = x3 - x4
  2201.   e = y1 - y2
  2202.   f = y3 - y4
  2203.  
  2204.   #            ⎛                ⎞
  2205.   #            ⎜ ⎢a c⎥    ⎢a e⎥ ⎟
  2206.   #            ⎜ ⎢b d⎥    ⎢b f⎥ ⎟
  2207.   # (x, y)  =  ⎜―――――――, ―――――――⎟
  2208.   #            ⎜ ⎢c e⎥    ⎢c e⎥ ⎟
  2209.   #            ⎜ ⎢d f⎥    ⎢d f⎥ ⎟
  2210.   #            ⎝                ⎠
  2211.  
  2212.   try:
  2213.  
  2214.     z = 1.0 / (c * f - e * d)
  2215.  
  2216.     a = x1 * y2 - y1 * x2
  2217.     b = x3 * y4 - y3 * x4
  2218.  
  2219.     x = (a * d - c * b) * z
  2220.     y = (a * f - e * b) * z
  2221.  
  2222.     Result = (x, y)
  2223.  
  2224.   except (ZeroDivisionError, OverflowError):
  2225.  
  2226.     Result = None
  2227.  
  2228.   return Result
  2229.  
  2230.  
  2231. #-------------------------------------------------------------------------------
  2232.  
  2233.  
  2234. def LineToLineIntersection(Line1, Line2):
  2235.  
  2236.   u'''Return an intersection point as a vector and as line parameters.
  2237.  
  2238.  Each line segment is described with two endpoints. The result is of the
  2239.  form (Point, t1, t2) where t1 and t2 are parameters between 0 and 1 for
  2240.  Line1 and Line2 respectively. If the intersection point lies outside
  2241.  either of the line segments or the lines are parallel or degenerate,
  2242.  None is returned.
  2243.  
  2244.  '''
  2245.  
  2246.   Result = None
  2247.  
  2248.   P = LineToLineIntersectionPoint(Line1, Line2)
  2249.  
  2250.   if P is not None:
  2251.  
  2252.     try:
  2253.       t1 = LineParameter(P, Line1)
  2254.       if 0.0 <= t1 <= 1.0:
  2255.         t2 = LineParameter(P, Line2)
  2256.         if 0.0 <= t2 <= 1.0:
  2257.           Result = (P, t1, t2)
  2258.     except (ZeroDivisionError, OverflowError):
  2259.       pass
  2260.  
  2261.   return Result
  2262.  
  2263.  
  2264. #-------------------------------------------------------------------------------
  2265. # Bézier functions
  2266. #-------------------------------------------------------------------------------
  2267.  
  2268.  
  2269. def CubicBezierArcHandleLength(AngleSweep):
  2270.  
  2271.   u'''Return the handle length to use for an approximately circular arc.
  2272.  
  2273.  “Handle length” refers to the distance of a cubic Bézier curve’s (inner)
  2274.  control point to the anchor (endpoint) nearest to it.
  2275.  
  2276.  '''
  2277.  
  2278.   return 4.0 / 3.0 * tan(AngleSweep / 4.0)
  2279.  
  2280.  
  2281. #-------------------------------------------------------------------------------
  2282.  
  2283.  
  2284. def BezierPoint(Curve, t):
  2285.  
  2286.   u'''Return the point on a Bézier curve at parameter t.
  2287.  
  2288.  The Bézier curve may be of any order and is specified by a list of
  2289.  control points, the first and last of which are anchors.
  2290.  
  2291.  The parameter t normally ranges from 0.0 to 1.0. Values outside this
  2292.  range result in extrapolations of the Bézier curve.
  2293.  
  2294.  '''
  2295.  
  2296.   # A point on a Bézier curve is a blend of all control points. The first
  2297.   # and last of those are anchor points. For a given parameter t, Each
  2298.   # successive control point is weighted with the product of a binomial
  2299.   # coefficient, an ascending power of t and a descending power of (1 − t).
  2300.   #
  2301.   # A point at parameter t on a cubic Bézier curve is thus
  2302.   #
  2303.   #   1s³𝐏₀ + 3s²t𝐏₁ + 3st²𝐏₂ + 1t³𝐏₃
  2304.   #
  2305.   # where s = (1 − t).
  2306.  
  2307.   # Evaluating a Bézier curve directly with a Bernstein polynomial, however
  2308.   # leads to numerical problems due to small numbers being raised to high
  2309.   # powers.
  2310.  
  2311.   # NumTerms = len(Curve)
  2312.   # Order = NumTerms - 1
  2313.   #
  2314.   # s = 1 - t
  2315.   #
  2316.   # SPowers = [1.0]
  2317.   # TPowers = [1.0]
  2318.   #
  2319.   # for i in range(Order):
  2320.   #   SPowers.append(s * SPowers[i])
  2321.   #   TPowers.append(t * TPowers[i])
  2322.   #
  2323.   # Result = VZeros(len(Curve[0]))
  2324.   #
  2325.   # for i in range(NumTerms):
  2326.   #   w = BinomialCoefficient(Order, i) * SPowers[Order - i] * TPowers[i]
  2327.   #   Result = VSum(Result, VScaled(Curve[i], w))
  2328.  
  2329.   # deCasteljau’s method is superior.
  2330.  
  2331.   C = Curve
  2332.  
  2333.   while len(C) > 1:
  2334.     D = []
  2335.     i = 1
  2336.     while i < len(C):
  2337.       D.append(VLerp(C[i - 1], C[i], t))
  2338.       i += 1
  2339.     C = D
  2340.  
  2341.   Result = C[0]
  2342.  
  2343.   return Result
  2344.  
  2345.  
  2346. #-------------------------------------------------------------------------------
  2347.  
  2348.  
  2349. def BezierPAT(Curve, t):
  2350.  
  2351.   u'''Return both the point and a tangent on a Bézier curve at parameter t.
  2352.  
  2353.  The Bézier curve may be of any order and is specified by a list of
  2354.  control points, the first and last of which are anchors.
  2355.  
  2356.  The parameter t normally ranges from 0.0 to 1.0. Values outside this
  2357.  range result in extrapolations of the Bézier curve.
  2358.  
  2359.  The result is in the form (Point, Tangent). Tangent is not normalised.
  2360.  
  2361.  '''
  2362.  
  2363.   # deCasteljau’s method
  2364.  
  2365.   C = Curve
  2366.  
  2367.   while len(C) > 2:
  2368.     D = []
  2369.     i = 1
  2370.     while i < len(C):
  2371.       D.append(VLerp(C[i - 1], C[i], t))
  2372.       i += 1
  2373.     C = D
  2374.  
  2375.   Point = VLerp(C[0], C[1], t)
  2376.   Tangent = VDiff(C[1], C[0])
  2377.  
  2378.   return (Point, Tangent)
  2379.  
  2380.  
  2381. #-------------------------------------------------------------------------------
  2382.  
  2383.  
  2384. def SplitBezier(Curve, t):
  2385.  
  2386.   u'''Split a Bézier curve into two parts.
  2387.  
  2388.  The Bézier curve may be of any order and is specified by a list of
  2389.  control points, the first and last of which are anchors.
  2390.  
  2391.  The parameter t normally ranges from 0.0 to 1.0. Values of t outside
  2392.  this range lie on the extrapolated curve and cause the returned curves
  2393.  to overlap and to run in opposite directions to each other.
  2394.  
  2395.  '''
  2396.  
  2397.   # The implementation of deCasteljau’s algorithm is surprisingly
  2398.   # straightforward:
  2399.   #
  2400.   # Successive lists of points describing Bézier curves of successively
  2401.   # lower orders are each formed from the linearly interpolated intermediate
  2402.   # points at parameter t for each line segment of the immediately previous
  2403.   # list. The last list to be generated contains just one point, which is
  2404.   # point at parameter t.
  2405.   #
  2406.   # The first curve segment of the split Bézier curve is formed from the
  2407.   # start point of each of the original and successive lists.
  2408.   #
  2409.   # The second curve segment is formed from the end point of each of the
  2410.   # original and successive lists. The second curve is reversed so that
  2411.   # its endpoint is the original curve’s endpoint.
  2412.  
  2413.   C = Curve
  2414.  
  2415.   C1 = []
  2416.   C2 = []
  2417.  
  2418.   while len(C) > 0:
  2419.  
  2420.     C1.append(C[0])
  2421.     C2.append(C[-1])
  2422.  
  2423.     D = []
  2424.     i = 1
  2425.  
  2426.     while i < len(C):
  2427.       D.append(VLerp(C[i - 1], C[i], t))
  2428.       i += 1
  2429.  
  2430.     C = D
  2431.  
  2432.   C2.reverse()
  2433.  
  2434.   return C1, C2
  2435.  
  2436.  
  2437. #-------------------------------------------------------------------------------
  2438.  
  2439.  
  2440. def BezierDerivative(Curve):
  2441.  
  2442.   u'''Return the first derivative of a given Bézier curve.
  2443.  
  2444.  Curve must have at least two points.
  2445.  
  2446.  The resulting curve, evaluated at parameter t, gives the derivative with
  2447.  respect to t of the original curve at evaluated at t.
  2448.  
  2449.  The plot of the resulting curve is called a hodograph.
  2450.  
  2451.  '''
  2452.  
  2453.   Order = len(Curve) - 1
  2454.  
  2455.   if Order < 1:
  2456.    raise Error(
  2457.      'Cannot compute the derivative of a Bézier curve ' +
  2458.        'with fewer than two control points.'
  2459.    )
  2460.  
  2461.   Result = []
  2462.   i = 0
  2463.  
  2464.   while i < Order:
  2465.     Result.append(VScaled(VDiff(Curve[i + 1], Curve[i]), Order))
  2466.     i += 1
  2467.  
  2468.   return Result
  2469.  
  2470.  
  2471. #-------------------------------------------------------------------------------
  2472.  
  2473.  
  2474. def ManhattanBezierDeviance(Curve):
  2475.  
  2476.   u'''Approximately measure a Bézier curve’s deviation from flatness.
  2477.  
  2478.  This function considers a Bézier curve flat if all the control points are
  2479.  spaced evenly between the endpoints, representing a line progressing at
  2480.  uniform velocity.
  2481.  
  2482.  The Manhattan distance, used to avoid a square root calculation, can
  2483.  overestimate the deviance by a factor of up to about 1.414.
  2484.  
  2485.  The deviance is useful for indicating the maximum distance a point on
  2486.  the curve at parameter t could be from a point parameter t on a line
  2487.  segment connecting the curve’s endpoints.
  2488.  
  2489.  Though strictly lateral deviance is sometimes used, the manner of
  2490.  deviance used here is far better for dealing with the small but
  2491.  easily perceptible lobes caused by inner control points lying close
  2492.  to or beyond the axial extents of the anchor control points. Lateral
  2493.  deviances on axially steady curves can be comparatively large before
  2494.  they are noticed unless there is an adjacent curve to be tracked.
  2495.  In such a case, the tolerance to use can be easily and precisely
  2496.  specified.
  2497.  
  2498.  '''
  2499.  
  2500.   Result = 0.0
  2501.   Order = len(Curve) - 1
  2502.  
  2503.   if Order >= 2:
  2504.  
  2505.     InvOrder = 1.0 / Order
  2506.     First = Curve[0]
  2507.     Last = Curve[-1]
  2508.  
  2509.     i = 1
  2510.  
  2511.     while i < Order:
  2512.  
  2513.       IdealCP = VLerp(First, Last, i * InvOrder)
  2514.       Deviation = VManhattan(VDiff(Curve[i], IdealCP))
  2515.       Result = max(Result, Deviation)
  2516.  
  2517.       i += 1
  2518.  
  2519.   return Result
  2520.  
  2521.  
  2522. #-------------------------------------------------------------------------------
  2523.  
  2524.  
  2525. def BezierLength(Curve, Interval=(0.0, 1.0)):
  2526.  
  2527.   u'''Estimate the length of a Bézier curve over a given parameter interval.
  2528.  
  2529.  This function uses 25-point Legendre–Gauss quadrature integration and
  2530.  deCasteljau’s algorithm over the curves’s derivative. It should be rather
  2531.  accurate and numerically stable for all but absurdly high order curves.
  2532.  
  2533.  If Interval is reversed, the result will be negative. As deCasteljau’s
  2534.  algorithm can both interpolate and extrapolate Bézier curves, Interval
  2535.  need not be confined to the range [0,1].
  2536.  
  2537.  '''
  2538.  
  2539.   Result = 0.0
  2540.  
  2541.   Order = len(Curve) - 1
  2542.  
  2543.   if Order < 2:
  2544.  
  2545.     if Order == 1:
  2546.       Result = VLength(VDiff(Curve[1], Curve[0])) * (Interval[1] - Interval[0])
  2547.  
  2548.   else:
  2549.  
  2550.     # The function to be integrated is ‖d𝐏/dt‖ where 𝐏 is the curve.
  2551.     # In the 2D case, that translates to √((d𝐏x/dt)²+(d𝐏y/dt)²).
  2552.     # Because evaluating ∫(√((d𝐏x(t)/dt)²+(d𝐏y(t)/dt)²).dt directly
  2553.     # will summon both Great Cthulhu and a great big non-Euclidean
  2554.     # jar of K-Y Jelly, the Legendre–Gauss quadrature numerical method
  2555.     # is used instead.
  2556.  
  2557.     def LenDerivative(H, t):
  2558.       # 𝐇(t) = d𝐏/dt, so ‖𝐇(t)‖ = ‖d𝐏/dt‖, the function to be integrated.
  2559.       D = BezierPoint(H, t)
  2560.       return VLength(D)
  2561.  
  2562.     H = BezierDerivative(Curve)
  2563.     Result = LGQIntegral25(Interval, LenDerivative, [H], [])
  2564.  
  2565.   return Result
  2566.  
  2567.  
  2568. #-------------------------------------------------------------------------------
  2569. # Bézier intersection functions
  2570. #-------------------------------------------------------------------------------
  2571.  
  2572.  
  2573. def UnitXToBezierIntersections(Curve, Tolerance=0.0):
  2574.  
  2575.   u'''Find intersections between (0, 0)–(1, 0) and a Bézier curve of any order.
  2576.  
  2577.  For arbitrary line segments, both the line and the curve will have to be
  2578.  scaled and translated so that the line maps to (0, 0)–(1, 0).
  2579.  
  2580.  The result is a list with zero or more intersection records. Each record
  2581.  is in the form (LParam, CParam) where LParam and CParam are the parameters
  2582.  for the line (0, 0)–(1, 0) and Curve respectively.
  2583.  
  2584.  '''
  2585.  
  2586.   #-----------------------------------------------------------------------------
  2587.  
  2588.   # Guard against an excessively small value of Tolerance. If it’s too
  2589.   # small, the subdivision will never end.
  2590.  
  2591.   MIN_REL_TOLERANCE = 1e-15
  2592.   MIN_ABS_TOLERANCE = 1e-315
  2593.  
  2594.   mUp = 0x08
  2595.   mDown = 0x04
  2596.   mLeft = 0x02
  2597.   mRight = 0x01
  2598.  
  2599.   #---------------------------------------------------------------------------
  2600.  
  2601.   def MinimumPermissibleTolerance(Points):
  2602.  
  2603.     u'''Determine a safe tolerance for the largest value found in points.'''
  2604.  
  2605.     return max(
  2606.       MIN_REL_TOLERANCE * max(VManhattan(P) for P in Points),
  2607.       MIN_ABS_TOLERANCE
  2608.     )
  2609.  
  2610.   #---------------------------------------------------------------------------
  2611.  
  2612.   def MayIntersect(Curve):
  2613.  
  2614.     u'''Quickly test if the curve has any chance of intersecting UnitX.'''
  2615.  
  2616.     Result = False
  2617.     ClipFlags = 0
  2618.  
  2619.     for P in Curve:
  2620.       if P[1] >= 0.0:
  2621.         ClipFlags |= mUp
  2622.       elif P[1] <= 0.0:
  2623.         ClipFlags |= mDown
  2624.       if ClipFlags == mUp | mDown:
  2625.         Result = True
  2626.         break
  2627.  
  2628.     if Result:
  2629.  
  2630.       ClipFlags = 0
  2631.  
  2632.       for P in Curve:
  2633.         if P[0] <= 0.0:
  2634.           ClipFlags |= mLeft
  2635.         elif P[0] >= 1.0:
  2636.           ClipFlags |= mRight
  2637.         else:
  2638.           Result = True
  2639.           break
  2640.         if ClipFlags == mLeft | mRight:
  2641.           Result = True
  2642.           break
  2643.  
  2644.     return Result
  2645.  
  2646.   #-----------------------------------------------------------------------------
  2647.  
  2648.   def UnitXToBezier_Recursive(Curve, Tolerance):
  2649.  
  2650.     u'''Find UnitX-to-Bézier intersections given validated arguments.
  2651.  
  2652.    Not only are the arguments taken to be valid, the trivial cases of
  2653.    a Bézier curve with an order of less than two is assumed to have
  2654.    already been excluded.
  2655.  
  2656.    The result is a list containing zero or more (LParam, CParam) pairs.
  2657.  
  2658.    '''
  2659.  
  2660.     #---------------------------------------------------------------------------
  2661.  
  2662.     def UnitXToCurveSegment(Curve, ParentStart, ParentEnd):
  2663.  
  2664.       u'''Find intersections expressed with parameters for the parent curve.
  2665.  
  2666.      The result is a list containing zero or more (LParam, CParam) pairs.
  2667.      LParam still refers to the line (0, 0)–(1, 0) but CParam is measured
  2668.      in terms of the Bézier curve of which Curve is a segment.
  2669.  
  2670.      '''
  2671.  
  2672.       Result = []
  2673.       Intercepts = UnitXToBezier_Recursive(Curve, Tolerance)
  2674.  
  2675.       for LParam, CParam in Intercepts:
  2676.         ParentCParam = (1.0 - CParam) * ParentStart + CParam * ParentEnd
  2677.         Result.append((LParam, ParentCParam))
  2678.  
  2679.       return Result
  2680.  
  2681.     #---------------------------------------------------------------------------
  2682.  
  2683.     Result = []
  2684.  
  2685.     # Here would be the obvious place to call the quick test function,
  2686.     # MayIntersect but it is instead called by this function’s parent
  2687.     # and for each of the subcurves below.
  2688.  
  2689.     Line2 = (Curve[0], Curve[-1])
  2690.     StraightIntercept = UnitXToLineIntersection(Line2)
  2691.     Deviance = ManhattanBezierDeviance(Curve)
  2692.  
  2693.     if Deviance < Tolerance:
  2694.  
  2695.       # The curve is close enough to being a line to be considered a line.
  2696.  
  2697.       if StraightIntercept is not None:
  2698.         Result.append(StraightIntercept)
  2699.  
  2700.     else:
  2701.  
  2702.       # The curve must be subdivided. Ideally the curve would be split
  2703.       # at a point such that the largest possible subcurve is easily
  2704.       # rejected by the quick test function MayIntersect.
  2705.  
  2706.       t = 0.5
  2707.  
  2708.       if StraightIntercept is not None:
  2709.  
  2710.         # The line connecting the endpoints of the curve, the curve’s
  2711.         # baseline is used as a guide to determining a good parameter
  2712.         # t at which the curve is to be be split. If the the baseline
  2713.         # parameter already calculated were used for t, it would likely
  2714.         # overshoot the true intersection point and cause only tiny
  2715.         # subcurves to be easily rejected, greatly slowing down the
  2716.         # convergence.
  2717.         #
  2718.         # The baseline parameter t0 is modified by a margin which varies
  2719.         # according to the deviance of the curve and the slope of the
  2720.         # curve’s baseline to the line (0, 0)–(1, 0). Grazing lines
  2721.         # require larger margins.
  2722.  
  2723.         try:
  2724.  
  2725.           t0 = StraightIntercept[1]
  2726.           D = VDiff(Line2[1], Line2[0])
  2727.           Shallowness = abs(D[0] / D[1])
  2728.  
  2729.           # A fudge factor partially accounts for the overestimation
  2730.           # of a control point’s ability to impart deviance. Occasional
  2731.           # overshoots are acceptable.
  2732.  
  2733.           AbsMargin = 0.4 * Deviance * (1.0 + Shallowness)
  2734.           Margin = AbsMargin / VManhattan(D)
  2735.  
  2736.           if t0 < 0.5:
  2737.             t = max(0.001, min(0.5, t0 + Margin))
  2738.           else:
  2739.             t = min(0.999, max(0.5, t0 - Margin))
  2740.  
  2741.         except (ZeroDivisionError, OverflowError):
  2742.           t = 0.5
  2743.  
  2744.       Curve1, Curve2 = SplitBezier(Curve, t)
  2745.  
  2746.       # The quick test is performed on the subcurves here rather than
  2747.       # at the start of this function in order to avoid the overhead of
  2748.       # calling the intermediate curve parameter-mapping function and
  2749.       # then this one again only to find the line obviously misses the
  2750.       # subcurve.
  2751.  
  2752.       if MayIntersect(Curve1):
  2753.         Result += UnitXToCurveSegment(Curve1, 0.0, t)
  2754.       if MayIntersect(Curve2):
  2755.         Result += UnitXToCurveSegment(Curve2, t, 1.0)
  2756.  
  2757.     return Result
  2758.  
  2759.   #-----------------------------------------------------------------------------
  2760.  
  2761.   Result = []
  2762.   Order = len(Curve) - 1
  2763.  
  2764.   # Handle the trivial cases.
  2765.  
  2766.   if Order < 2:
  2767.  
  2768.     if Order == 1:
  2769.  
  2770.       Intercept = UnitXToLineIntersection(Curve)
  2771.  
  2772.       if Intercept is not None:
  2773.         Result = [Intercept]
  2774.  
  2775.   else:
  2776.  
  2777.     ValTolerance = max(MinimumPermissibleTolerance(Curve), Tolerance)
  2778.  
  2779.     if MayIntersect(Curve):
  2780.       Result = UnitXToBezier_Recursive(Curve, ValTolerance)
  2781.  
  2782.   return Result
  2783.  
  2784.  
  2785. #-------------------------------------------------------------------------------
  2786.  
  2787.  
  2788. def LineToBezierIntersections(Line, Curve, Tolerance=0.0):
  2789.  
  2790.   u'''Find intersections between a line segment and a Bézier curve of any order.
  2791.  
  2792.  The result is a list with zero or more intersection records. Each record
  2793.  is in the form (Point, Tangent, LParam, CParam) where Point is the location
  2794.  of the intersection, Tangent is an unnormalised forward tangent to the curve
  2795.  and LParam and CParam are the parameters for Line and Curve respectively.
  2796.  
  2797.  '''
  2798.  
  2799.   Result = []
  2800.   Order = len(Curve) - 1
  2801.  
  2802.   # Handle the trivial cases.
  2803.  
  2804.   if Order < 2:
  2805.     if Order == 1:
  2806.       Intercept = LineToLineIntersection(Line, Curve)
  2807.       if Intercept is not None:
  2808.         Point, LParam, CParam = Intercept
  2809.         Tangent = VDiff(Line[1], Line[0])
  2810.         Result = [(Point, Tangent, LParam, CParam)]
  2811.     return Result
  2812.  
  2813.   # Now for the interesting cases.
  2814.  
  2815.   try:
  2816.  
  2817.     # Transform the curve so the line corresponds to the x-axis between
  2818.     # x=0 and x=1.
  2819.  
  2820.     Origin = Line[0]
  2821.     X = VDiff(Line[1], Origin)
  2822.     M, InvM = Affine2DMatrices(Origin, X)
  2823.     C = M.MultVectors(Curve)
  2824.     Scale = 1.0 / VLength(X)
  2825.  
  2826.     Intersections = UnitXToBezierIntersections(C, Scale * Tolerance)
  2827.  
  2828.     for LParam, CParam in Intersections:
  2829.       Point, Tangent = BezierPAT(Curve, CParam)
  2830.       Result.append((Point, Tangent, LParam, CParam))
  2831.  
  2832.   except (ZeroDivisionError, OverflowError):
  2833.  
  2834.     pass
  2835.  
  2836.   return Result
  2837.  
  2838.  
  2839. #-------------------------------------------------------------------------------
  2840. # Shape functions
  2841. #-------------------------------------------------------------------------------
  2842.  
  2843.  
  2844. def ShapeDim(Shape, n):
  2845.  
  2846.   u'''Return a Shape with vertices of n dimenions.
  2847.  
  2848.  Vertices set to None in Shape are left as None in the result.
  2849.  
  2850.  '''
  2851.  
  2852.   return [(PtType, None if P is None else VDim(P, n)) for PtType, P in Shape]
  2853.  
  2854.  
  2855. #-------------------------------------------------------------------------------
  2856.  
  2857.  
  2858. def ShapeFromVertices(Vertices, Order):
  2859.  
  2860.   u'''Create a poly-Bézier from a list of vertices.
  2861.  
  2862.  A poly-Bézier is a piecewise curve of Bézier curves. The poly-Bézier
  2863.  returned by this function are all of the same order except perhaps
  2864.  for the last one in the case of the number of vertices falling short.
  2865.  
  2866.  If Order is 1, a polyline is returned.
  2867.  
  2868.  '''
  2869.  
  2870.   Result = []
  2871.  
  2872.   LastIx = len(Vertices) - 1
  2873.   CPIx = 0
  2874.  
  2875.   Ix = 0
  2876.  
  2877.   while Ix < LastIx:
  2878.  
  2879.     PtType = Pt_Anchor if CPIx == 0 else Pt_Control
  2880.     Result.append((PtType, Vertices[Ix]))
  2881.  
  2882.     CPIx += 1
  2883.     if CPIx >= Order:
  2884.       CPIx = 0
  2885.  
  2886.     Ix += 1
  2887.  
  2888.   if LastIx >= 0:
  2889.     Result.append((Pt_Anchor, Vertices[LastIx]))
  2890.  
  2891.   return Result
  2892.  
  2893.  
  2894. #-------------------------------------------------------------------------------
  2895.  
  2896.  
  2897. def ShapePoints(Shape, ShapeRange=None):
  2898.  
  2899.   u'''Return the bare coordinates of a Shape or a part of one,
  2900.  
  2901.  The shape is defined as a list of (PtType, Coords) tuples which mark
  2902.  anchors, control points and breaks.
  2903.  
  2904.  ShapeRange, if set, selects only a portion of the Shape.
  2905.  
  2906.  '''
  2907.  
  2908.   RangeStart, RangeEnd = ValidatedRange(ShapeRange, len(Shape))
  2909.   Result = [Coords for Point, Coords in Shape[RangeStart:RangeEnd]]
  2910.  
  2911.   return Result
  2912.  
  2913.  
  2914. #-------------------------------------------------------------------------------
  2915.  
  2916.  
  2917. def ShapeSubpathRanges(Shape):
  2918.  
  2919.   u'''List the ranges of each subpaths in a Shape.
  2920.  
  2921.  The shape is defined as a list of (PtType, Coords) tuples which mark
  2922.  anchors, control points and breaks.
  2923.  
  2924.  A subpath is defined by a contiguous run of Shape points without any
  2925.  marked with Pt_Break. The ranges are suitable for use with the slice
  2926.  operator.
  2927.  
  2928.  '''
  2929.  
  2930.   Result = []
  2931.   SubpathStart = None
  2932.  
  2933.   for i, ShapePt in enumerate(Shape):
  2934.  
  2935.     PtType = ShapePt[0]
  2936.  
  2937.     if SubpathStart is None:
  2938.       if PtType != Pt_Break:
  2939.         SubpathStart = i
  2940.     else:
  2941.       if PtType == Pt_Break:
  2942.         Result.append((SubpathStart, i))
  2943.         SubpathStart = None
  2944.  
  2945.   if SubpathStart is not None:
  2946.     Result.append((SubpathStart, len(Shape)))
  2947.  
  2948.   return Result
  2949.  
  2950.  
  2951. #-------------------------------------------------------------------------------
  2952.  
  2953.  
  2954. def ShapeCurveRanges(Shape, ShapeRange=None):
  2955.  
  2956.   u'''List the ranges of each line segment or Bézier curve in a Shape.
  2957.  
  2958.  The Shape can be one contiguous piecewise curve or polyline or it
  2959.  can be several. A list of curve ranges is returned without regard
  2960.  to the Pt_Break items which are used to separate subpaths.
  2961.  
  2962.  ShapeRange, if set, selects only a portion of the Shape.
  2963.  
  2964.  '''
  2965.  
  2966.   Result = []
  2967.  
  2968.   RangeStart, RangeEnd = ValidatedRange(ShapeRange, len(Shape))
  2969.  
  2970.   SubpathStart = None
  2971.   CurveStart = None
  2972.  
  2973.   i = RangeStart
  2974.  
  2975.   while i < RangeEnd:
  2976.  
  2977.     PtType = Shape[i][0]
  2978.  
  2979.     if i + 1 < RangeEnd:
  2980.       IsEndOfSubpath = Shape[i + 1][0] == Pt_Break
  2981.     else:
  2982.       IsEndOfSubpath = True
  2983.  
  2984.     if SubpathStart is None:
  2985.       if PtType != Pt_Break:
  2986.         SubpathStart = i
  2987.         CurveStart = i
  2988.     else:
  2989.       if PtType == Pt_Anchor:
  2990.         Result.append((CurveStart, i + 1))
  2991.         if IsEndOfSubpath:
  2992.           CurveStart = None
  2993.         else:
  2994.           CurveStart = i
  2995.       elif PtType == Pt_Break:
  2996.         Result.append((CurveStart, i))
  2997.         SubpathStart = None
  2998.         CurveStart = None
  2999.  
  3000.     i += 1
  3001.  
  3002.   return Result
  3003.  
  3004.  
  3005. #-------------------------------------------------------------------------------
  3006.  
  3007.  
  3008. def ShapeLength(Shape):
  3009.  
  3010.   u'''Estimate the path length of a Shape.
  3011.  
  3012.  A Shape is defined by a list of (PtType, Coords) tuples which mark
  3013.  anchors, control points and breaks.
  3014.  
  3015.  The discontinuities implied by points tagged with Pt_Break do not
  3016.  contribute to the measured length.
  3017.  
  3018.  '''
  3019.  
  3020.   Result = 0.0
  3021.  
  3022.   Ranges = ShapeCurveRanges(Shape)
  3023.  
  3024.   for Range in Ranges:
  3025.     Curve = ShapePoints(Shape[Range[0]:Range[1]])
  3026.     Result += BezierLength(Curve)
  3027.  
  3028.   return Result
  3029.  
  3030.  
  3031. #-------------------------------------------------------------------------------
  3032.  
  3033.  
  3034. def LineToShapeIntersections(Line, Shape, Tolerance=0.0):
  3035.  
  3036.   u'''Find intersections between a line segment and a Shape.
  3037.  
  3038.  A Shape is defined by a list of (PtType, Coords) tuples which mark
  3039.  anchors, control points and breaks.
  3040.  
  3041.  The result is a list with zero or more intersection records. Each record
  3042.  is in the form (Point, Tangent, LParam, SubpathIx, CurveIx, CParam) where:
  3043.  
  3044.    Point is the location of the intersection;
  3045.    Tangent is an unnormalised (forward) tangent of the Shape at Point;
  3046.    LParam is the parameter for Line;
  3047.    SubpathIx is a zero-based index of subpath, a run (or loop) of curves;
  3048.    CurveIx is a zero-based index of a line or Bézier curve in a run; and
  3049.    CParam is the parameter for that line or curve.
  3050.  
  3051.  If no intersection is found, None is returned.
  3052.  
  3053.  '''
  3054.  
  3055.   Result = []
  3056.  
  3057.   try:
  3058.  
  3059.     # Transform the curve so that the line corresponds to the x-axis
  3060.     # between x = 0 and x = 1.
  3061.  
  3062.     Origin = Line[0]
  3063.     X = VDiff(Line[1], Origin)
  3064.     M, InvM = Affine2DMatrices(Origin, X)
  3065.     Scale = 1.0 / VLength(X)
  3066.  
  3067.     for SubpathIx, SPRange in enumerate(ShapeSubpathRanges(Shape)):
  3068.  
  3069.       for CurveIx, CRange in enumerate(ShapeCurveRanges(Shape, SPRange)):
  3070.  
  3071.         Curve = ShapePoints(Shape, CRange)
  3072.         TransformedCurve = M.MultVectors(Curve)
  3073.         Intersections = UnitXToBezierIntersections(
  3074.           TransformedCurve, Scale * Tolerance
  3075.         )
  3076.  
  3077.         for LParam, CParam in Intersections:
  3078.           Point, Tangent = BezierPAT(Curve, CParam)
  3079.           Result.append((Point, Tangent, LParam, SubpathIx, CurveIx, CParam))
  3080.  
  3081.   except (ZeroDivisionError, OverflowError):
  3082.  
  3083.     pass
  3084.  
  3085.   return Result
  3086.  
  3087.  
  3088. #-------------------------------------------------------------------------------
  3089.  
  3090.  
  3091. def TransformedShape(AM, Shape):
  3092.  
  3093.   u'''Return a Shape transformed by an affine matrix.
  3094.  
  3095.  The shape is defined as a list of (PtType, Coords) tuples which mark
  3096.  anchors, control points and breaks.
  3097.  
  3098.  '''
  3099.  
  3100.   Result = []
  3101.  
  3102.   for PtType, Coords in Shape:
  3103.     Result.append((PtType, AM.MultV(Coords)))
  3104.  
  3105.   return Result
  3106.  
  3107.  
  3108. #-------------------------------------------------------------------------------
  3109.  
  3110.  
  3111. def PiecewiseArc(Centre, Radius, AngleRange, NumPieces):
  3112.  
  3113.   u'''Return a Shape consisting of Bézier curves approximating a circular arc.
  3114.  
  3115.  The shape is defined as a list of (PtType, Coords) tuples which mark
  3116.  anchors, control points and breaks.
  3117.  
  3118.  '''
  3119.  
  3120.   Result = []
  3121.  
  3122.   StartAngle, EndAngle = AngleRange
  3123.   AngleStep = (EndAngle - StartAngle) / float(NumPieces)
  3124.  
  3125.   if EndAngle > StartAngle:
  3126.     h = CubicBezierArcHandleLength(AngleStep)
  3127.   else:
  3128.     h = -CubicBezierArcHandleLength(-AngleStep)
  3129.  
  3130.   SegStart = VPolToRect((Radius, StartAngle))
  3131.   Result.append((Pt_Anchor, VSum(Centre, SegStart)))
  3132.  
  3133.   for i in range(NumPieces):
  3134.  
  3135.     H1 = VSum(SegStart, VScaled(VPerp(SegStart), h))
  3136.     SegEndAngle = EndAngle - (NumPieces - 1 - i) * AngleStep
  3137.     SegEnd = VPolToRect((Radius, SegEndAngle))
  3138.     H2 = VSum(SegEnd, VScaled(VPerp(SegEnd), -h))
  3139.     SegStart = SegEnd
  3140.  
  3141.     Result.append((Pt_Control, VSum(Centre, H1)))
  3142.     Result.append((Pt_Control, VSum(Centre, H2)))
  3143.     Result.append((Pt_Anchor, VSum(Centre, SegEnd)))
  3144.  
  3145.   return Result
  3146.  
  3147.  
  3148. #-------------------------------------------------------------------------------
  3149. # Output formatting functions
  3150. #-------------------------------------------------------------------------------
  3151.  
  3152.  
  3153. def MaxDP(x, n):
  3154.  
  3155.   u'''Return as a string, x at a maximum of n decimal places.'''
  3156.  
  3157.   s = '%.*f' % (n, x)
  3158.  
  3159.   if '.' in s:
  3160.     while s[-1:] == '0':
  3161.       s = s[:-1]
  3162.     if s[-1:] == '.':
  3163.       s = s[:-1]
  3164.  
  3165.   return s
  3166.  
  3167.  
  3168. #-------------------------------------------------------------------------------
  3169.  
  3170.  
  3171. def GFListStr(L):
  3172.  
  3173.   u'''Return as a string, a list (or tuple) in general precision format.'''
  3174.  
  3175.   return '[' + (', '.join('%g' % (x) for x in L)) + ']'
  3176.  
  3177.  
  3178. #-------------------------------------------------------------------------------
  3179.  
  3180.  
  3181. def GFTupleStr(T):
  3182.  
  3183.   u'''Return as a string, a tuple (or list) in general precision format.'''
  3184.  
  3185.   if len(T) == 1:
  3186.     return '(%g,)' % (T[0])
  3187.   else:
  3188.     return '(' + (', '.join('%g' % (x) for x in T)) + ')'
  3189.  
  3190.  
  3191. #-------------------------------------------------------------------------------
  3192.  
  3193.  
  3194. def HTMLEscaped(Text):
  3195.  
  3196.   u'''Return text safe to use in HTML text and in quoted attributes values.
  3197.  
  3198.  Using unquoted values in attribute assignments where the values can be
  3199.  chosen by an untrusted agent can lead to a script injection attack unless
  3200.  the values are very heavily sanitised. It is good practice to always wrap
  3201.  attribute values in single or double quotes.
  3202.  
  3203.  '''
  3204.  
  3205.   EntityMap = {
  3206.     '&': '&amp;',
  3207.     '<': '&lt;',
  3208.     '>': '&gt;',
  3209.     '"': '&quot;',
  3210.     "'": '&apos',
  3211.     '`': '&#96;'
  3212.   }
  3213.  
  3214.   Result = ""
  3215.  
  3216.   for RawC in Text:
  3217.     SafeC = EntityMap[RawC] if RawC in EntityMap else RawC
  3218.     Result += SafeC
  3219.  
  3220.   return Result
  3221.  
  3222.  
  3223. #-------------------------------------------------------------------------------
  3224.  
  3225.  
  3226. def AttrMarkup(Attributes, PrependSpace):
  3227.  
  3228.   u'''Return HTML-style attribute markup from a dictionary.
  3229.  
  3230.  if PrependSpace is True and there is at least one attribute assignment
  3231.  generated, a space is added to the beginning of the result.
  3232.  
  3233.  Both an empty dictionary and None are valid values for Attributes.
  3234.  
  3235.  '''
  3236.  
  3237.   Result = ''
  3238.  
  3239.   if Attributes is not None:
  3240.     for Attr, Value in Attributes.items():
  3241.       if PrependSpace or (Result != ''):
  3242.         Result += ' '
  3243.       Result += HTMLEscaped(Attr) + '="' + HTMLEscaped(Value) + '"'
  3244.  
  3245.   return Result
  3246.  
  3247.  
  3248. #-------------------------------------------------------------------------------
  3249.  
  3250.  
  3251. def ProgressColourStr(Progress, Opacity=None):
  3252.  
  3253.   u'''Return an SVG or CSS colour string to indicate progress.'''
  3254.  
  3255.   Colours = [
  3256.     (1.0, 0.0, 0.0),
  3257.     (1.0, 1.0, 0.0),
  3258.     (0.0, 1.0, 0.0),
  3259.     (0.0, 1.0, 1.0),
  3260.     (0.0, 0.0, 1.0),
  3261.     (1.0, 0.0, 1.0)
  3262.   ]
  3263.  
  3264.   a = 4.25 * max(0.0, min(1.0, Progress))
  3265.   f = floor(a)
  3266.   t = a - f
  3267.   i = int(f)
  3268.  
  3269.   C0 = Colours[i % 6]
  3270.   C1 = Colours[(i + 1) % 6]
  3271.  
  3272.   # Surprisingly, working with linear light makes for more readily
  3273.   # distinguishable rainbow hues even though the purpose of nonlinear
  3274.   # framebuffer encoding is to provide a perceptually linear ramp of
  3275.   # brightnesses.
  3276.  
  3277.   # Since 0.0 and 1.0 are the same in linear and nonlinear form, no
  3278.   # conversion to linear is necessary.
  3279.   #
  3280.   # C0 = [pow(x, 2.2) for x in C0]
  3281.   # C1 = [pow(x, 2.2) for x in C1]
  3282.  
  3283.   C = VLerp(C0, C1, t)
  3284.   C = [pow(x, 1.0/2.2) for x in C]
  3285.   C = VScaled(C, 255.0)
  3286.  
  3287.   r = int(round(C[0]))
  3288.   g = int(round(C[1]))
  3289.   b = int(round(C[2]))
  3290.  
  3291.   if Opacity is None:
  3292.     OpacityStr = '1'
  3293.   else:
  3294.     a= max(0.0, min(1.0, Opacity))
  3295.     OpacityStr = MaxDP(a, 3)
  3296.  
  3297.   if OpacityStr == '1':
  3298.     Result = '#%.2x%.2x%.2x' % (r, g, b)
  3299.   else:
  3300.     Result = 'rgba(%d, %d, %d, %s)' % (r, g, b, OpacityStr)
  3301.  
  3302.   return Result
  3303.  
  3304.  
  3305. #-------------------------------------------------------------------------------
  3306. # SVG functions
  3307. #-------------------------------------------------------------------------------
  3308.  
  3309.  
  3310. def SVGStart(IT, Title=None, SVGAttributes=None):
  3311.  
  3312.   u'''Return a string for the beginning of an SVG file.
  3313.  
  3314.  IT is a tIndentTracker object, Title is used in the <title> tag and SVGAttr
  3315.  is a dictionary of attributes used to supplement or override the default
  3316.  attributes assigned to the <svg> tag.
  3317.  
  3318.  '''
  3319.  
  3320.   TitleToUse = '(Untitled)' if Title is None or Title == '' else Title
  3321.  
  3322.   Attr = {
  3323.     'width': '28cm',
  3324.     'height': '19cm',
  3325.     'viewBox': '0 0 28 19',
  3326.     'preserveAspectRatio': 'xMidYMid meet',
  3327.     'version': '1.1',
  3328.     'xmlns': 'http://www.w3.org/2000/svg',
  3329.     'xmlns:xlink': 'http://www.w3.org/1999/xlink'
  3330.    }
  3331.  
  3332.   Attr = MergedDictionary(Attr, SVGAttributes)
  3333.  
  3334.   Result = IT(
  3335.     '<?xml version="1.0" standalone="no"?>',
  3336.     '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"',
  3337.     IT.IndentUnitStr + '"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">',
  3338.     '<svg' + AttrMarkup(Attr, True) + '>',
  3339.     '',
  3340.     '<title>' + HTMLEscaped(TitleToUse) + '</title>',
  3341.     ''
  3342.   )
  3343.  
  3344.   #IT.StepIn()
  3345.  
  3346.   return Result
  3347.  
  3348.  
  3349. #-------------------------------------------------------------------------------
  3350.  
  3351.  
  3352. def SVGEnd(IT):
  3353.  
  3354.   u'''Return a string for the end of an SVG file.'''
  3355.  
  3356.   #IT.StepOut()
  3357.  
  3358.   Result = IT('', '</svg>')
  3359.  
  3360.   return Result
  3361.  
  3362.  
  3363. #-------------------------------------------------------------------------------
  3364.  
  3365.  
  3366. def SVGPathDataSegments(Shape):
  3367.  
  3368.   u'''Return a list of SVG path commands from a Shape.
  3369.  
  3370.  The result is ready for pretty-printing and stuffing into the
  3371.  “d” attribute of a path tag.
  3372.  
  3373.  SVG path commands are very much like Postscript commands.
  3374.  
  3375.  '''
  3376.  
  3377.   def SVGNStr(n):
  3378.     u'''Return a string representation of a number, fit for an SVG.'''
  3379.     s = MaxDP(n, 6)
  3380.     return s
  3381.  
  3382.   def SVGVStr(V):
  3383.     u'''Return a string representation of a 2D vector, fit for an SVG.'''
  3384.     return SVGNStr(V[0]) + ',' + SVGNStr(V[1])
  3385.  
  3386.   Result = []
  3387.  
  3388.   FirstSegAnchor = None
  3389.   LastAnchor = None
  3390.   LastSegCP = None
  3391.   LastSegWasQuadratic = False
  3392.  
  3393.   Segment = []
  3394.  
  3395.   for i, ShapePt in enumerate(Shape):
  3396.  
  3397.     PtType, Coords = ShapePt
  3398.  
  3399.     if PtType == Pt_Anchor:
  3400.  
  3401.       if i + 1 < len(Shape):
  3402.         IsTerminal = Shape[i + 1][0] == Pt_Break
  3403.       else:
  3404.         IsTerminal = True
  3405.  
  3406.       if LastAnchor is None:
  3407.  
  3408.         if not IsTerminal:
  3409.  
  3410.           Result.append('M ' + SVGVStr(Coords))
  3411.  
  3412.           FirstSegAnchor = Coords
  3413.           LastAnchor = Coords
  3414.           LastSegCP = None
  3415.  
  3416.       else:
  3417.  
  3418.         if len(Segment) > 0:
  3419.  
  3420.           IsSmooth = False
  3421.           IsQuadratic = len(Segment) == 1
  3422.           IsCubic = len(Segment) == 2
  3423.  
  3424.           if (LastAnchor is not None) and (LastSegCP is not None):
  3425.             MirrorCP = VSum(LastAnchor, VDiff(LastAnchor, LastSegCP))
  3426.             if LastSegWasQuadratic == IsQuadratic:
  3427.               IsSmooth = SVGVStr(Segment[0]) == SVGVStr(MirrorCP)
  3428.  
  3429.           if IsQuadratic:
  3430.             if IsSmooth:
  3431.               CmdStr = 'T'
  3432.             else:
  3433.               CmdStr = 'Q'
  3434.           elif IsCubic:
  3435.             if IsSmooth:
  3436.               CmdStr = 'S'
  3437.             else:
  3438.               CmdStr = 'C'
  3439.           else:
  3440.             raise Error(
  3441.               u'SVG requires a Bézier curve to be of the order ' +
  3442.                 'one, two or three (linear, quadratic or cubic).'
  3443.             )
  3444.  
  3445.           LastSegWasQuadratic = IsQuadratic
  3446.           LastAnchor = Coords
  3447.           LastSegCP = Segment[-1]
  3448.           Segment.append(Coords)
  3449.  
  3450.           if IsSmooth:
  3451.             PointsToOutput = Segment[1:]
  3452.           else:
  3453.             PointsToOutput = Segment[:]
  3454.  
  3455.           for V in PointsToOutput:
  3456.             CmdStr = CmdStr + ' ' + SVGVStr(V)
  3457.  
  3458.           Result.append(CmdStr)
  3459.           Segment = []
  3460.  
  3461.         else:
  3462.  
  3463.           XYStr = SVGVStr(Coords)
  3464.  
  3465.           if IsTerminal and XYStr == SVGVStr(FirstSegAnchor):
  3466.  
  3467.             Result.append('Z')
  3468.  
  3469.             FirstSegAnchor = None
  3470.             LastAnchor = None
  3471.             LastSegCP = None
  3472.  
  3473.           else:
  3474.  
  3475.             XStr = SVGNStr(Coords[0])
  3476.             YStr = SVGNStr(Coords[1])
  3477.  
  3478.             if LastAnchor is not None:
  3479.               IsH = YStr == SVGNStr(LastAnchor[1])
  3480.               IsV = XStr == SVGNStr(LastAnchor[0])
  3481.             else:
  3482.               IsH = False
  3483.               IsV = False
  3484.  
  3485.             if IsH:
  3486.               if not IsV:
  3487.                 Result.append('H ' + XStr)
  3488.             else:
  3489.               if IsV:
  3490.                 Result.append('V ' + YStr)
  3491.               else:
  3492.                 Result.append('L ' + XYStr)
  3493.  
  3494.             LastAnchor = Coords
  3495.             LastSegCP = None
  3496.  
  3497.     elif PtType == Pt_Control:
  3498.  
  3499.       if LastAnchor is None:
  3500.         raise Error('A curve may not start with a control point.')
  3501.  
  3502.       Segment.append(Coords)
  3503.  
  3504.     elif PtType == Pt_Break:
  3505.  
  3506.       if len(Segment) > 0:
  3507.         raise Error('A Break command must not occur within a curve.')
  3508.  
  3509.       FirstSegAnchor = None
  3510.       LastAnchor = None
  3511.       LastSegCP = None
  3512.  
  3513.     else:
  3514.  
  3515.       raise Error('Unhandled point type %s.' % (str(PtType)))
  3516.  
  3517.   if len(Segment) > 0:
  3518.     raise Error('No anchor point was given for the last curve.')
  3519.  
  3520.   return Result
  3521.  
  3522.  
  3523. #-------------------------------------------------------------------------------
  3524.  
  3525.  
  3526. def SVGPath(IT, Shape, Attributes=None):
  3527.  
  3528.   u'''Return SVG markup for a Shape.
  3529.  
  3530.  A Shape is defined as a list of (PtType, Coords) tuples which mark
  3531.  anchors, control points and breaks.
  3532.  
  3533.  Attributes is a dictionary for writing the attribute markup for
  3534.  the SVG path tag. Common attributes to set include fill, fill-rule,
  3535.  stroke, stroke-width, stroke-linejoin and stroke-linecap. Both an
  3536.  empty dictionary and None are valid values for Attributes.
  3537.  
  3538.  '''
  3539.  
  3540.   Result = ''
  3541.   FirstLinePart = '<path' + AttrMarkup(Attributes, True) + ' d="'
  3542.  
  3543.   # Defer writing the beginning of the path markup.
  3544.   IT.StepIn()
  3545.  
  3546.   InnerLines = []
  3547.   AvailTextColumns = max(0, 78 - len(IT.LIStr))
  3548.  
  3549.   CmdSep = '  '
  3550.   LineStr = ''
  3551.   Commands = SVGPathDataSegments(Shape)
  3552.  
  3553.   for Cmd in Commands:
  3554.     if LineStr != '':
  3555.       RemColumns = AvailTextColumns - len(LineStr) - len(CmdSep) - len(Cmd)
  3556.       if RemColumns < 0:
  3557.         InnerLines.append(LineStr)
  3558.         LineStr = ''
  3559.     if LineStr != '':
  3560.       LineStr += CmdSep
  3561.     LineStr += Cmd
  3562.   if LineStr != '':
  3563.     InnerLines.append(LineStr)
  3564.  
  3565.   InnerText = IT(*InnerLines)
  3566.   IT.StepOut()
  3567.  
  3568.   if len(InnerLines) > 0:
  3569.     Result += IT(FirstLinePart)
  3570.     Result += InnerText
  3571.     Result += IT('"/>')
  3572.   else:
  3573.     Result += IT(FirstLinePart + '"/>')
  3574.  
  3575.   return Result
  3576.  
  3577.  
  3578. #-------------------------------------------------------------------------------
  3579.  
  3580.  
  3581. def SVGText(IT, Position, Text, Attributes=None):
  3582.  
  3583.   u'''Return SVG markup for a line of text.
  3584.  
  3585.  Several lines of text with similar attributes can have their attributes
  3586.  inherited from those of a containing group.
  3587.  
  3588.  Attributes is a dictionary for writing the attribute markup for
  3589.  the <text> tag. Common attributes to set include font-family,
  3590.  font-style, font-variant, font-size, font-weight, kerning,
  3591.  letter-spacing, word-spacing, text-decoration, text-anchor, fill,
  3592.  stroke and stroke-width. Both an empty dictionary and None are
  3593.  valid values for Attributes.
  3594.  
  3595.  '''
  3596.  
  3597.   BadKeys = ['x', 'X', 'y', 'Y']
  3598.  
  3599.   A = Attributes
  3600.  
  3601.   if Position is not None:
  3602.  
  3603.     PosStr = (
  3604.       ' x="' + MaxDP(Position[0], 5) + '" ' +
  3605.       'y="' + MaxDP(Position[1], 5) + '"'
  3606.     )
  3607.  
  3608.     if A is not None:
  3609.       for BadKey in BadKeys:
  3610.         if BadKey in A:
  3611.           if A is Attributes:
  3612.             A = dict(Attributes)
  3613.           del A[BadKey]
  3614.  
  3615.   else:
  3616.  
  3617.     PosStr = ''
  3618.  
  3619.   Result = IT(
  3620.     '<text' + PosStr + AttrMarkup(A, True) + '>' +
  3621.     HTMLEscaped(Text) +
  3622.     '</text>'
  3623.   )
  3624.  
  3625.   return Result
  3626.  
  3627.  
  3628. #-------------------------------------------------------------------------------
  3629.  
  3630.  
  3631. def SVGGroup(IT, Attributes=None):
  3632.  
  3633.   u'''Return markup for the beginning of an SVG group.
  3634.  
  3635.  A group allows objects to easily share properties in a manner to
  3636.  the group function of a desktop publishing package. This allows
  3637.  the group to be transformed or styled as one unit.
  3638.  
  3639.  Attributes is a dictionary for writing the attribute markup for
  3640.  the <g> tag. Common attributes to set include fill, fill-rule,
  3641.  stroke, stroke-width, stroke-linejoin and stroke-linecap. Both
  3642.  an empty dictionary and None are valid values for Attributes.
  3643.  
  3644.  '''
  3645.  
  3646.   Result = IT('<g' + AttrMarkup(Attributes, True) + '>')
  3647.   IT.StepIn()
  3648.  
  3649.   return Result
  3650.  
  3651.  
  3652. #-------------------------------------------------------------------------------
  3653.  
  3654.  
  3655. def SVGGroupEnd(IT):
  3656.  
  3657.   u'''Return markup for the end of an SVG group.'''
  3658.  
  3659.   IT.StepOut()
  3660.   Result = IT('</g>')
  3661.  
  3662.   return Result
  3663.  
  3664.  
  3665. #-------------------------------------------------------------------------------
  3666.  
  3667.  
  3668. def SVGGrid(IT, Grid):
  3669.  
  3670.   u'''Return transformed SVG group markup to facilitate graph plotting.
  3671.  
  3672.  Grid is a dictionary with the keys:
  3673.  
  3674.    'CanvasMinima': (left, top)
  3675.    'CanvasMaxima': (right, bottom)
  3676.    'RangeMinima': (x0, y0)
  3677.    'RangeMaxima': (x1, y1)
  3678.    'YIsUp': True|False
  3679.    'Transpose': True|False
  3680.    'SquareAlignment': 'Corner'|'Centre'
  3681.    'DrawGrid': True|False
  3682.    'DrawUnitAxes': True|False
  3683.    'GridLineAttributes': SVG Attributes for grid lines
  3684.    'GeneralAttributes': SVG Attributes for the group tag
  3685.  
  3686.  YIsUp assumes the current reference frame resembles the frame
  3687.  SVG begins with: <x> is right and <y> is down.
  3688.  
  3689.  If SquareAlignment is “Corner” or is unspecified, integer
  3690.  coordinates lie on the intersections of grid lines and
  3691.  RangeMinima and RangeMaxima are inclusive. If SquareAlignment
  3692.  is “Centre” or “Center”, integer coordinates lie in the centres
  3693.  of grid squares, RangeMinima is inclusive and RangeMaxima is
  3694.  exclusive in regard to the square indices.
  3695.  
  3696.  '''
  3697.  
  3698.   #-----------------------------------------------------------------------------
  3699.  
  3700.   # Shorthand
  3701.  
  3702.   A = Pt_Anchor
  3703.   C = Pt_Control
  3704.   B = (Pt_Break, None)
  3705.  
  3706.   #-----------------------------------------------------------------------------
  3707.  
  3708.   def Field(Name, Default):
  3709.  
  3710.     u'''Return a value of Grid given a key name or return a default value.'''
  3711.  
  3712.     return Grid[Name] if Name in Grid else Default
  3713.  
  3714.   #-----------------------------------------------------------------------------
  3715.  
  3716.   def DictWithDefaults(ContainerDict, DictName, Defaults):
  3717.  
  3718.     u'''Allow an optional dictionary to be merged over some defaults.'''
  3719.  
  3720.     Result = Defaults
  3721.  
  3722.     if DictName in ContainerDict:
  3723.       Result = MergedDictionary(Defaults, ContainerDict[DictName])
  3724.  
  3725.     return Result
  3726.  
  3727.   #-----------------------------------------------------------------------------
  3728.  
  3729.   def T(V):
  3730.  
  3731.     u'''Return a 2D vector with its ordinals swapped.'''
  3732.  
  3733.     return (V[1], V[0])
  3734.  
  3735.   #-----------------------------------------------------------------------------
  3736.  
  3737.   Result = ''
  3738.  
  3739.   Transposed = Field('Transpose', False)
  3740.  
  3741.   CanvasMinima = tuple(Grid['CanvasMinima'])
  3742.   CanvasMaxima = tuple(Grid['CanvasMaxima'])
  3743.   CanvasCentre = VLerp(CanvasMinima, CanvasMaxima, 0.5)
  3744.   CanvasSpan = VDiff(CanvasMaxima, CanvasMinima)
  3745.  
  3746.   if Transposed:
  3747.     TCanvasSpan = T(CanvasSpan)
  3748.   else:
  3749.     TCanvasSpan = CanvasSpan
  3750.  
  3751.   RangeMinima = tuple(Grid['RangeMinima'])
  3752.   RangeMaxima = tuple(Grid['RangeMaxima'])
  3753.   RangeCentre = VLerp(RangeMinima, RangeMaxima, 0.5)
  3754.   RangeSpan = VDiff(RangeMaxima, RangeMinima)
  3755.  
  3756.   s = min(TCanvasSpan[0] / RangeSpan[0], TCanvasSpan[1] / RangeSpan[1])
  3757.   Scale = (s, s)
  3758.   ScaleStr = str(Scale[0]) + ', ' + str(Scale[1])
  3759.  
  3760.   AddressSquareCentres = Field('SquareAlignment', 'Corner') != 'Corner'
  3761.  
  3762.   if AddressSquareCentres:
  3763.     h = 0.5 * Scale[0]
  3764.   else:
  3765.     h = 0.0
  3766.   GridOffset = (h, h)
  3767.  
  3768.   if Field('YIsUp', False):
  3769.     GridOffset = (GridOffset[0], -GridOffset[1])
  3770.     Scale = (Scale[0], -Scale[1])
  3771.     ScaleStr = str(Scale[0]) + ', ' + str(Scale[1])
  3772.  
  3773.   StrokeWidth = max(0.0075, min(0.02, (0.15 / Scale[0])))
  3774.  
  3775.   if Transposed:
  3776.     Origin = VDiff(
  3777.       VSum(CanvasCentre, T(GridOffset)),
  3778.       T(VSchur(Scale, RangeCentre))
  3779.     )
  3780.   else:
  3781.     Origin = VDiff(VSum(CanvasCentre, GridOffset), VSchur(Scale, RangeCentre))
  3782.   OriginStr = str(Origin[0]) + ', ' + str(Origin[1])
  3783.  
  3784.   GeneralAttributes = DictWithDefaults(
  3785.     Grid, 'GeneralAttributes', {
  3786.       'stroke': 'rgba(0, 192, 255, 0.5)',
  3787.       'fill': 'none',
  3788.       'stroke-linecap': 'butt',
  3789.       'stroke-width': '%g' % (StrokeWidth)
  3790.   })
  3791.  
  3792.   XFormList = []
  3793.  
  3794.   if Transposed:
  3795.     XFormList.append('matrix(0, %s, 0, %s)' % (ScaleStr, OriginStr))
  3796.   else:
  3797.     XFormList = []
  3798.     if Origin != (0, 0):
  3799.       XFormList.append('translate(' + OriginStr + ')')
  3800.     if Scale != (1, 1):
  3801.       XFormList.append('scale(' + ScaleStr + ')')
  3802.  
  3803.   if len(XFormList) > 0:
  3804.     GeneralAttributes['transform'] = ' '.join(XFormList)
  3805.  
  3806.   GridLineAttributes = DictWithDefaults(
  3807.     Grid, 'GridLineAttributes', {
  3808.       'fill': 'none',
  3809.       'stroke-linecap': 'square',
  3810.       'stroke-width': '%g' % (max(0.0075, min(0.02, (0.1 / Scale[0]))))
  3811.   })
  3812.  
  3813.   Result += SVGGroup(IT, GeneralAttributes)
  3814.  
  3815.   if Field('DrawGrid', True):
  3816.  
  3817.     Result += IT('<!-- Grid lines -->')
  3818.     Result += SVGGroup(IT, GridLineAttributes)
  3819.  
  3820.     if AddressSquareCentres:
  3821.       GridOffset = (-0.5, -0.5)
  3822.     else:
  3823.       GridOffset = (0.0, 0.0)
  3824.  
  3825.     AM = AffineMtxTS(GridOffset, 1.0)
  3826.  
  3827.     Result += IT('<!-- Grid lines parallel to the y axis -->')
  3828.     S = []
  3829.     for x in range(RangeMinima[0], RangeMaxima[0] + 1):
  3830.       S += [(A, (x, RangeMinima[1])), (A, (x, RangeMaxima[1])), B]
  3831.     Result += SVGPath(IT, TransformedShape(AM, S))
  3832.  
  3833.     Result += IT('<!-- Grid lines parallel to the x axis -->')
  3834.     S = []
  3835.     for y in range(RangeMinima[1], RangeMaxima[1] + 1):
  3836.       S += [(A, (RangeMinima[0], y)), (A, (RangeMaxima[0], y)), B]
  3837.     Result += SVGPath(IT, TransformedShape(AM, S))
  3838.  
  3839.     if AddressSquareCentres:
  3840.       Result += IT('<!-- Grid square centres -->')
  3841.       S = []
  3842.       h = 0.125
  3843.       for x in range(RangeMinima[0], RangeMaxima[0]):
  3844.         for y in range(RangeMinima[1], RangeMaxima[1]):
  3845.           S += [(A, (x - h, y)), (A, (x + h, y)), B]
  3846.           S += [(A, (x, y - h)), (A, (x, y + h)), B]
  3847.       Result += SVGPath(IT, S, GridLineAttributes)
  3848.  
  3849.     Result += SVGGroupEnd(IT)
  3850.  
  3851.   if Field('DrawUnitAxes', False):
  3852.     Result += IT('<!-- Unit axes -->')
  3853.     Result += SVGGroup(IT, {'stroke': 'none'})
  3854.     h = 2.0 * (0.5 * StrokeWidth)
  3855.     ahx = 1.0 - 8.0 * h
  3856.     S = [
  3857.       (A, (-h, -h)), (A, (0.5 * h, h)), (A, (ahx, h)), (A, (ahx, 3.0 * h)),
  3858.       (A, (1.0, 0.0)), (A, (ahx, -3.0 * h)), (A, (ahx, -h)),
  3859.       (A, (-h, -h))
  3860.     ]
  3861.     Result += IT('<!-- Origin -->')
  3862.     Result += IT('<circle cx="0" cy="r" r="0.1" ' +
  3863.       'fill="none" stroke="#777" stroke-width="%g"/>' % (2.0 * h))
  3864.     Result += IT('<!-- x -->')
  3865.     Result += SVGPath(IT, S, {'fill': '#e00'})
  3866.     S[1] = (A, (h, h))
  3867.     S = TransformedShape(AffineMtxTRS2D((0.0, 0.0), 0.5 * pi, (1.0, -1.0)), S)
  3868.     Result += IT('<!-- y -->')
  3869.     Result += SVGPath(IT, S, {'fill': '#0c0'})
  3870.     Result += SVGGroupEnd(IT)
  3871.  
  3872.   return Result
  3873.  
  3874.  
  3875. #-------------------------------------------------------------------------------
  3876. # End
  3877. #-------------------------------------------------------------------------------
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement