summaryrefslogtreecommitdiff
path: root/graphics/figput/javascript/development/layout.ts
diff options
context:
space:
mode:
Diffstat (limited to 'graphics/figput/javascript/development/layout.ts')
-rw-r--r--graphics/figput/javascript/development/layout.ts654
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);
+ }
+}
+