Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/python
- # -*- coding: utf-8 -*-
- #-------------------------------------------------------------------------------
- # phatter.py
- # http://pastebin.com/yKVX8eyQ
- #
- # Custom modules:
- # vegesvgplot.py http://pastebin.com/6Aek3Exm
- #
- #-------------------------------------------------------------------------------
- u'''PHatter, a plotter for printing a former used to construct a pith helmet
- Description:
- Writes as a series of SVG images a pattern for making a former onto which
- a pith Helmet may be bult using cloth tape or thin strips of cardboard,
- glue and way too much spare time. This script requires the VegeSVGPlot
- module.
- Author:
- Daniel Neville (Blancmange), creamygoat@gmail.com
- Copyright:
- None
- Licence:
- Public domain
- INDEX
- Imports
- Exceptions:
- Error
- OptError
- FileError
- tCrownMetrics:
- __init__(
- Circumference, CrownAspect, ForeheadRatio, FrontAspect, RearAspect
- )
- CrownPath
- Circumference
- CrownAspect
- CrownLength
- CrownWidth
- ForeheadWidth
- RearWidth
- ForeheadRatio
- FrontAspect
- RearAspect
- tHatMetrics:
- __init__(CrownMetrics)
- RadialSlice2D(Angle)
- RadialSlice3D(Angle)
- CrownMetrics
- BrimPath
- DomeAspect
- tHatSliceMetrics:
- __init__(HatMetrics, NumSlices, [Verbosity])
- RadialSlice2D(RadialIndex)
- RadialSlice3D(RadialIndex)
- HatMetrics
- NumSlices
- HullScaleCorrection
- Angles
- Radials
- RadialBrimPoints
- RadialNames
- Saggitals
- Coronals
- HasMiddleRadials
- MaxSliceSpan
- Top
- Bottom
- BaseHeight
- RingCutPoints
- RingSegments
- RingCutLength
- FenceCutLength
- Command line parsing functions:
- ParseSequence(NumbersStr, MinValue, MaxValue)
- ParseValue(Name, ValueStr, DataType, TypeStr, MinValueStr, MaxValueStr)
- ParseCLOptions(Args, OptsTemplate, OptDataTypes)
- ParamsFromCLArgs(Args)
- Page output functions:
- PaperSize(Name)
- HatViewsSVG(HatSliceMetrics, [Mode], [ImageDim], [Padding])
- HatSliceSVG(HatSliceMetrics, PageNumber, [ImageDim], [Padding])
- HatRingSVG(HatSliceMetrics, [ImageDim], [Padding])
- Main:
- Main()
- '''
- #-------------------------------------------------------------------------------
- # Imports
- #-------------------------------------------------------------------------------
- from __future__ import division
- import sys
- import traceback
- import math
- from math import (
- pi, sqrt, hypot, sin, cos, tan, asin, acos, atan, atan2, radians, degrees,
- floor, ceil
- )
- # The SVG Plotting for Vegetables module can be found at
- # http://pastebin.com/6Aek3Exm
- from vegesvgplot import (
- # Shape constants
- Pt_Break, Pt_Anchor, Pt_Control,
- PtCmdWithCoordsSet, PtCmdSet,
- # Indent tracker class
- tIndentTracker,
- # Affine matrix class
- tAffineMtx,
- # Affine matrix creation functions
- AffineMtxTS, AffineMtxTRS2D, Affine2DMatrices,
- # Utility functions
- ValidatedRange, MergedDictionary, Save,
- ArrayDimensions, NewArray, CopyArray, At, SetAt, EnumerateArray,
- # Basic vector functions
- VZeros, VOnes, VStdBasis, VDim, VAug, VMajorAxis,
- VNeg, VSum, VDiff, VSchur, VDot,
- VLengthSquared, VLength, VManhattan,
- VScaled, VNormalised,
- VPerp, VRevPerp, VCrossProduct, VCrossProduct4D,
- VScalarTripleProduct, VVectorTripleProduct,
- VProjectionOnto,
- VDiagonalMAV, VTransposedMAV,
- VRectToPol, VPolToRect,
- VLerp,
- # Mathematical functions
- BinomialCoefficient, BinomialRow, NextBinomialRow,
- ApproxSaggita, LGQIntegral25,
- IntegralFunctionOfPLF, EvaluatePQF, EvaluateInvPQF,
- # Linear intersection functions
- LineParameter, XInterceptOfLine, UnitXToLineIntersection,
- LineToLineIntersectionPoint, LineToLineIntersection,
- # Bézier functions
- CubicBezierArcHandleLength, BezierPoint, BezierPAT, SplitBezier,
- BezierDerivative, ManhattanBezierDeviance, BezierLength,
- # Bézier intersection functions
- UnitXToBezierIntersections, LineToBezierIntersections,
- # Shape functions
- ShapeDim, ShapeFromVertices, ShapePoints,
- ShapeSubpathRanges, ShapeCurveRanges,
- ShapeLength, LineToShapeIntersections,
- TransformedShape, PiecewiseArc,
- # Output formatting functions
- MaxDP, GFListStr, GFTupleStr, HTMLEscaped, AttrMarkup, ProgressColourStr,
- # SVG functions
- SVGStart, SVGEnd, SVGPathDataSegments,
- SVGPath, SVGText, SVGGroup, SVGGroupEnd, SVGGrid
- )
- #-------------------------------------------------------------------------------
- # Exceptions
- #-------------------------------------------------------------------------------
- class Error (Exception):
- pass
- class OptError (Error):
- pass
- class FileError(Error):
- pass
- #-------------------------------------------------------------------------------
- # tCrownMetrics
- #-------------------------------------------------------------------------------
- class tCrownMetrics (object):
- '''A tCrownMetrics keeps the size and proprtions of the crown for a hat.
- The crown is approximated by Bézier curves approximating elliptical
- arcs for the front and back, connected by Bézier curves for the parietal
- regions. The crown can be square, rectangular, circular, ellipital or
- the vaguely oval shape that is a good approximation to the shape of a
- human head.
- Methods:
- __init__(
- Circumference, CrownAspect, ForeheadRatio, FrontAspect, RearAspect
- )
- Fields:
- CrownPath
- Circumference
- CrownAspect
- CrownLength
- CrownWidth
- ForeheadWidth
- RearWidth
- ForeheadRatio
- FrontAspect
- RearAspect
- '''
- #-----------------------------------------------------------------------------
- def __init__(self,
- Circumference, CrownAspect, ForeheadRatio, FrontAspect, RearAspect
- ):
- u'''Construct a keeper of metrics of the crown of a hat.
- The crown is approximated by elliptical arcs for the front and back,
- connected by Bézier curves. Examples of silly crown shapes include:
- Circular: (C, 1.0, 1.0, 1.0, 1.0)
- Elliptical: (C, W/L, 1.0, L/W, L/W)
- Square: (C, 1.0, 1.0, 0.0, 0.0)
- Rectangular: (C, W/L, 1.0, 0.0, 0.0)
- where C = circumference, w = width and L = length.
- Constructor Arguments:
- Circumference:
- The circumference of the crown, the perimeter of a transverse slice
- of the head just at the tops of the ears is measured in centimetres.
- CrownAspect:
- The Crown aspect is the head’s width divided by the head’s length.
- If BackAspect and Frontspect are each the inverse of this value
- and ForeheadRatio is 1, the shape of the crown is represented as
- a Bézier approximation to an ellipse.
- 0.71 is the reference value.
- ForeheadRatio:
- The width of the ellipse used to approximate the front curve of
- the crown as a ratio of the rear ellipse is the forehead ration.
- Because the front and read ellipical curves are connected on each
- side by a cubic Bézier curve that usually wraps slightly around the
- front ellipse, the forehead ratio is not easily guaged by looking
- at the generated crown curve.
- 0.69 is the reference value.
- FrontAspect:
- The front part of the crown is usually especially flat and is
- approximated by at most the front half of an ellipse.
- 0.65 is the reference value.
- RearAspect:
- The back part of the crown is fairly rounded and is approximated
- by the back half of an ellipse.
- 0.75 is the reference value.
- '''
- INITIAL_LENGTH = 20.0 # Front-to-back length of the crown before scaling.
- #---------------------------------------------------------------------------
- def UnscaledCrown(CrownAspect, ForeheadRatio, FrontAspect, RearAspect):
- '''Return a Shape for the crown of the head, given sanitised parameters.
- FrontAspect and RearAspect must not be so small as to be inconsistent
- with the overall aspect, CrownAspect. ForeheadRatio must be less than
- or equal to 1.0.
- '''
- # Here, the axis convention used for the head is the same as used for
- # aircraft, sea ships and spacecraft.
- #
- # x is forward, y is right and z is down.
- Result = []
- Length = INITIAL_LENGTH
- Width = Length * CrownAspect
- Ra = 0.5 * Width
- Rb = Ra * RearAspect
- Rc = (-0.5 * Length + Rb, 0.0)
- Fa = ForeheadRatio * Ra
- Fb = Fa * FrontAspect
- Fc = (0.5 * Length - Fb, 0.0)
- # Parietal region
- # Figure out where the parietal node should be. This node is where
- # the control point handles of the parietal segment point.
- PLength = Length - Fb - Rb
- MinP = 0.32 * PLength
- MaxP = PLength + 1.0 * Fb
- a = (1.0 - ForeheadRatio) * (1.0 - ForeheadRatio)
- ParietalNodeDisplacement = (1.0 - a) * MinP + a * MaxP
- PNPos = VSum(Rc, (ParietalNodeDisplacement, Ra))
- RearParietal = VSum(Rc, (0.0, Ra))
- FrontCurve = []
- if FrontAspect > 1e-6:
- # Consider the Parietal Node in the nonuniformly scaled
- # reference frame of the front ellipse.
- P = ((PNPos[0] - Fc[0]) / FrontAspect, PNPos[1] - Fc[1])
- r = Fa
- d = sqrt(max(1e-12, VLengthSquared(P) - r * r))
- # Now find the angle from the front to the temple where a tangent
- # line from the front ellipse to the parietal node point. The angle
- # is measured the front of the reference circle that is the same
- # (lateral) width of the front ellipse.
- FrontAngle = 0.0 * pi + atan(r / d) - atan(P[0] / P[1])
- NumAngleSteps = 2 if FrontAngle > pi / 3.0 else 1
- TangentPoint = VSum(Fc, (Fb * cos(FrontAngle), Fa * sin(FrontAngle)))
- M = AffineMtxTS(Fc, (Fb, Fa))
- FrontCurve = TransformedShape(
- M, PiecewiseArc((0.0, 0.0), 1.0, (0.0, FrontAngle), NumAngleSteps)
- )
- else:
- TangentPoint = VSum(Fc, (0.0, Fa))
- FrontCurve = [(Pt_Anchor, Fc)]
- if Fa > 1e-12:
- FrontCurve.append((Pt_Anchor, TangentPoint))
- if ForeheadRatio < 1.0:
- Result += FrontCurve
- Result.append((Pt_Control, VLerp(TangentPoint, PNPos, 0.25)))
- Result.append((Pt_Control, VLerp(RearParietal, PNPos, 0.90)))
- else:
- if PLength > 1e-6:
- Result += FrontCurve
- else:
- Result += FrontCurve[0 : max(1, len(FrontCurve) - 1)]
- if RearAspect > 1e-6:
- M = AffineMtxTS(Rc, (Rb, Ra))
- S = TransformedShape(
- M,
- PiecewiseArc((0.0, 0.0), 1.0, (0.5 * pi, pi), 2)
- )
- Result += S
- else:
- Result.append((Pt_Anchor, RearParietal))
- Result.append((Pt_Anchor, Rc))
- AM = AffineMtxTS((0.0, 0.0), (1.0, -1.0))
- Result += TransformedShape(AM, list(reversed(Result)))[1:]
- return Result
- #---------------------------------------------------------------------------
- if Circumference < 0.1:
- raise Error(
- (
- 'A crown circumference of %scm is far too small. ' +
- 'Try something between 34 and 66.'
- ) % (str(Circumference))
- )
- if not 0.1 <= CrownAspect <= 10.0:
- raise Error(
- (
- 'A crown aspect of %s is far too silly. ' +
- 'Try something between 0.6 and 0.8.'
- ) % (str(CrownAspect))
- )
- if not 0.1 <= ForeheadRatio <= 10.0:
- raise Error(
- (
- 'A forehead width ratio of %s is far too silly. ' +
- 'Try something between 0.6 and 0.8.'
- ) % (str(ForeheadRatio))
- )
- F = 0.5 * CrownAspect * ForeheadRatio * FrontAspect
- R = 0.5 * CrownAspect * RearAspect
- FRAspectScale = 1.0 / (F + R) if F + R > 1.0 else 1.0
- FA = FRAspectScale * FrontAspect
- RA = FRAspectScale * RearAspect
- FR = ForeheadRatio
- DoFlipBackToFront = ForeheadRatio > 1.0
- if DoFlipBackToFront:
- FA, RA = RA, FA
- FR = 1.0 / FR
- Crown = UnscaledCrown(CrownAspect, FR, FA, RA)
- InitialCircumference = ShapeLength(Crown)
- AdjScale = Circumference / InitialCircumference
- AdjScales = [AdjScale, AdjScale]
- if DoFlipBackToFront:
- AdjScales[0] *= -1
- Crown.reverse()
- M = AffineMtxTS((0.0, 0.0), AdjScales)
- Crown = TransformedShape(M, Crown)
- CL = INITIAL_LENGTH * AdjScale
- CW = CL * CrownAspect
- if ForeheadRatio < 1.0:
- RW = CW
- FW = RW * ForeheadRatio
- else:
- FW = CW
- RW = FW / ForeheadRatio
- self.CrownPath = Crown
- self.Circumference = Circumference
- self.CrownAspect = CrownAspect
- self.CrownLength = CL
- self.CrownWidth = CW
- self.ForeheadWidth = FW
- self.RearWidth = RW
- self.ForeheadRatio = ForeheadRatio
- self.FrontAspect = FrontAspect
- self.RearAspect = RearAspect
- #-----------------------------------------------------------------------------
- #-------------------------------------------------------------------------------
- # tHatMetrics
- #-------------------------------------------------------------------------------
- class tHatMetrics (object):
- u'''A tHatMetrics instance completely describes a pith helmet.
- The metrics are independent of how the helmet is sliced. No hull scale
- correction is applied. The coordinates (x, y, z) coreespond to forward,
- right and down. The xy plane is transverse, the xz plane is saggital
- and the yz plane is coronal.
- The angles used by RadialSlice2D and RadialSlice3D are in radians and
- run from x (forward) to y (right) in the first ½π radians (90°).
- RadialSlice2D returns a Shape with 2D points where (x, y) corresponds
- to (distal, down).
- RadialSlice3D returns 3D points in the (forward, right, down) system.
- Methods:
- __init__(CrownMetrics, DomeAspect)
- RadialSlice2D(self, Angle)
- RadialSlice3D(self, Angle)
- Fields:
- CrownMetrics
- BrimPath
- DomeAspect
- '''
- #-----------------------------------------------------------------------------
- def __init__(self, CrownMetrics, DomeAspect):
- u'''Construct a keeper of metrics for a pith helmet.
- Constructor Arguments:
- CrownMetrics:
- A tCrownMetrics instance which defines the crown of the head.
- DomeAspect:
- The height of the dome above the crown as a ratio of the radius
- of a circle with the same circumference as the crown.
- 1.1 is the reference value.
- '''
- #---------------------------------------------------------------------------
- def RadialTestLineLength(Shape):
- '''Find a suitable length for a radial intersection test line.'''
- Result = 0.0
- for PtType, P in Shape:
- Result = max(Result, VManhattan(P))
- Result *= 2.0
- return Result
- #---------------------------------------------------------------------------
- CM = CrownMetrics
- ch = 0.0
- fh = ch + 0.075 * CM.Circumference # 4.5
- ph = ch + 0.04 * CM.Circumference # 3.0
- rh = ch + 0.1 * CM.Circumference # 6.0
- fd = 0.045 * CM.Circumference # 2.5
- pd = 0.015 * CM.Circumference # 1.0
- rd = 0.065 * CM.Circumference # 4.0
- # Front and rear extremities
- fx = 0.5 * CM.CrownLength + fd
- rx = -0.5 * CM.CrownLength - rd
- # Slope from front to back
- g = (fh - rh) / (fx - rx)
- CrownPath = CrownMetrics.CrownPath
- TestLineLength = RadialTestLineLength(CrownPath)
- BrimPath = []
- # Parietal point and tangent for brim
- Dexter = ((0.0, 0.0), (0.0, TestLineLength))
- Intersections = LineToShapeIntersections(Dexter, CrownPath)
- P, PT2D = Intersections[0][:2]
- PT2D = VNormalised(VNeg(PT2D))
- PT = VNormalised(VAug(PT2D, g))
- PP = VAug(VSum(P, VScaled(VPerp(PT2D), pd)), ph)
- BrimPath += [
- (Pt_Anchor, (fx, 0.0, fh)),
- (
- Pt_Control,
- (
- fx - 0.18 * CM.ForeheadWidth,
- 1.2 * CM.ForeheadWidth / (2.0 + CM.FrontAspect),
- fh + 0.65 * (ph + g * fx - fh)
- )
- )
- ]
- BrimPath += [
- (Pt_Control, VSum(PP, VScaled(PT, 2.1 * fx / (3.0 + CM.FrontAspect)))),
- (Pt_Anchor, PP),
- (Pt_Control, VSum(PP, VScaled(PT, 1.3 * rx / (2.0 + CM.RearAspect))))
- ]
- # Radius and half-chord of rearmost part
- hc = 0.95 * CM.RearWidth / (3.0 + CM.RearAspect)
- r = max(1.01 * hc, 0.9 * CM.RearWidth / (1.0 + CM.RearAspect))
- # Angle and centre
- a = asin(hc / r)
- C = (rx + r, 0.0)
- RearArc = PiecewiseArc(C, r, (pi - a, pi), 1)
- Radial = VPolToRect((r, (pi - a)))
- QP = VSum(C, Radial)
- QT = VNormalised(VPerp(Radial))
- RearBrimHalf = ShapeDim(
- [(Pt_Control, VSum(QP, VScaled(QT, -0.27 * CM.RearWidth)))] + RearArc, 3
- )
- # Position and shear the circular segment up-from-forward so
- # that it and the front tip of the helmet are co-planar.
- AM = tAffineMtx(
- (0.0, 0.0, rh - g * rx),
- ((1.0, 0.0, g), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0))
- )
- RearBrimHalf = TransformedShape(AM, RearBrimHalf)
- BrimPath += RearBrimHalf
- AM = AffineMtxTS((0.0, 0.0, 0.0), (1.0, -1.0, 1.0))
- BrimPath += TransformedShape(AM, tuple(reversed(BrimPath)))
- self.CrownMetrics = CrownMetrics
- self.BrimPath = BrimPath
- self.DomeAspect = DomeAspect
- #-----------------------------------------------------------------------------
- def RadialSlice2D(self, Angle):
- u'''Return a 2D path describing the contour of the helmet at a given angle.
- The angle is measured in radians right from front. The path is a 2D Shape
- which runs from the centre of the button on top of the helmet down to a
- point on the brim.
- The vertices in Shape are (distal, down) pairs. The transformation matrix
- ⎡cos(α) 0 0 ⎤
- M = ⎢sin(α) 0 0 ⎥
- ⎣ 0 1 0 ⎦
- may be used to transform the 2D coordinates to the (forward, right, down)
- convention.
- '''
- CM = self.CrownMetrics;
- TestLine = ((0.0, 0.0), VPolToRect((CM.Circumference, Angle)))
- if Angle == 0:
- CRadius = CM.CrownPath[0][1][0]
- BrimPt3D = self.BrimPath[0][1]
- BrimPt = (BrimPt3D[0], BrimPt3D[2])
- else:
- Intersection = LineToShapeIntersections(
- TestLine, CM.CrownPath)[0]
- CRadius = VLength(Intersection[0])
- Intersection = LineToShapeIntersections(
- TestLine, ShapeDim(self.BrimPath, 2))[0]
- P, T, LParam, SubpathIx, CurveIx, CParam = Intersection
- CurveRange = ShapeCurveRanges(self.BrimPath)[CurveIx]
- BrimCurve = ShapePoints(self.BrimPath, CurveRange)
- BrimPt3D = BezierPoint(BrimCurve, CParam)
- BrimPt = (hypot(BrimPt3D[0], BrimPt3D[1]), BrimPt3D[2])
- ButtonRadius = 0.1 * min(CM.CrownWidth, CM.CrownLength)
- CrownZ = 0.0
- DomeHeight = CM.Circumference * self.DomeAspect / (2.0 * pi)
- CrownPath = [
- (Pt_Anchor, (0.0, 1.075)),
- (Pt_Control, (ButtonRadius * 0.43, 1.075)),
- (Pt_Control, (ButtonRadius * 0.91, 1.049)),
- (Pt_Anchor, (ButtonRadius, 1.0)),
- (Pt_Control, (CRadius * 0.496, 0.966)),
- (Pt_Control, (CRadius * 0.889, 0.641)),
- (Pt_Anchor, (CRadius, 0.0)),
- ]
- M = AffineMtxTS((0.0, CrownZ), (1.0, -DomeHeight))
- Result = TransformedShape(M, CrownPath)
- # Circular arc from crown to brim
- CrownPt = Result[-1][1]
- BrimChord = VDiff(BrimPt, CrownPt)
- BCLength = VLength(BrimChord)
- if BCLength > 1e-12:
- CT = VNormalised(VDiff(CrownPt, Result[-2][1]))
- X = VScaled(BrimChord, 1.0 / BCLength)
- Y = VPerp(X)
- Y = Y if VDot(CT, Y) >= 0.0 else VNeg(Y)
- CTx = VDot(CT, X)
- HalfSweep = acos(max(cos(radians(179)), min(1.0, CTx)))
- MinSafeHalfSweep = 5e-9
- SafeHalfSweep = max(MinSafeHalfSweep, HalfSweep)
- r = 0.5 * BCLength / sin(SafeHalfSweep)
- h = r * CubicBezierArcHandleLength(SafeHalfSweep)
- Saggitta = r * (1.0 - cos(SafeHalfSweep))
- M = VSum(VLerp(CrownPt, BrimPt, 0.5), VScaled(Y, Saggitta))
- BT = VSum(CT, VScaled(X, -2.0 * CTx))
- Curve = [
- VSum(CrownPt, VScaled(CT, h)),
- VSum(M, VScaled(X, -h)),
- M,
- VSum(M, VScaled(X, h)),
- VSum(BrimPt, VScaled(BT, h))
- ]
- if HalfSweep < MinSafeHalfSweep:
- f = HalfSweep / MinSafeHalfSweep
- Flat = [VLerp(CrownPt, BrimPt, i / 6.0) for i in range(1, 6)]
- Flattish = [VLerp(a, b, f) for a, b in zip(Flat, Curve)]
- Curve = Flattish
- for i, Pt in enumerate(Curve):
- Result.append(
- (Pt_Anchor if i == 2 else Pt_Control, Pt)
- )
- Result.append((Pt_Anchor, BrimPt))
- return Result
- #-----------------------------------------------------------------------------
- def RadialSlice3D(self, Angle):
- u'''Return a 3D Shape spacecurve for the helmet at a given angle.
- The angle is measured in radians right from front. The Shape runs from
- the centre of the button on top of the helmet down to a point on the
- brim.
- '''
- AM = tAffineMtx(
- (0.0, 0.0, 0.0),
- ((cos(Angle), sin(Angle), 0.0), (0.0, 0.0, 1.0), (0.0, 0.0, 0.0))
- )
- Result = TransformedShape(AM, ShapeDim(self.RadialSlice2D(Angle), 3))
- return Result
- #-----------------------------------------------------------------------------
- #-------------------------------------------------------------------------------
- # tHatSliceMetrics
- #-------------------------------------------------------------------------------
- class tHatSliceMetrics (object):
- u'''A tHatSliceMetrics keeps track of a hat cut into radial slices.
- The slices are indexed from 0 to NumSlices − 1 and are divided into
- two contiguous groups, the saggital group and the coronal group, each
- a stack of slices with slice indices increasing around the head
- clockwise from above for their front and right extremities. Each
- group is stapled along a vertical fold in the middle and its slices
- spread to form a double fan. By means of slots, the two groups are
- joined to form the circular fan of spars over which the hat material
- is lofted.
- The crown shape is approximated by a convex polygon with the given
- circumference and with twice the number of sides as there are slices.
- Methods:
- __init__(HatMetrics, NumSlices, [Verbosity])
- RadialSlice2D(RadialIndex)
- RadialSlice3D(RadialIndex)
- Fields:
- HatMetrics
- NumSlices
- HullScaleCorrection
- Angles
- Radials
- RadialBrimPoints
- RadialNames
- Saggitals
- Coronals
- HasMiddleRadials
- MaxSliceSpan
- Top
- Bottom
- RingCutPoints
- RingSegments
- RingCutLength
- FenceCutLength
- '''
- #-----------------------------------------------------------------------------
- def __init__(self, HatMetrics, NumSlices, Verbosity=0):
- u'''Construct a keeper of metrics of radial hat slices.
- Constructor Arguments:
- HatMetrics:
- An ideal, mathematical description of a pith helmet.
- NumSlices:
- The number of “slices” which must be folded and stapled together to
- make a former for the helmet. The number of slices is half the number
- of sides in the polygon used to approximate the crown.
- '''
- #---------------------------------------------------------------------------
- Circle = 2.0 * pi
- #---------------------------------------------------------------------------
- def SliceAssignments(NumSlices):
- u'''Divide slices into saggital and coronal pairs of indices to radials.
- The slices are arranged so they are grouped into saggital and coronal
- groups which are each spread to form a double fan. Because the slices
- are non-intersecting within a single group, all but perhaps the middle
- slice in a group with an odd number of slices are bent at the middle.
- This function returns (Saggitals, Coronals, HasMiddleRadials) where
- Saggitals and Coronals are each a list of pairs of indices to radials
- and HasMiddleRadials is True iff the front dead centre and rear dead
- centre radials should be included. (There can only be zero or two
- radials in the middle saggital plane.) It’s possible for the middle
- saggital radials, if they exist, to belong to different slices.
- '''
- #-------------------------------------------------------------------------
- PREFER_MIDDLE_SAGGITAL = True
- #-------------------------------------------------------------------------
- if NumSlices == 2:
- NumCoronals = 1
- NumSaggitals = 1
- Saggitals = [(0, 2)]
- Coronals = [(1, 3)]
- HasMiddleRadials = False
- else:
- NumSaggitals = NumSlices // 2
- NumCoronals = NumSlices - NumSaggitals
- if NumSaggitals & 1 == 0 and NumCoronals & 1 != 0:
- NumSaggitals, NumCoronals = NumCoronals, NumSaggitals
- Saggitals = []
- Coronals = []
- n = 2 * NumSlices
- d = -(NumSaggitals // 2)
- f = NumSlices + d + NumSaggitals - 1
- for i in range(NumSaggitals):
- Saggitals.append(((d + i) % n, f - i))
- f = (d - 1) % n
- d = d + NumSaggitals
- for i in range(NumCoronals):
- Coronals.append((d + i, f - i))
- HasMiddleRadials = PREFER_MIDDLE_SAGGITAL or (NumSaggitals & 1 != 0)
- return (Saggitals, Coronals, HasMiddleRadials)
- #---------------------------------------------------------------------------
- def RadialName(NumSlices, HasMiddleRadials, RadialIndex):
- u'''Return a short, friendly name for a radial hat slice.
- For a four-slice hat with a middle saggital slice, the eight radials
- would be named “Front”, “R1”, “R2”, “R3”, “Back”, “L3”, “L2” and “L1”.
- '''
- if HasMiddleRadials:
- if RadialIndex < NumSlices:
- NameIx = RadialIndex
- IsMiddleSaggital = (RadialIndex == 0)
- else:
- NameIx = -(2 * NumSlices - RadialIndex)
- IsMiddleSaggital = (RadialIndex == NumSlices)
- else:
- IsMiddleSaggital = False
- if RadialIndex < NumSlices:
- NameIx = RadialIndex + 1
- else:
- NameIx = -(2 * NumSlices - RadialIndex)
- if IsMiddleSaggital:
- Result = 'Front' if NameIx == 0 else 'Back'
- else:
- Result = ('L' if NameIx < 0 else 'R') + str(abs(NameIx))
- return Result
- #---------------------------------------------------------------------------
- def RadialTestLineLength(CrownShape):
- '''Find a suitable length for a radial intersection test line.'''
- Result = 0.0
- for PtType, P in CrownShape:
- Result = max(Result, VManhattan(P))
- Result *= 2.0
- return Result
- #---------------------------------------------------------------------------
- def OptimisedRadials(CrownShape, HasMiddleRadials, RHAngles):
- u'''Generate radial crown vectors from initial right hemisphere angles.
- The angles supplied must be strictly greater than zero and strictly
- less than pi. HasMiddleRadials is True iff the radials are to include
- those which lie in the middle saggital plane.
- The radial vectors returned lie in the transverse plane and include
- both the left and right hemispheres. If HasMiddleRadials is True,
- the front dead centre and rear dead centre radials are included.
- The radials are ordered clockwise from front as seen from above.
- If no front dead centre radial exists, the first radial is the one
- in the right hemisphere closest to front dead centre.
- '''
- #-------------------------------------------------------------------------
- def CumulativeArcLengthsForRanges(ShapeVertices, CRanges):
- u'''Return the cumulative arc length for each Bézier endpoint.
- The arc lengths returned allows the arc length of a Shape up to a
- particular point to be determined without having to sum the total
- arc lengths of all the Shape’s Bézier curves prior to the one which
- contains that point.
- '''
- Result = []
- d = 0.0
- for R in CRanges:
- d += BezierLength(ShapeVertices[R[0]:R[1]])
- Result.append(d)
- return Result
- #-------------------------------------------------------------------------
- def CumulativeArcLength(ShapeVertices, CRanges, CALs, CurveIx, CParam):
- u'''Efficiently compute a Shape’s arc length up to a point.
- ShapeVertices is the Shape with the tags removed. CRanges is a list
- of all the curve ranges in the Shape, one for each Bézier. Each curve
- range is a pair of inclusive-start and exclusive-end indices within
- ShapeVertices. CALs is the list of cumulative arc lengths.
- The point on the Shape is given by CurveIx and CParam, the Bézier
- curve parameter.
- '''
- Result = CALs[CurveIx - 1] if CurveIx > 0 else 0.0
- R = CRanges[CurveIx]
- Curve = ShapeVertices[R[0]:R[1]]
- Result += BezierLength(Curve, (0, CParam))
- return Result
- #-------------------------------------------------------------------------
- def WanderersFromVectors(
- CrownShape, CrownVertices, CRanges, TestLineLength, RadialVectors
- ):
- u'''Create Wanderers from radial vectors to optimise a hat’s crown.
- Returned is a list of (Point, Tangent, CurveIx, CParam) tuples, one
- for each radial vector extended until it intersects the crown Shape.
- '''
- Result = []
- for i, V in enumerate(RadialVectors):
- if HasMiddleRadials and (i == 0):
- Result.append((CrownVertices[0], (0.0, 1.0), 0, 0.0))
- else:
- Line = ((0.0, 0.0), VScaled(VNormalised(V), TestLineLength))
- Intersections = LineToShapeIntersections(Line, CrownShape)
- if len(Intersections) == 0:
- Angle = atan2(Line[1][1], Line[1][0])
- raise Error(
- u'No intersection found at radial %d (%g°)' %
- (i, degrees(Angle))
- )
- P, Tangent, LParam, SubpathIx, CurveIx, CParam = Intersections[0]
- R = CRanges[CurveIx]
- C1, C2 = SplitBezier(CrownVertices[R[0]:R[1]], CParam)
- Tangent = VNormalised(VDiff(C2[1], C1[-2]))
- Result.append((P, Tangent, CurveIx, CParam))
- return Result
- #-------------------------------------------------------------------------
- TestLineLength = RadialTestLineLength(CrownShape)
- Line = ((0.0, 0.0), (-TestLineLength, 0.0))
- FrontRadial = CrownShape[0][1]
- RearRadial = (LineToShapeIntersections(Line, CrownShape)[0][0][0], 0.0)
- if HasMiddleRadials:
- Angles = [0.0] + RHAngles + [pi]
- else:
- Angles = [-RHAngles[0]] + RHAngles + [-RHAngles[-1]]
- N = len(Angles)
- CVs = ShapePoints(CrownShape)
- CRs = ShapeCurveRanges(CrownShape)
- CALs = CumulativeArcLengthsForRanges(CVs, CRs)
- Circumference = CALs[-1]
- Tolerance = 0.02 * Circumference / max(8, N)
- RadialVectors = []
- for Angle in Angles:
- RadialVectors.append(VPolToRect((1.0, Angle)))
- Wanderers = WanderersFromVectors(
- CrownShape, CVs, CRs, TestLineLength, RadialVectors
- )
- LastDeltas = [0.0] * N
- SpeedLimits = [1.0] * N
- MaxStressDiff = float('inf')
- StressBounds = [0.0, float('inf')]
- NumSteps = 0
- Limit = 50 + 10 * N
- if Verbosity >= 2:
- print "Optimising angles of slice radials for crown shape..."
- while StressBounds[1] - StressBounds[0] > Tolerance and NumSteps < Limit:
- WCALs = []
- for W in Wanderers:
- CurveIx, CParam = W[2:4]
- a = CumulativeArcLength(CVs, CRs, CALs, CurveIx, CParam)
- WCALs.append(a)
- Sides = []
- Stresses = []
- for i in range(N - 1):
- j = i + 1
- ChordLength = VLength(VDiff(Wanderers[j][0], Wanderers[i][0]))
- CurveLength = (WCALs[j] - WCALs[i]) % Circumference
- Deviation = ApproxSaggita(CurveLength, min(CurveLength, ChordLength))
- Stress = Deviation + 0.02 * ChordLength
- Sides.append(ChordLength)
- Stresses.append(Stress)
- RadialVectors = []
- MaxStressDiff = 0.0
- StressBounds = [float('inf'), 0.0]
- for i in range(1, len(Wanderers) - 1):
- W = Wanderers[i]
- h = i - 1
- P, Tangent = W[0:2]
- ChordLength1 = Sides[h]
- ChordLength2 = Sides[i]
- s1 = Stresses[h]
- s2 = Stresses[i]
- StressBounds[0] = min(StressBounds[0], min(s1, s2))
- StressBounds[1] = max(StressBounds[1], max(s1, s2))
- StressDiff = s2 - s1
- MaxStressDiff = max(MaxStressDiff, abs(StressDiff))
- SpeedLimit = SpeedLimits[i]
- Delta = 6.0 * StressDiff
- if Delta < 0:
- Delta = max(-0.25 * ChordLength1, -(s1 + 1e-6) / (1e-6 + s1 + s2))
- else:
- Delta = min(0.25 * ChordLength2, (s2 + 1e-6) / (1e-6 + s1 + s2))
- LD = LastDeltas[i]
- if LD != 0.0 and Delta != 0.0:
- if LD * Delta < 0.0:
- SpeedLimit *= 0.25
- Delta *= SpeedLimit
- LastDeltas[i] = Delta
- SpeedLimit = min(1.0, SpeedLimit * 1.2)
- SpeedLimits[i] = SpeedLimit
- P = VSum(P, VScaled(Tangent, Delta))
- RadialVectors.append(P)
- if HasMiddleRadials:
- RadialVectors = [FrontRadial] + RadialVectors + [RearRadial]
- else:
- LeftFront = (RadialVectors[0][0], -RadialVectors[0][1])
- LeftRear = (RadialVectors[-1][0], -RadialVectors[-1][1])
- RadialVectors = [LeftFront] + RadialVectors + [LeftRear]
- Wanderers = WanderersFromVectors(
- CrownShape, CVs, CRs, TestLineLength, RadialVectors
- )
- NumSteps += 1
- if Verbosity >= 2:
- print "Step %d complete" % (NumSteps)
- if Verbosity >= 2:
- print "Optimised in %d steps." % (NumSteps)
- RHRadials = [W[0] for W in Wanderers[1:-1]]
- # The left hemisphere is a mirror of the right hemisphere. If
- # there is a pair of true saggital radials, they must be must
- # be inserted.
- LHRadials = [(P[0], -P[1]) for P in reversed(RHRadials)]
- if HasMiddleRadials:
- Result = [FrontRadial] + RHRadials + [RearRadial] + LHRadials
- else:
- Result = RHRadials + LHRadials
- return Result
- #---------------------------------------------------------------------------
- def CircumferenceOfPolygon(Vertices):
- '''Return the circumference of a polygon defined as a list of vertices.
- If the polygon contains no vertices, None is returned. If the polygon
- has just one vertex, 0.0 is returned.
- '''
- if len(Vertices) > 0:
- Result = 0.0
- LastP = Vertices[-1]
- for P in Vertices:
- Result += VLength(VDiff(P, LastP))
- LastP = P
- else:
- Result = None
- return Result
- #---------------------------------------------------------------------------
- def RingSegments(Angles, RingCutPoints):
- u'''Return four sequences of radial indices, one for each ring segment.
- Each ring segment spans roughly a quarter of the circumference of the
- bracing ring which holds the radial slices in place.
- If the first angle is 0°, the four segments are front-left, front-right,
- rear-right and rear-left, else they are front, back, left and right.
- '''
- CumSpans = []
- N = len(Angles)
- HasMiddleSaggitals = (Angles[0] == 0.0)
- if HasMiddleSaggitals:
- LastPt = RingCutPoints[0]
- CumSpan = 0.0
- CumSpans.append(CumSpan)
- RearIx = N // 2
- else:
- LastPt = RingCutPoints[0]
- CumSpan = LastPt[1]
- CumSpans.append(CumSpan)
- RearIx = N // 2 - 1
- for i in range(1, RearIx + 1):
- Pt = RingCutPoints[i]
- CumSpan += VLength(VDiff(LastPt, Pt))
- CumSpans.append(CumSpan)
- LastPt = Pt
- if not HasMiddleSaggitals:
- CumSpan += LastPt[1]
- CumSpans.append(CumSpan)
- if HasMiddleSaggitals:
- Threshold = 0.5 * CumSpan
- Ix = 0
- for i, c in enumerate(CumSpans[:-1]):
- c1 = CumSpans[i + 1]
- if c1 > Threshold:
- Ix = i if Threshold < (0.5 * (c + c1)) else i + 1
- break
- FL = tuple(range(N - Ix, N)) + (0,)
- FR = tuple(range(0, Ix + 1))
- BR = tuple(range(Ix, RearIx + 1))
- BL = tuple(range(RearIx, N - Ix + 1))
- Result = (FL, FR, BR, BL)
- else:
- Threshold1 = 0.25 * CumSpan
- Threshold2 = 0.75 * CumSpan
- Ix1 = 0
- while Ix1 < len(CumSpans) - 1:
- if CumSpans[Ix1 + 1] > Threshold1:
- break
- Ix1 += 1
- Ix2 = Ix1
- while Ix2 < len(CumSpans) - 1:
- if CumSpans[Ix2 + 1] > Threshold2:
- break
- Ix2 += 1
- Variants = (
- (Ix1, Ix2), (Ix1, Ix2 + 1), (Ix1 + 1, Ix2), (Ix1 + 1, Ix2 + 1)
- )
- Ix1 = None
- Ix1 = None
- BestD = CumSpan
- for Variant in Variants:
- c1 = CumSpans[Variant[0]]
- c2 = CumSpans[Variant[1]]
- D = max(2.0 * c1, c2 - c1, 2.0 * (CumSpan - c2))
- if D < BestD:
- Ix1, Ix2 = Variant
- BestD = D
- F = tuple(range(N - 1 - Ix1, N)) + tuple(range(0, Ix1 + 1))
- B = tuple(range(Ix2, N - Ix2))
- L = tuple(range(N - 1 - Ix2, N - Ix1))
- R = tuple(range(Ix1, Ix2 + 1))
- Result = (F, B, L, R)
- return Result
- #---------------------------------------------------------------------------
- if NumSlices < 2:
- raise Error('NumSlices must be at least 2.')
- HM = HatMetrics
- CM = HM.CrownMetrics
- Saggitals, Coronals, HasMiddleRadials = SliceAssignments(NumSlices)
- RadialNames = []
- for RadialIndex in range(2 * NumSlices):
- RadialNames.append(RadialName(NumSlices, HasMiddleRadials, RadialIndex))
- RHAngles = []
- if HasMiddleRadials:
- Phase = 1
- NumRightHemisphereRadials = NumSlices - 1
- else:
- Phase = 0.5
- NumRightHemisphereRadials = NumSlices
- for i in range(NumRightHemisphereRadials):
- RefAngle = ((Phase + float(i)) / (2.0 * NumSlices)) * Circle
- x = CM.CrownLength * cos(RefAngle)
- y = CM.CrownWidth * sin(RefAngle)
- Angle = atan2(y, x)
- RHAngles.append(Angle)
- Radials = OptimisedRadials(CM.CrownPath, HasMiddleRadials, RHAngles)
- Angles = []
- for P in Radials:
- Angle = atan2(P[1], P[0])
- Angles.append(Angle)
- HullCircumference = CircumferenceOfPolygon(Radials)
- HullAdj = CM.Circumference / HullCircumference
- Radials = [VScaled(P, HullAdj) for P in Radials]
- MaxSliceSpan = 0.0
- Top = float('inf')
- Bottom = float('-inf')
- RadialBrimPoints = []
- for i, Angle in enumerate(Angles):
- R = HM.RadialSlice2D(Angle)
- BrimPt = R[-1][1]
- Top = min(Top, R[0][1][1])
- Bottom = max(Bottom, BrimPt[1])
- BrimPt3D = (
- HullAdj * cos(Angle) * BrimPt[0],
- HullAdj * sin(Angle) * BrimPt[0],
- BrimPt[1]
- )
- RadialBrimPoints.append(BrimPt3D)
- for Ix1, Ix2 in Saggitals + Coronals:
- BrimPt1 = RadialBrimPoints[Ix1]
- BrimPt2 = RadialBrimPoints[Ix2]
- SliceSpan = hypot(BrimPt1[0], BrimPt1[1]) + hypot(BrimPt2[0], BrimPt2[1])
- MaxSliceSpan = max(MaxSliceSpan, SliceSpan)
- RingCutPoints = []
- Margin = 0.1 * HullAdj * min(CM.CrownWidth, CM.CrownLength)
- for i, BrimPt in enumerate(RadialBrimPoints):
- BrimXY = VDim(RadialBrimPoints[i], 2)
- MarginV = VPolToRect((-Margin, Angles[i]))
- CutPt = VSum(VLerp(BrimXY, Radials[i], 0.5), MarginV)
- RingCutPoints.append(CutPt)
- RS = RingSegments(Angles, RingCutPoints)
- self.HatMetrics = HM
- self.NumSlices = NumSlices
- self.HullScaleCorrection = HullAdj
- self.Angles = Angles
- self.Radials = Radials
- self.RadialBrimPoints = RadialBrimPoints
- self.RadialNames = RadialNames
- self.Saggitals = Saggitals
- self.Coronals = Coronals
- self.HasMiddleRadials = HasMiddleRadials
- self.MaxSliceSpan = MaxSliceSpan
- self.Top = Top
- self.Bottom = Bottom
- self.BaseHeight = 1.5
- self.RingCutPoints = RingCutPoints
- self.RingSegments = RS
- self.RingCutLength = max(1.0, min(1.9, Bottom / 3.0))
- self.FenceCutLength = max(1.0, (Bottom - Top) / 12.0)
- #-----------------------------------------------------------------------------
- def RadialSlice2D(self, RadialIndex):
- u'''Return a 2D path describing the contour of the helmet at a given radial.
- The horizontal scale of the 2D Shape that is returned is adjusted by the
- hull scale correction so that the circumference of the polygonal crown is
- the same as the smooth crown shape.
- The vertices in Shape are (distal, down) pairs. The transformation matrix
- ⎡cos(α) 0 0 ⎤
- M = ⎢sin(α) 0 0 ⎥
- ⎣ 0 1 0 ⎦
- may be used to transform the 2D coordinates to the (forward, right, down)
- convention.
- '''
- R = self.HatMetrics.RadialSlice2D(self.Angles[RadialIndex])
- s = self.HullScaleCorrection
- Result = [(Cmd, (s * Pt[0], Pt[1])) for Cmd, Pt in R]
- return Result
- #-----------------------------------------------------------------------------
- def RadialSlice3D(self, RadialIndex):
- u'''Return a 3D Shape spacecurve for the helmet at a given radial.
- The horizontal scale of the 3D Shape that is returned is adjusted by the
- hull scale correction so that the circumference of the polygonal crown is
- the same as the smooth crown shape.
- '''
- Angle = self.Angles[RadialIndex]
- R = self.HatMetrics.RadialSlice2D(Angle)
- sc = self.HullScaleCorrection * cos(Angle)
- ss = self.HullScaleCorrection * sin(Angle)
- Result = [(Cmd, (sc * Pt[0], ss * Pt[0], Pt[1])) for Cmd, Pt in R]
- return Result
- #-----------------------------------------------------------------------------
- #-------------------------------------------------------------------------------
- # Command line parsing functions
- #-------------------------------------------------------------------------------
- def ParseSequence(NumbersStr, MinValue, MaxValue):
- u'''Return a list of numbers from a comma-separated string of numeric ranges.
- Runs of numbers may be indicated with the hyphen-minus character "-".
- A missing number before the "-" character is taken to be MinValue and a
- missing after after the "-" is taken to be MaxValue. All the numbers must
- be non-negative and within the inclusive range [MinValue, MaxValue].
- Spaces are permitted but not required.
- '''
- #-----------------------------------------------------------------------------
- def CheckRange(x):
- u'''Throw an exception if x is outside the range [MinValue, MaxValue]. '''
- if not (MinValue <= x <= MaxValue):
- raise OptError('The value ' + str(x) + ' is outside the required ' +
- 'range ' + str(MinValue) + '-' + str(MaxValue) + '.')
- #-----------------------------------------------------------------------------
- if NumbersStr is None or NumbersStr.strip() == '':
- return []
- NSTail = NumbersStr
- NRangeStrs = []
- NRs = []
- NonreversedNRs = []
- while ',' in NSTail:
- CommaPos = NSTail.index(',')
- NRangeStrs.append(NSTail[:CommaPos].strip())
- NSTail = NSTail[CommaPos + 1:]
- NRangeStrs.append(NSTail.strip())
- for NRangeStr in NRangeStrs:
- if NRangeStr in ['*', 'all']:
- FirstN = MinValue
- LastN = MaxValue
- else:
- EnDash = '–'
- EmDash = '—'
- HyphenMinus = '-'
- RunCh = ''
- if EnDash in NRangeStr:
- RunCh = EnDash
- if EmDash in NRangeStr:
- RunCh = EmDash
- elif HyphenMinus in NRangeStr:
- RunCh = HyphenMinus
- if RunCh != '':
- x = NRangeStr.index(RunCh)
- FirstNStr = NRangeStr[:x].strip()
- LastNStr = NRangeStr[x + len(RunCh):].strip()
- try:
- NStr = FirstNStr
- FirstN = int(FirstNStr) if FirstNStr != '' else MinValue
- NStr = LastNStr
- LastN = int(LastNStr) if LastNStr != '' else MaxValue
- except (ValueError):
- raise OptError('Expected number but found "' + str(NStr) +
- '" in the range "' + str(NRangeStr) + '".')
- CheckRange(FirstN)
- CheckRange(LastN)
- NR = (FirstN, LastN)
- else:
- try:
- FirstN = int(NRangeStr.strip())
- except (ValueError):
- raise OptError('Expected number but found "' + str(NRangeStr) + '".')
- CheckRange(FirstN)
- LastN = FirstN
- if FirstN <= LastN:
- MinN, MaxN = FirstN, LastN
- else:
- MinN, MaxN = LastN, FirstN
- for NR in NonreversedNRs:
- if MinN <= NR[1] and MaxN >= NR[0]:
- raise OptError('The range "' + str(NRangeStr) + '" overlaps ' +
- 'a previous number or run of numbers.')
- NRs.append((FirstN, LastN))
- NonreversedNRs.append((MinN, MaxN))
- #NRs.sort()
- Result = []
- for NR in NRs:
- if NR[0] <= NR[1]:
- Result += list(range(NR[0], NR[1] + 1))
- else:
- Result += list(range(NR[0], NR[1] - 1, -1))
- return Result
- #-------------------------------------------------------------------------------
- def ParseValue(Name, ValueStr, DataType, TypeStr, MinValueStr, MaxValueStr):
- u'''Read an option argument string of a known datatype.
- If the argument string fails to be read or is out of range, an OptError
- exception is raised.
- Name:
- The name of the option, usually including the option introducer included.
- ValueStr:
- The option value as it appears on the command line, possibly preprocessd
- by a run-time environment which automatically handles quotes.
- DataType:
- Either None for an option which has no argument or a proper datatype such
- as int, float or str. The datatype must have the __name__ property.
- TypeStr:
- Either the name of the datatype or a human-friendly discription of the
- datatype.
- MinValueStr, ManValueStr:
- String prepresentations of the minimum and maximum permissible values
- for the given option. A value of None for each value means that checking
- for that limit is suppressed. If both are not None and ValueStr represents
- a value outside the limits, the valid range is displayed in the exception
- message.
- '''
- Result = None
- RErrStr1 = ('The value ' + str(ValueStr) +
- ' for option ' + str(Name) + ' is too ')
- RErrStr2 = '.'
- if MinValueStr is not None and MaxValueStr is not None:
- RErrStr2 += (' The valid range is from ' + str(MinValueStr) +
- ' to ' + str(MaxValueStr) + '.')
- if DataType is None:
- if ValueStr is not None:
- raise OptError('Option ' + str(Name) + ' cannot be assigned a value.')
- Result = True
- else:
- if ValueStr is None:
- raise OptError('Option ' + str(Name) + ' requires a value.')
- try:
- Result = DataType(ValueStr)
- except (ValueError):
- raise OptError('Expected ' + str(TypeStr) + ' for option ' +
- str(Name) + ' but instead found "' + str(ValueStr) + '".')
- if MinValueStr is not None and Result < DataType(MinValueStr):
- raise OptError(RErrStr1 + 'low' + RErrStr2)
- if MaxValueStr is not None and Result > DataType(MaxValueStr):
- raise OptError(RErrStr1 + 'high' + RErrStr2)
- return Result
- #-------------------------------------------------------------------------------
- def ParseCLOptions(Args, OptsTemplate, OptDataTypes):
- u'''Preprocess command line options and option arguments.
- The preprocessing normalises the option and the option arguments, checks the
- options and their arguments for syntax errors and separates the command line
- arguments into options and operands. Options are the keywords prefixed by “-”
- or “--” along with any arguments they require and operands are the positional
- arguments at the end of the command line.
- Option arguments may be separated from the option name with a space, an
- equals sign or a colon.
- Args:
- The command line command and its arguments. Fancy run-time environments
- such as that provided by Python handle quoting and thus allow arguments
- to have spaces.
- OptsTemplate:
- A dictionary indexed by canonical option names with option descriptors of
- the form (Aliases, DataType, MinValueStr, MaxValueStr). Aliases is a list
- or tuple of alternative names for the option and like the option name,
- include the introducer “-” or “--”. MinValueStr and MaxValueStr, when set
- to values other than None allow lower or upper bounds to be checked within
- this function.
- OptDataTypes:
- A dictionary indexed by the name of each datatype (except None) with
- entries of the form (DataType, FriendlyName).
- The result is an (Options, Operands) pair. Options is a list of (StdOptName,
- GivenOptName, Value) tuples where SydOptName is the canonical optional name,
- GivenOptName is the optio name as given bythe user and Value is the option
- argument value converted to the required datatype. Operands is a list of all
- the command line arguments which follow the options, excluding the optional
- options-operands separator “--”.
- '''
- # Check the template for errors that would easily lead to confusing or
- # misleading error reports.
- for BadArgName in ['-', '--', '']:
- if BadArgName in OptsTemplate:
- raise Error('INTERNAL ERROR: Bad name "' + str(BadArgName) +
- '" in options template')
- # NameMap allows the canonical command line option name to be found given
- # the option name as it appears on the command line.
- NameMap = {}
- for CName in OptsTemplate:
- Aliases = OptsTemplate[CName][0]
- NameMap[CName] = CName
- for Alias in Aliases:
- NameMap[Alias] = CName
- Ix = 1
- NextIx = 0
- Options = []
- while Ix < len(Args):
- NextIx = Ix + 1
- Name = Args[Ix]
- Value = None
- x1 = Name.index('=') if '=' in Name else len(Name)
- x2 = Name.index(':') if ':' in Name else len(Name)
- x = min(x1, x2)
- ValueInArg = x < len(Name)
- if ValueInArg:
- if 0 < x < len(Name) - 1:
- Value = Name[x + 1:]
- Name = Name[:x]
- else:
- raise OptError('Syntax error in option: "' + str(Name) + '"')
- if Name in NameMap:
- CName = NameMap[Name]
- Aliases, DataType, MinStr, MaxStr = OptsTemplate[CName]
- if DataType is not None:
- if Value is None:
- if NextIx < len(Args):
- Value = Args[NextIx]
- NextIx += 1
- if not hasattr(DataType, '__name__'):
- raise Error('INTERNAL ERROR: Datatype "' + TypeStr + '" has no name.')
- TypeStr = DataType.__name__
- if TypeStr in OptDataTypes:
- HRTypeStr = OptDataTypes[TypeStr][1]
- Value = ParseValue(Name, Value, DataType, HRTypeStr, MinStr, MaxStr)
- else:
- raise Error('INTERNAL ERROR: Unhandled datatype "' + TypeStr + '".')
- Options.append((CName, Name, Value))
- else:
- Value = ParseValue(Name, Value, None, None, None, None)
- Options.append((CName, Name, Value))
- else:
- if Name == '-':
- # Stop processing options and process command operands, starting
- # with this one, which stands for standard input or standard output.
- NextIx = Ix
- break
- elif Name == '--':
- # Stop processing options and process command operands.
- Ix = NextIx
- break
- else:
- if len(Name) > 0 and Name[0] == '-':
- raise OptError('Unrecognised option "' + str(Name) + '".')
- else:
- # Not an option but an operand instead.
- NextIx = Ix
- break
- Ix = NextIx
- Operands = Args[Ix:]
- return (Options, Operands)
- #-------------------------------------------------------------------------------
- def ParamsFromCLArgs(Args):
- u'''Parse the command line arguments to produce sanitised program parameters.
- The parameters are returned in a Python dictionary
- DoExecute: True if the normal operation of the program is to continue
- DoHelp: True if the command lien help text should be displayed
- CrownCircumference: Circumference of the part which rests on the head
- CrownAspect: Width of the widest part of the crown divided by the length
- ForeheadRatio: Width of forehead ellipse as a ratio of the rear width
- FrontAspect: Elliptical ratio for the forehead curve
- RearAspect: Elliptical ratio for the rear curve
- DomeAspect: Height of the dome as a ratio of the crown’s equivalent radius
- NumSlices: Number of pairs of radial spars over which material is lofted
- ViewsMode: 0 = polygon, 1 = smooth, 2 = combined
- IsLandscape: True if the image width is to be greater than the image height
- ViewsFileName: Name of the orthogonal views SVG file to write
- OutputPath: Where the slices and ring-and-fences SVGs are to be written
- PaperSize: Name of paper size or W×H in millimetres
- Margin: Space in mm between an image edge and the corresponding paper edge
- Padding: Space in mm between an image edge and the inked area
- PageNumbers: List of page numbers (and runs) of helmet former SVGs
- Verbosity: 0 = quiet, 1 = normal, 2 = verbose
- If the command line arguments cannot be parsed properly or failed to pass a
- range check, OpeError is raised.
- '''
- Result = {
- 'DoExecute': True,
- 'DoHelp': False,
- 'CrownCircumference': 59.0,
- 'CrownAspect': 0.71,
- 'ForeheadRatio': 0.69,
- 'FrontAspect': 0.65,
- 'RearAspect': 0.75,
- 'DomeAspect': 1.1,
- 'NumSlices': 9,
- 'ViewsMode': 0,
- 'IsLandscape': False,
- 'ViewsFileName': '',
- 'OutputPath': '',
- 'PaperSize': 'A4',
- 'Margin': None,
- 'Padding': 0,
- 'PageNumbers': None,
- 'Verbosity': 1
- }
- OptsTemplate = {
- '--size': (('-s',), float, '0.1', '500'),
- '--aspect': (('-a',), float, '0.2', '1.0'),
- '--elliptical': (('-e',), None, None, None),
- '--forehead-ratio': (('-b', '--fr'), float, '0.3', '1.0'),
- '--front-aspect': (('-f', '--fa'), float, '0', '5.0'),
- '--rear-aspect': (('-r', '--ra'), float, '0', '5.0'),
- '--dome-aspect': (('-d', '--da'), float, '0.1', '5.0'),
- '--num-slices': (('-n',), int, '2', '50'),
- '--views-mode': ((), int, '0', '2'),
- '--views-svg': (('-g',), str, None, None),
- '--output': (('-o',), str, None, None),
- '--pages': (('-p',), str, None, None),
- '--paper-size': (('-m', '--ps'), str, None, None),
- '--margin': ((), float, '0', None),
- '--padding': ((), float, '0', None),
- '--portrait': ((), None, None, None),
- '--landscape': ((), None, None, None),
- '--verbose': (('-v',), None, None, None),
- '--quiet': (('-q',), None, None, None),
- '--help': (('-h',), None, None, None)
- }
- OptDataTypes = {
- 'int': (int, 'integer'),
- 'float': (float, 'floating point number'),
- 'str': (str, 'string')
- }
- Options, Operands = ParseCLOptions(Args, OptsTemplate, OptDataTypes)
- PageNumbersOptionName = None
- PageNumbersStr = None
- IsElliptical = False
- IsNotElliptical = False
- NonEllipticalOptions = [
- '--forehead-ratio',
- '--front-aspect',
- '--rear-aspect'
- ]
- IsQuiet = False
- IsVerbose = False
- DoHelp = False
- for OptRec in Options:
- if OptRec[0] == '--help':
- DoHelp = True
- break
- if DoHelp:
- Result['DoHelp'] = True
- Result['DoExecute'] = False
- elif len(Operands) > 0:
- raise OptError('Too many operands (' + str(len(Operands)) + ') were given. ' +
- 'Perhaps an option introducer "-" was omitted.')
- else:
- for OptRec in Options:
- Option, SuppliedOptionName, Value = OptRec
- if Option in NonEllipticalOptions:
- if IsElliptical:
- raise OptError('Option ' + str(SuppliedOptionName) +
- ' cannot be used with the -e or --elliptical option.')
- IsNotElliptical = True
- if Option == '--elliptical':
- if IsNotElliptical:
- raise OptError('Option ' + str(SuppliedOptionName) +
- ' cannot be used with any of the non-elliptical options.')
- IsElliptical = True
- if Option == '--size':
- Result['CrownCircumference'] = Value
- elif Option == '--aspect':
- Result['CrownAspect'] = Value
- elif Option == '--forehead-ratio':
- Result['ForeheadRatio'] = Value
- elif Option == '--front-aspect':
- Result['FrontAspect'] = Value
- elif Option == '--rear-aspect':
- Result['RearAspect'] = Value
- elif Option == '--dome-aspect':
- Result['DomeAspect'] = Value
- elif Option == '--num-slices':
- Result['NumSlices'] = Value
- elif Option == '--views-mode':
- Result['ViewsMode'] = Value
- elif Option == '--views-svg':
- Result['ViewsFileName'] = Value
- elif Option == '--output':
- Result['OutputPath'] = Value
- elif Option == '--paper-size':
- try:
- PS = PaperSize(Value)
- except (Error), E:
- raise OptError('Invalid value for option ' +
- str(SuppliedOptionName) + ': ' + str(E))
- else:
- Result['PaperSize'] = Value
- elif Option == '--portrait':
- Result['IsLandscape'] = False
- elif Option == '--landscape':
- Result['IsLandscape'] = True
- elif Option == '--margin':
- Result['Margin'] = Value
- elif Option == '--padding':
- Result['Padding'] = Value
- elif Option == '--pages':
- PageNumbersOptionName = SuppliedOptionName
- PageNumbersStr = Value
- elif Option == '--verbose':
- IsVerbose = True
- elif Option == '--quiet':
- IsQuiet = True
- if IsElliptical:
- Result['ForeheadRatio'] = 1.0
- Result['FrontAspect'] = Result['RearAspect'] = 1.0 / Result['CrownAspect']
- N = Result['NumSlices']
- if PageNumbersStr is not None:
- try:
- Result['PageNumbers'] = ParseSequence(PageNumbersStr, 1, N + 1)
- except (OptError), E:
- raise OptError('Bad pages list for option ' +
- str(PageNumbersOptionName) + ': ' + str(E))
- else:
- Result['PageNumbers'] = list(range(1, N + 2))
- if Result['ViewsFileName'] == '-':
- # Write views SVG to stdout.
- IsQuiet = True
- if Result['OutputPath'] == '-':
- # Perversely concatenate SVGs to stdout.
- IsQuiet = True
- if IsQuiet:
- Verbosity = 0
- else:
- Verbosity = 2 if IsVerbose else 1
- Result['Verbosity'] = Verbosity
- return Result
- #-------------------------------------------------------------------------------
- # Page output functions
- #-------------------------------------------------------------------------------
- def PaperSize(Name):
- u'''Return the ISO 216 or 269 paper size as portrait (Width, Height) in mm.
- The ISO 216 paper sizes range from A0 to A10 and B0 to B10. The ISO 269 sizes
- range from C0 to C10.
- As a bonus, the Swedish SIS 014711 and the German DIN 476 extentions are
- supported, along with the US paper sizes, Letter, Legal, Ledger and Tabloid.
- The width and height of the paper in millimetres can be specified directly
- in the form W×H where “×” may be any of “×”, “x” or “X”.
- Ledger is the odd one out in that its width is greater than its height so
- its dimensions, still returned as (Width, Height) reflect its landscape
- orientation.
- if the name of the paper size is not known, an exception is raised.
- '''
- Result = None
- Inch = 25.4
- if Name == 'C7/6': # ISO 269
- Result = (81, 162)
- elif Name == 'DL': # ISO 269
- Result = (110, 220)
- elif Name == '4A0': # DIN 476
- Result = (1682, 2378)
- elif Name == '2A0': # DIN 476
- Result = (1189, 1682)
- elif Name == 'Letter': # US
- Result = (8.5 * Inch, 11.0 * Inch)
- elif Name == 'Legal': # US
- Result = (8.5 * Inch, 14.0 * Inch)
- elif Name in 'Ledger': # US
- Result = (17.0 * Inch, 11.0 * Inch)
- elif Name == 'Tabloid': # US
- Result = (11.0 * Inch, 17.0 * Inch)
- if Result is None:
- # Try the W×H format
- LCLatinX = 'x'
- UCLatinX = 'X'
- Times = '×'
- SepCh = LCLatinX if LCLatinX in Name else ''
- SepCh = UCLatinX if UCLatinX in Name else SepCh
- SepCh = Times if Times in Name else SepCh
- if SepCh != '':
- x = Name.index(SepCh)
- WidthStr = Name[:x].strip()
- HeightStr = Name[x + len(SepCh):].strip()
- try:
- Width = float(WidthStr)
- Height = float(HeightStr)
- W = int(round(Width))
- H = int(round(Height))
- if W == Width and H == Height:
- Width, Height = W, H
- except (ValueError):
- pass
- else:
- Result = (Width, Height)
- if Result is None:
- # A and B are ISO 216, C is ISO 269, D, E, F and G are SIS 014711 and
- # H is a logical extension of SIS 014711.
- if len(Name) >= 2:
- Series = Name[0]
- PSS = 'AECGBFDH'
- try:
- n = int(Name[1:])
- except (ValueError):
- n = -1
- else:
- if not (0 <= n <= 10):
- n = -1
- if Series in PSS and n >= 0:
- x0 = 1000.0 * 2.0 ** (PSS.index(Series) / 16.0)
- x = x0 / (2.0 ** (0.25 * (2 * n - 1)))
- Height = int(floor(x + 0.2))
- Width = int(floor(x / sqrt(2.0) + 0.2))
- Result = (Width, Height)
- if Result is None:
- raise Error('Unrecognised paper size: "' + str(Name) + '"')
- return Result
- #-------------------------------------------------------------------------------
- def HatViewsSVG(HatSliceMetrics, Mode=None, ImageDim=None, Padding=None):
- u'''Generate SVG markup for views of a pith helmet.
- Four wire-frame views are rendered: One isometric view (viewed from
- front-left-up) and three orthographic views. These views should be
- enough to determine if a particular helmet shape is acceptable.
- Mode:
- 0: Polygon model: Scaled to preserve the crown circumference (default)
- 1: Ideal: Smooth crown and brim curves
- 2: Polygon and ideal shapes superimposed
- ImageDim, if supplied, indicates the physical size of the image in
- millimetres as a (Width, Height) pair, usually the printable area for
- some particular paper size. By default the area is 287mm × 200mm, (A4
- in lanscape mode short by 5mm from each edge).
- Padding is the internal margin in millimetres from each edge of the image.
- By default, the padding is zero.
- '''
- #-----------------------------------------------------------------------------
- # Shorthand
- A = Pt_Anchor
- C = Pt_Control
- B = (Pt_Break, None)
- PathColourStr = '#0bf'
- PolyColourStr = '#e00'
- #-----------------------------------------------------------------------------
- def VStr(Vector, Scale=1.0):
- u'''Return a vector string, limited to two decimal places.'''
- return ', '.join((MaxDP(Scale * x, 2) for x in Vector))
- #-----------------------------------------------------------------------------
- def TxPath(IT, AM, Shape, Attributes=None):
- u'''Return an SVG path for a transformed Shape.'''
- return SVGPath(IT, ShapeDim(TransformedShape(AM, Shape), 2), Attributes)
- #-----------------------------------------------------------------------------
- def HatMarkup(IT, AM, HatSliceMetrics, Mode):
- u'''Return SVG markup for a helmet subject to a transformation.
- Mode:
- 0: Polygon model: Scaled to preserve the crown circumference.
- 1: Ideal: Smooth crown and brim curves.
- 2: Polygon and ideal shapes superimposed.
- '''
- HSM = HatSliceMetrics
- HM = HSM.HatMetrics
- CM = HM.CrownMetrics
- if Mode == 2:
- PathCS = PathColourStr
- PolyCS = PolyColourStr
- else:
- PathCS = 'black'
- PolyCS = 'black'
- Result = ''
- if Mode in [1, 2]:
- Result += IT('<!-- Ideal helmet shape -->')
- Result += IT('<!-- Crown -->')
- Result += TxPath(IT, PlotAM, CM.CrownPath, {'stroke': PathCS})
- Result += IT('<!-- Brim -->')
- Result += TxPath(IT, PlotAM, HM.BrimPath, {'stroke': PathCS})
- for i in range(2 * HSM.NumSlices):
- Result += IT('<!-- Radial at ' + HSM.RadialNames[i] + ' -->')
- S = HM.RadialSlice3D(HSM.Angles[i])
- Result += TxPath(IT, PlotAM, S, {'stroke': PathCS})
- if Mode != 1:
- Result += IT('<!-- Helmet shape with polygon scale correction -->')
- Result += IT('<!-- Crown -->')
- S = ShapeFromVertices(HSM.Radials + HSM.Radials[:1], 1)
- Result += TxPath(IT, PlotAM, S, {'stroke': PolyCS})
- Result += IT('<!-- Brim -->')
- S = ShapeFromVertices(HSM.RadialBrimPoints + HSM.RadialBrimPoints[:1], 1)
- Result += TxPath(IT, PlotAM, S, {'stroke': PolyCS})
- for i in range(2 * HSM.NumSlices):
- Result += IT('<!-- Radial at ' + HSM.RadialNames[i] + ' -->')
- S = HSM.RadialSlice3D(i)
- Result += TxPath(IT, PlotAM, S, {'stroke': PolyCS})
- return Result
- #-----------------------------------------------------------------------------
- Title = 'Pith Helmet Views'
- HSM = HatSliceMetrics
- HM = HSM.HatMetrics
- CM = HM.CrownMetrics
- if ImageDim is None:
- ID = (28.7, 20.0)
- else:
- ID = VScaled(ImageDim, 0.1)
- PrintWidthStrMM = MaxDP(10.0 * ID[0], 3)
- PrintHeightStrMM = MaxDP(10.0 * ID[1], 3)
- PrintWidthStr = MaxDP(ID[0], 4)
- PrintHeightStr = MaxDP(ID[1], 4)
- IsPortraitAspect = ID[0] < ID[1]
- ID = (float(PrintWidthStr), float(PrintHeightStr))
- if IsPortraitAspect:
- ID = (ID[1], ID[0])
- GD = VSum(ID, VScaled(VOnes(2), -0.2 * Padding))
- Scale = min(2.0, min(GD[0], GD[1]) / 20.0)
- BaseY = HSM.Bottom + HSM.BaseHeight
- QHeight = 0.5 * (GD[1] - Scale * 1.0)
- QWidth = QHeight * GD[0] / GD[1]
- QScale = QHeight / GD[1]
- FQExtent = (QScale * HM.RadialSlice2D(0.0)[-1][1][0]) / QWidth
- RQExtent = (QScale * HM.RadialSlice2D(pi)[-1][1][0]) / QWidth
- QGroupCentre = (0.5 * GD[0], GD[1] - QHeight)
- QCentres = []
- QOffsets = []
- for y in (-0.5, (GD[1] - 0.2 * Scale - BaseY) / GD[1]):
- for x in (-0.5, 0.5 + 0.5 * (FQExtent - RQExtent)):
- QOffsets.append((x, y))
- QOffsets[0] = (-0.5, -0.45)
- for QOffset in QOffsets:
- QCentres.append(
- VSum(QGroupCentre, VSchur(QOffset, (QWidth, QHeight)))
- )
- IT = tIndentTracker(' ')
- Result = SVGStart(IT, Title, {
- 'width': PrintWidthStrMM + 'mm',
- 'height': PrintHeightStrMM + 'mm',
- 'viewBox': '0 0 ' + PrintWidthStr + ' ' + PrintHeightStr,
- 'preserveAspectRatio': 'xMidYMid slice'
- })
- # Background
- Result += IT(
- '<!-- Background -->',
- '<rect x="0" y="0" width="' +
- PrintWidthStr +'" height="' + PrintHeightStr +
- '" stroke="none" fill="white"/>'
- )
- # Outer group
- OuterGroupAttrs = {
- 'fill': 'none', 'stroke': 'black', 'stroke-width': '0.05'
- }
- OGXforms = []
- if IsPortraitAspect:
- OGXforms.append('translate(' + MaxDP(ID[1], 4) + ', 0) rotate(90)')
- if Padding != 0:
- PaddingStr = MaxDP(0.1 * Padding, 4)
- OGXforms.append('translate(' + PaddingStr + ', ' + PaddingStr + ')')
- if len(OGXforms) > 0:
- OuterGroupAttrs['transform'] = ' '.join(OGXforms)
- Result += IT('<!-- Outer group -->')
- Result += SVGGroup(IT, OuterGroupAttrs)
- # Title and metrics
- TBlock = ''
- TBlock += IT('<!-- Title group -->')
- TBlock += SVGGroup(IT, {
- 'fill': 'black',
- 'stroke': 'none',
- 'font-family': 'sans-serif',
- 'font-size': MaxDP(0.36 * Scale, 2)
- })
- TBlock += IT('<!-- Title -->')
- TBlock += SVGText(IT,
- VScaled((0.2, 0.75), Scale),
- Title,
- {'font-size': MaxDP(0.72 * Scale, 2), 'font-weight': 'bold'}
- )
- TBlock += IT('<!-- Metrics -->')
- TBlock += (
- SVGText(IT, VScaled((0.2, 1.5), Scale),
- 'Crown Circumference: ' + MaxDP(CM.Circumference, 1) + 'cm') +
- SVGText(IT, VScaled((0.2, 2.0), Scale),
- 'Crown Aspect Ratio: ' + MaxDP(CM.CrownAspect, 2)) +
- SVGText(IT, VScaled((0.2, 2.5), Scale),
- 'Forehead Ratio: ' + MaxDP(CM.ForeheadRatio, 2)) +
- SVGText(IT, VScaled((0.2, 3.0), Scale),
- 'Front Aspect: ' + MaxDP(CM.FrontAspect, 2)) +
- SVGText(IT, VScaled((0.2, 3.5), Scale),
- 'Rear Aspect: ' + MaxDP(CM.RearAspect, 2)) +
- SVGText(IT, VScaled((0.2, 4.0), Scale),
- 'Dome Aspect: ' + MaxDP(HM.DomeAspect, 2))
- )
- TBlock += SVGGroupEnd(IT)
- Result += TBlock
- # Quadrant dividers
- x, y = QGroupCentre
- Result += IT('<!-- Quadrant dividers -->')
- Result += SVGPath(IT, [
- (A, (x - QWidth, y)), (A, (x + QWidth, y)), B,
- (A, (x, y - QHeight)), (A, (x, y + QHeight))
- ])
- # Upper-left quadrant: Isometric view
- QBlock = ''
- QBlock += IT('<!-- Upper-left quadrant: Isometric view -->')
- QBlock += SVGGroup(IT, {
- 'transform': 'translate(' + VStr(QCentres[0]) +') ' +
- 'scale(' + MaxDP(0.9 * QScale, 4) + ')'
- })
- PlotAM = tAffineMtx(
- (0.0, 0.0, 0.0),
- (
- (-0.5 * sqrt(2.0), sqrt(1.0 / 6.0), -sqrt(3.0) / 3.0),
- (-0.5 * sqrt(2.0), -sqrt(1.0 / 6.0), sqrt(3.0) / 3.0),
- (0.0, sqrt(2.0 / 3.0), sqrt(3.0) / 3.0)
- )
- )
- QBlock += HatMarkup(IT, PlotAM, HSM, Mode)
- QBlock += SVGGroupEnd(IT)
- Result += QBlock
- # Upper-right quadrant: Top view
- QBlock = ''
- QBlock += IT('<!-- Upper-right quadrant: Top view -->')
- QBlock += SVGGroup(IT, {
- 'transform': 'translate(' + VStr(QCentres[1]) +') ' +
- 'scale(' + MaxDP(QScale, 4) + ')'
- })
- PlotAM = tAffineMtx(
- (0.0, 0.0, 0.0),
- ((-1.0, 0.0, 0.0), (0.0, -1.0, 0.0), (0.0, 0.0, 1.0))
- )
- if False:
- QBlock += IT('<!-- Centre cross -->')
- QBlock += TxPath(IT, PlotAM,
- [(A, (12, 0)), (A, (-12, 0)), B, (A, (0, -8)), (A, (0, 8)), B],
- {'stroke': 'rgba(255,0,0,0.7)'}
- )
- S = ShapeFromVertices(HSM.RingCutPoints + HSM.RingCutPoints[:1], 1)
- QBlock += IT('<!-- Bracing ring -->')
- QBlock += TxPath(IT,
- PlotAM, S, {'stroke-width': '0.025', 'stroke-dasharray': '0.1 0.3'}
- )
- QBlock += HatMarkup(IT, PlotAM, HSM, Mode)
- QBlock += SVGGroupEnd(IT)
- Result += QBlock
- # Lower-left quadrant: Front view
- QBlock = ''
- QBlock += IT('<!-- Lower-left quadrant: Front view -->')
- QBlock += SVGGroup(IT, {
- 'transform': 'translate(' + VStr(QCentres[2]) +') ' +
- 'scale(' + MaxDP(QScale, 4) + ')'
- })
- PlotAM = tAffineMtx(
- (0.0, 0.0, 0.0),
- ((0.0, 0.0, -1.0), (-1.0, 0.0, 0.0), (0.0, 1.0, 0.0))
- )
- QBlock += HatMarkup(IT, PlotAM, HSM, Mode)
- QBlock += SVGGroupEnd(IT)
- Result += QBlock
- # Lower-right quadrant: Left side view
- QBlock = ''
- QBlock += IT('<!-- Lower-right quadrant: Left side view -->')
- QBlock += SVGGroup(IT, {
- 'transform': 'translate(' + VStr(QCentres[3]) +') ' +
- 'scale(' + MaxDP(QScale, 4) + ')'
- })
- PlotAM = tAffineMtx(
- (0.0, 0.0, 0.0),
- ((-1.0, 0.0, 0.0), (0.0, 0.0, 1.0), (0.0, 1.0, 0.0))
- )
- QBlock += HatMarkup(IT, PlotAM, HSM, Mode)
- QBlock += SVGGroupEnd(IT)
- Result += QBlock
- # End of outer group and SVG
- Result += SVGGroupEnd(IT) + SVGEnd(IT)
- return Result
- #-------------------------------------------------------------------------------
- def HatSliceSVG(HatSliceMetrics, PageNumber, ImageDim=None, Padding=None):
- u'''Generate SVG markup for one slice of a former for a pith helmet.
- Page number ranges from 1 to the number of slices. When all the slices
- are printed, cut out, folded, assembled and braced with fences at the
- top and bottom hubs and a ring at the bottom, the former is complete.
- The helmet is made by lofting material over the former.
- ImageDim, if supplied, indicates the physical size of the image in
- millimetres as a (Width, Height) pair, usually the printable area for
- some particular paper size. By default the area is 287mm × 200mm, (A4
- in lanscape mode short by 5mm from each edge).
- Padding is the internal margin in millimetres from each edge of the image.
- By default, the padding is zero.
- '''
- #-----------------------------------------------------------------------------
- # Shorthand
- A = Pt_Anchor
- C = Pt_Control
- B = (Pt_Break, None)
- ftFlat = 0
- ftMountain = 2
- ftValley = 1
- #-----------------------------------------------------------------------------
- def TxPath(IT, AM, Shape, Attributes=None):
- u'''Return an SVG path for a transformed Shape.'''
- return SVGPath(IT, ShapeDim(TransformedShape(AM, Shape), 2), Attributes)
- #-----------------------------------------------------------------------------
- if PageNumber < 1 or PageNumber > HatSliceMetrics.NumSlices:
- raise Error('Invalid page number %s.' % (str(PageNumber)))
- HSM = HatSliceMetrics
- HM = HSM.HatMetrics
- CM = HM.CrownMetrics
- Title = 'Pith Helmet Slice %d/%d' % (PageNumber, HSM.NumSlices)
- if ImageDim is None:
- ID = (28.7, 20.0)
- else:
- ID = VScaled(ImageDim, 0.1)
- PrintWidthStrMM = MaxDP(10.0 * ID[0], 3)
- PrintHeightStrMM = MaxDP(10.0 * ID[1], 3)
- PrintWidthStr = MaxDP(ID[0], 4)
- PrintHeightStr = MaxDP(ID[1], 4)
- IsPortraitAspect = ID[0] < ID[1]
- ID = (float(PrintWidthStr), float(PrintHeightStr))
- if IsPortraitAspect:
- ID = (ID[1], ID[0])
- GD = VSum(ID, VScaled(VOnes(2), -0.2 * Padding))
- Scale = min(2.0, min(GD[0], GD[1]) / 20.0)
- SliceIndex = PageNumber - 1
- IsSaggital = SliceIndex < len(HSM.Saggitals)
- if IsSaggital:
- SliceSetName = 'Saggital' if IsSaggital else 'Coronal'
- SliceIxInSet = SliceIndex
- SliceSetSize = len(HSM.Saggitals)
- else:
- SliceSetName = 'Coronal'
- SliceIxInSet = SliceIndex - len(HSM.Saggitals)
- SliceSetSize = len(HSM.Coronals)
- RIx1, RIx2 = (HSM.Saggitals + HSM.Coronals)[SliceIndex]
- AM = AffineMtxTS((0.0, 0.0), (-1.0, 1.0))
- R1 = TransformedShape(AM, HSM.RadialSlice2D(RIx1))
- R2 = HSM.RadialSlice2D(RIx2)
- x1 = R1[-1][1][0]
- x2 = R2[-1][1][0]
- Span = x2 - x1
- BaseY = HSM.Bottom + HSM.BaseHeight
- MiddleY = HSM.Top + 0.5 * (BaseY - HSM.Top)
- MiddleCutY = HSM.Top + 0.3 * (BaseY - HSM.Top)
- AxisOffset = -0.5 * Span - x1
- Origin = (0.5 * GD[0] + AxisOffset, GD[1] - 0.2 * Scale - BaseY)
- OffsetStr = MaxDP(Origin[0], 2) + ', ' + MaxDP(Origin[1], 2)
- CrownX1 = R1[-7][1][0]
- CrownX2 = R2[-7][1][0]
- RingX1 = -VLength(HSM.RingCutPoints[RIx1])
- RingX2 = VLength(HSM.RingCutPoints[RIx2])
- NR1 = VNormalised(HSM.Radials[RIx1])
- NR2 = VNormalised(HSM.Radials[RIx2])
- FoldAngle = pi - acos(max(-1.0, min(1.0, VDot(NR1, NR2))))
- FoldType = ftValley if VDot(VPerp(NR2), NR1) > 0 else ftMountain
- FoldAngleStr = MaxDP(degrees(FoldAngle), 0)
- if FoldAngleStr == '0':
- FoldStr = ''
- FoldType = ftFlat
- else:
- FoldStr = 'Valley' if FoldType == ftValley else 'Mountain'
- FoldStr += ' Fold by ' + FoldAngleStr + u'°'
- SliceOutline = list(reversed(R1))[:-1] + R2 + [
- (A, (x2, BaseY)), (A, (x1, BaseY)), R1[-1]
- ]
- IT = tIndentTracker(' ')
- Result = SVGStart(IT, Title, {
- 'width': PrintWidthStrMM + 'mm',
- 'height': PrintHeightStrMM + 'mm',
- 'viewBox': '0 0 ' + PrintWidthStr + ' ' + PrintHeightStr,
- 'preserveAspectRatio': 'xMidYMid slice'
- })
- Result += IT('<defs>')
- IT.StepIn()
- Result += IT(
- '<marker id="ArrowHead"',
- ' viewBox="0 0 10 10" refX="0" refY="5"',
- ' markerUnits="strokeWidth"',
- ' markerWidth="8" markerHeight="6"',
- ' orient="auto">',
- ' <path d="M 0,0 L 10,5 L 0,10 z"/>',
- '</marker>'
- )
- # More marker, symbol and gradient definitions can go here.
- IT.StepOut()
- Result += IT('</defs>')
- # Background
- Result += IT(
- '<!-- Background -->',
- '<rect x="0" y="0" width="' +
- PrintWidthStr +'" height="' + PrintHeightStr +
- '" stroke="none" fill="white"/>'
- )
- # Outer group
- OuterGroupAttrs = {
- 'fill': 'none', 'stroke': 'black', 'stroke-width': '0.05'
- }
- OGXforms = []
- if IsPortraitAspect:
- OGXforms.append('translate(' + MaxDP(ID[1], 4) + ', 0) rotate(90)')
- if Padding != 0:
- PaddingStr = MaxDP(0.1 * Padding, 4)
- OGXforms.append('translate(' + PaddingStr + ', ' + PaddingStr + ')')
- if len(OGXforms) > 0:
- OuterGroupAttrs['transform'] = ' '.join(OGXforms)
- Result += IT('<!-- Outer group -->')
- Result += SVGGroup(IT, OuterGroupAttrs)
- # Border
- if False:
- Result += IT('<!-- Page border -->')
- Result += SVGPath(IT, [
- (Pt_Anchor, (0, 0)), (Pt_Anchor, (28, 0)),
- (Pt_Anchor, (28, 19)), (Pt_Anchor, (0, 19)),
- (Pt_Anchor, (0, 0))
- ])
- # Title and metrics
- TBlock = ''
- TBlock += IT('<!-- Title group -->')
- TBlock += SVGGroup(IT, {
- 'fill': 'black',
- 'stroke': 'none',
- 'font-family': 'sans-serif',
- 'font-size': MaxDP(0.36 * Scale, 2)
- })
- TBlock += IT('<!-- Title -->')
- TBlock += SVGText(IT,
- VScaled((0.2, 0.75), Scale),
- Title,
- {'font-size': MaxDP(0.72 * Scale, 2), 'font-weight': 'bold'}
- )
- TBlock += IT('<!-- Metrics -->')
- TBlock += (
- SVGText(IT, VScaled((0.2, 1.5), Scale),
- 'Crown Circumference: ' + MaxDP(CM.Circumference, 1) + 'cm') +
- SVGText(IT, VScaled((0.2, 2.0), Scale),
- 'Crown Aspect Ratio: ' + MaxDP(CM.CrownAspect, 2)) +
- SVGText(IT, VScaled((0.2, 2.5), Scale),
- 'Forehead Ratio: ' + MaxDP(CM.ForeheadRatio, 2)) +
- SVGText(IT, VScaled((0.2, 3.0), Scale),
- 'Front Aspect: ' + MaxDP(CM.FrontAspect, 2)) +
- SVGText(IT, VScaled((0.2, 3.5), Scale),
- 'Rear Aspect: ' + MaxDP(CM.RearAspect, 2)) +
- SVGText(IT, VScaled((0.2, 4.0), Scale),
- 'Dome Aspect: ' + MaxDP(HM.DomeAspect, 2))
- )
- # End of title group
- TBlock += SVGGroupEnd(IT)
- # Slice group
- SBlock = ''
- SBlock += IT('<!-- Slice -->')
- SBlock += SVGGroup(IT,
- {'transform': 'translate(' + OffsetStr + ')'}
- )
- FoldClearance = 0.3
- if IsSaggital:
- MiddleCutStart = HSM.Top
- MiddleCutEnd = MiddleCutY
- FenceCutStart = BaseY
- FenceCutEnd = BaseY - HSM.FenceCutLength
- FoldStart = FenceCutEnd - FoldClearance
- FoldEnd = MiddleCutEnd + FoldClearance
- else:
- MiddleCutStart = BaseY
- MiddleCutEnd = MiddleCutY
- FenceCutStart = HSM.Top
- FenceCutEnd = HSM.Top + HSM.FenceCutLength
- FoldStart = FenceCutEnd + FoldClearance
- FoldEnd = MiddleCutEnd - FoldClearance
- # Outline and cuts
- SBlock += IT('<!-- Slice outline -->')
- SBlock += SVGPath(IT, SliceOutline)
- SBlock += IT('<!-- Ring and fence cuts -->')
- SBlock += SVGPath(IT, [
- (A, (RingX1, BaseY)), (A, (RingX1, BaseY - HSM.RingCutLength)), B,
- (A, (RingX2, BaseY)), (A, (RingX2, BaseY - HSM.RingCutLength)), B,
- (A, (0.0, MiddleCutStart)), (A, (0.0, MiddleCutEnd)), B,
- (A, (0.0, FenceCutStart)), (A, (0.0, FenceCutEnd))
- ], {'stroke-width': '0.1', 'stroke-linecap': 'butt'})
- # Fold line
- if FoldType in [ftMountain, ftValley]:
- if FoldType == ftValley:
- FoldAttrs = {'stroke-width': '0.025', 'stroke-dasharray': '0.2 0.17'}
- else:
- FoldAttrs = {'stroke-width': '0.025'}
- SBlock += IT('<!-- Fold line -->')
- SBlock += SVGPath(IT, [
- (A, (0.0, FoldStart)), (A, (0.0, FoldEnd))
- ], FoldAttrs)
- # Names of radials
- ABlock = ''
- ABlock += IT('<!-- Names of radials -->')
- ABlock += SVGGroup(IT, {
- 'font-family': 'sans-serif',
- 'font-size': MaxDP(min(2.0, 0.15 * Span), 1),
- 'fill': 'black',
- 'stroke': 'none'
- })
- y = BaseY - 2.0 * HSM.RingCutLength
- ABlock += SVGText(IT,
- (CrownX1, y),
- HSM.RadialNames[RIx1],
- {'text-anchor': 'start'}
- )
- ABlock += SVGText(IT,
- (CrownX2, y),
- HSM.RadialNames[RIx2],
- {'text-anchor': 'end'}
- )
- ABlock += SVGGroupEnd(IT)
- # Crown level
- CLScale = 0.055 * (BaseY - HSM.Top)
- ABlock += IT('<!-- Crown level -->')
- ABlock += SVGGroup(IT, {
- 'stroke-width': MaxDP(0.2 * CLScale, 4),
- 'stroke-linecap': 'butt',
- 'marker-end': 'url(#ArrowHead)'
- })
- ABlock += SVGPath(IT, [
- (A, (CrownX1 + 2.0 * CLScale, 0.0)), (A, (CrownX1 + 1.23 * CLScale, 0.0)),
- ])
- ABlock += SVGPath(IT, [
- (A, (CrownX2 - 2.0 * CLScale, 0.0)), (A, (CrownX2 - 1.23 * CLScale, 0.0))
- ])
- ABlock += SVGGroupEnd(IT)
- x = 0.75 * CrownX1
- s = max(0.2, min(0.8, -0.08 * HSM.Top))
- # Metrics annotations
- MBlock = ''
- MBlock += IT('<!-- Metrics annotations -->')
- MBlock += SVGGroup(IT, {
- 'font-family': 'sans-serif',
- 'font-size': '0.85',
- 'fill': 'black',
- 'stroke': 'none',
- 'transform': 'translate(' + MaxDP(x, 2) + ', 0) ' +
- 'scale (' + MaxDP(s, 1) + ')'
- })
- MBlock += (
- SVGText(IT, (0, -6.0),
- 'Size: ' + MaxDP(CM.Circumference, 1) + 'cm') +
- SVGText(IT, (0, -5.0),
- 'Aspect: ' + MaxDP(CM.CrownAspect, 2)) +
- SVGText(IT, (0, -4.0),
- 'FR: ' + MaxDP(CM.ForeheadRatio, 2)) +
- SVGText(IT, (0, -3.0),
- 'FA: ' + MaxDP(CM.FrontAspect, 2)) +
- SVGText(IT, (0, -2.0),
- 'RA: ' + MaxDP(CM.RearAspect, 2)) +
- SVGText(IT, (0, -1.0),
- 'DA: ' + MaxDP(HM.DomeAspect, 2))
- )
- MBlock += SVGGroupEnd(IT)
- # Slice number annotation
- SliceSetStr = (SliceSetName + ' ' +
- str(SliceIxInSet + 1) + '/' + str(SliceSetSize))
- NBlock = ''
- NBlock += IT('<!-- Slice number annotation -->')
- NBlock += SVGGroup(IT, {
- 'font-family': 'sans-serif',
- 'font-size': MaxDP(1.0 * s, 2),
- 'fill': 'black',
- 'stroke': 'none'
- })
- NBlock += (
- SVGText(IT,
- (0.6 * CrownX2, -2.5 * s),
- 'Slice %d/%d' % (PageNumber, HSM.NumSlices),
- {'text-anchor': 'middle'}
- )
- )
- NBlock += (
- SVGText(IT,
- (0.6 * CrownX2, -1.5 * s),
- SliceSetStr,
- {'font-size': MaxDP(0.65 * s, 2), 'text-anchor': 'middle'}
- )
- )
- NBlock += SVGGroupEnd(IT)
- MBlock += NBlock
- if FoldStr != '':
- # Fold instruction
- MBlock += IT('<!-- Fold instruction -->')
- MBlock += SVGGroup(IT, {
- 'font-family': 'sans-serif',
- 'font-size': '0.85',
- 'fill': 'black',
- 'stroke': 'none',
- 'transform': 'rotate(90) scale (' + MaxDP(s, 1) + ')'
- })
- MBlock += (SVGText(IT,
- (MiddleY, -0.1 - 0.5 * s), FoldStr, {'text-anchor': 'middle'})
- )
- MBlock += SVGGroupEnd(IT)
- # End of slice
- SBlock += ABlock + MBlock + SVGGroupEnd(IT)
- # End of outer group and SVG
- Result += TBlock + SBlock + SVGGroupEnd(IT) + SVGEnd(IT)
- return Result
- #-------------------------------------------------------------------------------
- def HatRingSVG(HatSliceMetrics, ImageDim=None, Padding=None):
- u'''Generate SVG markup for the bracing ring for a pith helmet former.
- The ring is divided into segments so it can fit on the page. Fortunately,
- they are labelled with the names of the radials, two of which form each
- slice.
- As a bonus, the markup of the fences is included. The fences keep the
- floppy bits either side of the long cuts of the coronal and saggital
- slices in place. The fences are labelled with both the radials they keep
- in place and the span of radials straddled by the fence’s cut.
- ImageDim, if supplied, indicates the physical size of the image in
- millimetres as a (Width, Height) pair, usually the printable area for
- some particular paper size. By default the area is 287mm × 200mm, (A4
- in lanscape mode short by 5mm from each edge).
- Padding is the internal margin in millimetres from each edge of the image.
- By default, the padding is zero.
- '''
- #-----------------------------------------------------------------------------
- # Shorthand
- A = Pt_Anchor
- C = Pt_Control
- B = (Pt_Break, None)
- #-----------------------------------------------------------------------------
- def TxPath(IT, AM, Shape, Attributes=None):
- u'''Return an SVG path for a transformed Shape.'''
- return SVGPath(IT, ShapeDim(TransformedShape(AM, Shape), 2), Attributes)
- #-----------------------------------------------------------------------------
- HSM = HatSliceMetrics
- HM = HSM.HatMetrics
- CM = HM.CrownMetrics
- SVGTitle = 'Pith Helmet Ring Segments and Fences'
- RingTitle = 'Pith Helmet Ring Segments'
- FencesTitle = 'Fences'
- if ImageDim is None:
- ID = (28.7, 20.0)
- else:
- ID = VScaled(ImageDim, 0.1)
- PrintWidthStrMM = MaxDP(10.0 * ID[0], 3)
- PrintHeightStrMM = MaxDP(10.0 * ID[1], 3)
- PrintWidthStr = MaxDP(ID[0], 4)
- PrintHeightStr = MaxDP(ID[1], 4)
- IsPortraitAspect = ID[0] < ID[1]
- ID = (float(PrintWidthStr), float(PrintHeightStr))
- if IsPortraitAspect:
- ID = (ID[1], ID[0])
- GD = VSum(ID, VScaled(VOnes(2), -0.2 * Padding))
- Scale = min(2.0, min(GD[0], GD[1]) / 20.0)
- VSpan = GD[1] - 1.4 * Scale
- N = len(HSM.RingSegments)
- StripHeight = HSM.RingCutLength / 0.45
- TotalStripHeight = N * StripHeight
- Spacing = min(1.0, (VSpan - TotalStripHeight) / max(1e-6, (N - 1)))
- TopPadding = min(
- 1.5 * Scale,
- 0.5 * (VSpan - TotalStripHeight - (N - 1) * Spacing)
- )
- LabelFontSize = 0.3 * HSM.RingCutLength
- LabelYOffset = StripHeight - 0.5 * HSM.RingCutLength + 0.5 * LabelFontSize
- IT = tIndentTracker(' ')
- Result = SVGStart(IT, SVGTitle, {
- 'width': PrintWidthStr + 'cm',
- 'height': PrintHeightStr + 'cm',
- 'viewBox': '0 0 ' + PrintWidthStr + ' ' + PrintHeightStr,
- 'preserveAspectRatio': 'xMidYMid slice'
- })
- # Background
- Result += IT(
- '<!-- Background -->',
- '<rect x="0" y="0" width="' +
- PrintWidthStr +'" height="' + PrintHeightStr +
- '" stroke="none" fill="white"/>'
- )
- # Outer group
- OuterGroupAttrs = {
- 'fill': 'none', 'stroke': 'black', 'stroke-width': '0.05'
- }
- OGXforms = []
- if IsPortraitAspect:
- OGXforms.append('translate(' + MaxDP(ID[1], 4) + ', 0) rotate(90)')
- if Padding != 0:
- PaddingStr = MaxDP(0.1 * Padding, 4)
- OGXforms.append('translate(' + PaddingStr + ', ' + PaddingStr + ')')
- if len(OGXforms) > 0:
- OuterGroupAttrs['transform'] = ' '.join(OGXforms)
- Result += IT('<!-- Outer group -->')
- Result += SVGGroup(IT, OuterGroupAttrs)
- # Title and metrics
- TBlock = ''
- TBlock += IT('<!-- Title group -->')
- TBlock += SVGGroup(IT, {
- 'fill': 'black',
- 'stroke': 'none',
- 'font-family': 'sans-serif',
- 'font-size': MaxDP(0.36 * Scale, 2)
- })
- TBlock += IT('<!-- Title -->')
- TBlock += SVGText(IT,
- VScaled((0.2, 0.75), Scale),
- RingTitle,
- {'font-size': MaxDP(0.72 * Scale, 2), 'font-weight': 'bold'}
- )
- TBlock += SVGText(IT,
- (GD[0] - 0.2 * Scale, 0.75 * Scale),
- FencesTitle,
- {'font-size': MaxDP(0.72 * Scale, 2),
- 'font-weight': 'bold', 'text-anchor': 'end'}
- )
- x = 0.63 * GD[0]
- TBlock += IT('<!-- Metrics -->')
- TBlock += (
- SVGText(IT, (x - 3.2 * Scale, 0.45 * Scale),
- 'Size: ' + MaxDP(CM.Circumference, 1) + 'cm') +
- SVGText(IT, (x - 3.2 * Scale, 0.85 * Scale),
- 'Aspect: ' + MaxDP(CM.CrownAspect, 2)) +
- SVGText(IT, (x, 0.45 * Scale),
- 'FA: ' + MaxDP(CM.FrontAspect, 2)) +
- SVGText(IT, (x, 0.85 * Scale),
- 'FR: ' + MaxDP(CM.ForeheadRatio, 2)) +
- SVGText(IT, (x + 2.4 * Scale, 0.45 * Scale),
- 'RA: ' + MaxDP(CM.RearAspect, 2)) +
- SVGText(IT, (x + 2.4 * Scale, 0.85 * Scale),
- 'DA: ' + MaxDP(HM.DomeAspect, 2))
- )
- # End of title group
- TBlock += SVGGroupEnd(IT)
- # Ring segments
- RBlock = ''
- SegmentY = 1.1 + TopPadding
- SegmentRightExtents = []
- for SegIx, Segment in enumerate(HSM.RingSegments):
- SBlock = ''
- CutY = StripHeight - HSM.RingCutLength
- FoldY = 0.5 * CutY
- SBlock += IT('<!-- Segment ' + str(SegIx + 1) + ' -->')
- SBlock += SVGGroup(IT,
- {'transform': 'translate(0.2, ' + MaxDP(SegmentY, 3) +')'}
- )
- TabIx = (Segment[0] - 1) % len(HSM.RingCutPoints)
- TabV = VDiff(HSM.RingCutPoints[Segment[0]], HSM.RingCutPoints[TabIx])
- StartTabWidth = min(2.0, VLength(TabV))
- TabIx = (Segment[-1] + 1) % len(HSM.RingCutPoints)
- TabV = VDiff(HSM.RingCutPoints[TabIx], HSM.RingCutPoints[Segment[-1]])
- EndTabWidth = min(2.0, VLength(TabV))
- S = [
- (A, (0.0, StripHeight)), (A, (0.0, 0.0)),
- (A, (StartTabWidth, 0.0))
- ]
- FoldSpans = []
- PosLabels = []
- x = StartTabWidth
- i = 1
- while i < len(Segment):
- C1 = HSM.RingCutPoints[Segment[i - 1]]
- C2 = HSM.RingCutPoints[Segment[i]]
- SegV = VDiff(C2, C1)
- SegLength = VLength(SegV)
- C1 = VNormalised(C1)
- C2 = VNormalised(C2)
- if SegLength > 1e-6:
- SegDir = VScaled(SegV, 1.0 / SegLength)
- else:
- SegDir = VPerp(C1)
- ChamferAngle1 = max(0.0, 0.5 * pi - acos(-VDot(SegDir, C1)))
- ChamferAngle2 = max(0.0, 0.5 * pi - acos(VDot(SegDir, C2)))
- ChamferDist1 = FoldY * tan(ChamferAngle1)
- ChamferDist2 = FoldY * tan(ChamferAngle2)
- Chamfered = (SegLength - ChamferDist1 - ChamferDist2 >= 0.0 and
- max(ChamferDist1, ChamferDist2) < 2.0 * FoldY)
- if Chamfered:
- S += [
- (A, (x, CutY)), (A, (x, FoldY)),
- (A, (x + ChamferDist1, 0.0)),
- (A, (x + SegLength - ChamferDist2, 0.0)),
- (A, (x + SegLength, FoldY))
- ]
- FoldSpans.append((x, x + SegLength))
- else:
- S += [
- (A, (x, CutY)),
- (A, (x, 0.0)),
- (A, (x + SegLength, 0.0))
- ]
- PosLabels.append(((x, LabelYOffset), HSM.RadialNames[Segment[i - 1]]))
- x += SegLength
- i += 1
- PosLabels.append(((x, LabelYOffset), HSM.RadialNames[Segment[-1]]))
- S += [
- (A, (x, CutY)), (A, (x, 0.0)),
- (A, (x + EndTabWidth, 0.0)), (A, (x + EndTabWidth, StripHeight)),
- (A, (0.0, StripHeight))
- ]
- SBlock += IT('<!-- Outline -->')
- SBlock += SVGPath(IT, S)
- SegmentRightExtents.append(x + EndTabWidth)
- S = []
- for x1, x2 in FoldSpans:
- mx = 0.5 * (x1 + x2)
- cx1 = min(mx, x1 + 0.2)
- cx2 = max(mx, x2 - 0.2)
- S += [(A, (cx1, FoldY)), (A, (cx2, FoldY)), B]
- for Pos, Label in PosLabels:
- S += [
- (A, (Pos[0], CutY + 0.3)),
- (A, (Pos[0], StripHeight - 0.2)), B
- ]
- if len(S) > 0:
- SBlock += IT('<!-- Fold lines -->')
- SBlock += SVGPath(IT,
- S, {'stroke-width': '0.025', 'stroke-dasharray': '0.21 0.18'}
- )
- ABlock = ''
- ABlock += IT('<!-- Names of radials -->')
- ABlock += SVGGroup(IT, {
- 'font-family': 'sans-serif',
- 'font-size': MaxDP(LabelFontSize, 2),
- 'fill': 'black',
- 'stroke': 'none',
- 'text-anchor': 'middle'
- })
- for Pos, Label in PosLabels:
- ABlock += SVGText(IT, Pos, Label)
- ABlock += SVGGroupEnd(IT)
- # End of segment
- SBlock += ABlock + SVGGroupEnd(IT)
- RBlock += SBlock
- SegmentY += StripHeight + Spacing
- # Fences
- FencesBlock = ''
- LabelMargin = 0.1 * HSM.FenceCutLength
- for i in range(4):
- FBlock = ''
- FenceIsForTop = i < 2
- if FenceIsForTop:
- hw = 1.5 * HSM.FenceCutLength
- h = 2.2 * HSM.FenceCutLength
- else:
- hw = 2.0 * HSM.FenceCutLength
- h = 2.8 * HSM.FenceCutLength
- if FenceIsForTop == (SegmentRightExtents[0] > SegmentRightExtents[-1]):
- y = 1.1 * Scale + (h + 0.5 * Scale) * (i & 1)
- else:
- y = GD[1] - 0.2 * Scale - (h + 0.5 * Scale) * (i & 1) - h
- if FenceIsForTop:
- if i & 1 == 0:
- FencedIndices = (HSM.Saggitals[0][0], HSM.Saggitals[0][1])
- StraddleIndices = (HSM.Coronals[0][1], HSM.Coronals[-1][1])
- else:
- FencedIndices = (HSM.Saggitals[-1][1], HSM.Saggitals[-1][0])
- StraddleIndices = (HSM.Coronals[-1][0], HSM.Coronals[0][0])
- PosLabel = 'Top'
- PosLabelYOffset = 0.75 * HSM.FenceCutLength
- SLabelYOffset = 0.95 * h
- FLabelYOffset = SLabelYOffset - 1.2 * LabelFontSize
- else:
- hw = 2.0 * HSM.FenceCutLength
- h = 2.8 * HSM.FenceCutLength
- if i & 1 == 0:
- FencedIndices = (HSM.Coronals[0][0], HSM.Coronals[0][1])
- StraddleIndices = (HSM.Saggitals[-1][0], HSM.Saggitals[0][0])
- else:
- FencedIndices = (HSM.Coronals[-1][1], HSM.Coronals[-1][0])
- StraddleIndices = (HSM.Saggitals[0][-1], HSM.Saggitals[-1][1])
- PosLabel = 'Bottom'
- PosLabelYOffset = h - 0.375 * HSM.FenceCutLength
- SLabelYOffset = 1.1 * LabelFontSize
- FLabelYOffset = SLabelYOffset + 1.2 * LabelFontSize
- w = 2.0 * hw
- Pos = (GD[0] - 0.2 * Scale - hw, y)
- PosStr = MaxDP(Pos[0], 3) + ', ' + MaxDP(Pos[1], 3)
- FBlock += IT('<!-- Fence %d (%s) -->' % (i + 1, PosLabel))
- FBlock += SVGGroup(IT,
- {'transform': 'translate(' + PosStr + ')'}
- )
- if FenceIsForTop:
- FBlock += IT('<!-- Outline -->')
- FBlock += SVGPath(IT, [
- (A, (-hw, h)), (A, (-hw, 0.5 * hw)),
- (A, (-0.1 * hw, 0.0)), (A, (0.1 * hw, 0.0)),
- (A, (hw, 0.5 * hw)), (A, (hw, h)), (A, (-hw, h))
- ])
- FBlock += IT('<!-- Cut -->')
- FBlock += SVGPath(IT, [
- (A, (0.0, h)), (A, (0.0, HSM.FenceCutLength))
- ], {'stroke-width': '0.1', 'stroke-linecap': 'butt'})
- FBlock += IT('<!-- Fold line -->')
- FBlock += SVGPath(IT, [
- (A, (0.0, 0.05 * h)), (A, (0.0, HSM.FenceCutLength - 0.05 * h))
- ], {'stroke-width': '0.025', 'stroke-dasharray': '0.21 0.2'})
- else:
- FBlock += IT('<!-- Outline -->')
- FBlock += SVGPath(IT, [
- (A, (-hw, h)), (A, (-hw, 0.0)),
- (A, (hw, 0.0)), (A, (hw, h)), (A, (-hw, h))
- ])
- FBlock += IT('<!-- Cut -->')
- FBlock += SVGPath(IT, [
- (A, (0.0, 0.0)), (A, (0.0, h - HSM.FenceCutLength))
- ], {'stroke-width': '0.1', 'stroke-linecap': 'butt'})
- FBlock += IT('<!-- Fold line -->')
- FBlock += SVGPath(IT, [
- (A, (0.0, h - 0.05 * h)), (A, (0.0, h - HSM.FenceCutLength + 0.05 * h))
- ], {'stroke-width': '0.025', 'stroke-dasharray': '0.21 0.2'})
- ABlock = ''
- ABlock += IT('<!-- Annotations -->')
- ABlock += SVGGroup(IT, {
- 'font-family': 'sans-serif',
- 'font-size': MaxDP(LabelFontSize, 2),
- 'fill': 'black',
- 'stroke': 'none',
- })
- ABlock += IT('<!-- Top/Bottom label -->')
- ABlock += SVGText(IT,
- (0.0, PosLabelYOffset), PosLabel, {'text-anchor': 'middle'}
- )
- ABlock += IT('<!-- Fenced radials -->')
- ABlock += SVGText(IT,
- (-hw + LabelMargin, FLabelYOffset),
- HSM.RadialNames[FencedIndices[0]],
- {'text-anchor': 'start'}
- )
- ABlock += SVGText(IT,
- (hw - LabelMargin, FLabelYOffset),
- HSM.RadialNames[FencedIndices[1]],
- {'text-anchor': 'end'}
- )
- ABlock += IT('<!-- Range of straddled radials -->')
- ABlock += SVGText(IT,
- (-LabelMargin, SLabelYOffset),
- HSM.RadialNames[StraddleIndices[0]],
- {'text-anchor': 'end'}
- )
- ABlock += SVGText(IT,
- (LabelMargin, SLabelYOffset),
- HSM.RadialNames[StraddleIndices[1]],
- {'text-anchor': 'start'}
- )
- ABlock += SVGGroupEnd(IT)
- # End of fence
- FBlock += ABlock + SVGGroupEnd(IT)
- FencesBlock += FBlock
- # End of outer group and SVG
- Result += TBlock + RBlock + FencesBlock + SVGGroupEnd(IT) + SVGEnd(IT)
- return Result
- #-------------------------------------------------------------------------------
- # Main
- #-------------------------------------------------------------------------------
- def Main():
- #-----------------------------------------------------------------------------
- def HelpText(CmdName):
- Result = (u'Usage: ' + CmdName + ' [OPTION]...\n' +
- u'Creates plans for a former for a pith helmet over which ' +
- u'material is lofted.\n' +
- u'\n' +
- u'Examples:\n' +
- u' ' + CmdName + u' -s 59 --landscape --views-svg images/views.svg\n' +
- u' ' + CmdName + u' -s 52 -a 0.75 -n 7 --elliptical -o images/\n' +
- u' ' + CmdName + u' -s 70 -a 0.8 -n 12 --paper-size B4 -o .\n' +
- u' ' + CmdName + u' -s 48 -n 9 --pages 1,4-7,9,10 -o .\n' +
- u' ' + CmdName + u' -v -s 59 -a 0.71 -b 0.69 -f 0.65 -r 0.75 -d 1.1\n' +
- u'\n' +
- u'Options:\n' +
- u' -s, --size <size>\n' +
- u' sets the helmet crown circumference in centimetres.\n' +
- u' -a, --aspect <aspect>\n' +
- u' is the width of the widest part of the crown divided by ' +
- u'the length.\n' +
- u' -e, --elliptical\n' +
- u' sets --fr to 1 and --fa and --fr to the inverse of -a.\n' +
- u' -b, --fr, --forehead-ratio <ratio>\n' +
- u' is the width of forehead ellipse as a ratio of the rear width.\n' +
- u' -f, --fa, --front-aspect <ratio>\n' +
- u' is the elliptical ratio for the forehead curve. 0 is flat.\n' +
- u' -r, --ra, --rear-aspect <ratio>\n' +
- u' is the elliptical ratio for the rear curve. 0 is flat.\n' +
- u' -d, --da, --dome-aspect <ratio>\n' +
- u' is the height of the dome as a ratio of the crown’s ' +
- u'equivalent radius.\n' +
- u' -n, --num-slices <n>\n' +
- u' is the Number of pairs of radial spars over which ' +
- u'material is lofted.\n' +
- u' --views-mode <n>\n' +
- u' Rendering: 0 = polygonal, 1 = ideal, 2 = composite.\n' +
- u' --views-svg [path]<filename>\n' +
- u' is name of the orthogonal ans isometric views ' +
- u'SVG file to write.\n' +
- u' -o, --output <path>\n' +
- u' is where the slices and ring-and-fences SVGs ' +
- u'are to be written.\n' +
- u' -p, --pages <range[,range[,range...]]>\n' +
- u' is a list of page numbers or ranges of ' +
- u'helmet former SVGs to save.\n' +
- u' -m, --ps, --paper-size <name|W×H>\n' +
- u' is the a paper size name or W×H in millimetres.\n' +
- u' --margin <x>\n' +
- u' is the space in mm between an image edge and the paper’s edge.\n' +
- u' --padding <x>\n' +
- u' is the space in mm between an image edge and the inked area.\n' +
- u' --portrait\n' +
- u' selects the (default) higher-than-wide orientation.\n' +
- u' --landscape\n' +
- u' selects the wide-than-high orientation ' +
- u'for easy reading on a monitor.\n' +
- u' -v, --verbose\n' +
- u' displays detailed metrics and progress information.\n' +
- u'- q, --quiet\n' +
- u' suppresses all output.\n' +
- u' -h, --help\n' +
- u' summons this help page.\n'
- )
- return Result
- #-----------------------------------------------------------------------------
- Result = 0
- ErrMsg = ''
- try:
- P = ParamsFromCLArgs(sys.argv)
- Verbosity = P['Verbosity']
- if P['DoHelp']:
- print HelpText(sys.argv[0])
- if P['DoExecute']:
- CM = tCrownMetrics(
- P['CrownCircumference'],
- P['CrownAspect'],
- P['ForeheadRatio'],
- P['FrontAspect'],
- P['RearAspect']
- )
- HM = tHatMetrics(CM, P['DomeAspect'])
- HSM = tHatSliceMetrics(HM, P['NumSlices'], Verbosity)
- Correction = (HSM.HullScaleCorrection - 1.0) * 100.0
- CorrectionStr = MaxDP(Correction, 1) + '%'
- if Correction < 0.0:
- CorrectionStr = u'−' + CorrectionStr
- elif Correction > 0.0:
- CorrectionStr = '+' + CorrectionStr
- else:
- CorrectionStr = 'None'
- PaperSizeName = P['PaperSize']
- PaperArea = PaperSize(PaperSizeName)
- Margin = P['Margin']
- Padding = P['Padding']
- if Margin is None:
- Margin = round(max(2.5, min(10.0,
- min(PaperArea[0], PaperArea[1]) / 40.0)))
- if P['IsLandscape'] != (PaperArea[0] > PaperArea[1]):
- PaperArea = tuple(reversed(PaperArea))
- PrintArea = VSum(PaperArea, VScaled(VOnes(2), -2 * Margin))
- ContentArea = VSum(PrintArea, VScaled(VOnes(2), -2 * Padding))
- if min(ContentArea) < 5.0:
- raise OptError('The available image area is too small.')
- LandscapeCA = ContentArea
- if LandscapeCA[0] < LandscapeCA[1]:
- LandscapeCA = tuple(reversed(LandscapeCA))
- SafeLCA = VDiff(LandscapeCA, (4, 4))
- MaxSliceHeight = HSM.Bottom - HSM.Top + HSM.BaseHeight
- DoIssueSizeWarning = (10.0 * HSM.MaxSliceSpan > SafeLCA[0] or
- 10.0 * MaxSliceHeight > SafeLCA[1])
- if Verbosity >= 1:
- print ('Size: ' + MaxDP(CM.Circumference, 2) + 'cm')
- print ('Width: ' + MaxDP(CM.CrownWidth, 1) + 'cm')
- print ('Length: ' + MaxDP(CM.CrownLength, 1) + 'cm')
- print ('Aspect: ' + MaxDP(CM.CrownAspect, 2) + '')
- print ('FR, FA, RA: ' + MaxDP(CM.ForeheadRatio, 2) + ', ' +
- MaxDP(CM.FrontAspect, 2) + ', ' + MaxDP(CM.RearAspect, 2))
- print ('Dome aspect: ' + MaxDP(HM.DomeAspect, 2))
- if Verbosity >= 2:
- print ('Crown polygon scale correction: ' + CorrectionStr)
- print ('Maximum slice width: ' + MaxDP(HSM.MaxSliceSpan, 1) + 'cm')
- print ('Slice height: ' + MaxDP(MaxSliceHeight, 1) + 'cm')
- print ('Pages in the set: 1 to ' + str(HSM.NumSlices + 1))
- if Verbosity >= 2:
- print ('Paper area: ' + MaxDP(PaperArea[0], 3) + u'×' +
- MaxDP(PaperArea[1], 3) + u'mm²' + ((' including ' +
- MaxDP(Margin, 3) + 'mm margin from each edge')
- if Margin > 0 else ''))
- print ('Image area: ' + MaxDP(PrintArea[0], 3) + u'×' +
- MaxDP(PrintArea[1], 3) + u'mm²' + ((' including ' +
- MaxDP(Padding, 3) + 'mm padding from each edge')
- if Padding > 0 else ''))
- print ('Inked area: ' + MaxDP(ContentArea[0], 3) + u'×' +
- MaxDP(ContentArea[1], 3) + u'mm²')
- if Verbosity >= 1 and DoIssueSizeWarning:
- print ('The helmet plans may be too large for the available ' +
- 'print area on ' +
- PaperSizeName + ' paper.')
- # Orthographic and isometric views
- FileName = P['ViewsFileName']
- if FileName:
- if Verbosity >= 2:
- print 'Rendering orthographic and isometric views.'
- SVG = HatViewsSVG(HSM, P['ViewsMode'], PrintArea, Padding)
- if FileName == '-':
- try:
- sys.stdout.write(SVG.encode('utf-8'))
- except (IOError), E:
- raise FileError('Failed to write views SVG to standard output:' +
- '": IOError: ' + str(E))
- else:
- if Verbosity >= 2:
- print 'Saving views SVG to "' + FileName + '".'
- try:
- Save(SVG.encode('utf_8'), FileName)
- except (IOError), E:
- raise FileError('Cannot save views file "' + str(FileName) +
- '": IOError: ' + str(E))
- Path = P['OutputPath']
- PageNos = P['PageNumbers']
- # Slices and ring-plus-fences
- if Path and PageNos:
- if Path[-1] not in ['/', '\\']:
- Path = Path + '/'
- NP = HSM.NumSlices + 1
- for PageNo in PageNos:
- if PageNo == NP:
- PageType = 'ring segments and fences'
- FileName = Path + 'ring.svg'
- else:
- NS = len(HSM.Saggitals)
- NC = len(HSM.Coronals)
- if PageNo - 1 < len(HSM.Saggitals):
- SliceType = 'saggital %d/%d' % (PageNo, NS)
- else:
- SliceType = 'coronal %d/%d' % (PageNo - NS, NC)
- PageType = SliceType
- FileName = Path + ('slice%02d' % PageNo) + '.svg'
- if Verbosity >= 2:
- print (('Saving page %d/%d' % (PageNo, NP)) +
- ' (' + PageType + ') to "' + FileName + '".')
- if PageNo == NP:
- SVG = HatRingSVG(HSM, PrintArea, Padding)
- else:
- SVG = HatSliceSVG(HSM, PageNo, PrintArea, Padding)
- try:
- Save(SVG.encode('utf_8'), FileName)
- except (IOError), E:
- raise FileError('Cannot save plan file "' + str(FileName) +
- '": IOError: ' + str(E))
- if Verbosity >= 2:
- print 'Done!'
- except (OptError), E:
- ErrMsg = str(E)
- Result = 1
- except (FileError), E:
- ErrMsg = str(E)
- Result = 2
- except (Exception), E:
- exc_type, exc_value, exc_traceback = sys.exc_info()
- ErrLines = traceback.format_exc().splitlines()
- ErrMsg = 'Unhandled exception:\n' + '\n'.join(ErrLines)
- Result = 3
- if ErrMsg != '':
- print >> sys.stderr, sys.argv[0] + ': ' + ErrMsg
- return Result
- #-------------------------------------------------------------------------------
- # Command line trigger
- #-------------------------------------------------------------------------------
- if __name__ == '__main__':
- sys.exit(Main())
- #-------------------------------------------------------------------------------
- # End
- #-------------------------------------------------------------------------------
Add Comment
Please, Sign In to add comment