Wie kann ich den Abstand nach einem Top Float verbessern?

Wie kann ich den Abstand nach einem Top Float verbessern?

Kontext

Ich versuche, ein Makro (hier genannt \myCommand) zu erstellen, das zwei Absätze trennt und je nach Kontext einen anderen vertikalen Abstand hinzufügt. Aber ich vermute, dass dies ein allgemeines Problem ist, das auch bei Makros wie auftreten kann \section{...}.

Normalerweise möchte ich am Anfang und am Ende einen Leerraum mit dem Wert \myLength(für dieses Beispiel nehmen wir = 30 pt) einfügen. Wenn das Makro am oberen Seitenrand steht, wird es nach einem Seitenumbruch ignoriert und es wird kein vertikaler Leerraum hinzugefügt, was ich auch möchte. ABER das Problem entsteht, wenn das Makro, wie im vorherigen Fall, nach einem Seitenumbruch steht und von einem oder mehreren oberen Floats eingeleitet wird. In diesem Fall möchte ich, dass das Makro einen vertikalen Leerraum einfügt, um die Konsistenz zu wahren.\myLength\myCommand
\vspace{\myLength}
\vspace*{\dimexpr\myLength-\textfloatsep}


Beispiel

Hier ist ein Beispiel zur Veranschaulichung:

Nachfolgend verhält sich das Makro korrekt: Wenn es nach einem Seitenumbruch am oberen Seitenrand steht, wird am Anfang des Makros kein Leerzeichen eingefügt, und wenn es nicht am oberen Seitenrand steht, \vspace{\myLength}wird es am Anfang des Makros korrekt eingefügt.

Bildbeschreibung hier eingeben

Wenn das Makro jedoch nach einem Seitenumbruch oben auf der Seite steht und ihm ein Top-Float vorangestellt ist, sind die Abstände völlig unausgeglichen.

Bildbeschreibung hier eingeben

Um zu zeigen, dass das Problem allgemein und nicht spezifisch ist \myCommand, hier das gleiche Problem mit \section{...}:

Bildbeschreibung hier eingeben

Hier ist ein nicht funktionierendes Minimalbeispiel:

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

Ziel

Um das oben genannte Problem zu lösen, hätte ich gerne einen vertikalen Abstand mit einem Wert von \vspace*{\dimexpr\myLength-\textfloatsep}(um den vertikalen Abstand auszugleichen \textfloatsep, der durch den oberen Float eingefügt wird) nur in folgendem Fall:
am Anfang einer Seite (nach einem Seitenumbruch) UND ein oberer Float ist vorhanden.

In allen anderen Fällen \vspace{\myLength}funktioniert die Verwendung einwandfrei.


Versuche

Mein (bisher erfolgloser) Ansatz bestand darin, zwei Bedingungssätze zu verwenden, um die richtigen vertikalen Leerzeichen an der richtigen Stelle einzufügen:

  • Eine erste Bedingung, um das Vorhandensein eines Top-Floats auf der aktuellen Seite zu testen.
  • Eine zweite Bedingung, um zu überprüfen, ob wir uns nach einem Seitenumbruch am Anfang einer neuen Seite befinden.

Wenn beide Bedingungen erfüllt sind, dann befinden wir uns tatsächlich am Anfang einer Seite (nach einem Seitenumbruch) UND es ist ein Top-Float vorhanden; dann fügen wir ein \vspace*{\dimexpr\myLength-\textfloatsep}. In allen anderen Fällen fügen wir ein \vspace{\myLength}.

Dank einer früheren Frage konnte ich eine Möglichkeit finden, das Vorhandensein eines Top-of-Page-Floats auf einer Seite zu erkennen:Wie kann man anhand des Dokumenttexts feststellen, ob eine Seite einen Top-Float hat oder nicht?.

Der zweite Konditional stellt für mich allerdings ein Problem dar und ich wollte versuchen, ihn zu verwenden \pagetotal, aber das scheint nicht geeignet:So greifen Sie auf dieWAHRWert von \pagetotal?.

Mein Makro \myCommandsah dann so aus:

\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

Mein Ansatz ist sicherlich nicht der richtige.

Antwort1

Hier ist eine LuaTeX-basierte Lösung. Alles innerhalb des Befehls \removefromtopwird entfernt, wenn es oben auf der Seite liegt, und andernfalls beibehalten. Ich verwende hier eine Regel, um den Effekt deutlicher zu machen, aber Sie können sie \vspacefür das endgültige Dokument durch ein (oder ein beliebiges anderes Material im vertikalen Modus) ersetzen.

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

Ausgabe

Der Code ist ziemlich komplex und ich habe ihn nicht sehr ausführlich getestet. Daher würde ich bei komplizierteren Dokumenten mit einigen Fehlern rechnen.

Wie es funktioniert

Zuerst definieren wir ein benutzerdefiniertes „Attribut“, mit dem wir den Inhalt von markieren können \removefromtop.

Als nächstes haken wir uns in den ein buildpage_filter, der aufgerufen wird, kurz bevor TeX Inhalte zur „Hauptvertikalliste“ hinzufügt. Der schwierige Teil dabei ist, dass wirtex.triggerbuildpage() innenDieser Rückruf veranlasst TeX, seine „letzten Beiträge“ zu löschen und einen Seitenumbruch zu versuchen.

