Replies: 2 comments 2 replies
-
Messing around with This could be a better approach using
This seems to work quite well but some misalignments are still visible in certain cases. Just for fun but parallel typesetting is driving me nut, but it's a very interesting problem ;) Function for glue calculation on page transitions: -- Balance overflow content across the overflow frames.
local balanceOverflow = function(frame, contentHeight)
-- Get the opposite frame
local oppositeFrame = (frame == "left") and "right" or "left"
-- Calculate the height of the opposite frame
local oppositeHeight = calculateFrameHeight(oppositeFrame, typesetterPool[oppositeFrame])
-- Calculate the difference in height between the frames
local glueHeight = contentHeight - oppositeHeight
-- If the current frame is taller, add glue to the opposite frame
if glueHeight:tonumber() > 0 then
SU.debug(package._name, "Adding glue to overflow frame: ", oppositeFrame, " of height:", glueHeight)
table.insert(
typesetterPool[oppositeFrame].state.outputQueue,
SILE.types.node.vglue({ height = SILE.types.length(glueHeight) })
)
elseif glueHeight:tonumber() < 0 then
-- If the opposite frame is taller, add glue to the current frame
SU.debug(package._name, "Adding glue to overflow frame: ", oppositeFrame, " of height: ", -glueHeight)
table.insert(
typesetterPool[frame].state.outputQueue,
SILE.types.node.vglue({ height = SILE.types.length(-glueHeight) })
)
end
end
Modified -- Handles page-breaking logic for parallel frames.
local parallelPagebreak = function()
for _, thisPageFrames in ipairs(folioOrder) do
local hasOverflow = false
-- Collect overflow content for each frame.
-- We don't need to clear it after flushing the captured content,
-- because it will be cleared in the next page.
local overflowContent = {}
-- Initialize and process frames in one loop
for _, frame in ipairs(thisPageFrames) do
local typesetter = typesetterPool[frame]
typesetter:initFrame(typesetter.frame)
local thispage = {}
local linesToFit = typesetter.state.outputQueue
local targetLength = typesetter:getTargetLength():tonumber()
local currentHeight = 0
-- Fit content into the frame
while
#linesToFit > 0
and currentHeight + (linesToFit[1].height:tonumber() + linesToFit[1].depth:tonumber())
<= targetLength
do
local line = table.remove(linesToFit, 1)
currentHeight = currentHeight + (line.height:tonumber() + line.depth:tonumber())
table.insert(thispage, line)
end
if #linesToFit > 0 then
hasOverflow = true
overflowContent[frame] = linesToFit
-- Clear outputQueue for next page, because its content has been added to overflowContent.
typesetter.state.outputQueue = {}
else
-- Make sure that every frame in thisPageFrames has an entry in overflowContent.
-- Otherwise, balancing might fail when restoring overflow content.
overflowContent[frame] = {}
end
-- Output content on the current page
typesetter:outputLinesToPage(thispage)
end
-- End current page
SILE.documentState.documentClass:endPage()
if hasOverflow then
-- Start a new page
SILE.documentState.documentClass:newPage()
-- Restore overflow content to the frames
for _, frame in ipairs(thisPageFrames) do
local typesetter = typesetterPool[frame]
for _, line in ipairs(overflowContent[frame]) do
table.insert(typesetter.state.outputQueue, line)
end
end
-- Balance overflow content across frames after restoring the overflow content.
-- You can combine the balancing mechanism with the overflow content restoration,
-- because the balancing mechanism only needs to be called once the restoration is done.
for _, frame in ipairs(thisPageFrames) do
local typesetter = typesetterPool[frame]
local contentHeight = calculateFrameHeight(frame, typesetter)
balanceOverflow(frame, contentHeight)
end
-- Let \sync handle alignment and marking
SILE.call("sync")
end
end
end
|
Beta Was this translation helpful? Give feedback.
-
I believe I'm about 90% there with parallel frame alignment. I had some hard time dealing with "soft" glue (like While perfect line-by-line alignment might not be achieved with this way, the visual outcome is acceptable, I think. And here is an Example and Another. I have acknowledged the Hope, someone will come up with a better solution. My modification of the original package: local base = require("packages.base")
local package = pl.class(base)
package._name = "parallel"
-- Typesetter pool for managing typesetters for different frames (e.g., left and right frames).
local typesetterPool = {}
-- Stores layout calculations for each frame, such as height, marking and overflow tracking.
local calculations = {}
-- Specifies the order of frames for synchronizing and page-breaking logic.
local folioOrder = {}
-- A null typesetter used as a placeholder. This typesetter doesn't output any content.
local nulTypesetter = pl.class(SILE.typesetters.base)
nulTypesetter.outputLinesToPage = function() end -- Override to suppress output
-- Utility function: Iterate through all typesetters and apply a callback function to each.
local allTypesetters = function(callback)
local oldtypesetter = SILE.typesetter -- Save the current typesetter
for frame, typesetter in pairs(typesetterPool) do
SILE.typesetter = typesetter -- Switch to the current frame's typesetter
callback(frame, typesetter) -- Execute the callback
end
SILE.typesetter = oldtypesetter -- Restore the original typesetter
end
-- Utility function: Calculate the height of new material for a given frame.
local calculateFrameHeight = function(frame, typesetter)
local height = calculations[frame].cumulativeHeight or SILE.types.length()
for i = calculations[frame].mark + 1, #typesetter.state.outputQueue do
local lineHeight = typesetter.state.outputQueue[i].height + typesetter.state.outputQueue[i].depth
height = height + lineHeight
end
return height
end
-- Create dummy content to fill up the overflowed frames.
local createDummyContent = function(height, frame, offset)
-- Retrieve document.baselineskip and document.lineskip
-- which are tables with the following fields: height, depth, is_vglue, adjustment, width
local baselineSkip = SILE.settings:get("document.baselineskip")
SU.debug(package._name, "Baseline skip is ", baselineSkip)
-- Safely retrieve lineskip or default to 0
local lineskip = SILE.settings:get("document.lineskip") or SILE.types.length.new({ length = 0 })
SU.debug(package._name, "Line skip is ", lineskip)
-- Extract the absolute lengths
local baselineHeight = baselineSkip.height and baselineSkip.height:tonumber() or 0
local lineSkipHeight = lineskip.height and lineskip.height:tonumber() or 0
-- Combine the lengths to get the total line height
local lineHeight = baselineHeight + lineSkipHeight
SU.debug(package._name, "textHeight = " .. lineHeight)
-- local typesetter = typesetterPool[frame]
-- local lineHeight = typesetter.state.outputQueue[1].height + typesetter.state.outputQueue[1].depth
-- Calculate the number of lines
local numLines = math.floor(height:tonumber() / lineHeight)
-- Validate offset
offset = offset or 0
if offset >= numLines then
SU.warn("Offset is larger than number of lines available; no dummy content will be generated.")
return
end
-- Get the typesetter for the frame
local typesetter = typesetterPool[frame]
SILE.call("color", { color = "white" }, function()
typesetter:typeset("sile")
for i = 1, numLines - offset do
-- Add dummy content and a line break
SILE.call("break")
typesetter:typeset("sile")
end
end)
end
local balanceFramesWithDummyContent = function(offset)
local frameHeights = {}
local maxHeight = SILE.types.length(0)
-- Step 1: Measure the height of all frames and track the maximum height
allTypesetters(function(frame, typesetter)
local height = calculateFrameHeight(frame, typesetter)
frameHeights[frame] = height
if height > maxHeight then
maxHeight = height -- Track the tallest frame
end
SU.debug(package._name, "Height of frame ", frame, ": ", height)
end)
-- Step 2: Add dummy content to shorter frames to match the maximum height
allTypesetters(function(frame, typesetter)
local heightDifference = maxHeight - frameHeights[frame]
if heightDifference:tonumber() > 0 then
-- Add dummy content to balance the frame height
SILE.typesetter = typesetter
createDummyContent(SILE.types.length(heightDifference), frame, offset or 0)
SU.debug(package._name, "Added dummy content to frame ", frame, " to balance height by: ", heightDifference)
end
end)
end
-- Balances the height of content across frames by adding glue to the shorter frame.
local addBalancingGlue = function(height)
allTypesetters(function(frame, typesetter)
calculations[frame].heightOfNewMaterial = calculateFrameHeight(frame, typesetter)
local glue = height - calculations[frame].heightOfNewMaterial
if glue:tonumber() > 0 then
table.insert(typesetter.state.outputQueue, SILE.types.node.vglue({ height = glue }))
SU.debug(package._name, "Already added balancing glue of", glue, " to bottom of frame", frame)
end
-- calculations[frame].mark = #typesetter.state.outputQueue
-- SU.debug(package._name, "Mark for frame", frame, "is", calculations[frame].mark)
end)
end
-- Adds a flexible glue (parskip) to the bottom of each frame
-- This is decoupled from addBalancingGlue calculations, serving a simple purpose.
local addParskipToFrames = function(parskipHeight)
allTypesetters(function(_, typesetter)
table.insert(typesetter.state.outputQueue, SILE.types.node.vglue({ height = parskipHeight }))
end)
end
-- Handles page-breaking logic for parallel frames.
-- Modify parallelPagebreak to use dummy content
local parallelPagebreak = function()
for _, thisPageFrames in ipairs(folioOrder) do
local hasOverflow = false
local overflowContent = {}
-- Initialize and process frames
for _, frame in ipairs(thisPageFrames) do
local typesetter = typesetterPool[frame]
typesetter:initFrame(typesetter.frame)
local thispage = {}
local linesToFit = typesetter.state.outputQueue
local targetLength = typesetter:getTargetLength():tonumber()
local currentHeight = 0
while
#linesToFit > 0
and currentHeight + (linesToFit[1].height:tonumber() + linesToFit[1].depth:tonumber())
<= targetLength
do
local line = table.remove(linesToFit, 1)
currentHeight = currentHeight + (line.height:tonumber() + line.depth:tonumber())
table.insert(thispage, line)
end
if #linesToFit > 0 then
hasOverflow = true
overflowContent[frame] = linesToFit
typesetter.state.outputQueue = {}
else
overflowContent[frame] = {}
end
typesetter:outputLinesToPage(thispage)
end
SILE.documentState.documentClass:endPage()
if hasOverflow then
-- Start a new page
SILE.documentState.documentClass:newPage()
-- Restore overflow content to the frames
for _, frame in ipairs(thisPageFrames) do
local typesetter = typesetterPool[frame]
for _, line in ipairs(overflowContent[frame]) do
table.insert(typesetter.state.outputQueue, line)
end
end
-- Balance the frames using dummy text
balanceFramesWithDummyContent()
-- Let \sync handle alignment and marking
SILE.call("sync")
end
end
end
-- Initialization function for the package.
function package:_init(options)
base._init(self, options)
-- Initialize the null typesetter.
SILE.typesetter = nulTypesetter(SILE.getFrame("page"))
-- Ensure the `frames` option is provided.
if type(options.frames) ~= "table" then
SU.error("Package parallel must be initialized with a set of appropriately named frames")
end
-- Set up typesetters for each frame.
for frame, typesetter in pairs(options.frames) do
typesetterPool[frame] = SILE.typesetters.base(SILE.getFrame(typesetter))
typesetterPool[frame].id = typesetter
typesetterPool[frame].buildPage = function() end -- Disable auto page-building
-- Register commands (e.g., \left, \right) for directing content to frames.
local fontcommand = frame .. ":font"
self:registerCommand(frame, function(_, _)
SILE.typesetter = typesetterPool[frame]
SILE.call(fontcommand)
end)
-- Define default font commands for frames if not already defined.
if not SILE.Commands[fontcommand] then
self:registerCommand(fontcommand, function(_, _) end)
end
end
-- Configure the order of frames for the folio (page layout).
if not options.folios then
folioOrder = { {} }
for frame, _ in pl.tablex.sort(options.frames) do
table.insert(folioOrder[1], frame)
end
else
folioOrder = options.folios
end
-- Customize the `newPage` method to synchronize frames.
-- Ensure that each new page starts clean but balanced
self.class.newPage = function(self_)
self.class._base.newPage(self_)
-- Reset calculations
allTypesetters(function(frame, _)
calculations[frame] = { mark = 0 }
end)
-- Align and balance frames
SILE.call("sync")
end
-- Initialize calculations for each frame.
allTypesetters(function(frame, _)
calculations[frame] = { mark = 0 }
end)
-- Override the `finish` method to handle parallel page-breaking.
local oldfinish = self.class.finish
self.class.finish = function(self_)
parallelPagebreak()
oldfinish(self_)
end
end
-- Registers commands for the package.
function package:registerCommands()
-- shortcut for \parskip
self:registerCommand("parskip", function(options, _)
local height = options.height or "12pt plus 3pt minus 1pt"
SILE.typesetter:leaveHmode()
SILE.typesetter:pushVglue(SILE.types.length(height))
end)
self:registerCommand("sync", function(_, _)
local anybreak = false
local maxheight = SILE.types.length()
-- Check for potential page breaks.
allTypesetters(function(_, typesetter)
typesetter:leaveHmode(true)
local lines = pl.tablex.copy(typesetter.state.outputQueue)
if SILE.pagebuilder:findBestBreak({ vboxlist = lines, target = typesetter:getTargetLength() }) then
anybreak = true
end
end)
-- Perform a page break if necessary.
if anybreak then
parallelPagebreak()
return
end
-- Calculate the height of new material for balancing.
allTypesetters(function(frame, typesetter)
calculations[frame].heightOfNewMaterial = calculateFrameHeight(frame, typesetter)
if calculations[frame].heightOfNewMaterial > maxheight then
maxheight = calculations[frame].heightOfNewMaterial
SU.debug(package._name, "Value of maxheight after balancing for frame ", frame, ": ", maxheight)
end
end)
-- Add balancing glue
addBalancingGlue(maxheight)
-- Check if parskip is effectively nil
local parskip = SILE.settings:get("document.parskip")
-- SU.debug("parallel", "parsing parskip", parskip.length, parskip.stretch, parskip.shrink)
if not parskip.length then
-- Insert flexible glue to manage space between two successive pairs of frames separated by the \sync command
-- Add parskip to the bottom of both frames
addParskipToFrames(SILE.types.length("12pt plus 3pt minus 1pt"))
else
-- Add the value of parskip set by user
addParskipToFrames(parskip)
end
end)
end
return package
|
Beta Was this translation helpful? Give feedback.
-
I have a modified
diglot
class like this:and a
parallel
package:I've managed to add balancing glue to the bottom of shorter frames, but I'm still struggling with overflow handling when a frame spills over to the next page. I've been working on calculating the
overflowHeight
(the height of all unprocessed line - the height of all processed ones) in theparallelPagebreak
function for over a week, but I haven't been able to crack it. It's quite an interesting challenge.Another problem I have with
parallel
package is displaying footnote text. I can see the frames for footnotes, but theirs contents are not typeset.Mininal example:
If you find it interesting, please play with it and share the outcome with me.
Thank you for you valuable time and interest.
Beta Was this translation helpful? Give feedback.
All reactions