如何改善頂部浮動後的間距?

如何改善頂部浮動後的間距?

情境

我正在嘗試創建一個巨集(此處稱為\myCommand),它分隔兩個段落並根據上下文添加不同的垂直空間。但我認為這是一個普遍問題,也可能發生在諸如\section{...}.

通常,我想在其開頭和結尾插入一個值空間\myLength(出於本範例的目的,我們將採用= 30 pt)。 如果巨集落在頁面頂部,則分頁符號後將被忽略,並且不會添加垂直空間,這就是我想要的。 但是,就像前一種情況一樣,當巨集在分頁符號之後落下並且前面有一個或多個頂部浮動時,就會出現問題。在這種情況下,我希望巨集插入垂直線以保持一致性。\myLength\myCommand
\vspace{\myLength}
\vspace*{\dimexpr\myLength-\textfloatsep}


例子

下面舉個例子來說明:

下面,巨集的行為正確:當它在分頁符號後落在頁面頂部時,不會在巨集的開頭插入空格;當它不在頁面頂部時,\vspace{\myLength}會正確插入到巨集的開頭。

在此輸入影像描述

但是,當巨集在分頁符號後落在頁面頂部並且前面有頂部浮動時,空間完全不平衡。

在此輸入影像描述

只是為了表明問題是普遍的而不是特定於 的\myCommand,這裡有同樣的問題\section{...}

在此輸入影像描述

這是一個非工作的最小範例:

\documentclass{article}
\usepackage{lipsum, mwe}

\newlength{\myLength}
\setlength{\myLength}{30pt}% exaggerated value to illustrate

\newcommand{\myCommand}{\par
    \vspace{\myLength}
    {\centering\LARGE * * *\par}
    \vspace{\myLength}
}

\begin{document}
\lipsum[1]

\lipsum[1]

\lipsum[1]

\lipsum[2]

\begin{figure}[t]
\centering
\includegraphics{example-image}
\end{figure}

\myCommand

\lipsum[1]

\lipsum[2]

\myCommand

\lipsum[1]

\myCommand

\lipsum[1]
\end{document}

目標

為了克服上述問題,我希望僅在以下情況下使用值為 的垂直空間\vspace*{\dimexpr\myLength-\textfloatsep}(以補償頂部浮動插入的垂直空間): 在頁面開頭(分頁符號之後)和存在頂部浮子。\textfloatsep

在所有其他情況下,使用\vspace{\myLength}效果都很好。


試煉

我的(到目前為止不成功的)方法是使用兩個條件在正確的位置插入正確的垂直空間:

  • 測試目前頁面上是否存在頂部浮動的第一個條件,
  • 第二個條件是檢查分頁符號後是否位於新頁面的頂部。

如果兩個條件都滿足,那麼我們確實位於頁面的開頭(在分頁符號之後)並且存在頂部浮動;然後我們插入\vspace*{\dimexpr\myLength-\textfloatsep}.在所有其他情況下,我們插入\vspace{\myLength}.

感謝上一個問題,我已經能夠找到一種檢測頁面中是否存在頁面頂部浮動的方法:如何從文件正文判斷頁面是否有頂部浮動?

然而,第二個條件確實給我帶來了問題,我想嘗試使用\pagetotal,但它似乎不合適:如何訪問真的\pagetotal 的值?

我的宏\myCommand看起來像這樣:

\makeatletter
\usepackage{refcount}
%From : https://tex.stackexchange.com/questions/712713/how-to-determine-from-the-document-body-whether-or-not-a-page-has-a-top-float
\def \@combinefloats {%
    \ifx \@toplist\@empty%
    \else%
        \@cflt%
        \immediate\write\@auxout{\string\global\string\@namedef{pageWithTopFloat-\thepage}{1}}%
    \fi%
    \ifx \@botlist\@empty \else \@cflb \fi%
}

\newcounter{ifTopFloatCnt}

\long\def\ifTopFloat#1#2{%
    \global\advance\c@ifTopFloatCnt\@ne%
    \label{\the\c@ifTopFloatCnt @ifTopFloat}\nopagebreak%
    \ifcsname pageWithTopFloat-\getpagerefnumber{\the\c@ifTopFloatCnt @ifTopFloat}\endcsname%
        #1%
    \else%
        #2%
    \fi%
}

