DIRECTORY Char, Convert, Feedback, FeedbackTypes, FileNames, GGBasicTypes, GGBoundBox, GGCoreOps, GGCoreTypes, GGFont, GGFromImager, GGHistoryTypes, GGInterfaceTypes, GGModelTypes, GGParseIn, GGParseOut, GGProps, GGScene, GGSegment, GGSegmentTypes, GGShapes, GGSlice, GGSliceOps, GGTransform, GGUtility, Imager, ImagerBox, ImagerFont, ImagerSys, ImagerTransformation, ImagerTypeface, IO, Lines2d, NodeStyle, PFS, Real, RealFns, Rope, SF, Vector2, Vectors2d; GGSliceImplB: CEDAR MONITOR -- changed to MONITOR because of scratchEdge -- IMPORTS Convert, Feedback, FileNames, GGBoundBox, GGCoreOps, GGFont, GGFromImager, GGParseIn, GGParseOut, GGProps, GGScene, GGSegment, GGShapes, GGSlice, GGSliceOps, GGTransform, GGUtility, Imager, ImagerBox, ImagerFont, ImagerSys, ImagerTransformation, ImagerTypeface, IO, Lines2d, PFS, Real, RealFns, Rope, Vector2, Vectors2d EXPORTS GGSlice = BEGIN BoundBox: TYPE = GGCoreTypes.BoundBox; BoundBoxObj: TYPE = GGCoreTypes.BoundBoxObj; Camera: TYPE = GGModelTypes.Camera; CameraObj: TYPE = GGModelTypes.CameraObj; Color: TYPE = Imager.Color; DefaultData: TYPE = GGModelTypes.DefaultData; DisplayStyle: TYPE = GGModelTypes.DisplayStyle; EditConstraints: TYPE = GGModelTypes.EditConstraints; ExtendMode: TYPE = GGModelTypes.ExtendMode; MsgRouter: TYPE = FeedbackTypes.MsgRouter; Font: TYPE = ImagerFont.Font; FontData: TYPE = GGFont.FontData; HistoryEvent: TYPE = GGHistoryTypes.HistoryEvent; Point: TYPE = GGBasicTypes.Point; PointGenerator: TYPE = GGModelTypes.PointGenerator; PointGeneratorObj: TYPE = GGModelTypes.PointGeneratorObj; PointPairGenerator: TYPE = GGModelTypes.PointPairGenerator; PointPairGeneratorObj: TYPE = GGModelTypes.PointPairGeneratorObj; PointWalkProc: TYPE = GGModelTypes.PointWalkProc; Scene: TYPE = GGModelTypes.Scene; Segment: TYPE = GGSegmentTypes.Segment; SegmentGenerator: TYPE = GGModelTypes.SegmentGenerator; SegmentGeneratorObj: TYPE = GGModelTypes.SegmentGeneratorObj; SelectedObjectData: TYPE = GGModelTypes.SelectedObjectData; SelectionClass: TYPE = GGSegmentTypes.SelectionClass; SelectMode: TYPE = GGModelTypes.SelectMode; Slice: TYPE = GGModelTypes.Slice; SliceClass: TYPE = GGModelTypes.SliceClass; SliceClassObj: TYPE = GGModelTypes.SliceClassObj; SliceDescriptor: TYPE = GGModelTypes.SliceDescriptor; SliceDescriptorObj: TYPE = GGModelTypes.SliceDescriptorObj; SliceObj: TYPE = GGModelTypes.SliceObj; SliceParts: TYPE = GGModelTypes.SliceParts; Transformation: TYPE = ImagerTransformation.Transformation; VEC: TYPE = Vector2.VEC; Vector: TYPE = GGBasicTypes.Vector; WalkProc: TYPE = GGModelTypes.WalkProc; jointSize: REAL = GGModelTypes.jointSize; halfJointSize: REAL = GGModelTypes.halfJointSize; hotJointSize: REAL = GGModelTypes.hotJointSize; halfHotJointSize: REAL = GGModelTypes.halfHotJointSize; Problem: PUBLIC SIGNAL [msg: Rope.ROPE] = Feedback.Problem; textMaxPoints: INTEGER = 12; textMaxEdges: INTEGER = 7; TextPoints: TYPE = REF TextPointsData; TextPointsData: TYPE = ARRAY [0..textMaxPoints) OF Point; -- see picture TextEdgeArray: TYPE = PACKED ARRAY [0..textMaxEdges) OF BOOL; -- left, top, right, bottom, baseline, centerline. TextPointArray: TYPE = PACKED ARRAY [0..textMaxPoints) OF BOOL; -- see picture. Used in text parts data. textPointRopes: ARRAY [0..textMaxPoints) OF Rope.ROPE = [ "lower left point", "base left point", "center left point", "upper left point", "lower mid point", "base mid point", "center mid point", "upper mid point", "lower right point", "base right point", "center right point", "upper right point" ]; textEdgeRopes: ARRAY [0..textMaxEdges) OF Rope.ROPE = [ "left edge", "top edge", "right edge", "bottom edge", "baseline", "horizontal center line", "vertical center line" ]; LeftRightAnchor: TYPE = {left, middle, right}; defaultLineSpacing: REAL = 1.2; TextData: TYPE = REF TextDataObj; TextDataObj: TYPE = RECORD [ box: BoundBox, -- not a bounding box but part of the text representation points: TextPoints, -- cache for "joints" leftRightAnchor: LeftRightAnchor, -- which point of the text remains fixed when the text is edited rope: Rope.ROPE, color: Imager.Color, fontData: FontData, trueFont: ImagerFont.Font, screenFont: ImagerFont.Font, displayStyle: DisplayStyle ¬ print, -- which of the fonts to use inverse: ImagerTransformation.Transformation, -- inverse of transform inverseScale: Vector, -- cached value of ImagerTransformation.Factor[inverse].s amplifySpace: REAL ¬ 1.0, -- because the Imager allows space correction (see Imager.mesa) dropShadowOn: BOOL ¬ FALSE, dropShadowOffset: Vector, shadowColor: Imager.Color, lineSpacing: REAL ¬ defaultLineSpacing, -- multiplicative factor on the font scale for line spacing after CR seg: Segment -- a null segment whose entire purpose is to contain properties in seg.props ]; TextParts: TYPE = REF TextPartsObj; TextPartsObj: TYPE = RECORD [ points: TextPointArray, -- which corners of text box are selected. edges: TextEdgeArray, -- which edges of text box are selected. includeText: BOOL -- FALSE => corners and edges only; no text ]; TextHitData: TYPE = REF TextHitDataObj; TextHitDataObj: TYPE = RECORD [ point: [-1..textMaxPoints), -- which point of box is hit. edge: [-1..textMaxEdges), -- which edge of box is hit. hitPoint: Point ]; FontNameError: PUBLIC SIGNAL = CODE; BuildTextSliceClass: PUBLIC PROC [] RETURNS [class: SliceClass] = { OPEN GGSlice; class ¬ NEW[SliceClassObj ¬ [ type: $Text, unlink: GGSlice.UnlinkSlice, -- textData doesn't need unlinking getBoundBox: TextGetBoundBox, getTransformedBoundBox: GGSlice.GenericTransformedBoundBox, getTightBox: TextGetTightBox, copy: TextCopy, restore: TextRestore, buildPath: NoOpBuildPath, drawBorder: NoOpDrawBorder, drawParts: TextDrawParts, drawTransform: TextDrawTransform, drawSelectionFeedback: TextDrawSelectionFeedback, drawAttractorFeedback: TextDrawAttractorFeedback, saveSelections: NoOpSaveSelections, remakeSelections: NoOpRemakeSelections, transform: TextTransform, describe: TextDescribe, describeHit: TextDescribeHit, fileout: TextFileout, filein: TextFilein, isEmptyParts: TextIsEmptyParts, isCompleteParts: TextIsCompleteParts, newParts: TextNewParts, unionParts: TextUnionParts, differenceParts: TextDiffParts, movingParts: TextMovingParts, augmentParts: TextAugmentParts, alterParts: NoOpAlterParts, setSelectedFields: NoOpSetSelectedFields, pointsInDescriptor: TextPointsInDescriptor, walkPointsInDescriptor: TextWalkPointsInDescriptor, pointPairsInDescriptor: TextPointPairsInDescriptor, segmentsInDescriptor: TextSegmentsInDescriptor, walkSegments: TextWalkSegments, nextPoint: TextNextPoint, nextPointPair: TextNextPointPair, nextSegment: TextNextSegment, closestPoint: TextClosestPoint, closestJointToHitData: TextClosestJointToHitData, closestPointAndTangent: NoOpClosestPointAndTangent, closestSegment: TextClosestSegment, filledPathsUnderPoint: NoOpFilledPathsUnderPoint, lineIntersection: NoOpLineIntersection, circleIntersection: NoOpCircleIntersection, hitDataAsSimpleCurve: NoOpHitDataAsSimpleCurve, setDefaults: TextSetDefaults, setStrokeWidth: NoOpSetStrokeWidth, getStrokeWidth: NoOpGetStrokeWidth, setStrokeEnd: NoOpSetStrokeEnd, getStrokeEnd: NoOpGetStrokeEnd, setStrokeJoint: NoOpSetStrokeJoint, getStrokeJoint: NoOpGetStrokeJoint, setStrokeColor: TextSetStrokeColor, getStrokeColor: TextGetStrokeColor, setFillColor: TextSetFillColor, getFillColor: TextGetFillColor, setArrows: NoOpSetArrows, getArrows: NoOpGetArrows, setDashed: NoOpSetDashed, getDashed: NoOpGetDashed, setOrientation: NoOpSetOrientation, getOrientation: NoOpGetOrientation ]]; }; MakeTextSlice: PUBLIC PROC [text: Rope.ROPE, color: Imager.Color, displayStyle: DisplayStyle, amplifySpace: REAL ¬ 1.0, dropShadowsOn: BOOL ¬ FALSE, dropShadowOffset: Vector ¬ [0.0, 0.0], shadowColor: Imager.Color ¬ Imager.black] RETURNS [slice: Slice] = { textData: TextData ¬ NEW[TextDataObj ¬ [ box: GGBoundBox.NullBoundBox[], -- filled in by call to TextSetBoundBox leftRightAnchor: left, inverseScale: [-1.0, -1.0], -- dummy vector amplifySpace: amplifySpace, points: NEW[TextPointsData], fontData: GGFont.CreateFontData[], -- local storage displayStyle: displayStyle, color: color, rope: text, dropShadowOn: dropShadowsOn, dropShadowOffset: dropShadowOffset, shadowColor: shadowColor, seg: GGSegment.MakeLine[p0: [-1.0, -1.0], p1: [1.0, 1.0], props: NIL] ]]; textData.seg.strokeWidth ¬ -1.0; -- so no legal stroke width matches slice ¬ NEW[SliceObj ¬ [ class: GGSlice.FetchSliceClass[$Text], data: textData, parent: NIL, selectedInFull: [FALSE, FALSE, FALSE], tightBox: GGBoundBox.NullBoundBox[], -- filled in by call to TextGetBoundBox boundBox: GGBoundBox.NullBoundBox[], -- filled in by call to TextGetBoundBox boxValid: FALSE ]]; }; IsWhitespace: PUBLIC PROC [slice: Slice] RETURNS [BOOL ¬ FALSE] = { IF GGSliceOps.GetType[slice]#$Text THEN RETURN[FALSE] ELSE { rope: Rope.ROPE ¬ NARROW[slice.data, TextData].rope; RETURN[Rope.Length[rope]=IO.SkipWhitespace[IO.RIS[rope]] ]; }; }; GetText: PUBLIC PROC [slice: Slice] RETURNS [text: Rope.ROPE] = { RETURN[IF GGSliceOps.GetType[slice]#$Text THEN NIL ELSE NARROW[slice.data, TextData].rope]; }; SetText: PUBLIC PROC [slice: Slice, text: Rope.ROPE] = { textData: TextData; IF GGSliceOps.GetType[slice]#$Text THEN RETURN; textData ¬ NARROW[slice.data]; textData.rope ¬ text; -- update text GGSlice.KillBoundBox[slice]; }; AppendText: PUBLIC PROC [slice: Slice, text: Rope.ROPE] = { textData: TextData; IF GGSliceOps.GetType[slice]#$Text THEN RETURN; textData ¬ NARROW[slice.data]; textData.rope ¬ Rope.Concat[textData.rope, text]; -- update text GGSlice.KillBoundBox[slice]; }; BackspaceText: PUBLIC PROC [slice: Slice, word: BOOL ¬ FALSE] = { textData: TextData; IF GGSliceOps.GetType[slice]#$Text THEN RETURN; textData ¬ NARROW[slice.data]; IF word THEN { WHILE TRUE DO length: INT ¬ Rope.Length[textData.rope]; lastSpaceAt: INT ¬ Rope.FindBackward[textData.rope, " ",]; -- index of last space in string IF lastSpaceAt=length-1 AND length#0 THEN { -- space at very end of non-empty string textData.rope ¬ Rope.Substr[base: textData.rope, len: lastSpaceAt]; -- slough the final space } ELSE { textData.rope ¬ IF lastSpaceAt=-1 THEN "" ELSE Rope.Substr[base: textData.rope, len: lastSpaceAt+1]; -- slough the non-spaces back to the next space EXIT; -- finished }; ENDLOOP; } ELSE textData.rope ¬ Rope.Substr[base: textData.rope, len: Rope.Length[textData.rope]-1]; -- BS GGSlice.KillBoundBox[slice]; }; DigitBreak: IO.BreakProc = { RETURN[IF char IN ['0..'9] THEN break ELSE other]; }; TFontParamsFromFontName: PROC [fontName: Rope.ROPE] RETURNS [fontFamily: Rope.ROPE, fontFace: Rope.ROPE, fontSize: REAL ¬ 0.0] = { GetNumber: PROC [s: IO.STREAM] RETURNS [x: REAL ¬ 0.0] = { frac, fracCount: REAL ¬ 0.0; c: CHAR; UNTIL (c ¬ IO.GetChar[s ! IO.EndOfStream => {c ¬ '\000; CONTINUE;}; ]) NOT IN ['0..'9] DO x ¬ x*10+(c-'0); ENDLOOP; IF c#'\000 AND c#'. THEN s.Backup[c]; -- restore the terminating character except for decimal point IF c='. THEN { -- fractional part UNTIL (c ¬ IO.GetChar[s ! IO.EndOfStream => {c ¬ '\000; CONTINUE;}; ]) NOT IN ['0..'9] DO fracCount ¬ fracCount+1; frac ¬ frac*10.0+(c-'0); ENDLOOP; x ¬ x+frac*RealFns.Power[10.0, -1*fracCount]; IF c#'\000 THEN s.Backup[c]; -- restore the terminating character }; }; face: Rope.ROPE; fontStream: IO.STREAM ¬ IO.RIS[fontName]; [fontFamily, ----] ¬ IO.GetTokenRope[stream: fontStream, breakProc: DigitBreak ! IO.EndOfStream => SIGNAL FontNameError]; fontSize ¬ GetNumber[fontStream ! IO.EndOfStream => SIGNAL FontNameError]; [face, ----] ¬ IO.GetTokenRope[stream: fontStream, breakProc: IO.TokenProc ! IO.EndOfStream => {face ¬ NIL; CONTINUE; };]; IF face#NIL THEN fontFace ¬ Rope.Concat["-", face]; }; SetTextFont: PUBLIC PROC [slice: Slice, fontData: FontData, router: MsgRouter, history: HistoryEvent] RETURNS [ success: BOOL ¬ TRUE] = { RETURN[SetTextFontAux[slice, fontData, router, FALSE, history]]; }; SetTextFontAndTransform: PUBLIC PROC [slice: Slice, fontData: FontData, router: MsgRouter, history: HistoryEvent] RETURNS [ success: BOOL ¬ TRUE] = { RETURN[SetTextFontAux[slice, fontData, router, TRUE, history]]; }; SetTextFontAux: PROC [slice: Slice, fontData: FontData, router: MsgRouter, andTransform: BOOL ¬ FALSE, history: HistoryEvent, substituteFont, trueFont, screenFont: Imager.Font ¬ NIL] RETURNS [success: BOOL ¬ TRUE] = { transform, oldTransform: ImagerTransformation.Transformation; eCode: ATOM; eExplanation: Rope.ROPE; IF GGSliceOps.GetType[slice]#$Text THEN RETURN[FALSE]; BEGIN textData: TextData ¬ NARROW[slice.data]; IF GGSliceOps.GetType[slice]#$Text THEN RETURN; IF fontData.namelessFont # NIL THEN { textData.fontData­ ¬ fontData­; textData.trueFont ¬ textData.fontData.namelessFont; textData.screenFont ¬ textData.trueFont; transform ¬ ImagerTransformation.Copy[fontData.transform]; textData.fontData.transform ¬ transform; textData.inverse ¬ ImagerTransformation.Invert[transform]; textData.inverseScale ¬ ImagerTransformation.Factor[textData.inverse].s; GGSlice.KillBoundBox[slice]; -- this is all we really care about RETURN; }; IF fontData.literal=NIL THEN GOTO Abort; oldTransform ¬ textData.fontData.transform; transform ¬ IF andTransform THEN ImagerTransformation.Copy[fontData.transform] ELSE IF fontData.transform=NIL THEN ImagerTransformation.PreScale[m: ImagerTransformation.Translate[ImagerTransformation.Factor[m: oldTransform].t], s: fontData.scale] ELSE ImagerTransformation.TranslateTo[fontData.transform, ImagerTransformation.Factor[oldTransform].t]; IF trueFont # NIL THEN textData.trueFont ¬ trueFont -- if we know it don't look for it ELSE IF fontData.substituted AND substituteFont#NIL THEN { textData.trueFont ¬ substituteFont; } ELSE { textData.trueFont ¬ ImagerFont.Find[fontData.literal, substituteWithWarning ! Imager.Error => GOTO Abort; PFS.Error => GOTO PFSError; ImagerSys.FileError => { eCode _ code; eExplanation _ explanation; GOTO ImagerSysFileError; }; Imager.Warning => { IF fontData.substituteOK THEN { fontData.substituted ¬ TRUE; RESUME; } ELSE GOTO NotFound; }; ]; IF fontData.substituted THEN Feedback.PutFL[router, oneLiner, $Warning, "%g substituted for %g", LIST[[rope[ImagerFont.Name[textData.trueFont]]], [rope[fontData.literal]]] ]; textData.trueFont ¬ ImagerFont.Scale[textData.trueFont, IF fontData.substituted THEN 1.0 ELSE 1.0/fontData.storedSize]; }; fontData.transform ¬ transform; -- must be done before computing alternate font textData.screenFont ¬ IF screenFont # NIL THEN screenFont ELSE NIL; textData.fontData­ ¬ fontData­; -- don't do this assignment until FindFont succeeds ! textData.inverse ¬ ImagerTransformation.Invert[transform]; textData.inverseScale ¬ ImagerTransformation.Factor[textData.inverse].s; GGSlice.KillBoundBox[slice]; EXITS PFSError => { Feedback.PutF[router, oneLiner, $Complaint, "Fatal PFS Error during ImagerFont.Find of %g", [rope[fontData.literal]] ]; success ¬ FALSE; }; Abort => { Feedback.PutF[router, oneLiner, $Complaint, "Fatal Imager Error during ImagerFont.Find of %g", [rope[fontData.literal]] ]; success ¬ FALSE; }; NotFound => { scratch: IO.STREAM ¬ IO.ROS[]; IF transform#NIL THEN GGParseOut.WriteFactoredTransformationVEC[scratch, transform]; Feedback.PutFL[router, oneLiner, $Complaint, "No Such Font: %g %g %g %g", LIST[[rope[fontData.literal]], [rope[IO.RopeFromROS[scratch]]], [real[fontData.storedSize]], [real[fontData.designSize]]] ]; success ¬ FALSE; }; ImagerSysFileError => { Feedback.PutFL[router, oneLiner, $Complaint, "ImagerSys.FileError [%g, %g] during ImagerFont.Find %g", LIST[[atom[eCode]], [rope[eExplanation]], [rope[fontData.literal]]] ]; success ¬ FALSE; }; END; }; GetLooksDataRope: PUBLIC PROC [slice: Slice] RETURNS [Rope.ROPE] = { scratch: IO.STREAM ¬ IO.ROS[]; IF GGSliceOps.GetType[slice]#$Text THEN RETURN[NIL] ELSE { scale: REAL ¬ 10.0; -- convert fractions to offsets needed by SetDropShadow textData: TextData ¬ NARROW[slice.data]; IF textData.color#NIL THEN scratch.PutF1["Text color: %g ", [rope[GGUtility.DescribeColor[textData.color]]] ] ELSE scratch.PutF1["%g", [rope["No Text Fill Color "]]]; IF textData.dropShadowOn THEN { IF textData.shadowColor#NIL THEN scratch.PutF1["ShadowColor: %g ", [rope[GGUtility.DescribeColor[textData.shadowColor]]] ] ELSE scratch.PutF1["%g", [rope["No Drop Shadow color "]]]; scratch.PutF[" offset: [%g, %g]", [real[textData.dropShadowOffset.x/scale]], [real[textData.dropShadowOffset.y/scale]] ]; }; }; RETURN[IO.RopeFromROS[scratch]]; }; GetFontData: PUBLIC PROC [slice: Slice] RETURNS [fontData: FontData] = { textData: TextData; IF GGSliceOps.GetType[slice]#$Text THEN RETURN[NIL]; textData ¬ NARROW[slice.data]; RETURN[textData.fontData]; }; GetFontDataRope: PUBLIC PROC [slice: Slice] RETURNS [Rope.ROPE] = { textData: TextData; fontData: FontData; scratch: IO.STREAM ¬ IO.ROS[]; IF GGSliceOps.GetType[slice]#$Text THEN RETURN[NIL]; textData ¬ NARROW[slice.data]; fontData ¬ textData.fontData; GGParseOut.WriteFactoredTransformationVEC[scratch, fontData.transform]; IF fontData.comfortable THEN RETURN[IO.PutFLR["%g%g %g %g %g %g", LIST[ [rope[textData.fontData.prefix]], [rope[textData.fontData.userFSF]], [rope[IO.RopeFromROS[scratch]]], [real[fontData.storedSize]], [real[fontData.designSize]], [rope[IF fontData.substituted THEN "SUBSTITUTED" ELSE ""]] ]] ] ELSE RETURN[IO.PutFLR["%g %g %g %g %g", LIST[[rope[fontData.literal]], [rope[IO.RopeFromROS[scratch]]], [real[fontData.storedSize]], [real[fontData.designSize]], [rope[IF fontData.substituted THEN "SUBSTITUTED" ELSE ""]]] ] ]; }; GetFontLiteralDataRope: PUBLIC PROC [slice: Slice] RETURNS [Rope.ROPE] = { textData: TextData; IF GGSliceOps.GetType[slice]#$Text THEN RETURN[NIL]; textData ¬ NARROW[slice.data]; RETURN[GGFont.FontAsLiteralRope[textData.fontData]]; }; UnpackComplexFontName: PROC [fontName: Rope.ROPE, fontSize: REAL] RETURNS [new: Rope.ROPE] = { face: Rope.ROPE; new ¬ FileNames.GetShortName[fontName]; -- throw away "xerox/pressfonts/" face ¬ FileNames.Tail[new, '-]; -- get face component (MIR, BRR, ...) new ¬ Rope.Substr[base: new, start: 0, len: Rope.SkipTo[s: new, pos: 0, skip: "-"]]; -- throw away "-XXX" new ¬ Rope.Cat[new, Convert.RopeFromReal[fontSize], SELECT TRUE FROM Rope.Equal[face, "MRR", FALSE] => "", Rope.Equal[face, "BRR", FALSE] => "B", Rope.Equal[face, "MIR", FALSE] => "I", Rope.Equal[face, "BIR", FALSE] => "BI", ENDCASE => ERROR ]; }; SetTextAmplifySpace: PUBLIC PROC [slice: Slice, amplifySpace: REAL, router: MsgRouter, history: HistoryEvent] = { IF GGSliceOps.GetType[slice]=$Text THEN { textData: TextData ¬ NARROW[slice.data]; textData.amplifySpace ¬ amplifySpace; GGSlice.KillBoundBox[slice]; }; }; GetTextAmplifySpace: PUBLIC PROC [slice: Slice] RETURNS [amplifySpace: REAL] = { IF GGSliceOps.GetType[slice]=$Text THEN { textData: TextData ¬ NARROW[slice.data]; RETURN[textData.amplifySpace]; }; RETURN[1.0]; }; DropShadowOn: PUBLIC PROC [slice: Slice, offset: Vector, history: HistoryEvent] = { IF GGSliceOps.GetType[slice]=$Text THEN { textData: TextData ¬ NARROW[slice.data]; textData.dropShadowOn ¬ TRUE; textData.dropShadowOffset ¬ offset; GGSlice.KillBoundBox[slice]; }; }; DropShadowOff: PUBLIC PROC [slice: Slice, history: HistoryEvent] = { IF GGSliceOps.GetType[slice]=$Text THEN { textData: TextData ¬ NARROW[slice.data]; textData.dropShadowOn ¬ FALSE; GGSlice.KillBoundBox[slice]; }; }; GetTextDropShadow: PUBLIC PROC [slice: Slice] RETURNS [dropShadowOn: BOOL ¬ FALSE, dropShadow: Vector ¬ [0.0, 0.0], color: Imager.Color] = { IF GGSliceOps.GetType[slice]=$Text THEN { textData: TextData ¬ NARROW[slice.data]; RETURN[textData.dropShadowOn, textData.dropShadowOffset, textData.shadowColor]; }; }; GetTextColors: PUBLIC PROC [slice: Slice] RETURNS [textColor, shadowColor: Imager.Color] = { IF GGSliceOps.GetType[slice]=$Text THEN { textData: TextData ¬ NARROW[slice.data]; RETURN[textData.color, textData.shadowColor]; }; }; SetTextColors: PUBLIC PROC [slice: Slice, textColor: Imager.Color, useTextColor: BOOL ¬ TRUE, shadowColor: Imager.Color, useShadowColor: BOOL ¬ FALSE, history: HistoryEvent] = { IF GGSliceOps.GetType[slice]=$Text THEN { textData: TextData ¬ NARROW[slice.data]; IF useTextColor THEN textData.color ¬ textColor; IF useShadowColor THEN textData.shadowColor ¬ shadowColor; }; }; GetTextLineSpacing: PUBLIC PROC [slice: Slice] RETURNS [REAL] = { IF GGSliceOps.GetType[slice]=$Text THEN { textData: TextData ¬ NARROW[slice.data]; RETURN[textData.lineSpacing]; } ELSE RETURN[0.0]; }; SetTextLineSpacing: PUBLIC PROC [slice: Slice, lineSpacing: REAL, history: HistoryEvent] = { IF GGSliceOps.GetType[slice]=$Text THEN { textData: TextData ¬ NARROW[slice.data]; textData.lineSpacing ¬ lineSpacing; }; }; SetTextDisplayStyle: PUBLIC PROC [slice: Slice, displayStyle: DisplayStyle, history: HistoryEvent] = { textData: TextData ¬ NARROW[slice.data]; IF displayStyle # textData.displayStyle THEN GGSlice.KillBoundBox[slice]; textData.displayStyle ¬ displayStyle; }; OutlinesFromTextString: PUBLIC PROC [slice: Slice] RETURNS [shadowOutlines, outlines: LIST OF Slice] = { textData: TextData ¬ NARROW[slice.data]; fontData: FontData ¬ textData.fontData; font: Font ¬ textData.trueFont; amplifySpace: REAL ¬ textData.amplifySpace; camera: Camera ¬ NEW[CameraObj]; -- this data is not really used in Capture in this case SplinesFromTextString: PROC [dc: Imager.Context] = { SplinesFromChar: PROC [char: Char.XCHAR] = { proc: PROC = { escapement: VEC ¬ ImagerFont.Escapement[font, char]; IF amplifySpace#1.0 AND ImagerFont.Amplified[font, char] THEN { escapement ¬ Vector2.Mul[escapement, amplifySpace]; }; Imager.Move[dc]; ImagerTypeface.MaskChar[font, char, dc]; Imager.SetXYRel[dc, escapement]; SELECT ImagerFont.Correction[font, char] FROM none => NULL; space => Imager.CorrectSpace[dc, escapement]; mask => Imager.CorrectMask[dc]; ENDCASE => ERROR; }; Imager.DoSave[dc, proc]; }; IF shadows THEN GGCoreOps.SetColor[dc, IF textData.shadowColor=NIL THEN Imager.black ELSE textData.shadowColor] ELSE GGCoreOps.SetColor[dc, IF textData.color=NIL THEN Imager.black ELSE textData.color]; Imager.ConcatT[dc, fontData.transform]; IF shadows THEN Imager.TranslateT[dc, tVec]; Imager.SetXY[dc, [0.0, 0.0]]; ImagerFont.MapRope[textData.rope, 0, INT.LAST, SplinesFromChar]; }; fakeScene: Scene; shadows: BOOL ¬ TRUE; tVec: VEC; IF textData.dropShadowOn THEN { tVec ¬ [ x: textData.dropShadowOffset.x*dropScale, y: textData.dropShadowOffset.y*dropScale]; fakeScene ¬ GGFromImager.Capture[SplinesFromTextString, camera]; shadowOutlines ¬ GGScene.ListSlices[fakeScene, first]; }; shadows ¬ FALSE; fakeScene ¬ GGFromImager.Capture[SplinesFromTextString, camera]; outlines ¬ GGScene.ListSlices[fakeScene, first]; }; StringEscapement: PROC [font: ImagerFont.Font, string: ImagerFont.XStringProc, amplifySpace: REAL] RETURNS [VEC] = { sum: VEC ¬ [0, 0]; charAction: ImagerFont.XCharProc ~ { width: VEC; width ¬ ImagerFont.Escapement[font, char]; IF ImagerFont.Amplified[font, char] THEN { width.x ¬ width.x*amplifySpace; width.y ¬ width.y*amplifySpace; }; sum.x ¬ sum.x+width.x; sum.y ¬ sum.y+width.y; }; string[charAction]; RETURN[sum]; }; RopeEscapement: PROC [font: ImagerFont.Font, rope: Rope.ROPE, amplifySpace: REAL, start: INT ¬ 0, len: INT ¬ INT.LAST] RETURNS [VEC] ~ { string: ImagerFont.XStringProc ~ { ImagerFont.MapRope[rope, start, len, charAction] }; RETURN[StringEscapement[font, string, amplifySpace]]; }; UpdateTextPoints: PROC [textData: TextData, left, middle, right, bottom, base, center, top: REAL] = { textData.points[0] ¬ [left, bottom]; textData.points[1] ¬ [left, base]; textData.points[2] ¬ [left, center]; textData.points[3] ¬ [left, top]; textData.points[4] ¬ [middle, bottom]; textData.points[5] ¬ [middle, base]; textData.points[6] ¬ [middle, center]; textData.points[7] ¬ [middle, top]; textData.points[8] ¬ [right, bottom]; textData.points[9] ¬ [right, base]; textData.points[10] ¬ [right, center]; textData.points[11] ¬ [right, top]; }; fontExtentRope: Rope.ROPE = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; tryNewBBox: BOOL _ TRUE; NewComputeTextBounds: PROC [font, namelessFont: Font, rope: Rope.ROPE, amplifySpace: REAL] RETURNS [boundBox: ImagerBox.Box, feedbackBox: ImagerBox.Box] = { fontExtents: ImagerFont.Extents; fbRight, bTop, bBottom, cpBottom, cpTop: REAL ¬ 0.0; fbRight ¬ RopeEscapement[font, rope, amplifySpace].x; -- feedbackBox, right boundary fontExtents ¬ ImagerFont.RopeBoundingBox[font, rope]; boundBox ¬ ImagerBox.BoxFromExtents[fontExtents]; -- accurate bound box for rope cpTop ¬ fontExtents.ascent; -- feedbackBox, top boundary cpBottom ¬ -fontExtents.descent; -- feedbackBox, bottom boundary IF namelessFont = NIL THEN { -- adjust boundaries to conform to fontExtentRope fontExtents ¬ ImagerFont.RopeBoundingBox[font, fontExtentRope]; cpTop ¬ fontExtents.ascent; -- feedbackBox, top boundary cpBottom ¬ -fontExtents.descent; -- feedbackBox, bottom boundary }; feedbackBox ¬ [0.0, cpBottom, fbRight, cpTop]; }; NewTextGetBoundBoxAux: PROC [slice: Slice, parts: SliceParts ¬ NIL] RETURNS [tightBox, boundBox: BoundBox]= { textData: TextData ¬ NARROW[slice.data]; fontData: FontData ¬ textData.fontData; eBox: ImagerBox.Box; -- the actual rope bounding box from the rope extents, nothing to do with the fontExtentRope bounds. fBox: ImagerBox.Box; -- the feedback box, which has its left edge at relative 0.0 (the rope origin), its right edge at the rope escapement, and its top and bottom calculated from the fontExtentRope bounds. Used to determine the feedback points. uBox: ImagerBox.Box; -- the union of fBox and eBox, used to calculate the slice boundBox screenFont: ImagerFont.Font; [eBox, fBox] ¬ NewComputeTextBounds[textData.trueFont, textData.fontData.namelessFont, textData.rope, textData.amplifySpace]; IF textData.displayStyle = print OR (screenFont _ GetScreenFont[textData]) = NIL THEN NULL ELSE eBox ¬ NewComputeTextBounds[screenFont, textData.fontData.namelessFont, textData.rope, textData.amplifySpace].boundBox; -- recompute eBox for alternate or screen font. Leave fBox the same so alignments can be performed by the user while displaying alternate fonts GGBoundBox.UpdateBoundBox[textData.box, fBox.xmin, fBox.ymin, fBox.xmax, fBox.ymax]; -- the feedback box, which may not be very tight because by definition includes the control points. GGBoundBox.UpdateBoundBoxOfBoundBox[textData.box, textData.box, fontData.transform]; -- transformed in place GGBoundBox.UpdateCopyBoundBox[slice.tightBox, textData.box]; -- slice.tightBox updated in place from textData.box. uBox ¬ ImagerBox.BoundingBox[fBox, eBox]; -- box union of feedback and extents boxes GGBoundBox.UpdateBoundBox[slice.boundBox, uBox.xmin, uBox.ymin, uBox.xmax, uBox.ymax]; -- slice.boundBox updated in place IF textData.dropShadowOn THEN GGBoundBox.EnlargeByVector[slice.boundBox, [x: textData.dropShadowOffset.x*dropScale, y: textData.dropShadowOffset.y*dropScale]]; -- enlarged for drop shadows GGBoundBox.UpdateBoundBoxOfBoundBox[slice.boundBox, slice.boundBox, fontData.transform]; -- transformed in place GGBoundBox.EnlargeByOffset[slice.boundBox, halfHotJointSize+1.0]; -- enlarge in place. Make the worst case assumption that there are hot selected joints instead of finding out. It is only a few pixels extra in the boundBox UpdateTextPoints[textData: textData, left: 0.0, middle: --fBox.xmin+-- fBox.xmax/2.0, right: fBox.xmax, bottom: fBox.ymin, base: 0.0, center: (fBox.ymax+fBox.ymin)/2.0, top: fBox.ymax]; -- fBox.xmin is always = 0.0 GGSlice.KillBoundBox[slice.parent]; -- invalidate ancestor caches slice.boxValid ¬ TRUE; -- remember that the boxes were already updated in place slice.tightBoxValid ¬ TRUE; -- not really necessary since only boxValid is used tightBox ¬ slice.tightBox; boundBox ¬ slice.boundBox; }; ComputeTextBounds: PROC [font, namelessFont: Font, rope: Rope.ROPE, amplifySpace: REAL] RETURNS [middle, right, bottom, top, cpBottom, cpCenter, cpTop: REAL] = { fontExtents: ImagerFont.Extents; left, base: REAL = 0.0; right ¬ RopeEscapement[font, rope, amplifySpace].x; middle ¬ (left + right)/2.0; fontExtents ¬ ImagerFont.RopeBoundingBox[font, rope]; top ¬ fontExtents.ascent; bottom ¬ -fontExtents.descent; IF namelessFont # NIL THEN { cpTop ¬ top; cpBottom ¬ bottom; } ELSE { fontExtents ¬ ImagerFont.RopeBoundingBox[font, fontExtentRope]; cpTop ¬ fontExtents.ascent; cpBottom ¬ -fontExtents.descent; }; cpCenter ¬ (cpTop + cpBottom)/2.0; }; TextGetBoundBoxAux: PROC [slice: Slice, parts: SliceParts ¬ NIL] RETURNS [tightBox, boundBox: BoundBox]= { TextFindBoundBox: PROC [slice: Slice, parts: SliceParts] RETURNS [tightBox, boundBox: BoundBox] = { textData: TextData ¬ NARROW[slice.data]; fontData: FontData ¬ textData.fontData; left, base: REAL = 0.0; middle, right, bottom, top, cpBottom, cpCenter, cpTop: REAL; altMiddle, altRight, altBottom, altTop: REAL; [middle, right, bottom, top, cpBottom, cpCenter, cpTop] ¬ ComputeTextBounds[textData.trueFont, textData.fontData.namelessFont, textData.rope, textData.amplifySpace]; IF textData.displayStyle = print OR GetScreenFont[textData] = NIL THEN { altMiddle ¬ middle; altRight ¬ right; altBottom ¬ bottom; altTop ¬ top; } ELSE [altMiddle, altRight, altBottom, altTop, ----, ----, ----] ¬ ComputeTextBounds[GetScreenFont[textData], textData.fontData.namelessFont, textData.rope, textData.amplifySpace]; GGBoundBox.UpdateBoundBox[textData.box, left, altBottom, altRight, altTop]; IF textData.dropShadowOn THEN GGBoundBox.EnlargeByVector[textData.box, [x: textData.dropShadowOffset.x*dropScale, y: textData.dropShadowOffset.y*dropScale]]; textData.box ¬ GGBoundBox.BoundBoxOfBoundBox[textData.box, fontData.transform]; GGBoundBox.EnlargeByOffset[textData.box, 1.0]; boundBox ¬ GGBoundBox.CopyBoundBox[textData.box]; tightBox ¬ GGBoundBox.CreateBoundBox[left, MIN[bottom, cpBottom], right, MAX[top, cpTop]]; tightBox ¬ GGBoundBox.BoundBoxOfBoundBox[tightBox, fontData.transform]; UpdateTextPoints[textData, left, middle, right, cpBottom, base, cpCenter, cpTop]; }; [tightBox, boundBox] ¬ TextFindBoundBox[slice, parts]; -- do the bound box update IF parts=NIL THEN { -- set up cache for fast case next time around GGSlice.KillBoundBox[slice.parent]; -- invalidate ancestor caches slice.boundBox ¬ boundBox; slice.tightBox ¬ tightBox; slice.boxValid ¬ TRUE; slice.tightBoxValid ¬ TRUE; -- not really necessary since only boxValid is used }; }; TextGetBoundBox: PROC [slice: Slice, parts: SliceParts] RETURNS [box: BoundBox] = { IF slice.boxValid AND parts=NIL THEN RETURN[slice.boundBox]; -- fast case RETURN[IF tryNewBBox THEN NewTextGetBoundBoxAux[slice].boundBox ELSE TextGetBoundBoxAux[slice, parts].boundBox]; -- cache update if possible }; useBBoxForTBox: BOOL _ FALSE; TextGetTightBox: PROC [slice: Slice, parts: SliceParts] RETURNS [box: BoundBox] = { IF slice.boxValid AND parts=NIL THEN RETURN[IF useBBoxForTBox THEN slice.boundBox ELSE slice.tightBox]; -- fast case [] _ IF tryNewBBox THEN NewTextGetBoundBoxAux[slice] ELSE TextGetBoundBoxAux[slice, parts]; RETURN[IF useBBoxForTBox THEN slice.boundBox ELSE slice.tightBox]; -- cache update if possible }; TextCopy: PROC [slice: Slice, parts: SliceParts ¬ NIL] RETURNS [copy: LIST OF Slice] = { copySlice: Slice; copyData: TextData; textData: TextData ¬ NARROW[slice.data]; success: BOOL ¬ TRUE; router: MsgRouter ¬ Feedback.EnsureRouter[$Gargoyle]; copySlice ¬ MakeTextSlice[textData.rope, GGCoreOps.CopyColor[textData.color], textData.displayStyle, textData.amplifySpace, textData.dropShadowOn, textData.dropShadowOffset, GGCoreOps.CopyColor[textData.shadowColor]]; copyData ¬ NARROW[copySlice.data]; success ¬ SetTextFontAux[slice: copySlice, fontData: GGFont.CopyFontData[textData.fontData, copyData.fontData], router: router, andTransform: TRUE, history: NIL, substituteFont: textData.trueFont, trueFont: textData.trueFont, screenFont: textData.screenFont]; IF NOT success THEN ERROR; copyData.seg ¬ GGSegment.CopySegment[seg: textData.seg]; copyData.lineSpacing ¬ GGSlice.GetTextLineSpacing[slice]; GGProps.CopyAll[fromSlice: slice, toSlice: copySlice]; RETURN[LIST[copySlice]]; }; TextRestore: PROC [from: Slice, to: Slice] = { IF to=NIL OR from=NIL THEN ERROR; IF to.class#from.class THEN ERROR; IF to.class.type#$Text THEN ERROR; BEGIN fromData: TextData ¬ NARROW[from.data]; toData: TextData ¬ NARROW[to.data]; IF GGSlice.copyRestore THEN { toData.box­ ¬ fromData.box­; toData.points­ ¬ fromData.points­; toData.rope ¬ fromData.rope; toData.color ¬ fromData.color; toData.fontData ¬ fromData.fontData; toData.trueFont ¬ fromData.trueFont; toData.screenFont ¬ fromData.screenFont; toData.displayStyle ¬ fromData.displayStyle; toData.inverse ¬ fromData.inverse; toData.inverseScale ¬ fromData.inverseScale; toData.amplifySpace ¬ fromData.amplifySpace; toData.dropShadowOn ¬ fromData.dropShadowOn; toData.dropShadowOffset ¬ fromData.dropShadowOffset; toData.dropShadowOffset ¬ fromData.dropShadowOffset; toData.shadowColor ¬ fromData.shadowColor; toData.lineSpacing ¬ fromData.lineSpacing; toData.seg ¬ fromData.seg; } ELSE to.data ¬ from.data; to.selectedInFull ¬ from.selectedInFull; -- RECORD of BOOL to.normalSelectedParts ¬ NIL; -- caller must reselect to.hotSelectedParts ¬ NIL; -- caller must reselect to.activeSelectedParts ¬ NIL; -- caller must reselect to.matchSelectedParts ¬ NIL; -- caller must reselect to.tightBox­ ¬ from.tightBox­; to.tightBoxValid ¬ from.tightBoxValid; to.boundBox­ ¬ from.boundBox­; to.boxValid ¬ from.boxValid; to.onOverlay ¬ from.onOverlay; to.extraPoints ¬ from.extraPoints; to.priority ¬ from.priority; to.historyTop ¬ from.historyTop; END; }; TextDrawParts: PROC [slice: Slice, parts: SliceParts ¬ NIL, dc: Imager.Context, camera: GGModelTypes.Camera, quick: BOOL] = { DoDrawText: PROC = { IF textData.dropShadowOn THEN DrawDropShadow[dc, textData, camera, quick]; DrawText[dc, textData, camera, quick]; }; textData: TextData ¬ NARROW[slice.data]; textParts: TextParts ¬ NARROW[parts]; IF textParts = NIL OR textParts.includeText THEN Imager.DoSave[dc, DoDrawText]; }; TextDrawSelectionFeedback: PROC [slice: Slice, selectedParts: SliceParts, hotParts: SliceParts, dc: Imager.Context, camera: Camera, dragInProgress, caretIsMoving, hideHot, quick: BOOL] = { slowNormal, slowHot, completeNormal, completeHot: BOOL ¬ FALSE; firstJoint: Point; normalTextParts, hotTextParts: TextParts; textData: TextData ¬ NARROW[slice.data]; transform: Transformation ¬ textData.fontData.transform; IF caretIsMoving OR dragInProgress OR camera.quality=quality THEN RETURN; IF selectedParts=NIL AND hotParts=NIL THEN RETURN; normalTextParts ¬ NARROW[selectedParts]; hotTextParts ¬ NARROW[hotParts]; completeNormal ¬ normalTextParts#NIL AND IsComplete[normalTextParts]; completeHot ¬ hotTextParts#NIL AND IsComplete[hotTextParts]; slowNormal ¬ normalTextParts#NIL AND (NOT quick OR (quick AND NOT completeNormal)); slowHot ¬ hotTextParts#NIL AND (NOT quick OR (quick AND NOT completeHot)); IF slowNormal AND slowHot THEN DrawSelectionFeedbackText[slice, textData, normalTextParts, hotTextParts, dc, transform, camera] ELSE IF slowNormal THEN DrawSelectionFeedbackText[slice, textData, normalTextParts, NIL, dc, transform, camera] ELSE IF slowHot THEN DrawSelectionFeedbackText[slice, textData, NIL, hotTextParts, dc, transform, camera]; IF (NOT slowNormal AND completeNormal) OR (NOT slowHot AND completeHot) THEN { fullParts: TextParts ¬ IF completeNormal THEN normalTextParts ELSE hotTextParts; [] ¬ GGSliceOps.GetTightBox[slice]; -- force update of internal data firstJoint ¬ ImagerTransformation.Transform[transform, textData.points[0]]; }; IF NOT slowHot AND completeHot THEN GGShapes.DrawQuickSelectedJoint[dc, firstJoint, hot, camera.cpScale]; IF NOT slowNormal AND completeNormal THEN GGShapes.DrawQuickSelectedJoint[dc, firstJoint, normal, camera.cpScale]; }; DrawSelectionFeedbackText: PROC [slice: Slice, textData: TextData, normalTextParts: TextParts, hotTextParts: TextParts, dc: Imager.Context, t: ImagerTransformation.Transformation, camera: Camera] = { DoDrawFeedback: PROC = { thisCPisHot, thisCPisSelected: BOOL ¬ FALSE; pts: ARRAY [0..11] OF Point; [] ¬ GGSliceOps.GetTightBox[slice]; -- force update of internal data FOR i: NAT IN [0..11] DO pts[i] ¬ ImagerTransformation.Transform[t, textData.points[i]]; ENDLOOP; Imager.SetColor[dc, Imager.black]; FOR point: INTEGER IN [0..textMaxPoints) DO thisCPisHot ¬ hotTextParts#NIL AND hotTextParts.points[point]; thisCPisSelected ¬ normalTextParts#NIL AND normalTextParts.points[point]; IF thisCPisHot THEN GGShapes.DrawSelectedJoint[dc, pts[point], hot, camera.cpScale]; IF thisCPisSelected THEN GGShapes.DrawSelectedJoint[dc, pts[point], normal, camera.cpScale]; IF NOT thisCPisHot AND NOT thisCPisSelected THEN GGShapes.DrawCP[dc, pts[point], camera.cpScale]; ENDLOOP; }; Imager.DoSave[dc, DoDrawFeedback]; }; TextDrawAttractorFeedback: PROC [slice: Slice, attractorParts: SliceParts, selectedParts: SliceParts, dragInProgress: BOOL, dc: Imager.Context, camera: Camera, editConstraints: EditConstraints] = { bigReal: REAL ¬ 1.0e37; IF NOT (dragInProgress AND selectedParts#NIL) THEN { DoDrawFeedback: PROC = { DrawEdgeAndPoints: PROC [start, end, cp1, cp2: Point ¬ [bigReal, bigReal] ] = { Imager.MaskVector[dc, start, end]; GGShapes.DrawCP[dc, start, camera.cpScale]; GGShapes.DrawCP[dc, end, camera.cpScale]; IF cp1.x#bigReal THEN GGShapes.DrawCP[dc, cp1, camera.cpScale]; IF cp2.x#bigReal THEN GGShapes.DrawCP[dc, cp2, camera.cpScale]; }; pts: ARRAY [0..11] OF Point; t: ImagerTransformation.Transformation ¬ fontData.transform; [] ¬ GGSliceOps.GetTightBox[slice]; -- force update of internal data FOR i: NAT IN [0..11] DO pts[i] ¬ ImagerTransformation.Transform[t, textData.points[i]]; ENDLOOP; Imager.SetColor[dc, Imager.black]; Imager.SetStrokeJoint[dc, round]; Imager.SetStrokeWidth[dc, 1.0]; IF hitParts.points#ALL[FALSE] THEN FOR point: INTEGER IN [0..textMaxPoints) DO GGShapes.DrawCP[dc, pts[point], camera.cpScale]; ENDLOOP; IF hitParts.edges#ALL[FALSE] THEN FOR edge: INTEGER IN [0..textMaxEdges) DO IF hitParts.edges[edge] THEN { SELECT edge FROM 0 => DrawEdgeAndPoints[pts[0], pts[3], pts[1], pts[2] ]; 1 => DrawEdgeAndPoints[pts[3], pts[11], pts[7] ]; 2 => DrawEdgeAndPoints[pts[11], pts[8], pts[9], pts[10] ]; 3 => DrawEdgeAndPoints[pts[8], pts[0], pts[4] ]; 4 => DrawEdgeAndPoints[pts[1], pts[9], pts[5] ]; 5 => DrawEdgeAndPoints[pts[2], pts[10], pts[6] ]; 6 => DrawEdgeAndPoints[pts[4], pts[7], pts[5], pts[6] ]; ENDCASE => ERROR; EXIT; }; ENDLOOP; }; textData: TextData ¬ NARROW[slice.data]; hitParts: TextParts ¬ NARROW[attractorParts]; fontData: FontData ¬ textData.fontData; IF camera.quality#quality AND hitParts#NIL THEN Imager.DoSave[dc, DoDrawFeedback]; }; }; DrawText: PROC [dc: Imager.Context, textData: TextData, camera: GGModelTypes.Camera, quick: BOOL] = { IF textData.color=NIL THEN RETURN; Imager.SetFont[dc, IF (camera.quality=quality OR camera.displayStyle=print OR textData.displayStyle=print) THEN textData.trueFont ELSE GetScreenFont[textData]]; GGCoreOps.SetColor[dc, textData.color]; -- must happen before the text transform is applied Imager.ConcatT[dc, textData.fontData.transform]; Imager.SetXY[dc, [0.0, 0.0]]; -- dc must be transformed completely here Imager.SetAmplifySpace[dc, textData.amplifySpace]; -- adjust spacing (for right justified text) Imager.ShowRope[dc, textData.rope]; }; DrawDropShadow: PROC [dc: Imager.Context, textData: TextData, camera: GGModelTypes.Camera, quick: BOOL] = { DoDrawDropShadow: PROC = { tVec: VEC ¬ [x: textData.dropShadowOffset.x*dropScale, y: textData.dropShadowOffset.y*dropScale]; GGCoreOps.SetColor[dc, textData.shadowColor]; -- must happen before text transform is applied Imager.ConcatT[dc, textData.fontData.transform]; Imager.TranslateT[dc, tVec]; Imager.SetFont[dc, IF (camera.quality=quality OR camera.displayStyle=print OR textData.displayStyle=print) THEN textData.trueFont ELSE GetScreenFont[textData]]; Imager.SetXY[dc, [0.0, 0.0]]; -- dc must be transformed completely here Imager.SetAmplifySpace[dc, textData.amplifySpace]; -- adjust spacing (for right justified text) Imager.ShowRope[dc, textData.rope]; }; IF textData.shadowColor=NIL THEN RETURN; Imager.DoSave[dc, DoDrawDropShadow]; }; GetScreenFont: PROC [textData: TextData] RETURNS [font: ImagerFont.Font] ~ { RETURN[IF textData.screenFont#NIL THEN textData.screenFont ELSE GGFont.AlternateFont[textData.fontData, textData.trueFont, $visible]]; }; TextDrawTransform: PROC [slice: Slice, parts: SliceParts ¬ NIL, dc: Imager.Context, camera: Camera, transform: ImagerTransformation.Transformation, editConstraints: EditConstraints] = { DoTextDrawTransform: PROC = { Imager.ConcatT[dc, transform]; IF textData.dropShadowOn THEN DrawDropShadow[dc, textData, camera, FALSE]; DrawText[dc, textData, camera, FALSE]; }; textData: TextData ¬ NARROW[slice.data]; Imager.DoSave[dc, DoTextDrawTransform]; }; TextTransform: PROC [slice: Slice, parts: SliceParts ¬ NIL, transform: ImagerTransformation.Transformation, editConstraints: EditConstraints, history: HistoryEvent] = { textData: TextData ¬ NARROW[slice.data]; fontData: FontData ¬ textData.fontData; fontData.transform ¬ ImagerTransformation.Concat[fontData.transform, transform]; textData.inverse ¬ ImagerTransformation.Invert[fontData.transform]; textData.inverseScale ¬ ImagerTransformation.Factor[textData.inverse].s; textData.color ¬ GGCoreOps.TransformColor[textData.color, transform]; textData.shadowColor ¬ GGCoreOps.TransformColor[textData.shadowColor, transform]; IF InvolvesScaling[transform] THEN textData.screenFont ¬ NIL; GGSlice.KillBoundBox[slice]; }; InvolvesScaling: PROC [transform: ImagerTransformation.Transformation] RETURNS [BOOL] = { epsilon: REAL = 0.01; sigma: VEC ¬ ImagerTransformation.SingularValues[transform]; IF ABS[sigma.x-1.0] > epsilon THEN RETURN[TRUE]; IF ABS[sigma.y-1.0] > epsilon THEN RETURN[TRUE]; RETURN[FALSE]; }; TextDescribe: PROC [sliceD: SliceDescriptor] RETURNS [rope: Rope.ROPE] = { prefix: Rope.ROPE; textParts: TextParts; eCount, pCount: NAT ¬ 0; IF sliceD.parts = NIL THEN RETURN["a Text slice"]; textParts ¬ NARROW[sliceD.parts]; FOR i: INTEGER IN [0..textMaxPoints) DO IF textParts.points[i] THEN { pCount ¬ pCount + 1; prefix ¬ textPointRopes[i]; }; ENDLOOP; FOR i: INTEGER IN [0..textMaxEdges) DO IF textParts.edges[i] THEN { eCount ¬ eCount + 1; prefix ¬ textEdgeRopes[i]; }; ENDLOOP; IF pCount+eCount>1 THEN RETURN["multiple parts of a Text slice"]; IF eCount=0 AND pCount=0 THEN RETURN["NO parts of a Text slice"]; rope ¬ Rope.Concat[prefix, " of a Text slice"]; }; TextDescribeHit: PROC [slice: Slice, hitData: REF ANY] RETURNS [rope: Rope.ROPE] = { textHitData: TextHitData ¬ NARROW[hitData]; prefix: Rope.ROPE; IF textHitData.point#-1 THEN prefix ¬ textPointRopes[textHitData.point] ELSE IF textHitData.edge#-1 THEN prefix ¬ textEdgeRopes[textHitData.edge] ELSE ERROR; rope ¬ Rope.Concat[prefix, " of a Text slice"]; }; TextFileout: PROC [slice: Slice, f: IO.STREAM] = { textData: TextData ¬ NARROW[slice.data]; fontData: FontData ¬ textData.fontData; GGParseOut.WriteBool[f, IF fontData.comfortable THEN TRUE ELSE FALSE]; IF fontData.comfortable THEN f.PutF[" \"%q\" %g%g ", [rope[textData.rope]], [rope[fontData.prefix]], [rope[fontData.userFSF]] ] ELSE f.PutF[" \"%q\" %g ", [rope[textData.rope]], [rope[fontData.literal]] ]; GGParseOut.WriteTransformation[f, fontData.transform]; IF NOT fontData.comfortable THEN f.PutF[" %g %g ", [real[fontData.storedSize]], [real[fontData.designSize]] ]; GGParseOut.WriteColor[f, textData.color]; f.PutChar[IO.SP]; IF textData.dropShadowOn THEN { GGParseOut.WriteBool[f, TRUE]; f.PutChar[IO.SP]; GGParseOut.WritePoint[f, textData.dropShadowOffset]; f.PutChar[IO.SP]; GGParseOut.WriteColor[f, textData.shadowColor]; } ELSE GGParseOut.WriteBool[f, FALSE]; f.PutF1[" %g", [real[textData.amplifySpace]]]; f.PutRope[" props: ( "]; GGParseOut.WriteBool[f, textData.seg.props#NIL]; IF textData.seg.props#NIL THEN GGParseOut.WriteProps[f, textData.seg.props] ELSE f.PutChar[IO.SP]; -- list of ROPE f.PutF1[") ls: %g", [real[GGSlice.GetTextLineSpacing[slice]]]]; }; TextFilein: PROC [f: IO.STREAM, version: REAL, router: MsgRouter, camera: Camera] RETURNS [slice: Slice] = { tempBBox, tempTBox: BoundBox; text, fontName: Rope.ROPE; prefix: Rope.ROPE ¬ "xerox/pressfonts/"; tiogaPrefix: Rope.ROPE ¬ "xerox/tiogafonts/"; family: Rope.ROPE ¬ "Helvetica"; face: Rope.ROPE ¬ ""; fontData: GGFont.FontData; fontDataComfortable: BOOL ¬ FALSE; size: REAL ¬ 1.0; preferredSize: REAL ¬ 1.0; preferredSizeRope: Rope.ROPE ¬ ""; defaultSize: REAL _ 10.0; transform: ImagerTransformation.Transformation; shadowColor: Imager.Color ¬ Imager.black; textColor: Imager.Color ¬ Imager.black; dropShadowOn, good: BOOL ¬ FALSE; dropShadowOffset: Vector ¬ [0.0, 0.0]; amplifySpace: REAL ¬ 1.0; lineSpacing: REAL ¬ 1.0; props: LIST OF Rope.ROPE ¬ NIL; IF version >= 8702.11 THEN { fontDataComfortable ¬ GGParseIn.ReadBool[f]; }; IF version > 8601.22 THEN { text ¬ f.GetRopeLiteral[]; } ELSE { GGParseIn.ReadChar[f, '(]; text ¬ GGParseIn.ReadWord[f]; GGParseIn.ReadChar[f, ')]; }; IF version < 8702.11 THEN { -- many old style formats IF version >= 8701.30 THEN { prefix ¬ GGParseIn.ReadWord[f]; }; -- ELSE uses default prefix IF version <= 8601.06 THEN {-- use default family and face and transformation size ¬ defaultSize; } ELSE IF version <= 8601.27 THEN { -- a simple name like "Helvetica" or "Gacha" family ¬ GGParseIn.ReadWord[f]; } ELSE IF version <= 8605.12 THEN { -- a complex name like "xerox/pressfonts/Helvetica-BIR" fontName ¬ GGParseIn.ReadWord[f]; size ¬ GGParseIn.ReadReal[f]; [] ¬ GGParseIn.ReadWord[f]; fontName ¬ UnpackComplexFontName[fontName, size]; -- like Helvetica12BI [family, face, ----] ¬ TFontParamsFromFontName[fontName]; } ELSE IF version <= 8605.28 THEN { -- a mixed mode name like Helvetica7BI or TimesRoman12 fontName ¬ GGParseIn.ReadWord[f]; size ¬ GGParseIn.ReadReal[f]; [] ¬ GGParseIn.ReadWord[f]; -- throw away redundant size [family, face, ----] ¬ TFontParamsFromFontName[fontName]; } ELSE IF version < 8701.30 THEN { -- a mixed mode name like Helvetica7BI or TimesRoman12. No colorName, real text color fontName ¬ GGParseIn.ReadWord[f]; size ¬ GGParseIn.ReadReal[f]; [family, face, ----] ¬ TFontParamsFromFontName[fontName]; } ELSE IF version = 8701.30 THEN {-- an interim font format ps: INT ¬ 0; fail: BOOL ¬ FALSE; [fail, ----, family, face] ¬ GGFont.OldParseFontData[f, FALSE, TRUE, TRUE]; IF fail THEN ERROR; transform ¬ GGParseIn.ReadTransformation[f]; preferredSize ¬ GGParseIn.ReadReal[f]; ps ¬ Real.Round[preferredSize]; IF Rope.Equal[prefix, tiogaPrefix, FALSE] THEN preferredSizeRope ¬ IO.PutFR1["%g", [integer[ps]] ]; }; fontData ¬ GGFont.CreateFontData[]; fontData.transform ¬ transform; fontData.storedSize ¬ preferredSize; -- needed by ParseFontData fontData ¬ GGFont.ParseFontData[data: fontData, inStream: IO.RIS[rope: Rope.Cat[prefix, family, face, preferredSizeRope], oldStream: NIL], prefixP: TRUE, familyP: TRUE, faceP: TRUE ! GGFont.ParseError => CONTINUE;]; -- failures handled downstream } ELSE { name: Rope.ROPE ¬ GGParseIn.ReadWord[f]; fontData ¬ GGFont.CreateFontData[]; -- assure non-NIL fontData IF fontDataComfortable THEN { -- User data in file: name, transformation fontData ¬ GGFont.ParseFontData[data: fontData, inStream: IO.RIS[rope: name, oldStream: NIL], prefixP: TRUE, familyP: TRUE, faceP: TRUE ! GGFont.ParseError => CONTINUE;]; -- failures handled downstream fontData.transform ¬ GGParseIn.ReadTransformation[f]; } ELSE { -- Literal data in file fontData.transform ¬ GGParseIn.ReadTransformation[f]; fontData.storedSize ¬ GGParseIn.ReadReal[f]; fontData.designSize ¬ GGParseIn.ReadReal[f]; fontData ¬ GGFont.ParseFontData[data: fontData, inStream: IO.RIS[name], literalP: TRUE ! GGFont.ParseError => CONTINUE;]; -- failures handled downstream }; }; IF version > 8605.28 THEN { textColor ¬ GGParseIn.ReadColor[f, version]; IF textColor = NIL AND version < 9106.27 THEN textColor ¬ Imager.black; }; IF version >= 8609.26 THEN { dropShadowOn ¬ GGParseIn.ReadBool[f]; IF dropShadowOn THEN { dropShadowOffset ¬ GGParseIn.ReadPoint[f]; shadowColor ¬ GGParseIn.ReadColor[f, version]; IF shadowColor = NIL AND version < 9106.27 THEN shadowColor ¬ Imager.black; }; } ELSE dropShadowOn ¬ FALSE; IF version <= 8607.22 THEN { -- a point rather than a transform point: Point ¬ GGParseIn.ReadPoint[f]; fontData.transform ¬ ImagerTransformation.Translate[point]; fontData.transform ¬ ImagerTransformation.PreScale[fontData.transform, size]; size ¬ 1.0; } ELSE IF version < 8701.30 THEN { fontData.transform ¬ GGParseIn.ReadTransformation[f]; fontData.transform ¬ ImagerTransformation.PreScale[fontData.transform, size]; size ¬ 1.0; }; -- transform already read in for later versions IF version >= 8701.13 THEN { amplifySpace ¬ GGParseIn.ReadReal[f]; } ELSE amplifySpace ¬ 1.0; IF version>=8706.08 THEN { -- read in segment props hasProps: BOOL ¬ FALSE; GGParseIn.ReadRope[f, "props: ( "]; hasProps ¬ GGParseIn.ReadBool[f]; props ¬ IF hasProps THEN GGParseIn.ReadListOfRope[f] ELSE NIL; GGParseIn.ReadChar[f, ')]; }; IF version>=8803.08 THEN { -- read in line spacing GGParseIn.ReadRope[f, "ls: "]; lineSpacing ¬ GGParseIn.ReadReal[f]; }; slice ¬ MakeTextSlice[text, textColor, camera.displayStyle, amplifySpace, dropShadowOn, dropShadowOffset, shadowColor]; fontData.substituteOK ¬ TRUE; -- allow font substitution on file reads IF NOT SetTextFontAndTransform[slice, fontData, router, NIL] THEN SIGNAL FontNameError; IF props#NIL THEN { seg: Segment ¬ NARROW[slice.data, TextData].seg; FOR next: LIST OF Rope.ROPE ¬ props, next.rest UNTIL next=NIL DO seg.props ¬ CONS[next.first, seg.props]; ENDLOOP; }; GGSlice.KillBoundBox[slice]; tempBBox _ TextGetBoundBox[slice, NIL]; tempTBox _ TextGetTightBox[slice, NIL]; GGSlice.SetTextLineSpacing[slice, lineSpacing, NIL]; }; MakeComplete: PROC [parts: SliceParts] = { textParts: TextParts ¬ NARROW[parts]; textParts.points ¬ ALL[TRUE]; textParts.edges ¬ ALL[TRUE]; textParts.includeText ¬ TRUE; }; IsComplete: PROC [parts: SliceParts] RETURNS [BOOL ¬ FALSE] = { textParts: TextParts ¬ NARROW[parts]; RETURN[textParts#NIL AND textParts.points=ALL[TRUE] AND textParts.edges=ALL[TRUE] AND textParts.includeText=TRUE]; }; IsEmpty: PROC [parts: SliceParts] RETURNS [BOOL ¬ FALSE] = { textParts: TextParts ¬ NARROW[parts]; RETURN[textParts=NIL OR (textParts.points=ALL[FALSE] AND textParts.edges=ALL[FALSE] AND textParts.includeText = FALSE)]; }; TextIsEmptyParts: PROC [sliceD: SliceDescriptor] RETURNS [BOOL ¬ FALSE] = { RETURN[IsEmpty[sliceD.parts]]; }; TextIsCompleteParts: PROC [sliceD: SliceDescriptor] RETURNS [BOOL ¬ FALSE] = { RETURN[IsComplete[sliceD.parts]]; }; TextNewParts: PROC [slice: Slice, hitData: REF ANY, mode: SelectMode] RETURNS [sliceD: SliceDescriptor] = { textHitData: TextHitData ¬ NARROW[hitData]; textParts: TextParts ¬ NEW[TextPartsObj ¬ [points: ALL[FALSE], edges: ALL[FALSE], includeText: FALSE ] ]; SELECT mode FROM literal => { IF textHitData.point#-1 THEN textParts.points[textHitData.point] ¬ TRUE ELSE IF textHitData.edge#-1 THEN textParts.edges[textHitData.edge] ¬ TRUE ELSE ERROR; }; joint => { IF textHitData.point#-1 THEN { textParts.points[textHitData.point] ¬ TRUE; } ELSE { hitData: REF ANY; pointHitData: TextHitData; success: BOOL; wholeD: SliceDescriptor; wholeD ¬ TextNewParts[slice, NIL, topLevel]; [----, ----, ----, hitData, success] ¬ GGSliceOps.ClosestPoint[wholeD, textHitData.hitPoint, GGUtility.plusInfinity]; IF NOT success THEN ERROR; pointHitData ¬ NARROW[hitData]; IF pointHitData.point = -1 THEN ERROR; textParts.points[pointHitData.point] ¬ TRUE; }; textParts.includeText ¬ TRUE; }; segment => IF textHitData.edge#-1 THEN { textParts.edges[textHitData.edge] ¬ TRUE; textParts.includeText ¬ TRUE; }; controlPoint => { textParts.points ¬ ALL[TRUE]; textParts.edges ¬ ALL[TRUE]; }; traj, slice, topLevel => MakeComplete[textParts]; none => { -- leave textParts empty }; ENDCASE => ERROR; sliceD ¬ GGSlice.DescriptorFromParts[slice, textParts]; }; TextUnionParts: PROC [partsA: SliceDescriptor, partsB: SliceDescriptor] RETURNS [aPlusB: SliceDescriptor] = { textPartsA: TextParts ¬ NARROW[partsA.parts]; textPartsB: TextParts ¬ NARROW[partsB.parts]; newParts: TextParts; IF partsA = NIL OR partsB = NIL THEN ERROR; IF partsA.parts = NIL THEN RETURN[partsB]; IF partsB.parts = NIL THEN RETURN[partsA]; newParts ¬ NEW[TextPartsObj ¬ [points: ALL[FALSE], edges: ALL[FALSE], includeText: FALSE]]; FOR i: INTEGER IN [0..textMaxPoints) DO newParts.points[i] ¬ textPartsA.points[i] OR textPartsB.points[i]; ENDLOOP; FOR i: INTEGER IN [0..textMaxEdges) DO newParts.edges[i] ¬ textPartsA.edges[i] OR textPartsB.edges[i]; ENDLOOP; newParts.includeText ¬ textPartsA.includeText OR textPartsB.includeText; aPlusB ¬ GGSlice.DescriptorFromParts[partsA.slice, newParts]; }; TextDiffParts: PROC [partsA: SliceDescriptor, partsB: SliceDescriptor] RETURNS [aMinusB: SliceDescriptor] = { textPartsA: TextParts ¬ NARROW[partsA.parts]; textPartsB: TextParts ¬ NARROW[partsB.parts]; newParts: TextParts; IF partsA = NIL OR partsB = NIL THEN ERROR; IF partsA.parts=NIL THEN RETURN[GGSlice.DescriptorFromParts[partsA.slice, NIL]]; IF partsB=NIL THEN RETURN[partsA]; newParts ¬ NEW[TextPartsObj ¬ [points: ALL[FALSE], edges: ALL[FALSE], includeText: FALSE ] ]; FOR i: INTEGER IN [0..textMaxPoints) DO newParts.points[i] ¬ IF textPartsB.points[i] THEN FALSE ELSE textPartsA.points[i]; ENDLOOP; FOR i: INTEGER IN [0..textMaxEdges) DO newParts.edges[i] ¬ IF textPartsB.edges[i] THEN FALSE ELSE textPartsA.edges[i]; ENDLOOP; newParts.includeText ¬ IF textPartsB.includeText THEN FALSE ELSE textPartsA.includeText; aMinusB ¬ GGSlice.DescriptorFromParts[partsA.slice, newParts]; }; TextMovingParts: PROC [slice: Slice, selectedParts: SliceParts, editConstraints: EditConstraints, bezierDrag: GGInterfaceTypes.BezierDragRecord] RETURNS [background, overlay, rubber, drag: SliceDescriptor] = { dragParts: TextParts ¬ NEW[TextPartsObj ¬ [points: ALL[TRUE], edges: ALL[TRUE], includeText: TRUE ] ]; backgroundParts: TextParts ¬ NEW[TextPartsObj ¬ [points: ALL[TRUE], edges: ALL[TRUE], includeText: TRUE ] ]; IF IsEmpty[selectedParts] THEN { -- everything on background dragParts.edges ¬ ALL[FALSE]; dragParts.points ¬ ALL[FALSE]; dragParts.includeText ¬ FALSE; } ELSE { -- everything is dragging backgroundParts.edges ¬ ALL[FALSE]; backgroundParts.points ¬ ALL[FALSE]; backgroundParts.includeText ¬ FALSE; }; background ¬ GGSlice.DescriptorFromParts[slice, backgroundParts]; overlay ¬ rubber ¬ GGSlice.DescriptorFromParts[slice, NIL]; drag ¬ GGSlice.DescriptorFromParts[slice, dragParts]; }; TextAugmentParts: PROC [sliceD: SliceDescriptor, selectClass: SelectionClass] RETURNS [more: SliceDescriptor] = { textParts: TextParts ¬ NARROW[sliceD.parts]; newParts: TextParts ¬ NEW[TextPartsObj ¬ [points: textParts.points, edges: textParts.edges, includeText: textParts.includeText] ]; FOR i: INTEGER IN [0..textMaxEdges) DO IF textParts.edges[i] THEN SELECT i FROM 0 => {newParts.points[0] ¬ TRUE; newParts.points[1] ¬ TRUE; newParts.points[2] ¬ TRUE; newParts.points[3] ¬ TRUE;}; 1 => {newParts.points[3] ¬ TRUE; newParts.points[7] ¬ TRUE; newParts.points[11] ¬ TRUE;}; 2 => {newParts.points[8] ¬ TRUE; newParts.points[9] ¬ TRUE; newParts.points[10] ¬ TRUE; newParts.points[11] ¬ TRUE;}; 3 => {newParts.points[0] ¬ TRUE; newParts.points[4] ¬ TRUE; newParts.points[8] ¬ TRUE;}; 4 => {newParts.points[1] ¬ TRUE; newParts.points[5] ¬ TRUE; newParts.points[9] ¬ TRUE;}; 5 => {newParts.points[2] ¬ TRUE; newParts.points[6] ¬ TRUE; newParts.points[10] ¬ TRUE;}; 6 => {newParts.points[4] ¬ TRUE; newParts.points[5] ¬ TRUE; newParts.points[6] ¬ TRUE; newParts.points[7] ¬ TRUE;}; ENDCASE => ERROR; ENDLOOP; more ¬ GGSlice.DescriptorFromParts[sliceD.slice, newParts]; }; TextPointsInDescriptor: PROC [sliceD: SliceDescriptor] RETURNS [pointGen: PointGenerator] = { parts: TextParts ¬ NARROW[sliceD.parts]; pointGen ¬ NEW[PointGeneratorObj ¬ [sliceD, 0, 0, NIL] ]; <> [] ¬ TextGetBoundBox[sliceD.slice, NIL]; -- has side effect of updating textData.points, if needed FOR point: INTEGER IN [0..textMaxPoints) DO IF parts.points[point] THEN pointGen.toGo ¬ pointGen.toGo + 1; ENDLOOP; }; TextWalkPointsInDescriptor: PROC [sliceD: SliceDescriptor, walkProc: PointWalkProc] = { parts: TextParts ¬ NARROW[sliceD.parts]; textData: TextData ¬ NARROW[sliceD.slice.data]; fontData: FontData ¬ textData.fontData; done: BOOL ¬ FALSE; <> [] ¬ TextGetBoundBox[sliceD.slice, NIL]; -- has side effect of updating textData.points, if needed FOR index: INTEGER IN [0..textMaxPoints) DO IF parts.points[index] THEN { point: Point ¬ ImagerTransformation.Transform[fontData.transform, textData.points[index]]; done ¬ walkProc[point]; }; ENDLOOP; }; TextPointPairsInDescriptor: PROC [sliceD: SliceDescriptor] RETURNS [pointPairGen: PointPairGenerator] = { parts: TextParts ¬ NARROW[sliceD.parts]; <> [] ¬ TextGetBoundBox[sliceD.slice, NIL]; -- has side effect of updating textData.points, if needed pointPairGen ¬ NEW[PointPairGeneratorObj ¬ [sliceD, 0, 0, NIL] ]; -- toGo and index now used FOR edge: INTEGER IN [0..textMaxEdges) DO IF parts.edges[edge] THEN pointPairGen.toGo ¬ pointPairGen.toGo + 1; ENDLOOP; }; TextSegmentsInDescriptor: PROC [sliceD: SliceDescriptor] RETURNS [segGen: SegmentGenerator] = { parts: TextParts ¬ NARROW[sliceD.parts]; segGen ¬ NEW[SegmentGeneratorObj ¬ [NIL, 0, 0, 0, NIL, FALSE, sliceD, NIL] ]; -- toGo and index now used FOR edge: INTEGER IN [0..textMaxEdges) DO IF parts.edges[edge] THEN segGen.toGo ¬ 1;-- any edge sets segGen.toGo = 1 ENDLOOP; }; TextWalkSegments: PROC [slice: Slice, walkProc: WalkProc] RETURNS [sliceD: SliceDescriptor] = { textData: TextData ¬ NARROW[slice.data]; RETURN[IF walkProc[textData.seg, NIL] THEN TextNewParts[slice, NIL, slice] ELSE TextNewParts[slice, NIL, none]]; }; TextNextSegment: PUBLIC PROC [slice: Slice, segGen: SegmentGenerator] RETURNS [seg: Segment, transform: Transformation] = { IF segGen=NIL OR segGen.toGo = 0 THEN RETURN[NIL, NIL] ELSE { textData: TextData ¬ NARROW[segGen.sliceD.slice.data]; seg ¬ textData.seg; segGen.toGo ¬ 0; }; }; TextNextPoint: PROC [slice: Slice, pointGen: PointGenerator] RETURNS [pointAndDone: GGModelTypes.PointAndDone] = { IF pointGen=NIL OR pointGen.toGo = 0 THEN { pointAndDone.done ¬ TRUE; RETURN; } ELSE { sliceD: SliceDescriptor ¬ pointGen.sliceD; textData: TextData ¬ NARROW[sliceD.slice.data]; fontData: FontData ¬ textData.fontData; textParts: TextParts ¬ NARROW[sliceD.parts]; index: INTEGER ¬ -1; pointAndDone.done ¬ FALSE; FOR index ¬ pointGen.index, index+1 UNTIL index >=textMaxPoints DO IF textParts.points[index] THEN EXIT; -- index will point to next available point ENDLOOP; pointAndDone.point ¬ ImagerTransformation.Transform[fontData.transform, textData.points[index] ]; pointGen.toGo ¬ pointGen.toGo-1; pointGen.index ¬ IF pointGen.toGo=0 THEN 99 ELSE index+1; -- bump to the next available point in the generator }; }; TextNextPointPair: PROC [slice: Slice, pointPairGen: PointPairGenerator] RETURNS [pointPairAndDone: GGModelTypes.PointPairAndDone] = { IF pointPairGen=NIL OR pointPairGen.toGo = 0 THEN { pointPairAndDone.done ¬ TRUE; RETURN; } ELSE { sliceD: SliceDescriptor ¬ pointPairGen.sliceD; textData: TextData ¬ NARROW[sliceD.slice.data]; textParts: TextParts ¬ NARROW[sliceD.parts]; t: ImagerTransformation.Transformation ¬ textData.fontData.transform; index: INTEGER ¬ -1; pointPairAndDone.done ¬ FALSE; FOR index ¬ pointPairGen.index, index+1 UNTIL index >=textMaxEdges DO IF textParts.edges[index] THEN EXIT; -- index will point to next availabe edge ENDLOOP; [] ¬ GGSliceOps.GetBoundBox[slice, NIL]; -- update of textData.points required SELECT index FROM -- index in [0..textMaxEdges) of next available edge 0 => { pointPairAndDone.lo ¬ ImagerTransformation.Transform[t, textData.points[0] ]; pointPairAndDone.hi ¬ ImagerTransformation.Transform[t, textData.points[3] ]; }; 1 => { pointPairAndDone.lo ¬ ImagerTransformation.Transform[t, textData.points[3] ]; pointPairAndDone.hi ¬ ImagerTransformation.Transform[t, textData.points[11] ]; }; 2 => { pointPairAndDone.lo ¬ ImagerTransformation.Transform[t, textData.points[11] ]; pointPairAndDone.hi ¬ ImagerTransformation.Transform[t, textData.points[8] ]; }; 3 => { pointPairAndDone.lo ¬ ImagerTransformation.Transform[t, textData.points[8] ]; pointPairAndDone.hi ¬ ImagerTransformation.Transform[t, textData.points[0] ]; }; 4 => { pointPairAndDone.lo ¬ ImagerTransformation.Transform[t, textData.points[1] ]; pointPairAndDone.hi ¬ ImagerTransformation.Transform[t, textData.points[9] ]; }; 5 => { pointPairAndDone.lo ¬ ImagerTransformation.Transform[t, textData.points[2] ]; pointPairAndDone.hi ¬ ImagerTransformation.Transform[t, textData.points[10] ]; }; 6 => { pointPairAndDone.lo ¬ ImagerTransformation.Transform[t, textData.points[4] ]; pointPairAndDone.hi ¬ ImagerTransformation.Transform[t, textData.points[7] ]; }; ENDCASE => SIGNAL Problem[msg: "Broken Invariant"]; pointPairGen.toGo ¬ pointPairGen.toGo - 1; pointPairGen.index ¬ IF pointPairGen.toGo=0 THEN 99 ELSE index+1; -- bump to the next available pair in the generator }; }; PointIsInBox: PROC [test: Point, box: BoundBoxObj] RETURNS [BOOL ¬ FALSE] = { RETURN[ NOT (test.x < box.loX OR test.x > box.hiX OR test.y < box.loY OR test.y > box.hiY) ]; }; TextClosestPoint: PROC [sliceD: SliceDescriptor, testPoint: Point, tolerance: REAL] RETURNS [bestPoint: Point, bestDist: REAL, bestNormal: Vector ¬ [0,-1], hitData: REF ANY, success: BOOL ¬ FALSE] = { index: NAT ¬ 9999; textData: TextData ¬ NARROW[sliceD.slice.data]; tightBox: BoundBox ¬ GGSliceOps.GetTightBox[sliceD.slice]; -- update may be required bigBox: BoundBoxObj ¬ [tightBox.loX-tolerance, tightBox.loY-tolerance, tightBox.hiX+tolerance, tightBox.hiY+tolerance, FALSE, FALSE]; -- box in GG coordinates IF PointIsInBox[testPoint, bigBox] THEN { textParts: TextParts ¬ NARROW[sliceD.parts]; localTestpoint: Point ¬ GGTransform.Transform[textData.inverse, testPoint]; localTolerance: REAL ¬ ABS[tolerance*MAX[textData.inverseScale.x, textData.inverseScale.y]]; -- MAX function is not correct when text is skewed. [bestDist, index, bestPoint, success] ¬ TextNearestPoint[textData.points, localTestpoint, localTolerance, textParts.points]; IF success THEN { bestPoint ¬ GGTransform.Transform[textData.fontData.transform, bestPoint]; bestDist ¬ Vectors2d.Distance[testPoint, bestPoint]; hitData ¬ NEW[TextHitDataObj ¬ [point: index, edge: -1, hitPoint: bestPoint] ]; }; }; }; TextClosestJointToHitData: PROC [sliceD: SliceDescriptor, mapPoint, testPoint: Point, hitData: REF ANY] RETURNS [jointD: SliceDescriptor, point: Point, normal: Vector] = { bestDist: REAL; newHitData: REF ANY; textHitData: TextHitData; textParts: TextParts ¬ NEW[TextPartsObj ¬ [points: ALL[FALSE], edges: ALL[FALSE], includeText: FALSE ] ]; success: BOOL ¬ FALSE; [point, bestDist, normal, newHitData, success] ¬ GGSliceOps.ClosestPoint[sliceD, mapPoint, GGUtility.plusInfinity]; textHitData ¬ NARROW[newHitData]; textParts.points[textHitData.point] ¬ TRUE; jointD ¬ GGSlice.DescriptorFromParts[slice: sliceD.slice, parts: textParts]; }; TextClosestSegment: PROC [sliceD: SliceDescriptor, testPoint: Point, tolerance: REAL] RETURNS [bestPoint: Point, bestDist: REAL, bestNormal: Vector ¬ [0, -1], hitData: REF ANY, success: BOOL ¬ FALSE] = { textData: TextData ¬ NARROW[sliceD.slice.data]; tightBox: BoundBox ¬ GGSliceOps.GetTightBox[sliceD.slice]; -- update may be required bigBox: BoundBoxObj ¬ [tightBox.loX-tolerance, tightBox.loY-tolerance, tightBox.hiX+tolerance, tightBox.hiY+tolerance, FALSE, FALSE]; -- box in GG coordinates IF PointIsInBox[testPoint, bigBox] THEN { seg: NAT ¬ 9999; textHitData: TextHitData; textParts: TextParts ¬ NARROW[sliceD.parts]; localTestpoint: Point ¬ GGTransform.Transform[textData.inverse, testPoint]; localTolerance: REAL ¬ ABS[tolerance*MAX[textData.inverseScale.x, textData.inverseScale.y]]; -- MAX function is not correct when text is skewed. [bestDist, seg, bestPoint, success] ¬ TextNearestEdge[textData.points, localTestpoint, localTolerance, textParts.edges]; IF success THEN { bestPoint ¬ GGTransform.Transform[textData.fontData.transform, bestPoint]; bestDist ¬ Vectors2d.Distance[testPoint, bestPoint]; hitData ¬ textHitData ¬ NEW[TextHitDataObj ¬ [point: -1, edge: seg, hitPoint: bestPoint] ]; bestNormal ¬ Vectors2d.Sub[testPoint, bestPoint]; }; }; }; TextNearestPoint: PROC [points: TextPoints, testPoint: Point, tolerance: REAL ¬ 1E6, mask: TextPointArray ] RETURNS [bestDist: REAL, bestPoint: NAT, bestXY: Point, success: BOOL ¬ FALSE] = { thisPoint: Point; thisDist2, bestDist2: REAL; tolerance2: REAL ¬ tolerance*tolerance; index: NAT ¬ 0; bestXY ¬ [-1.0, -1.0]; bestDist2 ¬ 1E12; -- better be big enough bestPoint ¬ 9999; FOR index IN [0..textMaxPoints) DO IF mask[index] THEN { thisPoint ¬ points[index]; -- use tightBox relative coordinates thisDist2 ¬ Vectors2d.DistanceSquared[thisPoint, testPoint]; IF thisDist2 < bestDist2 THEN { bestDist2 ¬ thisDist2; bestPoint ¬ index; bestXY ¬ thisPoint; }; }; ENDLOOP; bestDist ¬ RealFns.SqRt[bestDist2]; IF bestDist < tolerance THEN success ¬ TRUE; }; TextNearestEdge: ENTRY PROC [points: TextPoints, testPoint: Point, tolerance: REAL ¬ 1E6, mask: TextEdgeArray ¬ ALL[TRUE] ] RETURNS [bestDist: REAL, bestSeg: NAT, bestPoint: Point, success: BOOL ¬ FALSE] = { thisPoint: Point; thisDist2, bestDist2: REAL; tolerance2: REAL ¬ tolerance*tolerance; index: NAT ¬ 0; bestPoint ¬ [-1.0, -1.0]; bestDist2 ¬ 1E12; -- better be big enough bestSeg ¬ 9999; FOR index IN [0..textMaxEdges) DO IF mask[index] THEN { SELECT index FROM 0 => Lines2d.FillEdge[points[0], points[3], scratchEdge ]; 1 => Lines2d.FillEdge[points[3], points[11], scratchEdge ]; 2 => Lines2d.FillEdge[points[11], points[8], scratchEdge ]; 3 => Lines2d.FillEdge[points[8], points[0], scratchEdge ]; 4 => Lines2d.FillEdge[points[1], points[9], scratchEdge ]; 5 => Lines2d.FillEdge[points[2], points[10], scratchEdge ]; 6 => Lines2d.FillEdge[points[4], points[7], scratchEdge ]; ENDCASE => ERROR; thisPoint ¬ Lines2d.NearestPointOnEdge[testPoint, scratchEdge]; thisDist2 ¬ Vectors2d.DistanceSquared[thisPoint, testPoint]; IF thisDist2 < bestDist2 THEN { bestDist2 ¬ thisDist2; bestSeg ¬ index; bestPoint ¬ thisPoint; }; }; ENDLOOP; bestDist ¬ RealFns.SqRt[bestDist2]; IF bestDist < tolerance THEN success ¬ TRUE; }; TextSetStrokeColor: PROC [slice: Slice, parts: SliceParts, color: Imager.Color, setHow: ATOM, history: HistoryEvent] = { textData: TextData ¬ NARROW[slice.data]; SELECT setHow FROM $Set => textData.shadowColor ¬ color; $ChangeHue => { newColor: Color ¬ GGUtility.ChangeHue[textData.shadowColor, color]; textData.shadowColor ¬ newColor; }; ENDCASE => ERROR; }; TextGetStrokeColor: PROC [slice: Slice, parts: SliceParts] RETURNS [color: Imager.Color, isUnique: BOOL ¬ TRUE] = { textData: TextData ¬ NARROW[slice.data]; color ¬ textData.shadowColor; }; TextSetDefaults: PROC [slice: Slice, parts: SliceParts, defaults: DefaultData, history: HistoryEvent] = { textData: TextData ¬ NARROW[slice.data]; textParts: TextParts ¬ NARROW[parts]; router: MsgRouter ¬ Feedback.EnsureRouter[$Gargoyle]; IF textParts=NIL OR textParts.includeText THEN { [] ¬ SetTextFont[slice, defaults.font, router, NIL]; textData.color ¬ defaults.textColor; IF defaults.dropShadowOn THEN DropShadowOn[slice, defaults.dropShadowOffset, NIL] ELSE DropShadowOff[slice, NIL]; textData.shadowColor ¬ defaults.dropShadowColor; }; }; TextSetFillColor: PROC [slice: Slice, parts: SliceParts, color: Imager.Color, setHow: ATOM, history: HistoryEvent] = { textData: TextData ¬ NARROW[slice.data]; SELECT setHow FROM $Set => textData.color ¬ color; $ChangeHue => { newColor: Color ¬ GGUtility.ChangeHue[textData.color, color]; textData.color ¬ newColor; }; ENDCASE => ERROR; }; TextGetFillColor: PROC [slice: Slice, parts: SliceParts] RETURNS [color: Imager.Color, isUnique: BOOL ¬ TRUE] = { textData: TextData ¬ NARROW[slice.data]; RETURN[textData.color]; }; dropScale: REAL ¬ 0.1; -- ratio of drop shadow displacement to font size scratchEdge: GGBasicTypes.Edge ¬ Lines2d.CreateEmptyEdge[]; -- global storage used in TextNearestEdge END. . GGSliceImplB.mesa Contents: Implements Text slice class in Gargoyle. Copyright Ó 1987, 1988, 1989, 1991, 1992 by Xerox Corporation. All rights reserved. Kurlander, July 17, 1987 11:30:09 am PDT Eisenman, July 22, 1987 6:32:21 pm PDT Pier, December 1, 1992 11:14 am PST Bier, on December 3, 1992 5:28 pm PST Doug Wyatt, April 16, 1992 4:07 pm PDT Text Slice Class [Artwork node; type 'ArtworkInterpress on' to command tool] Text Points as shown Text Edges: 0 => left, 1=> top, 2=> right, 3=> bottom, 4 => baseline, 5 => horizontal centerline, 6 => vertical centerline IMPORTANT CONVENTION FOR TEXT SLICES: The procedure TextSetBoundBox maintains not only the bound and tight boxes but the data array textData.points as well. Any code that uses textData.points must be sure and call TextGetBoundBox or TextGetTightBox before using textData.points to force update of textData.points if needed before use. Text-Only Data and Procs The following 7 values are of interest: left: = 0.0. right: = Width[string]. middle: (left + right) / 2. top: base: 0.0 bottom: center: (top + bottom) / 2 TopBottomAnchor: TYPE = {lower, base, center, upper}; Filled in by TextClosestPoint, TextClosestSegment, in same order as TextPointArray and TextEdgeArray. -1 means not hit Fundamentals Drawing Transforming Textual Description Parts Part Generators Hit Testing Style Other font related values must be filled in by client call to SetTextFont A fontName is a font designation with family, size, and face, like Helvetica23BI or TimesRoman6.95 (real font sizes are OK) can't user IO.GetInt because a string like Helvetica9BI causes it to fail! The text slice's font will be set using fontData. IF andTransform, fontData.transform will be used unmodified ELSE the translation component of the existing slice transform will be used. If fontData.transform=NIL, the scale component of the new transform is set to fontData.scale and the rotation component to 0. Avoid finding an already substituted font. Just use the substitution. Why we get any error other than Imager.Error is not clear, but we do IF screenFont # NIL THEN textData.screenFont ¬ screenFont ELSE textData.screenFont ¬ GGFont.AlternateFont[fontData, textData.trueFont, $visible]; defer setting screenFont until it is really needed Takes a complex name like "xerox/pressfonts/Helvetica-MIR" and a font size and returns a simple name like Helvetica10I The tight box should still be OK. Modified starting from ImagerRasterShowImpl.BasicShowChar. Text Class Procs Fundamental ASSERT: extents all positivesubtract left and descent, add right and ascent base is the "origin" point for the text bounding box calculations always ignored parts and calculated the boxes for the complete rope. So be it. updating boxes in place saves lots of allocations, as does trafficking in ImagerBox.Boxes which are RECORDS The slice tightBox, by definition, contains the control points, which can be far away from the actual extent of the rope. So it goes. Update textData.box amd find bounding boxes The slice.boundBox will be bigger, taking into account the fontExtentRope bounds and the possibility of drop shadows and selected and hot joints. This is necessary for refresh to work correctly. keep the text points where they always were for backwards compatibility set up cache for fast case next time around, because we ignore parts and always get the boxes for the full slice live dangerously and return the actual boundBoxes inside the slice instead of copies since thats what the cache does anyway. ASSERT: extents all positivesubtract left and descent, add right and ascent cpBottom, cpCenter, and cpTop are the y coordinates to use for the Text control points IF textData.displayStyle = print OR textData.screenFont = NIL Update textData.box amd find boundBox. GGModelTypes.SliceBoundBoxProc GGModelTypes.SliceBoundBoxProc GGModelTypes.SliceCopyProc Just ignore parts and copy the whole slice. MakeTextSlice allocates but does not assign fields: box, points, fontData, and seg. MakeTextSlice puts dummy values into trueFont, screenFont, inverse, inverseScale, and lineSpacing GGModelTypes.SliceRestoreProc from and to must be nearly identically structured slices, probably because from was made as a copy of to. Restore the contents of "to", getting all non-REF values from "from". Good luck. to.nullDescriptor is always valid to.fullDescriptor is unused Drawing GGModelTypes.SliceDrawPartsProc GGModelTypes.SliceDrawSelectionFeedbackProc If any joint was hit, feedback all the joints If any edge was hit, feedback that edge and joints that touch that edge Assume a simple descriptor with only one edge selected called with the dc transformation already set to object coordinates N.B.: camera quality and displayStyle take precedence over textData displayStyle GGCoreOps.SetColor[dc, IF textData.color=NIL THEN Imager.black ELSE textData.color]; called with the dc transformation already set to object coordinates GGCoreOps.SetColor[dc, IF textData.shadowColor=NIL THEN Imager.black ELSE textData.shadowColor]; N.B.: camera quality and displayStyle take precedence over textData displayStyle This proc should only be called by callers who really need the screenFont. Callers that are copying or otherwise using the screenFont REF should do so directly and not call this proc. GGModelTypes.SliceDrawTransformProc Transforming GGModelTypes.SliceTransformProc IF InvolvesScaling[transform] THEN textData.screenFont ¬ GGFont.AlternateFont[fontData, textData.trueFont, $visible]; relys on deferred evaluation if the transformed screenFont is every really needed Textual Description GGModelTypes.SliceDescribeProc GGModelTypes.SliceFileoutProc Write a description of yourself onto stream f. GGModelTypes.SliceFileinProc Read a description of yourself from stream f. Get the comfortable bit if it is there Get the text Get the prefix Get the family and face and size now manufacture the stuff needed by SetTextFont. Use the parser! have: prefix, family, face, transform, size, preferredSize get color Handle Drop Shadows Hit Testing GGModelTypes.SliceNewPartsProc GGModelTypes.SliceUnionPartsProc GGModelTypes.SliceMovingPartsProc If anything is moving, everything is dragging. Otherwise, leave it in the background. GGModelTypes.SliceAugmentPartsProc For every edge, add its corresponding points Text slices have only one "segment" whose entire purpose is to contain properties in seg.props Text slices have only one "segment" whose entire purpose is to contain properties in seg.props GGModelTypes.SliceClosestPointProc GGModelTypes.SliceClosestJointToHitDataProc GGModelTypes.SliceClosestSegmentProc hitData _ NIL; -- automatically done by compiler Finds the point which is in mask and nearest to testPoint (and its distance from testPoint). Number the points as shown in the picture above Finds the edge which is in mask and nearest to testPoint (and its distance from testPoint). Style ÊOÙ•NewlineDelimiter –"cedarcode" style™codešœ™KšÏnœ+™3Kšœ ÏeœI™TK™(Kšœ&™&K™#K™%K™&Idefault™L™K™—šÏk ˜ KšœöŸœŸœŸœ˜¿—K˜š œŸœŸœÏc/˜KKšŸœ…Ÿœ7˜ÇKšŸœ Ÿ˜K˜I artworkFigure• Interpressˆ Interpress/Xerox/3.0  f j k j¡¥“ÄWB ¤ ¨  ì ¡£={ ¢ ¨ r jÄç­ ¢ ¨¡¡¨ÄWB ¤ ¨ r j¡¥“ÄWB ¤ ¨¡¡¨ r j¡¡¨¢¯“1Ë1¡¹¡¡¨¢¯“18¡¹¡¡¨¢¯“88Ë¡¹¡¡¨¢¯“8Ë1Ë¡¹ k é¢¯“¡¡¨¢·“1Ú™8Ž˜¢¯“¡¡¨¢·“8Ä™1Ž˜¢¯“¡¡¨¢·“Ä)™˘ r jÄù Ä­ ¢ ¨ÅXeroxÅ PressFontsÅ Helvetica-brr£¡ “® ¤ ” •  —¡¡¨  ŠÁ0–¡¡¨ k é r jij0Ä«  ¢ ¨ÅXeroxÅ PressFontsÅ Helvetica-brr£¡ “® ¤ ” •  —¡¡¨  ŠÁ1–¡¡¨ k é r jÄ×0Ämn ¢ ¨ÅXeroxÅ PressFontsÅ Helvetica-brr£¡ “® ¤ ” •  —¡¡¨  ŠÁ2–¡¡¨ k é r jÄ1…eÄáòŸ ¢ ¨ÅXeroxÅ PressFontsÅ Helvetica-brr£¡ “® ¤ ” •  —¡¡¨  ŠÁ3–¡¡¨ k é r jÄ"§Ì ¤Ä\¤£Ä‰Ãp ¢ ¥ ¨ÅXeroxÅ PressFontsÅ Helvetica-mrr£¡ “ª ¤ ” •  —¡¡¨  ŠÁ qwil jrut–¡¡¨ k é r j·Ä9 ¢ ¨ÅXeroxÅ PressFontsÅ Helvetica-brr£¡ “® ¤ ” •  —¡¡¨  ŠÁ4–¡¡¨ k é r jÄ ùÄÏ  ¢ ¨ÅXeroxÅ PressFontsÅ Helvetica-brr£¡ “® ¤ ” •  —¡¡¨  ŠÁ5–¡¡¨ k é r jÄ^ØWÄA¯ÿ ¢ ¨ÅXeroxÅ PressFontsÅ Helvetica-brr£¡ “® ¤ ” •  —¡¡¨  ŠÁ6–¡¡¨ k é r jÄMÄå  ¢ ¨ÅXeroxÅ PressFontsÅ Helvetica-brr£¡ “® ¤ ” •  —¡¡¨  ŠÁ7–¡¡¨ k é r jÄEÄE ¢ ¨ÅXeroxÅ PressFontsÅ Helvetica-brr£¡ “® ¤ ” •  —¡¡¨  ŠÁ8–¡¡¨ k é r jĆwRÄJÅ= ¢ ¨ÅXeroxÅ PressFontsÅ Helvetica-brr£¡ “® ¤ ” •  —¡¡¨  ŠÁ9–¡¡¨ k é r jÄM×0Ä mký ¢ ¨ÅXeroxÅ PressFontsÅ Helvetica-brr£¡ “® ¤ ” •  —¡¡¨  ŠÁ10–¡¡¨ k é r jÄM­0Ä  ¢ ¨ÅXeroxÅ PressFontsÅ Helvetica-brr£¡ “® ¤ ” •  —¡¡¨  ŠÁ11–¡¡¨ k é k é k é k g•Artwork Interpress•Bounds:0.0 mm xmin 0.0 mm ymin 117.1222 mm xmax 37.04167 mm ymax –G39.86389 mm topLeading 39.86389 mm topIndent 1.411111 mm bottomLeading šœ=™=IartworkCaption™N™zK˜Kšœ Ÿœ˜&Kšœ Ÿœ˜,KšœŸœ˜#Kšœ Ÿœ˜)KšœŸœ˜Kšœ Ÿœ˜-KšœŸœ˜/KšœŸœ ˜5Kšœ Ÿœ˜+Kšœ Ÿœ˜*KšœŸœ˜Kšœ Ÿœ˜!KšœŸœ˜1KšœŸœ˜!KšœŸœ˜3KšœŸœ"˜9KšœŸœ#˜;KšœŸœ&˜AKšœŸœ˜1KšœŸœ˜!Kšœ Ÿœ˜'KšœŸœ!˜7KšœŸœ$˜=KšœŸœ#˜;KšœŸœ!˜5Kšœ Ÿœ˜+KšœŸœ˜!Kšœ Ÿœ˜+KšœŸœ˜1KšœŸœ ˜5KšœŸœ#˜;Kšœ Ÿœ˜'Kšœ Ÿœ˜+KšœŸœ'˜Kšœ Ÿœ ,˜>K˜K˜—Kšœ ŸœŸœ˜'šœŸœŸœ˜Kšœv™vKšœ ˜9Kšœ ˜6K˜K˜—K˜Kš œŸœŸœŸœ˜$K˜šœŸœŸœŸœ˜CKšŸœ ˜ šœŸœ˜K˜ šœ "˜?Kš¡ ™ —Kšœ˜Kšœ œ˜;K˜Kšœ˜šœ˜Kš¡™—Kšœ˜Kšœ˜Kšœ˜K˜!K˜1K˜1K˜#šœ'˜'Kš¡ ™ —šœ˜Kš¡™—K˜K˜K˜˜Kš¡™—K˜K˜%K˜K˜K˜K˜K˜K˜šœ)˜)Kš¡™—K˜+K˜3K˜3K˜/K˜K˜K˜!˜Kš¡ ™ —Kšœ˜K˜1Kšœ3˜3Kšœ#˜#Kšœ1˜1K˜'K˜+˜/Kš¡™—K˜K˜#K˜#K˜K˜K˜#K˜#K˜#K˜#K˜K˜K˜K˜K˜K˜K˜#K˜"K˜—K˜K˜—š œŸœŸœ ŸœAŸœŸœŸœSŸœ˜€šœŸœ˜(Kšœ  '˜GK˜Kšœ ˜,Kšœ˜KšœŸœ˜K•StartOfExpansion[]šœ# ˜3Kšœ˜Kšœ ˜ Kšœ ˜ Kšœ˜Kšœ#˜#Kšœ˜K–N[p0: Lines2dTypes.Point, p1: Lines2dTypes.Point, props: LIST OF REF ANY]šœAŸœ˜EKšœ˜—Kš¡I™IKšœ! #˜DšœŸœ ˜K˜&Kšœ˜KšœŸœ˜ KšœŸœŸœŸœ˜&Kšœ% '˜LKšœ% '˜LKšœ Ÿ˜Kšœ˜—K˜K˜—š œŸœŸœŸœŸœŸœ˜Cš Ÿœ!ŸœŸœŸœŸœ˜ "˜yKšŸœŸœ ¡œh ˜¼Kšœ œ6 ˜pKšœ ¡œ( ž˜àK˜K™GKš¡œ( œt ˜ÖK˜Kš p™pKšœ$ ˜AKšœŸœ 8˜OšœŸœ 3˜OK™|—K˜K˜K˜K˜—J˜š œŸœ'ŸœŸœŸœ9Ÿœ˜¡KšŸœF™LKšœV™VKšœ ˜ Kšœ Ÿœ˜K˜3K˜K˜5K˜K˜šŸœŸœŸœ˜K˜ K˜K˜—šŸœ˜K˜?K˜K˜ K˜—K˜"K˜K˜—šœŸœ$ŸœŸœ"˜jšœŸœ#Ÿœ#˜cKšœŸœ ˜(K˜'Kšœ Ÿœ˜Kšœ7Ÿœ˜˜GK˜KšœQ˜QK˜K˜—Kšœ¡œ ˜QšŸœŸœŸœ .˜BKšœ$ ˜AK˜K˜KšœŸœ˜KšœŸœ 3˜OK˜—K˜K˜—šœŸœ#Ÿœ˜SKšœ™Kš ŸœŸœŸœŸœŸœ  ˜IKš ŸœŸœ Ÿœ&Ÿœ- ˜ŒK˜K˜—K–# -- [cluster: GGModelTypes.Cluster]šœŸœŸœ˜–# -- [cluster: GGModelTypes.Cluster]šœŸœ#Ÿœ˜SKšœ™KšŸœŸœŸœŸœŸœŸœŸœŸœ  ˜tKšœŸœ ŸœŸœ"˜[Kš ŸœŸœŸœŸœ ˜^K˜K˜—š œŸœ$ŸœŸœŸœŸœ ˜XKšœ™Kšœ+™+Kšœ˜Kšœ˜KšœŸœ ˜(Kšœ ŸœŸœ˜K˜5K˜Kš œ4¡œ¡œ¡œ¡œ™SKš œ%¡œ¡ œ¡œ¡ œ¡ ™aK˜ÙKšœ Ÿœ˜"K˜˜*KšœD˜DKšœ˜KšœŸœ˜Kšœ Ÿœ˜ Kšœ"˜"Kšœ˜Kšœ!˜!—KšŸœŸœ ŸœŸœ˜K˜K–[seg: GGSegmentTypes.Segment]˜8K˜9Kšœ6˜6KšŸœŸœ ˜K˜K˜—š œŸœŸœ˜.Kšœ™K™¼Kš ŸœŸœŸœŸœŸœŸœ˜!KšŸœŸœŸœ˜"KšŸœŸœŸœ˜"šŸ˜KšœŸœ ˜'KšœŸœ ˜#K˜šŸœ ¡ œŸœ˜Kš œ¡œÏtœ£ ¡œ£˜Kš œ¡œ£œ£ ¡œ£˜"Kšœ £œ£ œ£˜Kšœ £œ£ œ£˜Kšœ£œ£ œ£˜$Kšœ£œ£ œ£˜$Kšœ£œ£ œ £˜(Kšœ£œ£ œ £˜,Kšœ£œ£ œ£˜"Kšœ£œ£ œ £˜,Kšœ£œ£ œ £˜,Kšœ£œ£ œ £˜,Kšœ£œ£ œ£˜4Kšœ£œ£ œ£˜4Kšœ£œ£ œ £˜*Kšœ£œ£ œ £˜*Kšœ £œ£ œ£˜K˜—KšŸœ˜K˜K˜Kšœ) ˜:KšœŸœ ˜5KšœŸœ ˜2KšœŸœ ˜5KšœŸœ ˜4K™!K™K˜K˜&K˜K˜K˜K˜"K˜K˜ K™KšŸœ˜—K˜K˜—K™Kšœ™š œŸœ$Ÿœ:Ÿœ˜}Kšœ™š œŸœ˜KšŸœŸœ-˜JKšœ&˜&Kšœ˜—KšœŸœ ˜(KšœŸœ˜%KšŸœ ŸœŸœŸœ˜OK˜K˜—šœŸœ”Ÿœ˜¼K™+Kšœ2ŸœŸœ˜?Kšœ˜K˜)KšœŸœ ˜(K˜8Kš ŸœŸœŸœŸœŸœ˜IKš ŸœŸœŸœ ŸœŸœŸœ˜2KšœŸœ˜(KšœŸœ ˜ Kšœ!ŸœŸœ˜EKšœŸœŸœ˜Kšœ#ŸœŸœ˜IKšŸœ ŸœA˜TKšŸœŸœD˜\Kš ŸœŸœ ŸœŸœŸœ1˜aKšŸœ˜—K˜—Kšœ"˜"K˜K˜—šœŸœWŸœK˜ÆKšœ Ÿœ ˜š ŸœŸœŸœŸœŸœ˜4šœŸœ˜šœŸœ8˜OKšœ"˜"Kšœ+˜+Kšœ)˜)KšŸœŸœ*˜?KšŸœŸœ*˜?K˜—KšœŸœ Ÿœ˜K˜[context: Imager.Context, strokeJoint: Imager.StrokeJoint]šœ!˜!K˜K™-šŸœŸœŸœŸœŸœŸœŸœŸ˜NKšœ0˜0KšŸœ˜—K™GK™6šŸœŸœŸœŸœŸœŸœŸœŸ˜KšŸœŸœ˜šŸœŸ˜Kšœœ"˜9Kšœœ˜2Kšœœ$˜;Kšœœ˜1Kšœœ˜1Kšœœ˜2Kšœœ"˜9KšŸœŸœ˜—KšŸœ˜K˜—KšŸœ˜—K˜—KšœŸœ ˜(KšœŸœ˜-K˜'KšŸœŸœ ŸœŸœ#˜RK˜—˜K˜——šœŸœNŸœ˜eK™CKšŸœŸœŸœŸœ˜"K™PKš œŸœŸœŸœŸœŸœ¡ œ ˜ Kš œŸœŸœŸœŸœ™TKšœ( 3˜[Kšœ0˜0Kšœ )˜GKšœ3 ,˜_Kšœ#˜#K˜K˜—šœŸœNŸœ˜kK™CšœŸœ˜KšœŸœX˜aKš œŸœŸœŸœŸœ™`Kšœ. /˜]Kšœ0˜0Kšœ˜K™PKš œŸœŸœŸœŸœŸœ¡ œ ˜ Kšœ )˜GKšœ3 ,˜_Kšœ#˜#K˜—KšŸœŸœŸœŸœ˜(Kšœ$˜$K˜K˜—šÐbn œŸœŸœ˜LK™¸šŸœŸœŸœŸœ˜:KšŸœG˜K—K˜K˜—š¤ œŸœ$Ÿœ{˜¹Kšœ#™#š¤ œŸœ˜Kšœ˜KšŸœŸœ&Ÿœ˜JKšœŸœ˜&K˜—KšœŸœ ˜(Kšœ'˜'K˜K˜—K™Kšœ ™ š œŸœ$Ÿœn˜¨K™KšœŸœ ˜(K˜'K˜PK˜CK˜HK˜EK˜QKšŸœŸœS™ušŸœŸœŸœ˜=K™Q—Kšœ˜K˜K˜—K™šœŸœ2ŸœŸœ˜YKšœ Ÿœ˜KšœŸœ2˜šŸœŸœ *˜HK–)[rope: ROPE, oldStream: STREAM _ NIL]šœ:ŸœŸœŸœ Ÿœ Ÿœ ŸœŸœ ˜ÊKšœ£œ ˜5K˜—šŸœ ˜Kšœ£œ ˜5K˜,K˜,Kš œ:ŸœŸœŸœŸœ ˜˜K˜—K˜—˜Kš¡ ™ —šŸœŸœ˜K˜,KšŸœ ŸœŸœŸœ˜GK˜K– [f: STREAM]š¡™—šŸœŸœ˜K˜%šŸœŸœ˜K˜*K˜.KšŸœŸœŸœŸœ˜KK˜—K˜—šŸœŸœ˜K˜—šŸœŸœ "˜?K˜&K˜;K˜MK˜ K˜—šŸœŸœŸœ˜ K˜5K˜MK˜ Kšœ /˜2—K˜šŸœŸœ˜K˜%K˜—KšŸœ˜šŸœŸœ ˜3Kšœ ŸœŸœ˜K˜#K˜!Kš œŸœ ŸœŸœŸœ˜>K˜K˜—šŸœŸœ ˜2K˜K– [f: STREAM]˜$K˜K˜—K˜wK–ß[slice: GGModelTypes.Slice, prefix: ROPE, family: ROPE, face: ROPE, faceRope: ROPE, scale: REAL _ 1.0, fontScale: REAL _ 1.0, transform: ImagerTransformation.Transformation, feedback: ViewerClasses.Viewer]šœŸœ (˜FKš ŸœŸœ2ŸœŸœŸœ˜WšŸœŸœŸœ˜KšœŸœ˜0š ŸœŸœŸœŸœŸœŸœŸ˜@Pšœ Ÿœ˜(PšŸœ˜—K˜—K˜Kš¡˜Kš¡ ¤˜'Kš¡ ¤˜'K–2[slice: GGModelTypes.Slice, lineSpacing: REAL]˜Kšœ/Ÿœ˜4K˜K˜K˜—K™Kšœ ™ š œŸœ˜+KšœŸœ˜%KšœŸœŸœ˜KšœŸœŸœ˜KšœŸœ˜K˜K˜—š œŸœŸœŸœŸœ˜?KšœŸœ˜%KšŸœ ŸœŸœŸœŸœŸœŸœŸœŸœŸœ˜rK˜K˜—š œŸœŸœŸœŸœ˜K˜K˜—šœŸœ|Ÿœ9˜ÑKšœ!™!KšœV™VKš œŸœŸœŸœ ŸœŸœŸœ˜fKš œŸœŸœŸœ ŸœŸœŸœ˜lšŸœŸœ ˜KšŸœ˜—K˜K˜—šœŸœ7˜WKšœŸœ˜(KšœŸœ˜/K˜'KšœŸœŸœ˜Kš ŸœŸœŸœ$Ÿœ 9˜ˆšŸœŸœŸœŸ˜+šŸœŸœ˜K˜ZK˜K˜—KšŸœ˜—K˜K˜—šœŸœŸœ'˜iKšœŸœ˜(Kš ŸœŸœŸœ$Ÿœ 9˜ˆKšœŸœ(Ÿœ ˜\šŸœŸœŸœŸ˜)KšŸœŸœ+˜DKšŸœ˜—K˜K˜—šœŸœŸœ˜_K™^KšœŸœ˜(Kš œ ŸœŸœ ŸœŸœ Ÿœ ˜hšŸœŸœŸœŸ˜)KšŸœŸœ  ˜JKšŸœ˜—K˜K˜—šœŸœ$Ÿœ˜_K™^KšœŸœ ˜(KšŸœŸœŸœŸœŸœ ŸœŸœ ˜pK˜K˜—šœŸœŸœ*Ÿœ.˜{KšŸœŸœŸœŸœŸœŸœŸœ˜6šŸœ˜KšœŸœ˜6K˜K˜Kšœ˜—K˜K˜—š œŸœ*Ÿœ.˜ršŸœ ŸœŸœŸœ˜+KšœŸœ˜KšŸœ˜K˜—šŸœ˜K˜*KšœŸœ˜/K˜'KšœŸœ˜,KšœŸœ˜KšœŸœ˜šŸœ!ŸœŸ˜BKšŸœŸœŸœ +˜QKšŸœ˜—K˜aK˜ KšœŸœŸœŸœ  4˜nK˜—K˜K˜—šœŸœ2Ÿœ6˜†šŸœŸœŸœŸœ˜3KšœŸœ˜KšŸœ˜K˜—šŸœ˜K˜.KšœŸœ˜/KšœŸœ˜,K˜FKšœŸœ˜KšœŸœ˜šŸœ%ŸœŸ˜EKšŸœŸœŸœ )˜NKšŸœ˜—Kšœ#Ÿœ %˜NšŸœŸœ 4˜Fšœ˜K˜MK˜MK˜—šœ˜K˜MK˜NK˜—šœ˜K˜NK˜MK˜—šœ˜K˜MK˜MK˜—šœ˜K˜MK˜MK˜—šœ˜K˜MK˜NK˜—šœ˜K˜MK˜MK˜—KšŸœŸœ"˜3—K˜*KšœŸœŸœŸœ  3˜uK˜—K˜K˜—š œŸœ!ŸœŸœŸœ˜MKš ŸœŸœŸœŸœŸœ˜]K˜K˜—šœŸœ8ŸœŸœŸœ(ŸœŸœ ŸœŸœ˜ÈK™"KšœŸœ˜KšœŸœ˜/Kšœ; ˜TKšœwŸœŸœ ˜žšŸœ!Ÿœ˜)KšœŸœ˜,K˜KKšœŸœŸœ Ÿœ5 3˜K˜|šŸœ Ÿœ˜K˜JK˜4Kšœ ŸœB˜OK˜—K˜—K˜K˜—š œŸœ@ŸœŸœŸœ<˜«Kšœ+™+Kšœ Ÿœ˜Kšœ ŸœŸœ˜Kšœ˜Kš œŸœŸœŸœ ŸœŸœŸœ˜iKšœ ŸœŸœ˜Kšœ œY˜sKšœŸœ ˜!Kšœ&Ÿœ˜+K–?[slice: GGModelTypes.Slice, parts: GGModelTypes.SliceParts]˜LK˜K˜—šœŸœ8ŸœŸœŸœ)ŸœŸœ ŸœŸœ˜ËKšœ$™$Kšœ Ÿœ !™1KšœŸœ˜/Kšœ; ˜TKšœwŸœŸœ ˜žšŸœ!Ÿœ˜)KšœŸœ˜Kšœ˜KšœŸœ˜,K˜KKšœŸœŸœ Ÿœ5 3˜K˜xšŸœ Ÿœ˜K˜JK˜4KšœŸœ@˜[K˜1K˜—K˜—K˜K˜—šœŸœ3ŸœŸœ Ÿœ ŸœŸœŸœ˜¾Kšœ™Kšœ˜KšœŸœ˜Kšœ Ÿœ˜'KšœŸœ˜K˜Kšœ ˜)K˜šŸœŸœŸ˜"šŸœ Ÿœ˜Kšœ $˜?K˜<šŸœŸœ˜K˜K˜K˜K˜—K˜—KšŸœ˜—K˜#KšŸœŸœ Ÿœ˜,K˜K˜—šœŸœŸœ3ŸœŸœŸœŸœ Ÿœ ŸœŸœŸœ˜ÏKšœ]™]Kšœ˜KšœŸœ˜Kšœ Ÿœ˜'KšœŸœ˜K˜Kšœ ˜)K˜šŸœŸœŸ˜!šŸœ Ÿœ˜šŸœŸ˜Kšœ:˜:Kšœ;˜;Kšœ;˜;Kšœ:˜:Kšœ:˜:Kšœ;˜;Kšœ:˜:KšŸœŸœ˜—K˜?K˜<šŸœŸœ˜K˜K˜K˜K˜—K˜—KšŸœ˜—K˜#KšŸœŸœ Ÿœ˜,K˜—K˜Kšœ™šœŸœ@Ÿœ˜xKšœŸœ ˜(šŸœŸ˜K˜%˜K˜CK˜ K˜—KšŸœŸœ˜—Kšœ˜K˜—š œŸœ#Ÿœ!ŸœŸœ˜sKšœŸœ ˜(K˜Kšœ˜K˜—šœŸœT˜iKšœŸœ ˜(KšœŸœ˜%K˜5šŸœ ŸœŸœŸœ˜0Kšœ¡ œŸœ˜4K˜$Kš ŸœŸœ0ŸœŸœŸœ˜qK˜0K˜—Kšœ˜K˜—šœŸœ@Ÿœ˜vKšœŸœ ˜(šŸœŸ˜K˜˜K˜=K˜K˜—KšŸœŸœ˜—Kšœ˜K˜—š œŸœ#Ÿœ!ŸœŸœ˜qKšœŸœ ˜(KšŸœ˜Kšœ˜—K˜Kšœ Ÿœ 1˜HKšœ< )˜eK˜KšŸœ˜—…— v—