トップフロートの後の間隔を改善するにはどうすればよいですか?

トップフロートの後の間隔を改善するにはどうすればよいですか?

コンテクスト

2 つの段落を分離し、コンテキストに応じて異なる垂直スペースを追加するマクロ (ここでは\myCommand) を作成しようとしています。ただし、これは などのマクロでも発生する可能性がある一般的な問題だと思います\section{...}

通常、値のスペース\myLength(この例では、\myLength= 30 pt とします) をマクロの先頭\myCommandと末尾に挿入します。
マクロがページ区切りの後にページの上部にある場合は\vspace{\myLength}無視され、垂直スペースは追加されません。これが私の希望です。
ただし、前のケースのように、マクロがページ区切りの後にあって、その前に 1 つ以上の上部フロートがある場合に問題が発生します。この場合、一貫性を保つためにマクロで垂直スペースを挿入します\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}完璧に機能します。


トライアル

私の(これまでのところ成功していない)アプローチは、2 つの条件を使用して、適切な場所に適切な垂直スペースを挿入することです。

  • 現在のページにトップフロートが存在するかどうかをテストするための最初の条件文。
  • 改ページ後に新しいページの先頭にいるかどうかを確認する 2 番目の条件。

両方の条件が満たされる場合、ページの開始位置(ページ区切り後)にあり、上部フロートが存在するため、 を挿入します\vspace*{\dimexpr\myLength-\textfloatsep}。それ以外の場合は、 を挿入します\vspace{\myLength}

以前の質問のおかげで、ページ内のページ上部のフロートの存在を検出する方法を見つけることができました。ドキュメント本体から、ページにトップフロートがあるかどうかを判断するにはどうすればよいでしょうか?

しかし、2 番目の条件文は私にとっては問題を引き起こし、 を使用しようとしました\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最終ドキュメントでは (またはその他の垂直モードのマテリアル) に置き換えることができます。

\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

問題は、フロートの後に追加された垂直スペースにあると思います。そのため、以下の提案では、 を使用してそれを変更し\myLength\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 座標が補助ファイルに保存されます。

次のコンパイルでは、2 つの 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 の状態になります。
次のコンパイルでは、トップ フロートの後でなくなったため、\myCommandは に等しい垂直スペースを挿入します\myLength。今回は、ページ 1 に十分なスペースがないため、 は\myCommandページ 2 の先頭に戻されるため、ケース A の状態に戻ります。


MWE:

\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}

関連情報