\newlength{\myLength}
\setlength{\myLength}{30pt}% exaggerated value to illustrate

\newcommand{\mycommand}{\par%
    \ifTopFloat{%
        \ifdim\pagetotal=0pt% If there is a top float AND you are at the start of a new page
            \vspace*{\dimexpr\myLength-\textfloatsep}
            {\centering\LARGE * * *\par}
        \else% If there is a top float BUT you are not at the start of a new page
            \vspace{\myLength}
            {\centering\LARGE * * *\par}
        \fi
    }{% If there is no top float
        \vspace{\myLength}
        {\centering\LARGE * * *\par}
    }    
    \vspace{\myLength}
}
\makeatother

我的做法肯定不是正確的。

答案1

這是一個基於 LuaTeX 的解決方案。如果命令中的任何內容\removefromtop位於頁面頂部,則該命令中的任何內容都將被刪除,否則將被保留。我在這裡使用了一條規則來使效果更加明顯,但是您可以將其替換為\vspace最終文件的 a (或任何其他垂直模式材料)。

\documentclass{article}

\usepackage{luacode}
\begin{luacode*}
    local attr = luatexbase.new_attribute("removefromtop")
    token.set_char("removefromtopattr", attr, "global")

    local cancel_output = false
    local last_cancelled = false

    luatexbase.add_to_callback("pre_output_filter", function (head)
        local outputpenalty = tex.outputpenalty
        if outputpenalty <= -10002 and outputpenalty >= -10005 then
            return true
        end

        if token.get_macro("@toplist") ~= "" then
            return true
        end

        for n in node.traverse(head) do
            if node.get_attribute(n, attr) == 1 then
                head = node.remove(head, n)
                cancel_output = true
            elseif n.id ~= node.id("penalty") and
                   n.id ~= node.id("glue")    and
                   n.id ~= node.id("kern")    and
                   n.id ~= node.id("mark")    and
                   n.id ~= node.id("whatsit") and
                   n.id ~= node.id("ins")
            then
                break
            end
        end

        if last_cancelled then
            last_cancelled = false
            for n in node.traverse_id(node.id("glue"), head) do
                if n.subtype == 10 then
                    n.width = -tex.baselineskip.width
                end
            end
        end

        if cancel_output then
            return head
        else
            return true
        end
    end, "remove_from_top")

    luatexbase.add_to_callback("buildpage_filter", function (info)
        if status.output_active then
            return
        end

        if not tex.output:match("%}%}") then
            tex.runtoks(function()
                tex.sprint[[\output\expandafter{\the\output}]]
            end)
        end

        tex.triggerbuildpage()

        if not (status.output_active and cancel_output) then
            return
        end

        cancel_output = false
        last_cancelled = true

        local level = 0
        repeat
            local t = token.get_next()
            if t.command == 1 then
                level = level + 1
            elseif t.command == 2 then
                level = level - 1
            end
        until level == 0

        local box = node.copy_list(tex.box[255])
        node.write(box)
        tex.setbox("global", 255, nil)
    end, "remove_from_top")
\end{luacode*}

\newcommand{\removefromtop}[1]{%
    \vbox attr \removefromtopattr=1 {%
        #1%
    }%
}

\usepackage{lipsum, mwe}

\newlength{\myLength}
\setlength{\myLength}{20pt}% exaggerated value to illustrate

\newcommand{\myCommand}{
    \removefromtop{
        \hrule height \myLength width \textwidth depth 0pt
    }
    \nobreak
    \vbox{
        {\centering\LARGE * * *\par}
        \hrule height \myLength width \textwidth depth 0pt
    }
}

\begin{document}
\lipsum[1]

\lipsum[1]

\lipsum[1]

\lipsum[2]

\begin{figure}[t]
\centering
\includegraphics{example-image}
\end{figure}

\myCommand

\lipsum[1]

\lipsum[2]

\myCommand

\lipsum[1]

\myCommand

\lipsum[1]

\lipsum[2]

\myCommand

\lipsum[1]

\myCommand

\lipsum[1-3]

\myCommand

\lipsum[1-9]

\myCommand

\lipsum[1]

\begin{figure}[t]
    \centering
    \includegraphics{example-image}
\end{figure}

\end{document}

輸出

該程式碼相當複雜,我還沒有對其進行太多測試,因此我預計更複雜的文件會出現一些錯誤。

