diff options
Diffstat (limited to 'graphics/figput/javascript/development/layout.ts')
-rw-r--r-- | graphics/figput/javascript/development/layout.ts | 654 |
1 files changed, 654 insertions, 0 deletions
diff --git a/graphics/figput/javascript/development/layout.ts b/graphics/figput/javascript/development/layout.ts new file mode 100644 index 0000000000..e1e1c496f4 --- /dev/null +++ b/graphics/figput/javascript/development/layout.ts @@ -0,0 +1,654 @@ +/* +Code for page layout. The idea is a bit like Java Swing (or a zillion other +similar setups), but stripped down to only what I need. In particular, the +Panels are assumed to be stacked vertically. + +Everything is accessed through FullPanel, which consists of a list of +PagePanel objects. These are stacked vertically, and each one contains at +least one PDFPanel, and may contain FigurePanels. In the lingo of something +like Java, it would be better to think of PagePanel as a Container or a Box +since it's used for layout and doesn't directly do anything other than pass +things to its sub-parts. + +*/ + + +// This class is the only thing that should be touched by code outside +// this file. FullPanel.init() is called to set up page layout. Everything +// is static since there is only one window/canvas. + +class FullPanel { + + // The full document consists of a list of PagePanel objects. + // thePages[i] is the i-th page, counting from zero. + public static thePages : PagePanel[] = []; + + // The width of the widest page of the entire document. Don't access + // this directly; use getFullWidth(). + private static totalWidth : number = -1; + + + static init(specList : PageData[]) { + + // Add all the pages to this document. specList is PDFDocument.pageSpecs. + // Each element of specList corresponds to page. + // Call this once, when the program starts, to set up the page layout. + let cumV = 0; + for (let i = 0; i < specList.length; i++) + { + let pp = new PagePanel(i,specList[i],cumV); + this.thePages[i] = pp; + cumV += pp.h; + } + } + + static async renderAll( height : number ) { + + // Render every PagePanel in FullPanel.thePages. + + // Internal to this function, the height should be in "page pixels," + // meaning the number of pixels tall the destination canvas is at the + // current zoom ratio of the offsreen pages. + height = height / PDFDocument.getZoom(); + + // Because of the use of promises by pdf.js, rendering is broken into + // two steps: pre-render and the actual rendering. Pre-rendering + // generates a bunch of promises (the pages rendered offscreen) and + // rendering can't be done until those promises resolve. + let ps = []; + for (let i = 0; i < this.thePages.length; i++) + { + let p = this.thePages[i].preRender(height); + ps.push(p); + } + + // Don't return until this is done! + await Promise.all(ps); + + for (let i = 0; i < this.thePages.length; i++) + this.thePages[i].render(height); + + // Eliminate any excess pages from the buffer of offscreen pages. + // Do this after copying out, just in case there *is* some weird + // problem with race conditions. + PDFDocument.trimBuffer(); + } + + static totalHeight() : number { + + // Return the total height of all pages. This is used to set up + // the scroll bars. + let answer = 0; + for (let i = 0; i < this.thePages.length; i++) + answer += this.thePages[i].h; + + return answer; + } + + static getFullWidth() : number { + + // Return the width of the widest page. Most of the time, the pages of + // a document all have the same width, but they might not in some + // rare case. This is needed to center things left/right in the window. + if (this.totalWidth > 0) + return this.totalWidth; + + // Need to calculate it for the first time. + for (let i = 0; i < this.thePages.length; i++) + { + let pp = this.thePages[i]; + if (pp.w > this.totalWidth) + this.totalWidth = pp.w; + } + + return this.totalWidth; + } + + static mouseDown(x : number , y : number ) : void { + + // (x,y) is in pdf points, relative to the entire document. + // So y will be a huge number if it's on the umpteeth page. + + // To save CPU, I could start off by checking whether x is in + // [0,pageWidth] and return if it is not, but the user might want to + // place controls outside the page. This is unlikely, but possible. + + // Whoever had focus loses it. It may be taken up by some other widget + // (or the same widget again), but nobody has focus by default. + WidgetManager.focusOwner = null; + + // Figure out which page this is and hand it off. + let i = 0; + for ( ; i < this.thePages.length; i++) + { + if (this.thePages[i].v > y) + { + // Found the first and only page this could be. It was the + // page previous to this one. + i -= 1; + break; + } + } + + // If we got here then it had to be the very last page. + if (i === this.thePages.length) + i = this.thePages.length - 1; + + // Safety check: + if ((i == this.thePages.length) || (i < 0)) + return; + + this.thePages[i].mouseDown(x,y); + } + + static mouseMove(x : number , y : number ) : void { + + // As above. However, these are only of interest if some widget + // "owns" the mouse -- something was clicked on so that motion + // could mean something. + if (WidgetManager.mouseOwner === null) + return; + + // Although we know exactly which widget will ultimately get this + // event, it's easier to let this pass through the layout hierarchy, + // just as for mouseDown(), so that the coordinates are properly adjusted. + // BUG: Not DRY. + let i = 0; + for ( ; i < this.thePages.length; i++) + { + if (this.thePages[i].v > y) + { + i -= 1; + break; + } + } + + if (i === this.thePages.length) + i = this.thePages.length - 1; + + if ((i == this.thePages.length) || (i < 0)) + return; + + this.thePages[i].mouseMove(x,y); + } + + static mouseUp(x : number , y : number ) : void { + + // The big issue here is that the mouse was released so that ownership + // is once again up for grabs. In addition, certain widgets will want + // to know where the mouse was released. Buttons are a good example. + // You could have mouse-down on the button, then the user moves the + // mouse out of the button and releases it; the button should only be + // "clicked" if the mouse was released over the button. + // + // An annoying thing here is that the *only* widget that could care about + // this (at least, as designed) is the one that owns the mouse-down. + // As above, we want the event to pass through the layout hierarchy + // so that (x,y) is adjusted for the right frame, but the ultimate + // *consumer* of the event may not even be on the page where the + // mouse-up occured. + // BUG: Not DRY. + if (WidgetManager.mouseOwner === null) + return; + + let i = 0; + for ( ; i < this.thePages.length; i++) + { + if (this.thePages[i].v > y) + { + i -= 1; + break; + } + } + + if (i === this.thePages.length) + i = this.thePages.length - 1; + + if ((i == this.thePages.length) || (i < 0)) + return; + + this.thePages[i].mouseUp(x,y); + + // Whatever happened above, the mouse is now up for grabs. + WidgetManager.mouseOwner = null; + } + +} + + +// A PagePanel is the top-level thing, just under the canvas. Each +// PagePanel makes up a single page of the printed document. There's +// a list of them in FullPanel. It includes references to the PDFPanel +// and FigurePanel objects that it contains. + +class PagePanel { + + // Every panel has a vertical position within the entire document and height, + // in pdf pts. The vertical postion, v, is the top of the page, so the page + // extends from v to v+h. The caller must ensure that the Panels stack up + // correctly since there is no real page layout. + // + // In some ways the height, h, is redundant since it could be worked + // out from the heights of the individual sub-parts. In fact (see below) + // it *is* worked out by the constuctor, but it's easier to do it once + // and be done with it. This is the height of the page, as rendered, + // taking into account any mismatch due to extra "padding" in the figures + // (if there is any, and there often is not). + v = 0; + w = 0; + h = 0; + + // page number, counting from zero. + pageNum = 0; + + // The SubPanels that make up this PagePanel. + parts : SubPanel[] = []; + + + constructor(pageNum : number , pageSpec : PageData , v : number) { + + // This implicitly applies to the global (because everything is static) + // PDFDocument. pageNum is which page this is, counting from zero. + // pageSpec has the info about how the page breaks into pieces and the + // relevant figures. v is where this page lies in the entire vertical + // list of pages. + this.w = pageSpec.pageWidth; + this.v = v; + this.pageNum = pageNum; + + // Create the PDFPanels and FigurePanels in this PagePanel. + let s = pageSpec; + if (s.insertPoint.length == 0) + { + // There are no figures on this page. + let p = new PDFPanel(pageNum,this.v,0,0,this.v,this.w,s.pageHeight); + this.h = s.pageHeight; + this.parts = [p]; + } + else + { + // There are figures. + let srcV = 0; + let destV = 0; + let totalV = v; + for (let j = 0; j < s.insertPoint.length; j++) + { + // Bit of pdf above figure. + let p = new PDFPanel(pageNum,this.v,srcV,destV,totalV,this.w, + s.insertPoint[j]-srcV); + destV += s.insertPoint[j]-srcV; + totalV += s.insertPoint[j]-srcV; + + let f = new FigurePanel( this.v , destV , totalV , this.w , + s.deleteHeight[j] + s.aboveHeight[j] + s.belowHeight[j] , + s.aboveHeight[j] , s.belowHeight[j] , s.leftMargin , + s.textWidth , s.drawFcn[j] ); + + srcV = s.insertPoint[j] + s.deleteHeight[j]; + destV += s.deleteHeight[j] + s.aboveHeight[j] + s.belowHeight[j]; + totalV += s.deleteHeight[j] + s.aboveHeight[j] + s.belowHeight[j]; + + this.parts.push(p); + this.parts.push(f); + } + + // And the bit of pdf below the last figure on the page. + let p = new PDFPanel(pageNum,this.v,srcV,destV,totalV, + this.w,s.pageHeight - srcV); + this.parts.push(p); + + this.h = destV + s.pageHeight - srcV + } + } + + async preRender( height : number ) { + + // This renders the underlying page of pdf. It returns a promise + // so that the caller can wait for the promise to resolve before + // attempting to copy from the rendered page. + // + // The vpos is where the top of the ctx should be relative to the entire + // document, in pdf pts, and height is how much is visible, in rendered + // page pixels. + // The first question is whether any of this page is visible. + let vpos = window.scrollY; + if (this.v + this.h < vpos) + // Entire page is above the visible area. + return; + if (this.v > vpos + height) + // Entire page is below the visible area. + return; + + // Got here, so some portion of the page is visible. + await PDFDocument.render(this.pageNum); + } + + render( height : number ) : void { + + // Render every SubPanel of the current PagePanel. + + // BUG: Before returning, turn off any of these animations that are + // definitely not visible. + let vpos = window.scrollY; + if (this.v + this.h < vpos) + return; + if (this.v > vpos + height) + return; + + // Got here, so some portion of the page is visible. From here on, + // render everything, whether it's actually visible or not. + let ctx = ctxTopLevelAdjust(); + + // Call the parts of the page. These could be PDFPanel or FigurePanel + // objects. + for (let i = 0; i < this.parts.length; i++) + this.parts[i].render(); + + // BUG: I don't like this use of zoom here. Maybe no choice? + let z = PDFDocument.getZoom(); + + // Put a rectangle aroud the entire page. I'm not 100% convinced that + // I like this. + ctx.strokeStyle = "black"; + ctx.strokeRect(0,0,this.w*z,this.h*z); + } + + mouseDown(x : number , y : number ) : void { + + // Mouse clicked on this page. + + // y is given relative to the entire document; make it page-relative. + y -= this.v; + + // Only clicks on a figure could be of interest. + for (let i = 0; i < this.parts.length; i++) + { + // Either a PDFPanel or a FigurePanel. + let p = this.parts[i]; + if (p instanceof PDFPanel) + continue; + + // p must be a FigurePanel. + if ((p.destV <= y) && (y <= p.destV + p.h)) + return p.mouseDown(x,y); + } + } + + mouseMove(x : number , y : number ) : void { + + // As above. Note that this event could go to the "wrong" figure, + // but that's OK. Also, if the mouse is over a PDFPanel, and not + // a figure, then the event dies here, which is also OK. + y -= this.v; + + for (let i = 0; i < this.parts.length; i++) + { + let p = this.parts[i]; + if (p instanceof PDFPanel) + continue; + + if ((p.destV <= y) && (y <= p.destV + p.h)) + p.mouseMove(x,y); + } + } + + mouseUp(x : number , y : number ) : void { + + // As above, but the event can't be allowed to die. The owning widget + // must hear about the mouse up. At the same time, we can't just + // inform the widget directly of the mouse up since we also need to + // pass the correct coordinates. + y -= this.v; + + for (let i = 0; i < this.parts.length; i++) + { + let p = this.parts[i]; + + // This is different than above since *somebody* must take the event, + // and the relevant widget must hear about it. If the event is over + // a PDFPanel, then tell the widget using crazy coordinates. It + // doesn't matter exactly where the mouse was released; it only + // matters that it wasn't released anywhere near the widget. + if ((p.destV <= y) && (y <= p.destV + p.h)) + { + if (p instanceof PDFPanel) + WidgetManager.mouseOwner ! .mouseUp(10000000000000,10000000000000); + else + // Over a figure. + p.mouseUp(x,y); + } + } + } +} + + +// A PagePanel consists of one or more SubPanels. + +abstract class SubPanel { + + // The vertical position and height within a page, with zero being at the + // top, and measured in pdf points. This height, h, is the total height. + // There's no ambiguity to this height for PDFPanel subclasses, but for + // FigurePanel subclasses, it is the sum of the latex height + // (PageData.deleteHeight), plus any additional padding as given in + // PageData.aboveHeight/belowHeight. + destV = 0; + h = 0; + w = 0; + + // The position of this panel within the entire document. + // The only reason for this is the possible use of HTML DOM elements + // as widgets within a figure. I would prefer not to use those at all, + // but sometimes it's easier. See the NumberInputWidget for one example. + totalV = 0; + + + constructor(v : number , totalV : number , w : number , h : number ) { + this.destV = v; + this.totalV = totalV; + this.w = w; + this.h = h; + } + + // Will be filled in by sub-class. + public render( ) : void { + console.log("Error: called SubPanel.render()!"); + } + public mouseDown(x : number , y : number ) : void { + console.log("Error: called SubPanel.mouseDown()!"); + } + public mouseMove(x : number , y : number ) : void { + console.log("Error: called SubPanel.mouseMove()!"); + } + public mouseUp(x : number , y : number ) : void { + console.log("Error: called SubPanel.mouseUp()!"); + } + +} + + +// Used for portions of a page consisting of rendered pdf. + +class PDFPanel extends SubPanel { + + // The page numbers start at zero and positions are given in pdf points. + pageNum = 0; + srcV = 0; + + // BUG: I think I can fold this in elsewhere. It's the same as PagePanel.v. + offsetV = 0; + + constructor(pageNum : number , offsetV : number , srcV : number , destV : number , + totalV : number , w : number , h : number ) { + + // The pageNum is given relative to the global PDFDocument. The srcV + // and destV are locations relative to the page (in pdf points, with the + // top of the page at v=0) and h is the height of this piece, which + // is the same for src and dest. + super(destV,totalV,w,h); + this.pageNum = pageNum; + this.srcV = srcV; + this.offsetV = offsetV; + } + + render( ) : void { + + // Render a portion of the current page. + + let theCanvas = PDFDocument.getCanvas(this.pageNum) ; + if (theCanvas === null) + { + // I'm sure how this happens, but it does occasionally. + // It doesn't cause any noticable problems. It seems to happen + // if you move the scroll thumb too fast. + return; + } + + // BUG: I don't like this use of zoom here. Maybe no choice? + let z = PDFDocument.getZoom(); + + let ctx = ctxTopLevelAdjust(); + + // Adjust for scroll bar. + ctx.translate(0,(this.offsetV - window.scrollY) * z); + + // Arguments here: + // the source image (or canvas), + // the source (x,y), + // the source (width,height), + // the destination (x,y), + // the destination (width,height), + // It's confusing because optional things typically come after + // required things, but not here somehow. + ctx.drawImage(theCanvas, + 0,this.srcV * z,theCanvas.width,this.h * z, + 0,this.destV*z,theCanvas.width,this.h*z); + } +} + +class FigurePanel extends SubPanel { + + // Function name, as a string. + drawFcn : AugmentedDrawingFunction; + + // Page's v position. This is the position of the page on which + // this figure appears relative to the entire document. + pageV : number = 0; + + // This margin is the location of the left edge of the text, as + // reported by latex. + margin = 0; + + // Also from Latex (via figures.aux). This I have more confidence in. + textWidth = 0; + + // The height in SubPanel.h is the total height of the figure, including + // any padding above or below. The origin used for the figure occurs at + // a y-value that is PageData.belowHeight *above* the total height. + // lowerPadding is equal to PageData.belowHeight for this figure. + lowerPadding = 0; + upperPadding = 0; + + + constructor(pageV : number, destV : number , totalV : number , w : number , h : number , + upperPadding : number , lowerPadding : number , margin : number , + textWidth : number , drawFcn : AugmentedDrawingFunction) { + + // As for PDFPanel, plus the margin is the amount by which to shift the + // drawing to the right so that the origin is in line with the text.The + // drawFcn is the function provided through Latex. + // This is just the name of the function, as a string. + super(destV,totalV,w,h); + this.pageV = pageV; + this.upperPadding = upperPadding; + this.lowerPadding = lowerPadding; + this.margin = margin; + this.textWidth = textWidth; + this.drawFcn = drawFcn; + } + + render( ) : void { + + // Save this for widgets and animations to use. + let ctx = ctxTopLevelAdjust(); + + let z = PDFDocument.getZoom(); + ctx.translate(0,(this.pageV - window.scrollY) * z); + + // Erase the full width of the page. this.w is the correct width, + // but the origin will be shifted for drawing. So, erase, then + // return the origin to where it was and start over. + + // Shift to where the figure appears and erase. + ctx.translate(0,this.destV * z); + ctx.scale(z,z); + + // The small adjustment here is to prevent erasing the rectangle + // that encloses the entire page. + ctx.clearRect(1,0,this.w-2,this.h); + + // Return to the orginal t-matrix, then shift down and right before + // drawing the figure (and widgets). + // What we want is for the origin to be at the lower-right and + // right-handed, adjusted upwards by this.lowerPadding too. + ctx = ctxTopLevelAdjust(); + ctx.translate(0,(this.pageV - window.scrollY) * z); + ctx.translate(this.margin * z,(this.destV + this.h - this.lowerPadding) * z); + ctx.scale(1,-1); + ctx.scale(z,z); + + // Tack this FigurePanel onto the underlying figure-drawing code. + // The first time the figure is rendered, this is set, and it's re-set + // to the same value with every subsequent call. That seems like + // pointless extra work, but it gets the job done. + this.drawFcn.figurePanelClass = this; + this.drawFcn(ctx); + } + + mouseDown(x : number , y : number ) : void { + + // (x,y) is in pdf points, relative to the top-left of the page. + + // Adjust to be relative to the figure, but still LH, relative + // to the top of the figure. + y -= this.destV; + x -= this.margin; + + // y is now given relative to the top edge of the figure, getting + // larger as you go *down* the page. + + // Convert y to be RH relative to the correct origin, taking + // any padding into account. This is confusing. At this stage, + // y is the distance below the figure's top edge. Call that y0. + // We want the distance above the lower padding (if any); call + // that y1. The distance above the lower *edge* of the figure + // is this.h - y0, and from this we subtract the padding. + y = (this.h - y) - this.lowerPadding; + + // Pass to the relevant widget. + WidgetManager.mouseDown(this.drawFcn,x,y); + } + + mouseMove(x : number , y : number ) : void { + + // As above. + y -= this.destV; + x -= this.margin; + + y = (this.h - this.lowerPadding) - y; + + WidgetManager.mouseMove(this.drawFcn,x,y); + } + + mouseUp(x : number , y : number ) : void { + + // As above. + y -= this.destV; + x -= this.margin; + + y = (this.h - this.lowerPadding) - y; + + WidgetManager.mouseUp(this.drawFcn,x,y); + } +} + |