GGSliceImplB.mesa
Contents: Implements Text slice class in Gargoyle.
Copyright Ó 1987, 1988, 1989 by Xerox Corporation. All rights reserved.
Pier, June 10, 1992 12:28 pm PDT
Bier, on May 21, 1992 3:22 pm PDT
Kurlander, July 17, 1987 11:30:09 am PDT
Eisenman, July 22, 1987 6:32:21 pm PDT
Doug Wyatt, December 18, 1989 4:04:31 pm PST
Text Slice Class
DIRECTORY
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, ImagerFont, ImagerSys, ImagerTransformation, ImagerTypeface, IO, Lines2d, NodeStyle, PFS, Real, RealFns, Rope, SF, Vector2, Vectors2d;
GGSliceImplB: CEDAR PROGRAM
IMPORTS Convert, Feedback, FileNames, GGBoundBox, GGCoreOps, GGFont, GGFromImager, GGParseIn, GGParseOut, GGProps, GGScene, GGSegment, GGShapes, GGSlice, GGSliceOps, GGTransform, GGUtility, Imager, ImagerFont, ImagerSys, ImagerTransformation, ImagerTypeface, IO, Lines2d, PFS, Real, RealFns, Rope, Vector2, Vectors2d
EXPORTS GGSlice = BEGIN
[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
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 = Imager.VEC;
Vector: TYPE = GGBasicTypes.Vector;
Viewer: TYPE = ViewerClasses.Viewer;
WalkProc: TYPE = GGModelTypes.WalkProc;
Problem: PUBLIC SIGNAL [msg: Rope.ROPE] = Feedback.Problem;
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: <the origin of the first character> = 0.0.
right: <the right escapement of the last character> = Width[string].
middle: (left + right) / 2.
top: <the ascender extent of the string "acbdefghijklmnopqrstuvwxyzACBDEFGHIJKLMNOPQRSTUVWXYZ">
base: 0.0
bottom: <the descender extent of the string "acbdefghijklmnopqrstuvwxyzACBDEFGHIJKLMNOPQRSTUVWXYZ">
center: (top + bottom) / 2
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};
TopBottomAnchor: TYPE = {lower, base, center, upper};
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: BOOLFALSE,
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 [
Filled in by TextClosestPoint, TextClosestSegment, in same order as TextPointArray and TextEdgeArray. -1 means not hit
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
Fundamentals
getBoundBox: TextGetBoundBox,
getTransformedBoundBox: GGSlice.GenericTransformedBoundBox,
getTightBox: TextGetTightBox,
copy: TextCopy,
restore: TextRestore,
Drawing
buildPath: NoOpBuildPath,
drawBorder: NoOpDrawBorder,
drawParts: TextDrawParts,
drawTransform: TextDrawTransform,
drawSelectionFeedback: TextDrawSelectionFeedback,
drawAttractorFeedback: TextDrawAttractorFeedback,
saveSelections: NoOpSaveSelections,
remakeSelections: NoOpRemakeSelections,
Transforming
transform: TextTransform,
Textual Description
describe: TextDescribe,
describeHit: TextDescribeHit,
fileout: TextFileout,
filein: TextFilein,
Parts
isEmptyParts: TextIsEmptyParts,
isCompleteParts: TextIsCompleteParts,
newParts: TextNewParts,
unionParts: TextUnionParts,
differenceParts: TextDiffParts,
movingParts: TextMovingParts,
augmentParts: TextAugmentParts,
alterParts: NoOpAlterParts,
setSelectedFields: NoOpSetSelectedFields,
Part Generators
pointsInDescriptor: TextPointsInDescriptor,
walkPointsInDescriptor: TextWalkPointsInDescriptor,
pointPairsInDescriptor: TextPointPairsInDescriptor,
segmentsInDescriptor: TextSegmentsInDescriptor,
walkSegments: TextWalkSegments,
nextPoint: TextNextPoint,
nextPointPair: TextNextPointPair,
nextSegment: TextNextSegment,
Hit Testing
closestPoint: TextClosestPoint,
closestJointToHitData: TextClosestJointToHitData,
closestPointAndTangent: NoOpClosestPointAndTangent,
closestSegment: TextClosestSegment,
filledPathsUnderPoint: NoOpFilledPathsUnderPoint,
lineIntersection: NoOpLineIntersection,
circleIntersection: NoOpCircleIntersection,
hitDataAsSimpleCurve: NoOpHitDataAsSimpleCurve,
Style
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: BOOLFALSE, 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]
]];
Other font related values must be filled in by client call to SetTextFont
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.ROPENARROW[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: BOOLFALSE] = {
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] = {
A fontName is a font designation with family, size, and face, like Helvetica23BI or TimesRoman6.95 (real font sizes are OK)
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.STREAMIO.RIS[fontName];
[fontFamily, ----] ← IO.GetTokenRope[stream: fontStream, breakProc: DigitBreak ! IO.EndOfStream => SIGNAL FontNameError];
fontSize ← GetNumber[fontStream ! IO.EndOfStream => SIGNAL FontNameError];
can't user IO.GetInt because a string like Helvetica9BI causes it to fail!
[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: BOOLTRUE] = {
RETURN[SetTextFontAux[slice, fontData, router, FALSE, history]];
};
SetTextFontAndTransform: PUBLIC PROC [slice: Slice, fontData: FontData, router: MsgRouter, history: HistoryEvent] RETURNS [ success: BOOLTRUE] = {
RETURN[SetTextFontAux[slice, fontData, router, TRUE, history]];
};
SetTextFontAux: PROC [slice: Slice, fontData: FontData, router: MsgRouter, andTransform: BOOLFALSE, history: HistoryEvent, substituteFont, trueFont, screenFont: Imager.Font ← NIL] RETURNS [success: BOOLTRUE] = {
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.
transform, oldTransform: ImagerTransformation.Transformation;
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 {
Avoid finding an already substituted font. Just use the substitution.
textData.trueFont ← substituteFont;
}
ELSE {
textData.trueFont ← ImagerFont.Find[fontData.literal, substituteWithWarning !
PFS.Error => GOTO PFSError;
ImagerSys.FileError => {
Feedback.PutF[router, oneLiner, $Complaint, "ImagerSys.FileError [%g, %g] during ImagerFont.Find %g", [atom[code]], [rope[explanation]], [rope[fontData.literal]] ];
GOTO ImagerSysFileError;
};
Imager.Error => GOTO Abort;
Imager.Warning => {
IF fontData.substituteOK THEN {
fontData.substituted ← TRUE;
RESUME;
}
ELSE GOTO NotFound;
};
];
IF fontData.substituted THEN Feedback.PutF[router, oneLiner, $Warning, "%g substituted for %g", [rope[ImagerFont.Name[textData.trueFont]]], [rope[fontData.literal]] ];
textData.trueFontImagerFont.Scale[textData.trueFont, IF fontData.substituted THEN 1.0 ELSE 1.0/fontData.storedSize];
};
fontData.transform ← transform; -- must be done before computing alternate font
IF screenFont # NIL THEN textData.screenFont ← screenFont
ELSE textData.screenFont ← GGFont.AlternateFont[fontData, textData.trueFont, $visible];
textData.screenFont ← IF screenFont # NIL THEN screenFont ELSE NIL;
defer setting screenFont until it is really needed.
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.STREAMIO.ROS[];
IF transform#NIL THEN GGParseOut.WriteFactoredTransformationVEC[scratch, transform];
Feedback.PutF[router, oneLiner, $Complaint, "No Such Font: %g %g %g %g", [rope[fontData.literal]], [rope[IO.RopeFromROS[scratch]]], [real[fontData.storedSize]], [real[fontData.designSize]] ];
success ← FALSE;
};
ImagerSysFileError => success ← FALSE;
END;
};
GetLooksDataRope: PUBLIC PROC [slice: Slice] RETURNS [Rope.ROPE] = {
scratch: IO.STREAMIO.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.PutF["Text color: %g ", [rope[GGUtility.DescribeColor[textData.color]]] ]
ELSE
scratch.PutF["%g", [rope["No Text Fill Color "]]];
IF textData.dropShadowOn THEN {
IF textData.shadowColor#NIL
THEN
scratch.PutF["ShadowColor: %g ", [rope[GGUtility.DescribeColor[textData.shadowColor]]] ]
ELSE scratch.PutF["%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.STREAMIO.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.PutFR["%g %g %g %g %g", [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] = {
Takes a complex name like "xerox/pressfonts/Helvetica-MIR" and a font size and returns a simple name like Helvetica10I
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: BOOLFALSE, 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: BOOLTRUE, shadowColor: Imager.Color, useShadowColor: BOOLFALSE, 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];
The tight box should still be OK.
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
feedback: Viewer ← NIL; -- use the MessageWindow
SplinesFromTextString: PROC [dc: Imager.Context] = {
SplinesFromChar: PROC [char: ImagerFont.XChar] = {
Modified starting from ImagerRasterShowImpl.BasicShowChar.
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: BOOLTRUE;
tVec: Imager.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];
};
Text Class Procs
Fundamental
StringEscapement: PROC [font: ImagerFont.Font, string: ImagerFont.XStringProc, amplifySpace: REAL] RETURNS [ImagerFont.VEC] = {
sum: ImagerFont.VEC ← [0, 0];
charAction: ImagerFont.XCharProc ~ {
width: ImagerFont.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: INTINT.LAST] RETURNS [ImagerFont.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";
ComputeTextBounds: PROC [font, namelessFont: Font, rope: Rope.ROPE, amplifySpace: REAL] RETURNS [middle, right, bottom, top, cpBottom, cpCenter, cpTop: REAL] = {
ASSERT: extents all positive—subtract left and descent, add right and ascent
cpBottom, cpCenter, and cpTop are the y coordinates to use for the Text control points
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;
halfCP: REAL = GGModelTypes.halfJointSize + 1;
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 textData.screenFont = NIL
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];
Update textData.box amd find boundBox.
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] = {
GGModelTypes.SliceBoundBoxProc
IF slice.boxValid AND parts=NIL THEN RETURN[slice.boundBox]; -- fast case
RETURN[TextGetBoundBoxAux[slice, parts].boundBox]; -- cache update if possible
};
TextGetTightBox: PROC [slice: Slice, parts: SliceParts] RETURNS [box: BoundBox] = {
GGModelTypes.SliceBoundBoxProc
IF slice.boxValid AND parts=NIL THEN RETURN[slice.tightBox]; -- fast case
RETURN[TextGetBoundBoxAux[slice, parts].tightBox]; -- cache update if possible
};
TextCopy: PROC [slice: Slice, parts: SliceParts ← NIL] RETURNS [copy: LIST OF Slice] = {
GGModelTypes.SliceCopyProc
Just ignore parts and copy the whole slice.
copySlice: Slice;
copyData: TextData;
textData: TextData ← NARROW[slice.data];
success: BOOLTRUE;
router: MsgRouter ← Feedback.EnsureRouter[$Gargoyle];
MakeTextSlice allocates but does not assign fields: box, points, fontData, and seg.
MakeTextSlice puts dummy values into trueFont, screenFont, inverse, inverseScale, and lineSpacing
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] = {
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.
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.nullDescriptor is always valid
to.fullDescriptor is unused
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;
};
Drawing
TextDrawParts: PROC [slice: Slice, parts: SliceParts ← NIL, dc: Imager.Context, camera: GGModelTypes.Camera, quick: BOOL] = {
GGModelTypes.SliceDrawPartsProc
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] = {
GGModelTypes.SliceDrawSelectionFeedbackProc
jointSize: REAL = GGModelTypes.jointSize;
halfJointSize: REAL = GGModelTypes.halfJointSize;
hotJointSize: REAL = GGModelTypes.hotJointSize;
halfHotJointSize: REAL = GGModelTypes.halfHotJointSize;
slowNormal, slowHot, completeNormal, completeHot: BOOLFALSE;
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 any joint was hit, feedback all the joints
IF hitParts.points#ALL[FALSE] THEN FOR point: INTEGER IN [0..textMaxPoints) DO
GGShapes.DrawCP[dc, pts[point], camera.cpScale];
ENDLOOP;
If any edge was hit, feedback that edge and joints that touch that edge
Assume a simple descriptor with only one edge selected
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] = {
called with the dc transformation already set to object coordinates
IF textData.color=NIL THEN RETURN;
N.B.: camera quality and displayStyle take precedence over textData displayStyle
Imager.SetFont[dc, IF (camera.quality=quality OR camera.displayStyle=print OR textData.displayStyle=print) THEN textData.trueFont ELSE GetScreenFont[textData]];
GGCoreOps.SetColor[dc, IF textData.color=NIL THEN Imager.black ELSE textData.color];
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] = {
called with the dc transformation already set to object coordinates
DoDrawDropShadow: PROC = {
tVec: Imager.VEC ← [x: textData.dropShadowOffset.x*dropScale, y: textData.dropShadowOffset.y*dropScale];
GGCoreOps.SetColor[dc, IF textData.shadowColor=NIL THEN Imager.black ELSE textData.shadowColor];
GGCoreOps.SetColor[dc, textData.shadowColor]; -- must happen before text transform is applied
Imager.ConcatT[dc, textData.fontData.transform];
Imager.TranslateT[dc, tVec];
N.B.: camera quality and displayStyle take precedence over textData displayStyle
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] ~ {
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.
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] = {
GGModelTypes.SliceDrawTransformProc
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];
};
Transforming
TextTransform: PROC [slice: Slice, parts: SliceParts ← NIL, transform: ImagerTransformation.Transformation, editConstraints: EditConstraints, history: HistoryEvent] = {
GGModelTypes.SliceTransformProc
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 ← GGFont.AlternateFont[fontData, textData.trueFont, $visible];
IF InvolvesScaling[transform] THEN textData.screenFont ← NIL;
relys on deferred evaluation if the transformed screenFont is every really needed
GGSlice.KillBoundBox[slice];
};
InvolvesScaling: PROC [transform: ImagerTransformation.Transformation] RETURNS [BOOL] = {
epsilon: REAL = 0.01;
sigma: ImagerTransformation.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];
};
Textual Description
TextDescribe: PROC [sliceD: SliceDescriptor] RETURNS [rope: Rope.ROPE] = {
GGModelTypes.SliceDescribeProc
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] = {
GGModelTypes.SliceFileoutProc
Write a description of yourself onto stream f.
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.PutF[" %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.PutF[") ls: %g", [real[GGSlice.GetTextLineSpacing[slice]]]];
};
TextFilein: PROC [f: IO.STREAM, version: REAL, router: MsgRouter, camera: Camera] RETURNS [slice: Slice] = {
GGModelTypes.SliceFileinProc
Read a description of yourself from stream f.
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: BOOLFALSE;
size: REAL ← 1.0;
preferredSize: REAL ← 1.0;
preferredSizeRope: Rope.ROPE ← "";
transform: ImagerTransformation.Transformation;
shadowColor: Imager.Color ← Imager.black;
textColor: Imager.Color ← Imager.black;
dropShadowOn, good: BOOLFALSE;
dropShadowOffset: Vector ← [0.0, 0.0];
amplifySpace: REAL ← 1.0;
lineSpacing: REAL ← 1.0;
props: LIST OF Rope.ROPENIL;
Get the comfortable bit if it is there
IF version >= 8702.11 THEN {
fontDataComfortable ← GGParseIn.ReadBool[f, version].truth;
};
Get the text
IF version > 8601.22 THEN {
text ← f.GetRopeLiteral[];
}
ELSE {
GGParseIn.ReadWRope[f, "("];
text ← GGParseIn.ReadWWord[f];
GGParseIn.ReadWRope[f, ")"];
};
IF version < 8702.11 THEN { -- many old style formats
Get the prefix
IF version >= 8701.30 THEN {
prefix ← GGParseIn.ReadWWord[f];
}; -- ELSE uses default prefix
Get the family and face and size
IF version <= 8601.06 THEN { }-- use default family and face
ELSE IF version <= 8601.27 THEN { -- a simple name like "Helvetica" or "Gacha"
family ← GGParseIn.ReadWWord[f];
}
ELSE IF version <= 8605.12 THEN { -- a complex name like "xerox/pressfonts/Helvetica-BIR"
fontName ← GGParseIn.ReadWWord[f];
size ← GGParseIn.ReadWReal[f];
[] ← GGParseIn.ReadWWord[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.ReadWWord[f];
size ← GGParseIn.ReadWReal[f];
[] ← GGParseIn.ReadWWord[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.ReadWWord[f];
size ← GGParseIn.ReadWReal[f];
[family, face, ----] ← TFontParamsFromFontName[fontName];
}
ELSE IF version = 8701.30 THEN {-- an interim font format
ps: INT ← 0;
fail: BOOLFALSE;
[fail, ----, family, face] ← GGFont.OldParseFontData[f, FALSE, TRUE, TRUE];
IF fail THEN ERROR;
transform ← GGParseIn.ReadTransformation[f];
preferredSize ← GGParseIn.ReadWReal[f];
ps ← Real.Round[preferredSize];
IF Rope.Equal[prefix, tiogaPrefix, FALSE] THEN preferredSizeRopeIO.PutFR["%g", [integer[ps]] ];
};
now manufacture the stuff needed by SetTextFont. Use the parser!
have: prefix, family, face, transform, size, preferredSize
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.ReadWWord[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.ReadWReal[f];
fontData.designSize ← GGParseIn.ReadWReal[f];
fontData ← GGFont.ParseFontData[data: fontData, inStream: IO.RIS[name], literalP: TRUE ! GGFont.ParseError => CONTINUE;]; -- failures handled downstream
};
};
get color
IF version > 8605.28 THEN {
textColor ← GGParseIn.ReadColor[f, version];
IF textColor = NIL AND version < 9106.27 THEN textColor ← Imager.black;
};
Handle Drop Shadows
IF version >= 8609.26 THEN {
[dropShadowOn, good] ← GGParseIn.ReadBool[f, version];
IF NOT good THEN ERROR;
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.ReadWReal[f];
}
ELSE amplifySpace ← 1.0;
IF version>=8706.08 THEN { -- read in segment props
hasProps: BOOLFALSE;
GGParseIn.ReadWRope[f, "props: ( "];
hasProps ← GGParseIn.ReadBool[f, version].truth;
props ← IF hasProps THEN GGParseIn.ReadListOfRope[f] ELSE NIL;
GGParseIn.ReadWRope[f, ")"];
};
IF version>=8803.08 THEN { -- read in line spacing
GGParseIn.ReadWRope[f, "ls: "];
lineSpacing ← GGParseIn.ReadWReal[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.SetTextLineSpacing[slice, lineSpacing, NIL];
};
Hit Testing
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 [BOOLFALSE] = {
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] = {
GGModelTypes.SliceNewPartsProc
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] = {
GGModelTypes.SliceUnionPartsProc
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] = {
GGModelTypes.SliceMovingPartsProc
If anything is moving, everything is dragging. Otherwise, leave it in the background.
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] = {
GGModelTypes.SliceAugmentPartsProc
textParts: TextParts ← NARROW[sliceD.parts];
newParts: TextParts ← NEW[TextPartsObj ← [points: textParts.points, edges: textParts.edges, includeText: textParts.includeText] ];
For every edge, add its corresponding points
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] ];
IF NOT sliceD.slice.boxValid THEN [] ← TextGetBoundBox[sliceD.slice, NIL]; -- need 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: BOOLFALSE;
IF NOT sliceD.slice.boxValid THEN [] ← TextGetBoundBox[sliceD.slice, NIL]; -- need 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];
IF NOT sliceD.slice.boxValid THEN [] ← TextGetBoundBox[sliceD.slice, NIL]; -- need 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] = {
Text slices have only one "segment" whose entire purpose is to contain properties in seg.props
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] = {
Text slices have only one "segment" whose entire purpose is to contain properties in seg.props
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: BOOLFALSE] = {
GGModelTypes.SliceClosestPointProc
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: REALABS[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] = {
GGModelTypes.SliceClosestJointToHitDataProc
bestDist: REAL;
newHitData: REF ANY;
textHitData: TextHitData;
textParts: TextParts ← NEW[TextPartsObj ← [points: ALL[FALSE], edges: ALL[FALSE], includeText: FALSE ] ];
success: BOOLFALSE;
[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: BOOLFALSE] = {
GGModelTypes.SliceClosestSegmentProc
hitData ← NIL; -- automatically done by compiler
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: REALABS[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: BOOLFALSE] = {
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
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: PUBLIC PROC [points: TextPoints, testPoint: Point, tolerance: REAL ← 1E6, mask: TextEdgeArray ← ALL[TRUE] ] RETURNS [bestDist: REAL, bestSeg: NAT, bestPoint: Point, success: BOOLFALSE] = {
Finds the edge which is in mask and nearest to testPoint (and its distance from testPoint).
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;
};
Style
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: BOOLTRUE] = {
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: BOOLTRUE] = {
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.