怎麼運作的

首先,我們定義一個自訂“屬性”,我們可以用它來標記 的內容\removefromtop

接下來,我們掛鉤buildpage_filter,它在 TeX 將內容新增到「主垂直清單」之前被呼叫。這裡棘手的一點是我們稱之為tex.triggerbuildpage() 裡面這個回調會觸發 TeX 刷新其“最近的貢獻”並嘗試分頁。

如果觸發分頁符,則控制項跳到pre_output_filter.在這裡,我們添加了另一個回調,它首先檢查這是否是「真正的」分頁符號(而不是 LaTeX 用於刷新浮動的假分頁符號)以及當前頁面上是否沒有頂部浮動。如果是這種情況,我們將迭代頁面的內容並刪除任何標有自訂屬性的初始內容。如果我們標記了任何內容,那麼我們會將其發送回給我們buildpage_filter並返回新的頁面內容。

完成後pre_output_filter,控制將在 後恢復tex.triggerbuildpage(),但現在我們處於輸出例程內。如果最近pre_output_filter成功刪除了一些內容並且我們位於第一個輸出例程中,那麼我們將刪除 TeX 輸入堆疊上的所有標記。由於我們位於輸出例程內部,因此這會清空\output令牌清單的目前擴充。然後,我們清除\box255(當前頁面的內容),將其內容移回最近的貢獻列表,然後返回。

為什麼要這樣做呢?

在觸發輸出例程之前,我們無法知道該框是否\removefromtop位於頁面頂部。但是,當我們刪除\removefromtop框時,我們會縮短頁面,因此如果我們只是掛鉤來pre_output_filter刪除框,那麼所有頁面都會變短。

相反,我們讓輸出例程正常觸發,然後如果我們刪除任何框\removefromtop,我們會將當前頁面的內容推回最近的貢獻列表,取消輸出例程,並讓 TeX 正常進行以向頁面添加更多內容。底部。

答案2

我認為問題在於浮動後添加的垂直空間。因此,在我下面的建議中,我修改了 using\myLength並添加了 a \baselineskip

\documentclass{article}
\usepackage{lipsum, mwe}

\newlength{\myLength}
\setlength{\myLength}{30pt}% exaggerated value to illustrate

% Below, I increase the separation between float and text and reduce
% the flexibility of the space
% You can see the default values by adding \the\textfloatsep inside
% the document environment
\newlength{\myFloatskip}
\setlength{\myFloatskip}{\the\myLength}
\addtolength{\myFloatskip}{\baselineskip}
\setlength{\textfloatsep}{\the\myFloatskip plus 0.0pt minus 0.0pt}

\newcommand{\myCommand}{\par%
    \vspace{\myLength}%
    {\centering\LARGE * * *\par}%
    \vspace{\myLength}%
}

\begin{document}

\lipsum[1]

\lipsum[1]

\lipsum[1]

\lipsum[2]

\begin{figure}[t]
\centering
\includegraphics{example-image}
\end{figure}

\myCommand

\lipsum[2]

\lipsum[3]

\myCommand

\lipsum[1]

\myCommand

\lipsum[1]
\end{document}

答案3

經過幾天的研究,我想我已經找到了解決問題的方法。想法如下:

在輸出例程中,該\@cflt命令在適當的情況下將頂部浮動與正文文字組合在一起。我修改了這個命令,以便在輔助文件中記錄代表文本正文開始的 Y 座標(即緊接在任何頁面頂部浮動之後)。當我運行時\myCommand,我會做同樣的事情並將表示巨集開頭的 Y 座標保存在輔助檔案中。

在下一次編譯時,我比較兩個 Y 座標。如果它們相等,那麼\myCommand就在頂部浮動之後,我可以\vspace*{\dimexpr\myLength-\textfloatsep}根據問題中的要求插入。

結果如下:

在此輸入影像描述

這比原始結果更和諧(儘管可以進行細微調整):

在此輸入影像描述


然而,在測試過程中,我發現了應用程式固有的問題。實際上,巨集插入的空間取決於巨集在頁面中的位置,而頁面的位置又取決於巨集插入的空間等。為了避免這種情況,我想您必須\myCommand根據您想要如何使用它來防止之前或之後分頁。我願意接受改進建議。

