diff options
author | Karl Berry <karl@freefriends.org> | 2014-08-11 23:01:04 +0000 |
---|---|---|
committer | Karl Berry <karl@freefriends.org> | 2014-08-11 23:01:04 +0000 |
commit | 64660cd86edb8ae68a28d7ef7550583d76ae5f5e (patch) | |
tree | 35b526e4b35ea14914e1830ce292a3c442e44a25 /Master/texmf-dist/tex/lualatex/luatodonotes | |
parent | 7f08054bf5aebef2dca4f32b2d21e95a50f7e2c5 (diff) |
luatodonotes (8aug14)
git-svn-id: svn://tug.org/texlive/trunk@34908 c570f23f-e606-0410-a88d-b1316a301751
Diffstat (limited to 'Master/texmf-dist/tex/lualatex/luatodonotes')
5 files changed, 3284 insertions, 0 deletions
diff --git a/Master/texmf-dist/tex/lualatex/luatodonotes/inspect.lua b/Master/texmf-dist/tex/lualatex/luatodonotes/inspect.lua new file mode 100644 index 00000000000..638a673a708 --- /dev/null +++ b/Master/texmf-dist/tex/lualatex/luatodonotes/inspect.lua @@ -0,0 +1,297 @@ +local inspect ={ + _VERSION = 'inspect.lua 2.0.0', + _URL = 'http://github.com/kikito/inspect.lua', + _DESCRIPTION = 'human-readable representations of tables', + _LICENSE = [[ + MIT LICENSE + + Copyright (c) 2013 Enrique García Cota + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ]] +} + +-- Apostrophizes the string if it has quotes, but not aphostrophes +-- Otherwise, it returns a regular quoted string +local function smartQuote(str) + if str:match('"') and not str:match("'") then + return "'" .. str .. "'" + end + return '"' .. str:gsub('"', '\\"') .. '"' +end + +local controlCharsTranslation = { + ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", + ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v" +} + +local function escapeChar(c) return controlCharsTranslation[c] end + +local function escape(str) + local result = str:gsub("\\", "\\\\"):gsub("(%c)", escapeChar) + return result +end + +local function isIdentifier(str) + return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" ) +end + +local function isArrayKey(k, length) + return type(k) == 'number' and 1 <= k and k <= length +end + +local function isDictionaryKey(k, length) + return not isArrayKey(k, length) +end + +local defaultTypeOrders = { + ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4, + ['function'] = 5, ['userdata'] = 6, ['thread'] = 7 +} + +local function sortKeys(a, b) + local ta, tb = type(a), type(b) + + -- strings and numbers are sorted numerically/alphabetically + if ta == tb and (ta == 'string' or ta == 'number') then return a < b end + + local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb] + -- Two default types are compared according to the defaultTypeOrders table + if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb] + elseif dta then return true -- default types before custom ones + elseif dtb then return false -- custom types after default ones + end + + -- custom types are sorted out alphabetically + return ta < tb +end + +local function getDictionaryKeys(t) + local keys, length = {}, #t + for k,_ in pairs(t) do + if isDictionaryKey(k, length) then table.insert(keys, k) end + end + table.sort(keys, sortKeys) + return keys +end + +local function getToStringResultSafely(t, mt) + local __tostring = type(mt) == 'table' and rawget(mt, '__tostring') + local str, ok + if type(__tostring) == 'function' then + ok, str = pcall(__tostring, t) + str = ok and str or 'error: ' .. tostring(str) + end + if type(str) == 'string' and #str > 0 then return str end +end + +local maxIdsMetaTable = { + __index = function(self, typeName) + rawset(self, typeName, 0) + return 0 + end +} + +local idsMetaTable = { + __index = function (self, typeName) + local col = setmetatable({}, {__mode = "kv"}) + rawset(self, typeName, col) + return col + end +} + +local function countTableAppearances(t, tableAppearances) + tableAppearances = tableAppearances or setmetatable({}, {__mode = "k"}) + + if type(t) == 'table' then + if not tableAppearances[t] then + tableAppearances[t] = 1 + for k,v in pairs(t) do + countTableAppearances(k, tableAppearances) + countTableAppearances(v, tableAppearances) + end + countTableAppearances(getmetatable(t), tableAppearances) + else + tableAppearances[t] = tableAppearances[t] + 1 + end + end + + return tableAppearances +end + +local function parse_filter(filter) + if type(filter) == 'function' then return filter end + -- not a function, so it must be a table or table-like + filter = type(filter) == 'table' and filter or {filter} + local dictionary = {} + for _,v in pairs(filter) do dictionary[v] = true end + return function(x) return dictionary[x] end +end + +local function makePath(path, key) + local newPath, len = {}, #path + for i=1, len do newPath[i] = path[i] end + newPath[len+1] = key + return newPath +end + +------------------------------------------------------------------- +function inspect.inspect(rootObject, options) + options = options or {} + local depth = options.depth or math.huge + local filter = parse_filter(options.filter or {}) + + local tableAppearances = countTableAppearances(rootObject) + + local buffer = {} + local maxIds = setmetatable({}, maxIdsMetaTable) + local ids = setmetatable({}, idsMetaTable) + local level = 0 + local blen = 0 -- buffer length + + local function puts(...) + local args = {...} + for i=1, #args do + blen = blen + 1 + buffer[blen] = tostring(args[i]) + end + end + + local function down(f) + level = level + 1 + f() + level = level - 1 + end + + local function tabify() + puts("\n", string.rep(" ", level)) + end + + local function commaControl(needsComma) + if needsComma then puts(',') end + return true + end + + local function alreadyVisited(v) + return ids[type(v)][v] ~= nil + end + + local function getId(v) + local tv = type(v) + local id = ids[tv][v] + if not id then + id = maxIds[tv] + 1 + maxIds[tv] = id + ids[tv][v] = id + end + return id + end + + local putValue -- forward declaration that needs to go before putTable & putKey + + local function putKey(k) + if isIdentifier(k) then return puts(k) end + puts( "[" ) + putValue(k, {}) + puts("]") + end + + local function putTable(t, path) + if alreadyVisited(t) then + puts('<table ', getId(t), '>') + elseif level >= depth then + puts('{...}') + else + if tableAppearances[t] > 1 then puts('<', getId(t), '>') end + + local dictKeys = getDictionaryKeys(t) + local length = #t + local mt = getmetatable(t) + local to_string_result = getToStringResultSafely(t, mt) + + puts('{') + down(function() + if to_string_result then + puts(' -- ', escape(to_string_result)) + if length >= 1 then tabify() end -- tabify the array values + end + + local needsComma = false + for i=1, length do + needsComma = commaControl(needsComma) + puts(' ') + putValue(t[i], makePath(path, i)) + end + + for _,k in ipairs(dictKeys) do + needsComma = commaControl(needsComma) + tabify() + putKey(k) + puts(' = ') + putValue(t[k], makePath(path, k)) + end + + if mt then + needsComma = commaControl(needsComma) + tabify() + puts('<metatable> = ') + putValue(mt, makePath(path, '<metatable>')) + end + end) + + if #dictKeys > 0 or mt then -- dictionary table. Justify closing } + tabify() + elseif length > 0 then -- array tables have one extra space before closing } + puts(' ') + end + + puts('}') + end + + end + + -- putvalue is forward-declared before putTable & putKey + putValue = function(v, path) + if filter(v, path) then + puts('<filtered>') + else + local tv = type(v) + + if tv == 'string' then + puts(smartQuote(escape(v))) + elseif tv == 'number' or tv == 'boolean' or tv == 'nil' then + puts(tostring(v)) + elseif tv == 'table' then + putTable(v, path) + else + puts('<',tv,' ',getId(v),'>') + end + end + end + + putValue(rootObject, {}) + + return table.concat(buffer) +end + +setmetatable(inspect, { __call = function(_, ...) return inspect.inspect(...) end }) + +return inspect + diff --git a/Master/texmf-dist/tex/lualatex/luatodonotes/luatodonotes.lua b/Master/texmf-dist/tex/lualatex/luatodonotes/luatodonotes.lua new file mode 100644 index 00000000000..61175bbbf68 --- /dev/null +++ b/Master/texmf-dist/tex/lualatex/luatodonotes/luatodonotes.lua @@ -0,0 +1,2160 @@ +-- +-- Copyright (C) 2014 by Fabian Lipp <fabian.lipp@gmx.de> +-- ------------------------------------------------------- +-- +-- This file may be distributed and/or modified under the +-- conditions of the LaTeX Project Public License, either version 1.2 +-- of this license or (at your option) any later version. +-- The latest version of this license is in: +-- +-- http://www.latex-project.org/lppl.txt +-- +-- and version 1.2 or later is part of all distributions of LaTeX +-- version 1999/12/01 or later. +-- + +require("lualibs") +--require("debugger")() +local inspect = require('inspect') + +local point = require'path_point' +local pathLine = require'path_line' +--local bezier3 = require'path_bezier3' + +-- TODO Funktionen/Variablen in Namespace o. Ä. packen + +-- strings used to switch to standard catcodes for LaTeX packages +local catcodeStart = "\\makeatletter" +local catcodeEnd = "\\makeatother" + +local currentPage = 1 + +local const1In = string.todimen("1in") -- needed for calculations of page borders + -- (used as a constant in TeX) +-- constants set in sty-file +-- + noteInnerSep (inner sep used for tikz nodes) +-- + noteInterSpace (vertical space between notes) +-- + routingAreaWidth (width of the track routing area for opo-leaders) +-- + minNoteWidth (width that must be available for labels to consider the left or +-- right border of the page for placing labels) +-- + distanceNotesPageBorder (distance from the page borders to the outmost point +-- of the labels) +-- + distanceNotesText (horizontal distance between the labels and the text area) +-- + todonotesDebug (activate debug outputs when true) + + +-- stores information about available algorithms +positioningAlgos = {} +splittingAlgos = {} +leaderTypes = {} + +-- stores the notes for the current page +notesForPage = {} +-- Fields for each note: +-- index: numbers notes in whole document +-- indexOnPage: index of the note in the notesForPage array +-- textbox: links to a hbox that contains the text, which is displayed inside +-- the note +-- origInputX, origInputY: position in which the todo-command was issued +-- inputX, inputY: position to which the leader should be attached (can have a +-- certain offset to origInputX/Y) +-- heightLeft, heightRight: height of the contained text when placed on +-- left/right side +-- pageNr: absolute number of page on which site for note is placed +-- rightSide: true means the note should be placed on the right side; +-- otherwise left side is meant +-- fontsize: fontsize used for paragraph in that the note was defined +-- baselineskip: \baselineskip in the paragraph in that the note was defined +-- outputX, outputY: position on which the north west anchor of the note should +-- be placed +-- lineColor: color of line connecting note to text +-- backgroundColor: color of background of note +-- borderColor: color of border of note +-- leaderWidth: width of leader (used as argument for tikz line width) +-- +-- Additional fields for text area: +-- noteType: constant string "area" +-- origInputEndX, origInputEndY: position at which the todo area ends +-- pageNrEnd: absolute number of page on which todo area ends +-- lineCountInArea: highest line index in area +-- linesInArea: positions of lines in area (to detect page/column break) + +-- stores the areas for the labels on the current page +-- (calculated in function calcLabelAreaDimensions()) +local labelArea = {} + +-- stores the positions of the text lines on every page +local linePositions = {} +-- Fields for every position: +-- 1: position of baseline +-- 2: upper bound of line (baseline + height) +-- 3: lower bound of line (baseline - depth) + +-- variables used for construction of linePositions list when reading the file +local linePositionsCurPage = {} +local linePositionsPageNr = 0 + + +-- *** metatable for note objects *** +local noteMt = {} +-- getHeight(): yield heightLeft or heightRight depending on rightSide (implemented by metatable) +function noteMt:getHeight() + if self.rightSide then + return self.heightRight + else + return self.heightLeft + end +end +function noteMt:getLabelAnchorY() + local leaderAnchor = positioning.leaderAnchor + local y + if leaderAnchor == "north east" then + y = self.outputY + elseif leaderAnchor == "east" then + y = self.outputY - noteInnerSep - self:getHeight() / 2 + else + error("Invalid anchor for algorithm") + end + + if positioning.leaderShift then + y = y + self.leaderShiftY + end + + return y +end +function noteMt:getInTextAnchorTikz() + return "(" .. self.inputX .. "sp," .. self.inputY .. "sp)" +end +function noteMt:getLabelAnchorTikz() + local leaderAnchor = positioning.leaderAnchor + if leaderAnchor == "north east" and self.rightSide then + leaderAnchor = "north west" + elseif leaderAnchor == "east" and self.rightSide then + leaderAnchor = "west" + end + + local shiftStr = "" + if positioning.leaderShift then + shiftStr = "[shift={(" .. self.leaderShiftX .. "sp," .. self.leaderShiftY .. "sp)}]" + end + + return "(" .. shiftStr .. " @todonotes@" .. self.index .. " note." .. leaderAnchor .. ")" +end +function noteMt:boxForNoteText(rightSide) + local area = labelArea:getArea(rightSide) + local noteWidth + if area == nil then + noteWidth = minNoteWidth + else + noteWidth = area.noteWidth - 2*noteInnerSep + end + local retval = "\\directlua{tex.box[\"@todonotes@notetextbox\"] = " .. + "node.copy_list(notesForPage[" .. self.indexOnPage .. "].textbox)}" + retval = retval .. "\\parbox{" .. noteWidth .. "sp}" .. + "{\\raggedright\\unhbox\\@todonotes@notetextbox}" + return retval +end + +function noteMt:getClipPathForTodoArea() + -- detext which lines are in same column/page as start of area + local lineCount = self.lineCountInArea + local maxLine = 1 + local lines = self.linesInArea + local lastY = lines[1] + while maxLine < lineCount do + if lines[maxLine + 1] < lastY then + maxLine = maxLine + 1 + lastY = lines[maxLine] + end + end + + local function nodename(i, corner) + return "(@todonotes@" .. self.index .. "@" .. i .. " area" .. corner .. ")" + end + local path = nodename(1, "NW") + local pathLeft = "" + if maxLine == 1 then + -- only one line + path = path .. " -- " .. nodename(1, "NE") .. + " -- " .. nodename(1, "SE") + else + path = path .. " -- " .. nodename(1, "NE") .. + " decorate[@todonotes@todoarea] { -- " .. nodename(1, "SE") .. "}" + end + for i = 2, maxLine do + if i == lineCount then + -- area does not use the whole line + path = path .. " -| " .. nodename(i, "NE") .. + " -- " .. nodename(i, "SE") + else + -- area uses whole line + path = path .. " -- " .. nodename(i, "NE") .. + " decorate[@todonotes@todoarea] { -- " .. nodename(i, "SE") .. "}" + end + pathLeft = " -- " .. nodename(i, "SW") .. + " decorate[@todonotes@todoarea] { -- " .. nodename(i, "NW") .. "}" .. + pathLeft + end + path = path .. pathLeft .. " -| " .. nodename(1, "SW") .. "-- cycle" + return path +end + + + +-- *** label areas *** +-- stores areas for placing labels on current page +function labelArea:getArea(rightSide) + if rightSide then + return self.right + else + return self.left + end +end +-- yields the x-coordinate of the boundary of the label that is pointing +-- towards the text +function labelArea:getXTextSide(rightSide) + if rightSide then + return self.right.left + else + return self.left.right + end +end +function labelArea:isOneSided() + if self.right == nil or self.left == nil then + return true + else + return false + end +end + + + +-- is called by the sty-file when all settings (algorithms etc.) are made +function initTodonotes() + if positioning.needLinePositions then + luatexbase.add_to_callback("post_linebreak_filter", callbackOutputLinePositions, "outputLinePositions") + tex.print("\\@starttoc{lpo}") + tex.print("\\directlua{lpoFileStream = \\the\\tf@lpo}") + end +end + + +-- valid values for noteType: nil/"" (for point in text), "area" +function addNoteToList(index, drawLeader, noteType) + local newNote = {} + newNote.index = index + newNote.textbox = node.copy_list(tex.box["@todonotes@notetextbox"]) + newNote.baselineskip = tex.dimen["@todonotes@baselineskip"] + newNote.fontsize = tex.dimen["@todonotes@fontsize"] + newNote.lineColor = tex.toks["@todonotes@toks@currentlinecolor"] + newNote.backgroundColor = tex.toks["@todonotes@toks@currentbackgroundcolor"] + newNote.borderColor = tex.toks["@todonotes@toks@currentbordercolor"] + newNote.leaderWidth = tex.toks["@todonotes@toks@currentleaderwidth"] + newNote.drawLeader = drawLeader + if noteType == "area" then + newNote.noteType = "area" + newNote.lineCountInArea = 0 + -- else: newNote.noteType = nil (default value) + end + setmetatable(newNote, {__index = noteMt}) + newNote.indexOnPage = #notesForPage + 1 + notesForPage[newNote.indexOnPage] = newNote +end + +function clearNotes() + -- delete the texts for the notes on this page from memory + -- (garbage collection does not work for nodes) + for _, v in pairs(notesForPage) do + node.free(v.textbox) + end + notesForPage = notesForNextPage + -- update indexOnPage for the new notes + for k, v in pairs(notesForPage) do + v.indexOnPage = k + end + + currentPage = currentPage + 1 +end + +function processLastLineInTodoArea() + -- LaTeX counter is accessed as TeX count by prefixing c@ + ind = tex.count["c@@todonotes@numberoftodonotes"] + val = tex.count["c@@todonotes@numberofLinesInArea"] + notesForPage[#notesForPage].lineCountInArea = val +end + + +-- *** constructing the linePositions list *** +function linePositionsNextPage() + linePositionsPageNr = linePositionsPageNr + 1 + linePositionsCurPage = {} + linePositions[linePositionsPageNr] = linePositionsCurPage +end + +function linePositionsAddLine(ycoord, lineheight, linedepth) + local baseline = ycoord - tex.pageheight + linePositionsCurPage[#linePositionsCurPage + 1] = {baseline, baseline + lineheight, baseline - linedepth} +end + +-- DEBUG +function markLines() + print("marking lines for page " .. currentPage) + if linePositions[currentPage] ~= nil then + for _, v in pairs(linePositions[currentPage]) do + --local yPos = - (tex.pageheight - v[1]) + local yPos = v[3] + local yTop = v[2] + tex.print("\\draw[red] (10cm," .. yPos .."sp) circle(3pt);") + tex.print("\\fill[red] (10cm," .. yPos .."sp) circle(1pt);") + tex.print("\\draw[green] (2cm," .. yTop .."sp) -- ++(17cm,0);") + tex.print("\\draw[blue] (2cm," .. yPos .."sp) -- ++(17cm,0);") + end + end +end + + +function getInputCoordinatesForNotes() + tex.sprint(catcodeStart) + for k, v in ipairs(notesForPage) do + local nodename = "@todonotes@" .. v.index .. " inText" + + tex.sprint("\\pgfextractx{\\@todonotes@extractx}{\\pgfpointanchor{" .. + nodename .. "}{center}}") + tex.sprint("\\pgfextracty{\\@todonotes@extracty}{\\pgfpointanchor{" .. + nodename .. "}{center}}") + tex.print("\\directlua{notesForPage[" .. k .. "].origInputX = " .. + "tex.dimen[\"@todonotes@extractx\"]}") + tex.print("\\directlua{notesForPage[" .. k .. "].origInputY = " .. + "tex.dimen[\"@todonotes@extracty\"]}") + + if v.noteType == "area" then + nodename = nodename .. "End" + tex.sprint("\\pgfextractx{\\@todonotes@extractx}{\\pgfpointanchor{" .. + nodename .. "}{center}}") + tex.sprint("\\pgfextracty{\\@todonotes@extracty}{\\pgfpointanchor{" .. + nodename .. "}{center}}") + tex.print("\\directlua{notesForPage[" .. k .. "].origInputEndX = " .. + "tex.dimen[\"@todonotes@extractx\"]}") + tex.print("\\directlua{notesForPage[" .. k .. "].origInputEndY = " .. + "tex.dimen[\"@todonotes@extracty\"]}") + + notesForPage[k].linesInArea = {} + for i = 1, v.lineCountInArea do + nodename = "@todonotes@" .. v.index .. "@" .. i .. " areaSW" + tex.sprint("\\pgfextracty{\\@todonotes@extracty}{\\pgfpointanchor{" .. + nodename .. "}{center}}") + tex.print("\\directlua{notesForPage[" .. k .. "].linesInArea[" .. + i .. "] = " .. "tex.dimen[\"@todonotes@extracty\"]}") + end + end + end + tex.sprint(catcodeEnd) +end + +function calcLabelAreaDimensions() + local routingAreaSpace = 0 + if leaderType.needRoutingArea then + routingAreaSpace = routingAreaWidth + end + + local top = tex.voffset + tex.dimen.topmargin + const1In + local bottom = top + tex.dimen.headheight + tex.dimen.headsep + tex.dimen.textheight + tex.dimen.footskip + local currentsidemargin = tex.hoffset + tex.dimen["@todonotes@currentsidemargin"] + const1In + + local left = {} + left.top = -top + left.bottom = -bottom + left.left = distanceNotesPageBorder + left.right = currentsidemargin - distanceNotesText - routingAreaSpace + if left.right - left.left < minNoteWidth then + -- not enough space left of text + left = nil + else + left.noteWidth = left.right - left.left + end + + local right = {} + right.top = -top + right.bottom = -bottom + right.left = currentsidemargin + tex.dimen.textwidth + distanceNotesText + routingAreaSpace + right.right = tex.pagewidth - distanceNotesPageBorder + if right.right - right.left < minNoteWidth then + -- not enough space right of text + right = nil + else + right.noteWidth = right.right - right.left + end + + local text = {} + text.left = currentsidemargin + text.right = currentsidemargin + tex.dimen.textwidth + + labelArea.left = left + labelArea.right = right + labelArea.text = text +end + +function calcHeightsForNotes() + -- function has to be called outside of a tikzpicture-environment + tex.sprint(catcodeStart) + for k, v in ipairs(notesForPage) do + -- store height for note + -- (is determined by creating a box with the text and reading its size) + + -- left side + tex.sprint("\\savebox{\\@todonotes@heightcalcbox}" .. + "{" .. v:boxForNoteText(false) .. "}") + tex.sprint("\\@todonotes@heightcalcboxdepth=\\dp\\@todonotes@heightcalcbox") + tex.sprint("\\@todonotes@heightcalcboxheight=\\ht\\@todonotes@heightcalcbox") + tex.sprint("\\directlua{notesForPage[" .. k .. "].heightLeft = " .. + "tex.dimen[\"@todonotes@heightcalcboxheight\"]" .. + " + tex.dimen[\"@todonotes@heightcalcboxdepth\"]}") + + -- right side + tex.sprint("\\savebox{\\@todonotes@heightcalcbox}" .. + "{" .. v:boxForNoteText(true) .. "}") + tex.sprint("\\@todonotes@heightcalcboxdepth=\\dp\\@todonotes@heightcalcbox") + tex.sprint("\\@todonotes@heightcalcboxheight=\\ht\\@todonotes@heightcalcbox") + tex.sprint("\\directlua{notesForPage[" .. k .. "].heightRight = " .. + "tex.dimen[\"@todonotes@heightcalcboxheight\"]" .. + " + tex.dimen[\"@todonotes@heightcalcboxdepth\"]}") + + -- store pageNr for note + -- (is determined as reference to a label) + tex.sprint("\\directlua{notesForPage[" .. k .. "].pageNr = " .. + "\\zref@extract{@todonotes@" .. v.index .. "}{abspage}}") + if v.noteType == "area" then + tex.sprint("\\directlua{notesForPage[" .. k .. "].pageNrEnd = " .. + "\\zref@extract{@todonotes@" .. v.index .. "@end}{abspage}}") + end + end + tex.sprint(catcodeEnd) +end + +local inputShiftX = string.todimen("-0.05cm") -- sensible value depends on shape of mark +function printNotes() + print("drawing labels for page " .. currentPage) + + -- seperate notes that should be placed on another page + -- This can occur when note is in a paragraph which doesn't fit on the + -- current page and is thus moved to the next one. But the \todo-command is + -- still read before the shipout of the current page is done + notesForNextPage = {} + local k=1 + while k <= #notesForPage do + local v = notesForPage[k] + if v.pageNr ~= currentPage then + table.insert(notesForNextPage, v) + table.remove(notesForPage, k) + print("moving " .. k) + else + -- update index here (needed if a note was deleted before) + v.indexOnPage = k + k = k + 1 + end + end + + -- add offset to input coordinates + for _, v in pairs(notesForPage) do + if v.noteType ~= "area" then + v.inputX = v.origInputX + inputShiftX + v.inputY = v.origInputY - 1.3 * (v.baselineskip - v.fontsize) + else + v.inputX = v.origInputX + v.inputY = v.origInputY + end + end + + splitting.algo() + if positioning.twoSided then + local notesLeft, notesRight = segmentNotes(notesForPage) + if #notesLeft > 0 then + positioning.algo(notesLeft, false) + end + if #notesRight > 0 then + positioning.algo(notesRight, true) + end + else + positioning.algo() + end + for k, v in ipairs(notesForPage) do + if todonotesDebug then + local function outputWithPoints(val) + if val ~= nil then + return val .. " (" .. number.topoints(val) .. ")" + else + return "" + end + end + print("-----------------") + print(k .. ": ") + print("index: " .. v.index) + print("origInputX: " .. v.origInputX) + print("origInputY: " .. v.origInputY) + if (v.noteType ~= nil) then + print("noteType: " .. v.noteType) + print("origInputEndX:" .. v.origInputEndX) + print("origInputEndY:" .. v.origInputEndY) + print("lineCountInArea:" .. v.lineCountInArea) + print("linesInArea :" .. inspect(v.linesInArea)) + else + print("noteType: nil") + end + print("inputX: " .. v.inputX) + print("inputY: " .. v.inputY) + print("outputX: " .. v.outputX) + print("outputY: " .. v.outputY) + if (v.rasterSlots ~= nil) then + print("rasterSlots: " .. v.rasterSlots) + end + print("baselineskip: " .. outputWithPoints(v.baselineskip)) + print("fontsize: " .. outputWithPoints(v.fontsize)) + print("textbox: " .. inspect(v.textbox)) + print("height: " .. outputWithPoints(v:getHeight())) + print("heightLeft: " .. outputWithPoints(v.heightLeft)) + print("heightRight: " .. outputWithPoints(v.heightRight)) + print("rightSide: " .. tostring(v.rightSide)) + if v.pageNr ~= nil then + print("pageNr: " .. v.pageNr) + end + print("lineColor: " .. v.lineColor) + print("backgroundColor:" .. v.backgroundColor) + print("borderColor: " .. v.borderColor) + print("leaderWidth: " .. v.leaderWidth) + print("drawLeader: " .. tostring(v.drawLeader)) + end + + -- print note + tex.print(catcodeStart) + tex.print("\\node[@todonotes@notestyle,anchor=north west," .. + "fill=" .. v.backgroundColor .. ",draw=" .. v.borderColor .. "] " .. + "(@todonotes@" .. v.index .. + " note) at (" .. v.outputX .. "sp," .. v.outputY .. "sp) {" .. + v:boxForNoteText(v.rightSide) .. "};") + tex.print(catcodeEnd) + + + -- output debugging hints on page + if todonotesDebug then + tex.print("\\node[anchor=north west,text=blue,fill=white,rectangle] at (@todonotes@" .. v.index .. " inText) {" .. v.index .. "};") + tex.print("\\draw[green,fill] (@todonotes@" .. v.index .. " inText) circle(2pt);") + tex.print("\\draw[black,fill] (@todonotes@" .. v.index .. " inText) circle(0.2pt);") + if v.noteType == "area" then + tex.print("\\draw[red,fill] (@todonotes@" .. v.index .. " inTextEnd) circle(2pt);") + end + + if (v.noteType ~= nil) then + print(v:getClipPathForTodoArea()) + tex.print("\\draw[blue] " .. v:getClipPathForTodoArea() .. ";") + --for i=1, v.lineCountInArea do + --tex.print(" (@todonotes@" .. v.index .. "@" .. i .. " areaSW) -- ") + --end + --tex.print("cycle;") + end + end + end + -- draw leader + leaderType.algo() + + -- draw mark in text + for _, v in pairs(notesForPage) do + if v.drawLeader ~= false and v.noteType ~= "area" then + local shiftStr = "(" .. v.inputX .. "sp," .. v.inputY .. "sp)" + tex.print("\\draw[@todonotes@textmark," .. + "draw=" .. v.lineColor .. ",fill=" .. v.lineColor .. "," .. + "shift={" .. shiftStr .. "}," .. + "scale around={0.5:(-0,-0)},shift={(-0.5,-0.1)}]" .. + "(1,0) .. controls (0.5,0.2) and (0.65,0.3) .." .. + "(0.5,0.7) .. controls (0.35,0.3) and (0.5,0.2) .." .. + "(0,0) -- cycle;") + end + end + + --- draw label areas when requested + if todonotesDebug then + local area = labelArea.left + if area ~= nil then + tex.print("\\draw[blue] (" .. area.left .. "sp," .. area.top .. "sp) rectangle (" .. + area.right .. "sp," .. area.bottom .. "sp);") + end + area = labelArea.right + if area ~= nil then + tex.print("\\draw[blue] (" .. area.left .. "sp," .. area.top .. "sp) rectangle (" .. + area.right .. "sp," .. area.bottom .. "sp);") + end + end +end + + + + +-- ********** Helper Functions ********** + +-- * comparators for table.sort() * +-- (yields true if first parameter should be placed before second parameter in +-- sorted table) +function compareNoteInputXAsc(note1, note2) + if note1.inputX < note2.inputX then + return true + end +end + +function compareNoteIndInputXAsc(key1, key2) + if notesForPage[key1].inputX < notesForPage[key2].inputX then + return true + end +end + +function compareNoteIndInputXDesc(key1, key2) + if notesForPage[key1].inputX > notesForPage[key2].inputX then + return true + end +end + +function compareNoteIndInputYDesc(key1, key2) + local v1 = notesForPage[key1] + local v2 = notesForPage[key2] + if v1.inputY > v2.inputY then + return true + elseif v1.inputY == v2.inputY then + if v1.inputX < v2.inputX then + return true + end + end +end + +-- * callbacks for Luatex * +function appendStrToTokenlist(tokenlist, str) + str:gsub(".", function(c) + tokenlist[#tokenlist + 1] = {12, c:byte(), 0} + end) +end +-- writes commands into the node tree that print the absolute position on the +-- page to the output file (streamId is taken from lpoFileStream) at the +-- beginning of every line +-- should be called as post_linebreak_filter +function callbackOutputLinePositions(head) + while head do + if head.id == 0 then -- id 0 is a hlist + -- check if we are in the main text area (hlists in, e.g., + -- tikz nodes should have other widths) + if head.width == tex.dimen.textwidth then + -- check if there is a glyph in this hlist + -- -> then we consider it a text line + local foundGlyph = false + local glyphTest = head.head + while glyphTest do + if glyphTest.id == 37 then -- glyph + foundGlyph = true + break + end + glyphTest = glyphTest.next + end + + if foundGlyph then + local w = node.new("whatsit", "write") -- 8/1 + w.stream = lpoFileStream + local tokenlist = { + {12, 92, 0}, -- \ + {12, 64, 0}, -- @ + {12, 116, 0}, -- t + {12, 111, 0}, -- o + {12, 100, 0}, -- d + {12, 111, 0}, -- o + {12, 110, 0}, -- n + {12, 111, 0}, -- o + {12, 116, 0}, -- t + {12, 101, 0}, -- e + {12, 115, 0}, -- s + {12, 64, 0}, -- @ + {12, 108, 0}, -- l + {12, 105, 0}, -- i + {12, 110, 0}, -- n + {12, 101, 0}, -- e + {12, 112, 0}, -- p + {12, 111, 0}, -- o + {12, 115, 0}, -- s + {12, 105, 0}, -- i + {12, 116, 0}, -- t + {12, 105, 0}, -- i + {12, 111, 0}, -- o + {12, 110, 0}, -- n + {12, 123, 0} -- { + } + t = token.create("@todonotes@pdflastypos") + tokenlist[#tokenlist + 1] = t + tokenlist[#tokenlist + 1] = {12, 125, 0} + tokenlist[#tokenlist + 1] = {12, 123, 0} + appendStrToTokenlist(tokenlist, tostring(head.height)) + tokenlist[#tokenlist + 1] = {12, 125, 0} + tokenlist[#tokenlist + 1] = {12, 123, 0} + appendStrToTokenlist(tokenlist, tostring(head.depth)) + tokenlist[#tokenlist + 1] = {12, 125, 0} + w.data = tokenlist + head.head = node.insert_before(head.head,head.head,w) + + local w = node.new("whatsit", "pdf_save_pos") -- 8/23 + head.head = node.insert_before(head.head,head.head,w) + end + end + end + head = head.next + end + return true +end + + + + + +-- ********** Leader Drawing Algorithms ********** + +function drawLeaderPath(note, path) + if note.drawLeader == false then + return + end + local clipPath + if note.noteType == "area" then + clipPath = note:getClipPathForTodoArea() + tex.print("\\begin{scope}") + tex.print("\\clip (current page.north west) rectangle (current page.south east) ") + tex.print(clipPath) + tex.print(";") + end + tex.print("\\draw[@todonotes@leader,draw=" .. note.lineColor .. + ",line width=" .. note.leaderWidth .. ",name path=leader] " .. path .. ";") + if note.noteType == "area" then + tex.print("\\path[name path=clipping] " .. clipPath .. ";") + tex.print("\\fill[@todonotes@leader,name intersections={of=leader and clipping, by=x,sort by=leader}] (x) circle(3pt);") + tex.print("\\end{scope}") + end +end + + +-- ** leader drawing: s-leaders +function drawSLeaders() + for k, v in ipairs(notesForPage) do + drawLeaderPath(v, v:getLabelAnchorTikz() .. + " -- " .. v:getInTextAnchorTikz()) + end +end +leaderTypes["s"] = {algo = drawSLeaders} + + + +-- ** leader drawing: opo-leaders +function drawOpoLeader(v, opoShift, rightSide) + if rightSide then + opoShift = - opoShift + end + drawLeaderPath(v, v:getLabelAnchorTikz() .. " -- +(" .. opoShift .. "sp,0) " .. + "|- " .. v:getInTextAnchorTikz()) +end +function drawOpoGroup(group, directionDown, rightSide) + if directionDown == nil then + for _, v2 in ipairs(group) do + drawOpoLeader(notesForPage[v2], 0, rightSide) + end + else + if #group == 1 then + -- place p-section of leader in center of routing area + local opoShift = distanceNotesText / 2 + routingAreaWidth / 2 + drawOpoLeader(notesForPage[group[1]], opoShift, rightSide) + else + local leaderDistance = routingAreaWidth / (#group - 1) + + -- initialise shift value + local nextOpoShift, move + if directionDown then + nextOpoShift = distanceNotesText / 2 + routingAreaWidth + move = -leaderDistance + else + nextOpoShift = distanceNotesText / 2 + move = leaderDistance + end + + -- cycle through group + for _, v2 in ipairs(group) do + drawOpoLeader(notesForPage[v2], nextOpoShift, rightSide) + nextOpoShift = nextOpoShift + move + end + end + end +end +function drawOpoLeadersSide(notes, rightSide) + table.sort(notes, compareNoteIndInputYDesc) + + local lastDirectionDown = nil + local group = {} + local prevNote + for _, ind in ipairs(notes) do + local v = notesForPage[ind] + + local leaderAnchorY = v:getLabelAnchorY() + if leaderAnchorY > v.inputY then + newDirectionDown = true + elseif leaderAnchorY < v.inputY then + newDirectionDown = false + else + newDirectionDown = nil + end + + if lastDirectionDown == newDirectionDown and + prevNote ~= nil and + -- following conditions check that leaders would really intersect + -- otherwise we can start a new group + ((newDirectionDown and leaderAnchorY >= prevNote.inputY) or + (not newDirectionDown and v.inputY >= prevNote:getLabelAnchorY())) then + -- note belongs to group + table.insert(group, ind) + else + -- draw leaders for group + drawOpoGroup(group, lastDirectionDown, rightSide) + + -- initialise new group with this note + lastDirectionDown = newDirectionDown + group = {ind} + end + + prevNote = v + end + drawOpoGroup(group, lastDirectionDown, rightSide) +end +function drawOpoLeaders() + local notesLeft, notesRight = segmentNotes(notesForPage) + if #notesLeft > 0 then + drawOpoLeadersSide(notesLeft, false) + end + if #notesRight > 0 then + drawOpoLeadersSide(notesRight, true) + end +end +leaderTypes["opo"] = {algo = drawOpoLeaders, + needRoutingArea = true} + + + +-- ** leader drawing: po-leaders +function drawPoLeaders() + for _, v in ipairs(notesForPage) do + drawLeaderPath(v, v:getLabelAnchorTikz() .. " -| " .. v:getInTextAnchorTikz()) + end +end +leaderTypes["po"] = {algo = drawPoLeaders} + + + +-- ** leader drawing: os-leaders +function drawOsLeaders() + for _, v in ipairs(notesForPage) do + local cornerX + if v.rightSide then + cornerX = labelArea.right.left - distanceNotesText / 2 - routingAreaWidth + else + cornerX = labelArea.left.right + distanceNotesText / 2 + routingAreaWidth + end + drawLeaderPath(v, v:getInTextAnchorTikz() .. + " -- (" .. cornerX .. "sp,0 |- 0," .. v.inputY .. "sp) -- " .. + v:getLabelAnchorTikz()) + end +end +leaderTypes["os"] = {algo = drawOsLeaders, + needRoutingArea = true} + + + +-- ** leader drawing: s-Bezier-leaders +-- additional fields for each note: +-- leaderArmY +-- movableControlPointX +-- optimalPositionX +-- currentForce +-- forceLimitDec +-- forceLimitInc + +-- settings for algorithm +maxIterations = 1000 +factorRepulsiveControlPoint = 1 +factorAttractingControlPoint = 1 +stopCondition = 65536 -- corresponds to 1pt + +function constructCurve(l) + local curve = {} + + -- site + curve[1] = {} + curve[1].x = l.inputX + curve[1].y = l.inputY + + -- unmovable control point (middle point of site and movable control point) + curve[2] = {} + curve[2].x = (l.inputX + l.movableControlPointX) / 2 + curve[2].y = (l.inputY + l.leaderArmY) / 2 + + -- movable control point + curve[3] = {} + curve[3].x = l.movableControlPointX + curve[3].y = l.leaderArmY + + -- port + curve[4] = {} + curve[4].x = labelArea:getXTextSide(l.rightSide) + curve[4].y = l.leaderArmY + + return curve +end +function computeRepulsiveControlPointForces() + for k1, l1 in pairs(notesForPage) do + for k2, l2 in pairs(notesForPage) do + if k1 ~= k2 then + -- curves of the leaders + local curve1 = constructCurve(l1) + local curve2 = constructCurve(l2) + + local distance = checkCurveApproximation(curve1, curve2); + + -- check if R1 has to be increased or decreased to increase the distance of the 2 curves + -- if curve1 is bent into the direction of curve2, R1 has to be decreased + local actualR = math.abs(labelArea:getXTextSide(l1.rightSide) - l1.movableControlPointX) + if ((l1.inputY < l1.leaderArmY and + l2.leaderArmY < l1.leaderArmY) or + (l1.inputY > l1.leaderArmY and + l2.leaderArmY > l1.leaderArmY)) then + -- R1 has to be increased + local desiredR = math.abs(labelArea:getXTextSide(l1.rightSide) - l1.optimalPositionX) + local diff = math.abs(desiredR - actualR) + if distance == 0 then + distance = 0.01 + end + local force = diff / distance + local newForce = force * factorRepulsiveControlPoint + l1.currentForce = l1.currentForce + newForce + l1.forceLimitDec = math.min(l1.forceLimitDec, distance * 0.45) + else + -- R1 has to be decreased + if distance == 0 then + distance = 0.01 + end + local force = actualR / distance + local newForce = -force * factorRepulsiveControlPoint + l1.currentForce = l1.currentForce + newForce + local oldLim = l1.forceLimitInc + l1.forceLimitInc = math.min(l1.forceLimitInc, distance * 0.45) + --if oldLim ~= l1.forceLimitInc then + --print(k1 .. ": Reduced forceLimitInc from " .. oldLim .. " to " .. l1.forceLimitInc .. " because of " .. k2 .. " (distance: " .. distance .. ")") + --end + end + end + end + end +end +function computeAttractingControlPointForces() + for _, l in pairs(notesForPage) do + local desiredR = math.abs(labelArea:getXTextSide(l.rightSide) - l.optimalPositionX) + local actualR = math.abs(labelArea:getXTextSide(l.rightSide) - l.movableControlPointX) + local newForce = (desiredR - actualR) * factorAttractingControlPoint + l.currentForce = l.currentForce + newForce + end +end +function getPointOnCurve(t, curve) + if #curve ~= 4 then + error("4 points needed for a Bezier-curve. Given size was: " .. #curve) + end + + local x = (1 - t) * (1 - t) * (1 - t) * curve[1].x + + 3 * t * (1 - t) * (1 - t) * curve[2].x + + 3 * t * t * (1 - t) * curve[3].x + + t * t * t * curve[4].x + + local y = (1 - t) * (1 - t) * (1 - t) * curve[1].y + + 3 * t * (1 - t) * (1 - t) * curve[2].y + + 3 * t * t * (1 - t) * curve[3].y + + t * t * t * curve[4].y; + + return x, y +end +function checkCurveApproximation(curve1, curve2) + -- these lists will contain the sections of the approximation of the two curves + local sectionsCurve1 = {} + local sectionsCurve2 = {} + + -- get line segments of the first curve + local numberOfSectionsCurve1 = numberOfCurvePartitions + local temp1X, temp1Y = getPointOnCurve(0, curve1) + local i = 1 + while i <= numberOfSectionsCurve1 do + local t = i / numberOfSectionsCurve1 + local temp2X, temp2Y = getPointOnCurve(t, curve1) + local line = {} + line.x1 = temp1X + line.y1 = temp1Y + line.x2 = temp2X + line.y2 = temp2Y + table.insert(sectionsCurve1, line) + temp1X, temp1Y = temp2X, temp2Y + i = i + 1 + end + + -- get line segments of the second curve + local numberOfSectionsCurve2 = numberOfCurvePartitions + temp1X, temp1Y = getPointOnCurve(0, curve2) + i = 1 + while i <= numberOfSectionsCurve2 do + local t = i / numberOfSectionsCurve2 + local temp2X, temp2Y = getPointOnCurve(t, curve2) + local line = {} + line.x1 = temp1X + line.y1 = temp1Y + line.x2 = temp2X + line.y2 = temp2Y + table.insert(sectionsCurve2, line) + temp1X, temp1Y = temp2X, temp2Y + i = i + 1 + end + + -- get the minimal distance of the 2 curve approximations + local minDistance = math.huge + for _, line1 in pairs(sectionsCurve1) do + for _, line2 in pairs(sectionsCurve2) do + local distance = getDistance(line1, line2) + if distance <= minDistance then + minDistance = distance + end + end + end + + return minDistance +end +function getDistance(line1, line2) + local t1, t2 = pathLine.line_line_intersection(line1.x1, line1.y1, line1.x2, line1.y2, + line2.x1, line2.y1, line2.x2, line2.y2) + if 0 <= t1 and t1 <= 1 and 0 <= t2 and t2 <= 1 then + -- the lines do intersect + return 0 + end + + local d1 = pathLine.hit(line2.x1, line2.y1, line1.x1, line1.y1, line1.x2, line1.y2) + local d2 = pathLine.hit(line2.x2, line2.y2, line1.x1, line1.y1, line1.x2, line1.y2) + local d3 = pathLine.hit(line1.x1, line1.y1, line2.x1, line2.y1, line2.x2, line2.y2) + local d4 = pathLine.hit(line1.x2, line1.y2, line2.x1, line2.y1, line2.x2, line2.y2) + return math.sqrt(math.min(d1, d2, d3, d4)) +end +function applyForces(v) + --print("force on note " .. v.index .. ": " .. v.currentForce .. " (limit: +" .. v.forceLimitInc .. ", -" .. v.forceLimitDec .. ")") + + -- limit the force so the movable control point is between the port and the optimal position + local actualR = math.abs(labelArea:getXTextSide(v.rightSide) - v.movableControlPointX) + local differenceR = math.abs(labelArea:getXTextSide(v.rightSide) - v.optimalPositionX) - actualR + if (v.currentForce < 0 and math.abs(v.currentForce) > actualR) then + v.currentForce = (-1) * actualR + end + if (v.currentForce > 0 and v.currentForce > differenceR) then + v.currentForce = differenceR + end + + -- limit the force so 2 curves do not get too close to each other and do not cross + if v.currentForce > v.forceLimitInc then + v.currentForce = v.forceLimitInc + end + if v.currentForce < (-1) * v.forceLimitDec then + v.currentForce = (-1) * v.forceLimitDec + end + v.forceLimitDec = math.huge + v.forceLimitInc = math.huge + + if v.rightSide then + v.movableControlPointX = v.movableControlPointX - v.currentForce + else + v.movableControlPointX = v.movableControlPointX + v.currentForce + end + + --print("force on note " .. v.index .. ": " .. v.currentForce) + local c = v.currentForce + v.currentForce = 0 + return c +end +function getAngle(centerX, centerY, x, y) + local vectorX = x - centerX + local vectorY = y - centerY + local length = math.sqrt((vectorX ^ 2) + (vectorY ^ 2)) + + vectorX = vectorX / length + vectorY = vectorY / length + + local radAngle = math.acos(vectorX) + local degAngle = (radAngle * 180) / math.pi + + if vectorY < 0 then + degAngle = 360 - degAngle + end + + return degAngle +end +function solveQuadraticEquation(a, b, c) + local discr = (b * b) - (4 * a * c) + + if discr < 0 then + error("Fehler bei der Berechnung das optimalen Punktes") + end + + local solution1 = ((-b) + math.sqrt(discr)) / (2 * a) + local solution2 = ((-b) - math.sqrt(discr)) / (2 * a) + + if solution1 < 0 and solution2 < 0 then + error("no positive solution") + end + + if solution1 < solution2 then + return solution2 + else + return solution1 + end +end +function computeOptimalPosition(v) + local distance = point.distance(v.inputX, v.inputY, labelArea:getXTextSide(v.rightSide), v.leaderArmY) + + -- the angle at the port between the point and the movable control point + local tempAngle = getAngle(v.inputX, v.inputY, labelArea:getXTextSide(v.rightSide), v.leaderArmY) + + local gamma + if v.rightSide then + if tempAngle < 180 then + gamma = tempAngle + else + gamma = 360 - tempAngle + end + else + if tempAngle < 180 then + gamma = 180 - tempAngle + else + gamma = tempAngle - 180 + end + end + + -- a quadratic formula has to be solved to get the optimal position + local a = 3 + local b = 2 * distance * math.cos(math.rad(gamma)) + local c = -(distance * distance) + + local r = solveQuadraticEquation(a, b, c) + + if v.rightSide then + v.optimalPositionX = labelArea:getXTextSide(v.rightSide) - r + else + v.optimalPositionX = labelArea:getXTextSide(v.rightSide) + r + end +end +function drawSBezierLeaders() + for _, v in pairs(notesForPage) do + -- initialise leader + v.leaderArmY = v:getLabelAnchorY() + v.movableControlPointX = labelArea:getXTextSide(v.rightSide) + v.currentForce = 0 + v.forceLimitDec = math.huge + v.forceLimitInc = math.huge + end + + numberOfCurvePartitions = #notesForPage * 3 + + for _, v in pairs(notesForPage) do + computeOptimalPosition(v) + end + + -- main loop + local proceed = true + local loopCounter = 0 + while (proceed and loopCounter < maxIterations) do + --print("") + --print("--------------------------------------------------") + print("iteration " .. loopCounter .. ":") + + -- compute forces + computeRepulsiveControlPointForces() + computeAttractingControlPointForces() + + -- apply forces + proceed = false + for _, l in pairs(notesForPage) do + local diff = applyForces(l) + if diff > stopCondition then + proceed = true + end + end + + loopCounter = loopCounter + 1 + end + + print("") + print("End of Force-directed algo") + print("number of iterations: " .. loopCounter) + print("") + + -- draw + for _, v in pairs(notesForPage) do + local curve = constructCurve(v) + local unmovableStr = "(" .. curve[2].x .. "sp," .. curve[2].y .. "sp)" + local movableStr = "(" .. curve[3].x .. "sp," .. curve[3].y .. "sp)" + drawLeaderPath(v, v:getInTextAnchorTikz() .. " .. controls " .. + unmovableStr .. " and " .. movableStr .. " .. " .. + v:getLabelAnchorTikz()) + + -- draw control points when requested + if todonotesDebug then + local optimalStr = "(" .. v.optimalPositionX .. "sp," .. v.leaderArmY .. "sp)" + tex.print("\\node[anchor=north west,text=pink,fill=white,rectangle] at " .. optimalStr .. " {" .. v.index .. "};") + tex.print("\\node[anchor=north west,text=red,fill=white,rectangle] at " .. movableStr .. " {" .. v.index .. "};") + tex.print("\\node[anchor=north west,text=orange,fill=white,rectangle] at " .. unmovableStr .. " {" .. v.index .. "};") + tex.print("\\draw[red,fill] " .. movableStr .. " circle(2pt);") + tex.print("\\draw[orange,fill] " .. unmovableStr .. " circle(2pt);") + tex.print("\\draw[pink,fill] " .. optimalStr .. " circle(1pt);") + end + end +end +leaderTypes["sBezier"] = {algo = drawSBezierLeaders} + + + + + +-- ********** Positioning Algorithms ********** + +-- ** helper functions + +-- divides notes in two lists (for left and right side) +-- side must be stored in note.rightSide for every note before using this function +function segmentNotes(notes) + local availableNotesLeft = {} + local availableNotesRight = {} + for k, v in pairs(notes) do + if v.rightSide == true then + table.insert(availableNotesRight, k) + else + table.insert(availableNotesLeft, k) + end + end + return availableNotesLeft, availableNotesRight +end + +-- finds the index in the list given as parameter with the minimum angle +-- the function used for computation of the angle is given as second parameter +-- (the alphaFormula gets the note, to which the angle should be computed, as +-- the only parameter) +function findIndexMinAlpha(availableNotesIndex, alphaFormula) + local minAlpha = math.huge -- infinity + local minIndex = -1 + + for k, v in pairs(availableNotesIndex) do + local alpha = alphaFormula(notesForPage[v]) + if alpha < minAlpha then + minAlpha = alpha + minIndex = k + end + end + + return minIndex +end + + + +-- ** partition into stacks +function getMeanYHeight(stack) + -- TODO: Alternative: nicht das arithmetische Mittel verwenden, sondern + -- Mittelpunkt zwischen dem obersten und untersten Punkt + local sumY = 0 + local height = 0 + for _, v in pairs(stack) do + sumY = sumY + notesForPage[v].inputY + height = height + notesForPage[v]:getHeight() + 2 * noteInnerSep + noteInterSpace + end + + local area = labelArea:getArea(notesForPage[stack[1]].rightSide) + + local meanY = sumY / #stack + local height = height - noteInterSpace + if meanY + (height/2) > area.top then + meanY = area.top - (height/2) + elseif meanY - (height/2) < area.bottom then + meanY = area.bottom + (height/2) + end + return meanY, height +end +function stacksIntersect(stackTop, stackBottom) + local topMeanY, topHeight = getMeanYHeight(stackTop) + local topLower = topMeanY - topHeight/2 + + local bottomMeanY, bottomHeight = getMeanYHeight(stackBottom) + local bottomUpper = bottomMeanY + bottomHeight/2 + + if topLower - bottomUpper < noteInterSpace then + return true + else + return false + end +end +function findStacks(notesOnSide) + local notes = table.copy(notesOnSide) + table.sort(notes, compareNoteIndInputYDesc) + + -- list that contains stacks + -- is initialized by putting all notes as single stacks ordered by their inputY + local stacks = {} + for _, v in pairs(notes) do + table.insert(stacks, {v}) + end + + -- Collapse Stacks where needed + local i = 1 + while i <= #stacks - 1 do + if stacksIntersect(stacks[i], stacks[i+1]) then + collapsedStacks = true + for _, v in pairs(stacks[i+1]) do + table.insert(stacks[i], v) + end + table.remove(stacks,i+1) + if i > 1 then + -- as stack i has increased in size we look at the previous + -- stack again in next iteration + i = i - 1 + end + else + -- look at next stack in next iteration + i = i + 1 + end + end + + return stacks +end + + +-- ** positioning: inText +function posInText() + -- trivial algorithm + -- places notes in text on position where todo-command was issued + for k, v in ipairs(notesForPage) do + v.outputX = v.inputX + v.outputY = v.inputY + end +end +positioningAlgos["inText"] = {algo = posInText, + leaderAnchor = "north west", + leaderShift = false, + twoSided = false} + + + +-- ** positioning: inputOrder +-- start at top and place notes below each other on left/right side +-- notes are placed in the order induced by their y-coordinates +function posInputOrder(notes, rightSide) + table.sort(notes, compareNoteIndInputYDesc) + placeNotesInputOrder(notes, labelArea:getArea(rightSide).top, rightSide) +end +positioningAlgos["inputOrder"] = {algo = posInputOrder, + leaderAnchor = "east", + leaderShift = false, + twoSided = true} + + + +-- ** positioning: inputOrderStacks +function placeNotesInputOrder(stack, yStart, rightSide) + local freeY = yStart + + for _, k in ipairs(stack) do + local v = notesForPage[k] + v.outputX = labelArea:getArea(rightSide).left + v.outputY = freeY + freeY = freeY - v:getHeight() - 2 * noteInnerSep - noteInterSpace + end +end +function posInputOrderStacks(notesOnSide, rightSide) + table.sort(notesOnSide, compareNoteIndInputYDesc) + + local stacks = findStacks(notesOnSide) + + -- place stacks + for k, stack in pairs(stacks) do + local meanY, height = getMeanYHeight(stack) + local stackStart = meanY + height / 2 + placeNotesInputOrder(stack, stackStart, rightSide) + end +end +positioningAlgos["inputOrderStacks"] = {algo = posInputOrderStacks, + leaderAnchor = "east", + leaderShift = false, + twoSided = true} + + + +-- ** positioning: sLeaderNorthEast +function posSLeaderNorthEast(notes, rightSide) + local noteY = labelArea:getArea(rightSide).top + + local alphaFormula + local noteX = labelArea:getXTextSide(rightSide) + local outputX = labelArea:getArea(rightSide).left + if rightSide then + alphaFormula = function (note) + return (noteY - note.inputY) / (noteX - note.inputX) + end + else + alphaFormula = function (note) + return (noteY - note.inputY) / (note.inputX - noteX) + end + end + + while #notes > 0 do + local minIndex = findIndexMinAlpha(notes, alphaFormula) + + -- place note identified by minIndex + local note = notesForPage[notes[minIndex]] + note.outputX = outputX + note.outputY = noteY + noteY = noteY - note:getHeight() - 2 * noteInnerSep - noteInterSpace + + table.remove(notes, minIndex) + end +end +positioningAlgos["sLeaderNorthEast"] = {algo = posSLeaderNorthEast, + leaderAnchor = "north east", + leaderShift = false, + twoSided = true} + + + +-- ** positioning: sLeaderNorthEastBelow +function placeNotesNorthEastBelow(stack, yStart, rightSide) + -- calculate minimum height of all notes + local minHeight = math.huge -- (infinity) + for _, v in pairs(stack) do + if notesForPage[v]:getHeight() < minHeight then + minHeight = notesForPage[v]:getHeight() + end + end + local leaderShiftY = (- minHeight - 2 * noteInnerSep) / 2 + + local noteY = yStart + local availableNotes = table.copy(stack) + + local alphaFormula + local noteX = labelArea:getXTextSide(rightSide) + local outputX = labelArea:getArea(rightSide).left + if rightSide == true then + alphaFormula = function (note) + return ((noteY + leaderShiftY) - note.inputY) / (noteX - note.inputX) + end + else + alphaFormula = function (note) + return ((noteY + leaderShiftY) - note.inputY) / (note.inputX - noteX) + end + end + while #availableNotes > 0 do + local minIndex = findIndexMinAlpha(availableNotes, alphaFormula) + + -- place note identified by minIndex + local note = notesForPage[availableNotes[minIndex]] + note.outputX = outputX + note.outputY = noteY + note.leaderShiftX = 0 + note.leaderShiftY = leaderShiftY + noteY = noteY - note:getHeight() - 2 * noteInnerSep - noteInterSpace + + table.remove(availableNotes, minIndex) + end +end +function posSLeaderNorthEastBelow(notes, rightSide) + placeNotesNorthEastBelow(notes, labelArea:getArea(rightSide).top, rightSide) +end +positioningAlgos["sLeaderNorthEastBelow"] = {algo = posSLeaderNorthEastBelow, + leaderAnchor = "north east", + leaderShift = true, + twoSided = true} + + + +-- ** positioning: sLeaderNorthEastBelowStacks +function posSLeaderNorthEastBelowStacks(notesOnSide, rightSide) + local stacks = findStacks(notesOnSide) + + -- place stacks + for k, stack in pairs(stacks) do + local meanY, height = getMeanYHeight(stack) + local stackStart = meanY + height / 2 + placeNotesNorthEastBelow(stack, stackStart, rightSide) + end +end +positioningAlgos["sLeaderNorthEastBelowStacks"] = {algo = posSLeaderNorthEastBelowStacks, + leaderAnchor = "north east", + leaderShift = true, + twoSided = true} + + + +-- ** positioning: sLeaderEast +function posSLeaderEast(notes, rightSide) + local leaderPosY + local noteY = labelArea:getArea(rightSide).top + + local alphaFormula + local noteX = labelArea:getXTextSide(rightSide) + local outputX = labelArea:getArea(rightSide).left + if rightSide == true then + alphaFormula = function (note) + return (leaderPosY - note.inputY) / (noteX - note.inputX) + end + else + alphaFormula = function (note) + return (leaderPosY - note.inputY) / (note.inputX - noteX) + end + end + + local placedNotes = {} + while #notes > 0 do + -- build a array with all distinct heights of the notes + -- first create a set and then convert to sorted array + local heights = {} + print(#heights) + for _, v in pairs(notes) do + heights[notesForPage[v]:getHeight()] = true + end + heights = table.keys(heights) + table.sort(heights) + + local chosenIndex = -1 + local chosenH = -1 + for _, h in pairs(heights) do + print("testing height: " .. h) -- DEBUG + leaderPosY = noteY - noteInnerSep - h/2 + + -- find point with highest angle + local minIndex = findIndexMinAlpha(notes, alphaFormula) + + -- found a valid note + if notesForPage[notes[minIndex]]:getHeight() <= h then + chosenIndex = minIndex + chosenH = h + break + end + end + + -- place note identified by chosenIndex + local note = notesForPage[notes[chosenIndex]] + note.outputX = outputX + -- let free space above note if needed (if chosenH ~= note:getHeight()) + note.outputY = noteY - (chosenH - note:getHeight()) / 2 + -- no extraordinary free space below note (even if chosenH ~= note:getHeight()) + noteY = note.outputY - note:getHeight() - 2 * noteInnerSep - noteInterSpace + -- DEBUG + if chosenH ~= note:getHeight() then + print("Creating free space above note " .. note.index) + end + + table.insert(placedNotes, notes[chosenIndex]) + table.remove(notes, chosenIndex) + end + + -- postprocessing: reduce spaces between notes where possible + for ind, noteNr in pairs(placedNotes) do + local note = notesForPage[noteNr] + + local aimedPos + if ind == 1 then + aimedPos = labelArea:getArea(rightSide).top + else + local prevNote = notesForPage[placedNotes[ind-1]] + aimedPos = prevNote.outputY - prevNote:getHeight() - 2 * noteInnerSep - noteInterSpace + end + + -- DEBUG + if aimedPos ~= note.outputY then + print("note " .. note.index .. " got moved:") + print("aimed: " .. aimedPos) + print("real: " .. note.outputY) + end + + local aimedLeaderAnchorY = aimedPos - noteInnerSep - note:getHeight() / 2 + local realLeaderAnchorY = note.outputY - noteInnerSep - note:getHeight() / 2 + -- it holds: realLeaderAnchorY < aimedLeaderAnchorY (realLeaderAnchor is lower on page) + + -- check if there are points in triangle (aimedLeaderAnchor, note.input, realLeaderAnchor) + -- we perform this check by calculating the angle of the points referred to note.input + local pointsInTriangle = false + local denom + if rightSide then + denom = noteX - note.inputX + else + denom = note.inputX - noteX + end + local aimedLeaderAnchorAngle = (aimedLeaderAnchorY - note.inputY) / denom + local realLeaderAnchorAngle = (realLeaderAnchorY - note.inputY) / denom + local minAngle = math.huge + local minAngleIndex = -1 -- takes index of lowest point in triangle + for otherInd, otherNote in pairs(notesForPage) do + if otherInd ~= noteNr and + ((not rightSide and otherNote.inputX < note.inputX) or + (rightSide and otherNote.inputX > note.inputX)) then + local otherNoteAngle + if rightSide then + otherNoteAngle = (otherNote.inputY - note.inputY) / (otherNote.inputX - note.inputX) + else + otherNoteAngle = (otherNote.inputY - note.inputY) / (note.inputX - otherNote.inputX) + end + + if (realLeaderAnchorAngle < otherNoteAngle) + and (otherNoteAngle < aimedLeaderAnchorAngle) then + pointsInTriangle = true + if otherNoteAngle < minAngle then + minAngle = otherNoteAngle + minAngleIndex = otherInd + end + print(otherNote.index .. " is in triangle for " .. note.index) -- DEBUG + end + end + end + + if not pointsInTriangle then + -- no points in triangle + -- => we can move this note to aimedPos + note.outputY = aimedPos + else + -- move note upwards so that leader touches lowest point in triangle + -- new point for leader anchor is determined by the ray from note.input through the lowest point in triangle (otherNote.input) + -- TODO: force a certain distance between leader and other points (at the moment a leader can contain endpoints of other leaders) + local otherNote = notesForPage[minAngleIndex] + local aimedLeaderAnchorY = note.inputY - (note.inputY - otherNote.inputY) * (note.inputX - noteX) / (note.inputX - otherNote.inputX) + note.outputY = aimedLeaderAnchorY + noteInnerSep + note:getHeight() / 2 + end + end +end +positioningAlgos["sLeaderEast"] = {algo = posSLeaderEast, + leaderAnchor = "east", + leaderShift = false, + twoSided = true} + + + +-- ** positioning: poLeaders +function getRasterAbsolute(rasterHeight, top, rasterIndex) + return top - (rasterIndex - 1) * rasterHeight +end + -- distance between line and leader that algorithm tries to reach when there is + -- no neighbouring line +local poMinDistLine = string.todimen("4pt") +function getPosAboveLine(linePositionsCurPage, lineInd) + local line = linePositionsCurPage[lineInd] + local posAbove + if linePositionsCurPage[lineInd - 1] ~= nil then + posAbove = (line[2] + linePositionsCurPage[lineInd - 1][3]) / 2 + end + if posAbove == nil or posAbove - line[2] > poMinDistLine then + posAbove = line[2] + poMinDistLine + end + return posAbove +end +function getPosBelowLine(linePositionsCurPage, lineInd) + local line = linePositionsCurPage[lineInd] + local posBelow + if linePositionsCurPage[lineInd + 1] ~= nil then + posBelow = (line[3] + (linePositionsCurPage[lineInd + 1][2])) / 2 + end + if posBelow == nil or line[3] - posBelow > poMinDistLine then + posBelow = line[3] - poMinDistLine + end + return posBelow +end +function posPoLeaders(notes, rightSide, avoidLines) + print("rasterHeight: " .. rasterHeight) + + local linePositionsCurPage + if avoidLines then + linePositionsCurPage = linePositions[currentPage] or {} + end + + -- number of slots on the whole page + local area = labelArea:getArea(rightSide) + local totalNumSlots = math.floor((area.top - area.bottom) / rasterHeight) + + -- calculate number of raster slots for each note + for _, ind in pairs(notes) do + local v = notesForPage[ind] + local height = v:getHeight() + 2 * noteInnerSep + noteInterSpace + v.rasterSlots = math.ceil(height / rasterHeight) + end + + -- sort notes by inputY + table.sort(notes, compareNoteIndInputYDesc) + + -- draw slots + if todonotesDebug then + for i = 1,totalNumSlots+1 do + local pos = area.top - (i-1) * rasterHeight + tex.print("\\draw[blue,dashed] (0," .. pos .. "sp) -- +(21cm,0);") + end + end + + -- initialise table opt for dynamic program + -- opt[topPoint, bottomPoint, topSlot, bottomSlot, numberLabeledSites] + -- opt[a][b][c][d][e] describes length-minimal placement of the labels for + -- sites from a to b in the raster slots c to d + -- the leftmost/rightmost e sites between a and b are selected until there + -- are no more free slots + local opt = {} + for a = 1, #notes do + opt[a] = {} + for b = a, #notes do + opt[a][b] = {} + -- TODO: needed label slots are restricted by points + -- only create tables for needed slots + for i = 1, totalNumSlots do + opt[a][b][i] = {} + for j = i, totalNumSlots do + if i == j then + opt[a][b][i][j] = {} + opt[a][b][i][j][0] = {} + opt[a][b][i][j][0].totalLength = 0 + opt[a][b][i][j][0].positions = {} + opt[a][b][i][j][0].leaderShiftY = {} + end + end + end + end + end + + -- constant use an partial solution without labeled points + local optEmpty = {} + optEmpty.totalLength = 0 + optEmpty.positions = {} + optEmpty.leaderShiftY = {} + + -- fill table opt for dynamic program + -- numberOfPoints is difference between topPoint and bottomPoint when computing opt + for numberOfPoints = 1, #notes do + for topPoint = 1, (#notes - numberOfPoints + 1) do + -- compute opt[topStrip][bottomStrip] + local bottomPoint = topPoint + numberOfPoints - 1 + + local pointsBetween = {} + for i = topPoint, bottomPoint do + table.insert(pointsBetween, notes[i]) + end + if rightSide then + table.sort(pointsBetween, compareNoteIndInputXDesc) + else + table.sort(pointsBetween, compareNoteIndInputXAsc) + end + + -- TODO: Einschränken, nicht alle Kombinationen von Slots notwendig + -- (siehe auch oben) + -- numberOfSlots is difference between topSlot and bottomSlot when computing opt + for numberOfSlots = 1, totalNumSlots do + for topSlot = 1, (totalNumSlots - numberOfSlots + 1) do + local bottomSlot = topSlot + numberOfSlots - 1 + + -- DEBUG + --print("computing opt[" .. topPoint .. "][" .. bottomPoint .. + --"][" .. topSlot .. "][" .. bottomSlot .. "]") + + opt[topPoint][bottomPoint][topSlot][bottomSlot] = {} + opt[topPoint][bottomPoint][topSlot][bottomSlot][0] = {} + opt[topPoint][bottomPoint][topSlot][bottomSlot][0].totalLength = 0 + opt[topPoint][bottomPoint][topSlot][bottomSlot][0].positions = {} + opt[topPoint][bottomPoint][topSlot][bottomSlot][0].leaderShiftY = {} + + -- stelle fest, wie viele Punkte gelabelt werden (bestimme also r) + local labeledSites = {} + local usedSlots = 0 + for _, v in pairs(pointsBetween) do + local note = notesForPage[v] + if usedSlots + note.rasterSlots <= numberOfSlots then + usedSlots = usedSlots + note.rasterSlots + table.insert(labeledSites, v) + else + break + end + end + + -- TODO: Teste, ob bei kleinerer Anzahl an Slots gleiche Punkte gelabelt werden + -- -> dann kann Teillösung übernommen werden + + -- Mögliche Aufteilungen (Positionierung des Labels für r) testen + -- und Optimum auswählen + if #labeledSites > 0 then + for numLabeledSites = 1, #labeledSites do + -- we place rightmost point: labeledSites[#labeledSites] + local rIndex = labeledSites[numLabeledSites] + local r = notesForPage[rIndex] + + -- slotIndexR is the slot in that the label for r begins (topmost slot) + local bestVal = math.huge + local bestOpt = {} + -- try all label positions for r (leader should enter the label at east-anchor) + for slotIndexR = topSlot, (bottomSlot + 1 - r.rasterSlots) do + -- calculate position in which leader arm is placed + local labelTopR = getRasterAbsolute(rasterHeight, area.top, slotIndexR) + local leaderArmR, leaderShiftR + if avoidLines then + leaderArmR = labelTopR - noteInnerSep - r:getHeight() / 2 -- east anchor + + -- find first line (from the top) which lower bound is below leaderArmR + local lineBelowInd + for ind, v in pairs(linePositionsCurPage) do + if v[3] <= leaderArmR then + lineBelowInd = ind + break + end + end + + -- choose the desired position for the leader arm + -- (later we check if the label is high enough to shift the port to this position) + local desiredPos = leaderArmR + if lineBelowInd == nil then + -- there is no line below the leaderArmR + local lowestLine = linePositionsCurPage[#linePositionsCurPage] + if lowestLine ~= nil and lowestLine[3] - leaderArmR < poMinDistLine then + -- leader is too near to lowest line on page + -- -> use the valid position below this line + desiredPos = getPosBelowLine(linePositionsCurPage, #linePositionsCurPage) + end + else + local lineBelow = linePositionsCurPage[lineBelowInd] + local lineAbove = linePositionsCurPage[lineBelowInd - 1] + if lineBelow ~= nil and leaderArmR - lineBelow[2] < poMinDistLine then + -- leader is too near (or conflicting) to the line below + -- -> move below or above this line (using the position closer to the original one) + + local posAbove = getPosAboveLine(linePositionsCurPage, lineBelowInd) + local posBelow = getPosBelowLine(linePositionsCurPage, lineBelowInd) + + -- chose position which is closer to east anchor + if posAbove - leaderArmR <= leaderArmR - posBelow then + desiredPos = posAbove + else + desiredPos = posBelow + end + elseif lineAbove ~= nil and lineAbove[3] - leaderArmR < poMinDistLine then + -- leader is too near to the line below + -- -> use the valid position above this line + desiredPos = getPosAboveLine(linePositionsCurPage, lineBelowInd) + end + end + + -- check if label is high enough to move leader to desired position + if math.abs(desiredPos - leaderArmR) <= r:getHeight() / 2 + noteInnerSep then + leaderShiftR = desiredPos - leaderArmR + leaderArmR = desiredPos + else + leaderShiftR = 0 + end + else + leaderArmR = labelTopR - noteInnerSep - r:getHeight() / 2 + end + + -- determine index of last point above arm + local pointAboveArm = 0 + for k, ind in pairs(notes) do + v = notesForPage[ind] + if v.inputY >= leaderArmR then + pointAboveArm = k + else + break + end + end + local numPointsAbove = 0 + local numPointsBelow = 0 + for _, v in pairs(pointsBetween) do + if v == rIndex then + break + end + + local note = notesForPage[v] + if note.inputY >= leaderArmR then + numPointsAbove = numPointsAbove + 1 + else + numPointsBelow = numPointsBelow + 1 + end + end + + local optAbove, optBelow + if pointAboveArm < topPoint then + optAbove = optEmpty + elseif slotIndexR - 1 < topSlot then + optAbove = optEmpty + elseif pointAboveArm > bottomPoint then + optAbove = opt[topPoint][bottomPoint][topSlot][slotIndexR - 1][numPointsAbove] + else + optAbove = opt[topPoint][pointAboveArm][topSlot][slotIndexR - 1][numPointsAbove] + end + if pointAboveArm + 1 > bottomPoint then + optBelow = optEmpty + elseif slotIndexR + r.rasterSlots > bottomSlot then + optBelow = optEmpty + elseif pointAboveArm + 1 < topPoint then + optBelow = opt[topPoint][bottomPoint][slotIndexR + r.rasterSlots][bottomSlot][numPointsBelow] + else + optBelow = opt[pointAboveArm + 1][bottomPoint][slotIndexR + r.rasterSlots][bottomSlot][numPointsBelow] + end + + local partitionValid = true + if optAbove == nil or optBelow == nil then + partitionValid = false + else + local labeledAboveArm = table.keys(optAbove.positions) + local labeledBelowArm = table.keys(optBelow.positions) + if #labeledAboveArm + #labeledBelowArm + 1 ~= numLabeledSites then + partitionValid = false + else + -- test if all of labeledNotes are in one of the sets + -- last element (= r) must not be tested + for testIndex = 1, (numLabeledSites - 1) do + if not table.contains(labeledAboveArm, labeledSites[testIndex]) + and not table.contains(labeledBelowArm, labeledSites[testIndex]) then + partitionValid = false + end + end + end + end + + if partitionValid then + local newVal = math.abs(r.inputY - leaderArmR) + optAbove.totalLength + optBelow.totalLength + if newVal < bestVal then + bestVal = newVal + bestOpt = {} + bestOpt.totalLength = newVal + bestOpt.positions = {} + for k, v in pairs(optAbove.positions) do + bestOpt.positions[k] = v + end + for k, v in pairs(optBelow.positions) do + bestOpt.positions[k] = v + end + -- DEBUG + if bestOpt.positions[rIndex] ~= nil then + error("WARNING: Overwriting position of " .. rIndex .. " from " .. bestOpt.positions[rIndex] .. " to " .. slotIndexR) + end + bestOpt.positions[rIndex] = slotIndexR + bestOpt.leaderShiftY = {} + if avoidLines then + for k, v in pairs(optAbove.leaderShiftY) do + bestOpt.leaderShiftY[k] = v + end + for k, v in pairs(optBelow.leaderShiftY) do + bestOpt.leaderShiftY[k] = v + end + bestOpt.leaderShiftY[rIndex] = leaderShiftR + end + end + end + end + if next(bestOpt) ~= nil then -- bestOpt is not an empty table + --print("setting opt[" .. topPoint .. "][" .. bottomPoint .. + --"][" .. topSlot .. "][" .. bottomSlot .. "][" .. numLabeledSites .. "] = " .. inspect(bestOpt)) + opt[topPoint][bottomPoint][topSlot][bottomSlot][numLabeledSites] = bestOpt + else + --print ("WARNING: Found no valid position for label in opt[" .. topPoint .. "][" .. bottomPoint .. + --"][" .. topSlot .. "][" .. bottomSlot .. "][" .. numLabeledSites .. "] = " .. inspect(bestOpt)) + opt[topPoint][bottomPoint][topSlot][bottomSlot][numLabeledSites] = nil + end + end + end + end + end + end + end + + if #notes > 0 then + --DEBUG + local maxPlaced = 0 + for i = 1,#notes do + if opt[1][#notes][1][totalNumSlots][i] ~= nil then + maxPlaced = i + end + end + + if maxPlaced < #notes then + print("WARNING: could not fit all labels on page") + end + + print("Using result: opt[1][" .. #notes .. + "][1][" .. totalNumSlots .. "][" .. maxPlaced .. "]") + local res = opt[1][#notes][1][totalNumSlots][maxPlaced] + local length = res.totalLength + local positions = res.positions + local leaderShiftY = res.leaderShiftY + + print("") + print("----------------") + print("resulting length: " .. length) + print("") + print("resulting positions:") + print(inspect(positions)) + print("") + print("resulting leaderShifts:") + print(inspect(leaderShiftY)) + print("") + print("") + print("") + + + -- DEBUG on page + if todonotesDebug then + tex.print("\\node[text=blue,fill=white,rectangle,align=center] at (10.5cm,-27cm) {" .. + "total length: " .. number.tocentimeters(length) .. "\\\\ " .. + "rasterHeight: " .. number.tocentimeters(rasterHeight) .. + "};") + end + + for _, ind in pairs(notes) do + local v = notesForPage[ind] + v.outputX = area.left + v.leaderShiftX = 0 + if positions[ind] == nil then + print("did not define a position for note " .. v.index) + v.outputY = 0 + v.leaderShiftY = 0 + else + v.outputY = getRasterAbsolute(rasterHeight, area.top, positions[ind]) + if leaderShiftY[ind] ~= nil then + v.leaderShiftY = leaderShiftY[ind] + end + end + end + end +end +positioningAlgos["poLeaders"] = {algo = posPoLeaders, + leaderAnchor = "east", + leaderShift = false, + twoSided = true} + +function posPoLeadersAvoid(notes, rightSide) + posPoLeaders(notes, rightSide, true) +end +positioningAlgos["poLeadersAvoidLines"] = {algo = posPoLeadersAvoid, + leaderAnchor = "east", + leaderShift = true, + twoSided = true, + needLinePositions = true} + + + + +-- ********** Splitting Algorithms ********** + +-- ** splittingAlgorithm: none +-- place all notes on the wider side +function splitNone() + local rightSideSelected = false + if labelArea.left == nil and labelArea.right == nil then + error("Cannot place labels on any side of text (not enough space). " .. + "Consider using the minNoteWidth option of the package to " .. + "decrease the minimum width required") + elseif labelArea.left == nil then + rightSideSelected = true + elseif labelArea.right ~= nil and + labelArea.right.noteWidth > labelArea.left.noteWidth then + rightSideSelected = true + end + + for _, v in pairs(notesForPage) do + v.rightSide = rightSideSelected + end +end +splittingAlgos["none"] = {algo = splitNone} + + + +-- ** splittingAlgorithm: middle +-- split on middle of page +function splitMiddle() + if labelArea:isOneSided() then + splitNone() + else + local splitLine = (labelArea.text.right + labelArea.text.left)/2 + for _, v in pairs(notesForPage) do + if v.inputX <= splitLine then + v.rightSide = false + else + v.rightSide = true + end + end + end +end +splittingAlgos["middle"] = {algo = splitMiddle} + + + +-- ** splittingAlgorithm: median +-- split at median (sites sorted by x-coordinate) +function splitMedian() + if labelArea:isOneSided() then + splitNone() + else + if #notesForPage == 0 then + return + end + + -- list that contains notes sorted by their inputX-coordinate + local notesSorted = {} + for _, v in pairs(notesForPage) do + table.insert(notesSorted, v) + end + table.sort(notesSorted, compareNoteInputXAsc) + + local maxIndLeft + if #notesSorted % 2 == 1 then + maxIndLeft = math.ceil(#notesSorted / 2) + else + maxIndLeft = #notesSorted / 2 + end + + for k, v in pairs(notesSorted) do + if k <= maxIndLeft then + v.rightSide = false + else + v.rightSide = true + end + end + end +end +splittingAlgos["median"] = {algo = splitMedian} + + + +-- ** splittingAlgorithm: weightedMedian +-- split at weighted median (sites sorted by x-coordinate) +-- sum of heights of labels on both sides are similiar to each other +function splitWeightedMedian() + if labelArea:isOneSided() then + splitNone() + else + if #notesForPage == 0 then + return + end + + -- list that contains notes sorted by their inputX-coordinate + local notesSorted = {} + for _, v in pairs(notesForPage) do + table.insert(notesSorted, v) + end + table.sort(notesSorted, compareNoteInputXAsc) + + local heightLeft = 0 + local heightRight = 0 + while #notesSorted > 0 do + if heightRight < heightLeft then + -- place next note on the right side + local note = notesSorted[#notesSorted] + note.rightSide = true + heightRight = heightRight + note:getHeight() + 2 * noteInnerSep + noteInterSpace + table.remove(notesSorted, #notesSorted) + else + -- place next note on the left side + local note = notesSorted[1] + note.rightSide = false + heightLeft = heightLeft + note:getHeight() + 2 * noteInnerSep + noteInterSpace + table.remove(notesSorted, 1) + end + end + end +end +splittingAlgos["weightedMedian"] = {algo = splitWeightedMedian} + diff --git a/Master/texmf-dist/tex/lualatex/luatodonotes/luatodonotes.sty b/Master/texmf-dist/tex/lualatex/luatodonotes/luatodonotes.sty new file mode 100644 index 00000000000..6f7a193ccce --- /dev/null +++ b/Master/texmf-dist/tex/lualatex/luatodonotes/luatodonotes.sty @@ -0,0 +1,634 @@ +%% +%% This is file `luatodonotes.sty', +%% generated with the docstrip utility. +%% +%% The original source files were: +%% +%% luatodonotes.dtx (with options: `package') +%% +%% This is a generated file. +%% +%% Copyright (C) 2014 by Fabian Lipp <fabian.lipp@gmx.de> +%% based on the todonotes package by +%% Henrik Skov Midtiby <henrikmidtiby@gmail.com> +%% +%% This file may be distributed and/or modified under the conditions of +%% the LaTeX Project Public License, either version 1.2 of this license +%% or (at your option) any later version. The latest version of this +%% license is in: +%% +%% http://www.latex-project.org/lppl.txt +%% +%% and version 1.2 or later is part of all distributions of LaTeX version +%% 1999/12/01 or later. +%% +\NeedsTeXFormat{LaTeX2e}[1999/12/01] +\ProvidesPackage{luatodonotes} + [2014/08/07 v0.1 luatodonotes source and documentation.] + +\ProvidesPackage{luatodonotes}[2014/06/08] +\RequirePackage{ifthen} +\RequirePackage{xkeyval} +\RequirePackage{xcolor} +\RequirePackage{tikz} +\usetikzlibrary{positioning} +\usetikzlibrary{intersections} +\usetikzlibrary{decorations.pathmorphing} +\RequirePackage{luacode} +\RequirePackage{luatex} +\RequirePackage{atbegshi} +\RequirePackage{xstring} +\RequirePackage{zref-abspage} +\RequirePackage{ifoddpage} +\RequirePackage{soul} +\RequirePackage{soulpos} +\newcommand{\@todonotes@text}{}% +\newcommand{\@todonotes@backgroundcolor}{orange} +\newcommand{\@todonotes@linecolor}{black!30} +\newcommand{\@todonotes@bordercolor}{black} +\newcommand{\@todonotes@leaderwidth}{1.6pt} +\newcommand{\@todonotes@textsize}{\normalsize} +\newcommand{\@todonotes@figwidth}{\columnwidth} +\newcommand{\@todonotes@figheight}{4cm} +\newcommand{\@todonotes@positioning}{inputOrderStacks} +\newcommand{\@todonotes@splitting}{none} +\newcommand{\@todonotes@leadertype}{opo} +\newcommand{\@todonotes@interNoteSpace}{5pt} +\newcommand{\@todonotes@noteInnerSep}{5pt} +\newcommand{\@todonotes@routingAreaWidth}{0.4cm} +\newcommand{\@todonotes@minNoteWidth}{2.0cm} +\newcommand{\@todonotes@distanceNotesPageBorder}{0.5cm} +\newcommand{\@todonotes@distanceNotesText}{0.2cm} +\newcommand{\@todonotes@rasterHeight}{1cm} +\AtBeginDocument{ +\ifx\undefined\phantomsection +\newcommand{\phantomsection}{} +\fi +} +\newcommand{\@todonotes@todolistname}{Todo list} +\newcommand{\@todonotes@MissingFigureText}{Figure} +\newcommand{\@todonotes@MissingFigureUp}{Missing} +\newcommand{\@todonotes@MissingFigureDown}{figure} +\newcommand{\@todonotes@SetTodoListName}[1] + {\renewcommand{\@todonotes@todolistname}{#1}} +\newcommand{\@todonotes@SetMissingFigureText}[1] + {\renewcommand{\@todonotes@MissingFigureText}{#1}} +\newcommand{\@todonotes@SetMissingFigureUp}[1] + {\renewcommand{\@todonotes@MissingFigureUp}{#1}} +\newcommand{\@todonotes@SetMissingFigureDown}[1] + {\renewcommand{\@todonotes@MissingFigureDown}{#1}} +\newif{\if@todonotes@reverseMissingFigureTriangle} +\DeclareOptionX{catalan}{ + \@todonotes@SetTodoListName{Llista de feines pendents}% + \@todonotes@SetMissingFigureText{Figura}% + \@todonotes@SetMissingFigureUp{Figura}% + \@todonotes@SetMissingFigureDown{pendent}% +} +\DeclareOptionX{danish}{% + \@todonotes@SetTodoListName{G\o{}rem\aa{}lsliste}% + \@todonotes@SetMissingFigureText{Figur}% + \@todonotes@SetMissingFigureUp{Manglende}% + \@todonotes@SetMissingFigureDown{figur}% +} +\DeclareOptionX{dutch}{% + \@todonotes@SetTodoListName{Lijst van onafgewerkte taken}% + \@todonotes@SetMissingFigureText{Figuur}% + \@todonotes@SetMissingFigureUp{Ontbrekende}% + \@todonotes@SetMissingFigureDown{figuur}% +} +\DeclareOptionX{english}{% + \@todonotes@SetTodoListName{Todo list}% + \@todonotes@SetMissingFigureText{Figure}% + \@todonotes@SetMissingFigureUp{Missing}% + \@todonotes@SetMissingFigureDown{figure}% +} +\DeclareOptionX{french}{% + \@todonotes@SetTodoListName{Liste des points \`a traiter}% + \@todonotes@SetMissingFigureText{Figure}% + \@todonotes@SetMissingFigureUp{Figure}% + \@todonotes@SetMissingFigureDown{manquante}% + \@todonotes@reverseMissingFigureTrianglefalse +} +\DeclareOptionX{german}{% + \@todonotes@SetTodoListName{Liste der noch zu erledigenden Punkte}% + \@todonotes@SetMissingFigureText{Abbildung}% + \@todonotes@SetMissingFigureUp{Fehlende}% + \@todonotes@SetMissingFigureDown{Abbildung}% +} +\DeclareOptionX{italian}{ + \@todonotes@SetTodoListName{Elenco delle cose da fare}% + \@todonotes@SetMissingFigureText{Figura}% + \@todonotes@SetMissingFigureUp{Figura}% + \@todonotes@SetMissingFigureDown{mancante}% +} +\DeclareOptionX{ngerman}{% + \@todonotes@SetTodoListName{Liste der noch zu erledigenden Punkte}% + \@todonotes@SetMissingFigureText{Abbildung}% + \@todonotes@SetMissingFigureUp{Fehlende}% + \@todonotes@SetMissingFigureDown{Abbildung}% +} +\DeclareOptionX{portuguese}{ + \@todonotes@SetTodoListName{Lista de tarefas pendentes}% + \@todonotes@SetMissingFigureText{Figura}% + \@todonotes@SetMissingFigureUp{Figura}% + \@todonotes@SetMissingFigureDown{pendente}% +} +\DeclareOptionX{spanish}{ + \@todonotes@SetTodoListName{Lista de tareas pendientes}% + \@todonotes@SetMissingFigureText{Figura}% + \@todonotes@SetMissingFigureUp{Figura}% + \@todonotes@SetMissingFigureDown{pendiente}% +} +\DeclareOptionX{swedish}{% + \@todonotes@SetTodoListName{Att g\"{o}ra-lista}% + \@todonotes@SetMissingFigureText{Figur}% + \@todonotes@SetMissingFigureUp{Figur}% + \@todonotes@SetMissingFigureDown{saknas}% +} +\newcounter{@todonotes@numberoftodonotes} +\newcounter{@todonotes@numberofLinesInArea} +\newif{\if@todonotes@obeyDraft} +\DeclareOptionX{obeyDraft}{\@todonotes@obeyDrafttrue} +\newif{\if@todonotes@isDraft} +\DeclareOptionX{draft}{\@todonotes@isDrafttrue} +\DeclareOptionX{draftcls}{\@todonotes@isDrafttrue} +\DeclareOptionX{draftclsnofoot}{\@todonotes@isDrafttrue} +\newif{\if@todonotes@obeyFinal} +\DeclareOptionX{obeyFinal}{\@todonotes@obeyFinaltrue} +\newif{\if@todonotes@isFinal} +\DeclareOptionX{final}{\@todonotes@isFinaltrue} +\newif{\if@todonotes@disabled} +\DeclareOptionX{disable}{\@todonotes@disabledtrue} +\newif{\if@todonotes@colorinlistoftodos} +\DeclareOptionX{colorinlistoftodos}{\@todonotes@colorinlistoftodostrue} +\DeclareOptionX{dvistyle}{\PackageWarningNoLine{luatodonotes} + {Parameter dvistyle is not supported by luatodonotes. + Ignoring this option}} +\define@key{luatodonotes.sty}% + {color}{ + \renewcommand{\@todonotes@backgroundcolor}{#1} + \renewcommand{\@todonotes@linecolor}{#1}} +\define@key{luatodonotes.sty}% + {backgroundcolor}{\renewcommand{\@todonotes@backgroundcolor}{#1}} +\define@key{luatodonotes.sty}% + {linecolor}{\renewcommand{\@todonotes@linecolor}{#1}} +\define@key{luatodonotes.sty}% + {bordercolor}{\renewcommand{\@todonotes@bordercolor}{#1}} +\define@key{luatodonotes.sty}% + {leaderwidth}{\renewcommand{\@todonotes@leaderwidth}{#1}} +\newif{\if@todonotes@prependcaptionglobal} +\@todonotes@prependcaptionglobalfalse +\DeclareOptionX{prependcaption}{\@todonotes@prependcaptionglobaltrue} +\define@key{luatodonotes.sty}% + {textwidth}{\PackageWarningNoLine{luatodonotes} + {Parameter textwidth is not supported by luatodonotes}} +\define@key{luatodonotes.sty}% + {textsize}{\renewcommand{\@todonotes@textsize}{\csname #1\endcsname}} +\newif{\if@todonotes@shadowenabled} +\@todonotes@shadowenabledfalse +\DeclareOptionX{shadow}{\@todonotes@shadowenabledtrue +\usetikzlibrary{shadows}} +\define@key{luatodonotes.sty}% + {figwidth}{\renewcommand{\@todonotes@figwidth}{#1}} +\define@key{luatodonotes.sty}% + {figheight}{\renewcommand{\@todonotes@figheight}{#1}} +\define@key{luatodonotes.sty}% + {positioning}{\renewcommand{\@todonotes@positioning}{#1}} +\define@key{luatodonotes.sty}% + {splitting}{\renewcommand{\@todonotes@splitting}{#1}} +\define@key{luatodonotes.sty}% + {leadertype}{\renewcommand{\@todonotes@leadertype}{#1}} +\define@key{luatodonotes.sty}% + {interNoteSpace}{\renewcommand{\@todonotes@interNoteSpace}{#1}} +\define@key{luatodonotes.sty}% + {noteInnerSep}{\renewcommand{\@todonotes@noteInnerSep}{#1}} +\define@key{luatodonotes.sty}% + {routingAreaWidth}{\renewcommand{\@todonotes@routingAreaWidth}{#1}} +\define@key{luatodonotes.sty}% + {minNoteWidth}{\renewcommand{\@todonotes@minNoteWidth}{#1}} +\define@key{luatodonotes.sty}% + {distanceNotesPageBorder}% + {\renewcommand{\@todonotes@distanceNotesPageBorder}{#1}} +\define@key{luatodonotes.sty}% + {distanceNotesText}{\renewcommand{\@todonotes@distanceNotesText}{#1}} +\define@key{luatodonotes.sty}% + {rasterHeight}{\renewcommand{\@todonotes@rasterHeight}{#1}} +\newif{\if@todonotes@debugenabled} +\@todonotes@debugenabledfalse +\DeclareOptionX{debug}{\@todonotes@debugenabledtrue} +\ProcessOptionsX* +\if@todonotes@disabled +\else + \if@todonotes@obeyDraft + \@todonotes@disabledtrue + \if@todonotes@isDraft + \@todonotes@disabledfalse + \fi + \fi + \if@todonotes@obeyFinal + \@todonotes@disabledfalse + \if@todonotes@isFinal + \@todonotes@disabledtrue + \fi + \fi +\fi +\newdimen\@todonotes@extractx +\newdimen\@todonotes@extracty +\newsavebox\@todonotes@heightcalcbox +\newdimen\@todonotes@heightcalcboxdepth +\newdimen\@todonotes@heightcalcboxheight +\newsavebox\@todonotes@notetextbox +\newdimen\@todonotes@baselineskip +\newdimen\@todonotes@fontsize +\newdimen\@todonotes@currentsidemargin +\directlua{require("luatodonotes")} +\directlua{noteInnerSep = + string.todimen("\luatexluaescapestring{\@todonotes@noteInnerSep}")} +\directlua{noteInterSpace = + string.todimen("\luatexluaescapestring{\@todonotes@interNoteSpace}")} +\directlua{routingAreaWidth = + string.todimen("\luatexluaescapestring{\@todonotes@routingAreaWidth}")} +\directlua{minNoteWidth = + string.todimen("\luatexluaescapestring{\@todonotes@minNoteWidth}")} +\directlua{distanceNotesPageBorder = + string.todimen("\luatexluaescapestring{\@todonotes@distanceNotesPageBorder}")} +\directlua{distanceNotesText = + string.todimen("\luatexluaescapestring{\@todonotes@distanceNotesText}")} +\directlua{rasterHeight = + string.todimen("\luatexluaescapestring{\@todonotes@rasterHeight}")} +\IfStrEqCase{\@todonotes@positioning}{% + {inText}{\directlua{positioning = positioningAlgos["inText"]}}% + {inputOrder}{\directlua{positioning = positioningAlgos["inputOrder"]}}% + {inputOrderStacks}{\directlua{positioning = + positioningAlgos["inputOrderStacks"]}}% + {sLeaderNorthEast}{\directlua{positioning = + positioningAlgos["sLeaderNorthEast"]}}% + {sLeaderNorthEastBelow}{\directlua{positioning = + positioningAlgos["sLeaderNorthEastBelow"]}}% + {sLeaderEast}{\directlua{positioning = + positioningAlgos["sLeaderEast"]}}% + {poLeaders}{\directlua{positioning = positioningAlgos["poLeaders"]}}% + {poLeadersAvoidLines}{\directlua{positioning = + positioningAlgos["poLeadersAvoidLines"]}}% + {sLeaderNorthEastBelowStacks}{\directlua{positioning = + positioningAlgos["sLeaderNorthEastBelowStacks"]}}}% + [\directlua{positioning = positioningAlgos["inputOrderStacks"]} + \PackageWarningNoLine{luatodonotes} + {Invalid value for parameter positioning: \@todonotes@positioning}] +\IfStrEqCase{\@todonotes@splitting}{% + {none}{\directlua{splitting = splittingAlgos["none"]}}% + {middle}{\directlua{splitting = splittingAlgos["middle"]}}% + {median}{\directlua{splitting = splittingAlgos["median"]}}% + {weightedMedian}{\directlua{splitting = splittingAlgos["weightedMedian"]}}}% + [\directlua{splitting = splittingAlgos["none"]} + \PackageWarningNoLine{luatodonotes} + {Invalid value for parameter split: \@todonotes@splitting}] +\IfStrEqCase{\@todonotes@leadertype}{% + {s}{\directlua{leaderType = leaderTypes["s"]}}% + {opo}{\directlua{leaderType = leaderTypes["opo"]}}% + {po}{\directlua{leaderType = leaderTypes["po"]}}% + {sBezier}{\directlua{leaderType = leaderTypes["sBezier"]}}% + {os}{\directlua{leaderType = leaderTypes["os"]}}}% + [\directlua{leaderType = leaderTypes["opo"]} + \PackageWarningNoLine{luatodonotes} + {Invalid value for parameter leadertype: \@todonotes@leadertype}] +\def\@todonotes@pdflastypos{\the\pdflastypos} +\newcommand{\@todonotes@lineposition}[3]{% + \directlua{linePositionsAddLine(#1,#2,#3)}% +} +\newcommand{\@todonotes@nextpage}{% + \directlua{linePositionsNextPage()}% +}% +\newcommand{\@todonotes@writeNextpageToLpo}{% + \ifdefined\tf@lpo% + \immediate\write\tf@lpo{\@backslashchar @todonotes@nextpage}% + \fi +} +\if@todonotes@debugenabled + \directlua{todonotesDebug = true} + \newcommand{\@todonotes@AtBeginShipoutUpperLeft} + {\AtBeginShipoutUpperLeftForeground} +\else + \directlua{todonotesDebug = false} + \newcommand{\@todonotes@AtBeginShipoutUpperLeft} + {\AtBeginShipoutUpperLeft} +\fi +\directlua{initTodonotes()} +\soulregister{\ }{0} +\newlength{\todonotes@textmark@width} +\newlength{\todonotes@textmark@fontsize} +\newlength{\todonotes@textmark@linebelow} +\newlength{\todonotes@textmark@lineabove} +\ulposdef{\todonotes@textmark@highlight}{% + \setlength\todonotes@textmark@width\ulwidth% + \setlength\todonotes@textmark@fontsize{\f@size pt}% + \stepcounter{@todonotes@numberofLinesInArea}% + \ifulstarttype{0}% + {% begin of area + \def\todonotes@textmark@decoLeft{}% + \def\todonotes@textmark@shift{-2pt}% + \addtolength\todonotes@textmark@width{2pt}% + \setcounter{@todonotes@numberofLinesInArea}{1}}% + {\def\todonotes@textmark@decoLeft{@todonotes@todoarea}% + \def\todonotes@textmark@shift{-4pt}% + \addtolength\todonotes@textmark@width{4pt}}% + \ifulendtype{0}% + {% last line of area + \def\todonotes@textmark@decoRight{}% + \addtolength\todonotes@textmark@width{2pt}% + \directlua{processLastLineInTodoArea()}}% + {\def\todonotes@textmark@decoRight{@todonotes@todoarea}% + \addtolength\todonotes@textmark@width{4pt}}% + \newcommand{\@todonotes@nodeNamePrefix}% + {@todonotes@\arabic{@todonotes@numberoftodonotes}% + @\arabic{@todonotes@numberofLinesInArea} }% + \hspace*{\todonotes@textmark@shift}{\smash{% + \begin{tikzpicture}[overlay,remember picture, + deco/.style={}]% + \setlength\todonotes@textmark@linebelow% + {-0.95\dimexpr\baselineskip-\f@size pt\relax}% + \setlength\todonotes@textmark@lineabove% + {\dimexpr\f@size pt+\todonotes@textmark@linebelow\relax}% + \coordinate + (\@todonotes@nodeNamePrefix areaSW) + at (0,\todonotes@textmark@linebelow); + \coordinate + (\@todonotes@nodeNamePrefix areaSE) + at (\todonotes@textmark@width, \todonotes@textmark@linebelow); + \coordinate + (\@todonotes@nodeNamePrefix areaNE) + at (\todonotes@textmark@width,\todonotes@textmark@lineabove); + \coordinate + (\@todonotes@nodeNamePrefix areaNW) + at (0,\todonotes@textmark@lineabove); + \draw[draw=green!70,fill=green,fill opacity=.2] + (\@todonotes@nodeNamePrefix areaSW) + decorate[\todonotes@textmark@decoLeft] { + -- (\@todonotes@nodeNamePrefix areaNW) + } + -- (\@todonotes@nodeNamePrefix areaNE) + decorate[\todonotes@textmark@decoRight] { + -- (\@todonotes@nodeNamePrefix areaSE) + } + -- cycle; + \end{tikzpicture}% + }}% +}% +\newcommand{\@todonotes@currentlinecolor}{}% +\newcommand{\@todonotes@currentbackgroundcolor}{}% +\newcommand{\@todonotes@currentbordercolor}{}% +\define@key{todonotes}{color}{% + \renewcommand{\@todonotes@currentlinecolor}{#1}% + \renewcommand{\@todonotes@currentbackgroundcolor}{#1}}% +\define@key{todonotes}{linecolor}{% + \renewcommand{\@todonotes@currentlinecolor}{#1}}% +\define@key{todonotes}{backgroundcolor}{% + \renewcommand{\@todonotes@currentbackgroundcolor}{#1}}% +\define@key{todonotes}{bordercolor}{% + \renewcommand{\@todonotes@currentbordercolor}{#1}}% +\newcommand{\@todonotes@currentleaderwidth}{}% +\define@key{todonotes}{leaderwidth}{% + \renewcommand{\@todonotes@currentleaderwidth}{#1}}% +\newcommand{\@todonotes@sizecommand}{}% +\define@key{todonotes}{size}{\renewcommand{\@todonotes@sizecommand}{#1}}% +\newif\if@todonotes@localdisable% +\define@key{todonotes}{disable}[]{\@todonotes@localdisabletrue}% +\define@key{todonotes}{nodisable}[]{\@todonotes@localdisablefalse}% +\newif\if@todonotes@appendtolistoftodos% +\define@key{todonotes}{list}[]{\@todonotes@appendtolistoftodostrue}% +\define@key{todonotes}{nolist}[]{\@todonotes@appendtolistoftodosfalse}% +\newif\if@todonotes@inlinenote% +\define@key{todonotes}{inline}[]{\@todonotes@inlinenotetrue}% +\define@key{todonotes}{noinline}[]{\@todonotes@inlinenotefalse}% +\newif\if@todonotes@prependcaption% +\define@key{todonotes}{prepend}[]{\@todonotes@prependcaptiontrue}% +\define@key{todonotes}{noprepend}[]{\@todonotes@prependcaptionfalse}% +\newif\if@todonotes@line% +\define@key{todonotes}{line}[]{\@todonotes@linetrue}% +\define@key{todonotes}{noline}[]{\@todonotes@linefalse}% +\define@key{todonotes}{fancyline}[]{\PackageWarningNoLine{luatodonotes} + {Parameter fancyline is not supported by luatodonotes}}% +\define@key{todonotes}{nofancyline}[]{}% +\newcommand{\@todonotes@author}{}% +\newif\if@todonotes@authorgiven% +\define@key{todonotes}{author}{% + \renewcommand{\@todonotes@author}{#1}% + \@todonotes@authorgiventrue}% +\define@key{todonotes}{noauthor}[]{\@todonotes@authorgivenfalse}% +\newcommand{\@todonotes@caption}{}% +\newif\if@todonotes@captiongiven% +\define@key{todonotes}{caption}% + {\renewcommand{\@todonotes@caption}{#1}% + \@todonotes@captiongiventrue}% +\define@key{todonotes}{nocaption}[]{\@todonotes@captiongivenfalse}% +\newcommand{\@todonotes@currentfigwidth}{\@todonotes@figwidth} +\define@key{todonotes}% + {figwidth}{\renewcommand{\@todonotes@currentfigwidth}{#1}} +\newcommand{\@todonotes@currentfigheight}{\@todonotes@figheight} +\define@key{todonotes}% + {figheight}{\renewcommand{\@todonotes@currentfigheight}{#1}} +\presetkeys% + {todonotes}% + {linecolor=\@todonotes@linecolor,% + backgroundcolor=\@todonotes@backgroundcolor,% + bordercolor=\@todonotes@bordercolor,% + leaderwidth=\@todonotes@leaderwidth,% + nodisable,% + noinline,% + nocaption,% + noauthor,% + figwidth=\@todonotes@figwidth,% + figheight=\@todonotes@figheight,% + line, list, size=\@todonotes@textsize}{}% +\newif\if@todonotes@areaselected% +\newtoks\@todonotes@toks@currentlinecolor% +\newtoks\@todonotes@toks@currentbackgroundcolor% +\newtoks\@todonotes@toks@currentbordercolor% +\newtoks\@todonotes@toks@currentleaderwidth% +\newtoks\@todonotes@toks@sizecommand% +\if@todonotes@disabled% + \newcommand{\listoftodos}[1][]{} + \newcommand{\@todo}[2][]{} + \newcommand{\@todoarea}[3][]{} + \newcommand{\missingfigure}[2][]{} +\else % \if@todonotes@disabled +\newcommand{\listoftodos}[1][\@todonotes@todolistname] + {\@ifundefined{chapter}{\section*{#1}}{\chapter*{#1}} \@starttoc{tdo}} +\newcommand{\l@todo} + {\@dottedtocline{1}{0em}{2.3em}} +\tikzset{@todonotes@todoarea/.style={ + decoration={snake,amplitude=3.5pt,segment length=5pt}}} +\tikzset{@todonotes@notestyleraw/.style={ + line width=0.5pt, + inner sep = \@todonotes@noteInnerSep, + rounded corners=4pt}} +\if@todonotes@shadowenabled + \tikzset{@todonotes@notestyle/.style={@todonotes@notestyleraw, + general shadow={shadow xshift=.5ex, shadow yshift=-.5ex, + opacity=1,fill=black!50}}} +\else + \tikzset{@todonotes@notestyle/.style={@todonotes@notestyleraw}} +\fi +\tikzset{@todonotes@leader/.style={}} +\tikzset{@todonotes@textmark/.style={rounded corners}} +\tikzset{@todonotes@inlinenote/.style={ + @todonotes@notestyle, + draw=\@todonotes@currentbordercolor, + fill=\@todonotes@currentbackgroundcolor, + text width=\linewidth - 1.6 ex - 1 pt}} +\newcommand{\@todocommon}[2]{% +\if@todonotes@prependcaptionglobal% +\@todonotes@prependcaptiontrue% +\else% +\@todonotes@prependcaptionfalse% +\fi% +\renewcommand{\@todonotes@text}{#2}% +\renewcommand{\@todonotes@caption}{#2}% +\setkeys{todonotes}{#1}% +\if@todonotes@localdisable% +\else% +\addtocounter{@todonotes@numberoftodonotes}{1}% +\if@todonotes@appendtolistoftodos% + \phantomsection% + \if@todonotes@captiongiven% + \else% + \renewcommand{\@todonotes@caption}{#2}% + \fi% + \@todonotes@addElementToListOfTodos% +\fi% +\if@todonotes@captiongiven% + \if@todonotes@prependcaption% + \renewcommand{\@todonotes@text}{\@todonotes@caption: #2}% + \fi% +\fi% +\if@todonotes@inlinenote% + \@todonotes@drawInlineNote% +\else% + \@todonotes@drawMarginNoteWithLine% +\fi%\if@todonotes@inlinenote +\fi%\if@todonotes@localdisable +}% +\newcommand{\@todo}[2][]{% + \@todonotes@areaselectedfalse% + \@todocommon{#1}{#2}% +}% +\newcommand{\@todoarea}[3][]{% + \@todonotes@areaselectedtrue% + \@todocommon{#1}{#2}% + \todonotes@textmark@highlight{#3}% + \begin{tikzpicture}[remember picture, overlay]% + \node [coordinate] (@todonotes@\arabic{@todonotes@numberoftodonotes} % + inTextEnd) {};% + \end{tikzpicture}% + \zref@label{@todonotes@\arabic{@todonotes@numberoftodonotes}@end}% +}% +\newcommand{\@todonotes@drawMarginNoteWithLine}{% + \begin{tikzpicture}[remember picture, overlay]% + \node [coordinate] (@todonotes@\arabic{@todonotes@numberoftodonotes} % + inText) {};% + \end{tikzpicture}% + \@todonotes@baselineskip=\baselineskip% + \@todonotes@fontsize=\f@size pt% + \zref@label{@todonotes@\arabic{@todonotes@numberoftodonotes}}% + \if@todonotes@authorgiven% + \let\@todonotes@text@old=\@todonotes@text + \renewcommand{\@todonotes@text}{\@todonotes@author: \@todonotes@text@old}% + \fi% + \edef\@todonotes@tmp{\@todonotes@currentlinecolor}% + \@todonotes@toks@currentlinecolor=\expandafter{\@todonotes@tmp}% + \edef\@todonotes@tmp{\@todonotes@currentbackgroundcolor}% + \@todonotes@toks@currentbackgroundcolor=\expandafter{\@todonotes@tmp}% + \edef\@todonotes@tmp{\@todonotes@currentbordercolor}% + \@todonotes@toks@currentbordercolor=\expandafter{\@todonotes@tmp}% + \edef\@todonotes@tmp{\@todonotes@currentleaderwidth}% + \@todonotes@toks@currentleaderwidth=\expandafter{\@todonotes@tmp}% + \@todonotes@toks@sizecommand=\expandafter{\@todonotes@sizecommand}% + \savebox\@todonotes@notetextbox{\@todonotes@sizecommand\@todonotes@text}% + \if@todonotes@line% + \def\@todonotes@param@drawLeader{true}% + \else% + \def\@todonotes@param@drawLeader{false}% + \fi% + \if@todonotes@areaselected% + \def\@todonotes@param@noteType{area}% + \else% + \def\@todonotes@param@noteType{}% + \fi% + \directlua{addNoteToList(\arabic{@todonotes@numberoftodonotes},% + \@todonotes@param@drawLeader,\luastringO{\@todonotes@param@noteType})}% +}% +\newcommand{\@todonotes@addElementToListOfTodos}{% + \if@todonotes@colorinlistoftodos% + \addcontentsline{tdo}{todo}{% + \fcolorbox{\@todonotes@currentbordercolor}% + {\@todonotes@currentbackgroundcolor}% + {\textcolor{\@todonotes@currentbackgroundcolor}{o}}% + \ \@todonotes@caption}% + \else% + \addcontentsline{tdo}{todo}{\@todonotes@caption}% + \fi}% +\newcommand{\@todonotes@drawInlineNote}{% + {\par\noindent\begin{tikzpicture}[remember picture]% + \draw node[@todonotes@inlinenote,font=\@todonotes@sizecommand]{% + \if@todonotes@authorgiven% + {\noindent \@todonotes@sizecommand % + \@todonotes@author:\,\@todonotes@text}% + \else% + {\noindent \@todonotes@sizecommand \@todonotes@text}% + \fi};% + \end{tikzpicture}\par}% + }% +\newcommand{\missingfigure}[2][]{% +\setkeys{todonotes}{#1}% +\addcontentsline{tdo}{todo}{\@todonotes@MissingFigureText: #2}% +\par +\noindent +\begin{tikzpicture} +\draw[fill=black!40, draw = white, line width=0pt] + (-2, -2.5) rectangle +(\@todonotes@currentfigwidth, \@todonotes@currentfigheight); +\draw (2, -0.3) node[right, text + width=\@todonotes@currentfigwidth-4.5cm] {#2}; +\draw[red, fill=white, rounded corners = 5pt, line width=10pt] + (30:2cm) -- (150:2cm) -- (270:2cm) -- cycle; +\draw (0, 0.3) node {\@todonotes@MissingFigureUp}; +\draw (0, -0.3) node {\@todonotes@MissingFigureDown}; +\end{tikzpicture} +}% Ending \missingfigure command +\fi % Ending \@todonotes@ifdisabled +\newcommand{\todototoc} +{ + \if@todonotes@disabled + \else + \addcontentsline{toc}{\@ifundefined{chapter}{section}{chapter}}{\@todonotes@todolistname} + \fi +} +\newcommand{\todo}[2][]{\@bsphack\@todo[#1]{#2}\@esphack\ignorespaces}% +\newcommand{\todoarea}[3][]{\@bsphack\@todoarea[#1]{#2}{#3}\@esphack}% +\if@todonotes@disabled +\else +\AtBeginShipout{% + \@todonotes@AtBeginShipoutUpperLeft{ + \@todonotes@writeNextpageToLpo + \checkoddpage% + \ifoddpageoroneside% + \@todonotes@currentsidemargin=\the\oddsidemargin% + \directlua{currentPageOdd = true}% + \else% + \@todonotes@currentsidemargin=\the\evensidemargin% + \directlua{currentPageOdd = false}% + \fi\relax% + \BeginCatcodeRegime\CatcodeTableLaTeX + \directlua{calcLabelAreaDimensions()}% + \directlua{calcHeightsForNotes()}% has to be outside of tikzpicture + \begin{tikzpicture}[remember picture,overlay] + \directlua{getInputCoordinatesForNotes()} + \directlua{printNotes()} + \end{tikzpicture}% + \directlua{clearNotes()}% + \EndCatcodeRegime + }% +} +\fi % Ending \@todonotes@ifdisabled +\endinput +%% +%% End of file `luatodonotes.sty'. diff --git a/Master/texmf-dist/tex/lualatex/luatodonotes/path_line.lua b/Master/texmf-dist/tex/lualatex/luatodonotes/path_line.lua new file mode 100644 index 00000000000..e365a36bf48 --- /dev/null +++ b/Master/texmf-dist/tex/lualatex/luatodonotes/path_line.lua @@ -0,0 +1,120 @@ +-- Taken from luapower.com (Public domain) +-- +--math for 2D line segments defined as (x1, y1, x2, y2). + +local abs, min, max = math.abs, math.min, math.max + +local distance = require'path_point'.distance +local distance2 = require'path_point'.distance2 + +--evaluate a line at time t using linear interpolation. +--the time between 0..1 covers the segment interval. +local function point(t, x1, y1, x2, y2) + return x1 + t * (x2 - x1), y1 + t * (y2 - y1) +end + +--length of line at time t. +local function length(t, x1, y1, x2, y2) + return t * distance(x1, y1, x2, y2) +end + +--bounding box of line in (x,y,w,h) form. +local function bounding_box(x1, y1, x2, y2) + if x1 > x2 then x1, x2 = x2, x1 end + if y1 > y2 then y1, y2 = y2, y1 end + return x1, y1, x2-x1, y2-y1 +end + +--split line segment into two line segments at time t (t is capped between 0..1). +local function split(t, x1, y1, x3, y3) + t = min(max(t,0),1) + local x2, y2 = point(t, x1, y1, x3, y3) + return + x1, y1, x2, y2, --first segment + x2, y2, x3, y3 --second segment +end + +--intersect infinite line with its perpendicular from point (x, y); return the intersection point. +local function point_line_intersection(x, y, x1, y1, x2, y2) + local dx = x2 - x1 + local dy = y2 - y1 + local k = dx^2 + dy^2 + if k == 0 then return x1, y1 end --line has no length + local k = ((x - x1) * dy - (y - y1) * dx) / k + return x - k * dy, y + k * dx +end + +--return shortest distance-squared from point (x0, y0) to line, plus the touch point, and the time in the line +--where the touch point splits the line. +local function hit(x0, y0, x1, y1, x2, y2) + local x, y = point_line_intersection(x0, y0, x1, y1, x2, y2) + local tx = x2 == x1 and 0 or (x - x1) / (x2 - x1) + local ty = y2 == y1 and 0 or (y - y1) / (y2 - y1) + if tx < 0 or ty < 0 then --intersection occurs outside the segment, closer to the first endpoint + return distance2(x0, y0, x1, y1), x1, y1, 0 + elseif tx > 1 or ty > 1 then --intersection occurs outside the segment, closer to the second endpoint + return distance2(x0, y0, x2, y2), x2, y2, 1 + end + return distance2(x0, y0, x, y), x, y, max(tx, ty) +end + +--intersect line segment (x1, y1, x2, y2) with line segment (x3, y3, x4, y4). +--returns the time on the first line and the time on the second line where intersection occurs. +--if the intersection occurs outside the segments themselves, then t1 and t2 are outside the 0..1 range. +--if the lines are parallel or coincidental then t1 and t2 are infinite. +local function line_line_intersection(x1, y1, x2, y2, x3, y3, x4, y4) + local d = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1) + if d == 0 then return 1/0, 1/0 end --lines are parallel or coincidental + return + ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / d, + ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / d +end + +--transform to a quad bezier that advances linearly i.e. the point on the line at t +--best matches the point on the curve at t. +local function to_bezier2(x1, y1, x2, y2) + return + x1, y1, + (x1 + x2) / 2, + (y1 + y2) / 2, + x2, y2 +end + +--transform to a cubic bezier that advances linearly i.e. the point on the line at t +--best matches the point on the curve at t. +local function to_bezier3(x1, y1, x2, y2) + return + x1, y1, + (2 * x1 + x2) / 3, + (2 * y1 + y2) / 3, + (x1 + 2 * x2) / 3, + (y1 + 2 * y2) / 3, + x2, y2 +end + +--parallel line segment at a distance on the right side of a segment. +--use a negative distance for the left side, or reflect the returned points against their respective initial points +local function offset(d, x1, y1, x2, y2) + local dx, dy = -(y2-y1), x2-x1 --normal vector of the same length as original segment + local k = d / distance(x1, y1, x2, y2) --normal vector scale factor + return --normal vector scaled and translated to (x1,y1) and (x2,y2) + x1 + dx * k, y1 + dy * k, + x2 + dx * k, y2 + dy * k +end + +if not ... then require'path_line_demo' end + +return { + point_line_intersection = point_line_intersection, + line_line_intersection = line_line_intersection, + to_bezier2 = to_bezier2, + to_bezier3 = to_bezier3, + offset = offset, + --path API + bounding_box = bounding_box, + point = point, + length = length, + split = split, + hit = hit, +} + diff --git a/Master/texmf-dist/tex/lualatex/luatodonotes/path_point.lua b/Master/texmf-dist/tex/lualatex/luatodonotes/path_point.lua new file mode 100644 index 00000000000..1144e6de093 --- /dev/null +++ b/Master/texmf-dist/tex/lualatex/luatodonotes/path_point.lua @@ -0,0 +1,73 @@ +-- Taken from luapower.com (Public domain) + +--basic math for the cartesian plane. +--angles are expressed in degrees, not radians. + +local sqrt, abs, min, max, sin, cos, radians, degrees, atan2 = + math.sqrt, math.abs, math.min, math.max, math.sin, math.cos, math.rad, math.deg, math.atan2 + +--hypotenuse function: computes sqrt(a^2 + b^2) without underflow / overflow problems. +local function hypot(a, b) + if a == 0 and b == 0 then return 0 end + a, b = abs(a), abs(b) + a, b = max(a,b), min(a,b) + return a * sqrt(1 + (b / a)^2) +end + +--distance between two points. avoids underflow and overflow. +local function distance(x1, y1, x2, y2) + return hypot(x2-x1, y2-y1) +end + +--distance between two points squared. +local function distance2(x1, y1, x2, y2) + return (x2-x1)^2 + (y2-y1)^2 +end + +--point at a specified angle on a circle. +local function point_around(cx, cy, r, angle) + angle = radians(angle) + return + cx + cos(angle) * r, + cy + sin(angle) * r +end + +--rotate point (x,y) around origin (cx,cy) by angle. +local function rotate_point(x, y, cx, cy, angle) + if angle == 0 then return x, y end + angle = radians(angle) + x, y = x-cx, y-cy + local c, s = cos(angle), sin(angle) + return cx + x*c - y*s, cy + y*c + x*s +end + +--angle between two points in -360..360 degree range. +local function point_angle(x, y, cx, cy) + return degrees(atan2(y - cy, x - cx)) +end + +--reflect point through origin (i.e. rotate point 180deg around another point). +local function reflect_point(x, y, cx, cy) + return 2 * cx - x, 2 * cy - y +end + +--reflect point through origin at a specified distance. +local function reflect_point_distance(x, y, cx, cy, length) + local d = distance(x, y, cx, cy) + if d == 0 then return cx, cy end + local scale = length / d + return + cx + (cx - x) * scale, + cy + (cy - y) * scale +end + +return { + hypot = hypot, + distance = distance, + distance2 = distance2, + point_around = point_around, + rotate_point = rotate_point, + point_angle = point_angle, + reflect_point = reflect_point, + reflect_point_distance = reflect_point_distance, +} |