creamygoat

PHatter

Sep 19th, 2012
349
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 105.26 KB | None | 0 0
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. #-------------------------------------------------------------------------------
  4. # phatter.py
  5. # http://pastebin.com/yKVX8eyQ
  6. #
  7. # Custom modules:
  8. #   vegesvgplot.py        http://pastebin.com/6Aek3Exm
  9. #
  10. #-------------------------------------------------------------------------------
  11.  
  12.  
  13. u'''PHatter, a plotter for printing a former used to construct a pith helmet
  14.  
  15. Description:
  16.  Writes as a series of SVG images a pattern for making a former onto which
  17.  a pith Helmet may be bult using cloth tape or thin strips of cardboard,
  18.  glue and way too much spare time. This script requires the VegeSVGPlot
  19.  module.
  20.  
  21. Author:
  22.  Daniel Neville (Blancmange), creamygoat@gmail.com
  23.  
  24. Copyright:
  25.  None
  26.  
  27. Licence:
  28.  Public domain
  29.  
  30.  
  31. INDEX
  32.  
  33.  
  34. Imports
  35.  
  36. Exceptions:
  37.  
  38.  Error
  39.  OptError
  40.  FileError
  41.  
  42. tCrownMetrics:
  43.  
  44.  __init__(
  45.    Circumference, CrownAspect, ForeheadRatio, FrontAspect, RearAspect
  46.  )
  47.  CrownPath
  48.  Circumference
  49.  CrownAspect
  50.  CrownLength
  51.  CrownWidth
  52.  ForeheadWidth
  53.  RearWidth
  54.  ForeheadRatio
  55.  FrontAspect
  56.  RearAspect
  57.  
  58. tHatMetrics:
  59.  
  60.  __init__(CrownMetrics)
  61.  RadialSlice2D(Angle)
  62.  RadialSlice3D(Angle)
  63.  CrownMetrics
  64.  BrimPath
  65.  DomeAspect
  66.  
  67. tHatSliceMetrics:
  68.  
  69.  __init__(HatMetrics, NumSlices, [Verbosity])
  70.  RadialSlice2D(RadialIndex)
  71.  RadialSlice3D(RadialIndex)
  72.  HatMetrics
  73.  NumSlices
  74.  HullScaleCorrection
  75.  Angles
  76.  Radials
  77.  RadialBrimPoints
  78.  RadialNames
  79.  Saggitals
  80.  Coronals
  81.  HasMiddleRadials
  82.  MaxSliceSpan
  83.  Top
  84.  Bottom
  85.  BaseHeight
  86.  RingCutPoints
  87.  RingSegments
  88.  RingCutLength
  89.  FenceCutLength
  90.  
  91. Command line parsing functions:
  92.  
  93.  ParseSequence(NumbersStr, MinValue, MaxValue)
  94.  ParseValue(Name, ValueStr, DataType, TypeStr, MinValueStr, MaxValueStr)
  95.  ParseCLOptions(Args, OptsTemplate, OptDataTypes)
  96.  ParamsFromCLArgs(Args)
  97.  
  98. Page output functions:
  99.  
  100.  PaperSize(Name)
  101.  HatViewsSVG(HatSliceMetrics, [Mode], [ImageDim], [Padding])
  102.  HatSliceSVG(HatSliceMetrics, PageNumber, [ImageDim], [Padding])
  103.  HatRingSVG(HatSliceMetrics, [ImageDim], [Padding])
  104.  
  105. Main:
  106.  
  107.  Main()
  108.  
  109. '''
  110.  
  111.  
  112. #-------------------------------------------------------------------------------
  113. # Imports
  114. #-------------------------------------------------------------------------------
  115.  
  116.  
  117. from __future__ import division
  118.  
  119. import sys
  120. import traceback
  121.  
  122. import math
  123.  
  124. from math import (
  125.   pi, sqrt, hypot, sin, cos, tan, asin, acos, atan, atan2, radians, degrees,
  126.   floor, ceil
  127. )
  128.  
  129. # The SVG Plotting for Vegetables module can be found at
  130. # http://pastebin.com/6Aek3Exm
  131.  
  132. from vegesvgplot import (
  133.  
  134.   # Shape constants
  135.   Pt_Break, Pt_Anchor, Pt_Control,
  136.   PtCmdWithCoordsSet, PtCmdSet,
  137.  
  138.   # Indent tracker class
  139.   tIndentTracker,
  140.  
  141.   # Affine matrix class
  142.   tAffineMtx,
  143.  
  144.   # Affine matrix creation functions
  145.   AffineMtxTS, AffineMtxTRS2D, Affine2DMatrices,
  146.  
  147.   # Utility functions
  148.   ValidatedRange, MergedDictionary, Save,
  149.   ArrayDimensions, NewArray, CopyArray, At, SetAt, EnumerateArray,
  150.  
  151.   # Basic vector functions
  152.   VZeros, VOnes, VStdBasis, VDim, VAug, VMajorAxis,
  153.   VNeg, VSum, VDiff, VSchur, VDot,
  154.   VLengthSquared, VLength, VManhattan,
  155.   VScaled, VNormalised,
  156.   VPerp, VRevPerp, VCrossProduct, VCrossProduct4D,
  157.   VScalarTripleProduct, VVectorTripleProduct,
  158.   VProjectionOnto,
  159.   VDiagonalMAV, VTransposedMAV,
  160.   VRectToPol, VPolToRect,
  161.   VLerp,
  162.  
  163.   # Mathematical functions
  164.   BinomialCoefficient, BinomialRow, NextBinomialRow,
  165.   ApproxSaggita, LGQIntegral25,
  166.   IntegralFunctionOfPLF, EvaluatePQF, EvaluateInvPQF,
  167.  
  168.   # Linear intersection functions
  169.   LineParameter, XInterceptOfLine, UnitXToLineIntersection,
  170.   LineToLineIntersectionPoint, LineToLineIntersection,
  171.  
  172.   # Bézier functions
  173.   CubicBezierArcHandleLength, BezierPoint, BezierPAT, SplitBezier,
  174.   BezierDerivative, ManhattanBezierDeviance, BezierLength,
  175.  
  176.   # Bézier intersection functions
  177.   UnitXToBezierIntersections, LineToBezierIntersections,
  178.  
  179.   # Shape functions
  180.   ShapeDim, ShapeFromVertices, ShapePoints,
  181.   ShapeSubpathRanges, ShapeCurveRanges,
  182.   ShapeLength, LineToShapeIntersections,
  183.   TransformedShape, PiecewiseArc,
  184.  
  185.   # Output formatting functions
  186.   MaxDP, GFListStr, GFTupleStr, HTMLEscaped, AttrMarkup, ProgressColourStr,
  187.  
  188.   # SVG functions
  189.   SVGStart, SVGEnd, SVGPathDataSegments,
  190.   SVGPath, SVGText, SVGGroup, SVGGroupEnd, SVGGrid
  191.  
  192. )
  193.  
  194.  
  195. #-------------------------------------------------------------------------------
  196. # Exceptions
  197. #-------------------------------------------------------------------------------
  198.  
  199.  
  200. class Error (Exception):
  201.   pass
  202.  
  203. class OptError (Error):
  204.   pass
  205.  
  206. class FileError(Error):
  207.   pass
  208.  
  209.  
  210. #-------------------------------------------------------------------------------
  211. # tCrownMetrics
  212. #-------------------------------------------------------------------------------
  213.  
  214.  
  215. class tCrownMetrics (object):
  216.  
  217.   '''A tCrownMetrics keeps the size and proprtions of the crown for a hat.
  218.  
  219.  The crown is approximated by Bézier curves approximating elliptical
  220.  arcs for the front and back, connected by Bézier curves for the parietal
  221.  regions. The crown can be square, rectangular, circular, ellipital or
  222.  the vaguely oval shape that is a good approximation to the shape of a
  223.  human head.
  224.  
  225.  Methods:
  226.  
  227.    __init__(
  228.      Circumference, CrownAspect, ForeheadRatio, FrontAspect, RearAspect
  229.    )
  230.  
  231.  Fields:
  232.  
  233.    CrownPath
  234.    Circumference
  235.    CrownAspect
  236.    CrownLength
  237.    CrownWidth
  238.    ForeheadWidth
  239.    RearWidth
  240.    ForeheadRatio
  241.    FrontAspect
  242.    RearAspect
  243.  
  244.  '''
  245.  
  246.   #-----------------------------------------------------------------------------
  247.  
  248.   def __init__(self,
  249.     Circumference, CrownAspect, ForeheadRatio, FrontAspect, RearAspect
  250.   ):
  251.  
  252.     u'''Construct a keeper of metrics of the crown of a hat.
  253.  
  254.    The crown is approximated by elliptical arcs for the front and back,
  255.    connected by Bézier curves. Examples of silly crown shapes include:
  256.  
  257.      Circular: (C, 1.0, 1.0, 1.0, 1.0)
  258.      Elliptical: (C, W/L, 1.0, L/W, L/W)
  259.      Square: (C, 1.0, 1.0, 0.0, 0.0)
  260.      Rectangular: (C, W/L, 1.0, 0.0, 0.0)
  261.  
  262.    where C = circumference, w = width and L = length.
  263.  
  264.    Constructor Arguments:
  265.  
  266.      Circumference:
  267.        The circumference of the crown, the perimeter of a transverse slice
  268.        of the head just at the tops of the ears is measured in centimetres.
  269.  
  270.      CrownAspect:
  271.        The Crown aspect is the head’s width divided by the head’s length.
  272.        If BackAspect and Frontspect are each the inverse of this value
  273.        and ForeheadRatio is 1, the shape of the crown is represented as
  274.        a Bézier approximation to an ellipse.
  275.        0.71 is the reference value.
  276.  
  277.      ForeheadRatio:
  278.        The width of the ellipse used to approximate the front curve of
  279.        the crown as a ratio of the rear ellipse is the forehead ration.
  280.        Because the front and read ellipical curves are connected on each
  281.        side by a cubic Bézier curve that usually wraps slightly around the
  282.        front ellipse, the forehead ratio is not easily guaged by looking
  283.        at the generated crown curve.
  284.        0.69 is the reference value.
  285.  
  286.      FrontAspect:
  287.        The front part of the crown is usually especially flat and is
  288.        approximated by at most the front half of an ellipse.
  289.        0.65 is the reference value.
  290.  
  291.      RearAspect:
  292.        The back part of the crown is fairly rounded and is approximated
  293.        by the back half of an ellipse.
  294.        0.75 is the reference value.
  295.  
  296.    '''
  297.  
  298.     INITIAL_LENGTH = 20.0  # Front-to-back length of the crown before scaling.
  299.  
  300.     #---------------------------------------------------------------------------
  301.  
  302.     def UnscaledCrown(CrownAspect, ForeheadRatio, FrontAspect, RearAspect):
  303.  
  304.       '''Return a Shape for the crown of the head, given sanitised parameters.
  305.  
  306.      FrontAspect and RearAspect must not be so small as to be inconsistent
  307.      with the overall aspect, CrownAspect. ForeheadRatio must be less than
  308.      or equal to 1.0.
  309.  
  310.      '''
  311.  
  312.       # Here, the axis convention used for the head is the same as used for
  313.       # aircraft, sea ships and spacecraft.
  314.       #
  315.       # x is forward, y is right and z is down.
  316.  
  317.       Result = []
  318.  
  319.       Length = INITIAL_LENGTH
  320.       Width = Length * CrownAspect
  321.  
  322.       Ra = 0.5 * Width
  323.       Rb = Ra * RearAspect
  324.       Rc = (-0.5 * Length + Rb, 0.0)
  325.  
  326.       Fa = ForeheadRatio * Ra
  327.       Fb = Fa * FrontAspect
  328.       Fc = (0.5 * Length - Fb, 0.0)
  329.  
  330.       # Parietal region
  331.  
  332.       # Figure out where the parietal node should be. This node is where
  333.       # the control point handles of the parietal segment point.
  334.  
  335.       PLength = Length - Fb - Rb
  336.  
  337.       MinP = 0.32 * PLength
  338.       MaxP = PLength + 1.0 * Fb
  339.       a = (1.0 - ForeheadRatio) * (1.0 - ForeheadRatio)
  340.       ParietalNodeDisplacement = (1.0 - a) * MinP + a * MaxP
  341.  
  342.       PNPos = VSum(Rc, (ParietalNodeDisplacement, Ra))
  343.       RearParietal = VSum(Rc, (0.0, Ra))
  344.  
  345.       FrontCurve = []
  346.  
  347.       if FrontAspect > 1e-6:
  348.  
  349.         # Consider the Parietal Node in the nonuniformly scaled
  350.         # reference frame of the front ellipse.
  351.  
  352.         P = ((PNPos[0] - Fc[0]) / FrontAspect, PNPos[1] - Fc[1])
  353.  
  354.         r = Fa
  355.         d = sqrt(max(1e-12, VLengthSquared(P) - r * r))
  356.  
  357.         # Now find the angle from the front to the temple where a tangent
  358.         # line from the front ellipse to the parietal node point. The angle
  359.         # is measured the front of the reference circle that is the same
  360.         # (lateral) width of the front ellipse.
  361.  
  362.         FrontAngle = 0.0 * pi + atan(r / d) - atan(P[0] / P[1])
  363.         NumAngleSteps = 2 if FrontAngle > pi / 3.0 else 1
  364.         TangentPoint = VSum(Fc, (Fb * cos(FrontAngle), Fa * sin(FrontAngle)))
  365.  
  366.         M = AffineMtxTS(Fc, (Fb, Fa))
  367.         FrontCurve = TransformedShape(
  368.           M, PiecewiseArc((0.0, 0.0), 1.0, (0.0, FrontAngle), NumAngleSteps)
  369.         )
  370.  
  371.       else:
  372.  
  373.         TangentPoint = VSum(Fc, (0.0, Fa))
  374.         FrontCurve = [(Pt_Anchor, Fc)]
  375.  
  376.         if Fa > 1e-12:
  377.           FrontCurve.append((Pt_Anchor, TangentPoint))
  378.  
  379.       if ForeheadRatio < 1.0:
  380.         Result += FrontCurve
  381.         Result.append((Pt_Control, VLerp(TangentPoint, PNPos, 0.25)))
  382.         Result.append((Pt_Control, VLerp(RearParietal, PNPos, 0.90)))
  383.       else:
  384.         if PLength > 1e-6:
  385.           Result += FrontCurve
  386.         else:
  387.           Result += FrontCurve[0 : max(1, len(FrontCurve) - 1)]
  388.  
  389.       if RearAspect > 1e-6:
  390.         M = AffineMtxTS(Rc, (Rb, Ra))
  391.         S = TransformedShape(
  392.           M,
  393.           PiecewiseArc((0.0, 0.0), 1.0, (0.5 * pi, pi), 2)
  394.         )
  395.         Result += S
  396.       else:
  397.         Result.append((Pt_Anchor, RearParietal))
  398.         Result.append((Pt_Anchor, Rc))
  399.  
  400.       AM = AffineMtxTS((0.0, 0.0), (1.0, -1.0))
  401.       Result += TransformedShape(AM, list(reversed(Result)))[1:]
  402.  
  403.       return Result
  404.  
  405.     #---------------------------------------------------------------------------
  406.  
  407.     if Circumference < 0.1:
  408.       raise Error(
  409.         (
  410.           'A crown circumference of %scm is far too small. ' +
  411.             'Try something between 34 and 66.'
  412.         ) % (str(Circumference))
  413.       )
  414.  
  415.     if not 0.1 <= CrownAspect <= 10.0:
  416.       raise Error(
  417.         (
  418.           'A crown aspect of %s is far too silly. ' +
  419.             'Try something between 0.6 and 0.8.'
  420.         ) % (str(CrownAspect))
  421.       )
  422.  
  423.     if not 0.1 <= ForeheadRatio <= 10.0:
  424.       raise Error(
  425.         (
  426.           'A forehead width ratio of %s is far too silly. ' +
  427.             'Try something between 0.6 and 0.8.'
  428.         ) % (str(ForeheadRatio))
  429.       )
  430.  
  431.     F = 0.5 * CrownAspect * ForeheadRatio * FrontAspect
  432.     R = 0.5 * CrownAspect * RearAspect
  433.  
  434.     FRAspectScale = 1.0 / (F + R) if F + R > 1.0 else 1.0
  435.     FA = FRAspectScale * FrontAspect
  436.     RA = FRAspectScale * RearAspect
  437.     FR = ForeheadRatio
  438.  
  439.     DoFlipBackToFront = ForeheadRatio > 1.0
  440.  
  441.     if DoFlipBackToFront:
  442.       FA, RA = RA, FA
  443.       FR = 1.0 / FR
  444.  
  445.     Crown = UnscaledCrown(CrownAspect, FR, FA, RA)
  446.  
  447.     InitialCircumference = ShapeLength(Crown)
  448.  
  449.     AdjScale = Circumference / InitialCircumference
  450.     AdjScales = [AdjScale, AdjScale]
  451.  
  452.     if DoFlipBackToFront:
  453.       AdjScales[0] *= -1
  454.       Crown.reverse()
  455.  
  456.     M = AffineMtxTS((0.0, 0.0), AdjScales)
  457.     Crown = TransformedShape(M, Crown)
  458.  
  459.     CL = INITIAL_LENGTH * AdjScale
  460.     CW = CL * CrownAspect
  461.  
  462.     if ForeheadRatio < 1.0:
  463.       RW = CW
  464.       FW = RW * ForeheadRatio
  465.     else:
  466.       FW = CW
  467.       RW = FW / ForeheadRatio
  468.  
  469.     self.CrownPath = Crown
  470.     self.Circumference = Circumference
  471.     self.CrownAspect = CrownAspect
  472.     self.CrownLength = CL
  473.     self.CrownWidth =  CW
  474.     self.ForeheadWidth = FW
  475.     self.RearWidth = RW
  476.     self.ForeheadRatio = ForeheadRatio
  477.     self.FrontAspect = FrontAspect
  478.     self.RearAspect = RearAspect
  479.  
  480.   #-----------------------------------------------------------------------------
  481.  
  482.  
  483. #-------------------------------------------------------------------------------
  484. # tHatMetrics
  485. #-------------------------------------------------------------------------------
  486.  
  487.  
  488. class tHatMetrics (object):
  489.  
  490.   u'''A tHatMetrics instance completely describes a pith helmet.
  491.  
  492.  The metrics are independent of how the helmet is sliced. No hull scale
  493.  correction is applied. The coordinates (x, y, z) coreespond to forward,
  494.  right and down. The xy plane is transverse, the xz plane is saggital
  495.  and the yz plane is coronal.
  496.  
  497.  The angles used by RadialSlice2D and RadialSlice3D are in radians and
  498.  run from x (forward) to y (right) in the first ½π radians (90°).
  499.  
  500.  RadialSlice2D returns a Shape with 2D points where (x, y) corresponds
  501.  to (distal, down).
  502.  
  503.  RadialSlice3D returns 3D points in the (forward, right, down) system.
  504.  
  505.  Methods:
  506.  
  507.    __init__(CrownMetrics, DomeAspect)
  508.    RadialSlice2D(self, Angle)
  509.    RadialSlice3D(self, Angle)
  510.  
  511.  Fields:
  512.  
  513.    CrownMetrics
  514.    BrimPath
  515.    DomeAspect
  516.  
  517.  '''
  518.  
  519.   #-----------------------------------------------------------------------------
  520.  
  521.   def __init__(self, CrownMetrics, DomeAspect):
  522.  
  523.     u'''Construct a keeper of metrics for a pith helmet.
  524.  
  525.    Constructor Arguments:
  526.  
  527.      CrownMetrics:
  528.        A tCrownMetrics instance which defines the crown of the head.
  529.  
  530.      DomeAspect:
  531.        The height of the dome above the crown as a ratio of the radius
  532.        of a circle with the same circumference as the crown.
  533.        1.1 is the reference value.
  534.  
  535.    '''
  536.  
  537.     #---------------------------------------------------------------------------
  538.  
  539.     def RadialTestLineLength(Shape):
  540.  
  541.       '''Find a suitable length for a radial intersection test line.'''
  542.  
  543.       Result = 0.0
  544.  
  545.       for PtType, P in Shape:
  546.         Result = max(Result, VManhattan(P))
  547.  
  548.       Result *= 2.0
  549.  
  550.       return Result
  551.  
  552.     #---------------------------------------------------------------------------
  553.  
  554.     CM = CrownMetrics
  555.  
  556.     ch = 0.0
  557.     fh = ch + 0.075 * CM.Circumference # 4.5
  558.     ph = ch + 0.04 * CM.Circumference # 3.0
  559.     rh = ch + 0.1 * CM.Circumference # 6.0
  560.     fd = 0.045 * CM.Circumference # 2.5
  561.     pd = 0.015 * CM.Circumference # 1.0
  562.     rd = 0.065 * CM.Circumference # 4.0
  563.  
  564.     # Front and rear extremities
  565.     fx = 0.5 * CM.CrownLength + fd
  566.     rx = -0.5 * CM.CrownLength - rd
  567.  
  568.     # Slope from front to back
  569.     g = (fh - rh) / (fx - rx)
  570.  
  571.     CrownPath = CrownMetrics.CrownPath
  572.     TestLineLength = RadialTestLineLength(CrownPath)
  573.  
  574.     BrimPath = []
  575.  
  576.     # Parietal point and tangent for brim
  577.     Dexter = ((0.0, 0.0), (0.0, TestLineLength))
  578.     Intersections = LineToShapeIntersections(Dexter, CrownPath)
  579.     P, PT2D = Intersections[0][:2]
  580.     PT2D = VNormalised(VNeg(PT2D))
  581.     PT = VNormalised(VAug(PT2D, g))
  582.     PP = VAug(VSum(P, VScaled(VPerp(PT2D), pd)), ph)
  583.  
  584.     BrimPath += [
  585.       (Pt_Anchor, (fx, 0.0, fh)),
  586.       (
  587.         Pt_Control,
  588.         (
  589.           fx - 0.18 * CM.ForeheadWidth,
  590.           1.2 * CM.ForeheadWidth / (2.0 + CM.FrontAspect),
  591.           fh + 0.65 * (ph + g * fx - fh)
  592.         )
  593.       )
  594.     ]
  595.  
  596.     BrimPath += [
  597.       (Pt_Control, VSum(PP, VScaled(PT, 2.1 * fx / (3.0 + CM.FrontAspect)))),
  598.       (Pt_Anchor, PP),
  599.       (Pt_Control, VSum(PP, VScaled(PT, 1.3 * rx / (2.0 + CM.RearAspect))))
  600.     ]
  601.  
  602.     # Radius and half-chord of rearmost part
  603.     hc = 0.95 * CM.RearWidth / (3.0 + CM.RearAspect)
  604.     r = max(1.01 * hc, 0.9 * CM.RearWidth / (1.0 + CM.RearAspect))
  605.     # Angle and centre
  606.     a = asin(hc / r)
  607.     C = (rx + r, 0.0)
  608.     RearArc = PiecewiseArc(C, r, (pi - a, pi), 1)
  609.     Radial = VPolToRect((r, (pi - a)))
  610.     QP = VSum(C, Radial)
  611.     QT = VNormalised(VPerp(Radial))
  612.     RearBrimHalf = ShapeDim(
  613.       [(Pt_Control, VSum(QP, VScaled(QT, -0.27 * CM.RearWidth)))] + RearArc, 3
  614.     )
  615.     # Position and shear the circular segment up-from-forward so
  616.     # that it and the front tip of the helmet are co-planar.
  617.     AM = tAffineMtx(
  618.       (0.0, 0.0, rh - g * rx),
  619.       ((1.0, 0.0, g), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0))
  620.     )
  621.     RearBrimHalf = TransformedShape(AM, RearBrimHalf)
  622.     BrimPath += RearBrimHalf
  623.  
  624.     AM = AffineMtxTS((0.0, 0.0, 0.0), (1.0, -1.0, 1.0))
  625.     BrimPath += TransformedShape(AM, tuple(reversed(BrimPath)))
  626.  
  627.     self.CrownMetrics = CrownMetrics
  628.     self.BrimPath = BrimPath
  629.     self.DomeAspect = DomeAspect
  630.  
  631.   #-----------------------------------------------------------------------------
  632.  
  633.   def RadialSlice2D(self, Angle):
  634.  
  635.     u'''Return a 2D path describing the contour of the helmet at a given angle.
  636.  
  637.    The angle is measured in radians right from front. The path is a 2D Shape
  638.    which runs from the centre of the button on top of the helmet down to a
  639.    point on the brim.
  640.  
  641.    The vertices in Shape are (distal, down) pairs. The transformation matrix
  642.  
  643.        ⎡cos(α) 0  0 ⎤
  644.    M = ⎢sin(α) 0  0 ⎥
  645.        ⎣  0    1  0 ⎦
  646.  
  647.    may be used to transform the 2D coordinates to the (forward, right, down)
  648.    convention.
  649.  
  650.    '''
  651.  
  652.     CM = self.CrownMetrics;
  653.  
  654.     TestLine = ((0.0, 0.0), VPolToRect((CM.Circumference, Angle)))
  655.  
  656.     if Angle == 0:
  657.  
  658.       CRadius = CM.CrownPath[0][1][0]
  659.       BrimPt3D = self.BrimPath[0][1]
  660.       BrimPt = (BrimPt3D[0], BrimPt3D[2])
  661.  
  662.     else:
  663.  
  664.       Intersection = LineToShapeIntersections(
  665.           TestLine, CM.CrownPath)[0]
  666.       CRadius = VLength(Intersection[0])
  667.  
  668.       Intersection = LineToShapeIntersections(
  669.           TestLine, ShapeDim(self.BrimPath, 2))[0]
  670.  
  671.       P, T, LParam, SubpathIx, CurveIx, CParam = Intersection
  672.  
  673.       CurveRange = ShapeCurveRanges(self.BrimPath)[CurveIx]
  674.       BrimCurve = ShapePoints(self.BrimPath, CurveRange)
  675.       BrimPt3D = BezierPoint(BrimCurve, CParam)
  676.       BrimPt = (hypot(BrimPt3D[0], BrimPt3D[1]), BrimPt3D[2])
  677.  
  678.     ButtonRadius = 0.1 * min(CM.CrownWidth, CM.CrownLength)
  679.     CrownZ = 0.0
  680.     DomeHeight = CM.Circumference * self.DomeAspect / (2.0 * pi)
  681.  
  682.     CrownPath = [
  683.       (Pt_Anchor, (0.0, 1.075)),
  684.       (Pt_Control, (ButtonRadius * 0.43, 1.075)),
  685.       (Pt_Control, (ButtonRadius * 0.91, 1.049)),
  686.       (Pt_Anchor, (ButtonRadius, 1.0)),
  687.       (Pt_Control, (CRadius * 0.496, 0.966)),
  688.       (Pt_Control, (CRadius * 0.889, 0.641)),
  689.       (Pt_Anchor, (CRadius, 0.0)),
  690.     ]
  691.  
  692.     M = AffineMtxTS((0.0, CrownZ), (1.0, -DomeHeight))
  693.     Result = TransformedShape(M, CrownPath)
  694.  
  695.     # Circular arc from crown to brim
  696.  
  697.     CrownPt = Result[-1][1]
  698.     BrimChord = VDiff(BrimPt, CrownPt)
  699.     BCLength = VLength(BrimChord)
  700.  
  701.     if BCLength > 1e-12:
  702.  
  703.       CT = VNormalised(VDiff(CrownPt, Result[-2][1]))
  704.  
  705.       X = VScaled(BrimChord, 1.0 / BCLength)
  706.       Y = VPerp(X)
  707.       Y = Y if VDot(CT, Y) >= 0.0 else VNeg(Y)
  708.       CTx = VDot(CT, X)
  709.       HalfSweep = acos(max(cos(radians(179)), min(1.0, CTx)))
  710.       MinSafeHalfSweep = 5e-9
  711.       SafeHalfSweep = max(MinSafeHalfSweep, HalfSweep)
  712.       r = 0.5 * BCLength / sin(SafeHalfSweep)
  713.       h = r * CubicBezierArcHandleLength(SafeHalfSweep)
  714.       Saggitta = r * (1.0 - cos(SafeHalfSweep))
  715.       M = VSum(VLerp(CrownPt, BrimPt, 0.5), VScaled(Y, Saggitta))
  716.       BT = VSum(CT, VScaled(X, -2.0 * CTx))
  717.  
  718.       Curve = [
  719.         VSum(CrownPt, VScaled(CT, h)),
  720.         VSum(M, VScaled(X, -h)),
  721.         M,
  722.         VSum(M, VScaled(X, h)),
  723.         VSum(BrimPt, VScaled(BT, h))
  724.       ]
  725.  
  726.       if HalfSweep < MinSafeHalfSweep:
  727.         f = HalfSweep / MinSafeHalfSweep
  728.         Flat = [VLerp(CrownPt, BrimPt, i / 6.0) for i in range(1, 6)]
  729.         Flattish = [VLerp(a, b, f) for a, b in zip(Flat, Curve)]
  730.         Curve = Flattish
  731.  
  732.       for i, Pt in enumerate(Curve):
  733.         Result.append(
  734.           (Pt_Anchor if i == 2 else Pt_Control, Pt)
  735.         )
  736.  
  737.     Result.append((Pt_Anchor, BrimPt))
  738.  
  739.     return Result
  740.  
  741.   #-----------------------------------------------------------------------------
  742.  
  743.   def RadialSlice3D(self, Angle):
  744.  
  745.     u'''Return a 3D Shape spacecurve for the helmet at a given angle.
  746.  
  747.    The angle is measured in radians right from front. The Shape runs from
  748.    the centre of the button on top of the helmet down to a point on the
  749.    brim.
  750.  
  751.    '''
  752.  
  753.     AM = tAffineMtx(
  754.       (0.0, 0.0, 0.0),
  755.       ((cos(Angle), sin(Angle), 0.0), (0.0, 0.0, 1.0), (0.0, 0.0, 0.0))
  756.     )
  757.  
  758.     Result = TransformedShape(AM, ShapeDim(self.RadialSlice2D(Angle), 3))
  759.  
  760.     return Result
  761.  
  762.   #-----------------------------------------------------------------------------
  763.  
  764.  
  765. #-------------------------------------------------------------------------------
  766. # tHatSliceMetrics
  767. #-------------------------------------------------------------------------------
  768.  
  769.  
  770. class tHatSliceMetrics (object):
  771.  
  772.   u'''A tHatSliceMetrics keeps track of a hat cut into radial slices.
  773.  
  774.  The slices are indexed from 0 to NumSlices − 1 and are divided into
  775.  two contiguous groups, the saggital group and the coronal group, each
  776.  a stack of slices with slice indices increasing around the head
  777.  clockwise from above for their front and right extremities. Each
  778.  group is stapled along a vertical fold in the middle and its slices
  779.  spread to form a double fan. By means of slots, the two groups are
  780.  joined to form the circular fan of spars over which the hat material
  781.  is lofted.
  782.  
  783.  The crown shape is approximated by a convex polygon with the given
  784.  circumference and with twice the number of sides as there are slices.
  785.  
  786.  Methods:
  787.  
  788.    __init__(HatMetrics, NumSlices, [Verbosity])
  789.    RadialSlice2D(RadialIndex)
  790.    RadialSlice3D(RadialIndex)
  791.  
  792.  Fields:
  793.  
  794.    HatMetrics
  795.    NumSlices
  796.    HullScaleCorrection
  797.    Angles
  798.    Radials
  799.    RadialBrimPoints
  800.    RadialNames
  801.    Saggitals
  802.    Coronals
  803.    HasMiddleRadials
  804.    MaxSliceSpan
  805.    Top
  806.    Bottom
  807.    RingCutPoints
  808.    RingSegments
  809.    RingCutLength
  810.    FenceCutLength
  811.  
  812.  '''
  813.  
  814.   #-----------------------------------------------------------------------------
  815.  
  816.   def __init__(self, HatMetrics, NumSlices, Verbosity=0):
  817.  
  818.     u'''Construct a keeper of metrics of radial hat slices.
  819.  
  820.    Constructor Arguments:
  821.  
  822.      HatMetrics:
  823.        An ideal, mathematical description of a pith helmet.
  824.  
  825.      NumSlices:
  826.        The number of “slices” which must be folded and stapled together to
  827.        make a former for the helmet. The number of slices is half the number
  828.        of sides in the polygon used to approximate the crown.
  829.  
  830.    '''
  831.  
  832.     #---------------------------------------------------------------------------
  833.  
  834.     Circle = 2.0 * pi
  835.  
  836.     #---------------------------------------------------------------------------
  837.  
  838.     def SliceAssignments(NumSlices):
  839.  
  840.       u'''Divide slices into saggital and coronal pairs of indices to radials.
  841.  
  842.      The slices are arranged so they are grouped into saggital and coronal
  843.      groups which are each spread to form a double fan. Because the slices
  844.      are non-intersecting within a single group, all but perhaps the middle
  845.      slice in a group with an odd number of slices are bent at the middle.
  846.  
  847.      This function returns (Saggitals, Coronals, HasMiddleRadials) where
  848.      Saggitals and Coronals are each a list of pairs of indices to radials
  849.      and HasMiddleRadials is True iff the front dead centre and rear dead
  850.      centre radials should be included. (There can only be zero or two
  851.      radials in the middle saggital plane.) It’s possible for the middle
  852.      saggital radials, if they exist, to belong to different slices.
  853.  
  854.      '''
  855.  
  856.       #-------------------------------------------------------------------------
  857.  
  858.       PREFER_MIDDLE_SAGGITAL = True
  859.  
  860.       #-------------------------------------------------------------------------
  861.  
  862.       if NumSlices == 2:
  863.  
  864.         NumCoronals = 1
  865.         NumSaggitals = 1
  866.         Saggitals = [(0, 2)]
  867.         Coronals = [(1, 3)]
  868.         HasMiddleRadials = False
  869.  
  870.       else:
  871.  
  872.         NumSaggitals = NumSlices // 2
  873.         NumCoronals = NumSlices - NumSaggitals
  874.  
  875.         if NumSaggitals & 1 == 0 and NumCoronals & 1 != 0:
  876.           NumSaggitals, NumCoronals = NumCoronals, NumSaggitals
  877.  
  878.         Saggitals = []
  879.         Coronals = []
  880.         n = 2 * NumSlices
  881.  
  882.         d = -(NumSaggitals // 2)
  883.         f = NumSlices + d + NumSaggitals - 1
  884.  
  885.         for i in range(NumSaggitals):
  886.           Saggitals.append(((d + i) % n, f - i))
  887.  
  888.         f = (d - 1) % n
  889.         d = d + NumSaggitals
  890.  
  891.         for i in range(NumCoronals):
  892.           Coronals.append((d + i, f - i))
  893.  
  894.         HasMiddleRadials = PREFER_MIDDLE_SAGGITAL or (NumSaggitals & 1 != 0)
  895.  
  896.       return (Saggitals, Coronals, HasMiddleRadials)
  897.  
  898.     #---------------------------------------------------------------------------
  899.  
  900.     def RadialName(NumSlices, HasMiddleRadials, RadialIndex):
  901.  
  902.       u'''Return a short, friendly name for a radial hat slice.
  903.  
  904.      For a four-slice hat with a middle saggital slice, the eight radials
  905.      would be named “Front”, “R1”, “R2”, “R3”, “Back”, “L3”, “L2” and “L1”.
  906.  
  907.      '''
  908.  
  909.       if HasMiddleRadials:
  910.  
  911.         if RadialIndex < NumSlices:
  912.           NameIx = RadialIndex
  913.           IsMiddleSaggital = (RadialIndex == 0)
  914.         else:
  915.           NameIx = -(2 * NumSlices - RadialIndex)
  916.           IsMiddleSaggital = (RadialIndex == NumSlices)
  917.  
  918.       else:
  919.  
  920.         IsMiddleSaggital = False
  921.  
  922.         if RadialIndex < NumSlices:
  923.           NameIx = RadialIndex + 1
  924.         else:
  925.           NameIx = -(2 * NumSlices - RadialIndex)
  926.  
  927.       if IsMiddleSaggital:
  928.         Result = 'Front' if NameIx == 0 else 'Back'
  929.       else:
  930.         Result = ('L' if NameIx < 0 else 'R') + str(abs(NameIx))
  931.  
  932.       return Result
  933.  
  934.     #---------------------------------------------------------------------------
  935.  
  936.     def RadialTestLineLength(CrownShape):
  937.  
  938.       '''Find a suitable length for a radial intersection test line.'''
  939.  
  940.       Result = 0.0
  941.  
  942.       for PtType, P in CrownShape:
  943.         Result = max(Result, VManhattan(P))
  944.  
  945.       Result *= 2.0
  946.  
  947.       return Result
  948.  
  949.     #---------------------------------------------------------------------------
  950.  
  951.     def OptimisedRadials(CrownShape, HasMiddleRadials, RHAngles):
  952.  
  953.       u'''Generate radial crown vectors from initial right hemisphere angles.
  954.  
  955.      The angles supplied must be strictly greater than zero and strictly
  956.      less than pi. HasMiddleRadials is True iff the radials are to include
  957.      those which lie in the middle saggital plane.
  958.  
  959.      The radial vectors returned lie in the transverse plane and include
  960.      both the left and right hemispheres. If HasMiddleRadials is True,
  961.      the front dead centre and rear dead centre radials are included.
  962.  
  963.      The radials are ordered clockwise from front as seen from above.
  964.      If no front dead centre radial exists, the first radial is the one
  965.      in the right hemisphere closest to front dead centre.
  966.  
  967.      '''
  968.  
  969.       #-------------------------------------------------------------------------
  970.  
  971.       def CumulativeArcLengthsForRanges(ShapeVertices, CRanges):
  972.  
  973.         u'''Return the cumulative arc length for each Bézier endpoint.
  974.  
  975.        The arc lengths returned allows the arc length of a Shape up to a
  976.        particular point to be determined without having to sum the total
  977.        arc lengths of all the Shape’s Bézier curves prior to the one which
  978.        contains that point.
  979.  
  980.        '''
  981.  
  982.         Result = []
  983.         d = 0.0
  984.  
  985.         for R in CRanges:
  986.           d += BezierLength(ShapeVertices[R[0]:R[1]])
  987.           Result.append(d)
  988.  
  989.         return Result
  990.  
  991.       #-------------------------------------------------------------------------
  992.  
  993.       def CumulativeArcLength(ShapeVertices, CRanges, CALs, CurveIx, CParam):
  994.  
  995.         u'''Efficiently compute a Shape’s arc length up to a point.
  996.  
  997.        ShapeVertices is the Shape with the tags removed. CRanges is a list
  998.        of all the curve ranges in the Shape, one for each Bézier. Each curve
  999.        range is a pair of inclusive-start and exclusive-end indices within
  1000.        ShapeVertices. CALs is the list of cumulative arc lengths.
  1001.  
  1002.        The point on the Shape is given by CurveIx and CParam, the Bézier
  1003.        curve parameter.
  1004.  
  1005.        '''
  1006.  
  1007.         Result = CALs[CurveIx - 1] if CurveIx > 0 else 0.0
  1008.         R = CRanges[CurveIx]
  1009.         Curve = ShapeVertices[R[0]:R[1]]
  1010.         Result += BezierLength(Curve, (0, CParam))
  1011.  
  1012.         return Result
  1013.  
  1014.       #-------------------------------------------------------------------------
  1015.  
  1016.       def WanderersFromVectors(
  1017.         CrownShape, CrownVertices, CRanges, TestLineLength, RadialVectors
  1018.       ):
  1019.  
  1020.         u'''Create Wanderers from radial vectors to optimise a hat’s crown.
  1021.  
  1022.        Returned is a list of (Point, Tangent, CurveIx, CParam) tuples, one
  1023.        for each radial vector extended until it intersects the crown Shape.
  1024.  
  1025.        '''
  1026.  
  1027.         Result = []
  1028.  
  1029.         for i, V in enumerate(RadialVectors):
  1030.  
  1031.           if HasMiddleRadials and (i == 0):
  1032.  
  1033.             Result.append((CrownVertices[0], (0.0, 1.0), 0, 0.0))
  1034.  
  1035.           else:
  1036.  
  1037.             Line = ((0.0, 0.0), VScaled(VNormalised(V), TestLineLength))
  1038.             Intersections = LineToShapeIntersections(Line, CrownShape)
  1039.  
  1040.             if len(Intersections) == 0:
  1041.               Angle = atan2(Line[1][1], Line[1][0])
  1042.               raise Error(
  1043.                 u'No intersection found at radial %d (%g°)' %
  1044.                   (i, degrees(Angle))
  1045.               )
  1046.  
  1047.             P, Tangent, LParam, SubpathIx, CurveIx, CParam = Intersections[0]
  1048.  
  1049.             R = CRanges[CurveIx]
  1050.             C1, C2 = SplitBezier(CrownVertices[R[0]:R[1]], CParam)
  1051.             Tangent = VNormalised(VDiff(C2[1], C1[-2]))
  1052.             Result.append((P, Tangent, CurveIx, CParam))
  1053.  
  1054.         return Result
  1055.  
  1056.       #-------------------------------------------------------------------------
  1057.  
  1058.       TestLineLength = RadialTestLineLength(CrownShape)
  1059.       Line = ((0.0, 0.0), (-TestLineLength, 0.0))
  1060.       FrontRadial = CrownShape[0][1]
  1061.       RearRadial = (LineToShapeIntersections(Line, CrownShape)[0][0][0], 0.0)
  1062.  
  1063.       if HasMiddleRadials:
  1064.         Angles = [0.0] + RHAngles + [pi]
  1065.       else:
  1066.         Angles = [-RHAngles[0]] + RHAngles + [-RHAngles[-1]]
  1067.  
  1068.       N = len(Angles)
  1069.       CVs = ShapePoints(CrownShape)
  1070.       CRs = ShapeCurveRanges(CrownShape)
  1071.       CALs = CumulativeArcLengthsForRanges(CVs, CRs)
  1072.       Circumference = CALs[-1]
  1073.       Tolerance = 0.02 * Circumference / max(8, N)
  1074.  
  1075.       RadialVectors = []
  1076.  
  1077.       for Angle in Angles:
  1078.         RadialVectors.append(VPolToRect((1.0, Angle)))
  1079.  
  1080.       Wanderers = WanderersFromVectors(
  1081.         CrownShape, CVs, CRs, TestLineLength, RadialVectors
  1082.       )
  1083.  
  1084.       LastDeltas = [0.0] * N
  1085.       SpeedLimits = [1.0] * N
  1086.  
  1087.       MaxStressDiff = float('inf')
  1088.       StressBounds = [0.0, float('inf')]
  1089.       NumSteps = 0
  1090.       Limit = 50 + 10 * N
  1091.  
  1092.       if Verbosity >= 2:
  1093.         print "Optimising angles of slice radials for crown shape..."
  1094.  
  1095.       while StressBounds[1] - StressBounds[0] > Tolerance and NumSteps < Limit:
  1096.  
  1097.         WCALs = []
  1098.  
  1099.         for W in Wanderers:
  1100.           CurveIx, CParam = W[2:4]
  1101.           a = CumulativeArcLength(CVs, CRs, CALs, CurveIx, CParam)
  1102.           WCALs.append(a)
  1103.  
  1104.         Sides = []
  1105.         Stresses = []
  1106.  
  1107.         for i in range(N - 1):
  1108.           j = i + 1
  1109.           ChordLength = VLength(VDiff(Wanderers[j][0], Wanderers[i][0]))
  1110.           CurveLength = (WCALs[j] - WCALs[i]) % Circumference
  1111.           Deviation = ApproxSaggita(CurveLength, min(CurveLength, ChordLength))
  1112.           Stress = Deviation + 0.02 * ChordLength
  1113.           Sides.append(ChordLength)
  1114.           Stresses.append(Stress)
  1115.  
  1116.         RadialVectors = []
  1117.         MaxStressDiff = 0.0
  1118.         StressBounds = [float('inf'), 0.0]
  1119.  
  1120.         for i in range(1, len(Wanderers) - 1):
  1121.           W = Wanderers[i]
  1122.           h = i - 1
  1123.           P, Tangent = W[0:2]
  1124.           ChordLength1 = Sides[h]
  1125.           ChordLength2 = Sides[i]
  1126.           s1 = Stresses[h]
  1127.           s2 = Stresses[i]
  1128.           StressBounds[0] = min(StressBounds[0], min(s1, s2))
  1129.           StressBounds[1] = max(StressBounds[1], max(s1, s2))
  1130.           StressDiff = s2 - s1
  1131.           MaxStressDiff = max(MaxStressDiff, abs(StressDiff))
  1132.           SpeedLimit = SpeedLimits[i]
  1133.           Delta = 6.0 * StressDiff
  1134.           if Delta < 0:
  1135.             Delta = max(-0.25 * ChordLength1, -(s1 + 1e-6) / (1e-6 + s1 + s2))
  1136.           else:
  1137.             Delta = min(0.25 * ChordLength2, (s2 + 1e-6) / (1e-6 + s1 + s2))
  1138.           LD = LastDeltas[i]
  1139.           if LD != 0.0 and Delta != 0.0:
  1140.             if LD * Delta < 0.0:
  1141.               SpeedLimit *= 0.25
  1142.           Delta *= SpeedLimit
  1143.           LastDeltas[i] = Delta
  1144.           SpeedLimit = min(1.0, SpeedLimit * 1.2)
  1145.           SpeedLimits[i] = SpeedLimit
  1146.           P = VSum(P, VScaled(Tangent, Delta))
  1147.           RadialVectors.append(P)
  1148.  
  1149.         if HasMiddleRadials:
  1150.           RadialVectors = [FrontRadial] + RadialVectors + [RearRadial]
  1151.         else:
  1152.           LeftFront = (RadialVectors[0][0], -RadialVectors[0][1])
  1153.           LeftRear = (RadialVectors[-1][0], -RadialVectors[-1][1])
  1154.           RadialVectors = [LeftFront] + RadialVectors + [LeftRear]
  1155.  
  1156.         Wanderers = WanderersFromVectors(
  1157.           CrownShape, CVs, CRs, TestLineLength, RadialVectors
  1158.         )
  1159.  
  1160.         NumSteps += 1
  1161.  
  1162.         if Verbosity >= 2:
  1163.           print "Step %d complete" % (NumSteps)
  1164.  
  1165.       if Verbosity >= 2:
  1166.         print "Optimised in %d steps." % (NumSteps)
  1167.  
  1168.       RHRadials = [W[0] for W in Wanderers[1:-1]]
  1169.  
  1170.       # The left hemisphere is a mirror of the right hemisphere. If
  1171.       # there is a pair of true saggital radials, they must be must
  1172.       # be inserted.
  1173.  
  1174.       LHRadials = [(P[0], -P[1]) for P in reversed(RHRadials)]
  1175.  
  1176.       if HasMiddleRadials:
  1177.         Result = [FrontRadial] + RHRadials + [RearRadial] + LHRadials
  1178.       else:
  1179.         Result = RHRadials + LHRadials
  1180.  
  1181.       return Result
  1182.  
  1183.     #---------------------------------------------------------------------------
  1184.  
  1185.     def CircumferenceOfPolygon(Vertices):
  1186.  
  1187.       '''Return the circumference of a polygon defined as a list of vertices.
  1188.  
  1189.      If the polygon contains no vertices, None is returned. If the polygon
  1190.      has just one vertex, 0.0 is returned.
  1191.  
  1192.      '''
  1193.  
  1194.       if len(Vertices) > 0:
  1195.  
  1196.         Result = 0.0
  1197.         LastP = Vertices[-1]
  1198.  
  1199.         for P in Vertices:
  1200.           Result += VLength(VDiff(P, LastP))
  1201.           LastP = P
  1202.  
  1203.       else:
  1204.  
  1205.         Result = None
  1206.  
  1207.       return Result
  1208.  
  1209.     #---------------------------------------------------------------------------
  1210.  
  1211.     def RingSegments(Angles, RingCutPoints):
  1212.  
  1213.       u'''Return four sequences of radial indices, one for each ring segment.
  1214.  
  1215.      Each ring segment spans roughly a quarter of the circumference of the
  1216.      bracing ring which holds the radial slices in place.
  1217.  
  1218.      If the first angle is 0°, the four segments are front-left, front-right,
  1219.      rear-right and rear-left, else they are front, back, left and right.
  1220.  
  1221.      '''
  1222.  
  1223.       CumSpans = []
  1224.       N = len(Angles)
  1225.       HasMiddleSaggitals = (Angles[0] == 0.0)
  1226.  
  1227.       if HasMiddleSaggitals:
  1228.         LastPt = RingCutPoints[0]
  1229.         CumSpan = 0.0
  1230.         CumSpans.append(CumSpan)
  1231.         RearIx = N // 2
  1232.       else:
  1233.         LastPt = RingCutPoints[0]
  1234.         CumSpan = LastPt[1]
  1235.         CumSpans.append(CumSpan)
  1236.         RearIx = N // 2 - 1
  1237.  
  1238.       for i in range(1, RearIx + 1):
  1239.         Pt = RingCutPoints[i]
  1240.         CumSpan += VLength(VDiff(LastPt, Pt))
  1241.         CumSpans.append(CumSpan)
  1242.         LastPt = Pt
  1243.  
  1244.       if not HasMiddleSaggitals:
  1245.         CumSpan += LastPt[1]
  1246.         CumSpans.append(CumSpan)
  1247.  
  1248.       if HasMiddleSaggitals:
  1249.  
  1250.         Threshold = 0.5 * CumSpan
  1251.         Ix = 0
  1252.  
  1253.         for i, c in enumerate(CumSpans[:-1]):
  1254.           c1 = CumSpans[i + 1]
  1255.           if c1 > Threshold:
  1256.             Ix = i if Threshold < (0.5 * (c + c1)) else i + 1
  1257.             break
  1258.  
  1259.         FL = tuple(range(N - Ix, N)) + (0,)
  1260.         FR = tuple(range(0, Ix + 1))
  1261.         BR = tuple(range(Ix, RearIx + 1))
  1262.         BL = tuple(range(RearIx, N - Ix + 1))
  1263.  
  1264.         Result = (FL, FR, BR, BL)
  1265.  
  1266.       else:
  1267.  
  1268.         Threshold1 = 0.25 * CumSpan
  1269.         Threshold2 = 0.75 * CumSpan
  1270.  
  1271.         Ix1 = 0
  1272.  
  1273.         while Ix1 < len(CumSpans) - 1:
  1274.           if CumSpans[Ix1 + 1] > Threshold1:
  1275.             break
  1276.           Ix1 += 1
  1277.  
  1278.         Ix2 = Ix1
  1279.  
  1280.         while Ix2 < len(CumSpans) - 1:
  1281.           if CumSpans[Ix2 + 1] > Threshold2:
  1282.             break
  1283.           Ix2 += 1
  1284.  
  1285.         Variants = (
  1286.           (Ix1, Ix2), (Ix1, Ix2 + 1), (Ix1 + 1, Ix2), (Ix1 + 1, Ix2 + 1)
  1287.         )
  1288.  
  1289.         Ix1 = None
  1290.         Ix1 = None
  1291.         BestD = CumSpan
  1292.  
  1293.         for Variant in Variants:
  1294.           c1 = CumSpans[Variant[0]]
  1295.           c2 = CumSpans[Variant[1]]
  1296.           D = max(2.0 * c1, c2 - c1, 2.0 * (CumSpan - c2))
  1297.           if D < BestD:
  1298.             Ix1, Ix2 = Variant
  1299.             BestD = D
  1300.  
  1301.         F = tuple(range(N - 1 - Ix1, N)) + tuple(range(0, Ix1 + 1))
  1302.         B = tuple(range(Ix2, N - Ix2))
  1303.         L = tuple(range(N - 1 - Ix2, N - Ix1))
  1304.         R = tuple(range(Ix1, Ix2 + 1))
  1305.  
  1306.         Result = (F, B, L, R)
  1307.  
  1308.       return Result
  1309.  
  1310.     #---------------------------------------------------------------------------
  1311.  
  1312.     if NumSlices < 2:
  1313.       raise Error('NumSlices must be at least 2.')
  1314.  
  1315.     HM = HatMetrics
  1316.     CM = HM.CrownMetrics
  1317.  
  1318.     Saggitals, Coronals, HasMiddleRadials = SliceAssignments(NumSlices)
  1319.  
  1320.     RadialNames = []
  1321.  
  1322.     for RadialIndex in range(2 * NumSlices):
  1323.       RadialNames.append(RadialName(NumSlices, HasMiddleRadials, RadialIndex))
  1324.  
  1325.     RHAngles = []
  1326.  
  1327.     if HasMiddleRadials:
  1328.       Phase = 1
  1329.       NumRightHemisphereRadials = NumSlices - 1
  1330.     else:
  1331.       Phase = 0.5
  1332.       NumRightHemisphereRadials = NumSlices
  1333.  
  1334.     for i in range(NumRightHemisphereRadials):
  1335.       RefAngle = ((Phase + float(i)) / (2.0 * NumSlices)) * Circle
  1336.       x = CM.CrownLength * cos(RefAngle)
  1337.       y = CM.CrownWidth * sin(RefAngle)
  1338.       Angle = atan2(y, x)
  1339.       RHAngles.append(Angle)
  1340.  
  1341.     Radials = OptimisedRadials(CM.CrownPath, HasMiddleRadials, RHAngles)
  1342.  
  1343.     Angles = []
  1344.  
  1345.     for P in Radials:
  1346.       Angle = atan2(P[1], P[0])
  1347.       Angles.append(Angle)
  1348.  
  1349.     HullCircumference = CircumferenceOfPolygon(Radials)
  1350.     HullAdj = CM.Circumference / HullCircumference
  1351.     Radials = [VScaled(P, HullAdj) for P in Radials]
  1352.  
  1353.     MaxSliceSpan = 0.0
  1354.     Top = float('inf')
  1355.     Bottom = float('-inf')
  1356.  
  1357.     RadialBrimPoints = []
  1358.  
  1359.     for i, Angle in enumerate(Angles):
  1360.       R = HM.RadialSlice2D(Angle)
  1361.       BrimPt = R[-1][1]
  1362.       Top = min(Top, R[0][1][1])
  1363.       Bottom = max(Bottom, BrimPt[1])
  1364.       BrimPt3D = (
  1365.         HullAdj * cos(Angle) * BrimPt[0],
  1366.         HullAdj * sin(Angle) * BrimPt[0],
  1367.         BrimPt[1]
  1368.       )
  1369.       RadialBrimPoints.append(BrimPt3D)
  1370.  
  1371.     for Ix1, Ix2 in Saggitals + Coronals:
  1372.       BrimPt1 = RadialBrimPoints[Ix1]
  1373.       BrimPt2 = RadialBrimPoints[Ix2]
  1374.       SliceSpan = hypot(BrimPt1[0], BrimPt1[1]) + hypot(BrimPt2[0], BrimPt2[1])
  1375.       MaxSliceSpan = max(MaxSliceSpan, SliceSpan)
  1376.  
  1377.     RingCutPoints = []
  1378.     Margin = 0.1 * HullAdj * min(CM.CrownWidth, CM.CrownLength)
  1379.  
  1380.     for i, BrimPt in enumerate(RadialBrimPoints):
  1381.       BrimXY = VDim(RadialBrimPoints[i], 2)
  1382.       MarginV = VPolToRect((-Margin, Angles[i]))
  1383.       CutPt = VSum(VLerp(BrimXY, Radials[i], 0.5), MarginV)
  1384.       RingCutPoints.append(CutPt)
  1385.  
  1386.     RS = RingSegments(Angles, RingCutPoints)
  1387.  
  1388.     self.HatMetrics = HM
  1389.     self.NumSlices = NumSlices
  1390.     self.HullScaleCorrection = HullAdj
  1391.     self.Angles = Angles
  1392.     self.Radials = Radials
  1393.     self.RadialBrimPoints = RadialBrimPoints
  1394.     self.RadialNames = RadialNames
  1395.     self.Saggitals = Saggitals
  1396.     self.Coronals = Coronals
  1397.     self.HasMiddleRadials = HasMiddleRadials
  1398.     self.MaxSliceSpan = MaxSliceSpan
  1399.     self.Top = Top
  1400.     self.Bottom = Bottom
  1401.     self.BaseHeight = 1.5
  1402.     self.RingCutPoints = RingCutPoints
  1403.     self.RingSegments = RS
  1404.     self.RingCutLength = max(1.0, min(1.9, Bottom / 3.0))
  1405.     self.FenceCutLength = max(1.0, (Bottom - Top) / 12.0)
  1406.  
  1407.   #-----------------------------------------------------------------------------
  1408.  
  1409.   def RadialSlice2D(self, RadialIndex):
  1410.  
  1411.     u'''Return a 2D path describing the contour of the helmet at a given radial.
  1412.  
  1413.    The horizontal scale of the 2D Shape that is returned is adjusted by the
  1414.    hull scale correction so that the circumference of the polygonal crown is
  1415.    the same as the smooth crown shape.
  1416.  
  1417.    The vertices in Shape are (distal, down) pairs. The transformation matrix
  1418.  
  1419.        ⎡cos(α) 0  0 ⎤
  1420.    M = ⎢sin(α) 0  0 ⎥
  1421.        ⎣  0    1  0 ⎦
  1422.  
  1423.    may be used to transform the 2D coordinates to the (forward, right, down)
  1424.    convention.
  1425.  
  1426.    '''
  1427.  
  1428.     R = self.HatMetrics.RadialSlice2D(self.Angles[RadialIndex])
  1429.     s = self.HullScaleCorrection
  1430.     Result = [(Cmd, (s * Pt[0], Pt[1])) for Cmd, Pt in R]
  1431.  
  1432.     return Result
  1433.  
  1434.   #-----------------------------------------------------------------------------
  1435.  
  1436.   def RadialSlice3D(self, RadialIndex):
  1437.  
  1438.     u'''Return a 3D Shape spacecurve for the helmet at a given radial.
  1439.  
  1440.    The horizontal scale of the 3D Shape that is returned is adjusted by the
  1441.    hull scale correction so that the circumference of the polygonal crown is
  1442.    the same as the smooth crown shape.
  1443.  
  1444.    '''
  1445.  
  1446.     Angle = self.Angles[RadialIndex]
  1447.     R = self.HatMetrics.RadialSlice2D(Angle)
  1448.     sc = self.HullScaleCorrection * cos(Angle)
  1449.     ss = self.HullScaleCorrection * sin(Angle)
  1450.     Result = [(Cmd, (sc * Pt[0], ss * Pt[0], Pt[1])) for Cmd, Pt in R]
  1451.  
  1452.     return Result
  1453.  
  1454.   #-----------------------------------------------------------------------------
  1455.  
  1456.  
  1457. #-------------------------------------------------------------------------------
  1458. # Command line parsing functions
  1459. #-------------------------------------------------------------------------------
  1460.  
  1461.  
  1462. def ParseSequence(NumbersStr, MinValue, MaxValue):
  1463.  
  1464.   u'''Return a list of numbers from a comma-separated string of numeric ranges.
  1465.  
  1466.  Runs of numbers may be indicated with the hyphen-minus character "-".
  1467.  A missing number before the "-" character is taken to be MinValue and a
  1468.  missing after after the "-" is taken to be MaxValue. All the numbers must
  1469.  be non-negative and within the inclusive range [MinValue, MaxValue].
  1470.  
  1471.  Spaces are permitted but not required.
  1472.  
  1473.  '''
  1474.  
  1475.   #-----------------------------------------------------------------------------
  1476.  
  1477.   def CheckRange(x):
  1478.  
  1479.     u'''Throw an exception if x is outside the range [MinValue, MaxValue]. '''
  1480.  
  1481.     if not (MinValue <= x <= MaxValue):
  1482.       raise OptError('The value ' + str(x) + ' is outside the required ' +
  1483.           'range ' + str(MinValue) + '-' + str(MaxValue) + '.')
  1484.  
  1485.   #-----------------------------------------------------------------------------
  1486.  
  1487.   if NumbersStr is None or NumbersStr.strip() == '':
  1488.     return []
  1489.  
  1490.   NSTail = NumbersStr
  1491.   NRangeStrs = []
  1492.   NRs = []
  1493.   NonreversedNRs = []
  1494.  
  1495.   while ',' in NSTail:
  1496.     CommaPos = NSTail.index(',')
  1497.     NRangeStrs.append(NSTail[:CommaPos].strip())
  1498.     NSTail = NSTail[CommaPos + 1:]
  1499.  
  1500.   NRangeStrs.append(NSTail.strip())
  1501.  
  1502.   for NRangeStr in NRangeStrs:
  1503.  
  1504.     if NRangeStr in ['*', 'all']:
  1505.  
  1506.       FirstN = MinValue
  1507.       LastN = MaxValue
  1508.  
  1509.     else:
  1510.  
  1511.       EnDash = '–'
  1512.       EmDash = '—'
  1513.       HyphenMinus = '-'
  1514.       RunCh = ''
  1515.  
  1516.       if EnDash in NRangeStr:
  1517.         RunCh = EnDash
  1518.       if EmDash in NRangeStr:
  1519.         RunCh = EmDash
  1520.       elif HyphenMinus in NRangeStr:
  1521.         RunCh = HyphenMinus
  1522.  
  1523.       if RunCh != '':
  1524.  
  1525.         x = NRangeStr.index(RunCh)
  1526.         FirstNStr = NRangeStr[:x].strip()
  1527.         LastNStr = NRangeStr[x + len(RunCh):].strip()
  1528.  
  1529.         try:
  1530.           NStr = FirstNStr
  1531.           FirstN = int(FirstNStr) if FirstNStr != '' else MinValue
  1532.           NStr = LastNStr
  1533.           LastN = int(LastNStr) if LastNStr != '' else MaxValue
  1534.         except (ValueError):
  1535.           raise OptError('Expected number but found "' + str(NStr) +
  1536.               '" in the range "' + str(NRangeStr) + '".')
  1537.  
  1538.         CheckRange(FirstN)
  1539.         CheckRange(LastN)
  1540.         NR = (FirstN, LastN)
  1541.  
  1542.       else:
  1543.  
  1544.         try:
  1545.           FirstN = int(NRangeStr.strip())
  1546.         except (ValueError):
  1547.           raise OptError('Expected number but found "' + str(NRangeStr) + '".')
  1548.  
  1549.         CheckRange(FirstN)
  1550.         LastN = FirstN
  1551.  
  1552.     if FirstN <= LastN:
  1553.       MinN, MaxN = FirstN, LastN
  1554.     else:
  1555.       MinN, MaxN = LastN, FirstN
  1556.  
  1557.     for NR in NonreversedNRs:
  1558.       if MinN <= NR[1] and MaxN >= NR[0]:
  1559.         raise OptError('The range "' + str(NRangeStr) + '" overlaps ' +
  1560.             'a previous number or run of numbers.')
  1561.  
  1562.     NRs.append((FirstN, LastN))
  1563.     NonreversedNRs.append((MinN, MaxN))
  1564.  
  1565.   #NRs.sort()
  1566.  
  1567.   Result = []
  1568.  
  1569.   for NR in NRs:
  1570.     if NR[0] <= NR[1]:
  1571.       Result += list(range(NR[0], NR[1] + 1))
  1572.     else:
  1573.       Result += list(range(NR[0], NR[1] - 1, -1))
  1574.  
  1575.   return Result
  1576.  
  1577.  
  1578. #-------------------------------------------------------------------------------
  1579.  
  1580.  
  1581. def ParseValue(Name, ValueStr, DataType, TypeStr, MinValueStr, MaxValueStr):
  1582.  
  1583.   u'''Read an option argument string of a known datatype.
  1584.  
  1585.  If the argument string fails to be read or is out of range, an OptError
  1586.  exception is raised.
  1587.  
  1588.  Name:
  1589.    The name of the option, usually including the option introducer included.
  1590.  
  1591.  ValueStr:
  1592.    The option value as it appears on the command line, possibly preprocessd
  1593.    by a run-time environment which automatically handles quotes.
  1594.  
  1595.  DataType:
  1596.    Either None for an option which has no argument or a proper datatype such
  1597.    as int, float or str. The datatype must have the __name__ property.
  1598.  
  1599.  TypeStr:
  1600.    Either the name of the datatype or a human-friendly discription of the
  1601.    datatype.
  1602.  
  1603.  MinValueStr, ManValueStr:
  1604.    String prepresentations of the minimum and maximum permissible values
  1605.    for the given option. A value of None for each value means that checking
  1606.    for that limit is suppressed. If both are not None and ValueStr represents
  1607.    a value outside the limits, the valid range is displayed in the exception
  1608.    message.
  1609.  
  1610.  '''
  1611.  
  1612.   Result = None
  1613.   RErrStr1 = ('The value ' + str(ValueStr) +
  1614.       ' for option ' + str(Name) + ' is too ')
  1615.   RErrStr2 = '.'
  1616.  
  1617.   if MinValueStr is not None and MaxValueStr is not None:
  1618.     RErrStr2 += (' The valid range is from ' + str(MinValueStr) +
  1619.         ' to ' + str(MaxValueStr) + '.')
  1620.  
  1621.   if DataType is None:
  1622.  
  1623.     if ValueStr is not None:
  1624.       raise OptError('Option ' + str(Name) + ' cannot be assigned a value.')
  1625.     Result = True
  1626.  
  1627.   else:
  1628.  
  1629.     if ValueStr is None:
  1630.       raise OptError('Option ' + str(Name) + ' requires a value.')
  1631.  
  1632.     try:
  1633.       Result = DataType(ValueStr)
  1634.     except (ValueError):
  1635.       raise OptError('Expected ' + str(TypeStr) + ' for option ' +
  1636.           str(Name) + ' but instead found "' + str(ValueStr) + '".')
  1637.  
  1638.     if MinValueStr is not None and Result < DataType(MinValueStr):
  1639.       raise OptError(RErrStr1 + 'low' + RErrStr2)
  1640.  
  1641.     if MaxValueStr is not None and Result > DataType(MaxValueStr):
  1642.       raise OptError(RErrStr1 + 'high' + RErrStr2)
  1643.  
  1644.   return Result
  1645.  
  1646.  
  1647. #-------------------------------------------------------------------------------
  1648.  
  1649.  
  1650. def ParseCLOptions(Args, OptsTemplate, OptDataTypes):
  1651.  
  1652.   u'''Preprocess command line options and option arguments.
  1653.  
  1654.  The preprocessing normalises the option and the option arguments, checks the
  1655.  options and their arguments for syntax errors and separates the command line
  1656.  arguments into options and operands. Options are the keywords prefixed by “-”
  1657.  or “--” along with any arguments they require and operands are the positional
  1658.  arguments at the end of the command line.
  1659.  
  1660.  Option arguments may be separated from the option name with a space, an
  1661.  equals sign or a colon.
  1662.  
  1663.  Args:
  1664.    The command line command and its arguments. Fancy run-time environments
  1665.    such as that provided by Python handle quoting and thus allow arguments
  1666.    to have spaces.
  1667.  
  1668.  OptsTemplate:
  1669.    A dictionary indexed by canonical option names with option descriptors of
  1670.    the form (Aliases, DataType, MinValueStr, MaxValueStr). Aliases is a list
  1671.    or tuple of alternative names for the option and like the option name,
  1672.    include the introducer “-” or “--”. MinValueStr and MaxValueStr, when set
  1673.    to values other than None allow lower or upper bounds to be checked within
  1674.    this function.
  1675.  
  1676.  OptDataTypes:
  1677.    A dictionary indexed by the name of each datatype (except None) with
  1678.    entries of the form (DataType, FriendlyName).
  1679.  
  1680.  The result is an (Options, Operands) pair. Options is a list of (StdOptName,
  1681.  GivenOptName, Value) tuples where SydOptName is the canonical optional name,
  1682.  GivenOptName is the optio name as given bythe user and Value is the option
  1683.  argument value converted to the required datatype. Operands is a list of all
  1684.  the command line arguments which follow the options, excluding the optional
  1685.  options-operands separator “--”.
  1686.  
  1687.  '''
  1688.  
  1689.   # Check the template for errors that would easily lead to confusing or
  1690.   # misleading error reports.
  1691.  
  1692.   for BadArgName in ['-', '--', '']:
  1693.     if BadArgName in OptsTemplate:
  1694.       raise Error('INTERNAL ERROR: Bad name "' + str(BadArgName) +
  1695.           '" in options template')
  1696.  
  1697.   # NameMap allows the canonical command line option name to be found given
  1698.   # the option name as it appears on the command line.
  1699.  
  1700.   NameMap = {}
  1701.  
  1702.   for CName in OptsTemplate:
  1703.     Aliases = OptsTemplate[CName][0]
  1704.     NameMap[CName] = CName
  1705.     for Alias in Aliases:
  1706.       NameMap[Alias] = CName
  1707.  
  1708.   Ix = 1
  1709.   NextIx = 0
  1710.   Options = []
  1711.  
  1712.   while Ix < len(Args):
  1713.  
  1714.     NextIx = Ix + 1
  1715.  
  1716.     Name = Args[Ix]
  1717.     Value = None
  1718.  
  1719.     x1 = Name.index('=') if '=' in Name else len(Name)
  1720.     x2 = Name.index(':') if ':' in Name else len(Name)
  1721.     x = min(x1, x2)
  1722.     ValueInArg = x < len(Name)
  1723.  
  1724.     if ValueInArg:
  1725.  
  1726.       if 0 < x < len(Name) - 1:
  1727.         Value = Name[x + 1:]
  1728.         Name = Name[:x]
  1729.       else:
  1730.         raise OptError('Syntax error in option: "' + str(Name) + '"')
  1731.  
  1732.     if Name in NameMap:
  1733.  
  1734.       CName = NameMap[Name]
  1735.       Aliases, DataType, MinStr, MaxStr = OptsTemplate[CName]
  1736.  
  1737.       if DataType is not None:
  1738.  
  1739.         if Value is None:
  1740.           if NextIx < len(Args):
  1741.             Value = Args[NextIx]
  1742.             NextIx += 1
  1743.  
  1744.         if not hasattr(DataType, '__name__'):
  1745.           raise Error('INTERNAL ERROR: Datatype "' + TypeStr + '" has no name.')
  1746.  
  1747.         TypeStr = DataType.__name__
  1748.  
  1749.         if TypeStr in OptDataTypes:
  1750.           HRTypeStr = OptDataTypes[TypeStr][1]
  1751.           Value = ParseValue(Name, Value, DataType, HRTypeStr, MinStr, MaxStr)
  1752.         else:
  1753.           raise Error('INTERNAL ERROR: Unhandled datatype "' + TypeStr + '".')
  1754.  
  1755.         Options.append((CName, Name, Value))
  1756.  
  1757.       else:
  1758.  
  1759.         Value = ParseValue(Name, Value, None, None, None, None)
  1760.         Options.append((CName, Name, Value))
  1761.  
  1762.     else:
  1763.  
  1764.       if Name == '-':
  1765.         # Stop processing options and process command operands, starting
  1766.         # with this one, which stands for standard input or standard output.
  1767.         NextIx = Ix
  1768.         break
  1769.       elif Name == '--':
  1770.         # Stop processing options and process command operands.
  1771.         Ix = NextIx
  1772.         break
  1773.       else:
  1774.         if len(Name) > 0 and Name[0] == '-':
  1775.           raise OptError('Unrecognised option "' + str(Name) + '".')
  1776.         else:
  1777.           # Not an option but an operand instead.
  1778.           NextIx = Ix
  1779.           break
  1780.  
  1781.     Ix = NextIx
  1782.  
  1783.   Operands = Args[Ix:]
  1784.  
  1785.   return (Options, Operands)
  1786.  
  1787.  
  1788. #-------------------------------------------------------------------------------
  1789.  
  1790.  
  1791. def ParamsFromCLArgs(Args):
  1792.  
  1793.   u'''Parse the command line arguments to produce sanitised program parameters.
  1794.  
  1795.  The parameters are returned in a Python dictionary
  1796.  
  1797.    DoExecute: True if the normal operation of the program is to continue
  1798.    DoHelp: True if the command lien help text should be displayed
  1799.    CrownCircumference: Circumference of the part which rests on the head
  1800.    CrownAspect: Width of the widest part of the crown divided by the length
  1801.    ForeheadRatio: Width of forehead ellipse as a ratio of the rear width
  1802.    FrontAspect: Elliptical ratio for the forehead curve
  1803.    RearAspect: Elliptical ratio for the rear curve
  1804.    DomeAspect: Height of the dome as a ratio of the crown’s equivalent radius
  1805.    NumSlices: Number of pairs of radial spars over which material is lofted
  1806.    ViewsMode: 0 = polygon, 1 = smooth, 2 = combined
  1807.    IsLandscape: True if the image width is to be greater than the image height
  1808.    ViewsFileName: Name of the orthogonal views SVG file to write
  1809.    OutputPath: Where the slices and ring-and-fences SVGs are to be written
  1810.    PaperSize: Name of paper size or W×H in millimetres
  1811.    Margin: Space in mm between an image edge and the corresponding paper edge
  1812.    Padding: Space in mm between an image edge and the inked area
  1813.    PageNumbers: List of page numbers (and runs) of helmet former SVGs
  1814.    Verbosity: 0 = quiet, 1 = normal, 2 = verbose
  1815.  
  1816.  If the command line arguments cannot be parsed properly or failed to pass a
  1817.  range check, OpeError is raised.
  1818.  
  1819.  '''
  1820.  
  1821.   Result = {
  1822.     'DoExecute': True,
  1823.     'DoHelp': False,
  1824.     'CrownCircumference': 59.0,
  1825.     'CrownAspect': 0.71,
  1826.     'ForeheadRatio': 0.69,
  1827.     'FrontAspect': 0.65,
  1828.     'RearAspect': 0.75,
  1829.     'DomeAspect': 1.1,
  1830.     'NumSlices': 9,
  1831.     'ViewsMode': 0,
  1832.     'IsLandscape': False,
  1833.     'ViewsFileName': '',
  1834.     'OutputPath': '',
  1835.     'PaperSize': 'A4',
  1836.     'Margin': None,
  1837.     'Padding': 0,
  1838.     'PageNumbers': None,
  1839.     'Verbosity': 1
  1840.   }
  1841.  
  1842.   OptsTemplate = {
  1843.     '--size': (('-s',), float, '0.1', '500'),
  1844.     '--aspect': (('-a',), float, '0.2', '1.0'),
  1845.     '--elliptical': (('-e',), None, None, None),
  1846.     '--forehead-ratio': (('-b', '--fr'), float, '0.3', '1.0'),
  1847.     '--front-aspect': (('-f', '--fa'), float, '0', '5.0'),
  1848.     '--rear-aspect': (('-r', '--ra'), float, '0', '5.0'),
  1849.     '--dome-aspect': (('-d', '--da'), float, '0.1', '5.0'),
  1850.     '--num-slices': (('-n',), int, '2', '50'),
  1851.     '--views-mode': ((), int, '0', '2'),
  1852.     '--views-svg': (('-g',), str, None, None),
  1853.     '--output': (('-o',), str, None, None),
  1854.     '--pages': (('-p',), str, None, None),
  1855.     '--paper-size': (('-m', '--ps'), str, None, None),
  1856.     '--margin': ((), float, '0', None),
  1857.     '--padding': ((), float, '0', None),
  1858.     '--portrait': ((), None, None, None),
  1859.     '--landscape': ((), None, None, None),
  1860.     '--verbose': (('-v',), None, None, None),
  1861.     '--quiet': (('-q',), None, None, None),
  1862.     '--help': (('-h',), None, None, None)
  1863.   }
  1864.  
  1865.   OptDataTypes = {
  1866.     'int': (int, 'integer'),
  1867.     'float': (float, 'floating point number'),
  1868.     'str': (str, 'string')
  1869.   }
  1870.  
  1871.   Options, Operands = ParseCLOptions(Args, OptsTemplate, OptDataTypes)
  1872.  
  1873.   PageNumbersOptionName = None
  1874.   PageNumbersStr = None
  1875.  
  1876.   IsElliptical = False
  1877.   IsNotElliptical = False
  1878.   NonEllipticalOptions = [
  1879.     '--forehead-ratio',
  1880.     '--front-aspect',
  1881.     '--rear-aspect'
  1882.   ]
  1883.  
  1884.   IsQuiet = False
  1885.   IsVerbose = False
  1886.   DoHelp = False
  1887.  
  1888.   for OptRec in Options:
  1889.     if OptRec[0] == '--help':
  1890.       DoHelp = True
  1891.       break
  1892.  
  1893.   if DoHelp:
  1894.  
  1895.     Result['DoHelp'] = True
  1896.     Result['DoExecute'] = False
  1897.  
  1898.   elif len(Operands) > 0:
  1899.  
  1900.     raise OptError('Too many operands (' + str(len(Operands)) + ') were given. ' +
  1901.         'Perhaps an option introducer "-" was omitted.')
  1902.  
  1903.   else:
  1904.  
  1905.     for OptRec in Options:
  1906.  
  1907.       Option, SuppliedOptionName, Value = OptRec
  1908.  
  1909.       if Option in NonEllipticalOptions:
  1910.         if IsElliptical:
  1911.           raise OptError('Option ' + str(SuppliedOptionName) +
  1912.               ' cannot be used with the -e or --elliptical option.')
  1913.         IsNotElliptical = True
  1914.  
  1915.       if Option == '--elliptical':
  1916.         if IsNotElliptical:
  1917.           raise OptError('Option ' + str(SuppliedOptionName) +
  1918.               ' cannot be used with any of the non-elliptical options.')
  1919.         IsElliptical = True
  1920.  
  1921.       if Option == '--size':
  1922.         Result['CrownCircumference'] = Value
  1923.       elif Option == '--aspect':
  1924.         Result['CrownAspect'] = Value
  1925.       elif Option == '--forehead-ratio':
  1926.         Result['ForeheadRatio'] = Value
  1927.       elif Option == '--front-aspect':
  1928.         Result['FrontAspect'] = Value
  1929.       elif Option == '--rear-aspect':
  1930.         Result['RearAspect'] = Value
  1931.       elif Option == '--dome-aspect':
  1932.         Result['DomeAspect'] = Value
  1933.       elif Option == '--num-slices':
  1934.         Result['NumSlices'] = Value
  1935.       elif Option == '--views-mode':
  1936.         Result['ViewsMode'] = Value
  1937.       elif Option == '--views-svg':
  1938.         Result['ViewsFileName'] = Value
  1939.       elif Option == '--output':
  1940.         Result['OutputPath'] = Value
  1941.       elif Option == '--paper-size':
  1942.         try:
  1943.           PS = PaperSize(Value)
  1944.         except (Error), E:
  1945.           raise OptError('Invalid value for option ' +
  1946.               str(SuppliedOptionName) + ': ' + str(E))
  1947.         else:
  1948.           Result['PaperSize'] = Value
  1949.       elif Option == '--portrait':
  1950.         Result['IsLandscape'] = False
  1951.       elif Option == '--landscape':
  1952.         Result['IsLandscape'] = True
  1953.       elif Option == '--margin':
  1954.         Result['Margin'] = Value
  1955.       elif Option == '--padding':
  1956.         Result['Padding'] = Value
  1957.       elif Option == '--pages':
  1958.         PageNumbersOptionName = SuppliedOptionName
  1959.         PageNumbersStr = Value
  1960.       elif Option == '--verbose':
  1961.         IsVerbose = True
  1962.       elif Option == '--quiet':
  1963.         IsQuiet = True
  1964.  
  1965.     if IsElliptical:
  1966.       Result['ForeheadRatio'] = 1.0
  1967.       Result['FrontAspect'] = Result['RearAspect'] = 1.0 / Result['CrownAspect']
  1968.  
  1969.     N = Result['NumSlices']
  1970.  
  1971.     if PageNumbersStr is not None:
  1972.       try:
  1973.         Result['PageNumbers'] = ParseSequence(PageNumbersStr, 1, N + 1)
  1974.       except (OptError), E:
  1975.         raise OptError('Bad pages list for option ' +
  1976.             str(PageNumbersOptionName) + ': ' + str(E))
  1977.     else:
  1978.       Result['PageNumbers'] = list(range(1, N + 2))
  1979.  
  1980.     if Result['ViewsFileName'] == '-':
  1981.       # Write views SVG to stdout.
  1982.       IsQuiet = True
  1983.  
  1984.     if Result['OutputPath'] == '-':
  1985.       # Perversely concatenate SVGs to stdout.
  1986.       IsQuiet = True
  1987.  
  1988.     if IsQuiet:
  1989.       Verbosity = 0
  1990.     else:
  1991.       Verbosity = 2 if IsVerbose else 1
  1992.  
  1993.     Result['Verbosity'] = Verbosity
  1994.  
  1995.   return Result
  1996.  
  1997.  
  1998. #-------------------------------------------------------------------------------
  1999. # Page output functions
  2000. #-------------------------------------------------------------------------------
  2001.  
  2002.  
  2003. def PaperSize(Name):
  2004.  
  2005.   u'''Return the ISO 216 or 269 paper size as portrait (Width, Height) in mm.
  2006.  
  2007.  The ISO 216 paper sizes range from A0 to A10 and B0 to B10. The ISO 269 sizes
  2008.  range from C0 to C10.
  2009.  
  2010.  As a bonus, the Swedish SIS 014711 and the German DIN 476 extentions are
  2011.  supported, along with the US paper sizes, Letter, Legal, Ledger and Tabloid.
  2012.  
  2013.  The width and height of the paper in millimetres can be specified directly
  2014.  in the form W×H where “×” may be any of “×”, “x” or “X”.
  2015.  
  2016.  Ledger is the odd one out in that its width is greater than its height so
  2017.  its dimensions, still returned as (Width, Height) reflect its landscape
  2018.  orientation.
  2019.  
  2020.  if the name of the paper size is not known, an exception is raised.
  2021.  
  2022.  '''
  2023.  
  2024.   Result = None
  2025.  
  2026.   Inch = 25.4
  2027.  
  2028.   if Name == 'C7/6': # ISO 269
  2029.     Result = (81, 162)
  2030.   elif Name == 'DL': # ISO 269
  2031.     Result = (110, 220)
  2032.   elif Name == '4A0': # DIN 476
  2033.     Result = (1682, 2378)
  2034.   elif Name == '2A0': # DIN 476
  2035.     Result = (1189, 1682)
  2036.   elif Name == 'Letter': # US
  2037.     Result = (8.5 * Inch, 11.0 * Inch)
  2038.   elif Name == 'Legal': # US
  2039.     Result = (8.5 * Inch, 14.0 * Inch)
  2040.   elif Name in 'Ledger': # US
  2041.     Result = (17.0 * Inch, 11.0 * Inch)
  2042.   elif Name == 'Tabloid': # US
  2043.     Result = (11.0 * Inch, 17.0 * Inch)
  2044.  
  2045.   if Result is None:
  2046.     # Try the W×H format
  2047.     LCLatinX = 'x'
  2048.     UCLatinX = 'X'
  2049.     Times = '×'
  2050.     SepCh = LCLatinX if LCLatinX in Name else ''
  2051.     SepCh = UCLatinX if UCLatinX in Name else SepCh
  2052.     SepCh = Times if Times in Name else SepCh
  2053.     if SepCh != '':
  2054.       x = Name.index(SepCh)
  2055.       WidthStr = Name[:x].strip()
  2056.       HeightStr = Name[x + len(SepCh):].strip()
  2057.       try:
  2058.         Width = float(WidthStr)
  2059.         Height = float(HeightStr)
  2060.         W = int(round(Width))
  2061.         H = int(round(Height))
  2062.         if W == Width and H == Height:
  2063.           Width, Height = W, H
  2064.       except (ValueError):
  2065.         pass
  2066.       else:
  2067.         Result = (Width, Height)
  2068.  
  2069.   if Result is None:
  2070.     # A and B are ISO 216, C is ISO 269, D, E, F and G are SIS 014711 and
  2071.     # H is a logical extension of SIS 014711.
  2072.     if len(Name) >= 2:
  2073.       Series = Name[0]
  2074.       PSS = 'AECGBFDH'
  2075.       try:
  2076.         n = int(Name[1:])
  2077.       except (ValueError):
  2078.         n = -1
  2079.       else:
  2080.         if not (0 <= n <= 10):
  2081.           n = -1
  2082.       if Series in PSS and n >= 0:
  2083.         x0 = 1000.0 * 2.0 ** (PSS.index(Series) / 16.0)
  2084.         x = x0 / (2.0 ** (0.25 * (2 * n - 1)))
  2085.         Height = int(floor(x + 0.2))
  2086.         Width = int(floor(x / sqrt(2.0) + 0.2))
  2087.         Result = (Width, Height)
  2088.  
  2089.   if Result is None:
  2090.     raise Error('Unrecognised paper size: "' + str(Name) + '"')
  2091.  
  2092.   return Result
  2093.  
  2094.  
  2095. #-------------------------------------------------------------------------------
  2096.  
  2097.  
  2098. def HatViewsSVG(HatSliceMetrics, Mode=None, ImageDim=None, Padding=None):
  2099.  
  2100.   u'''Generate SVG markup for views of a pith helmet.
  2101.  
  2102.  Four wire-frame views are rendered: One isometric view (viewed from
  2103.  front-left-up) and three orthographic views. These views should be
  2104.  enough to determine if a particular helmet shape is acceptable.
  2105.  
  2106.  Mode:
  2107.    0: Polygon model: Scaled to preserve the crown circumference (default)
  2108.    1: Ideal: Smooth crown and brim curves
  2109.    2: Polygon and ideal shapes superimposed
  2110.  
  2111.  ImageDim, if supplied, indicates the physical size of the image in
  2112.  millimetres as a (Width, Height) pair, usually the printable area for
  2113.  some particular paper size. By default the area is 287mm × 200mm, (A4
  2114.  in lanscape mode short by 5mm from each edge).
  2115.  
  2116.  Padding is the internal margin in millimetres from each edge of the image.
  2117.  By default, the padding is zero.
  2118.  
  2119.  '''
  2120.  
  2121.   #-----------------------------------------------------------------------------
  2122.  
  2123.   # Shorthand
  2124.  
  2125.   A = Pt_Anchor
  2126.   C = Pt_Control
  2127.   B = (Pt_Break, None)
  2128.  
  2129.   PathColourStr = '#0bf'
  2130.   PolyColourStr = '#e00'
  2131.  
  2132.   #-----------------------------------------------------------------------------
  2133.  
  2134.   def VStr(Vector, Scale=1.0):
  2135.  
  2136.     u'''Return a vector string, limited to two decimal places.'''
  2137.  
  2138.     return ', '.join((MaxDP(Scale * x, 2) for x in Vector))
  2139.  
  2140.   #-----------------------------------------------------------------------------
  2141.  
  2142.   def TxPath(IT, AM, Shape, Attributes=None):
  2143.  
  2144.     u'''Return an SVG path for a transformed Shape.'''
  2145.  
  2146.     return SVGPath(IT, ShapeDim(TransformedShape(AM, Shape), 2), Attributes)
  2147.  
  2148.   #-----------------------------------------------------------------------------
  2149.  
  2150.   def HatMarkup(IT, AM, HatSliceMetrics, Mode):
  2151.  
  2152.     u'''Return SVG markup for a helmet subject to a transformation.
  2153.  
  2154.    Mode:
  2155.      0: Polygon model: Scaled to preserve the crown circumference.
  2156.      1: Ideal: Smooth crown and brim curves.
  2157.      2: Polygon and ideal shapes superimposed.
  2158.  
  2159.    '''
  2160.  
  2161.     HSM = HatSliceMetrics
  2162.     HM = HSM.HatMetrics
  2163.     CM = HM.CrownMetrics
  2164.  
  2165.     if Mode == 2:
  2166.       PathCS = PathColourStr
  2167.       PolyCS = PolyColourStr
  2168.     else:
  2169.       PathCS = 'black'
  2170.       PolyCS = 'black'
  2171.  
  2172.     Result = ''
  2173.  
  2174.     if Mode in [1, 2]:
  2175.  
  2176.       Result += IT('<!-- Ideal helmet shape -->')
  2177.       Result += IT('<!-- Crown -->')
  2178.       Result += TxPath(IT, PlotAM, CM.CrownPath, {'stroke': PathCS})
  2179.       Result += IT('<!-- Brim -->')
  2180.       Result += TxPath(IT, PlotAM, HM.BrimPath, {'stroke': PathCS})
  2181.  
  2182.       for i in range(2 * HSM.NumSlices):
  2183.         Result += IT('<!-- Radial at ' + HSM.RadialNames[i] + ' -->')
  2184.         S = HM.RadialSlice3D(HSM.Angles[i])
  2185.         Result += TxPath(IT, PlotAM, S, {'stroke': PathCS})
  2186.  
  2187.     if Mode != 1:
  2188.  
  2189.       Result += IT('<!-- Helmet shape with polygon scale correction -->')
  2190.       Result += IT('<!-- Crown -->')
  2191.       S = ShapeFromVertices(HSM.Radials + HSM.Radials[:1], 1)
  2192.       Result += TxPath(IT, PlotAM, S, {'stroke': PolyCS})
  2193.       Result += IT('<!-- Brim -->')
  2194.       S = ShapeFromVertices(HSM.RadialBrimPoints + HSM.RadialBrimPoints[:1], 1)
  2195.       Result += TxPath(IT, PlotAM, S, {'stroke': PolyCS})
  2196.  
  2197.       for i in range(2 * HSM.NumSlices):
  2198.         Result += IT('<!-- Radial at ' + HSM.RadialNames[i] + ' -->')
  2199.         S = HSM.RadialSlice3D(i)
  2200.         Result += TxPath(IT, PlotAM, S, {'stroke': PolyCS})
  2201.  
  2202.  
  2203.     return Result
  2204.  
  2205.   #-----------------------------------------------------------------------------
  2206.  
  2207.   Title = 'Pith Helmet Views'
  2208.  
  2209.   HSM = HatSliceMetrics
  2210.   HM = HSM.HatMetrics
  2211.   CM = HM.CrownMetrics
  2212.  
  2213.   if ImageDim is None:
  2214.     ID = (28.7, 20.0)
  2215.   else:
  2216.     ID = VScaled(ImageDim, 0.1)
  2217.  
  2218.   PrintWidthStrMM = MaxDP(10.0 * ID[0], 3)
  2219.   PrintHeightStrMM = MaxDP(10.0 * ID[1], 3)
  2220.   PrintWidthStr = MaxDP(ID[0], 4)
  2221.   PrintHeightStr = MaxDP(ID[1], 4)
  2222.  
  2223.   IsPortraitAspect = ID[0] < ID[1]
  2224.   ID = (float(PrintWidthStr), float(PrintHeightStr))
  2225.  
  2226.   if IsPortraitAspect:
  2227.     ID = (ID[1], ID[0])
  2228.  
  2229.   GD = VSum(ID, VScaled(VOnes(2), -0.2 * Padding))
  2230.  
  2231.   Scale = min(2.0, min(GD[0], GD[1]) / 20.0)
  2232.   BaseY = HSM.Bottom + HSM.BaseHeight
  2233.  
  2234.   QHeight = 0.5 * (GD[1] - Scale * 1.0)
  2235.   QWidth = QHeight * GD[0] / GD[1]
  2236.   QScale = QHeight / GD[1]
  2237.  
  2238.   FQExtent = (QScale * HM.RadialSlice2D(0.0)[-1][1][0]) / QWidth
  2239.   RQExtent = (QScale * HM.RadialSlice2D(pi)[-1][1][0]) / QWidth
  2240.  
  2241.   QGroupCentre = (0.5 * GD[0], GD[1] - QHeight)
  2242.   QCentres = []
  2243.  
  2244.   QOffsets = []
  2245.  
  2246.   for y in (-0.5, (GD[1] - 0.2 * Scale - BaseY) / GD[1]):
  2247.     for x in (-0.5, 0.5 + 0.5 * (FQExtent - RQExtent)):
  2248.       QOffsets.append((x, y))
  2249.  
  2250.   QOffsets[0] = (-0.5, -0.45)
  2251.  
  2252.   for QOffset in QOffsets:
  2253.     QCentres.append(
  2254.       VSum(QGroupCentre, VSchur(QOffset, (QWidth, QHeight)))
  2255.     )
  2256.  
  2257.   IT = tIndentTracker('  ')
  2258.  
  2259.   Result = SVGStart(IT, Title, {
  2260.     'width': PrintWidthStrMM + 'mm',
  2261.     'height': PrintHeightStrMM + 'mm',
  2262.     'viewBox': '0 0 ' + PrintWidthStr + ' ' + PrintHeightStr,
  2263.     'preserveAspectRatio': 'xMidYMid slice'
  2264.   })
  2265.  
  2266.   # Background
  2267.  
  2268.   Result += IT(
  2269.     '<!-- Background -->',
  2270.     '<rect x="0" y="0" width="' +
  2271.         PrintWidthStr +'" height="' + PrintHeightStr +
  2272.         '" stroke="none" fill="white"/>'
  2273.   )
  2274.  
  2275.   # Outer group
  2276.  
  2277.   OuterGroupAttrs = {
  2278.     'fill': 'none', 'stroke': 'black', 'stroke-width': '0.05'
  2279.   }
  2280.  
  2281.   OGXforms = []
  2282.  
  2283.   if IsPortraitAspect:
  2284.     OGXforms.append('translate(' + MaxDP(ID[1], 4) + ', 0) rotate(90)')
  2285.  
  2286.   if Padding != 0:
  2287.     PaddingStr = MaxDP(0.1 * Padding, 4)
  2288.     OGXforms.append('translate(' + PaddingStr + ', ' + PaddingStr + ')')
  2289.  
  2290.   if len(OGXforms) > 0:
  2291.     OuterGroupAttrs['transform'] = ' '.join(OGXforms)
  2292.  
  2293.   Result += IT('<!-- Outer group -->')
  2294.   Result += SVGGroup(IT, OuterGroupAttrs)
  2295.  
  2296.   # Title and metrics
  2297.  
  2298.   TBlock = ''
  2299.  
  2300.   TBlock += IT('<!-- Title group -->')
  2301.   TBlock += SVGGroup(IT, {
  2302.     'fill': 'black',
  2303.     'stroke': 'none',
  2304.     'font-family': 'sans-serif',
  2305.     'font-size': MaxDP(0.36 * Scale, 2)
  2306.   })
  2307.  
  2308.   TBlock += IT('<!-- Title -->')
  2309.   TBlock += SVGText(IT,
  2310.     VScaled((0.2, 0.75), Scale),
  2311.     Title,
  2312.     {'font-size': MaxDP(0.72 * Scale, 2), 'font-weight': 'bold'}
  2313.   )
  2314.  
  2315.   TBlock += IT('<!-- Metrics -->')
  2316.   TBlock += (
  2317.     SVGText(IT, VScaled((0.2, 1.5), Scale),
  2318.         'Crown Circumference: ' + MaxDP(CM.Circumference, 1) + 'cm') +
  2319.     SVGText(IT, VScaled((0.2, 2.0), Scale),
  2320.         'Crown Aspect Ratio: ' + MaxDP(CM.CrownAspect, 2)) +
  2321.     SVGText(IT, VScaled((0.2, 2.5), Scale),
  2322.         'Forehead Ratio: ' + MaxDP(CM.ForeheadRatio, 2)) +
  2323.     SVGText(IT, VScaled((0.2, 3.0), Scale),
  2324.         'Front Aspect: ' + MaxDP(CM.FrontAspect, 2)) +
  2325.     SVGText(IT, VScaled((0.2, 3.5), Scale),
  2326.         'Rear Aspect: ' + MaxDP(CM.RearAspect, 2)) +
  2327.     SVGText(IT, VScaled((0.2, 4.0), Scale),
  2328.         'Dome Aspect: ' + MaxDP(HM.DomeAspect, 2))
  2329.   )
  2330.  
  2331.   TBlock += SVGGroupEnd(IT)
  2332.   Result += TBlock
  2333.  
  2334.   # Quadrant dividers
  2335.  
  2336.   x, y = QGroupCentre
  2337.  
  2338.   Result += IT('<!-- Quadrant dividers -->')
  2339.   Result += SVGPath(IT, [
  2340.     (A, (x - QWidth, y)), (A, (x + QWidth, y)), B,
  2341.     (A, (x, y - QHeight)), (A, (x, y + QHeight))
  2342.   ])
  2343.  
  2344.   # Upper-left quadrant: Isometric view
  2345.  
  2346.   QBlock = ''
  2347.  
  2348.   QBlock += IT('<!-- Upper-left quadrant: Isometric view -->')
  2349.   QBlock += SVGGroup(IT, {
  2350.     'transform': 'translate(' + VStr(QCentres[0]) +') ' +
  2351.         'scale(' + MaxDP(0.9 * QScale, 4) + ')'
  2352.   })
  2353.  
  2354.   PlotAM = tAffineMtx(
  2355.     (0.0, 0.0, 0.0),
  2356.     (
  2357.       (-0.5 * sqrt(2.0), sqrt(1.0 / 6.0), -sqrt(3.0) / 3.0),
  2358.       (-0.5 * sqrt(2.0), -sqrt(1.0 / 6.0), sqrt(3.0) / 3.0),
  2359.       (0.0, sqrt(2.0 / 3.0), sqrt(3.0) / 3.0)
  2360.     )
  2361.   )
  2362.  
  2363.   QBlock += HatMarkup(IT, PlotAM, HSM, Mode)
  2364.  
  2365.   QBlock += SVGGroupEnd(IT)
  2366.  
  2367.   Result += QBlock
  2368.  
  2369.   # Upper-right quadrant: Top view
  2370.  
  2371.   QBlock = ''
  2372.  
  2373.   QBlock += IT('<!-- Upper-right quadrant: Top view -->')
  2374.   QBlock += SVGGroup(IT, {
  2375.     'transform': 'translate(' + VStr(QCentres[1]) +') ' +
  2376.         'scale(' + MaxDP(QScale, 4) + ')'
  2377.   })
  2378.  
  2379.   PlotAM = tAffineMtx(
  2380.     (0.0, 0.0, 0.0),
  2381.     ((-1.0, 0.0, 0.0), (0.0, -1.0, 0.0), (0.0, 0.0, 1.0))
  2382.   )
  2383.  
  2384.   if False:
  2385.     QBlock += IT('<!-- Centre cross -->')
  2386.     QBlock += TxPath(IT, PlotAM,
  2387.       [(A, (12, 0)), (A, (-12, 0)), B, (A, (0, -8)), (A, (0, 8)), B],
  2388.       {'stroke': 'rgba(255,0,0,0.7)'}
  2389.     )
  2390.  
  2391.   S = ShapeFromVertices(HSM.RingCutPoints + HSM.RingCutPoints[:1], 1)
  2392.  
  2393.   QBlock += IT('<!-- Bracing ring -->')
  2394.   QBlock += TxPath(IT,
  2395.     PlotAM, S, {'stroke-width': '0.025', 'stroke-dasharray': '0.1 0.3'}
  2396.   )
  2397.  
  2398.   QBlock += HatMarkup(IT, PlotAM, HSM, Mode)
  2399.  
  2400.   QBlock += SVGGroupEnd(IT)
  2401.  
  2402.   Result += QBlock
  2403.  
  2404.   # Lower-left quadrant: Front view
  2405.  
  2406.   QBlock = ''
  2407.  
  2408.   QBlock += IT('<!-- Lower-left quadrant: Front view -->')
  2409.   QBlock += SVGGroup(IT, {
  2410.     'transform': 'translate(' + VStr(QCentres[2]) +') ' +
  2411.         'scale(' + MaxDP(QScale, 4) + ')'
  2412.   })
  2413.  
  2414.   PlotAM = tAffineMtx(
  2415.     (0.0, 0.0, 0.0),
  2416.     ((0.0, 0.0, -1.0), (-1.0, 0.0, 0.0), (0.0, 1.0, 0.0))
  2417.   )
  2418.  
  2419.   QBlock += HatMarkup(IT, PlotAM, HSM, Mode)
  2420.  
  2421.   QBlock += SVGGroupEnd(IT)
  2422.  
  2423.   Result += QBlock
  2424.  
  2425.   # Lower-right quadrant: Left side view
  2426.  
  2427.   QBlock = ''
  2428.  
  2429.   QBlock += IT('<!-- Lower-right quadrant: Left side view -->')
  2430.   QBlock += SVGGroup(IT, {
  2431.     'transform': 'translate(' + VStr(QCentres[3]) +') ' +
  2432.         'scale(' + MaxDP(QScale, 4) + ')'
  2433.   })
  2434.  
  2435.   PlotAM = tAffineMtx(
  2436.     (0.0, 0.0, 0.0),
  2437.     ((-1.0, 0.0, 0.0), (0.0, 0.0, 1.0), (0.0, 1.0, 0.0))
  2438.   )
  2439.  
  2440.   QBlock += HatMarkup(IT, PlotAM, HSM, Mode)
  2441.  
  2442.   QBlock += SVGGroupEnd(IT)
  2443.  
  2444.   Result += QBlock
  2445.  
  2446.   # End of outer group and SVG
  2447.  
  2448.   Result += SVGGroupEnd(IT) + SVGEnd(IT)
  2449.  
  2450.   return Result
  2451.  
  2452.  
  2453. #-------------------------------------------------------------------------------
  2454.  
  2455.  
  2456. def HatSliceSVG(HatSliceMetrics, PageNumber, ImageDim=None, Padding=None):
  2457.  
  2458.   u'''Generate SVG markup for one slice of a former for a pith helmet.
  2459.  
  2460.  Page number ranges from 1 to the number of slices. When all the slices
  2461.  are printed, cut out, folded, assembled and braced with fences at the
  2462.  top and bottom hubs and a ring at the bottom, the former is complete.
  2463.  
  2464.  The helmet is made by lofting material over the former.
  2465.  
  2466.  ImageDim, if supplied, indicates the physical size of the image in
  2467.  millimetres as a (Width, Height) pair, usually the printable area for
  2468.  some particular paper size. By default the area is 287mm × 200mm, (A4
  2469.  in lanscape mode short by 5mm from each edge).
  2470.  
  2471.  Padding is the internal margin in millimetres from each edge of the image.
  2472.  By default, the padding is zero.
  2473.  
  2474.  '''
  2475.  
  2476.   #-----------------------------------------------------------------------------
  2477.  
  2478.   # Shorthand
  2479.  
  2480.   A = Pt_Anchor
  2481.   C = Pt_Control
  2482.   B = (Pt_Break, None)
  2483.  
  2484.   ftFlat = 0
  2485.   ftMountain = 2
  2486.   ftValley = 1
  2487.  
  2488.   #-----------------------------------------------------------------------------
  2489.  
  2490.   def TxPath(IT, AM, Shape, Attributes=None):
  2491.  
  2492.     u'''Return an SVG path for a transformed Shape.'''
  2493.  
  2494.     return SVGPath(IT, ShapeDim(TransformedShape(AM, Shape), 2), Attributes)
  2495.  
  2496.   #-----------------------------------------------------------------------------
  2497.  
  2498.   if PageNumber < 1 or PageNumber > HatSliceMetrics.NumSlices:
  2499.     raise Error('Invalid page number %s.' % (str(PageNumber)))
  2500.  
  2501.   HSM = HatSliceMetrics
  2502.   HM = HSM.HatMetrics
  2503.   CM = HM.CrownMetrics
  2504.  
  2505.   Title = 'Pith Helmet Slice %d/%d' % (PageNumber, HSM.NumSlices)
  2506.  
  2507.   if ImageDim is None:
  2508.     ID = (28.7, 20.0)
  2509.   else:
  2510.     ID = VScaled(ImageDim, 0.1)
  2511.  
  2512.   PrintWidthStrMM = MaxDP(10.0 * ID[0], 3)
  2513.   PrintHeightStrMM = MaxDP(10.0 * ID[1], 3)
  2514.   PrintWidthStr = MaxDP(ID[0], 4)
  2515.   PrintHeightStr = MaxDP(ID[1], 4)
  2516.  
  2517.   IsPortraitAspect = ID[0] < ID[1]
  2518.   ID = (float(PrintWidthStr), float(PrintHeightStr))
  2519.  
  2520.   if IsPortraitAspect:
  2521.     ID = (ID[1], ID[0])
  2522.  
  2523.   GD = VSum(ID, VScaled(VOnes(2), -0.2 * Padding))
  2524.  
  2525.   Scale = min(2.0, min(GD[0], GD[1]) / 20.0)
  2526.  
  2527.   SliceIndex = PageNumber - 1
  2528.   IsSaggital = SliceIndex < len(HSM.Saggitals)
  2529.  
  2530.   if IsSaggital:
  2531.     SliceSetName = 'Saggital' if IsSaggital else 'Coronal'
  2532.     SliceIxInSet = SliceIndex
  2533.     SliceSetSize = len(HSM.Saggitals)
  2534.   else:
  2535.     SliceSetName = 'Coronal'
  2536.     SliceIxInSet = SliceIndex - len(HSM.Saggitals)
  2537.     SliceSetSize = len(HSM.Coronals)
  2538.  
  2539.   RIx1, RIx2 = (HSM.Saggitals + HSM.Coronals)[SliceIndex]
  2540.  
  2541.   AM = AffineMtxTS((0.0, 0.0), (-1.0, 1.0))
  2542.   R1 = TransformedShape(AM, HSM.RadialSlice2D(RIx1))
  2543.   R2 = HSM.RadialSlice2D(RIx2)
  2544.  
  2545.   x1 = R1[-1][1][0]
  2546.   x2 = R2[-1][1][0]
  2547.   Span = x2 - x1
  2548.   BaseY = HSM.Bottom + HSM.BaseHeight
  2549.   MiddleY = HSM.Top + 0.5 * (BaseY - HSM.Top)
  2550.   MiddleCutY = HSM.Top + 0.3 * (BaseY - HSM.Top)
  2551.  
  2552.   AxisOffset = -0.5 * Span - x1
  2553.  
  2554.   Origin = (0.5 * GD[0] + AxisOffset, GD[1] - 0.2 * Scale - BaseY)
  2555.   OffsetStr = MaxDP(Origin[0], 2) + ', ' + MaxDP(Origin[1], 2)
  2556.  
  2557.   CrownX1 = R1[-7][1][0]
  2558.   CrownX2 = R2[-7][1][0]
  2559.   RingX1 = -VLength(HSM.RingCutPoints[RIx1])
  2560.   RingX2 = VLength(HSM.RingCutPoints[RIx2])
  2561.  
  2562.   NR1 = VNormalised(HSM.Radials[RIx1])
  2563.   NR2 = VNormalised(HSM.Radials[RIx2])
  2564.   FoldAngle = pi - acos(max(-1.0, min(1.0, VDot(NR1, NR2))))
  2565.   FoldType = ftValley if VDot(VPerp(NR2), NR1) > 0 else ftMountain
  2566.   FoldAngleStr = MaxDP(degrees(FoldAngle), 0)
  2567.  
  2568.   if FoldAngleStr == '0':
  2569.     FoldStr = ''
  2570.     FoldType = ftFlat
  2571.   else:
  2572.     FoldStr = 'Valley' if FoldType == ftValley else 'Mountain'
  2573.     FoldStr += ' Fold by ' + FoldAngleStr + u'°'
  2574.  
  2575.   SliceOutline = list(reversed(R1))[:-1] + R2 + [
  2576.     (A, (x2, BaseY)), (A, (x1, BaseY)), R1[-1]
  2577.   ]
  2578.  
  2579.   IT = tIndentTracker('  ')
  2580.  
  2581.   Result = SVGStart(IT, Title, {
  2582.     'width': PrintWidthStrMM + 'mm',
  2583.     'height': PrintHeightStrMM + 'mm',
  2584.     'viewBox': '0 0 ' + PrintWidthStr + ' ' + PrintHeightStr,
  2585.     'preserveAspectRatio': 'xMidYMid slice'
  2586.   })
  2587.  
  2588.   Result += IT('<defs>')
  2589.   IT.StepIn()
  2590.   Result += IT(
  2591.     '<marker id="ArrowHead"',
  2592.     '    viewBox="0 0 10 10" refX="0" refY="5"',
  2593.     '    markerUnits="strokeWidth"',
  2594.     '    markerWidth="8" markerHeight="6"',
  2595.     '    orient="auto">',
  2596.     '  <path d="M 0,0  L 10,5  L 0,10  z"/>',
  2597.     '</marker>'
  2598.   )
  2599.   # More marker, symbol and gradient definitions can go here.
  2600.   IT.StepOut()
  2601.   Result += IT('</defs>')
  2602.  
  2603.   # Background
  2604.  
  2605.   Result += IT(
  2606.     '<!-- Background -->',
  2607.     '<rect x="0" y="0" width="' +
  2608.         PrintWidthStr +'" height="' + PrintHeightStr +
  2609.         '" stroke="none" fill="white"/>'
  2610.   )
  2611.  
  2612.   # Outer group
  2613.  
  2614.   OuterGroupAttrs = {
  2615.     'fill': 'none', 'stroke': 'black', 'stroke-width': '0.05'
  2616.   }
  2617.  
  2618.   OGXforms = []
  2619.  
  2620.   if IsPortraitAspect:
  2621.     OGXforms.append('translate(' + MaxDP(ID[1], 4) + ', 0) rotate(90)')
  2622.  
  2623.   if Padding != 0:
  2624.     PaddingStr = MaxDP(0.1 * Padding, 4)
  2625.     OGXforms.append('translate(' + PaddingStr + ', ' + PaddingStr + ')')
  2626.  
  2627.   if len(OGXforms) > 0:
  2628.     OuterGroupAttrs['transform'] = ' '.join(OGXforms)
  2629.  
  2630.   Result += IT('<!-- Outer group -->')
  2631.   Result += SVGGroup(IT, OuterGroupAttrs)
  2632.  
  2633.   # Border
  2634.  
  2635.   if False:
  2636.  
  2637.     Result += IT('<!-- Page border -->')
  2638.     Result += SVGPath(IT, [
  2639.       (Pt_Anchor, (0, 0)), (Pt_Anchor, (28, 0)),
  2640.       (Pt_Anchor, (28, 19)), (Pt_Anchor, (0, 19)),
  2641.       (Pt_Anchor, (0, 0))
  2642.     ])
  2643.  
  2644.   # Title and metrics
  2645.  
  2646.   TBlock = ''
  2647.  
  2648.   TBlock += IT('<!-- Title group -->')
  2649.   TBlock += SVGGroup(IT, {
  2650.     'fill': 'black',
  2651.     'stroke': 'none',
  2652.     'font-family': 'sans-serif',
  2653.     'font-size': MaxDP(0.36 * Scale, 2)
  2654.   })
  2655.  
  2656.   TBlock += IT('<!-- Title -->')
  2657.   TBlock += SVGText(IT,
  2658.     VScaled((0.2, 0.75), Scale),
  2659.     Title,
  2660.     {'font-size': MaxDP(0.72 * Scale, 2), 'font-weight': 'bold'}
  2661.   )
  2662.  
  2663.   TBlock += IT('<!-- Metrics -->')
  2664.   TBlock += (
  2665.     SVGText(IT, VScaled((0.2, 1.5), Scale),
  2666.         'Crown Circumference: ' + MaxDP(CM.Circumference, 1) + 'cm') +
  2667.     SVGText(IT, VScaled((0.2, 2.0), Scale),
  2668.         'Crown Aspect Ratio: ' + MaxDP(CM.CrownAspect, 2)) +
  2669.     SVGText(IT, VScaled((0.2, 2.5), Scale),
  2670.         'Forehead Ratio: ' + MaxDP(CM.ForeheadRatio, 2)) +
  2671.     SVGText(IT, VScaled((0.2, 3.0), Scale),
  2672.         'Front Aspect: ' + MaxDP(CM.FrontAspect, 2)) +
  2673.     SVGText(IT, VScaled((0.2, 3.5), Scale),
  2674.         'Rear Aspect: ' + MaxDP(CM.RearAspect, 2)) +
  2675.     SVGText(IT, VScaled((0.2, 4.0), Scale),
  2676.         'Dome Aspect: ' + MaxDP(HM.DomeAspect, 2))
  2677.   )
  2678.  
  2679.   # End of title group
  2680.  
  2681.   TBlock += SVGGroupEnd(IT)
  2682.  
  2683.   # Slice group
  2684.  
  2685.   SBlock = ''
  2686.  
  2687.   SBlock += IT('<!-- Slice -->')
  2688.   SBlock += SVGGroup(IT,
  2689.     {'transform': 'translate(' + OffsetStr + ')'}
  2690.   )
  2691.  
  2692.   FoldClearance = 0.3
  2693.  
  2694.   if IsSaggital:
  2695.     MiddleCutStart = HSM.Top
  2696.     MiddleCutEnd = MiddleCutY
  2697.     FenceCutStart = BaseY
  2698.     FenceCutEnd = BaseY - HSM.FenceCutLength
  2699.     FoldStart = FenceCutEnd - FoldClearance
  2700.     FoldEnd = MiddleCutEnd + FoldClearance
  2701.   else:
  2702.     MiddleCutStart = BaseY
  2703.     MiddleCutEnd = MiddleCutY
  2704.     FenceCutStart = HSM.Top
  2705.     FenceCutEnd = HSM.Top + HSM.FenceCutLength
  2706.     FoldStart = FenceCutEnd + FoldClearance
  2707.     FoldEnd = MiddleCutEnd - FoldClearance
  2708.  
  2709.   # Outline and cuts
  2710.  
  2711.   SBlock += IT('<!-- Slice outline -->')
  2712.   SBlock += SVGPath(IT, SliceOutline)
  2713.  
  2714.   SBlock += IT('<!-- Ring and fence cuts -->')
  2715.   SBlock += SVGPath(IT, [
  2716.     (A, (RingX1, BaseY)), (A, (RingX1, BaseY - HSM.RingCutLength)), B,
  2717.     (A, (RingX2, BaseY)), (A, (RingX2, BaseY - HSM.RingCutLength)), B,
  2718.     (A, (0.0, MiddleCutStart)), (A, (0.0, MiddleCutEnd)), B,
  2719.     (A, (0.0, FenceCutStart)), (A, (0.0, FenceCutEnd))
  2720.   ], {'stroke-width': '0.1', 'stroke-linecap': 'butt'})
  2721.  
  2722.   # Fold line
  2723.  
  2724.   if FoldType in [ftMountain, ftValley]:
  2725.  
  2726.     if FoldType == ftValley:
  2727.       FoldAttrs = {'stroke-width': '0.025', 'stroke-dasharray': '0.2 0.17'}
  2728.     else:
  2729.       FoldAttrs = {'stroke-width': '0.025'}
  2730.  
  2731.     SBlock += IT('<!-- Fold line -->')
  2732.     SBlock += SVGPath(IT, [
  2733.       (A, (0.0, FoldStart)), (A, (0.0, FoldEnd))
  2734.     ], FoldAttrs)
  2735.  
  2736.   # Names of radials
  2737.  
  2738.   ABlock = ''
  2739.  
  2740.   ABlock += IT('<!-- Names of radials -->')
  2741.   ABlock += SVGGroup(IT, {
  2742.     'font-family': 'sans-serif',
  2743.     'font-size': MaxDP(min(2.0, 0.15 * Span), 1),
  2744.     'fill': 'black',
  2745.     'stroke': 'none'
  2746.   })
  2747.  
  2748.   y = BaseY - 2.0 * HSM.RingCutLength
  2749.  
  2750.   ABlock += SVGText(IT,
  2751.     (CrownX1, y),
  2752.     HSM.RadialNames[RIx1],
  2753.     {'text-anchor': 'start'}
  2754.   )
  2755.  
  2756.   ABlock += SVGText(IT,
  2757.     (CrownX2, y),
  2758.     HSM.RadialNames[RIx2],
  2759.     {'text-anchor': 'end'}
  2760.   )
  2761.  
  2762.   ABlock += SVGGroupEnd(IT)
  2763.  
  2764.   # Crown level
  2765.  
  2766.   CLScale = 0.055 * (BaseY - HSM.Top)
  2767.  
  2768.   ABlock += IT('<!-- Crown level -->')
  2769.   ABlock += SVGGroup(IT, {
  2770.     'stroke-width': MaxDP(0.2 * CLScale, 4),
  2771.     'stroke-linecap': 'butt',
  2772.     'marker-end': 'url(#ArrowHead)'
  2773.   })
  2774.  
  2775.   ABlock += SVGPath(IT, [
  2776.     (A, (CrownX1 + 2.0 * CLScale, 0.0)), (A, (CrownX1 + 1.23 * CLScale, 0.0)),
  2777.   ])
  2778.  
  2779.   ABlock += SVGPath(IT, [
  2780.     (A, (CrownX2 - 2.0 * CLScale, 0.0)), (A, (CrownX2 - 1.23 * CLScale, 0.0))
  2781.   ])
  2782.  
  2783.   ABlock += SVGGroupEnd(IT)
  2784.  
  2785.   x = 0.75 * CrownX1
  2786.   s = max(0.2, min(0.8, -0.08 * HSM.Top))
  2787.  
  2788.   # Metrics annotations
  2789.  
  2790.   MBlock = ''
  2791.  
  2792.   MBlock += IT('<!-- Metrics annotations -->')
  2793.   MBlock += SVGGroup(IT, {
  2794.     'font-family': 'sans-serif',
  2795.     'font-size': '0.85',
  2796.     'fill': 'black',
  2797.     'stroke': 'none',
  2798.     'transform': 'translate(' + MaxDP(x, 2) + ', 0) ' +
  2799.         'scale (' + MaxDP(s, 1) + ')'
  2800.   })
  2801.  
  2802.   MBlock += (
  2803.     SVGText(IT, (0, -6.0),
  2804.         'Size: ' + MaxDP(CM.Circumference, 1) + 'cm') +
  2805.     SVGText(IT, (0, -5.0),
  2806.         'Aspect: ' + MaxDP(CM.CrownAspect, 2)) +
  2807.     SVGText(IT, (0, -4.0),
  2808.         'FR: ' + MaxDP(CM.ForeheadRatio, 2)) +
  2809.     SVGText(IT, (0, -3.0),
  2810.         'FA: ' + MaxDP(CM.FrontAspect, 2)) +
  2811.     SVGText(IT, (0, -2.0),
  2812.         'RA: ' + MaxDP(CM.RearAspect, 2)) +
  2813.     SVGText(IT, (0, -1.0),
  2814.         'DA: ' + MaxDP(HM.DomeAspect, 2))
  2815.   )
  2816.  
  2817.   MBlock += SVGGroupEnd(IT)
  2818.  
  2819.   # Slice number annotation
  2820.  
  2821.   SliceSetStr = (SliceSetName + ' ' +
  2822.       str(SliceIxInSet + 1) + '/' + str(SliceSetSize))
  2823.  
  2824.   NBlock = ''
  2825.  
  2826.   NBlock += IT('<!-- Slice number annotation -->')
  2827.   NBlock += SVGGroup(IT, {
  2828.     'font-family': 'sans-serif',
  2829.     'font-size': MaxDP(1.0 * s, 2),
  2830.     'fill': 'black',
  2831.     'stroke': 'none'
  2832.   })
  2833.  
  2834.   NBlock += (
  2835.     SVGText(IT,
  2836.       (0.6 * CrownX2, -2.5 * s),
  2837.       'Slice %d/%d' % (PageNumber, HSM.NumSlices),
  2838.       {'text-anchor': 'middle'}
  2839.     )
  2840.   )
  2841.  
  2842.   NBlock += (
  2843.     SVGText(IT,
  2844.       (0.6 * CrownX2, -1.5 * s),
  2845.       SliceSetStr,
  2846.       {'font-size': MaxDP(0.65 * s, 2), 'text-anchor': 'middle'}
  2847.     )
  2848.   )
  2849.  
  2850.   NBlock += SVGGroupEnd(IT)
  2851.  
  2852.   MBlock += NBlock
  2853.  
  2854.   if FoldStr != '':
  2855.  
  2856.     # Fold instruction
  2857.  
  2858.     MBlock += IT('<!-- Fold instruction -->')
  2859.     MBlock += SVGGroup(IT, {
  2860.       'font-family': 'sans-serif',
  2861.       'font-size': '0.85',
  2862.       'fill': 'black',
  2863.       'stroke': 'none',
  2864.       'transform': 'rotate(90) scale (' + MaxDP(s, 1) + ')'
  2865.     })
  2866.  
  2867.     MBlock += (SVGText(IT,
  2868.       (MiddleY, -0.1 - 0.5 * s), FoldStr, {'text-anchor': 'middle'})
  2869.     )
  2870.  
  2871.     MBlock += SVGGroupEnd(IT)
  2872.  
  2873.   # End of slice
  2874.  
  2875.   SBlock += ABlock + MBlock + SVGGroupEnd(IT)
  2876.  
  2877.   # End of outer group and SVG
  2878.  
  2879.   Result += TBlock + SBlock + SVGGroupEnd(IT) + SVGEnd(IT)
  2880.  
  2881.   return Result
  2882.  
  2883.  
  2884. #-------------------------------------------------------------------------------
  2885.  
  2886.  
  2887. def HatRingSVG(HatSliceMetrics, ImageDim=None, Padding=None):
  2888.  
  2889.   u'''Generate SVG markup for the bracing ring for a pith helmet former.
  2890.  
  2891.  The ring is divided into segments so it can fit on the page. Fortunately,
  2892.  they are labelled with the names of the radials, two of which form each
  2893.  slice.
  2894.  
  2895.  As a bonus, the markup of the fences is included. The fences keep the
  2896.  floppy bits either side of the long cuts of the coronal and saggital
  2897.  slices in place. The fences are labelled with both the radials they keep
  2898.  in place and the span of radials straddled by the fence’s cut.
  2899.  
  2900.  ImageDim, if supplied, indicates the physical size of the image in
  2901.  millimetres as a (Width, Height) pair, usually the printable area for
  2902.  some particular paper size. By default the area is 287mm × 200mm, (A4
  2903.  in lanscape mode short by 5mm from each edge).
  2904.  
  2905.  Padding is the internal margin in millimetres from each edge of the image.
  2906.  By default, the padding is zero.
  2907.  
  2908.  '''
  2909.  
  2910.   #-----------------------------------------------------------------------------
  2911.  
  2912.   # Shorthand
  2913.  
  2914.   A = Pt_Anchor
  2915.   C = Pt_Control
  2916.   B = (Pt_Break, None)
  2917.  
  2918.   #-----------------------------------------------------------------------------
  2919.  
  2920.   def TxPath(IT, AM, Shape, Attributes=None):
  2921.  
  2922.     u'''Return an SVG path for a transformed Shape.'''
  2923.  
  2924.     return SVGPath(IT, ShapeDim(TransformedShape(AM, Shape), 2), Attributes)
  2925.  
  2926.   #-----------------------------------------------------------------------------
  2927.  
  2928.   HSM = HatSliceMetrics
  2929.   HM = HSM.HatMetrics
  2930.   CM = HM.CrownMetrics
  2931.  
  2932.   SVGTitle = 'Pith Helmet Ring Segments and Fences'
  2933.   RingTitle = 'Pith Helmet Ring Segments'
  2934.   FencesTitle = 'Fences'
  2935.  
  2936.   if ImageDim is None:
  2937.     ID = (28.7, 20.0)
  2938.   else:
  2939.     ID = VScaled(ImageDim, 0.1)
  2940.  
  2941.   PrintWidthStrMM = MaxDP(10.0 * ID[0], 3)
  2942.   PrintHeightStrMM = MaxDP(10.0 * ID[1], 3)
  2943.   PrintWidthStr = MaxDP(ID[0], 4)
  2944.   PrintHeightStr = MaxDP(ID[1], 4)
  2945.  
  2946.   IsPortraitAspect = ID[0] < ID[1]
  2947.   ID = (float(PrintWidthStr), float(PrintHeightStr))
  2948.  
  2949.   if IsPortraitAspect:
  2950.     ID = (ID[1], ID[0])
  2951.  
  2952.   GD = VSum(ID, VScaled(VOnes(2), -0.2 * Padding))
  2953.  
  2954.   Scale = min(2.0, min(GD[0], GD[1]) / 20.0)
  2955.  
  2956.   VSpan = GD[1] - 1.4 * Scale
  2957.   N = len(HSM.RingSegments)
  2958.   StripHeight = HSM.RingCutLength / 0.45
  2959.   TotalStripHeight = N * StripHeight
  2960.   Spacing = min(1.0, (VSpan - TotalStripHeight) /  max(1e-6, (N - 1)))
  2961.   TopPadding = min(
  2962.     1.5 * Scale,
  2963.     0.5 * (VSpan - TotalStripHeight - (N - 1) * Spacing)
  2964.   )
  2965.  
  2966.   LabelFontSize = 0.3 * HSM.RingCutLength
  2967.   LabelYOffset = StripHeight - 0.5 * HSM.RingCutLength + 0.5 * LabelFontSize
  2968.  
  2969.   IT = tIndentTracker('  ')
  2970.  
  2971.   Result = SVGStart(IT, SVGTitle, {
  2972.     'width': PrintWidthStr + 'cm',
  2973.     'height': PrintHeightStr + 'cm',
  2974.     'viewBox': '0 0 ' + PrintWidthStr + ' ' + PrintHeightStr,
  2975.     'preserveAspectRatio': 'xMidYMid slice'
  2976.   })
  2977.  
  2978.   # Background
  2979.  
  2980.   Result += IT(
  2981.     '<!-- Background -->',
  2982.     '<rect x="0" y="0" width="' +
  2983.         PrintWidthStr +'" height="' + PrintHeightStr +
  2984.         '" stroke="none" fill="white"/>'
  2985.   )
  2986.  
  2987.   # Outer group
  2988.  
  2989.   OuterGroupAttrs = {
  2990.     'fill': 'none', 'stroke': 'black', 'stroke-width': '0.05'
  2991.   }
  2992.  
  2993.   OGXforms = []
  2994.  
  2995.   if IsPortraitAspect:
  2996.     OGXforms.append('translate(' + MaxDP(ID[1], 4) + ', 0) rotate(90)')
  2997.  
  2998.   if Padding != 0:
  2999.     PaddingStr = MaxDP(0.1 * Padding, 4)
  3000.     OGXforms.append('translate(' + PaddingStr + ', ' + PaddingStr + ')')
  3001.  
  3002.   if len(OGXforms) > 0:
  3003.     OuterGroupAttrs['transform'] = ' '.join(OGXforms)
  3004.  
  3005.   Result += IT('<!-- Outer group -->')
  3006.   Result += SVGGroup(IT, OuterGroupAttrs)
  3007.  
  3008.   # Title and metrics
  3009.  
  3010.   TBlock = ''
  3011.  
  3012.   TBlock += IT('<!-- Title group -->')
  3013.   TBlock += SVGGroup(IT, {
  3014.     'fill': 'black',
  3015.     'stroke': 'none',
  3016.     'font-family': 'sans-serif',
  3017.     'font-size': MaxDP(0.36 * Scale, 2)
  3018.   })
  3019.  
  3020.   TBlock += IT('<!-- Title -->')
  3021.   TBlock += SVGText(IT,
  3022.     VScaled((0.2, 0.75), Scale),
  3023.     RingTitle,
  3024.     {'font-size': MaxDP(0.72 * Scale, 2), 'font-weight': 'bold'}
  3025.   )
  3026.  
  3027.   TBlock += SVGText(IT,
  3028.     (GD[0] - 0.2 * Scale, 0.75 * Scale),
  3029.     FencesTitle,
  3030.     {'font-size': MaxDP(0.72 * Scale, 2),
  3031.         'font-weight': 'bold', 'text-anchor': 'end'}
  3032.   )
  3033.  
  3034.   x = 0.63 * GD[0]
  3035.  
  3036.   TBlock += IT('<!-- Metrics -->')
  3037.   TBlock += (
  3038.     SVGText(IT, (x - 3.2 * Scale, 0.45 * Scale),
  3039.         'Size: ' + MaxDP(CM.Circumference, 1) + 'cm') +
  3040.     SVGText(IT, (x - 3.2 * Scale, 0.85 * Scale),
  3041.         'Aspect: ' + MaxDP(CM.CrownAspect, 2)) +
  3042.     SVGText(IT, (x, 0.45 * Scale),
  3043.         'FA: ' + MaxDP(CM.FrontAspect, 2)) +
  3044.     SVGText(IT, (x, 0.85 * Scale),
  3045.         'FR: ' + MaxDP(CM.ForeheadRatio, 2)) +
  3046.     SVGText(IT, (x + 2.4 * Scale, 0.45 * Scale),
  3047.         'RA: ' + MaxDP(CM.RearAspect, 2)) +
  3048.     SVGText(IT, (x + 2.4 * Scale, 0.85 * Scale),
  3049.         'DA: ' + MaxDP(HM.DomeAspect, 2))
  3050.   )
  3051.  
  3052.   # End of title group
  3053.  
  3054.   TBlock += SVGGroupEnd(IT)
  3055.  
  3056.   # Ring segments
  3057.  
  3058.   RBlock = ''
  3059.   SegmentY = 1.1 + TopPadding
  3060.   SegmentRightExtents = []
  3061.  
  3062.   for SegIx, Segment in enumerate(HSM.RingSegments):
  3063.  
  3064.     SBlock = ''
  3065.  
  3066.     CutY = StripHeight - HSM.RingCutLength
  3067.     FoldY = 0.5 * CutY
  3068.  
  3069.     SBlock += IT('<!-- Segment ' + str(SegIx + 1) + ' -->')
  3070.     SBlock += SVGGroup(IT,
  3071.       {'transform': 'translate(0.2, ' + MaxDP(SegmentY, 3) +')'}
  3072.     )
  3073.  
  3074.     TabIx = (Segment[0] - 1) % len(HSM.RingCutPoints)
  3075.     TabV = VDiff(HSM.RingCutPoints[Segment[0]], HSM.RingCutPoints[TabIx])
  3076.     StartTabWidth = min(2.0, VLength(TabV))
  3077.     TabIx = (Segment[-1] + 1) % len(HSM.RingCutPoints)
  3078.     TabV = VDiff(HSM.RingCutPoints[TabIx], HSM.RingCutPoints[Segment[-1]])
  3079.     EndTabWidth = min(2.0, VLength(TabV))
  3080.  
  3081.     S = [
  3082.       (A, (0.0, StripHeight)), (A, (0.0, 0.0)),
  3083.       (A, (StartTabWidth, 0.0))
  3084.     ]
  3085.  
  3086.     FoldSpans = []
  3087.     PosLabels = []
  3088.     x = StartTabWidth
  3089.     i = 1
  3090.  
  3091.     while i < len(Segment):
  3092.  
  3093.       C1 = HSM.RingCutPoints[Segment[i - 1]]
  3094.       C2 = HSM.RingCutPoints[Segment[i]]
  3095.       SegV = VDiff(C2, C1)
  3096.       SegLength = VLength(SegV)
  3097.       C1 = VNormalised(C1)
  3098.       C2 = VNormalised(C2)
  3099.  
  3100.       if SegLength > 1e-6:
  3101.         SegDir = VScaled(SegV, 1.0 / SegLength)
  3102.       else:
  3103.         SegDir = VPerp(C1)
  3104.  
  3105.       ChamferAngle1 = max(0.0, 0.5 * pi - acos(-VDot(SegDir, C1)))
  3106.       ChamferAngle2 = max(0.0, 0.5 * pi - acos(VDot(SegDir, C2)))
  3107.  
  3108.       ChamferDist1 = FoldY * tan(ChamferAngle1)
  3109.       ChamferDist2 = FoldY * tan(ChamferAngle2)
  3110.  
  3111.       Chamfered = (SegLength - ChamferDist1 - ChamferDist2 >= 0.0 and
  3112.           max(ChamferDist1, ChamferDist2) < 2.0 * FoldY)
  3113.  
  3114.       if Chamfered:
  3115.  
  3116.         S += [
  3117.           (A, (x, CutY)), (A, (x, FoldY)),
  3118.           (A, (x + ChamferDist1, 0.0)),
  3119.           (A, (x + SegLength - ChamferDist2, 0.0)),
  3120.           (A, (x + SegLength, FoldY))
  3121.         ]
  3122.  
  3123.         FoldSpans.append((x, x + SegLength))
  3124.  
  3125.       else:
  3126.  
  3127.         S += [
  3128.           (A, (x, CutY)),
  3129.           (A, (x, 0.0)),
  3130.           (A, (x + SegLength, 0.0))
  3131.         ]
  3132.  
  3133.       PosLabels.append(((x, LabelYOffset), HSM.RadialNames[Segment[i - 1]]))
  3134.  
  3135.       x += SegLength
  3136.       i += 1
  3137.  
  3138.     PosLabels.append(((x, LabelYOffset), HSM.RadialNames[Segment[-1]]))
  3139.  
  3140.     S += [
  3141.       (A, (x, CutY)), (A, (x, 0.0)),
  3142.       (A, (x + EndTabWidth, 0.0)), (A, (x + EndTabWidth, StripHeight)),
  3143.       (A, (0.0, StripHeight))
  3144.     ]
  3145.  
  3146.     SBlock += IT('<!-- Outline -->')
  3147.     SBlock += SVGPath(IT, S)
  3148.  
  3149.     SegmentRightExtents.append(x + EndTabWidth)
  3150.  
  3151.     S = []
  3152.  
  3153.     for x1, x2 in FoldSpans:
  3154.       mx = 0.5 * (x1 + x2)
  3155.       cx1 = min(mx, x1 + 0.2)
  3156.       cx2 = max(mx, x2 - 0.2)
  3157.       S += [(A, (cx1, FoldY)), (A, (cx2, FoldY)), B]
  3158.  
  3159.     for Pos, Label in PosLabels:
  3160.       S += [
  3161.         (A, (Pos[0], CutY + 0.3)),
  3162.         (A, (Pos[0], StripHeight - 0.2)), B
  3163.       ]
  3164.  
  3165.     if len(S) > 0:
  3166.       SBlock += IT('<!-- Fold lines -->')
  3167.       SBlock += SVGPath(IT,
  3168.         S, {'stroke-width': '0.025', 'stroke-dasharray': '0.21 0.18'}
  3169.       )
  3170.  
  3171.     ABlock = ''
  3172.  
  3173.     ABlock += IT('<!-- Names of radials -->')
  3174.     ABlock += SVGGroup(IT, {
  3175.       'font-family': 'sans-serif',
  3176.       'font-size': MaxDP(LabelFontSize, 2),
  3177.       'fill': 'black',
  3178.       'stroke': 'none',
  3179.       'text-anchor': 'middle'
  3180.     })
  3181.  
  3182.     for Pos, Label in PosLabels:
  3183.       ABlock += SVGText(IT, Pos, Label)
  3184.  
  3185.     ABlock += SVGGroupEnd(IT)
  3186.  
  3187.     # End of segment
  3188.  
  3189.     SBlock += ABlock + SVGGroupEnd(IT)
  3190.     RBlock += SBlock
  3191.  
  3192.     SegmentY += StripHeight + Spacing
  3193.  
  3194.   # Fences
  3195.  
  3196.   FencesBlock = ''
  3197.   LabelMargin = 0.1 * HSM.FenceCutLength
  3198.  
  3199.   for i in range(4):
  3200.  
  3201.     FBlock = ''
  3202.     FenceIsForTop = i < 2
  3203.  
  3204.     if FenceIsForTop:
  3205.       hw = 1.5 * HSM.FenceCutLength
  3206.       h = 2.2 * HSM.FenceCutLength
  3207.     else:
  3208.       hw = 2.0 * HSM.FenceCutLength
  3209.       h = 2.8 * HSM.FenceCutLength
  3210.  
  3211.     if FenceIsForTop == (SegmentRightExtents[0] > SegmentRightExtents[-1]):
  3212.       y = 1.1 * Scale + (h + 0.5 * Scale) * (i & 1)
  3213.     else:
  3214.       y = GD[1] - 0.2 * Scale - (h + 0.5 * Scale) * (i & 1) - h
  3215.  
  3216.     if FenceIsForTop:
  3217.  
  3218.       if i & 1 == 0:
  3219.         FencedIndices = (HSM.Saggitals[0][0], HSM.Saggitals[0][1])
  3220.         StraddleIndices = (HSM.Coronals[0][1], HSM.Coronals[-1][1])
  3221.       else:
  3222.         FencedIndices = (HSM.Saggitals[-1][1], HSM.Saggitals[-1][0])
  3223.         StraddleIndices = (HSM.Coronals[-1][0], HSM.Coronals[0][0])
  3224.  
  3225.       PosLabel = 'Top'
  3226.       PosLabelYOffset = 0.75 * HSM.FenceCutLength
  3227.       SLabelYOffset = 0.95 * h
  3228.       FLabelYOffset = SLabelYOffset - 1.2 * LabelFontSize
  3229.  
  3230.     else:
  3231.  
  3232.       hw = 2.0 * HSM.FenceCutLength
  3233.       h = 2.8 * HSM.FenceCutLength
  3234.  
  3235.       if i & 1 == 0:
  3236.         FencedIndices = (HSM.Coronals[0][0], HSM.Coronals[0][1])
  3237.         StraddleIndices = (HSM.Saggitals[-1][0], HSM.Saggitals[0][0])
  3238.       else:
  3239.         FencedIndices = (HSM.Coronals[-1][1], HSM.Coronals[-1][0])
  3240.         StraddleIndices = (HSM.Saggitals[0][-1], HSM.Saggitals[-1][1])
  3241.  
  3242.       PosLabel = 'Bottom'
  3243.       PosLabelYOffset = h - 0.375 * HSM.FenceCutLength
  3244.       SLabelYOffset = 1.1 * LabelFontSize
  3245.       FLabelYOffset = SLabelYOffset + 1.2 * LabelFontSize
  3246.  
  3247.     w = 2.0 * hw
  3248.  
  3249.     Pos = (GD[0] - 0.2 * Scale - hw, y)
  3250.     PosStr = MaxDP(Pos[0], 3) + ', ' + MaxDP(Pos[1], 3)
  3251.  
  3252.     FBlock += IT('<!-- Fence %d (%s) -->' % (i + 1, PosLabel))
  3253.     FBlock += SVGGroup(IT,
  3254.       {'transform': 'translate(' + PosStr + ')'}
  3255.     )
  3256.  
  3257.     if FenceIsForTop:
  3258.  
  3259.       FBlock += IT('<!-- Outline -->')
  3260.       FBlock += SVGPath(IT, [
  3261.         (A, (-hw, h)), (A, (-hw, 0.5 * hw)),
  3262.         (A, (-0.1 * hw, 0.0)), (A, (0.1 * hw, 0.0)),
  3263.         (A, (hw, 0.5 * hw)), (A, (hw, h)), (A, (-hw, h))
  3264.       ])
  3265.  
  3266.       FBlock += IT('<!-- Cut -->')
  3267.       FBlock += SVGPath(IT, [
  3268.         (A, (0.0, h)), (A, (0.0, HSM.FenceCutLength))
  3269.       ], {'stroke-width': '0.1', 'stroke-linecap': 'butt'})
  3270.  
  3271.       FBlock += IT('<!-- Fold line -->')
  3272.       FBlock += SVGPath(IT, [
  3273.         (A, (0.0, 0.05 * h)), (A, (0.0, HSM.FenceCutLength - 0.05 * h))
  3274.       ], {'stroke-width': '0.025', 'stroke-dasharray': '0.21 0.2'})
  3275.  
  3276.     else:
  3277.  
  3278.       FBlock += IT('<!-- Outline -->')
  3279.       FBlock += SVGPath(IT, [
  3280.         (A, (-hw, h)), (A, (-hw, 0.0)),
  3281.         (A, (hw, 0.0)), (A, (hw, h)), (A, (-hw, h))
  3282.       ])
  3283.  
  3284.       FBlock += IT('<!-- Cut -->')
  3285.       FBlock += SVGPath(IT, [
  3286.         (A, (0.0, 0.0)), (A, (0.0, h - HSM.FenceCutLength))
  3287.       ], {'stroke-width': '0.1', 'stroke-linecap': 'butt'})
  3288.  
  3289.       FBlock += IT('<!-- Fold line -->')
  3290.       FBlock += SVGPath(IT, [
  3291.         (A, (0.0, h - 0.05 * h)), (A, (0.0, h - HSM.FenceCutLength + 0.05 * h))
  3292.       ], {'stroke-width': '0.025', 'stroke-dasharray': '0.21 0.2'})
  3293.  
  3294.     ABlock = ''
  3295.  
  3296.     ABlock += IT('<!-- Annotations -->')
  3297.     ABlock += SVGGroup(IT, {
  3298.       'font-family': 'sans-serif',
  3299.       'font-size': MaxDP(LabelFontSize, 2),
  3300.       'fill': 'black',
  3301.       'stroke': 'none',
  3302.     })
  3303.  
  3304.     ABlock += IT('<!-- Top/Bottom label -->')
  3305.     ABlock += SVGText(IT,
  3306.       (0.0, PosLabelYOffset), PosLabel, {'text-anchor': 'middle'}
  3307.     )
  3308.  
  3309.     ABlock += IT('<!-- Fenced radials -->')
  3310.     ABlock += SVGText(IT,
  3311.       (-hw + LabelMargin, FLabelYOffset),
  3312.       HSM.RadialNames[FencedIndices[0]],
  3313.       {'text-anchor': 'start'}
  3314.     )
  3315.  
  3316.     ABlock += SVGText(IT,
  3317.       (hw - LabelMargin, FLabelYOffset),
  3318.       HSM.RadialNames[FencedIndices[1]],
  3319.       {'text-anchor': 'end'}
  3320.     )
  3321.  
  3322.     ABlock += IT('<!-- Range of straddled radials -->')
  3323.     ABlock += SVGText(IT,
  3324.       (-LabelMargin, SLabelYOffset),
  3325.       HSM.RadialNames[StraddleIndices[0]],
  3326.       {'text-anchor': 'end'}
  3327.     )
  3328.  
  3329.     ABlock += SVGText(IT,
  3330.       (LabelMargin, SLabelYOffset),
  3331.       HSM.RadialNames[StraddleIndices[1]],
  3332.       {'text-anchor': 'start'}
  3333.     )
  3334.  
  3335.     ABlock += SVGGroupEnd(IT)
  3336.  
  3337.     # End of fence
  3338.  
  3339.     FBlock += ABlock + SVGGroupEnd(IT)
  3340.     FencesBlock += FBlock
  3341.  
  3342.   # End of outer group and SVG
  3343.  
  3344.   Result += TBlock + RBlock + FencesBlock + SVGGroupEnd(IT) + SVGEnd(IT)
  3345.  
  3346.   return Result
  3347.  
  3348.  
  3349. #-------------------------------------------------------------------------------
  3350. # Main
  3351. #-------------------------------------------------------------------------------
  3352.  
  3353.  
  3354. def Main():
  3355.  
  3356.   #-----------------------------------------------------------------------------
  3357.  
  3358.   def HelpText(CmdName):
  3359.  
  3360.     Result = (u'Usage: ' + CmdName + ' [OPTION]...\n' +
  3361.       u'Creates plans for a former for a pith helmet over which ' +
  3362.           u'material is lofted.\n' +
  3363.       u'\n' +
  3364.       u'Examples:\n' +
  3365.       u'  ' + CmdName + u' -s 59 --landscape --views-svg images/views.svg\n' +
  3366.       u'  ' + CmdName + u' -s 52 -a 0.75 -n 7 --elliptical -o images/\n' +
  3367.       u'  ' + CmdName + u' -s 70 -a 0.8 -n 12 --paper-size B4 -o .\n' +
  3368.       u'  ' + CmdName + u' -s 48 -n 9 --pages 1,4-7,9,10 -o .\n' +
  3369.       u'  ' + CmdName + u' -v -s 59 -a 0.71 -b 0.69 -f 0.65 -r 0.75 -d 1.1\n' +
  3370.       u'\n' +
  3371.       u'Options:\n' +
  3372.       u'  -s, --size <size>\n' +
  3373.       u'      sets the helmet crown circumference in centimetres.\n' +
  3374.       u'  -a, --aspect <aspect>\n' +
  3375.       u'      is the width of the widest part of the crown divided by ' +
  3376.           u'the length.\n' +
  3377.       u'  -e, --elliptical\n' +
  3378.       u'      sets --fr to 1 and --fa and --fr to the inverse of -a.\n' +
  3379.       u'  -b, --fr, --forehead-ratio <ratio>\n' +
  3380.       u'      is the width of forehead ellipse as a ratio of the rear width.\n' +
  3381.       u'  -f, --fa, --front-aspect <ratio>\n' +
  3382.       u'      is the elliptical ratio for the forehead curve. 0 is flat.\n' +
  3383.       u'  -r, --ra, --rear-aspect <ratio>\n' +
  3384.       u'      is the elliptical ratio for the rear curve. 0 is flat.\n' +
  3385.       u'  -d, --da, --dome-aspect <ratio>\n' +
  3386.       u'      is the height of the dome as a ratio of the crown’s ' +
  3387.           u'equivalent radius.\n' +
  3388.       u'  -n, --num-slices <n>\n' +
  3389.       u'      is the Number of pairs of radial spars over which ' +
  3390.           u'material is lofted.\n' +
  3391.       u'  --views-mode <n>\n' +
  3392.       u'      Rendering: 0 = polygonal, 1 = ideal, 2 = composite.\n' +
  3393.       u'  --views-svg [path]<filename>\n' +
  3394.       u'      is name of the orthogonal ans isometric views ' +
  3395.           u'SVG file to write.\n' +
  3396.       u'  -o, --output <path>\n' +
  3397.       u'      is where the slices and ring-and-fences SVGs ' +
  3398.           u'are to be written.\n' +
  3399.       u'  -p, --pages <range[,range[,range...]]>\n' +
  3400.       u'      is a list of page numbers or ranges of ' +
  3401.           u'helmet former SVGs to save.\n' +
  3402.       u'  -m, --ps, --paper-size <name|W×H>\n' +
  3403.       u'      is the a paper size name or W×H in millimetres.\n' +
  3404.       u'  --margin <x>\n' +
  3405.       u'      is the space in mm between an image edge and the paper’s edge.\n' +
  3406.       u'  --padding <x>\n' +
  3407.       u'      is the space in mm between an image edge and the inked area.\n' +
  3408.       u'  --portrait\n' +
  3409.       u'      selects the (default) higher-than-wide orientation.\n' +
  3410.       u'  --landscape\n' +
  3411.       u'      selects the wide-than-high orientation ' +
  3412.           u'for easy reading on a monitor.\n' +
  3413.       u'  -v, --verbose\n' +
  3414.       u'      displays detailed metrics and progress information.\n' +
  3415.       u'-  q, --quiet\n' +
  3416.       u'      suppresses all output.\n' +
  3417.       u'  -h, --help\n' +
  3418.       u'      summons this help page.\n'
  3419.     )
  3420.  
  3421.     return Result
  3422.  
  3423.   #-----------------------------------------------------------------------------
  3424.  
  3425.   Result = 0
  3426.  
  3427.   ErrMsg = ''
  3428.  
  3429.   try:
  3430.  
  3431.     P = ParamsFromCLArgs(sys.argv)
  3432.     Verbosity = P['Verbosity']
  3433.  
  3434.     if P['DoHelp']:
  3435.  
  3436.       print HelpText(sys.argv[0])
  3437.  
  3438.     if P['DoExecute']:
  3439.  
  3440.       CM = tCrownMetrics(
  3441.         P['CrownCircumference'],
  3442.         P['CrownAspect'],
  3443.         P['ForeheadRatio'],
  3444.         P['FrontAspect'],
  3445.         P['RearAspect']
  3446.       )
  3447.  
  3448.       HM = tHatMetrics(CM, P['DomeAspect'])
  3449.       HSM = tHatSliceMetrics(HM, P['NumSlices'], Verbosity)
  3450.  
  3451.       Correction = (HSM.HullScaleCorrection - 1.0) * 100.0
  3452.       CorrectionStr = MaxDP(Correction, 1) + '%'
  3453.  
  3454.       if Correction < 0.0:
  3455.         CorrectionStr = u'−' + CorrectionStr
  3456.       elif Correction > 0.0:
  3457.         CorrectionStr = '+' + CorrectionStr
  3458.       else:
  3459.         CorrectionStr = 'None'
  3460.  
  3461.       PaperSizeName = P['PaperSize']
  3462.       PaperArea = PaperSize(PaperSizeName)
  3463.       Margin = P['Margin']
  3464.       Padding = P['Padding']
  3465.  
  3466.       if Margin is None:
  3467.         Margin = round(max(2.5, min(10.0,
  3468.             min(PaperArea[0], PaperArea[1]) / 40.0)))
  3469.  
  3470.       if P['IsLandscape'] != (PaperArea[0] > PaperArea[1]):
  3471.         PaperArea = tuple(reversed(PaperArea))
  3472.  
  3473.       PrintArea = VSum(PaperArea, VScaled(VOnes(2), -2 * Margin))
  3474.       ContentArea = VSum(PrintArea, VScaled(VOnes(2), -2 * Padding))
  3475.  
  3476.       if min(ContentArea) < 5.0:
  3477.         raise OptError('The available image area is too small.')
  3478.  
  3479.       LandscapeCA = ContentArea
  3480.  
  3481.       if LandscapeCA[0] < LandscapeCA[1]:
  3482.         LandscapeCA = tuple(reversed(LandscapeCA))
  3483.  
  3484.       SafeLCA = VDiff(LandscapeCA, (4, 4))
  3485.  
  3486.       MaxSliceHeight = HSM.Bottom - HSM.Top + HSM.BaseHeight
  3487.  
  3488.       DoIssueSizeWarning = (10.0 * HSM.MaxSliceSpan > SafeLCA[0] or
  3489.           10.0 * MaxSliceHeight > SafeLCA[1])
  3490.  
  3491.       if Verbosity >= 1:
  3492.  
  3493.         print ('Size: ' + MaxDP(CM.Circumference, 2) + 'cm')
  3494.         print ('Width: ' + MaxDP(CM.CrownWidth, 1) + 'cm')
  3495.         print ('Length: ' + MaxDP(CM.CrownLength, 1) + 'cm')
  3496.         print ('Aspect: ' + MaxDP(CM.CrownAspect, 2) + '')
  3497.         print ('FR, FA, RA: ' + MaxDP(CM.ForeheadRatio, 2) + ', ' +
  3498.             MaxDP(CM.FrontAspect, 2) + ', ' + MaxDP(CM.RearAspect, 2))
  3499.         print ('Dome aspect: ' + MaxDP(HM.DomeAspect, 2))
  3500.  
  3501.         if Verbosity >= 2:
  3502.           print ('Crown polygon scale correction: ' + CorrectionStr)
  3503.           print ('Maximum slice width: ' + MaxDP(HSM.MaxSliceSpan, 1) + 'cm')
  3504.           print ('Slice height: ' + MaxDP(MaxSliceHeight, 1) + 'cm')
  3505.  
  3506.         print ('Pages in the set: 1 to ' + str(HSM.NumSlices + 1))
  3507.  
  3508.       if Verbosity >= 2:
  3509.         print ('Paper area: ' + MaxDP(PaperArea[0], 3) + u'×' +
  3510.             MaxDP(PaperArea[1], 3) + u'mm²' + ((' including ' +
  3511.             MaxDP(Margin, 3) + 'mm margin from each edge')
  3512.             if Margin > 0 else ''))
  3513.         print ('Image area: ' + MaxDP(PrintArea[0], 3) + u'×' +
  3514.             MaxDP(PrintArea[1], 3) + u'mm²' + ((' including ' +
  3515.             MaxDP(Padding, 3) + 'mm padding from each edge')
  3516.             if Padding > 0 else ''))
  3517.         print ('Inked area: ' + MaxDP(ContentArea[0], 3) + u'×' +
  3518.             MaxDP(ContentArea[1], 3) + u'mm²')
  3519.  
  3520.       if Verbosity >= 1 and DoIssueSizeWarning:
  3521.         print ('The helmet plans may be too large for the available ' +
  3522.             'print area on ' +
  3523.             PaperSizeName + ' paper.')
  3524.  
  3525.       # Orthographic and isometric views
  3526.  
  3527.       FileName = P['ViewsFileName']
  3528.  
  3529.       if FileName:
  3530.  
  3531.         if Verbosity >= 2:
  3532.           print 'Rendering orthographic and isometric views.'
  3533.  
  3534.         SVG = HatViewsSVG(HSM, P['ViewsMode'], PrintArea, Padding)
  3535.  
  3536.         if FileName == '-':
  3537.  
  3538.           try:
  3539.             sys.stdout.write(SVG.encode('utf-8'))
  3540.           except (IOError), E:
  3541.             raise FileError('Failed to write views SVG to standard output:' +
  3542.                 '": IOError: ' + str(E))
  3543.  
  3544.         else:
  3545.  
  3546.           if Verbosity >= 2:
  3547.             print 'Saving views SVG to "' + FileName + '".'
  3548.  
  3549.           try:
  3550.             Save(SVG.encode('utf_8'), FileName)
  3551.           except (IOError), E:
  3552.             raise FileError('Cannot save views file "' + str(FileName) +
  3553.                 '": IOError: ' + str(E))
  3554.  
  3555.       Path = P['OutputPath']
  3556.       PageNos = P['PageNumbers']
  3557.  
  3558.       # Slices and ring-plus-fences
  3559.  
  3560.       if Path and PageNos:
  3561.  
  3562.         if Path[-1] not in ['/', '\\']:
  3563.           Path = Path + '/'
  3564.  
  3565.         NP = HSM.NumSlices + 1
  3566.  
  3567.         for PageNo in PageNos:
  3568.  
  3569.           if PageNo == NP:
  3570.  
  3571.             PageType = 'ring segments and fences'
  3572.             FileName = Path + 'ring.svg'
  3573.  
  3574.           else:
  3575.  
  3576.             NS = len(HSM.Saggitals)
  3577.             NC = len(HSM.Coronals)
  3578.  
  3579.             if PageNo - 1 < len(HSM.Saggitals):
  3580.               SliceType = 'saggital %d/%d' % (PageNo, NS)
  3581.             else:
  3582.               SliceType = 'coronal %d/%d' % (PageNo - NS, NC)
  3583.  
  3584.             PageType = SliceType
  3585.             FileName = Path + ('slice%02d' % PageNo) + '.svg'
  3586.  
  3587.           if Verbosity >= 2:
  3588.             print (('Saving page %d/%d' % (PageNo, NP)) +
  3589.                 ' (' + PageType + ') to "' + FileName + '".')
  3590.  
  3591.           if PageNo == NP:
  3592.             SVG = HatRingSVG(HSM, PrintArea, Padding)
  3593.           else:
  3594.             SVG = HatSliceSVG(HSM, PageNo, PrintArea, Padding)
  3595.  
  3596.           try:
  3597.             Save(SVG.encode('utf_8'), FileName)
  3598.           except (IOError), E:
  3599.             raise FileError('Cannot save plan file "' + str(FileName) +
  3600.                 '": IOError: ' + str(E))
  3601.  
  3602.       if Verbosity >= 2:
  3603.         print 'Done!'
  3604.  
  3605.   except (OptError), E:
  3606.  
  3607.     ErrMsg = str(E)
  3608.     Result = 1
  3609.  
  3610.   except (FileError), E:
  3611.  
  3612.     ErrMsg = str(E)
  3613.     Result = 2
  3614.  
  3615.   except (Exception), E:
  3616.  
  3617.     exc_type, exc_value, exc_traceback = sys.exc_info()
  3618.     ErrLines = traceback.format_exc().splitlines()
  3619.     ErrMsg = 'Unhandled exception:\n' + '\n'.join(ErrLines)
  3620.     Result = 3
  3621.  
  3622.   if ErrMsg != '':
  3623.     print >> sys.stderr, sys.argv[0] + ': ' + ErrMsg
  3624.  
  3625.   return Result
  3626.  
  3627.  
  3628. #-------------------------------------------------------------------------------
  3629. # Command line trigger
  3630. #-------------------------------------------------------------------------------
  3631.  
  3632.  
  3633. if __name__ == '__main__':
  3634.   sys.exit(Main())
  3635.  
  3636.  
  3637. #-------------------------------------------------------------------------------
  3638. # End
  3639. #-------------------------------------------------------------------------------
Add Comment
Please, Sign In to add comment