include plain_scaling; // After a transformation, produce new coordinate bounds. For paths that // have been added, this is only an approximation since it takes the bounds of // their transformed bounding box. private void addTransformedCoords(coords2 dest, transform t, coords2 point, coords2 min, coords2 max) { dest.push(t, point, point); // Add in all 4 corner coords, to properly size rectangular pictures. dest.push(t,min,min); dest.push(t,min,max); dest.push(t,max,min); dest.push(t,max,max); } // Adds another sizing restriction to the coordinates, but only if it is // maximal, that is, if under some scaling, this coordinate could be the // largest. private void addIfMaximal(coord[] coords, real user, real truesize) { // TODO: Test promoting coordinates for efficiency. for (coord c : coords) if (user <= c.user && truesize <= c.truesize) // Not maximal. return; // The coordinate is not dominated by any existing extreme, so it is // maximal and will be added, but first remove any coords it now dominates. int i = 0; while (i < coords.length) { coord c = coords[i]; if (c.user <= user && c.truesize <= truesize) coords.delete(i); else ++i; } // Add the coordinate to the extremes. coords.push(coord.build(user, truesize)); } private void addIfMaximal(coord[] dest, coord[] src) { // This may be inefficient, as it rebuilds the coord struct when adding it. for (coord c : src) addIfMaximal(dest, c.user, c.truesize); } // Same as addIfMaximal, but testing for minimal coords. private void addIfMinimal(coord[] coords, real user, real truesize) { for (coord c : coords) if (user >= c.user && truesize >= c.truesize) return; int i = 0; while (i < coords.length) { coord c = coords[i]; if (c.user >= user && c.truesize >= truesize) coords.delete(i); else ++i; } coords.push(coord.build(user, truesize)); } private void addIfMinimal(coord[] dest, coord[] src) { for (coord c : src) addIfMinimal(dest, c.user, c.truesize); } // This stores a list of sizing bounds for picture data. If the object is // frozen, then it cannot be modified further, and therefore can be safely // passed by reference and stored in the sizing data for multiple pictures. private struct freezableBounds { restricted bool frozen = false; void freeze() { frozen = true; } // Optional links to further (frozen) sizing data. private freezableBounds[] links; // Links to (frozen) sizing data that is transformed when added here. private static struct transformedBounds { transform t; freezableBounds link; }; private transformedBounds[] tlinks; // The sizing data. It cannot be modified once this object is frozen. private coords2 point, min, max; // A bound represented by a path. Using the path instead of the bounding // box means it will be accurate after a transformation by coordinates. private path[] pathBounds; // A bound represented by a path and a pen. // As often many paths use the same pen, we store an array of paths. private static struct pathpen { path[] g; pen p; void operator init(path g, pen p) { this.g.push(g); this.p = p; } } private static pathpen operator *(transform t, pathpen pp) { // Should the pen be transformed? pathpen newpp; for (path g : pp.g) newpp.g.push(t*g); newpp.p = pp.p; return newpp; } // WARNING: Due to crazy optimizations, if this array is changed between an // empty and non-empty state, the assignment of a method to // addPath(path,pen) must also change. private pathpen[] pathpenBounds; // Once frozen, the sizing is immutable, and therefore we can compute and // store the extremal coordinates. public static struct extremes { coord[] left, bottom, right, top; void operator init(coord[] left, coord[] bottom, coord[] right, coord[] top) { this.left = left; this.bottom = bottom; this.right = right; this.top = top; } } private static void addMaxToExtremes(extremes e, pair user, pair truesize) { addIfMaximal(e.right, user.x, truesize.x); addIfMaximal(e.top, user.y, truesize.y); } private static void addMinToExtremes(extremes e, pair user, pair truesize) { addIfMinimal(e.left, user.x, truesize.x); addIfMinimal(e.bottom, user.y, truesize.y); } private static void addMaxToExtremes(extremes e, coords2 coords) { addIfMaximal(e.right, coords.x); addIfMaximal(e.top, coords.y); } private static void addMinToExtremes(extremes e, coords2 coords) { addIfMinimal(e.left, coords.x); addIfMinimal(e.bottom, coords.y); } private extremes cachedExtremes = null; // Once frozen, getMutable returns a new object based on this one, which can // be modified. freezableBounds getMutable() { assert(frozen); var f = new freezableBounds; f.links.push(this); return f; } freezableBounds transformed(transform t) { // Freeze these bounds, as we are storing a reference to them. freeze(); var tlink = new transformedBounds; tlink.t = t; tlink.link = this; var b = new freezableBounds; b.tlinks.push(tlink); return b; } void append(freezableBounds b) { // Check that we can modify the object. assert(!frozen); //TODO: If b is "small", ie. a single tlink or cliplink, just copy the //link. // As we only reference b, we must freeze it to ensure it does not change. b.freeze(); links.push(b); } void addPoint(pair user, pair truesize) { assert(!frozen); point.push(user, truesize); } void addBox(pair userMin, pair userMax, pair trueMin, pair trueMax) { assert(!frozen); this.min.push(userMin, trueMin); this.max.push(userMax, trueMax); } void addPath(path g) { // This, and other asserts have been removed to speed things up slightly. //assert(!frozen); this.pathBounds.push(g); } void addPath(path[] g) { //assert(!frozen); this.pathBounds.append(g); } // To squeeze out a bit more performance, this method is either assigned // addPathToNonEmptyArray or addPathToEmptyArray depending on the state of // the pathpenBounds array. void addPath(path g, pen p); private void addPathToNonEmptyArray(path g, pen p) { //assert(!frozen); //assert(!pathpenBounds.empty()); var pp = pathpenBounds[0]; // Test if the pens are equal or have the same bounds. if (pp.p == p || (min(pp.p) == min(p) && max(pp.p) == max(p))) { // If this path has the same pen as the last one, just add it to the // array corresponding to that pen. pp.g.push(g); } else { // A different pen. Start a new bound and put it on the front. Put // the old bound at the end of the array. pathpenBounds[0] = pathpen(g,p); pathpenBounds.push(pp); } } void addPathToEmptyArray(path g, pen p) { //assert(!frozen); //assert(pathpenBounds.empty()); pathpenBounds.push(pathpen(g,p)); addPath = addPathToNonEmptyArray; } // Initial setting for addPath. addPath = addPathToEmptyArray; // Transform the sizing info by t then add the result to the coords // structure. private void accumulateCoords(transform t, coords2 coords) { for (var link : links) link.accumulateCoords(t, coords); for (var tlink : tlinks) tlink.link.accumulateCoords(t*tlink.t, coords); addTransformedCoords(coords, t, this.point, this.min, this.max); for (var g : pathBounds) { g = t*g; coords.push(min(g), (0,0)); coords.push(max(g), (0,0)); } for (var pp: pathpenBounds) { pair pm = min(pp.p), pM = max(pp.p); for (var g : pp.g) { g = t*g; coords.push(min(g), pm); coords.push(max(g), pM); } } } // Add all of the sizing info to the given coords structure. private void accumulateCoords(coords2 coords) { for (var link : links) link.accumulateCoords(coords); for (var tlink : tlinks) tlink.link.accumulateCoords(tlink.t, coords); coords.append(this.point); coords.append(this.min); coords.append(this.max); for (var g : pathBounds) { coords.push(min(g), (0,0)); coords.push(max(g), (0,0)); } for (var pp: pathpenBounds) { pair pm = min(pp.p), pM = max(pp.p); for (var g : pp.g) { coords.push(min(g), pm); coords.push(max(g), pM); } } } // Returns all of the coords that this sizing data represents. private coords2 allCoords() { coords2 coords; accumulateCoords(coords); return coords; } private void addLocalsToExtremes(transform t, extremes e) { coords2 coords; addTransformedCoords(coords, t, this.point, this.min, this.max); addMinToExtremes(e, coords); addMaxToExtremes(e, coords); if (pathBounds.length > 0) { addMinToExtremes(e, minAfterTransform(t, pathBounds), (0,0)); addMaxToExtremes(e, maxAfterTransform(t, pathBounds), (0,0)); } for (var pp : pathpenBounds) { if (pp.g.length > 0) { addMinToExtremes(e, minAfterTransform(t, pp.g), min(pp.p)); addMaxToExtremes(e, maxAfterTransform(t, pp.g), max(pp.p)); } } } private void addToExtremes(transform t, extremes e) { for (var link : links) link.addToExtremes(t, e); for (var tlink : tlinks) tlink.link.addToExtremes(t*tlink.t, e); addLocalsToExtremes(t, e); } private void addLocalsToExtremes(extremes e) { addMinToExtremes(e, point); addMaxToExtremes(e, point); addMinToExtremes(e, min); addMaxToExtremes(e, max); if (pathBounds.length > 0) { addMinToExtremes(e, min(pathBounds), (0,0)); addMaxToExtremes(e, max(pathBounds), (0,0)); } for(var pp : pathpenBounds) { pair m=min(pp.p); pair M=max(pp.p); for(path gg : pp.g) { if (size(gg) > 0) { addMinToExtremes(e,min(gg),m); addMaxToExtremes(e,max(gg),M); } } } } private void addToExtremes(extremes e) { for (var link : links) link.addToExtremes(e); for (var tlink : tlinks) tlink.link.addToExtremes(tlink.t, e); addLocalsToExtremes(e); } private static void write(extremes e) { static void write(coord[] coords) { for (coord c : coords) write(" " + (string)c.user + " u + " + (string)c.truesize); } write("left:"); write(e.left); write("bottom:"); write(e.bottom); write("right:"); write(e.right); write("top:"); write(e.top); } // Returns the extremal coordinates of the sizing data. public extremes extremes() { if (cachedExtremes == null) { freeze(); extremes e; addToExtremes(e); cachedExtremes = e; } return cachedExtremes; } // Helper functions for computing the usersize bounds. usermin and usermax // would be easily computable from extremes, except that the picture // interface actually allows calls that manually change the usermin and // usermax values. Therefore, we have to compute these values separately. private static struct userbounds { bool areSet=false; pair min; pair max; } private static struct boundsAccumulator { pair[] mins; pair[] maxs; void push(pair m, pair M) { mins.push(m); maxs.push(M); } void push(userbounds b) { if (b.areSet) push(b.min, b.max); } void push(transform t, userbounds b) { if (b.areSet) { pair[] box = { t*(b.min.x,b.max.y), t*b.max, t*b.min, t*(b.max.x,b.min.y) }; for (var z : box) push(z,z); } } void pushUserCoords(coords2 min, coords2 max) { int n = min.x.length; assert(min.y.length == n); assert(max.x.length == n); assert(max.y.length == n); for (int i = 0; i < n; ++i) push((min.x[i].user, min.y[i].user), (max.x[i].user, max.y[i].user)); } userbounds collapse() { userbounds b; if (mins.length > 0) { b.areSet = true; b.min = minbound(mins); b.max = maxbound(maxs); } else { b.areSet = false; } return b; } } // The user bounds already calculated for this data. private userbounds storedUserBounds = null; private void accumulateUserBounds(boundsAccumulator acc) { if (storedUserBounds != null) { assert(frozen); acc.push(storedUserBounds); } else { acc.pushUserCoords(point, point); acc.pushUserCoords(min, max); if (pathBounds.length > 0) acc.push(min(pathBounds), max(pathBounds)); for (var pp : pathpenBounds) if(size(pp.g) > 0) acc.push(min(pp.g), max(pp.g)); for (var link : links) link.accumulateUserBounds(acc); // Transforms are handled as they were in the old system. for (var tlink : tlinks) { boundsAccumulator tacc; tlink.link.accumulateUserBounds(tacc); acc.push(tlink.t, tacc.collapse()); } } } private void computeUserBounds() { freeze(); boundsAccumulator acc; accumulateUserBounds(acc); storedUserBounds = acc.collapse(); } private userbounds userBounds() { if (storedUserBounds == null) computeUserBounds(); assert(storedUserBounds != null); return storedUserBounds; } // userMin/userMax returns the minimal/maximal userspace coordinate of the // sizing data. As coordinates for objects such as labels can have // significant truesize dimensions, this userMin/userMax values may not // correspond closely to the end of the screen, and are of limited use. // userSetx and userSety determine if there is sizing data in order to even // have userMin/userMax defined. public bool userBoundsAreSet() { return userBounds().areSet; } public pair userMin() { return userBounds().min; } public pair userMax() { return userBounds().max; } // To override the true userMin and userMax bounds, first compute the // userBounds as they should be at this point, then change the values. public void alterUserBound(string which, real val) { // We are changing the bounds data, so it cannot be frozen yet. After the // user bounds are set, however, the sizing data cannot change, so it will // be frozen. assert(!frozen); computeUserBounds(); assert(frozen); var b = storedUserBounds; if (which == "minx") b.min = (val, b.min.y); else if (which == "miny") b.min = (b.min.x, val); else if (which == "maxx") b.max = (val, b.max.y); else { assert(which == "maxy"); b.max = (b.max.x, val); } } // A temporary measure. Stuffs all of the data from the links and paths // into the coords. private void flatten() { assert(!frozen); // First, compute the user bounds, taking into account any manual // alterations. computeUserBounds(); // Calculate all coordinates. coords2 coords = allCoords(); // Erase all the old data. point.erase(); min.erase(); max.erase(); pathBounds.delete(); pathpenBounds.delete(); addPath = addPathToEmptyArray; links.delete(); tlinks.delete(); // Put all of the coordinates into point. point = coords; } void xclip(real Min, real Max) { assert(!frozen); flatten(); point.xclip(Min,Max); min.xclip(Min,Max); max.xclip(Min,Max); // Cap the userBounds. userbounds b = storedUserBounds; b.min = (max(Min, b.min.x), b.min.y); b.max = (min(Max, b.max.x), b.max.y); } void yclip(real Min, real Max) { assert(!frozen); flatten(); point.yclip(Min,Max); min.yclip(Min,Max); max.yclip(Min,Max); // Cap the userBounds. userbounds b = storedUserBounds; b.min = (b.min.x, max(Min, b.min.y)); b.max = (b.max.x, min(Max, b.max.y)); } // Calculate the min for the final frame, given the coordinate transform. pair min(transform t) { extremes e = extremes(); if (e.left.length == 0) return 0; pair a=t*(1,1)-t*(0,0), b=t*(0,0); scaling xs=scaling.build(a.x,b.x); scaling ys=scaling.build(a.y,b.y); return (min(infinity, xs, e.left), min(infinity, ys, e.bottom)); } // Calculate the max for the final frame, given the coordinate transform. pair max(transform t) { extremes e = extremes(); if (e.right.length == 0) return 0; pair a=t*(1,1)-t*(0,0), b=t*(0,0); scaling xs=scaling.build(a.x,b.x); scaling ys=scaling.build(a.y,b.y); return (max(-infinity, xs, e.right), max(-infinity, ys, e.top)); } // Returns the transform for turning user-space pairs into true-space pairs. transform scaling(real xsize, real ysize, real xunitsize, real yunitsize, bool keepAspect, bool warn) { if(xsize == 0 && xunitsize == 0 && ysize == 0 && yunitsize == 0) return identity(); // Get the extremal coordinates. extremes e = extremes(); real sx; if(xunitsize == 0) { if(xsize != 0) sx=calculateScaling("x",e.left,e.right,xsize,warn); } else sx=xunitsize; /* Possible alternative code : real sx = xunitsize != 0 ? xunitsize : xsize != 0 ? calculateScaling("x", Coords.x, xsize, warn) : 0; */ real sy; if(yunitsize == 0) { if(ysize != 0) sy=calculateScaling("y",e.bottom,e.top,ysize,warn); } else sy=yunitsize; if(sx == 0) { sx=sy; if(sx == 0) return identity(); } else if(sy == 0) sy=sx; if(keepAspect && (xunitsize == 0 || yunitsize == 0)) return scale(min(sx,sy)); else return scale(sx,sy); } } struct bounds { private var base = new freezableBounds; // We should probably put this back into picture. bool exact = true; // Called just before modifying the sizing data. It ensures base is // non-frozen. // Note that this is manually inlined for speed reasons in a couple often // called methods below. private void makeMutable() { if (base.frozen) base = base.getMutable(); //assert(!base.frozen); // Disabled for speed reasons. } void erase() { // Just discard the old bounds. base = new freezableBounds; // We don't reset the 'exact' field, for backward compatibility. } bounds copy() { // Freeze the underlying bounds and make a shallow copy. base.freeze(); var b = new bounds; b.base = this.base; b.exact = this.exact; return b; } bounds transformed(transform t) { var b = new bounds; b.base = base.transformed(t); b.exact = this.exact; return b; } void append(bounds b) { makeMutable(); base.append(b.base); } void append(transform t, bounds b) { // makeMutable will be called by append. if (t == identity()) append(b); else append(b.transformed(t)); } void addPoint(pair user, pair truesize) { makeMutable(); base.addPoint(user, truesize); } void addBox(pair userMin, pair userMax, pair trueMin, pair trueMax) { makeMutable(); base.addBox(userMin, userMax, trueMin, trueMax); } void addPath(path g) { //makeMutable(); // Manually inlined here for speed reasons. if (base.frozen) base = base.getMutable(); base.addPath(g); } void addPath(path[] g) { //makeMutable(); // Manually inlined here for speed reasons. if (base.frozen) base = base.getMutable(); base.addPath(g); } void addPath(path g, pen p) { //makeMutable(); // Manually inlined here for speed reasons. if (base.frozen) base = base.getMutable(); base.addPath(g, p); } public bool userBoundsAreSet() { return base.userBoundsAreSet(); } public pair userMin() { return base.userMin(); } public pair userMax() { return base.userMax(); } public void alterUserBound(string which, real val) { makeMutable(); base.alterUserBound(which, val); } void xclip(real Min, real Max) { makeMutable(); base.xclip(Min,Max); } void yclip(real Min, real Max) { makeMutable(); base.yclip(Min,Max); } void clip(pair Min, pair Max) { // TODO: If the user bounds have been manually altered, they may be // incorrect after the clip. xclip(Min.x,Max.x); yclip(Min.y,Max.y); } pair min(transform t) { return base.min(t); } pair max(transform t) { return base.max(t); } transform scaling(real xsize, real ysize, real xunitsize, real yunitsize, bool keepAspect, bool warn) { return base.scaling(xsize, ysize, xunitsize, yunitsize, keepAspect, warn); } } bounds operator *(transform t, bounds b) { return b.transformed(t); }