Wenn ein Seitenumbruch ausgelöst wird, springt die Steuerung zum pre_output_filter. Hier haben wir einen weiteren Rückruf hinzugefügt, der zunächst prüft, ob es sich um einen „echten“ Seitenumbruch handelt (und nicht um einen falschen Seitenumbruch, der von LaTeX zum Leeren von Floats verwendet wird) und ob auf der aktuellen Seite keine Top-Floats vorhanden sind. Wenn dies der Fall ist, durchlaufen wir den Inhalt der Seite und entfernen alle anfänglichen Inhalte, die mit unserem benutzerdefinierten Attribut markiert sind. Und wenn wir Inhalte markiert haben, signalisieren wir dies an unseren zurück buildpage_filterund geben den neuen Seiteninhalt zurück.

Sobald der pre_output_filterabgeschlossen ist, wird die Steuerung nach dem wieder aufgenommen tex.triggerbuildpage(), außer dass wir uns jetzt in der Ausgaberoutine befinden. Wenn der letzte pre_output_filtererfolgreich Inhalte entfernt hat und wir uns in der ersten Ausgaberoutine befinden, entfernen wir alle Token auf dem Eingabestapel von TeX. Da wir uns in der Ausgaberoutine befinden, wird die aktuelle Erweiterung der \outputTokenliste geleert. Dann löschen wir \box255(den Inhalt der aktuellen Seite), verschieben ihren Inhalt zurück in die Liste der letzten Beiträge und kehren zurück.

Warum so?

Wir können nicht wissen, ob sich die \removefromtopBox oben auf der Seite befindet oder nicht, bis die Ausgaberoutine ausgelöst wird. Wenn wir die \removefromtopBoxen jedoch entfernen, verkürzen wir die Seite. Wenn wir also nur den Haken setzen würden, pre_output_filterum die Boxen zu entfernen, wären alle Seiten zu kurz.

Stattdessen lassen wir die Ausgaberoutine wie gewohnt auslösen. Wenn wir dann eines der \removefromtopKästchen entfernen, schieben wir den Inhalt der aktuellen Seite zurück in die Liste der letzten Beiträge, brechen die Ausgaberoutine ab und lassen TeX wie gewohnt fortfahren, um weiteren Inhalt am unteren Ende der Seite hinzuzufügen.

Antwort2

Ich denke, das Problem liegt an dem vertikalen Abstand, der nach dem Float hinzugefügt wird. In meinem Vorschlag unten ändere ich das also \myLengthund füge \baselineskipauch ein hinzu.

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

Antwort3

Nach mehreren Tagen der Recherche glaube ich, eine Lösung für mein Problem gefunden zu haben. Die Idee ist folgende:

In der Ausgaberoutine \@cfltfügt der Befehl, wo angemessen, die oberen Floats mit dem Haupttext zusammen. Ich habe diesen Befehl so geändert, dass in der Hilfsdatei die Y-Koordinate aufgezeichnet wird, die den Anfang des Haupttexts darstellt (also direkt nach allen Floats am oberen Seitenrand). Wenn ich ausführe \myCommand, mache ich dasselbe und speichere die Y-Koordinate, die den Anfang des Makros darstellt, in der Hilfsdatei.

Bei der nächsten Kompilierung vergleiche ich die beiden Y-Koordinaten. Wenn sie gleich sind, dann \myCommandsteht gleich nach oben Floats, und ich kann sie \vspace*{\dimexpr\myLength-\textfloatsep}wie in der Frage gewünscht einfügen.

Das Ergebnis ist wie folgt:

Bildbeschreibung hier eingeben

das harmonischer ist als das ursprüngliche Ergebnis (obwohl kleinere Anpassungen vorgenommen werden könnten):

Bildbeschreibung hier eingeben


Bei meinen Tests stieß ich jedoch auf ein inhärentes Problem der Anwendung. Tatsächlich hängt der vom Makro eingefügte Abstand von der Position des Makros auf der Seite ab, die wiederum vom vom Makro eingefügten Abstand usw. abhängt. Es besteht eine zirkuläre Abhängigkeit. Um dies zu vermeiden, müssten Sie vermutlich einen Seitenumbruch davor oder danach verhindern, \myCommandje nachdem, wie Sie ihn verwenden möchten. Ich bin offen für Verbesserungsvorschläge.

Wenn ich beispielsweise in meinem MWE für meinen vierten Absatz \lipsum[2]anstelle von verwende, \lipsum[3]tritt Instabilität auf und die Zusammenstellung der Dokumente ergibt nacheinander die folgenden Fälle A (links) und B (rechts):

Bildbeschreibung hier eingebenBildbeschreibung hier eingeben

Wenn wir mit Fall A beginnen, landet das Makro oben im Haupttext auf Seite 2.
Bei der nächsten Kompilierung fügen wir daher \myLength-\textfloatsepam Anfang von ein Leerzeichen ein, das gleich ist \myCommand. Jetzt ist der vertikale Abstand \myLength-\textfloatsepkleiner als der vertikale Abstand \myLength, sodass genügend Platz für den Befehl auf Seite 1 vorhanden ist und wir uns im Fall B befinden.
Da wir bei der nächsten Kompilierung nicht mehr nach einem Top-Float suchen, \myCommandfügt ein vertikales Leerzeichen ein, das gleich ist \myLength. Dieses Mal ist auf Seite 1 nicht genügend Platz vorhanden und es \myCommandwird an den Anfang von Seite 2 zurückgeschickt, sodass wir uns wieder im Fall A befinden.


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

verwandte Informationen