例如,在我的MWE中,如果對於我的第4段,我使用\lipsum[2]代替\lipsum[3],則會出現不穩定情況,並且文檔的編譯相繼給出了以下情況A(左)和B(右):

在此輸入影像描述在此輸入影像描述

如果我們從情況 A 開始,巨集將出現在第 2 頁正文
\myLength-\textfloatsep頂部\myCommand。 現在,垂直空間\myLength-\textfloatsep小於垂直空間,\myLength因此有足夠的空間讓命令位於第 1 頁,我們處於情況 B 中。這次,第 1 頁上沒有足夠的空間,並且被發送回第 2 頁的開頭,因此我們回到了情況 A。
\myCommand\myLength\myCommand


氣象局:

\documentclass{article}
\usepackage{lipsum, mwe, refcount, iftex}

\ifluatex% For compatibility with LuaTeX, which I generally use.
    \let\pdfsavepos\savepos
    \let\pdflastypos\lastypos
\fi

\makeatletter
\def \@cflt{%
    \let \@elt \@comflelt
    \setbox\@tempboxa \vbox{}%
    \@toplist
    \setbox\@outputbox \vbox{%
        \boxmaxdepth \maxdepth
        \unvbox\@tempboxa
        \vskip -\floatsep
        \topfigrule
        \vskip \textfloatsep
        \pdfsavepos% <--- Added
        \write\@auxout{\string\global\string\@namedef{@pageWithTopFloatYPos-\thepage}{\the\pdflastypos}}% <--- Added. We save the Y position of the top of "\@outputbox", i. e. the body of the text.
        \unvbox\@outputbox
    }%
    \let\@elt\relax
    \xdef\@freelist{\@freelist\@toplist}%
    \global\let\@toplist\@empty
}

\newcounter{@ifTopFloatCnt}

\long\def\ifTopFloat#1#2{% A conditional to see if there is a top float on the current page. See https://tex.stackexchange.com/questions/712713/how-to-determine-from-the-document-body-whether-or-not-a-page-has-a-top-float.
    \stepcounter{@ifTopFloatCnt}
    \label{@ifTopFloat-\the@ifTopFloatCnt}\nopagebreak%
    \ifcsname @pageWithTopFloatYPos-\getpagerefnumber{@ifTopFloat-\the@ifTopFloatCnt}\endcsname%
        #1%
    \else%
        #2%
    \fi%
}

\newlength{\myLength}
\setlength{\myLength}{30pt}% exaggerated value to illustrate

\newcounter{@myCommandCnt}
\newcounter{@myCommandCntAux}

\newcommand{\myCommand}{\par%
    \stepcounter{@myCommandCnt}%
    \pdfsavepos%
    \write\@auxout{%
        \string\stepcounter{@myCommandCntAux}%
        ^^J% New line in the auxiliary file
        \string\global\string\@namedef{@myCommandYPos-\string\the@myCommandCntAux}{\the\pdflastypos}% Save the Y coordinate.
    }%
    \ifTopFloat{%
        \ifnum \@nameuse{@myCommandYPos-\the@myCommandCnt} = 
            \csname @pageWithTopFloatYPos-\getpagerefnumber{@ifTopFloat-\the@ifTopFloatCnt}\endcsname% If there's a top float AND you're right after it
            \vspace*{\dimexpr\myLength-\textfloatsep}
            {\centering\LARGE * * * (a)\par}
        \else% If there is a top float BUT you are not right after it
            \vspace{\myLength}
            {\centering\LARGE * * * (b)\par}
        \fi
    }{% If there is no top float
        \vspace{\myLength}
        {\centering\LARGE * * * (c)\par}
    }    
    \vspace{\myLength}
}
\makeatother

\begin{document}
\lipsum[1]

\lipsum[1]

\lipsum[1]

\lipsum[3]% <--- If the current line is uncommented and the next line is commented, stability is achieved.
%\lipsum[2]% <--- If the previous line is commented and the current line is uncommented, instability occurs.

\begin{figure}[t]
\centering
\includegraphics{example-image}
\end{figure}

\myCommand

\lipsum[1]

\lipsum[2]

\begin{figure}[t]
\centering
\includegraphics{example-image-9x16}
\end{figure}

\myCommand

\lipsum[2]

\myCommand

\lipsum[1]

\myCommand

\lipsum[1]
\end{document}

相關內容